update the complaint and inquiry creation for the source user

This commit is contained in:
Faheed 2026-01-14 11:26:44 +03:00 committed by ismail
parent 90dd2a66af
commit 8b65f9a52e
7 changed files with 589 additions and 398 deletions

View File

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

View File

@ -12,12 +12,13 @@ from apps.complaints.models import (
ComplaintCategory, ComplaintCategory,
ComplaintSource, ComplaintSource,
ComplaintStatus, ComplaintStatus,
Inquiry,
ComplaintSLAConfig, ComplaintSLAConfig,
EscalationRule, EscalationRule,
ComplaintThreshold, ComplaintThreshold,
) )
from apps.core.models import PriorityChoices, SeverityChoices 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): class MultiFileInput(forms.FileInput):
@ -35,7 +36,7 @@ class MultiFileInput(forms.FileInput):
def value_from_datadict(self, data, files, name): 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. Returns a list of uploaded files instead of a single file.
""" """
@ -252,6 +253,201 @@ class PublicComplaintForm(forms.ModelForm):
return cleaned_data 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): class SLAConfigForm(forms.ModelForm):
"""Form for creating and editing SLA configurations""" """Form for creating and editing SLA configurations"""

View File

@ -14,6 +14,7 @@ from django.views.decorators.http import require_http_methods
from apps.accounts.models import User from apps.accounts.models import User
from apps.core.services import AuditService from apps.core.services import AuditService
from apps.organizations.models import Department, Hospital, Staff from apps.organizations.models import Department, Hospital, Staff
from apps.px_sources.models import SourceUser, PXSource
from .models import ( from .models import (
Complaint, Complaint,
@ -178,6 +179,10 @@ def complaint_detail(request, pk):
- Linked PX actions - Linked PX actions
- Workflow actions (assign, status change, add note) - 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 = get_object_or_404(
Complaint.objects.select_related( Complaint.objects.select_related(
"patient", "hospital", "department", "staff", "assigned_to", "resolved_by", "closed_by", "resolution_survey" "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, "status_choices": ComplaintStatus.choices,
"can_edit": user.is_px_admin() or user.is_hospital_admin(), "can_edit": user.is_px_admin() or user.is_hospital_admin(),
"hospital_departments": hospital_departments, "hospital_departments": hospital_departments,
'base_layout': base_layout,
'source_user': source_user,
"explanation": explanation, "explanation": explanation,
"explanations": explanations, "explanations": explanations,
"explanation_attachments": explanation_attachments, "explanation_attachments": explanation_attachments,
@ -256,6 +263,8 @@ def complaint_detail(request, pk):
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
def complaint_create(request): def complaint_create(request):
"""Create new complaint with AI-powered classification""" """Create new complaint with AI-powered classification"""
from apps.complaints.forms import ComplaintForm
# Determine base layout based on user type # Determine base layout based on user type
from apps.px_sources.models import SourceUser from apps.px_sources.models import SourceUser
source_user = SourceUser.objects.filter(user=request.user).first() source_user = SourceUser.objects.filter(user=request.user).first()
@ -263,47 +272,49 @@ def complaint_create(request):
if request.method == 'POST': if request.method == 'POST':
# Handle form submission # 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: 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 # Create complaint with AI defaults
complaint = Complaint.objects.create( complaint = form.save(commit=False)
patient_id=patient_id,
hospital_id=hospital_id, # Set AI-determined defaults
department_id=department_id if department_id else None, complaint.title = 'Complaint' # AI will generate title
staff_id=staff_id if staff_id else None, # category can be None, AI will determine it
title="Complaint", # AI will generate title complaint.subcategory = '' # AI will determine
description=description,
category=category, # Set source from logged-in source user
subcategory=subcategory_obj.code if subcategory_obj else "", if source_user and source_user.source:
priority="medium", # AI will update complaint.source = source_user.source
severity="medium", # AI will update else:
source=source, # Fallback: get or create a 'staff' source
encounter_id=encounter_id, 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 # Create initial update
ComplaintUpdate.objects.create( ComplaintUpdate.objects.create(
@ -313,7 +324,7 @@ def complaint_create(request):
created_by=request.user, 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 from apps.complaints.tasks import analyze_complaint_with_ai
analyze_complaint_with_ai.delay(str(complaint.id)) analyze_complaint_with_ai.delay(str(complaint.id))
@ -325,7 +336,7 @@ def complaint_create(request):
user=request.user, user=request.user,
content_object=complaint, content_object=complaint,
metadata={ metadata={
"category": category.name_en, # "category": category.name_en,
"severity": complaint.severity, "severity": complaint.severity,
"patient_mrn": complaint.patient.mrn, "patient_mrn": complaint.patient.mrn,
"ai_analysis_pending": True, "ai_analysis_pending": True,
@ -346,12 +357,16 @@ def complaint_create(request):
return redirect("complaints:complaint_create") return redirect("complaints:complaint_create")
# GET request - show form # GET request - show form
hospitals = Hospital.objects.filter(status="active") # Check for hospital parameter from URL (for pre-selection)
if not request.user.is_px_admin() and request.user.hospital: initial_data = {}
hospitals = hospitals.filter(id=request.user.hospital.id) hospital_id = request.GET.get('hospital')
if hospital_id:
initial_data['hospital'] = hospital_id
form = ComplaintForm(user=request.user, initial=initial_data)
context = { context = {
'hospitals': hospitals, 'form': form,
'base_layout': base_layout, 'base_layout': base_layout,
'source_user': source_user, 'source_user': source_user,
} }
@ -904,6 +919,10 @@ def inquiry_detail(request, pk):
- Attachments management - Attachments management
- Workflow actions (assign, status change, add note, respond) - 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 = get_object_or_404(
Inquiry.objects.select_related( Inquiry.objects.select_related(
"patient", "hospital", "department", "assigned_to", "responded_by" "patient", "hospital", "department", "assigned_to", "responded_by"
@ -947,6 +966,8 @@ def inquiry_detail(request, pk):
"assignable_users": assignable_users, "assignable_users": assignable_users,
"status_choices": status_choices, "status_choices": status_choices,
"can_edit": user.is_px_admin() or user.is_hospital_admin(), "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) return render(request, "complaints/inquiry_detail.html", context)
@ -957,46 +978,38 @@ def inquiry_detail(request, pk):
def inquiry_create(request): def inquiry_create(request):
"""Create new inquiry""" """Create new inquiry"""
from .models import Inquiry from .models import Inquiry
from .forms import InquiryForm
from apps.organizations.models import Patient from apps.organizations.models import Patient
from apps.px_sources.models import SourceUser, PXSource
# Determine base layout based on user type # Determine base layout based on user type
from apps.px_sources.models import SourceUser
source_user = SourceUser.objects.filter(user=request.user).first() source_user = SourceUser.objects.filter(user=request.user).first()
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html' base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
if request.method == 'POST': 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: try:
# Get form data # Save inquiry
patient_id = request.POST.get("patient_id", None) inquiry = form.save(commit=False)
hospital_id = request.POST.get("hospital_id")
department_id = request.POST.get("department_id", None)
subject = request.POST.get("subject") # Set source for source users
message = request.POST.get("message") source_user = SourceUser.objects.filter(user=request.user).first()
category = request.POST.get("category") if source_user:
inquiry.source = source_user.source
inquiry.created_by = request.user
# Contact info (if no patient) inquiry.save()
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,
)
# Log audit # Log audit
AuditService.log_event( AuditService.log_event(
@ -1015,12 +1028,13 @@ def inquiry_create(request):
return redirect("complaints:inquiry_create") return redirect("complaints:inquiry_create")
# GET request - show form # 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: if not request.user.is_px_admin() and request.user.hospital:
hospitals = hospitals.filter(id=request.user.hospital.id) hospitals = hospitals.filter(id=request.user.hospital.id)
context = { context = {
'hospitals': hospitals, 'form': form,
'base_layout': base_layout, 'base_layout': base_layout,
'source_user': source_user, 'source_user': source_user,
} }

View File

@ -1,4 +1,4 @@
{% extends "layouts/base.html" %} {% extends base_layout %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load math %} {% load math %}
@ -119,9 +119,15 @@
<div class="container-fluid"> <div class="container-fluid">
<!-- Back Button --> <!-- Back Button -->
<div class="mb-3"> <div class="mb-3">
{% if source_user %}
<a href="{% url 'px_sources:source_user_complaint_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i> {{ _("Back to My Complaints")}}
</a>
{% else %}
<a href="{% url 'complaints:complaint_list' %}" class="btn btn-outline-secondary btn-sm"> <a href="{% url 'complaints:complaint_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to Complaints" %} <i class="bi bi-arrow-left me-1"></i> {% trans "Back to Complaints" %}
</a> </a>
{% endif %}
</div> </div>
<!-- Complaint Header --> <!-- Complaint Header -->

View File

@ -48,7 +48,7 @@
<p class="text-muted mb-0">{{ _("File a new patient complaint with SLA tracking")}}</p> <p class="text-muted mb-0">{{ _("File a new patient complaint with SLA tracking")}}</p>
</div> </div>
<form method="post" action="{% url 'complaints:complaint_create' %}" id="complaintForm"> <form method="post" action="{% url 'complaints:complaint_create' %}" id="complaintForm" novalidate>
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
@ -61,17 +61,24 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label required-field">{% trans "Patient" %}</label> <label for="{{ form.patient.id_for_label }}" class="form-label required-field">
<select name="patient_id" class="form-select" id="patientSelect" required> {{ form.patient.label }}
<option value="">{{ _("Search and select patient")}}</option> </label>
</select> {{ form.patient }}
<small class="form-text text-muted">{{ _("Search by MRN or name")}}</small> {% if form.patient.errors %}
<div class="invalid-feedback d-block">
{% for error in form.patient.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">{% trans "Encounter ID" %}</label> <label for="{{ form.encounter_id.id_for_label }}" class="form-label">
<input type="text" name="encounter_id" class="form-control" {{ form.encounter_id.label }}
placeholder="{% trans 'Optional encounter/visit ID' %}"> </label>
{{ form.encounter_id }}
</div> </div>
</div> </div>
</div> </div>
@ -84,36 +91,51 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label required-field">{% trans "Hospital" %}</label> <label for="{{ form.hospital.id_for_label }}" class="form-label required-field">
<select name="hospital_id" class="form-select" id="hospitalSelect" required> {{ form.hospital.label }}
<option value="">{{ _("Select hospital")}}</option> </label>
{% for hospital in hospitals %} {{ form.hospital }}
<option value="{{ hospital.id }}">{{ hospital.name_en }}</option> {% if form.hospital.errors %}
<div class="invalid-feedback d-block">
{% for error in form.hospital.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %} {% endfor %}
</select> </div>
{% endif %}
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">{% trans "Department" %}</label> <label for="{{ form.department.id_for_label }}" class="form-label">
<select name="department_id" class="form-select" id="departmentSelect"> {{ form.department.label }}
<option value="">{{ _("Select department")}}</option> </label>
</select> {{ form.department }}
{% if form.department.errors %}
<div class="invalid-feedback d-block">
{% for error in form.department.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">{% trans "Staff" %}</label> <label for="{{ form.staff.id_for_label }}" class="form-label">
<select name="staff_id" class="form-select" id="staffSelect"> {{ form.staff.label }}
<option value="">{{ _("Select staff")}}</option> </label>
</select> {{ form.staff }}
{% if form.staff.errors %}
<div class="invalid-feedback d-block">
{% for error in form.staff.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- Classification Section -->
<!-- Complaint Details --> <!-- Complaint Details -->
<div class="form-section"> <div class="form-section">
<h5 class="form-section-title"> <h5 class="form-section-title">
@ -121,9 +143,17 @@
</h5> </h5>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required-field">{% trans "Description" %}</label> <label for="{{ form.description.id_for_label }}" class="form-label required-field">
<textarea name="description" class="form-control" rows="6" {{ form.description.label }}
placeholder="{% trans 'Detailed description of the complaint...' %}" required></textarea> </label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
<small class="text-danger">{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -133,7 +163,23 @@
<!-- AI Information --> <!-- AI Information -->
<div class="alert alert-info"> <div class="alert alert-info">
<h6 class="alert-heading"> <h6 class="alert-heading">
<i class="bi bi-info-circle me-2"></i>{{ _("SLA Information")}} <i class="bi bi-info-circle me-2"></i>{{ _("AI Classification")}}
</h6>
<p class="mb-0 small">
{{ _("AI will automatically analyze and classify your complaint:")}}
</p>
<ul class="mb-0 mt-2 small">
<li><strong>{{ _("Title") }}:</strong> {{ _("AI-generated title")}}</li>
<li><strong>{{ _("Category") }}:</strong> {{ _("AI-determined category")}}</li>
<li><strong>{{ _("Severity") }}:</strong> {{ _("AI-calculated severity")}}</li>
<li><strong>{{ _("Priority") }}:</strong> {{ _("AI-calculated priority")}}</li>
</ul>
</div>
<!-- SLA Information -->
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-clock me-2"></i>{{ _("SLA Information")}}
</h6> </h6>
<p class="mb-0 small"> <p class="mb-0 small">
{{ _("SLA deadline will be automatically calculated based on severity")}}: {{ _("SLA deadline will be automatically calculated based on severity")}}:
@ -173,81 +219,37 @@ document.addEventListener('DOMContentLoaded', function() {
const hospitalSelect = document.getElementById('hospitalSelect'); const hospitalSelect = document.getElementById('hospitalSelect');
const departmentSelect = document.getElementById('departmentSelect'); const departmentSelect = document.getElementById('departmentSelect');
const staffSelect = document.getElementById('staffSelect'); const staffSelect = document.getElementById('staffSelect');
const categorySelect = document.getElementById('categorySelect');
const subcategorySelect = document.getElementById('subcategorySelect');
const patientSelect = document.getElementById('patientSelect');
// Get current language // Hospital change handler - reload form with selected hospital
const currentLang = document.documentElement.lang || 'en'; if (hospitalSelect) {
// Hospital change handler
hospitalSelect.addEventListener('change', function() { hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value; const hospitalId = this.value;
const form = document.getElementById('complaintForm');
const actionUrl = form.action;
// Clear dependent dropdowns // Create URL with hospital_id parameter
departmentSelect.innerHTML = '<option value="">Select hospital first...</option>'; const url = new URL(actionUrl, window.location);
staffSelect.innerHTML = '<option value="">Select department first...</option>'; url.searchParams.set('hospital', hospitalId);
categorySelect.innerHTML = '<option value="">Loading categories...</option>';
subcategorySelect.innerHTML = '<option value="">Select category first...</option>';
if (hospitalId) { // Reload form with selected hospital
// Load departments window.location.href = url.toString();
fetch(`/api/organizations/departments/?hospital=${hospitalId}`)
.then(response => response.json())
.then(data => {
departmentSelect.innerHTML = '<option value="">Select department...</option>';
data.results.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
const deptName = currentLang === 'ar' && dept.name_ar ? dept.name_ar : dept.name_en;
option.textContent = deptName;
departmentSelect.appendChild(option);
}); });
})
.catch(error => {
console.error('Error loading departments:', error);
departmentSelect.innerHTML = '<option value="">Error loading departments</option>';
});
// Load categories (using public API endpoint)
fetch(`/complaints/public/api/load-categories/?hospital_id=${hospitalId}`)
.then(response => response.json())
.then(data => {
categorySelect.innerHTML = '<option value="">Select category...</option>';
data.categories.forEach(cat => {
// Only show parent categories (no parent_id)
if (!cat.parent_id) {
const option = document.createElement('option');
option.value = cat.id;
option.dataset.code = cat.code;
const catName = currentLang === 'ar' && cat.name_ar ? cat.name_ar : cat.name_en;
option.textContent = catName;
categorySelect.appendChild(option);
} }
});
})
.catch(error => {
console.error('Error loading categories:', error);
categorySelect.innerHTML = '<option value="">Error loading categories</option>';
});
} else {
categorySelect.innerHTML = '<option value="">Select hospital first...</option>';
}
});
// Department change handler - load staff // Department change handler - load staff
if (departmentSelect) {
departmentSelect.addEventListener('change', function() { departmentSelect.addEventListener('change', function() {
const departmentId = this.value; const departmentId = this.value;
// Clear staff dropdown // Clear staff dropdown
staffSelect.innerHTML = '<option value="">Select department first...</option>'; staffSelect.innerHTML = '<option value="">{{ _("Select staff")}}</option>';
if (departmentId) { if (departmentId) {
// Load staff (filtered by department) // Load staff via minimal AJAX
fetch(`/complaints/ajax/get-staff-by-department/?department_id=${departmentId}`) fetch(`/complaints/ajax/physicians/?department_id=${departmentId}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
staffSelect.innerHTML = '<option value="">Select staff...</option>'; staffSelect.innerHTML = '<option value="">{{ _("Select staff")}}</option>';
data.staff.forEach(staff => { data.staff.forEach(staff => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = staff.id; option.value = staff.id;
@ -257,73 +259,9 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.catch(error => { .catch(error => {
console.error('Error loading staff:', error); console.error('Error loading staff:', error);
staffSelect.innerHTML = '<option value="">Error loading staff</option>';
}); });
} }
}); });
// Category change handler - load subcategories
categorySelect.addEventListener('change', function() {
const categoryId = this.value;
// Clear subcategory dropdown
subcategorySelect.innerHTML = '<option value="">Select category first...</option>';
if (categoryId) {
// Load categories again and filter for subcategories of selected parent
const hospitalId = hospitalSelect.value;
if (hospitalId) {
fetch(`/complaints/public/api/load-categories/?hospital_id=${hospitalId}`)
.then(response => response.json())
.then(data => {
subcategorySelect.innerHTML = '<option value="">Select subcategory...</option>';
data.categories.forEach(cat => {
// Only show subcategories (has parent_id matching selected category)
if (cat.parent_id == categoryId) {
const option = document.createElement('option');
option.value = cat.id;
option.dataset.code = cat.code;
const catName = currentLang === 'ar' && cat.name_ar ? cat.name_ar : cat.name_en;
option.textContent = catName;
subcategorySelect.appendChild(option);
}
});
if (subcategorySelect.options.length <= 1) {
subcategorySelect.innerHTML = '<option value="">No subcategories available</option>';
}
})
.catch(error => {
console.error('Error loading subcategories:', error);
subcategorySelect.innerHTML = '<option value="">Error loading subcategories</option>';
});
}
}
});
// Patient search
patientSelect.addEventListener('focus', function() {
if (this.options.length === 1) {
loadPatients('');
}
});
function loadPatients(searchTerm) {
const url = searchTerm
? `/api/organizations/patients/?search=${encodeURIComponent(searchTerm)}`
: '/api/organizations/patients/?page_size=50';
fetch(url)
.then(response => response.json())
.then(data => {
patientSelect.innerHTML = '<option value="">Search and select patient...</option>';
data.results.forEach(patient => {
const option = document.createElement('option');
option.value = patient.id;
option.textContent = `${patient.first_name} ${patient.last_name} (MRN: ${patient.mrn})`;
patientSelect.appendChild(option);
});
})
.catch(error => console.error('Error loading patients:', error));
} }
// Form validation // Form validation

View File

@ -1,4 +1,4 @@
{% extends "layouts/base.html" %} {% extends base_layout %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
@ -103,9 +103,15 @@
<div class="container-fluid"> <div class="container-fluid">
<!-- Back Button --> <!-- Back Button -->
<div class="mb-3"> <div class="mb-3">
{% if source_user %}
<a href="{% url 'px_sources:source_user_inquiry_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i> {{ _("Back to My Inquiries")}}
</a>
{% else %}
<a href="{% url 'complaints:inquiry_list' %}" class="btn btn-outline-secondary btn-sm"> <a href="{% url 'complaints:inquiry_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i> {{ _("Back to Inquiries")}} <i class="bi bi-arrow-left me-1"></i> {{ _("Back to Inquiries")}}
</a> </a>
{% endif %}
</div> </div>
<!-- Inquiry Header --> <!-- Inquiry Header -->

View File

@ -21,10 +21,6 @@
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 2px solid #17a2b8; border-bottom: 2px solid #17a2b8;
} }
.required-field::after {
content: " *";
color: #dc3545;
}
</style> </style>
{% endblock %} {% endblock %}
@ -51,6 +47,12 @@
<form method="post" action="{% url 'complaints:inquiry_create' %}" id="inquiryForm"> <form method="post" action="{% url 'complaints:inquiry_create' %}" id="inquiryForm">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<!-- Organization Information --> <!-- Organization Information -->
@ -59,23 +61,23 @@
<i class="bi bi-hospital me-2"></i>{{ _("Organization") }} <i class="bi bi-hospital me-2"></i>{{ _("Organization") }}
</h5> </h5>
<div class="row"> <div class="mb-3">
<div class="col-md-6 mb-3"> {{ form.hospital.label_tag }}
<label class="form-label required-field">{% trans "Hospital" %}</label> {{ form.hospital }}
<select name="hospital_id" class="form-select" id="hospital-select" required> {% if form.hospital.help_text %}
<option value="">{{ _("Select hospital")}}</option> <small class="form-text text-muted">{{ form.hospital.help_text }}</small>
{% for hospital in hospitals %} {% endif %}
<option value="{{ hospital.id }}">{{ hospital.name_en }}</option> {% for error in form.hospital.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %} {% endfor %}
</select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="mb-3">
<label class="form-label">{% trans "Department" %}</label> {{ form.department.label_tag }}
<select name="department_id" class="form-select" id="department-select"> {{ form.department }}
<option value="">{{ _("Select department")}}</option> {% for error in form.department.errors %}
</select> <div class="invalid-feedback d-block">{{ error }}</div>
</div> {% endfor %}
</div> </div>
</div> </div>
@ -85,37 +87,37 @@
<i class="bi bi-person-fill me-2"></i>{{ _("Contact Information")}} <i class="bi bi-person-fill me-2"></i>{{ _("Contact Information")}}
</h5> </h5>
<!-- Patient Search --> <!-- Patient Field -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{% trans "Patient" %} ({% trans "Optional" %})</label> {{ form.patient.label_tag }}
<input type="text" class="form-control" id="patient-search" {{ form.patient }}
placeholder="{% trans 'Search by MRN or name...' %}"> {% for error in form.patient.errors %}
<input type="hidden" name="patient_id" id="patient-id"> <div class="invalid-feedback d-block">{{ error }}</div>
<div id="patient-results" class="list-group mt-2" style="display: none;"></div> {% endfor %}
<div id="selected-patient" class="alert alert-info mt-2" style="display: none;"></div>
</div> </div>
<div class="text-muted mb-3">
<small>{{ _("OR") }}</small>
</div>
<!-- Contact Information (if no patient) -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{% trans "Contact Name" %}</label> {{ form.contact_name.label_tag }}
<input type="text" name="contact_name" class="form-control" {{ form.contact_name }}
placeholder="{% trans 'Name of contact person' %}"> {% for error in form.contact_name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">{% trans "Contact Phone" %}</label> {{ form.contact_phone.label_tag }}
<input type="tel" name="contact_phone" class="form-control" {{ form.contact_phone }}
placeholder="{% trans 'Phone number' %}"> {% for error in form.contact_phone.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">{% trans "Contact Email" %}</label> {{ form.contact_email.label_tag }}
<input type="email" name="contact_email" class="form-control" {{ form.contact_email }}
placeholder="{% trans 'Email address' %}"> {% for error in form.contact_email.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
@ -127,45 +129,33 @@
</h5> </h5>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required-field">{% trans "Category" %}</label> {{ form.category.label_tag }}
<select name="category" class="form-select" required> {{ form.category }}
<option value="">{{ _("Select category")}}</option> {% for error in form.category.errors %}
<option value="appointment">{{ _("Appointment")}}</option> <div class="invalid-feedback d-block">{{ error }}</div>
<option value="billing">{{ _("Billing")}}</option> {% endfor %}
<option value="medical_records">{{ _("Medical Records")}}</option>
<option value="pharmacy">{{ _("Pharmacy")}}</option>
<option value="insurance">{{ _("Insurance")}}</option>
<option value="feedback">{{ _("Feedback")}}</option>
<option value="general">{{ _("General Information")}}</option>
<option value="other">{{ _("Other")}}</option>
</select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required-field">{% trans "Subject" %}</label> {{ form.subject.label_tag }}
<input type="text" name="subject" class="form-control" required maxlength="500" {{ form.subject }}
placeholder="{% trans 'Brief summary of the inquiry' %}"> {% for error in form.subject.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required-field">{% trans "Message" %}</label> {{ form.message.label_tag }}
<textarea name="message" class="form-control" rows="6" required {{ form.message }}
placeholder="{% trans 'Detailed description of the inquiry...' %}"></textarea> {% for error in form.message.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
<!-- Sidebar --> <!-- Sidebar -->
<div class="col-lg-4"> <div class="col-lg-4">
<!-- Due Date -->
<div class="mb-3">
<label class="form-label">{% trans "Due Date" %}</label>
<input type="datetime-local" name="due_date" class="form-control"
placeholder="{% trans 'Optional due date' %}">
<small class="form-text text-muted">
{{ _("Leave empty for default")}}
</small>
</div>
<!-- Help Information --> <!-- Help Information -->
<div class="alert alert-info"> <div class="alert alert-info">
@ -177,10 +167,7 @@
</p> </p>
<hr class="my-2"> <hr class="my-2">
<p class="mb-0 small"> <p class="mb-0 small">
{{ _("If the inquiry is from a registered patient, search and select them. Otherwise, provide contact information.")}} {{ _("Fill in the inquiry details. Fields marked with * are required.")}}
</p>
<p class="mb-0 small mt-2 text-muted">
{{ _("Fields marked with * are required.")}}
</p> </p>
</div> </div>
@ -208,9 +195,9 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
// Department loading // Department loading
document.getElementById('hospital-select')?.addEventListener('change', function() { document.getElementById('{{ form.hospital.id_for_label }}')?.addEventListener('change', function() {
const hospitalId = this.value; const hospitalId = this.value;
const departmentSelect = document.getElementById('department-select'); const departmentSelect = document.getElementById('{{ form.department.id_for_label }}');
if (!hospitalId) { if (!hospitalId) {
departmentSelect.innerHTML = '<option value="">{{ _("Select department")}}</option>'; departmentSelect.innerHTML = '<option value="">{{ _("Select department")}}</option>';
@ -231,77 +218,15 @@ document.getElementById('hospital-select')?.addEventListener('change', function(
.catch(error => console.error('Error loading departments:', error)); .catch(error => console.error('Error loading departments:', error));
}); });
// Patient search with debounce // Patient search (optional - for better UX)
let searchTimeout; const patientSelect = document.getElementById('{{ form.patient.id_for_label }}');
document.getElementById('patient-search')?.addEventListener('input', function() { if (patientSelect) {
const query = this.value; patientSelect.addEventListener('change', function() {
const resultsDiv = document.getElementById('patient-results'); const selectedOption = this.options[this.selectedIndex];
if (!selectedOption || selectedOption.value === '') {
clearTimeout(searchTimeout);
if (query.length < 2) {
resultsDiv.style.display = 'none';
return;
}
searchTimeout = setTimeout(() => {
fetch(`/complaints/ajax/search-patients/?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
if (data.patients.length === 0) {
resultsDiv.innerHTML = `<div class="list-group-item">{{ _("No patients found")}}</div>`;
resultsDiv.style.display = 'block';
return;
}
resultsDiv.innerHTML = '';
data.patients.forEach(patient => {
const item = document.createElement('a');
item.href = '#';
item.className = 'list-group-item list-group-item-action';
item.innerHTML = `
<strong>${patient.name}</strong><br>
<small>{{ _("MRN") }}: ${patient.mrn} | ${patient.phone || ''} | ${patient.email || ''}</small>
`;
item.addEventListener('click', function(e) {
e.preventDefault();
selectPatient(patient);
});
resultsDiv.appendChild(item);
});
resultsDiv.style.display = 'block';
})
.catch(error => console.error('Error searching patients:', error));
}, 300);
});
function selectPatient(patient) {
document.getElementById('patient-id').value = patient.id;
document.getElementById('patient-search').value = '';
document.getElementById('patient-results').style.display = 'none'; document.getElementById('patient-results').style.display = 'none';
const selectedDiv = document.getElementById('selected-patient');
selectedDiv.innerHTML = `
<strong>{{ _("Selected Patient") }}:</strong> ${patient.name}<br>
<small>{{ _("MRN") }}: ${patient.mrn}</small>
<button type="button" class="btn btn-sm btn-link" onclick="clearPatient()">{{ _("Clear") }}</button>
`;
selectedDiv.style.display = 'block';
} }
function clearPatient() {
document.getElementById('patient-id').value = '';
document.getElementById('selected-patient').style.display = 'none';
}
// Form validation
const form = document.getElementById('inquiryForm');
form?.addEventListener('submit', function(e) {
if (!form.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
form.classList.add('was-validated');
}); });
}
</script> </script>
{% endblock %} {% endblock %}