127 lines
4.7 KiB
Python
127 lines
4.7 KiB
Python
from datetime import timedelta
|
|
|
|
from django import forms
|
|
from django.utils import timezone
|
|
|
|
from .models import ActivityItem, QuoteLine, SalesWorkspace
|
|
|
|
|
|
class StyledFormMixin:
|
|
def _apply_styles(self):
|
|
for name, field in self.fields.items():
|
|
widget = field.widget
|
|
css_class = "form-select" if isinstance(widget, forms.Select) else "form-control"
|
|
existing = widget.attrs.get("class", "")
|
|
widget.attrs["class"] = f"{existing} {css_class} workspace-input".strip()
|
|
|
|
|
|
class WorkspaceIntakeForm(StyledFormMixin, forms.ModelForm):
|
|
next_meeting_at = forms.DateTimeField(
|
|
required=False,
|
|
widget=forms.DateTimeInput(attrs={"type": "datetime-local"}),
|
|
)
|
|
|
|
class Meta:
|
|
model = SalesWorkspace
|
|
fields = [
|
|
"customer_name",
|
|
"project_name",
|
|
"project_number",
|
|
"zipcode",
|
|
"address",
|
|
"city",
|
|
"contact_name",
|
|
"contact_email",
|
|
"contact_phone",
|
|
"opportunity_title",
|
|
"estimated_value",
|
|
"layout_template",
|
|
"summary",
|
|
"next_step",
|
|
"next_meeting_at",
|
|
]
|
|
widgets = {
|
|
"summary": forms.Textarea(attrs={"rows": 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._apply_styles()
|
|
placeholders = {
|
|
"customer_name": "Van der Meer Construction",
|
|
"project_name": "Renovation of showroom",
|
|
"project_number": "PRJ-24018",
|
|
"zipcode": "3011 AA",
|
|
"address": "Binnenweg 18",
|
|
"city": "Rotterdam",
|
|
"contact_name": "Eva Jansen",
|
|
"contact_email": "eva@example.com",
|
|
"contact_phone": "+31 6 12345678",
|
|
"opportunity_title": "Lighting upgrade quotation",
|
|
"estimated_value": "18500",
|
|
"summary": "Existing customer requesting quick quote with site visit.",
|
|
"next_step": "Confirm site meeting and draft first quotation.",
|
|
}
|
|
for name, placeholder in placeholders.items():
|
|
self.fields[name].widget.attrs.setdefault("placeholder", placeholder)
|
|
self.fields["layout_template"].widget.attrs.setdefault("aria-label", "Workspace template")
|
|
|
|
def clean_next_meeting_at(self):
|
|
meeting = self.cleaned_data.get("next_meeting_at")
|
|
if meeting and meeting < timezone.now():
|
|
raise forms.ValidationError("Choose a future time for the next meeting.")
|
|
return meeting
|
|
|
|
|
|
class StageUpdateForm(StyledFormMixin, forms.Form):
|
|
stage = forms.ChoiceField(choices=SalesWorkspace.Stage.choices)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
current_stage = kwargs.pop("current_stage", None)
|
|
super().__init__(*args, **kwargs)
|
|
self._apply_styles()
|
|
if current_stage:
|
|
self.fields["stage"].initial = current_stage
|
|
|
|
|
|
class QuoteLineForm(StyledFormMixin, forms.ModelForm):
|
|
class Meta:
|
|
model = QuoteLine
|
|
fields = ["product_name", "description", "quantity", "unit_price"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._apply_styles()
|
|
self.fields["product_name"].widget.attrs.setdefault("placeholder", "Product or service")
|
|
self.fields["description"].widget.attrs.setdefault("placeholder", "Optional scope note")
|
|
self.fields["quantity"].widget.attrs.setdefault("min", 1)
|
|
self.fields["unit_price"].widget.attrs.setdefault("min", 0)
|
|
self.fields["unit_price"].widget.attrs.setdefault("step", "0.01")
|
|
|
|
|
|
class ActivityForm(StyledFormMixin, forms.ModelForm):
|
|
due_at = forms.DateTimeField(
|
|
widget=forms.DateTimeInput(attrs={"type": "datetime-local"})
|
|
)
|
|
|
|
class Meta:
|
|
model = ActivityItem
|
|
fields = ["title", "activity_type", "due_at", "owner", "notes"]
|
|
widgets = {
|
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._apply_styles()
|
|
self.fields["title"].widget.attrs.setdefault("placeholder", "Schedule follow-up or task")
|
|
self.fields["owner"].widget.attrs.setdefault("placeholder", "Rep or team owner")
|
|
self.fields["notes"].widget.attrs.setdefault("placeholder", "Talking points, reminders, dependencies")
|
|
self.fields["due_at"].initial = (timezone.now() + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M")
|
|
|
|
def clean_due_at(self):
|
|
due_at = self.cleaned_data["due_at"]
|
|
if due_at < timezone.now() - timedelta(minutes=5):
|
|
raise forms.ValidationError("Activity time should be now or in the future.")
|
|
return due_at
|