update the complaint and inquiry creation for the source user
This commit is contained in:
parent
90dd2a66af
commit
8b65f9a52e
106
apps/complaints/COMPLAINT_FORM_DJANGO_FORM_IMPLEMENTATION.md
Normal file
106
apps/complaints/COMPLAINT_FORM_DJANGO_FORM_IMPLEMENTATION.md
Normal 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
|
||||||
@ -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"""
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 -->
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 -->
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user