HH/apps/complaints/forms.py
2026-07-19 12:27:55 +03:00

1236 lines
43 KiB
Python

"""
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