diff --git a/apps/complaints/COMPLAINT_FORM_DJANGO_FORM_IMPLEMENTATION.md b/apps/complaints/COMPLAINT_FORM_DJANGO_FORM_IMPLEMENTATION.md new file mode 100644 index 0000000..95fa52b --- /dev/null +++ b/apps/complaints/COMPLAINT_FORM_DJANGO_FORM_IMPLEMENTATION.md @@ -0,0 +1,106 @@ +# Complaint Form Django Form Implementation + +## Summary + +The complaint form has been successfully refactored to use Django's built-in form rendering instead of manual HTML fields and complex AJAX calls. + +## Changes Made + +### 1. New `ComplaintForm` in `apps/complaints/forms.py` + +**Fields Included:** +- `patient` - Required dropdown, filtered by user hospital +- `hospital` - Required dropdown, pre-filtered by user permissions +- `department` - Optional dropdown, filtered by selected hospital +- `staff` - Optional dropdown, filtered by selected department +- `encounter_id` - Optional text field +- `description` - Required textarea + +**Fields Removed (AI will determine):** +- `category` - AI will determine automatically +- `subcategory` - AI will determine automatically +- `source` - Set to 'staff' for authenticated users + +**Features:** +- User permission filtering (PX admins see all, hospital users see only their hospital) +- Dependent queryset initialization (departments load when hospital is pre-selected) +- Full Django form validation +- Clean error messages + +### 2. Updated `complaint_create` View in `apps/complaints/ui_views.py` + +**Changes:** +- Uses `ComplaintForm(request.POST, user=request.user)` for form handling +- Handles `form.is_valid()` validation +- Sets AI defaults before saving: + - `title = 'Complaint'` (AI will generate) + - `category = None` (AI will determine) + - `subcategory = ''` (AI will determine) + - `source = 'staff'` (default for authenticated users) + - `priority = 'medium'` (AI will update) + - `severity = 'medium'` (AI will update) +- Creates initial update record +- Triggers AI analysis via Celery +- Logs audit trail +- Handles hospital parameter for form pre-selection + +### 3. Updated Template `templates/complaints/complaint_form.html` + +**Changes:** +- Uses Django form rendering: `{{ form.field }}` +- Removed all manual HTML input fields +- Removed complex AJAX endpoints +- Kept minimal JavaScript: + - Hospital change → reload form with hospital parameter + - Department change → load staff via `/complaints/ajax/physicians/` + - Form validation + +**Removed AJAX Endpoints:** +- `/api/organizations/departments/` - No longer needed +- `/api/organizations/patients/` - No longer needed +- `/complaints/ajax/get-staff-by-department/` - Changed to `/complaints/ajax/physicians/` + +**Structure:** +- Patient Information section +- Organization section (Hospital, Department, Staff) +- Complaint Details section (Description) +- AI Classification info alert +- SLA Information alert +- Action buttons + +## Benefits + +1. **Simpler Code** - Django handles form rendering and validation +2. **Better Error Handling** - Form validation with clear error messages +3. **Less JavaScript** - Only minimal JS for dependent dropdowns +4. **Cleaner Separation** - Business logic in forms, presentation in templates +5. **User Permissions** - Automatic filtering based on user role +6. **AI Integration** - Category, subcategory, severity, and priority determined by AI + +## Testing Checklist + +- [ ] Form loads correctly for PX admin users +- [ ] Form loads correctly for hospital users (filtered to their hospital) +- [ ] Hospital dropdown pre-fills when hospital parameter in URL +- [ ] Department dropdown populates when hospital selected +- [ ] Staff dropdown populates when department selected +- [ ] Form validation works for required fields +- [ ] Complaint saves successfully +- [ ] AI analysis task is triggered after creation +- [ ] User is redirected to complaint detail page +- [ ] Back links work for both regular and source users + +## Related Files + +- `apps/complaints/forms.py` - ComplaintForm definition +- `apps/complaints/ui_views.py` - complaint_create view +- `templates/complaints/complaint_form.html` - Form template +- `apps/complaints/urls.py` - URL configuration +- `apps/complaints/tasks.py` - AI analysis task + +## Notes + +- The form uses a simple reload approach for hospital selection to keep JavaScript minimal +- Staff loading still uses AJAX because it's a common pattern and provides good UX +- All AI-determined fields are hidden from the user interface +- The form is bilingual-ready using Django's translation system diff --git a/apps/complaints/forms.py b/apps/complaints/forms.py index 199b0be..7bcb337 100644 --- a/apps/complaints/forms.py +++ b/apps/complaints/forms.py @@ -12,12 +12,13 @@ from apps.complaints.models import ( ComplaintCategory, ComplaintSource, ComplaintStatus, + Inquiry, ComplaintSLAConfig, EscalationRule, ComplaintThreshold, ) from apps.core.models import PriorityChoices, SeverityChoices -from apps.organizations.models import Department, Hospital +from apps.organizations.models import Department, Hospital, Patient, Staff class MultiFileInput(forms.FileInput): @@ -35,7 +36,7 @@ class MultiFileInput(forms.FileInput): def value_from_datadict(self, data, files, name): """ - Get all uploaded files for the given field name. + Get all uploaded files for a given field name. Returns a list of uploaded files instead of a single file. """ @@ -252,6 +253,201 @@ class PublicComplaintForm(forms.ModelForm): 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""" diff --git a/apps/complaints/ui_views.py b/apps/complaints/ui_views.py index 8239edd..11a25a6 100644 --- a/apps/complaints/ui_views.py +++ b/apps/complaints/ui_views.py @@ -14,6 +14,7 @@ from django.views.decorators.http import require_http_methods from apps.accounts.models import User from apps.core.services import AuditService from apps.organizations.models import Department, Hospital, Staff +from apps.px_sources.models import SourceUser, PXSource from .models import ( Complaint, @@ -178,6 +179,10 @@ def complaint_detail(request, pk): - Linked PX actions - Workflow actions (assign, status change, add note) """ + from apps.px_sources.models import SourceUser + source_user = SourceUser.objects.filter(user=request.user).first() + base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html' + complaint = get_object_or_404( Complaint.objects.select_related( "patient", "hospital", "department", "staff", "assigned_to", "resolved_by", "closed_by", "resolution_survey" @@ -244,6 +249,8 @@ def complaint_detail(request, pk): "status_choices": ComplaintStatus.choices, "can_edit": user.is_px_admin() or user.is_hospital_admin(), "hospital_departments": hospital_departments, + 'base_layout': base_layout, + 'source_user': source_user, "explanation": explanation, "explanations": explanations, "explanation_attachments": explanation_attachments, @@ -256,6 +263,8 @@ def complaint_detail(request, pk): @require_http_methods(["GET", "POST"]) def complaint_create(request): """Create new complaint with AI-powered classification""" + from apps.complaints.forms import ComplaintForm + # Determine base layout based on user type from apps.px_sources.models import SourceUser source_user = SourceUser.objects.filter(user=request.user).first() @@ -263,47 +272,49 @@ def complaint_create(request): if request.method == 'POST': # Handle form submission + form = ComplaintForm(request.POST, user=request.user) + + if not form.is_valid(): + # Debug: print form errors + print("Form validation errors:", form.errors) + messages.error(request, f"Please correct the errors: {form.errors}") + context = { + 'form': form, + 'base_layout': base_layout, + 'source_user': source_user, + } + return render(request, 'complaints/complaint_form.html', context) + try: - from apps.organizations.models import Patient - - # Get form data - patient_id = request.POST.get("patient_id") - hospital_id = request.POST.get("hospital_id") - department_id = request.POST.get("department_id", None) - staff_id = request.POST.get("staff_id", None) - - description = request.POST.get("description") - category_id = request.POST.get("category") - subcategory_id = request.POST.get("subcategory", "") - source = request.POST.get("source") - encounter_id = request.POST.get("encounter_id", "") - - # Validate required fields - if not all([patient_id, hospital_id, description, category_id, source]): - messages.error(request, "Please fill in all required fields.") - return redirect("complaints:complaint_create") - - # Get category and subcategory objects - category = ComplaintCategory.objects.get(id=category_id) - subcategory_obj = None - if subcategory_id: - subcategory_obj = ComplaintCategory.objects.get(id=subcategory_id) - # Create complaint with AI defaults - complaint = Complaint.objects.create( - patient_id=patient_id, - hospital_id=hospital_id, - department_id=department_id if department_id else None, - staff_id=staff_id if staff_id else None, - title="Complaint", # AI will generate title - description=description, - category=category, - subcategory=subcategory_obj.code if subcategory_obj else "", - priority="medium", # AI will update - severity="medium", # AI will update - source=source, - encounter_id=encounter_id, - ) + complaint = form.save(commit=False) + + # Set AI-determined defaults + complaint.title = 'Complaint' # AI will generate title + # category can be None, AI will determine it + complaint.subcategory = '' # AI will determine + + # Set source from logged-in source user + if source_user and source_user.source: + complaint.source = source_user.source + else: + # Fallback: get or create a 'staff' source + from apps.px_sources.models import PXSource + try: + source_obj = PXSource.objects.get(code='staff') + except PXSource.DoesNotExist: + source_obj = PXSource.objects.create( + code='staff', + name='Staff', + description='Complaints submitted by staff members' + ) + complaint.source = source_obj + + complaint.priority = 'medium' # AI will update + complaint.severity = 'medium' # AI will update + complaint.created_by = request.user + + complaint.save() # Create initial update ComplaintUpdate.objects.create( @@ -313,7 +324,7 @@ def complaint_create(request): created_by=request.user, ) - # Trigger AI analysis in the background using Celery + # Trigger AI analysis in background using Celery from apps.complaints.tasks import analyze_complaint_with_ai analyze_complaint_with_ai.delay(str(complaint.id)) @@ -325,7 +336,7 @@ def complaint_create(request): user=request.user, content_object=complaint, metadata={ - "category": category.name_en, + # "category": category.name_en, "severity": complaint.severity, "patient_mrn": complaint.patient.mrn, "ai_analysis_pending": True, @@ -346,12 +357,16 @@ def complaint_create(request): return redirect("complaints:complaint_create") # GET request - show form - hospitals = Hospital.objects.filter(status="active") - if not request.user.is_px_admin() and request.user.hospital: - hospitals = hospitals.filter(id=request.user.hospital.id) + # Check for hospital parameter from URL (for pre-selection) + initial_data = {} + hospital_id = request.GET.get('hospital') + if hospital_id: + initial_data['hospital'] = hospital_id + + form = ComplaintForm(user=request.user, initial=initial_data) context = { - 'hospitals': hospitals, + 'form': form, 'base_layout': base_layout, 'source_user': source_user, } @@ -904,6 +919,10 @@ def inquiry_detail(request, pk): - Attachments management - Workflow actions (assign, status change, add note, respond) """ + from apps.px_sources.models import SourceUser + source_user = SourceUser.objects.filter(user=request.user).first() + base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html' + inquiry = get_object_or_404( Inquiry.objects.select_related( "patient", "hospital", "department", "assigned_to", "responded_by" @@ -947,6 +966,8 @@ def inquiry_detail(request, pk): "assignable_users": assignable_users, "status_choices": status_choices, "can_edit": user.is_px_admin() or user.is_hospital_admin(), + 'base_layout': base_layout, + 'source_user': source_user, } return render(request, "complaints/inquiry_detail.html", context) @@ -957,46 +978,38 @@ def inquiry_detail(request, pk): def inquiry_create(request): """Create new inquiry""" from .models import Inquiry + from .forms import InquiryForm from apps.organizations.models import Patient + from apps.px_sources.models import SourceUser, PXSource # Determine base layout based on user type - from apps.px_sources.models import SourceUser source_user = SourceUser.objects.filter(user=request.user).first() base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html' if request.method == 'POST': + # Handle form submission + form = InquiryForm(request.POST, user=request.user) + + if not form.is_valid(): + messages.error(request, f"Please correct the errors: {form.errors}") + context = { + 'form': form, + 'base_layout': base_layout, + 'source_user': source_user, + } + return render(request, 'complaints/inquiry_form.html', context) + try: - # Get form data - patient_id = request.POST.get("patient_id", None) - hospital_id = request.POST.get("hospital_id") - department_id = request.POST.get("department_id", None) - - subject = request.POST.get("subject") - message = request.POST.get("message") - category = request.POST.get("category") - - # Contact info (if no patient) - contact_name = request.POST.get("contact_name", "") - contact_phone = request.POST.get("contact_phone", "") - contact_email = request.POST.get("contact_email", "") - - # Validate required fields - if not all([hospital_id, subject, message, category]): - messages.error(request, "Please fill in all required fields.") - return redirect("complaints:inquiry_create") - - # Create inquiry - inquiry = Inquiry.objects.create( - patient_id=patient_id if patient_id else None, - hospital_id=hospital_id, - department_id=department_id if department_id else None, - subject=subject, - message=message, - category=category, - contact_name=contact_name, - contact_phone=contact_phone, - contact_email=contact_email, - ) + # Save inquiry + inquiry = form.save(commit=False) + + # Set source for source users + source_user = SourceUser.objects.filter(user=request.user).first() + if source_user: + inquiry.source = source_user.source + inquiry.created_by = request.user + + inquiry.save() # Log audit AuditService.log_event( @@ -1015,12 +1028,13 @@ def inquiry_create(request): return redirect("complaints:inquiry_create") # GET request - show form - hospitals = Hospital.objects.filter(status="active") + form = InquiryForm(user=request.user) + hospitals = Hospital.objects.filter(status='active') if not request.user.is_px_admin() and request.user.hospital: hospitals = hospitals.filter(id=request.user.hospital.id) context = { - 'hospitals': hospitals, + 'form': form, 'base_layout': base_layout, 'source_user': source_user, } diff --git a/templates/complaints/complaint_detail.html b/templates/complaints/complaint_detail.html index f6c8cd8..567c317 100644 --- a/templates/complaints/complaint_detail.html +++ b/templates/complaints/complaint_detail.html @@ -1,4 +1,4 @@ -{% extends "layouts/base.html" %} +{% extends base_layout %} {% load i18n %} {% load static %} {% load math %} @@ -119,9 +119,15 @@
{{ _("File a new patient complaint with SLA tracking")}}