""" Complaints forms """ import os from django import forms from django.db import models from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.utils.translation import gettext_lazy as _ from apps.complaints.models import ( Complaint, ComplaintCategory, ComplaintSource, ComplaintSourceType, ComplaintStatus, ComplaintType, Inquiry, ComplaintSLAConfig, ComplaintThreshold, ComplaintInvolvedDepartment, ComplaintInvolvedStaff, GovernmentTicket, ) from apps.core.models import PriorityChoices, SeverityChoices from apps.core.form_mixins import HospitalFieldMixin, DepartmentFieldMixin from apps.core.validators import SAUDI_PHONE_HTML_PATTERN, validate_saudi_phone from apps.organizations.models import Area, Department, Hospital, LocationType, Patient, Staff, Section class MultiFileInput(forms.FileInput): """ Custom FileInput widget that supports multiple file uploads. Unlike standard FileInput which only supports single files, this widget allows users to upload multiple files at once. """ def __init__(self, attrs=None): # Call parent's __init__ first to avoid Django's 'multiple' check super().__init__(attrs) # Add 'multiple' attribute after initialization self.attrs["multiple"] = "multiple" def value_from_datadict(self, data, files, name): """ Get all uploaded files for a given field name. Returns a list of uploaded files instead of a single file. """ if name in files: return files.getlist(name) return [] class PublicComplaintForm(forms.ModelForm): """ Simplified public complaint submission form. Key changes for AI-powered classification: - Fewer required fields (simplified for public users) - Severity and priority removed (AI will determine these automatically) - Only essential information collected - Updated with new fields: relation_to_patient, patient_name, national_id, incident_date, staff_name, expected_result """ # Contact Information complainant_name = forms.CharField( label=_("Complainant Name"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your full name")}), ) relation_to_patient = forms.ChoiceField( label=_("Relation to Patient"), choices=[ ("patient", "Patient"), ("relative", "Relative") ], required=True, widget=forms.Select(attrs={"class": "form-control"}), ) email = forms.EmailField( label=_("Email Address"), required=False, widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": _("your@email.com")}), ) mobile_number = forms.CharField( label=_("Mobile Number"), max_length=20, required=True, widget=forms.TextInput(attrs={ "class": "form-control", "placeholder": _("05XXXXXXXX or +9665XXXXXXXX"), "pattern": SAUDI_PHONE_HTML_PATTERN, "inputmode": "tel", }), ) # Patient Information patient_name = forms.CharField( label=_("Patient Name"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Name of the patient involved")}), ) national_id = forms.CharField( label=_("National ID/ Iqama No."), max_length=20, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Saudi National ID or Iqama number")}), ) incident_date = forms.DateField( label=_("Incident Date"), required=True, widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}) ) # Hospital and Department hospital = forms.ModelChoiceField( label=_("Hospital"), queryset=Hospital.objects.filter(status="active").order_by("name"), empty_label=_("Select Hospital"), required=True, widget=forms.Select( attrs={"class": "form-control", "id": "hospital_select", "data-action": "load-departments"} ), ) department = forms.ModelChoiceField( label=_("Department (Optional)"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "department_select"}), ) # Location Type and Area location_type = forms.ChoiceField( label=_("Location Type"), choices=[("", _("Select Location Type"))] + list(LocationType.choices), required=True, widget=forms.Select(attrs={"class": "form-control", "id": "location_type_select"}), ) area = forms.ModelChoiceField( label=_("Area (Optional)"), queryset=Area.objects.none(), empty_label=_("Select Area"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "area_select"}), ) section = forms.ModelChoiceField( label=_("Section"), queryset=Section.objects.none(), empty_label=_("Select Section"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "section_select"}), ) staff_name = forms.CharField( label=_("Staff Involved"), max_length=200, required=False, widget=forms.TextInput( attrs={"class": "form-control", "placeholder": _("Name of staff member involved (if known)")} ), ) complaint_details = forms.CharField( label=_("Complaint Details"), required=True, widget=forms.Textarea( attrs={ "class": "form-control", "rows": 6, "placeholder": _( "Please describe your complaint in detail. Our AI system will analyze and prioritize your complaint accordingly." ), } ), ) expected_result = forms.CharField( label=_("Expected Complaint Result"), required=False, widget=forms.Textarea( attrs={"class": "form-control", "rows": 3, "placeholder": _("What do you expect as a resolution?")} ), ) # Hidden fields - these will be set by view or AI severity = forms.ChoiceField( label=_("Severity"), choices=SeverityChoices.choices, initial=SeverityChoices.MEDIUM, required=False, widget=forms.HiddenInput(), ) priority = forms.ChoiceField( label=_("Priority"), choices=PriorityChoices.choices, initial=PriorityChoices.MEDIUM, required=False, widget=forms.HiddenInput(), ) # Source type - always internal for public complaints complaint_source_type = forms.ChoiceField( label=_("Complaint Source Type"), choices=ComplaintSourceType.choices, initial=ComplaintSourceType.INTERNAL, required=False, widget=forms.HiddenInput(), ) # File uploads attachments = forms.FileField( label=_("Attach Documents (Optional)"), required=False, widget=MultiFileInput(attrs={"class": "form-control", "accept": "image/*,.pdf,.doc,.docx"}), help_text=_("You can upload images, PDFs, or Word documents (max 10MB each)"), ) class Meta: model = Complaint fields = [ "complainant_name", "email", "mobile_number", "hospital", "relation_to_patient", "patient_name", "national_id", "incident_date", "location_type", "area", "department", "section", "staff_name", "complaint_details", "expected_result", "severity", "priority", "complaint_source_type", ] # Note: 'attachments' is not in fields because Complaint model doesn't have this field. # Attachments are handled separately via ComplaintAttachment model in the view. def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["section"].queryset = Section.objects.none() self.fields["area"].queryset = Area.objects.none() hospital_id = None if "hospital" in self.initial: hospital_id = self.initial["hospital"] elif "hospital" in self.data: hospital_id = self.data["hospital"] if hospital_id: self.fields["department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") self.fields["area"].queryset = Area.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name_en") department_id = None if "department" in self.initial: department_id = self.initial["department"] elif "department" in self.data: department_id = self.data["department"] if department_id: self.fields["section"].queryset = Section.objects.filter( department_id=department_id, status="active" ).order_by("name_en") def clean_mobile_number(self): """Validate Saudi mobile number format (05xxxxxxxx / 5xxxxxxxx / +9665xxxxxxxx / 9665xxxxxxxx).""" mobile_number = self.cleaned_data.get("mobile_number") if mobile_number: validate_saudi_phone(mobile_number) return mobile_number def clean_national_id(self): """Validate National ID/Iqama format""" national_id = self.cleaned_data.get("national_id") # Remove spaces national_id = national_id.replace(" ", "") # Validate it's 10 digits if len(national_id) != 10 or not national_id.isdigit(): raise ValidationError(_("Please enter a valid National ID or Iqama number (10 digits)")) return national_id def clean_incident_date(self): """Validate incident date is not in the future""" incident_date = self.cleaned_data.get("incident_date") from datetime import date if incident_date and incident_date > date.today(): raise ValidationError(_("Incident date cannot be in the future")) return incident_date def clean_attachments(self): """Validate file attachments""" files = self.files.getlist("attachments") # Check file count if len(files) > 5: raise ValidationError(_("Maximum 5 files allowed")) # Check each file for file in files: # Check file size (10MB limit) if file.size > 10 * 1024 * 1024: raise ValidationError(_("File size must be less than 10MB")) # Check file type allowed_extensions = [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"] ext = os.path.splitext(file.name)[1].lower() if ext not in allowed_extensions: raise ValidationError(_("Allowed file types: JPG, PNG, GIF, PDF, DOC, DOCX")) return files def clean(self): """Custom cross-field validation""" cleaned_data = super().clean() # Basic validation - all required fields are already validated by field definitions # This method is kept for future custom cross-field validation needs return cleaned_data class ComplaintForm(HospitalFieldMixin, forms.ModelForm): """ Form for creating complaints by authenticated users. Uses Category → Department → Section hierarchy. Includes new fields for detailed patient information and complaint type. Uses cascading dropdowns for department/section selection. Hospital field visibility: - PX Admins: See dropdown with all hospitals - Others: Hidden field, auto-set to user's hospital """ # Complaint Type complaint_type = forms.ChoiceField( label=_("Feedback Type"), choices=ComplaintType.choices, initial=ComplaintType.COMPLAINT, required=False, widget=forms.HiddenInput(), ) # Source type complaint_source_type = forms.ChoiceField( label=_("Complaint Source Type"), choices=ComplaintSourceType.choices, initial=ComplaintSourceType.INTERNAL, required=False, widget=forms.Select(attrs={"class": "form-select", "id": "complaintSourceType"}), ) # PX Source (optional) source = forms.ModelChoiceField( label=_("PX Source"), queryset=None, empty_label=_("Select source (optional)"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "sourceSelect"}), ) # Patient Information (text-based fields only) relation_to_patient = forms.ChoiceField( label=_("Relation to Patient"), choices=[ ("patient", "Patient"), ("relative", "Relative"), ], required=True, widget=forms.Select(attrs={"class": "form-select", "id": "relationToPatient"}), ) patient_name = forms.CharField( label=_("Patient Name"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Name of the patient involved")}), ) national_id = forms.CharField( label=_("National ID/Iqama No."), max_length=20, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Saudi National ID or Iqama number")}), ) incident_date = forms.DateField( label=_("Incident Date"), required=True, widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}) ) hospital = forms.ModelChoiceField( label=_("Hospital"), queryset=Hospital.objects.filter(status="active"), empty_label=_("Select Hospital"), required=True, widget=forms.Select(attrs={"class": "form-select", "id": "hospitalSelect"}), ) location_type = forms.ChoiceField( label=_("Location Type"), choices=[("", _("Select Location Type"))] + list(LocationType.choices), required=True, widget=forms.Select(attrs={"class": "form-select", "id": "locationTypeSelect"}), ) area = forms.ModelChoiceField( label=_("Area"), queryset=Area.objects.none(), empty_label=_("Select Area (optional)"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "areaSelect"}), ) department = forms.ModelChoiceField( label=_("Department"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=True, widget=forms.Select(attrs={"class": "form-select", "id": "departmentSelect"}), ) section = forms.ModelChoiceField( label=_("Section"), queryset=Section.objects.none(), empty_label=_("Select Section (optional)"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "sectionSelect"}), ) staff = forms.ModelChoiceField( label=_("Staff"), queryset=Staff.objects.none(), empty_label=_("Select Staff"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "staffSelect"}), ) description = forms.CharField( label=_("Description"), required=True, widget=forms.Textarea( attrs={"class": "form-control", "rows": 6, "placeholder": _("Detailed description of complaint...")} ), ) expected_result = forms.CharField( label=_("Expected Complaint Result"), required=False, widget=forms.Textarea( attrs={"class": "form-control", "rows": 3, "placeholder": _("What do you expect as a resolution?")} ), ) class Meta: model = Complaint fields = [ "complaint_type", "complaint_source_type", "source", "relation_to_patient", "patient_name", "national_id", "incident_date", "location_type", "area", "hospital", "department", "section", "staff", "description", "expected_result", ] def __init__(self, *args, **kwargs): # Note: user is handled by HospitalFieldMixin super().__init__(*args, **kwargs) from apps.organizations.models import Section from apps.px_sources.models import PXSource # Initialize cascading dropdowns with empty querysets self.fields["section"].queryset = Section.objects.none() self.fields["area"].queryset = Area.objects.none() # Load active PX sources for optional selection self.fields["source"].queryset = PXSource.objects.filter(is_active=True).order_by("name_en") # Hospital field is configured by HospitalFieldMixin # Now filter departments and staff based on hospital hospital_id = None if self.data.get("hospital"): hospital_id = self.data.get("hospital") elif self.initial.get("hospital"): hospital_id = self.initial.get("hospital") elif self.user and self.user.is_px_admin(): tenant_hospital = getattr(self.request, "tenant_hospital", None) if tenant_hospital: hospital_id = tenant_hospital.id elif self.user and self.user.hospital: hospital_id = self.user.hospital.id if hospital_id: # Filter departments based on selected hospital self.fields["department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") # Filter staff based on selected hospital self.fields["staff"].queryset = Staff.objects.filter(hospital_id=hospital_id, status="active").order_by( "first_name", "last_name" ) # Filter areas based on selected hospital self.fields["area"].queryset = Area.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name_en") # Populate section dropdown based on selected department department_id = self.data.get("department") or self.initial.get("department") if department_id: self.fields["section"].queryset = Section.objects.filter( department_id=department_id ).order_by("name_en") def clean_incident_date(self): incident_date = self.cleaned_data.get("incident_date") from datetime import date if incident_date and incident_date > date.today(): raise ValidationError(_("Incident date cannot be in the future")) return incident_date class InquiryForm(HospitalFieldMixin, forms.ModelForm): """ Form for creating inquiries by authenticated users. Similar to ComplaintForm - supports patient search, department filtering, and proper field validation with AJAX support. Hospital field visibility: - PX Admins: See dropdown with all hospitals - Others: Hidden field, auto-set to user's hospital """ patient = forms.ModelChoiceField( label=_("Patient (Optional)"), queryset=Patient.objects.filter(status="active"), empty_label=_("Select Patient"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "patientSelect"}), ) hospital = forms.ModelChoiceField( label=_("Hospital"), queryset=Hospital.objects.filter(status="active"), empty_label=_("Select Hospital"), required=True, widget=forms.Select(attrs={"class": "form-select", "id": "hospitalSelect"}), ) location_type = forms.ChoiceField( label=_("Location Type"), choices=[("", _("Select Location Type"))] + list(LocationType.choices), required=True, widget=forms.Select(attrs={"class": "form-select", "id": "locationTypeSelect"}), ) area = forms.ModelChoiceField( label=_("Area"), queryset=Area.objects.none(), empty_label=_("Select Area (optional)"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "areaSelect"}), ) department = forms.ModelChoiceField( label=_("Department (Optional)"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "departmentSelect"}), ) category = forms.ChoiceField( label=_("Inquiry Type"), choices=[ ("general", "General Inquiry"), ("appointment", "Appointment Related"), ("billing", "Billing & Insurance"), ("medical_records", "Medical Records"), ("pharmacy", "Pharmacy"), ("insurance", "Insurance"), ("feedback", "Feedback"), ("other", "Other"), ], required=True, widget=forms.Select(attrs={"class": "form-control"}), ) subject = forms.CharField( label=_("Subject"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Brief subject")}), ) message = forms.CharField( label=_("Message"), required=True, widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": _("Describe your inquiry")}), ) # Contact info for inquiries without patient contact_name = forms.CharField( label=_("Contact Name"), max_length=200, required=False, widget=forms.TextInput(attrs={"class": "form-control"}) ) contact_phone = forms.CharField( label=_("Contact Phone"), max_length=20, required=False, widget=forms.TextInput(attrs={"class": "form-control"}) ) contact_email = forms.EmailField( label=_("Contact Email"), required=False, widget=forms.EmailInput(attrs={"class": "form-control"}) ) section = forms.ModelChoiceField( label=_("Section"), queryset=None, empty_label=_("Select Section"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "sectionSelect"}), ) priority = forms.ChoiceField( label=_("Priority"), choices=[ ("low", _("Low")), ("medium", _("Medium")), ("high", _("High")), ("critical", _("Critical")), ], initial="medium", required=False, widget=forms.Select(attrs={"class": "form-select", "id": "prioritySelect"}), ) is_outgoing = forms.BooleanField( label=_("Outgoing Inquiry"), required=False, initial=False, widget=forms.HiddenInput(attrs={"id": "isOutgoing"}), ) outgoing_department = forms.ModelChoiceField( label=_("Outgoing Department"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=False, widget=forms.Select( attrs={"class": "form-select", "id": "outgoingDepartmentSelect", "data-tomselect": ""} ), ) source = forms.ModelChoiceField( label=_("Source"), queryset=None, empty_label=_("Select source (optional)"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "sourceSelect"}), ) class Meta: model = Inquiry fields = [ "patient", "hospital", "location_type", "area", "department", "subject", "message", "contact_name", "contact_phone", "contact_email", "national_id", "section", "priority", "source", "is_outgoing", "outgoing_department", ] def __init__(self, *args, **kwargs): # Note: user is handled by HospitalFieldMixin super().__init__(*args, **kwargs) from apps.organizations.models import Section from apps.px_sources.models import PXSource self.fields["section"].queryset = Section.objects.none() self.fields["area"].queryset = Area.objects.none() # Load active PX sources for optional selection self.fields["source"].queryset = PXSource.objects.filter(is_active=True).order_by("name_en") self.fields["source"].empty_label = "Select source (optional)" self.fields["source"].required = False hospital_id = None if self.data.get("hospital"): hospital_id = self.data.get("hospital") elif self.initial.get("hospital"): hospital_id = self.initial.get("hospital") elif self.user and self.user.is_px_admin(): tenant_hospital = getattr(self.request, "tenant_hospital", None) if tenant_hospital: hospital_id = tenant_hospital.id elif self.user and self.user.hospital: hospital_id = self.user.hospital.id if hospital_id: self.fields["department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") self.fields["outgoing_department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") self.fields["area"].queryset = Area.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name_en") # Populate section dropdown based on selected department department_id = self.data.get("department") or self.initial.get("department") if department_id: self.fields["section"].queryset = Section.objects.filter( department_id=department_id ).order_by("name_en") def clean_area(self): area = self.cleaned_data.get("area") if not area: return None return area def clean_department(self): dept = self.cleaned_data.get("department") if not dept: return None return dept def clean_section(self): section = self.cleaned_data.get("section") if not section: return None return section class SLAConfigForm(HospitalFieldMixin, forms.ModelForm): """Form for creating and editing SLA configurations""" class Meta: model = ComplaintSLAConfig fields = [ "hospital", "source", "severity", "sla_hours", "reminder_hours_before", "second_reminder_hours_before", "is_active", ] widgets = { "hospital": forms.Select(attrs={"class": "form-select"}), "source": forms.Select(attrs={"class": "form-select"}), "severity": forms.Select(attrs={"class": "form-select"}), "sla_hours": forms.NumberInput(attrs={"class": "form-control", "min": "1"}), "reminder_hours_before": forms.NumberInput(attrs={"class": "form-control", "min": "0"}), "second_reminder_hours_before": forms.NumberInput(attrs={"class": "form-control", "min": "0"}), "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}), } def clean(self): cleaned_data = super().clean() hospital = cleaned_data.get("hospital") source = cleaned_data.get("source") severity = cleaned_data.get("severity") sla_hours = cleaned_data.get("sla_hours") reminder_hours_before = cleaned_data.get("reminder_hours_before") second_reminder_hours_before = cleaned_data.get("second_reminder_hours_before") # Validate SLA hours is positive if sla_hours and sla_hours <= 0: raise ValidationError({"sla_hours": "SLA hours must be greater than 0"}) # Validate first reminder hours (must be less than SLA hours) if reminder_hours_before and reminder_hours_before > 0: if reminder_hours_before >= sla_hours: raise ValidationError({"reminder_hours_before": "First reminder must be less than SLA hours"}) # Validate second reminder hours if second_reminder_hours_before and second_reminder_hours_before > 0: if second_reminder_hours_before >= sla_hours: raise ValidationError({"second_reminder_hours_before": "Second reminder must be less than SLA hours"}) if reminder_hours_before and second_reminder_hours_before >= reminder_hours_before: raise ValidationError({"second_reminder_hours_before": "Second reminder must be closer to deadline than first"}) # Check for unique combination (excluding current instance when editing) filters = {} if hospital: filters["hospital"] = hospital if source: filters["source"] = source if severity: filters["severity"] = severity if filters: queryset = ComplaintSLAConfig.objects.filter(**filters) if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) if queryset.exists(): raise ValidationError("An SLA configuration with these settings already exists.") return cleaned_data class ComplaintThresholdForm(HospitalFieldMixin, forms.ModelForm): """ Form for creating and editing complaint thresholds. Hospital field visibility: - PX Admins: See dropdown with all hospitals - Others: Hidden field, auto-set to user's hospital """ class Meta: model = ComplaintThreshold fields = ["hospital", "threshold_type", "threshold_value", "comparison_operator", "action_type", "is_active"] widgets = { "hospital": forms.Select(attrs={"class": "form-select"}), "threshold_type": forms.Select(attrs={"class": "form-select"}), "threshold_value": forms.NumberInput(attrs={"class": "form-control", "step": "0.1"}), "comparison_operator": forms.Select(attrs={"class": "form-select"}), "action_type": forms.Select(attrs={"class": "form-select"}), "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}), } class PublicInquiryForm(forms.Form): """Public inquiry submission form (simpler, for general questions)""" # Contact Information name = forms.CharField( label=_("Name"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your full name")}), ) phone = forms.CharField( label=_("Phone Number"), max_length=20, required=True, validators=[validate_saudi_phone], widget=forms.TextInput(attrs={ "class": "form-control", "placeholder": _("05XXXXXXXX or +9665XXXXXXXX"), "pattern": SAUDI_PHONE_HTML_PATTERN, "inputmode": "tel", }), ) email = forms.EmailField( label=_("Email Address"), required=False, widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": _("your@email.com")}), ) national_id = forms.CharField( label=_("National ID/Iqama"), max_length=20, required=False, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("National ID or Iqama (optional)")}), ) # Inquiry Details hospital = forms.ModelChoiceField( label=_("Hospital"), queryset=Hospital.objects.filter(status="active").order_by("name"), empty_label=_("Select Hospital"), required=True, widget=forms.Select(attrs={"class": "form-control", "id": "hospital_select"}), ) location_type = forms.ChoiceField( label=_("Location Type"), choices=[("", _("Select Location Type"))] + list(LocationType.choices), required=True, widget=forms.Select(attrs={"class": "form-control", "id": "location_type_select"}), ) area = forms.ModelChoiceField( label=_("Area (Optional)"), queryset=Area.objects.none(), empty_label=_("Select Area"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "area_select"}), ) department = forms.ModelChoiceField( label=_("Department (Optional)"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "department_select"}), ) section = forms.ModelChoiceField( label=_("Section (Optional)"), queryset=Section.objects.none(), empty_label=_("Select Section"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "section_select"}), ) category = forms.ChoiceField( label=_("Inquiry Type"), choices=[ ("general", "General Inquiry"), ("appointment", "Appointment Related"), ("billing", "Billing & Insurance"), ("medical_records", "Medical Records"), ("other", "Other"), ], required=True, widget=forms.Select(attrs={"class": "form-control"}), ) subject = forms.CharField( label=_("Subject"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Brief subject")}), ) message = forms.CharField( label=_("Message"), required=True, widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": _("Describe your inquiry")}), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["area"].queryset = Area.objects.none() self.fields["section"].queryset = Section.objects.none() hospital_id = None if "hospital" in self.initial: hospital_id = self.initial["hospital"] elif "hospital" in self.data: hospital_id = self.data["hospital"] if hospital_id: self.fields["department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") self.fields["area"].queryset = Area.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name_en") department_id = None if "department" in self.initial: department_id = self.initial["department"] elif "department" in self.data: department_id = self.data["department"] if department_id: self.fields["section"].queryset = Section.objects.filter( department_id=department_id, status="active" ).order_by("name_en") class ComplaintInvolvedDepartmentForm(forms.ModelForm): """ Form for adding an involved department to a complaint. Allows specifying the department, role, and assignment. """ class Meta: model = ComplaintInvolvedDepartment fields = ["department", "role", "is_primary", "notes", "assigned_to"] widgets = { "department": forms.Select(attrs={"class": "form-select"}), "role": forms.Select(attrs={"class": "form-select"}), "is_primary": forms.CheckboxInput(attrs={"class": "form-check-input"}), "notes": forms.Textarea(attrs={"class": "form-control", "rows": 2}), "assigned_to": forms.Select(attrs={"class": "form-select"}), } def __init__(self, *args, **kwargs): self.complaint = kwargs.pop("complaint", None) user = kwargs.pop("user", None) super().__init__(*args, **kwargs) # Filter departments based on complaint's hospital if self.complaint and self.complaint.hospital: self.fields["department"].queryset = Department.objects.filter( hospital=self.complaint.hospital, status="active" ).order_by("name") else: self.fields["department"].queryset = Department.objects.none() # Filter assigned_to users based on hospital if self.complaint and self.complaint.hospital: from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(self.complaint.hospital) else: self.fields["assigned_to"].queryset = User.objects.none() # Make assigned_to optional self.fields["assigned_to"].required = False def clean_department(self): department = self.cleaned_data.get("department") if self.complaint and department: # Check if this department is already involved existing = ComplaintInvolvedDepartment.objects.filter(complaint=self.complaint, department=department) if self.instance.pk: existing = existing.exclude(pk=self.instance.pk) if existing.exists(): raise ValidationError(_("This department is already involved in this complaint.")) return department def save(self, commit=True): instance = super().save(commit=False) if self.complaint: instance.complaint = self.complaint if commit: instance.save() return instance class ComplaintInvolvedStaffForm(forms.ModelForm): """ Form for adding an involved staff member to a complaint. Allows specifying the staff member and their role in the complaint. """ class Meta: model = ComplaintInvolvedStaff fields = ["staff", "role", "notes"] widgets = { "staff": forms.Select(attrs={"class": "form-select", "id": "involvedStaffSelect"}), "role": forms.Select(attrs={"class": "form-select"}), "notes": forms.Textarea(attrs={"class": "form-control", "rows": 2}), } def __init__(self, *args, **kwargs): self.complaint = kwargs.pop("complaint", None) user = kwargs.pop("user", None) super().__init__(*args, **kwargs) # Filter staff based on complaint's hospital if self.complaint and self.complaint.hospital: from apps.organizations.models import Staff self.fields["staff"].queryset = Staff.objects.filter( hospital=self.complaint.hospital, status="active" ).order_by("first_name", "last_name") else: self.fields["staff"].queryset = Staff.objects.none() def clean_staff(self): staff = self.cleaned_data.get("staff") if self.complaint and staff: # Check if this staff is already involved existing = ComplaintInvolvedStaff.objects.filter(complaint=self.complaint, staff=staff) if self.instance.pk: existing = existing.exclude(pk=self.instance.pk) if existing.exists(): raise ValidationError(_("This staff member is already involved in this complaint.")) return staff def save(self, commit=True): instance = super().save(commit=False) if self.complaint: instance.complaint = self.complaint if commit: instance.save() return instance class DepartmentResponseForm(forms.ModelForm): """ Form for an involved department to submit their response. """ class Meta: model = ComplaintInvolvedDepartment fields = ["response_notes"] widgets = { "response_notes": forms.Textarea( attrs={ "class": "form-control", "rows": 4, "placeholder": _("Enter department response and findings..."), } ), } class StaffExplanationForm(forms.ModelForm): """ Form for an involved staff member to submit their explanation. """ class Meta: model = ComplaintInvolvedStaff fields = ["explanation"] widgets = { "explanation": forms.Textarea( attrs={ "class": "form-control", "rows": 4, "placeholder": _("Enter your explanation regarding this complaint..."), } ), } class GovernmentTicketForm(forms.ModelForm): """Form for creating and editing government tickets""" class Meta: model = GovernmentTicket fields = [ "source", "ticket_number", "complainant_name", "national_id", "contact_number", "department", "section", "received_date", "classification", "content", "status", "assigned_to", ] widgets = { "received_date": forms.DateTimeInput( attrs={"type": "datetime-local", "class": "form-control"}, format="%Y-%m-%dT%H:%M", ), "content": forms.Textarea( attrs={"class": "form-control", "rows": 5, "placeholder": _("Enter ticket content...")} ), "ticket_number": forms.TextInput( attrs={"class": "form-control", "placeholder": _("e.g., B2022807")} ), "complainant_name": forms.TextInput( attrs={"class": "form-control", "placeholder": _("Complainant name")} ), "national_id": forms.TextInput( attrs={"class": "form-control", "placeholder": _("National ID")} ), "contact_number": forms.TextInput( attrs={"class": "form-control", "placeholder": _("Contact number")} ), "classification": forms.TextInput( attrs={"class": "form-control", "placeholder": _("Classification")} ), } def __init__(self, *args, hospital=None, **kwargs): super().__init__(*args, **kwargs) # Filter source to only government sources self.fields["source"].queryset = self.fields["source"].queryset.filter( source_type="government", is_active=True ) self.fields["national_id"].required = False self.fields["contact_number"].required = False self.fields["classification"].required = False self.fields["assigned_to"].required = False if hospital: from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(hospital) from apps.organizations.models import Section if args and args[0]: self.fields["section"].queryset = Section.objects.all() else: self.fields["section"].queryset = Section.objects.none() def clean_ticket_number(self): ticket_number = self.cleaned_data.get("ticket_number") if ticket_number: # Check for duplicates, excluding current instance qs = GovernmentTicket.objects.filter(ticket_number=ticket_number) if self.instance and self.instance.pk: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): raise ValidationError(_("A ticket with this number already exists.")) return ticket_number