""" Complaints forms """ 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, ComplaintStatus, Inquiry, ComplaintSLAConfig, EscalationRule, ComplaintThreshold, ) from apps.core.models import PriorityChoices, SeverityChoices from apps.organizations.models import Department, Hospital, Patient, Staff class MultiFileInput(forms.FileInput): """ Custom FileInput widget that supports multiple file uploads. Unlike the 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 """ # Contact Information name = forms.CharField( label=_("Name"), max_length=200, required=True, widget=forms.TextInput( attrs={ 'class': 'form-control', 'placeholder': _('Your full name') } ) ) email = forms.EmailField( label=_("Email Address"), required=True, widget=forms.EmailInput( attrs={ 'class': 'form-control', 'placeholder': _('your@email.com') } ) ) phone = forms.CharField( label=_("Phone Number"), max_length=20, required=True, widget=forms.TextInput( attrs={ 'class': 'form-control', 'placeholder': _('Your phone number') } ) ) # 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' } ) ) # Complaint Details category = forms.ModelChoiceField( label=_("Complaint Category"), queryset=ComplaintCategory.objects.filter(is_active=True).order_by('name_en'), empty_label=_("Select Category"), required=True, widget=forms.Select( attrs={ 'class': 'form-control', 'id': 'category_select' } ) ) title = forms.CharField( label=_("Complaint Title"), max_length=200, required=True, widget=forms.TextInput( attrs={ 'class': 'form-control', 'placeholder': _('Brief title of your complaint') } ) ) description = forms.CharField( label=_("Complaint Description"), 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.') } ) ) # Hidden fields - these will be set by the 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() ) # 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 = [ 'name', 'email', 'phone', 'hospital', 'department', 'category', 'title', 'description', 'severity', 'priority' ] # 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) # Check both initial data and POST data for hospital 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: # Filter departments self.fields['department'].queryset = Department.objects.filter( hospital_id=hospital_id, status='active' ).order_by('name') # Filter categories (show hospital-specific first, then system-wide) self.fields['category'].queryset = ComplaintCategory.objects.filter( models.Q(hospital_id=hospital_id) | models.Q(hospital__isnull=True), is_active=True ).order_by('hospital', 'order', 'name_en') 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'] import os 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(forms.ModelForm): """ Form for creating complaints by authenticated users. Uses Django form rendering with minimal JavaScript for dependent dropdowns. Category, subcategory, and source are omitted - AI will determine them. """ patient = forms.ModelChoiceField( label=_("Patient"), queryset=Patient.objects.filter(status='active'), empty_label=_("Select Patient"), required=True, 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'}) ) department = forms.ModelChoiceField( label=_("Department"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=False, widget=forms.Select(attrs={'class': 'form-select', 'id': 'departmentSelect'}) ) staff = forms.ModelChoiceField( label=_("Staff"), queryset=Staff.objects.none(), empty_label=_("Select Staff"), required=False, widget=forms.Select(attrs={'class': 'form-select', 'id': 'staffSelect'}) ) encounter_id = forms.CharField( label=_("Encounter ID"), required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Optional encounter/visit ID')}) ) description = forms.CharField( label=_("Description"), required=True, widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6, 'placeholder': _('Detailed description of complaint...')}) ) class Meta: model = Complaint fields = ['patient', 'hospital', 'department', 'staff', 'encounter_id', 'description'] def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter hospital and patient by user permissions if user and not user.is_px_admin() and user.hospital: self.fields['hospital'].queryset = Hospital.objects.filter( id=user.hospital.id ) self.fields['patient'].queryset = Patient.objects.filter( primary_hospital=user.hospital, status='active' ) # Check for hospital selection in both initial data and POST data # This is needed for validation to work correctly hospital_id = None if 'hospital' in self.data: hospital_id = self.data.get('hospital') elif 'hospital' in self.initial: hospital_id = self.initial.get('hospital') 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') class InquiryForm(forms.ModelForm): """ Form for creating inquiries by authenticated users. Similar to ComplaintForm - supports patient search, department filtering, and proper field validation with AJAX support. """ 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'}) ) 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'})) class Meta: model = Inquiry fields = ['patient', 'hospital', 'department', 'subject', 'message', 'contact_name', 'contact_phone', 'contact_email'] def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter hospital by user permissions if user and not user.is_px_admin() and user.hospital: self.fields['hospital'].queryset = Hospital.objects.filter( id=user.hospital.id ) # Check for hospital selection in both initial data and POST data hospital_id = None if 'hospital' in self.data: hospital_id = self.data.get('hospital') elif 'hospital' in self.initial: hospital_id = self.initial.get('hospital') 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') class SLAConfigForm(forms.ModelForm): """Form for creating and editing SLA configurations""" class Meta: model = ComplaintSLAConfig fields = ['hospital', 'severity', 'priority', 'sla_hours', 'reminder_hours_before', 'is_active'] widgets = { 'hospital': forms.Select(attrs={'class': 'form-select'}), 'severity': forms.Select(attrs={'class': 'form-select'}), 'priority': 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'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter hospitals based on user role if user and not user.is_px_admin() and user.hospital: self.fields['hospital'].queryset = Hospital.objects.filter(id=user.hospital.id) self.fields['hospital'].initial = user.hospital self.fields['hospital'].widget.attrs['readonly'] = True def clean(self): cleaned_data = super().clean() hospital = cleaned_data.get('hospital') severity = cleaned_data.get('severity') priority = cleaned_data.get('priority') sla_hours = cleaned_data.get('sla_hours') reminder_hours = cleaned_data.get('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 reminder hours < SLA hours if sla_hours and reminder_hours and reminder_hours >= sla_hours: raise ValidationError({'reminder_hours_before': 'Reminder hours must be less than SLA hours'}) # Check for unique combination (excluding current instance when editing) if hospital and severity and priority: queryset = ComplaintSLAConfig.objects.filter( hospital=hospital, severity=severity, priority=priority ) if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) if queryset.exists(): raise ValidationError( 'An SLA configuration for this hospital, severity, and priority already exists.' ) return cleaned_data class EscalationRuleForm(forms.ModelForm): """Form for creating and editing escalation rules""" class Meta: model = EscalationRule fields = [ 'hospital', 'name', 'description', 'escalation_level', 'max_escalation_level', 'trigger_on_overdue', 'trigger_hours_overdue', 'reminder_escalation_enabled', 'reminder_escalation_hours', 'escalate_to_role', 'escalate_to_user', 'severity_filter', 'priority_filter', 'is_active' ] widgets = { 'hospital': forms.Select(attrs={'class': 'form-select'}), 'name': forms.TextInput(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'escalation_level': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'max_escalation_level': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'trigger_on_overdue': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'trigger_hours_overdue': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}), 'reminder_escalation_enabled': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'reminder_escalation_hours': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}), 'escalate_to_role': forms.Select(attrs={'class': 'form-select', 'id': 'escalate_to_role'}), 'escalate_to_user': forms.Select(attrs={'class': 'form-select'}), 'severity_filter': forms.Select(attrs={'class': 'form-select'}), 'priority_filter': forms.Select(attrs={'class': 'form-select'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter hospitals based on user role if user and not user.is_px_admin() and user.hospital: self.fields['hospital'].queryset = Hospital.objects.filter(id=user.hospital.id) self.fields['hospital'].initial = user.hospital self.fields['hospital'].widget.attrs['readonly'] = True # Filter users for escalate_to_user field from apps.accounts.models import User if user and user.is_px_admin(): self.fields['escalate_to_user'].queryset = User.objects.filter(is_active=True) elif user and user.hospital: self.fields['escalate_to_user'].queryset = User.objects.filter( is_active=True, hospital=user.hospital ) else: self.fields['escalate_to_user'].queryset = User.objects.none() def clean(self): cleaned_data = super().clean() escalate_to_role = cleaned_data.get('escalate_to_role') escalate_to_user = cleaned_data.get('escalate_to_user') # If role is 'specific_user', user must be specified if escalate_to_role == 'specific_user' and not escalate_to_user: raise ValidationError({'escalate_to_user': 'Please select a user when role is set to Specific User'}) return cleaned_data class ComplaintThresholdForm(forms.ModelForm): """Form for creating and editing complaint thresholds""" 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'}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter hospitals based on user role if user and not user.is_px_admin() and user.hospital: self.fields['hospital'].queryset = Hospital.objects.filter(id=user.hospital.id) self.fields['hospital'].initial = user.hospital self.fields['hospital'].widget.attrs['readonly'] = True 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, widget=forms.TextInput( attrs={ 'class': 'form-control', 'placeholder': _('Your phone number') } ) ) email = forms.EmailField( label=_("Email Address"), required=False, widget=forms.EmailInput( attrs={ 'class': 'form-control', 'placeholder': _('your@email.com') } ) ) # 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'}) ) 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') } ) )