more changes

This commit is contained in:
ismail 2026-02-25 04:47:05 +03:00
parent d07cb052f3
commit b3d9bd17cb
61 changed files with 14164 additions and 3096 deletions

5001
CALLS..csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -160,6 +160,18 @@ class KPIReport(UUIDModel, TimeStampedModel):
# Error tracking # Error tracking
error_message = models.TextField(blank=True) error_message = models.TextField(blank=True)
# AI-generated analysis
ai_analysis = models.JSONField(
null=True,
blank=True,
help_text=_("AI-generated analysis and recommendations for this report")
)
ai_analysis_generated_at = models.DateTimeField(
null=True,
blank=True,
help_text=_("When the AI analysis was generated")
)
class Meta: class Meta:
ordering = ["-year", "-month", "report_type"] ordering = ["-year", "-month", "report_type"]
unique_together = [["report_type", "hospital", "year", "month"]] unique_together = [["report_type", "hospital", "year", "month"]]

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,17 @@ Views for listing, viewing, and generating KPI reports.
Follows the PX360 UI patterns with Tailwind, Lucide icons, and HTMX. Follows the PX360 UI patterns with Tailwind, Lucide icons, and HTMX.
""" """
import json import json
import logging
from datetime import datetime from datetime import datetime
from django.contrib import messages from django.contrib import messages
logger = logging.getLogger(__name__)
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
@ -21,6 +25,7 @@ from .kpi_models import KPIReport, KPIReportStatus, KPIReportType
from .kpi_service import KPICalculationService from .kpi_service import KPICalculationService
@login_required @login_required
def kpi_report_list(request): def kpi_report_list(request):
""" """
@ -442,3 +447,129 @@ def kpi_report_api_data(request, report_id):
} }
return JsonResponse(data) return JsonResponse(data)
@login_required
def kpi_report_ai_analysis(request, report_id):
"""
Generate or retrieve AI analysis for a KPI report.
GET: Retrieve existing AI analysis
POST: Generate new AI analysis
"""
from django.http import JsonResponse
from .kpi_service import KPICalculationService
user = request.user
report = get_object_or_404(
KPIReport.objects.select_related('hospital', 'generated_by'),
id=report_id
)
# Check permissions
if not user.is_px_admin() and user.hospital != report.hospital:
return JsonResponse({'error': 'Permission denied'}, status=403)
if request.method == 'GET':
# Return existing analysis
if report.ai_analysis:
return JsonResponse({
'success': True,
'analysis': report.ai_analysis,
'generated_at': report.ai_analysis_generated_at.isoformat() if report.ai_analysis_generated_at else None
})
else:
return JsonResponse({
'success': False,
'message': 'No AI analysis available. Use POST to generate.'
}, status=404)
elif request.method == 'POST':
# Generate new analysis
try:
analysis = KPICalculationService.generate_ai_analysis(report)
if 'error' in analysis:
return JsonResponse({
'success': False,
'error': analysis['error']
}, status=500)
return JsonResponse({
'success': True,
'analysis': analysis,
'generated_at': report.ai_analysis_generated_at.isoformat() if report.ai_analysis_generated_at else None
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
return JsonResponse({'error': 'Method not allowed'}, status=405)
@login_required
def kpi_report_save_analysis(request, report_id):
"""
Save edited AI analysis for a KPI report.
POST: Save edited analysis JSON
"""
import json
from django.http import JsonResponse
user = request.user
report = get_object_or_404(KPIReport, id=report_id)
# Check permissions - only PX admins and hospital admins can edit
if not user.is_px_admin() and user.hospital != report.hospital:
return JsonResponse({'error': 'Permission denied'}, status=403)
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
# Parse the edited analysis from request body
body = json.loads(request.body)
edited_analysis = body.get('analysis')
if not edited_analysis:
return JsonResponse({'error': 'No analysis data provided'}, status=400)
# Preserve metadata if it exists
if report.ai_analysis and '_metadata' in report.ai_analysis:
edited_analysis['_metadata'] = report.ai_analysis['_metadata']
edited_analysis['_metadata']['last_edited_at'] = timezone.now().isoformat()
edited_analysis['_metadata']['last_edited_by'] = user.get_full_name() or user.email
else:
edited_analysis['_metadata'] = {
'generated_at': timezone.now().isoformat(),
'report_id': str(report.id),
'report_type': report.report_type,
'hospital': report.hospital.name,
'year': report.year,
'month': report.month,
'last_edited_at': timezone.now().isoformat(),
'last_edited_by': user.get_full_name() or user.email
}
# Save to report
report.ai_analysis = edited_analysis
report.save(update_fields=['ai_analysis'])
logger.info(f"AI analysis edited for KPI report {report.id} by user {user.id}")
return JsonResponse({
'success': True,
'message': 'Analysis saved successfully'
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON data'}, status=400)
except Exception as e:
logger.exception(f"Error saving AI analysis for report {report.id}: {e}")
return JsonResponse({'error': str(e)}, status=500)

View File

@ -21,4 +21,6 @@ urlpatterns = [
path('kpi-reports/<uuid:report_id>/pdf/', kpi_views.kpi_report_pdf, name='kpi_report_pdf'), path('kpi-reports/<uuid:report_id>/pdf/', kpi_views.kpi_report_pdf, name='kpi_report_pdf'),
path('kpi-reports/<uuid:report_id>/regenerate/', kpi_views.kpi_report_regenerate, name='kpi_report_regenerate'), path('kpi-reports/<uuid:report_id>/regenerate/', kpi_views.kpi_report_regenerate, name='kpi_report_regenerate'),
path('api/kpi-reports/<uuid:report_id>/data/', kpi_views.kpi_report_api_data, name='kpi_report_api_data'), path('api/kpi-reports/<uuid:report_id>/data/', kpi_views.kpi_report_api_data, name='kpi_report_api_data'),
path('api/kpi-reports/<uuid:report_id>/ai-analysis/', kpi_views.kpi_report_ai_analysis, name='kpi_report_ai_analysis'),
path('api/kpi-reports/<uuid:report_id>/ai-analysis/save/', kpi_views.kpi_report_save_analysis, name='kpi_report_save_analysis'),
] ]

View File

@ -132,3 +132,98 @@ class CallCenterInteraction(UUIDModel, TimeStampedModel):
if self.satisfaction_rating and self.satisfaction_rating < 3: if self.satisfaction_rating and self.satisfaction_rating < 3:
self.is_low_rating = True self.is_low_rating = True
super().save(*args, **kwargs) super().save(*args, **kwargs)
class CallRecord(UUIDModel, TimeStampedModel):
"""
Call Record - Tracks call center recordings imported from CSV.
Stores all call metadata from the call recording system including:
- Call details (start, end, duration)
- Caller information
- Department and extension
- Recording file information
- Inbound/Outbound call details
"""
# Core identifiers
media_id = models.UUIDField(unique=True, db_index=True, help_text="Unique media ID from recording system")
media_type = models.CharField(max_length=50, default="Calls", help_text="Type of media (e.g., Calls)")
chain = models.CharField(max_length=255, blank=True, help_text="Chain identifier")
evaluated = models.BooleanField(default=False, help_text="Whether the call has been evaluated")
# Call timing
call_start = models.DateTimeField(help_text="Call start time")
call_end = models.DateTimeField(null=True, blank=True, help_text="Call end time")
call_length = models.CharField(max_length=20, blank=True, help_text="Call length as HH:MM:SS")
call_duration_seconds = models.IntegerField(null=True, blank=True, help_text="Call duration in seconds")
# Caller information
first_name = models.CharField(max_length=100, blank=True, help_text="Caller first name")
last_name = models.CharField(max_length=100, blank=True, help_text="Caller last name")
# Department information
extension = models.CharField(max_length=20, blank=True, help_text="Extension number")
department = models.CharField(max_length=255, blank=True, help_text="Department name")
location = models.CharField(max_length=255, blank=True, help_text="Location")
# Inbound call details
inbound_id = models.CharField(max_length=50, blank=True, help_text="Inbound call ID")
inbound_name = models.CharField(max_length=255, blank=True, help_text="Inbound caller name/number")
dnis = models.CharField(max_length=50, blank=True, help_text="Dialed Number Identification Service")
# Outbound call details
outbound_id = models.CharField(max_length=50, blank=True, help_text="Outbound call ID")
outbound_name = models.CharField(max_length=255, blank=True, help_text="Outbound caller name/number")
# Flag information
flag_name = models.CharField(max_length=100, blank=True, help_text="Flag name")
flag_value = models.CharField(max_length=100, blank=True, help_text="Flag value")
# Recording file information
file_location = models.CharField(max_length=500, blank=True, help_text="File system location")
file_name = models.CharField(max_length=500, blank=True, help_text="Recording file name")
file_hash = models.CharField(max_length=64, blank=True, help_text="File hash for integrity")
# Additional metadata
external_ref = models.CharField(max_length=100, blank=True, help_text="External reference number")
transfer_from = models.CharField(max_length=255, blank=True, help_text="Transfer source")
recorded_by = models.CharField(max_length=255, blank=True, help_text="Recording system/user")
time_zone = models.CharField(max_length=50, default="03:00:00", help_text="Time zone offset")
recording_server_name = models.CharField(max_length=100, blank=True, help_text="Recording server name")
# Hospital link (for multi-hospital systems)
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='call_records'
)
class Meta:
ordering = ['-call_start']
indexes = [
models.Index(fields=['-call_start']),
models.Index(fields=['media_id']),
models.Index(fields=['department']),
models.Index(fields=['evaluated']),
models.Index(fields=['hospital', '-call_start']),
]
def __str__(self):
return f"{self.first_name} {self.last_name} - {self.call_start.strftime('%Y-%m-%d %H:%M')}"
@property
def caller_full_name(self):
"""Get full caller name"""
return f"{self.first_name} {self.last_name}".strip()
@property
def is_inbound(self):
"""Check if call is inbound"""
return bool(self.inbound_id or self.inbound_name)
@property
def is_outbound(self):
"""Check if call is outbound"""
return bool(self.outbound_id or self.outbound_name)

View File

@ -9,13 +9,14 @@ from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone from django.utils import timezone
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from uuid import UUID
from apps.complaints.models import Complaint, Inquiry from apps.complaints.models import Complaint, Inquiry
from apps.px_sources.models import PXSource from apps.px_sources.models import PXSource
from apps.core.services import AuditService from apps.core.services import AuditService
from apps.organizations.models import Department, Hospital, Patient, Staff from apps.organizations.models import Department, Hospital, Patient, Staff
from .models import CallCenterInteraction from .models import CallCenterInteraction, CallRecord
@login_required @login_required
@ -586,4 +587,367 @@ def search_patients(request):
] ]
return JsonResponse({'patients': results}) return JsonResponse({'patients': results})
# ============================================================================
# CALL RECORDS (CSV IMPORT) VIEWS
# ============================================================================
@login_required
def call_records_list(request):
"""
Call records list view with stats cards.
Shows all imported call records with filtering and search.
"""
queryset = CallRecord.objects.select_related('hospital')
# Apply RBAC filters
user = request.user
if user.is_px_admin():
pass
elif user.hospital:
queryset = queryset.filter(hospital=user.hospital)
else:
queryset = queryset.none()
# Apply filters
evaluated_filter = request.GET.get('evaluated')
if evaluated_filter:
queryset = queryset.filter(evaluated=evaluated_filter == 'true')
call_type_filter = request.GET.get('call_type')
if call_type_filter == 'inbound':
queryset = queryset.filter(Q(inbound_id__isnull=False) | Q(inbound_name__isnull=False))
queryset = queryset.exclude(Q(inbound_id='') & Q(inbound_name=''))
elif call_type_filter == 'outbound':
queryset = queryset.filter(Q(outbound_id__isnull=False) | Q(outbound_name__isnull=False))
queryset = queryset.exclude(Q(outbound_id='') & Q(outbound_name=''))
department_filter = request.GET.get('department')
if department_filter:
queryset = queryset.filter(department__icontains=department_filter)
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) |
Q(department__icontains=search_query) |
Q(extension__icontains=search_query) |
Q(inbound_name__icontains=search_query) |
Q(outbound_name__icontains=search_query)
)
# Date range
date_from = request.GET.get('date_from')
if date_from:
queryset = queryset.filter(call_start__gte=date_from)
date_to = request.GET.get('date_to')
if date_to:
queryset = queryset.filter(call_start__lte=date_to)
# Ordering
queryset = queryset.order_by('-call_start')
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
# Statistics for cards
stats = {
'total_calls': queryset.count(),
'total_duration': sum(
(r.call_duration_seconds or 0) for r in queryset
),
'inbound_calls': queryset.filter(
Q(inbound_id__isnull=False) | Q(inbound_name__isnull=False)
).exclude(Q(inbound_id='') & Q(inbound_name='')).count(),
'outbound_calls': queryset.filter(
Q(outbound_id__isnull=False) | Q(outbound_name__isnull=False)
).exclude(Q(outbound_id='') & Q(outbound_name='')).count(),
'evaluated_calls': queryset.filter(evaluated=True).count(),
'not_evaluated_calls': queryset.filter(evaluated=False).count(),
'avg_duration': queryset.filter(
call_duration_seconds__isnull=False
).aggregate(avg=Avg('call_duration_seconds'))['avg'] or 0,
}
# Format duration for display
def format_duration(seconds):
if not seconds:
return "0:00"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
if hours > 0:
return f"{hours}:{minutes:02d}:{secs:02d}"
return f"{minutes}:{secs:02d}"
stats['total_duration_formatted'] = format_duration(stats['total_duration'])
stats['avg_duration_formatted'] = format_duration(stats['avg_duration'])
# Get unique departments for filter dropdown
departments = CallRecord.objects.values_list('department', flat=True).distinct()
departments = [d for d in departments if d]
context = {
'page_obj': page_obj,
'call_records': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'filters': request.GET,
}
return render(request, 'callcenter/call_records_list.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def import_call_records(request):
"""
Import call records from CSV file.
CSV must have the same headers as the export format.
"""
if request.method == 'POST':
try:
csv_file = request.FILES.get('csv_file')
if not csv_file:
messages.error(request, "Please select a CSV file to upload.")
return redirect('callcenter:import_call_records')
# Check file extension
if not csv_file.name.endswith('.csv'):
messages.error(request, "Please upload a valid CSV file.")
return redirect('callcenter:import_call_records')
import csv
from datetime import datetime
import hashlib
import codecs
# Decode the file and remove BOM if present
decoded_file = csv_file.read().decode('utf-8-sig') # utf-8-sig removes BOM
reader = csv.DictReader(decoded_file.splitlines())
# Required headers
required_headers = [
'Media ID', 'Media Type', 'Call Start', 'First Name', 'Last Name',
'Extension', 'Department', 'Call End', 'Length', 'File Name'
]
# Validate headers
if reader.fieldnames is None:
messages.error(request, "Invalid CSV file format.")
return redirect('callcenter:import_call_records')
# Clean headers (remove any remaining BOM or whitespace)
cleaned_fieldnames = [f.strip() if f else f for f in reader.fieldnames]
reader.fieldnames = cleaned_fieldnames
missing_headers = [h for h in required_headers if h not in reader.fieldnames]
if missing_headers:
messages.error(request, f"Missing required headers: {', '.join(missing_headers)}")
return redirect('callcenter:import_call_records')
# Parse CSV and create records
imported_count = 0
skipped_count = 0
error_count = 0
for row_num, row in enumerate(reader, start=2): # Start at 2 (header is row 1)
try:
# Parse Media ID
media_id_str = row.get('Media ID', '').strip()
if not media_id_str:
skipped_count += 1
continue
try:
media_id = UUID(media_id_str)
except ValueError:
skipped_count += 1
continue
# Check if record already exists
if CallRecord.objects.filter(media_id=media_id).exists():
skipped_count += 1
continue
# Parse call start time
call_start_str = row.get('Call Start', '').strip()
if not call_start_str:
skipped_count += 1
continue
# Try multiple datetime formats
call_start = None
for fmt in [
'%m/%d/%y %H:%M', # 10/30/25 19:57
'%m/%d/%Y %H:%M', # 10/30/2025 19:57
'%m/%d/%y %I:%M:%S %p', # 10/30/25 7:57:48 PM
'%m/%d/%Y %I:%M:%S %p', # 10/30/2025 7:57:48 PM
'%m/%d/%y %I:%M %p', # 10/30/25 7:57 PM
'%m/%d/%Y %I:%M %p', # 10/30/2025 7:57 PM
]:
try:
call_start = datetime.strptime(call_start_str, fmt)
break
except ValueError:
continue
if not call_start:
skipped_count += 1
continue
# Parse call end time
call_end = None
call_end_str = row.get('Call End', '').strip()
if call_end_str:
for fmt in [
'%m/%d/%y %H:%M',
'%m/%d/%Y %H:%M',
'%m/%d/%y %I:%M:%S %p',
'%m/%d/%Y %I:%M:%S %p',
'%m/%d/%y %I:%M %p',
'%m/%d/%Y %I:%M %p',
]:
try:
call_end = datetime.strptime(call_end_str, fmt)
break
except ValueError:
continue
# Parse call duration
call_duration_seconds = None
length_str = row.get('Length', '').strip()
if length_str:
try:
parts = length_str.split(':')
if len(parts) == 3:
# HH:MM:SS format
call_duration_seconds = int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2])
elif len(parts) == 2:
# M:SS or MM:SS format
call_duration_seconds = int(parts[0]) * 60 + int(parts[1])
elif len(parts) == 1:
# Just seconds
call_duration_seconds = int(parts[0])
except ValueError:
pass
# Get or create hospital
hospital = None
if request.user.hospital:
hospital = request.user.hospital
# Create the record
CallRecord.objects.create(
media_id=media_id,
media_type=row.get('Media Type', 'Calls').strip(),
chain=row.get('Chain', '').strip(),
evaluated=row.get('Evaluated', '').strip().lower() == 'true',
call_start=call_start,
call_end=call_end,
call_length=length_str,
call_duration_seconds=call_duration_seconds,
first_name=row.get('First Name', '').strip(),
last_name=row.get('Last Name', '').strip(),
extension=row.get('Extension', '').strip(),
department=row.get('Department', '').strip(),
location=row.get('Location', '').strip(),
inbound_id=row.get('Inbound ID', '').strip(),
inbound_name=row.get('Inbound Name', '').strip(),
dnis=row.get('DNIS', '').strip(),
outbound_id=row.get('Outbound ID', '').strip(),
outbound_name=row.get('Outbound Name', '').strip(),
flag_name=row.get('Flag Name', '').strip(),
flag_value=row.get('Flag Value', '').strip(),
file_location=row.get('File Location', '').strip(),
file_name=row.get('File Name', '').strip(),
file_hash=row.get('FileHash', '').strip(),
external_ref=row.get('External Ref', '').strip(),
transfer_from=row.get('Transfer From', '').strip(),
recorded_by=row.get('Recorded By', '').strip(),
time_zone=row.get('Time Zone', '03:00:00').strip(),
recording_server_name=row.get('Recording Server Name', '').strip(),
hospital=hospital,
)
imported_count += 1
except Exception as e:
error_count += 1
continue
messages.success(
request,
f"Import completed: {imported_count} records imported, {skipped_count} skipped (duplicates/invalid), {error_count} errors."
)
return redirect('callcenter:call_records_list')
except Exception as e:
messages.error(request, f"Error importing CSV: {str(e)}")
return redirect('callcenter:import_call_records')
# GET request - show upload form
context = {
'sample_headers': [
'Media ID', 'Media Type', 'Chain', 'Evaluated', 'Call Start',
'First Name', 'Last Name', 'Extension', 'Department', 'Location',
'Inbound ID', 'Inbound Name', 'DNIS', 'Outbound ID', 'Outbound Name',
'Length', 'Call End', 'Flag Name', 'Flag Value', 'File Location',
'File Name', 'External Ref', 'FileHash', 'Transfer From', 'Recorded By',
'Time Zone', 'Recording Server Name'
],
}
return render(request, 'callcenter/import_call_records.html', context)
@login_required
def export_call_records_template(request):
"""
Export a sample CSV template for importing call records.
"""
import csv
from django.http import HttpResponse
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="call_records_template.csv"'
writer = csv.writer(response)
writer.writerow([
'Media ID', 'Media Type', 'Chain', 'Evaluated', 'Call Start',
'First Name', 'Last Name', 'Extension', 'Department', 'Location',
'Inbound ID', 'Inbound Name', 'DNIS', 'Outbound ID', 'Outbound Name',
'Length', 'Call End', 'Flag Name', 'Flag Value', 'File Location',
'File Name', 'External Ref', 'FileHash', 'Transfer From', 'Recorded By',
'Time Zone', 'Recording Server Name'
])
# Add one sample row
writer.writerow([
'aade2430-2eb0-4e05-93eb-9567e2be07ae', 'Calls', '', 'False',
'10/30/2025 7:57:48 PM', 'Patient', 'Relation', '1379', 'Patient Relation', '',
'597979769', '', '', '', '', '00:01:11', '10/30/2025 7:59:00 PM',
'', '', 'E:\\Calls', '2025-10-30\\x1379 19.57.48.467 10-30-2025.mp3',
'12946311', '', '0', '', '03:00:00', 'ahnuzdcnqms02'
])
return response

View File

@ -18,6 +18,11 @@ urlpatterns = [
path('inquiries/create/', ui_views.create_inquiry, name='create_inquiry'), path('inquiries/create/', ui_views.create_inquiry, name='create_inquiry'),
path('inquiries/<uuid:pk>/success/', ui_views.inquiry_success, name='inquiry_success'), path('inquiries/<uuid:pk>/success/', ui_views.inquiry_success, name='inquiry_success'),
# Call Records (CSV Import)
path('records/', ui_views.call_records_list, name='call_records_list'),
path('records/import/', ui_views.import_call_records, name='import_call_records'),
path('records/export-template/', ui_views.export_call_records_template, name='export_call_records_template'),
# AJAX Helpers # AJAX Helpers
path('ajax/departments/', ui_views.get_departments_by_hospital, name='ajax_departments'), path('ajax/departments/', ui_views.get_departments_by_hospital, name='ajax_departments'),
path('ajax/physicians/', ui_views.get_staff_by_hospital, name='ajax_physicians'), path('ajax/physicians/', ui_views.get_staff_by_hospital, name='ajax_physicians'),

View File

@ -40,6 +40,14 @@ class ResolutionCategory(models.TextChoices):
PATIENT_WITHDRAWN = "patient_withdrawn", "Patient Withdrawn" PATIENT_WITHDRAWN = "patient_withdrawn", "Patient Withdrawn"
class ResolutionOutcome(models.TextChoices):
"""Resolution outcome - who was in wrong/right"""
PATIENT = "patient", "Patient"
HOSPITAL = "hospital", "Hospital"
OTHER = "other", "Other — please specify"
class ComplaintType(models.TextChoices): class ComplaintType(models.TextChoices):
"""Complaint type choices - distinguish between complaints and appreciations""" """Complaint type choices - distinguish between complaints and appreciations"""
@ -378,6 +386,17 @@ class Complaint(UUIDModel, TimeStampedModel):
db_index=True, db_index=True,
help_text="Category of resolution" help_text="Category of resolution"
) )
resolution_outcome = models.CharField(
max_length=20,
choices=ResolutionOutcome.choices,
blank=True,
db_index=True,
help_text="Who was in wrong/right (Patient / Hospital / Other)"
)
resolution_outcome_other = models.TextField(
blank=True,
help_text="Specify if Other was selected for resolution outcome"
)
resolved_at = models.DateTimeField(null=True, blank=True) resolved_at = models.DateTimeField(null=True, blank=True)
resolved_by = models.ForeignKey( resolved_by = models.ForeignKey(
"accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="resolved_complaints" "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="resolved_complaints"
@ -2274,23 +2293,434 @@ class ComplaintAdverseActionAttachment(UUIDModel, TimeStampedModel):
filename = models.CharField(max_length=255) filename = models.CharField(max_length=255)
file_type = models.CharField(max_length=100, blank=True) file_type = models.CharField(max_length=100, blank=True)
file_size = models.IntegerField(help_text=_("File size in bytes")) file_size = models.IntegerField(help_text=_("File size in bytes"))
description = models.TextField( description = models.TextField(
blank=True, blank=True,
help_text=_("Description of what this attachment shows") help_text=_("Description of what this attachment shows")
) )
uploaded_by = models.ForeignKey( uploaded_by = models.ForeignKey(
"accounts.User", "accounts.User",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
related_name="adverse_action_attachments" related_name="adverse_action_attachments"
) )
class Meta: class Meta:
ordering = ["-created_at"] ordering = ["-created_at"]
verbose_name = _("Adverse Action Attachment") verbose_name = _("Adverse Action Attachment")
verbose_name_plural = _("Adverse Action Attachments") verbose_name_plural = _("Adverse Action Attachments")
def __str__(self):
return f"{self.adverse_action.reference_number} - {self.filename}"
# ============================================================================
# COMPLAINT TEMPLATES
# ============================================================================
class ComplaintTemplate(UUIDModel, TimeStampedModel):
"""
Pre-defined templates for common complaints.
Allows quick selection of common complaint types with pre-filled
description, category, severity, and auto-assignment.
"""
hospital = models.ForeignKey(
'organizations.Hospital',
on_delete=models.CASCADE,
related_name='complaint_templates',
help_text=_("Hospital this template belongs to")
)
name = models.CharField(
max_length=200,
help_text=_("Template name (e.g., 'Long Wait Time', 'Rude Staff')")
)
description = models.TextField(
help_text=_("Default description template with placeholders")
)
# Pre-set classification
category = models.ForeignKey(
ComplaintCategory,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='templates',
help_text=_("Default category for this template")
)
# Default severity/priority
default_severity = models.CharField(
max_length=20,
choices=SeverityChoices.choices,
default=SeverityChoices.MEDIUM,
help_text=_("Default severity level")
)
default_priority = models.CharField(
max_length=20,
choices=PriorityChoices.choices,
default=PriorityChoices.MEDIUM,
help_text=_("Default priority level")
)
# Auto-assignment
auto_assign_department = models.ForeignKey(
'organizations.Department',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='template_assignments',
help_text=_("Auto-assign to this department when template is used")
)
# Usage tracking
usage_count = models.IntegerField(
default=0,
editable=False,
help_text=_("Number of times this template has been used")
)
# Placeholders that can be used in description
# e.g., "Patient waited for {{wait_time}} minutes"
placeholders = models.JSONField(
default=list,
blank=True,
help_text=_("List of placeholder names used in description")
)
is_active = models.BooleanField(
default=True,
db_index=True,
help_text=_("Whether this template is available for selection")
)
class Meta:
ordering = ['-usage_count', 'name']
verbose_name = _('Complaint Template')
verbose_name_plural = _('Complaint Templates')
unique_together = [['hospital', 'name']]
indexes = [
models.Index(fields=['hospital', 'is_active']),
models.Index(fields=['-usage_count']),
]
def __str__(self): def __str__(self):
return f"{self.adverse_action} - {self.filename}" return f"{self.hospital.name} - {self.name} ({self.usage_count} uses)"
def use_template(self):
"""Increment usage count"""
self.usage_count += 1
self.save(update_fields=['usage_count'])
def render_description(self, placeholder_values):
"""
Render description with placeholder values.
Args:
placeholder_values: Dict of placeholder name -> value
Returns:
Rendered description string
"""
description = self.description
for key, value in placeholder_values.items():
description = description.replace(f'{{{{{key}}}}}', str(value))
return description
# ============================================================================
# COMMUNICATION LOG
# ============================================================================
class ComplaintCommunicationType(models.TextChoices):
"""Types of communication"""
PHONE_CALL = 'phone_call', 'Phone Call'
EMAIL = 'email', 'Email'
SMS = 'sms', 'SMS'
MEETING = 'meeting', 'Meeting'
LETTER = 'letter', 'Letter'
OTHER = 'other', 'Other'
class ComplaintCommunication(UUIDModel, TimeStampedModel):
"""
Tracks all communications related to a complaint.
Records phone calls, emails, meetings, and other communications
with complainants, involved staff, or other stakeholders.
"""
complaint = models.ForeignKey(
Complaint,
on_delete=models.CASCADE,
related_name='communications',
help_text=_("Related complaint")
)
# Communication details
communication_type = models.CharField(
max_length=20,
choices=ComplaintCommunicationType.choices,
help_text=_("Type of communication")
)
direction = models.CharField(
max_length=20,
choices=[
('inbound', 'Inbound'),
('outbound', 'Outbound'),
],
help_text=_("Direction of communication")
)
# Participants
contacted_person = models.CharField(
max_length=200,
help_text=_("Name of person contacted")
)
contacted_role = models.CharField(
max_length=100,
blank=True,
help_text=_("Role/relation (e.g., Complainant, Patient, Staff)")
)
contacted_phone = models.CharField(
max_length=20,
blank=True,
help_text=_("Phone number")
)
contacted_email = models.EmailField(
blank=True,
help_text=_("Email address")
)
# Communication content
subject = models.CharField(
max_length=500,
blank=True,
help_text=_("Subject/summary of communication")
)
notes = models.TextField(
help_text=_("Details of what was discussed")
)
# Follow-up
requires_followup = models.BooleanField(
default=False,
help_text=_("Whether this communication requires follow-up")
)
followup_date = models.DateField(
null=True,
blank=True,
help_text=_("Date when follow-up is needed")
)
followup_notes = models.TextField(
blank=True,
help_text=_("Notes from follow-up")
)
# Attachments (emails, letters, etc.)
attachment = models.FileField(
upload_to='complaints/communications/%Y/%m/%d/',
null=True,
blank=True,
help_text=_("Attached document (email export, letter, etc.)")
)
# Created by
created_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
related_name='complaint_communications',
help_text=_("User who logged this communication")
)
class Meta:
ordering = ['-created_at']
verbose_name = _('Complaint Communication')
verbose_name_plural = _('Complaint Communications')
indexes = [
models.Index(fields=['complaint', '-created_at']),
models.Index(fields=['communication_type']),
models.Index(fields=['requires_followup', 'followup_date']),
]
def __str__(self):
return f"{self.complaint.reference_number} - {self.get_communication_type_display()} - {self.contacted_person}"
# ============================================================================
# ROOT CAUSE ANALYSIS (RCA)
# ============================================================================
class RootCauseCategory(models.TextChoices):
"""Root cause categories for RCA"""
PEOPLE = 'people', 'People (Training, Staffing)'
PROCESS = 'process', 'Process/Procedure'
EQUIPMENT = 'equipment', 'Equipment/Technology'
ENVIRONMENT = 'environment', 'Environment/Facility'
COMMUNICATION = 'communication', 'Communication'
POLICY = 'policy', 'Policy/Protocol'
PATIENT_FACTOR = 'patient_factor', 'Patient-Related Factor'
OTHER = 'other', 'Other'
class ComplaintRootCauseAnalysis(UUIDModel, TimeStampedModel):
"""
Root Cause Analysis (RCA) for complaints.
Structured analysis to identify underlying causes and prevent recurrence.
Linked to complaints that require formal investigation.
"""
complaint = models.OneToOneField(
Complaint,
on_delete=models.CASCADE,
related_name='root_cause_analysis',
help_text=_("Related complaint")
)
# RCA Team
team_leader = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='led_rcas',
help_text=_("RCA team leader")
)
team_members = models.TextField(
blank=True,
help_text=_("List of RCA team members (one per line)")
)
# Problem statement
problem_statement = models.TextField(
help_text=_("Clear description of what happened")
)
impact_description = models.TextField(
help_text=_("Impact on patient, organization, etc.")
)
# Root cause categories (can select multiple)
root_cause_categories = models.JSONField(
default=list,
help_text=_("Selected root cause categories")
)
# 5 Whys analysis
why_1 = models.TextField(blank=True, help_text=_("Why did this happen? (Level 1)"))
why_2 = models.TextField(blank=True, help_text=_("Why? (Level 2)"))
why_3 = models.TextField(blank=True, help_text=_("Why? (Level 3)"))
why_4 = models.TextField(blank=True, help_text=_("Why? (Level 4)"))
why_5 = models.TextField(blank=True, help_text=_("Why? (Level 5)"))
# Root cause summary
root_cause_summary = models.TextField(
help_text=_("Summary of identified root causes")
)
# Contributing factors
contributing_factors = models.TextField(
blank=True,
help_text=_("Factors that contributed to the incident")
)
# Corrective and Preventive Actions (CAPA)
corrective_actions = models.TextField(
help_text=_("Actions to correct the immediate issue")
)
preventive_actions = models.TextField(
help_text=_("Actions to prevent recurrence")
)
# Action tracking
action_owner = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='owned_rca_actions',
help_text=_("Person responsible for implementing actions")
)
action_due_date = models.DateField(
null=True,
blank=True,
help_text=_("Due date for implementing actions")
)
action_status = models.CharField(
max_length=20,
choices=[
('not_started', 'Not Started'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('verified', 'Verified Effective'),
],
default='not_started',
help_text=_("Status of corrective actions")
)
# Effectiveness verification
effectiveness_verified = models.BooleanField(
default=False,
help_text=_("Whether the effectiveness of actions has been verified")
)
effectiveness_date = models.DateField(
null=True,
blank=True,
help_text=_("Date when effectiveness was verified")
)
effectiveness_notes = models.TextField(
blank=True,
help_text=_("Notes on effectiveness verification")
)
# RCA Status
status = models.CharField(
max_length=20,
choices=[
('draft', 'Draft'),
('in_review', 'In Review'),
('approved', 'Approved'),
('closed', 'Closed'),
],
default='draft',
help_text=_("RCA status")
)
# Approval
approved_by = models.ForeignKey(
'accounts.User',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approved_rcas',
help_text=_("User who approved the RCA")
)
approved_at = models.DateTimeField(
null=True,
blank=True,
help_text=_("Date when RCA was approved")
)
class Meta:
verbose_name = _('Root Cause Analysis')
verbose_name_plural = _('Root Cause Analyses')
indexes = [
models.Index(fields=['action_status', 'action_due_date']),
models.Index(fields=['status']),
]
def __str__(self):
return f"RCA for {self.complaint.reference_number}"
@property
def is_overdue(self):
"""Check if action is overdue"""
from django.utils import timezone
if self.action_due_date and self.action_status not in ['completed', 'verified']:
return timezone.now().date() > self.action_due_date
return False

View File

View File

@ -0,0 +1,243 @@
"""
Complaint duplicate detection service
Identifies potential duplicate complaints based on:
- Patient name similarity
- Incident date proximity
- Description text similarity
- Category match
"""
from difflib import SequenceMatcher
from django.utils import timezone
from datetime import timedelta
from django.db.models import Q
class ComplaintDuplicateDetector:
"""
Detect potential duplicate complaints.
Uses fuzzy matching to identify complaints that may be duplicates
of each other, helping prevent fragmentation and redundant work.
"""
def __init__(self, complaint_data, threshold=0.75):
"""
Initialize detector with complaint data.
Args:
complaint_data: Dict with complaint fields (patient_name, description, etc.)
threshold: Similarity threshold (0.0-1.0) for considering duplicates
"""
self.data = complaint_data
self.threshold = threshold
def find_duplicates(self, hospital_id, days=30, exclude_id=None):
"""
Find potential duplicate complaints from last N days.
Args:
hospital_id: Hospital to search within
days: Number of days to look back
exclude_id: Complaint ID to exclude (useful when editing)
Returns:
List of dicts with complaint, score, and match reasons
"""
from apps.complaints.models import Complaint
cutoff = timezone.now() - timedelta(days=days)
# Get candidate complaints (exclude closed/cancelled)
candidates = Complaint.objects.filter(
hospital_id=hospital_id,
created_at__gte=cutoff,
status__in=['open', 'in_progress', 'partially_resolved']
)
if exclude_id:
candidates = candidates.exclude(id=exclude_id)
# Select only needed fields for performance
candidates = candidates.select_related('patient', 'category').only(
'id', 'patient_name', 'description', 'created_at',
'incident_date', 'category_id'
)
duplicates = []
for complaint in candidates:
score, reasons = self._calculate_similarity(complaint)
if score >= self.threshold:
duplicates.append({
'complaint': complaint,
'score': score,
'score_percentage': int(score * 100),
'reasons': reasons,
'is_likely_duplicate': score >= 0.85 # 85%+ is very likely duplicate
})
# Sort by score descending
return sorted(duplicates, key=lambda x: x['score'], reverse=True)
def _calculate_similarity(self, complaint):
"""
Calculate similarity score between new complaint and existing one.
Returns:
Tuple of (score, list of match reasons)
"""
score = 0.0
reasons = []
# Weights for different factors
weights = {
'patient': 0.30, # Patient name match
'date': 0.20, # Incident date match
'description': 0.35, # Description text similarity
'category': 0.15, # Category match
}
# Patient name match (30%)
patient_match, patient_reason = self._match_patient(complaint)
if patient_match:
score += weights['patient']
reasons.append(patient_reason)
# Date match (20%)
date_match, date_reason = self._match_date(complaint)
if date_match:
score += weights['date']
reasons.append(date_reason)
# Description similarity (35%)
desc_score, desc_reason = self._text_similarity(complaint)
score += desc_score * weights['description']
if desc_score > 0.5:
reasons.append(desc_reason)
# Category match (15%)
category_match, category_reason = self._match_category(complaint)
if category_match:
score += weights['category']
reasons.append(category_reason)
return score, reasons
def _match_patient(self, complaint):
"""Check if patient names match"""
new_name = self._normalize_name(self.data.get('patient_name', ''))
existing_name = self._normalize_name(getattr(complaint, 'patient_name', ''))
if not new_name or not existing_name:
return False, None
# Exact match
if new_name == existing_name:
return True, f"Patient name matches: {complaint.patient_name}"
# Fuzzy match (80%+ similar)
similarity = SequenceMatcher(None, new_name, existing_name).ratio()
if similarity >= 0.80:
return True, f"Patient name similar ({int(similarity*100)}% match)"
return False, None
def _normalize_name(self, name):
"""Normalize name for comparison"""
if not name:
return ''
# Convert to uppercase, remove extra spaces
name = ' '.join(str(name).upper().split())
# Remove common prefixes/suffixes
for prefix in ['MR.', 'MRS.', 'MS.', 'DR.', 'MR ', 'MRS ', 'MS ', 'DR ']:
name = name.replace(prefix, '')
return name.strip()
def _match_date(self, complaint):
"""Check if incident dates are within 3 days"""
new_date = self.data.get('incident_date')
existing_date = getattr(complaint, 'incident_date', None)
if not new_date or not existing_date:
return False, None
# Check if dates are the same or within 3 days
date_diff = abs((new_date - existing_date).days)
if date_diff == 0:
return True, f"Same incident date: {existing_date}"
elif date_diff <= 3:
return True, f"Incident date within 3 days ({existing_date})"
return False, None
def _text_similarity(self, complaint):
"""Calculate text similarity between descriptions"""
new_desc = self._normalize_text(self.data.get('description', ''))
existing_desc = self._normalize_text(getattr(complaint, 'description', ''))
if not new_desc or not existing_desc:
return 0.0, None
# Use SequenceMatcher for text similarity
similarity = SequenceMatcher(None, new_desc, existing_desc).ratio()
# Also check for keyword overlap
new_words = set(new_desc.split())
existing_words = set(existing_desc.split())
if new_words and existing_words:
word_overlap = len(new_words & existing_words) / min(len(new_words), len(existing_words))
# Boost score if there's significant word overlap
if word_overlap > 0.5:
similarity = max(similarity, word_overlap * 0.8)
reason = None
if similarity > 0.7:
reason = f"Description {int(similarity*100)}% similar"
return similarity, reason
def _normalize_text(self, text):
"""Normalize text for comparison"""
if not text:
return ''
# Lowercase, remove extra spaces
text = ' '.join(str(text).lower().split())
# Remove common words
stop_words = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will',
'would', 'could', 'should', 'may', 'might', 'must', 'shall'}
words = [w for w in text.split() if w not in stop_words and len(w) > 2]
return ' '.join(words)
def _match_category(self, complaint):
"""Check if categories match"""
new_category_id = self.data.get('category_id')
existing_category_id = getattr(complaint, 'category_id', None)
if not new_category_id or not existing_category_id:
return False, None
if new_category_id == existing_category_id:
# Get category name for display
category_name = getattr(complaint, 'category', None)
if category_name:
return True, f"Same category: {category_name.name_en}"
return True, "Same category"
return False, None
def check_for_duplicates(complaint_data, hospital_id, exclude_id=None):
"""
Convenience function to check for duplicate complaints.
Args:
complaint_data: Dict with complaint fields
hospital_id: Hospital ID to search within
exclude_id: Optional complaint ID to exclude
Returns:
List of potential duplicates with scores
"""
detector = ComplaintDuplicateDetector(complaint_data)
return detector.find_duplicates(hospital_id, exclude_id=exclude_id)

View File

@ -661,6 +661,8 @@ def complaint_change_status(request, pk):
new_status = request.POST.get("status") new_status = request.POST.get("status")
note = request.POST.get("note", "") note = request.POST.get("note", "")
resolution = request.POST.get("resolution", "") resolution = request.POST.get("resolution", "")
resolution_outcome = request.POST.get("resolution_outcome", "")
resolution_outcome_other = request.POST.get("resolution_outcome_other", "")
if not new_status: if not new_status:
messages.error(request, "Please select a status.") messages.error(request, "Please select a status.")
@ -677,6 +679,11 @@ def complaint_change_status(request, pk):
if resolution: if resolution:
complaint.resolution = resolution complaint.resolution = resolution
complaint.resolution_sent_at = timezone.now() complaint.resolution_sent_at = timezone.now()
# Save resolution outcome
if resolution_outcome:
complaint.resolution_outcome = resolution_outcome
if resolution_outcome == "other" and resolution_outcome_other:
complaint.resolution_outcome_other = resolution_outcome_other
elif new_status == ComplaintStatus.CLOSED: elif new_status == ComplaintStatus.CLOSED:
complaint.closed_at = timezone.now() complaint.closed_at = timezone.now()
complaint.closed_by = request.user complaint.closed_by = request.user

View File

@ -111,12 +111,34 @@ def request_explanation_form(request, pk):
selected_manager_ids, request_message selected_manager_ids, request_message
) )
messages.success( # Check results and show appropriate message
request, if results['staff_count'] == 0 and results['manager_count'] == 0:
_("Explanation requests sent successfully! Staff: {}, Managers notified: {}.").format( if results['skipped_no_email'] > 0:
results['staff_count'], results['manager_count'] messages.warning(
request,
_("No explanation requests were sent. {} staff member(s) do not have email addresses. Please update staff records with email addresses before sending explanation requests.").format(
results['skipped_no_email']
)
)
else:
messages.warning(
request,
_("No explanation requests were sent. Please check staff email configuration.")
)
elif results['staff_count'] == 0 and results['manager_count'] > 0:
messages.warning(
request,
_("Only manager notifications were sent ({}). Staff explanation requests could not be sent due to missing email addresses.").format(
results['manager_count']
)
)
else:
messages.success(
request,
_("Explanation requests sent successfully! Staff: {}, Managers notified: {}.").format(
results['staff_count'], results['manager_count']
)
) )
)
return redirect('complaints:complaint_detail', pk=complaint.pk) return redirect('complaints:complaint_detail', pk=complaint.pk)
return render(request, 'complaints/request_explanation_form.html', { return render(request, 'complaints/request_explanation_form.html', {
@ -142,6 +164,14 @@ def _send_explanation_requests(request, complaint, recipients, selected_staff_id
staff_count = 0 staff_count = 0
manager_count = 0 manager_count = 0
skipped_no_email = 0
# Debug logging
import logging
logger = logging.getLogger(__name__)
logger.info(f"Sending explanation requests. Selected staff IDs: {selected_staff_ids}")
logger.info(f"Selected manager IDs: {selected_manager_ids}")
logger.info(f"Total recipients: {len(recipients)}")
# Track which managers we've already notified # Track which managers we've already notified
notified_managers = set() notified_managers = set()
@ -150,13 +180,19 @@ def _send_explanation_requests(request, complaint, recipients, selected_staff_id
staff = recipient['staff'] staff = recipient['staff']
staff_id = recipient['staff_id'] staff_id = recipient['staff_id']
logger.info(f"Processing staff: {staff.get_full_name()} (ID: {staff_id})")
# Skip if staff not selected # Skip if staff not selected
if staff_id not in selected_staff_ids: if staff_id not in selected_staff_ids:
logger.info(f" Skipping - staff_id {staff_id} not in selected_staff_ids: {selected_staff_ids}")
continue continue
# Check if staff has email # Check if staff has email
staff_email = recipient['staff_email'] staff_email = recipient['staff_email']
logger.info(f" Staff email: {staff_email}")
if not staff_email: if not staff_email:
logger.warning(f" Skipping - no email for staff {staff.get_full_name()}")
skipped_no_email += 1
continue continue
# Generate unique token # Generate unique token
@ -230,6 +266,7 @@ This is an automated message from PX360 Complaint Management System.
# Send email to staff # Send email to staff
try: try:
logger.info(f" Sending email to: {staff_email}")
NotificationService.send_email( NotificationService.send_email(
email=staff_email, email=staff_email,
subject=staff_subject, subject=staff_subject,
@ -242,10 +279,9 @@ This is an automated message from PX360 Complaint Management System.
} }
) )
staff_count += 1 staff_count += 1
logger.info(f" Email sent successfully to {staff_email}")
except Exception as e: except Exception as e:
import logging logger.error(f" Failed to send explanation request to staff {staff.id}: {e}")
logger = logging.getLogger(__name__)
logger.error(f"Failed to send explanation request to staff {staff.id}: {e}")
# Send notification to manager if selected and not already notified # Send notification to manager if selected and not already notified
manager = recipient['manager'] manager = recipient['manager']
@ -302,15 +338,16 @@ This is an automated message from PX360 Complaint Management System.
# Log audit event # Log audit event
AuditService.log_event( AuditService.log_event(
event_type='explanation_request', event_type='explanation_request',
description=f'Explanation requests sent to {staff_count} staff and {manager_count} managers', description=f'Explanation requests sent to {staff_count} staff and {manager_count} managers ({skipped_no_email} skipped due to no email)',
user=user, user=user,
content_object=complaint, content_object=complaint,
metadata={ metadata={
'staff_count': staff_count, 'staff_count': staff_count,
'manager_count': manager_count, 'manager_count': manager_count,
'skipped_no_email': skipped_no_email,
'selected_staff_ids': selected_staff_ids, 'selected_staff_ids': selected_staff_ids,
'selected_manager_ids': selected_manager_ids, 'selected_manager_ids': selected_manager_ids,
} }
) )
return {'staff_count': staff_count, 'manager_count': manager_count} return {'staff_count': staff_count, 'manager_count': manager_count, 'skipped_no_email': skipped_no_email}

View File

@ -0,0 +1,265 @@
"""
Complaint Templates UI views
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from apps.complaints.models import ComplaintTemplate, ComplaintCategory
from apps.organizations.models import Hospital, Department
@login_required
def template_list(request):
"""
List all complaint templates
"""
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
messages.error(request, _("You don't have permission to manage templates."))
return redirect('complaints:complaint_list')
templates = ComplaintTemplate.objects.select_related('hospital', 'category', 'auto_assign_department').all()
# Filter by hospital
hospital_filter = request.GET.get('hospital')
if hospital_filter:
templates = templates.filter(hospital_id=hospital_filter)
# Filter by active status
is_active = request.GET.get('is_active')
if is_active:
templates = templates.filter(is_active=is_active == 'true')
# Search
search = request.GET.get('search')
if search:
templates = templates.filter(name_en__icontains=search)
templates = templates.order_by('-usage_count', 'name_en')
# Get hospitals for filter
if request.user.is_px_admin():
hospitals = Hospital.objects.filter(status='active').order_by('name')
else:
hospitals = Hospital.objects.filter(id=request.user.hospital_id) if request.user.hospital else []
context = {
'templates': templates,
'hospitals': hospitals,
'hospital_filter': hospital_filter,
'is_active_filter': is_active,
'search': search,
}
return render(request, 'complaints/templates/template_list.html', context)
@login_required
def template_create(request):
"""
Create a new complaint template
"""
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
messages.error(request, _("You don't have permission to create templates."))
return redirect('complaints:template_list')
if request.method == 'POST':
try:
# Get hospital
hospital_id = request.POST.get('hospital')
hospital = get_object_or_404(Hospital, id=hospital_id)
# Get category if provided
category_id = request.POST.get('category')
category = None
if category_id:
category = get_object_or_404(ComplaintCategory, id=category_id)
# Get auto-assign department if provided
auto_assign_dept_id = request.POST.get('auto_assign_department')
auto_assign_department = None
if auto_assign_dept_id:
auto_assign_department = get_object_or_404(Department, id=auto_assign_dept_id)
# Parse placeholders from JSON or empty list
import json
try:
placeholders = json.loads(request.POST.get('placeholders', '[]'))
except:
placeholders = []
template = ComplaintTemplate.objects.create(
hospital=hospital,
name=request.POST.get('name'),
description=request.POST.get('description'),
category=category,
default_severity=request.POST.get('default_severity', 'medium'),
default_priority=request.POST.get('default_priority', 'medium'),
auto_assign_department=auto_assign_department,
placeholders=placeholders,
is_active=request.POST.get('is_active') == 'on',
)
messages.success(request, _("Template created successfully!"))
return redirect('complaints:template_detail', pk=template.pk)
except Exception as e:
messages.error(request, _("Error creating template: {}").format(str(e)))
# Get hospitals and categories for form
if request.user.is_px_admin():
hospitals = Hospital.objects.filter(status='active').order_by('name')
else:
hospitals = Hospital.objects.filter(id=request.user.hospital_id) if request.user.hospital else []
categories = ComplaintCategory.objects.filter(level=2, is_active=True).order_by('name_en')[:50]
context = {
'hospitals': hospitals,
'categories': categories,
'template': None,
'action': 'create',
}
return render(request, 'complaints/templates/template_form.html', context)
@login_required
def template_detail(request, pk):
"""
View template details
"""
template = get_object_or_404(
ComplaintTemplate.objects.select_related('hospital', 'category', 'auto_assign_department'),
pk=pk
)
context = {
'template': template,
}
return render(request, 'complaints/templates/template_detail.html', context)
@login_required
def template_edit(request, pk):
"""
Edit an existing template
"""
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
messages.error(request, _("You don't have permission to edit templates."))
return redirect('complaints:template_detail', pk=pk)
template = get_object_or_404(
ComplaintTemplate.objects.select_related('hospital', 'category', 'auto_assign_department'),
pk=pk
)
if request.method == 'POST':
try:
# Get hospital
hospital_id = request.POST.get('hospital')
template.hospital = get_object_or_404(Hospital, id=hospital_id)
# Get category if provided
category_id = request.POST.get('category')
if category_id:
template.category = get_object_or_404(ComplaintCategory, id=category_id)
else:
template.category = None
# Get auto-assign department if provided
auto_assign_dept_id = request.POST.get('auto_assign_department')
if auto_assign_dept_id:
template.auto_assign_department = get_object_or_404(Department, id=auto_assign_dept_id)
else:
template.auto_assign_department = None
# Parse placeholders
import json
try:
template.placeholders = json.loads(request.POST.get('placeholders', '[]'))
except:
template.placeholders = []
template.name = request.POST.get('name')
template.description = request.POST.get('description')
template.default_severity = request.POST.get('default_severity', 'medium')
template.default_priority = request.POST.get('default_priority', 'medium')
template.is_active = request.POST.get('is_active') == 'on'
template.save()
messages.success(request, _("Template updated successfully!"))
return redirect('complaints:template_detail', pk=template.pk)
except Exception as e:
messages.error(request, _("Error updating template: {}").format(str(e)))
# Get hospitals and categories for form
if request.user.is_px_admin():
hospitals = Hospital.objects.filter(status='active').order_by('name')
else:
hospitals = Hospital.objects.filter(id=request.user.hospital_id) if request.user.hospital else []
categories = ComplaintCategory.objects.filter(level=2, is_active=True).order_by('name_en')[:50]
context = {
'hospitals': hospitals,
'categories': categories,
'template': template,
'action': 'edit',
}
return render(request, 'complaints/templates/template_form.html', context)
@login_required
def template_delete(request, pk):
"""
Delete a template
"""
if not request.user.is_px_admin():
messages.error(request, _("You don't have permission to delete templates."))
return redirect('complaints:template_detail', pk=pk)
template = get_object_or_404(ComplaintTemplate, pk=pk)
if request.method == 'POST':
template_name = template.name
template.delete()
messages.success(request, _("Template '{}' deleted successfully!").format(template_name))
return redirect('complaints:template_list')
context = {
'template': template,
}
return render(request, 'complaints/templates/template_confirm_delete.html', context)
@login_required
def template_toggle_status(request, pk):
"""
Toggle template active status (AJAX)
"""
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
from django.http import JsonResponse
return JsonResponse({'error': 'Permission denied'}, status=403)
from django.http import JsonResponse
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
template = get_object_or_404(ComplaintTemplate, pk=pk)
template.is_active = not template.is_active
template.save()
return JsonResponse({
'success': True,
'is_active': template.is_active,
'message': 'Template {} successfully'.format(
'activated' if template.is_active else 'deactivated'
)
})

View File

@ -14,7 +14,7 @@ from .views import (
api_subsections, api_subsections,
api_departments, api_departments,
) )
from . import ui_views, ui_views_explanation, ui_views_oncall from . import ui_views, ui_views_explanation, ui_views_oncall, ui_views_templates
app_name = "complaints" app_name = "complaints"
@ -69,6 +69,15 @@ urlpatterns = [
path("settings/thresholds/new/", ui_views.complaint_threshold_create, name="complaint_threshold_create"), path("settings/thresholds/new/", ui_views.complaint_threshold_create, name="complaint_threshold_create"),
path("settings/thresholds/<uuid:pk>/edit/", ui_views.complaint_threshold_edit, name="complaint_threshold_edit"), path("settings/thresholds/<uuid:pk>/edit/", ui_views.complaint_threshold_edit, name="complaint_threshold_edit"),
path("settings/thresholds/<uuid:pk>/delete/", ui_views.complaint_threshold_delete, name="complaint_threshold_delete"), path("settings/thresholds/<uuid:pk>/delete/", ui_views.complaint_threshold_delete, name="complaint_threshold_delete"),
# Complaint Templates Management
path("templates/", ui_views_templates.template_list, name="template_list"),
path("templates/new/", ui_views_templates.template_create, name="template_create"),
path("templates/<uuid:pk>/", ui_views_templates.template_detail, name="template_detail"),
path("templates/<uuid:pk>/edit/", ui_views_templates.template_edit, name="template_edit"),
path("templates/<uuid:pk>/delete/", ui_views_templates.template_delete, name="template_delete"),
path("templates/<uuid:pk>/toggle/", ui_views_templates.template_toggle_status, name="template_toggle_status"),
# AJAX Helpers # AJAX Helpers
path("ajax/departments/", ui_views.get_departments_by_hospital, name="get_departments_by_hospital"), path("ajax/departments/", ui_views.get_departments_by_hospital, name="get_departments_by_hospital"),
path("ajax/physicians/", ui_views.get_staff_by_department, name="get_physicians_by_department"), path("ajax/physicians/", ui_views.get_staff_by_department, name="get_physicians_by_department"),

View File

@ -7,6 +7,8 @@ from django.shortcuts import render
from apps.organizations.models import Hospital from apps.organizations.models import Hospital
from apps.px_action_center.models import PXActionSLAConfig, RoutingRule from apps.px_action_center.models import PXActionSLAConfig, RoutingRule
from apps.complaints.models import OnCallAdminSchedule
from apps.callcenter.models import CallRecord
@login_required @login_required
@ -23,11 +25,15 @@ def config_dashboard(request):
sla_configs_count = PXActionSLAConfig.objects.filter(is_active=True).count() sla_configs_count = PXActionSLAConfig.objects.filter(is_active=True).count()
routing_rules_count = RoutingRule.objects.filter(is_active=True).count() routing_rules_count = RoutingRule.objects.filter(is_active=True).count()
hospitals_count = Hospital.objects.filter(status='active').count() hospitals_count = Hospital.objects.filter(status='active').count()
oncall_schedules_count = OnCallAdminSchedule.objects.filter(is_active=True).count()
call_records_count = CallRecord.objects.count()
context = { context = {
'sla_configs_count': sla_configs_count, 'sla_configs_count': sla_configs_count,
'routing_rules_count': routing_rules_count, 'routing_rules_count': routing_rules_count,
'hospitals_count': hospitals_count, 'hospitals_count': hospitals_count,
'oncall_schedules_count': oncall_schedules_count,
'call_records_count': call_records_count,
} }
return render(request, 'config/dashboard.html', context) return render(request, 'config/dashboard.html', context)

View File

@ -258,6 +258,10 @@ class Staff(UUIDModel, TimeStampedModel):
# Head of department/section/subsection indicator # Head of department/section/subsection indicator
is_head = models.BooleanField(default=False, verbose_name="Is Head") is_head = models.BooleanField(default=False, verbose_name="Is Head")
# Physician indicator - set to True when staff comes from physician rating import
physician = models.BooleanField(default=False, verbose_name="Is Physician",
help_text="Set to True when staff record comes from physician rating import")
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE) status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE)
def __str__(self): def __str__(self):

View File

@ -282,6 +282,11 @@ class DoctorRatingAdapter:
doctor_id, hospital, doctor_name doctor_id, hospital, doctor_name
) )
# If staff found, mark as physician
if staff and not staff.physician:
staff.physician = True
staff.save(update_fields=['physician'])
# Extract patient info # Extract patient info
uhid = data.get('uhid', '').strip() uhid = data.get('uhid', '').strip()
patient_name = data.get('patient_name', '').strip() patient_name = data.get('patient_name', '').strip()

View File

View File

@ -0,0 +1,81 @@
"""
Management command to mark staff as physicians based on existing ratings.
This command scans all PhysicianIndividualRating records and marks
the associated Staff records as physicians.
Usage:
python manage.py mark_physicians_from_ratings [--hospital_id <uuid>]
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.organizations.models import Staff
from apps.physicians.models import PhysicianIndividualRating
class Command(BaseCommand):
help = 'Mark staff as physicians based on existing physician ratings'
def add_arguments(self, parser):
parser.add_argument(
'--hospital_id',
type=str,
help='Optional hospital ID to limit the update to a specific hospital',
required=False
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be updated without making changes',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
hospital_id = options.get('hospital_id')
# Get all staff IDs that have physician ratings
ratings_query = PhysicianIndividualRating.objects.filter(
staff__isnull=False
)
if hospital_id:
ratings_query = ratings_query.filter(hospital_id=hospital_id)
self.stdout.write(f"Filtering by hospital_id: {hospital_id}")
# Get unique staff IDs from ratings
staff_ids = list(ratings_query.values_list('staff_id', flat=True).distinct())
self.stdout.write(f"Found {len(staff_ids)} staff members with physician ratings")
if not staff_ids:
self.stdout.write(self.style.WARNING("No staff with physician ratings found"))
return
# Find staff that are not yet marked as physicians
staff_to_update = Staff.objects.filter(
id__in=staff_ids,
physician=False
)
count = staff_to_update.count()
if count == 0:
self.stdout.write(self.style.SUCCESS("All staff with physician ratings are already marked as physicians"))
return
self.stdout.write(f"Will update {count} staff records to mark as physicians")
if dry_run:
self.stdout.write(self.style.WARNING("DRY RUN - No changes made"))
for staff in staff_to_update[:10]:
self.stdout.write(f" Would update: {staff.get_full_name()} (ID: {staff.id})")
if count > 10:
self.stdout.write(f" ... and {count - 10} more")
return
# Update the staff records
with transaction.atomic():
updated = staff_to_update.update(physician=True)
self.stdout.write(self.style.SUCCESS(f"Successfully updated {updated} staff records as physicians"))

View File

@ -24,8 +24,11 @@ def physician_list(request):
- Search by name or license number - Search by name or license number
- Current month rating display - Current month rating display
""" """
# Base queryset with optimizations # Base queryset with optimizations - only show staff marked as physicians
queryset = Staff.objects.select_related('hospital', 'department') # Include both: staff with physician=True (from rating imports) OR staff_type='physician'
queryset = Staff.objects.filter(
Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN)
).select_related('hospital', 'department').distinct()
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -98,8 +101,10 @@ def physician_list(request):
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital) departments = departments.filter(hospital=user.hospital)
# Get unique specializations # Get unique specializations (only from physicians)
specializations = Staff.objects.values_list('specialization', flat=True).distinct().order_by('specialization') specializations = Staff.objects.filter(
Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN)
).values_list('specialization', flat=True).distinct().order_by('specialization')
# Statistics # Statistics
stats = { stats = {
@ -237,10 +242,11 @@ def leaderboard(request):
department_filter = request.GET.get('department') department_filter = request.GET.get('department')
limit = int(request.GET.get('limit', 20)) limit = int(request.GET.get('limit', 20))
# Build queryset # Build queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(
year=year, year=year,
month=month month=month,
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department') ).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
@ -393,10 +399,10 @@ def physician_ratings_dashboard_api(request):
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get('hospital')
department_filter = request.GET.get('department') department_filter = request.GET.get('department')
# Base queryset # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.select_related( queryset = PhysicianMonthlyRating.objects.filter(
'staff', 'staff__hospital', 'staff__department' staff__physician=True
) ).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -539,10 +545,10 @@ def ratings_list(request):
- Search by physician name - Search by physician name
- Pagination - Pagination
""" """
# Base queryset # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.select_related( queryset = PhysicianMonthlyRating.objects.filter(
'staff', 'staff__hospital', 'staff__department' staff__physician=True
) ).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -630,10 +636,11 @@ def specialization_overview(request):
month = int(request.GET.get('month', now.month)) month = int(request.GET.get('month', now.month))
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get('hospital')
# Base queryset # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(
year=year, year=year,
month=month month=month,
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department') ).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
@ -721,10 +728,11 @@ def department_overview(request):
month = int(request.GET.get('month', now.month)) month = int(request.GET.get('month', now.month))
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get('hospital')
# Base queryset # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(
year=year, year=year,
month=month month=month,
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department') ).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters

View File

@ -15,10 +15,20 @@ from apps.core.models import UUIDModel, TimeStampedModel
class PXSource(UUIDModel, TimeStampedModel): class PXSource(UUIDModel, TimeStampedModel):
""" """
PX Source model for managing feedback origins. PX Source model for managing feedback origins.
Simple model with bilingual naming and active status management. Simple model with bilingual naming and active status management.
""" """
# Code for API references
code = models.CharField(
max_length=50,
unique=True,
db_index=True,
help_text="Unique code for API references",
blank=True,
default=''
)
# Bilingual names # Bilingual names
name_en = models.CharField( name_en = models.CharField(
max_length=200, max_length=200,
@ -29,63 +39,153 @@ class PXSource(UUIDModel, TimeStampedModel):
blank=True, blank=True,
help_text="Source name in Arabic" help_text="Source name in Arabic"
) )
# Description # Description
description = models.TextField( description = models.TextField(
blank=True, blank=True,
help_text="Detailed description" help_text="Detailed description"
) )
# Source type
SOURCE_TYPE_CHOICES = [
('internal', 'Internal'),
('external', 'External'),
('partner', 'Partner'),
('government', 'Government'),
('other', 'Other'),
]
source_type = models.CharField(
max_length=50,
choices=SOURCE_TYPE_CHOICES,
default='internal',
db_index=True,
help_text="Type of source"
)
# Contact information for external sources
contact_email = models.EmailField(
blank=True,
help_text="Contact email for external sources"
)
contact_phone = models.CharField(
max_length=20,
blank=True,
help_text="Contact phone for external sources"
)
# Status # Status
is_active = models.BooleanField( is_active = models.BooleanField(
default=True, default=True,
db_index=True, db_index=True,
help_text="Whether this source is active for selection" help_text="Whether this source is active for selection"
) )
# Metadata
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional metadata"
)
# Cached usage stats
total_complaints = models.IntegerField(
default=0,
editable=False,
help_text="Cached total complaints count"
)
total_inquiries = models.IntegerField(
default=0,
editable=False,
help_text="Cached total inquiries count"
)
class Meta: class Meta:
ordering = ['name_en'] ordering = ['name_en']
verbose_name = 'PX Source' verbose_name = 'PX Source'
verbose_name_plural = 'PX Sources' verbose_name_plural = 'PX Sources'
indexes = [ indexes = [
models.Index(fields=['is_active', 'name_en']), models.Index(fields=['is_active', 'name_en']),
models.Index(fields=['code']),
models.Index(fields=['source_type']),
] ]
def __str__(self): def __str__(self):
return self.name_en return f"{self.code} - {self.name_en}"
def save(self, *args, **kwargs):
# Auto-generate code if not provided
if not self.code and self.name_en:
# Create code from name (e.g., "Hospital A" -> "HOSP-A")
words = self.name_en.upper().split()
if len(words) >= 2:
self.code = '-'.join(word[:4] for word in words[:3])
else:
self.code = self.name_en[:10].upper().replace(' ', '-')
# Ensure uniqueness
from django.db.models import Count
base_code = self.code
counter = 1
while PXSource.objects.filter(code=self.code).exclude(pk=self.pk).exists():
self.code = f"{base_code}-{counter}"
counter += 1
super().save(*args, **kwargs)
def get_localized_name(self, language='en'): def get_localized_name(self, language='en'):
"""Get localized name based on language""" """Get localized name based on language"""
if language == 'ar' and self.name_ar: if language == 'ar' and self.name_ar:
return self.name_ar return self.name_ar
return self.name_en return self.name_en
def get_localized_description(self): def get_localized_description(self):
"""Get localized description""" """Get localized description"""
return self.description return self.description
def activate(self): def activate(self):
"""Activate this source""" """Activate this source"""
if not self.is_active: if not self.is_active:
self.is_active = True self.is_active = True
self.save(update_fields=['is_active']) self.save(update_fields=['is_active'])
def deactivate(self): def deactivate(self):
"""Deactivate this source""" """Deactivate this source"""
if self.is_active: if self.is_active:
self.is_active = False self.is_active = False
self.save(update_fields=['is_active']) self.save(update_fields=['is_active'])
@classmethod @classmethod
def get_active_sources(cls): def get_active_sources(cls):
""" """
Get all active sources. Get all active sources.
Returns: Returns:
QuerySet of active PXSource objects QuerySet of active PXSource objects
""" """
return cls.objects.filter(is_active=True).order_by('name_en') return cls.objects.filter(is_active=True).order_by('name_en')
def update_usage_stats(self):
"""Update cached usage statistics"""
from apps.complaints.models import Complaint, Inquiry
self.total_complaints = Complaint.objects.filter(source=self).count()
self.total_inquiries = Inquiry.objects.filter(source=self).count()
self.save(update_fields=['total_complaints', 'total_inquiries'])
def get_usage_stats(self, days=30):
"""Get usage statistics for the last N days"""
from django.utils import timezone
from datetime import timedelta
cutoff = timezone.now() - timedelta(days=days)
return {
'total_usage': self.usage_records.filter(created_at__gte=cutoff).count(),
'complaints': self.usage_records.filter(
created_at__gte=cutoff,
content_type__model='complaint'
).count(),
'inquiries': self.usage_records.filter(
created_at__gte=cutoff,
content_type__model='inquiry'
).count(),
}
class SourceUser(UUIDModel, TimeStampedModel): class SourceUser(UUIDModel, TimeStampedModel):
""" """

View File

@ -11,11 +11,14 @@ class PXSourceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = PXSource model = PXSource
fields = [ fields = [
'id', 'name_en', 'name_ar', 'id', 'code', 'name_en', 'name_ar',
'description', 'is_active', 'description', 'source_type',
'contact_email', 'contact_phone',
'is_active', 'metadata',
'total_complaints', 'total_inquiries',
'created_at', 'updated_at' 'created_at', 'updated_at'
] ]
read_only_fields = ['id', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at', 'total_complaints', 'total_inquiries']
class PXSourceListSerializer(serializers.ModelSerializer): class PXSourceListSerializer(serializers.ModelSerializer):
@ -23,22 +26,28 @@ class PXSourceListSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = PXSource model = PXSource
fields = [ fields = [
'id', 'name_en', 'name_ar', 'id', 'code', 'name_en', 'name_ar',
'is_active' 'source_type', 'is_active',
'total_complaints', 'total_inquiries'
] ]
class PXSourceDetailSerializer(PXSourceSerializer): class PXSourceDetailSerializer(PXSourceSerializer):
"""Detailed serializer including usage statistics""" """Detailed serializer including usage statistics"""
usage_count = serializers.SerializerMethodField() usage_count = serializers.SerializerMethodField()
recent_usage = serializers.SerializerMethodField()
class Meta(PXSourceSerializer.Meta): class Meta(PXSourceSerializer.Meta):
fields = PXSourceSerializer.Meta.fields + ['usage_count'] fields = PXSourceSerializer.Meta.fields + ['usage_count', 'recent_usage']
def get_usage_count(self, obj): def get_usage_count(self, obj):
"""Get total usage count for this source""" """Get total usage count for this source"""
return obj.usage_records.count() return obj.usage_records.count()
def get_recent_usage(self, obj):
"""Get recent usage stats (last 30 days)"""
return obj.get_usage_stats(days=30)
class SourceUserSerializer(serializers.ModelSerializer): class SourceUserSerializer(serializers.ModelSerializer):
"""Serializer for SourceUser model""" """Serializer for SourceUser model"""
@ -46,7 +55,8 @@ class SourceUserSerializer(serializers.ModelSerializer):
user_full_name = serializers.CharField(source='user.get_full_name', read_only=True) user_full_name = serializers.CharField(source='user.get_full_name', read_only=True)
source_name = serializers.CharField(source='source.name_en', read_only=True) source_name = serializers.CharField(source='source.name_en', read_only=True)
source_name_ar = serializers.CharField(source='source.name_ar', read_only=True) source_name_ar = serializers.CharField(source='source.name_ar', read_only=True)
source_code = serializers.CharField(source='source.code', read_only=True)
class Meta: class Meta:
model = SourceUser model = SourceUser
fields = [ fields = [
@ -55,6 +65,7 @@ class SourceUserSerializer(serializers.ModelSerializer):
'user_email', 'user_email',
'user_full_name', 'user_full_name',
'source', 'source',
'source_code',
'source_name', 'source_name',
'source_name_ar', 'source_name_ar',
'is_active', 'is_active',
@ -71,13 +82,15 @@ class SourceUserListSerializer(serializers.ModelSerializer):
user_email = serializers.EmailField(source='user.email', read_only=True) user_email = serializers.EmailField(source='user.email', read_only=True)
user_full_name = serializers.CharField(source='user.get_full_name', read_only=True) user_full_name = serializers.CharField(source='user.get_full_name', read_only=True)
source_name = serializers.CharField(source='source.name_en', read_only=True) source_name = serializers.CharField(source='source.name_en', read_only=True)
source_code = serializers.CharField(source='source.code', read_only=True)
class Meta: class Meta:
model = SourceUser model = SourceUser
fields = [ fields = [
'id', 'id',
'user_email', 'user_email',
'user_full_name', 'user_full_name',
'source_code',
'source_name', 'source_name',
'is_active', 'is_active',
'can_create_complaints', 'can_create_complaints',
@ -88,14 +101,17 @@ class SourceUserListSerializer(serializers.ModelSerializer):
class SourceUsageSerializer(serializers.ModelSerializer): class SourceUsageSerializer(serializers.ModelSerializer):
"""Serializer for SourceUsage model""" """Serializer for SourceUsage model"""
source_name = serializers.CharField(source='source.name_en', read_only=True) source_name = serializers.CharField(source='source.name_en', read_only=True)
source_code = serializers.CharField(source='source.code', read_only=True)
content_type_name = serializers.CharField(source='content_type.model', read_only=True) content_type_name = serializers.CharField(source='content_type.model', read_only=True)
hospital_name = serializers.CharField(source='hospital.name', read_only=True)
user_email = serializers.EmailField(source='user.email', read_only=True)
class Meta: class Meta:
model = PXSource model = PXSource
fields = [ fields = [
'id', 'source', 'source_name', 'id', 'source', 'source_code', 'source_name',
'content_type', 'content_type_name', 'object_id', 'content_type', 'content_type_name', 'object_id',
'hospital', 'user', 'created_at' 'hospital', 'hospital_name', 'user', 'user_email', 'created_at'
] ]
read_only_fields = ['id', 'created_at'] read_only_fields = ['id', 'created_at']
@ -103,8 +119,9 @@ class SourceUsageSerializer(serializers.ModelSerializer):
class PXSourceChoiceSerializer(serializers.Serializer): class PXSourceChoiceSerializer(serializers.Serializer):
"""Simple serializer for dropdown choices""" """Simple serializer for dropdown choices"""
id = serializers.UUIDField() id = serializers.UUIDField()
code = serializers.CharField()
name = serializers.SerializerMethodField() name = serializers.SerializerMethodField()
def get_name(self, obj): def get_name(self, obj):
"""Get localized name based on request language""" """Get localized name based on request language"""
request = self.context.get('request') request = self.context.get('request')

View File

@ -1,24 +1,37 @@
""" """
PX Sources signals PX Sources signals - Auto-create SourceUsage records
This module defines signals for the PX Sources app.
Currently, this is a placeholder for future signal implementations.
""" """
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.contenttypes.models import ContentType
# Placeholder for future signal implementations @receiver(post_save, sender='complaints.Complaint')
# Example signals could include: @receiver(post_save, sender='complaints.Inquiry')
# - Logging when a source is created/updated/deleted def create_source_usage(sender, instance, created, **kwargs):
# - Invalidating caches when sources change
# - Sending notifications when sources are deactivated
@receiver(post_save)
def log_source_activity(sender, instance, created, **kwargs):
""" """
Log source activity for audit purposes. Auto-create SourceUsage when Complaint/Inquiry has a source.
This signal is handled in the views.py via AuditService.
This ensures all feedback from PX sources is tracked for analytics.
""" """
pass if created and hasattr(instance, 'source') and instance.source_id:
# Get content type for the instance
content_type = ContentType.objects.get_for_model(instance)
# Create or get the SourceUsage record
from .models import SourceUsage
SourceUsage.objects.get_or_create(
source_id=instance.source_id,
content_type=content_type,
object_id=instance.id,
defaults={
'hospital_id': getattr(instance, 'hospital_id', None),
'user_id': getattr(instance, 'created_by_id', None),
}
)
# Update cached stats on the source
if hasattr(instance.source, 'update_usage_stats'):
# Use transaction.on_commit to update stats after transaction completes
from django.db import transaction
transaction.on_commit(lambda: instance.source.update_usage_stats())

View File

@ -12,35 +12,41 @@ from .models import PXSource, SourceUser
from apps.accounts.models import User from apps.accounts.models import User
def check_source_permission(user):
"""Check if user has permission to manage sources"""
return user.is_px_admin() or user.is_hospital_admin()
@login_required @login_required
def source_list(request): def source_list(request):
""" """
List all PX sources List all PX sources
""" """
sources = PXSource.objects.all() sources = PXSource.objects.all()
# Filter by active status # Filter by active status
is_active = request.GET.get('is_active') is_active = request.GET.get('is_active')
if is_active: if is_active:
sources = sources.filter(is_active=is_active == 'true') sources = sources.filter(is_active=is_active == 'true')
# Search # Search
search = request.GET.get('search') search = request.GET.get('search')
if search: if search:
sources = sources.filter( sources = sources.filter(
models.Q(name_en__icontains=search) | models.Q(name_en__icontains=search) |
models.Q(name_ar__icontains=search) | models.Q(name_ar__icontains=search) |
models.Q(description__icontains=search) models.Q(description__icontains=search) |
models.Q(code__icontains=search)
) )
sources = sources.order_by('name_en') sources = sources.order_by('name_en')
context = { context = {
'sources': sources, 'sources': sources,
'is_active': is_active, 'is_active': is_active,
'search': search, 'search': search,
} }
return render(request, 'px_sources/source_list.html', context) return render(request, 'px_sources/source_list.html', context)
@ -53,21 +59,25 @@ def source_detail(request, pk):
usage_records = source.usage_records.select_related( usage_records = source.usage_records.select_related(
'content_type', 'hospital', 'user' 'content_type', 'hospital', 'user'
).order_by('-created_at')[:20] ).order_by('-created_at')[:20]
# Get source users for this source # Get source users for this source
source_users = source.source_users.select_related('user').order_by('-created_at') source_users = source.source_users.select_related('user').order_by('-created_at')
# Get available users (not already assigned to this source) # Get available users (not already assigned to this source)
assigned_user_ids = source_users.values_list('user_id', flat=True) assigned_user_ids = source_users.values_list('user_id', flat=True)
available_users = User.objects.exclude(id__in=assigned_user_ids).order_by('email') available_users = User.objects.exclude(id__in=assigned_user_ids).order_by('email')
# Get usage stats
usage_stats = source.get_usage_stats(days=30)
context = { context = {
'source': source, 'source': source,
'usage_records': usage_records, 'usage_records': usage_records,
'source_users': source_users, 'source_users': source_users,
'available_users': available_users, 'available_users': available_users,
'usage_stats': usage_stats,
} }
return render(request, 'px_sources/source_detail.html', context) return render(request, 'px_sources/source_detail.html', context)
@ -76,27 +86,33 @@ def source_create(request):
""" """
Create a new PX source Create a new PX source
""" """
# if not (request.user.is_px_admin() or request.user.is_hospital_admin()): if not check_source_permission(request.user):
# messages.error(request, _("You don't have permission to create sources.")) messages.error(request, _("You don't have permission to create sources."))
# return redirect('px_sources:source_list') return redirect('px_sources:source_list')
if request.method == 'POST': if request.method == 'POST':
try: try:
source = PXSource( source = PXSource(
code=request.POST.get('code', ''),
name_en=request.POST.get('name_en'), name_en=request.POST.get('name_en'),
name_ar=request.POST.get('name_ar', ''), name_ar=request.POST.get('name_ar', ''),
description=request.POST.get('description', ''), description=request.POST.get('description', ''),
source_type=request.POST.get('source_type', 'internal'),
contact_email=request.POST.get('contact_email', ''),
contact_phone=request.POST.get('contact_phone', ''),
is_active=request.POST.get('is_active') == 'on', is_active=request.POST.get('is_active') == 'on',
) )
source.save() source.save()
messages.success(request, _("Source created successfully!")) messages.success(request, _("Source created successfully!"))
return redirect('px_sources:source_detail', pk=source.pk) return redirect('px_sources:source_detail', pk=source.pk)
except Exception as e: except Exception as e:
messages.error(request, _("Error creating source: {}").format(str(e))) messages.error(request, _("Error creating source: {}").format(str(e)))
context = {} context = {
'source_types': PXSource.SOURCE_TYPE_CHOICES,
}
return render(request, 'px_sources/source_form.html', context) return render(request, 'px_sources/source_form.html', context)
@ -106,30 +122,35 @@ def source_edit(request, pk):
""" """
Edit an existing PX source Edit an existing PX source
""" """
if not (request.user.is_px_admin() or request.user.is_hospital_admin()): if not check_source_permission(request.user):
messages.error(request, _("You don't have permission to edit sources.")) messages.error(request, _("You don't have permission to edit sources."))
return redirect('px_sources:source_detail', pk=pk) return redirect('px_sources:source_detail', pk=pk)
source = get_object_or_404(PXSource, pk=pk) source = get_object_or_404(PXSource, pk=pk)
if request.method == 'POST': if request.method == 'POST':
try: try:
source.code = request.POST.get('code', source.code)
source.name_en = request.POST.get('name_en') source.name_en = request.POST.get('name_en')
source.name_ar = request.POST.get('name_ar', '') source.name_ar = request.POST.get('name_ar', '')
source.description = request.POST.get('description', '') source.description = request.POST.get('description', '')
source.source_type = request.POST.get('source_type', 'internal')
source.contact_email = request.POST.get('contact_email', '')
source.contact_phone = request.POST.get('contact_phone', '')
source.is_active = request.POST.get('is_active') == 'on' source.is_active = request.POST.get('is_active') == 'on'
source.save() source.save()
messages.success(request, _("Source updated successfully!")) messages.success(request, _("Source updated successfully!"))
return redirect('px_sources:source_detail', pk=source.pk) return redirect('px_sources:source_detail', pk=source.pk)
except Exception as e: except Exception as e:
messages.error(request, _("Error updating source: {}").format(str(e))) messages.error(request, _("Error updating source: {}").format(str(e)))
context = { context = {
'source': source, 'source': source,
'source_types': PXSource.SOURCE_TYPE_CHOICES,
} }
return render(request, 'px_sources/source_form.html', context) return render(request, 'px_sources/source_form.html', context)

View File

@ -143,10 +143,32 @@ def survey_form(request, token):
elif question.question_type == 'multiple_choice': elif question.question_type == 'multiple_choice':
choice_value = request.POST.get(field_name) choice_value = request.POST.get(field_name)
if choice_value: if choice_value:
# Find the selected choice to get its label
selected_choice = None
for choice in question.choices_json:
if str(choice.get('value', '')) == str(choice_value):
selected_choice = choice
break
# Get the label based on language
language = request.POST.get('language', 'en')
if language == 'ar' and selected_choice and selected_choice.get('label_ar'):
text_value = selected_choice['label_ar']
elif selected_choice and selected_choice.get('label'):
text_value = selected_choice['label']
else:
text_value = choice_value
# Try to convert choice value to numeric for scoring
try:
numeric_value = float(choice_value)
except (ValueError, TypeError):
numeric_value = None
responses_data.append({ responses_data.append({
'question': question, 'question': question,
'numeric_value': None, 'numeric_value': numeric_value,
'text_value': '', 'text_value': text_value,
'choice_value': choice_value 'choice_value': choice_value
}) })

View File

@ -160,17 +160,48 @@ class SurveySubmissionSerializer(serializers.Serializer):
survey_instance = self.context['survey_instance'] survey_instance = self.context['survey_instance']
responses_data = validated_data['responses'] responses_data = validated_data['responses']
from apps.surveys.models import SurveyResponse from apps.surveys.models import SurveyResponse, SurveyQuestion
from django.utils import timezone from django.utils import timezone
# Create responses # Create responses
for response_data in responses_data: for response_data in responses_data:
question_id = response_data['question_id']
choice_value = response_data.get('choice_value', '')
numeric_value = response_data.get('numeric_value')
text_value = response_data.get('text_value', '')
# For multiple_choice with choice_value but missing numeric/text values,
# look up the choice details from the question
if choice_value and numeric_value is None:
try:
question = SurveyQuestion.objects.get(id=question_id)
if question.question_type == 'multiple_choice':
# Find the selected choice to get its label
selected_choice = None
for choice in question.choices_json:
if str(choice.get('value', '')) == str(choice_value):
selected_choice = choice
break
# Set text_value from choice label if not provided
if not text_value and selected_choice:
text_value = selected_choice.get('label', choice_value)
# Set numeric_value from choice value if it's numeric
if numeric_value is None:
try:
numeric_value = float(choice_value)
except (ValueError, TypeError):
numeric_value = None
except SurveyQuestion.DoesNotExist:
pass
SurveyResponse.objects.create( SurveyResponse.objects.create(
survey_instance=survey_instance, survey_instance=survey_instance,
question_id=response_data['question_id'], question_id=question_id,
numeric_value=response_data.get('numeric_value'), numeric_value=numeric_value,
text_value=response_data.get('text_value', ''), text_value=text_value,
choice_value=response_data.get('choice_value', ''), choice_value=choice_value,
response_time_seconds=response_data.get('response_time_seconds') response_time_seconds=response_data.get('response_time_seconds')
) )

View File

@ -329,8 +329,7 @@ def create_action_from_negative_survey(survey_instance_id):
survey = SurveyInstance.objects.select_related( survey = SurveyInstance.objects.select_related(
'survey_template', 'survey_template',
'patient', 'patient',
'hospital', 'hospital'
'department'
).get(id=survey_instance_id) ).get(id=survey_instance_id)
# Verify survey is negative # Verify survey is negative
@ -404,7 +403,7 @@ def create_action_from_negative_survey(survey_instance_id):
title=f"Negative Survey: {survey.survey_template.name} (Score: {score:.1f})", title=f"Negative Survey: {survey.survey_template.name} (Score: {score:.1f})",
description=description, description=description,
hospital=survey.hospital, hospital=survey.hospital,
department=survey.department, department=None,
category=category, category=category,
priority=priority, priority=priority,
severity=severity, severity=severity,

View File

@ -0,0 +1,36 @@
"""
Custom Celery Beat scheduler to fix Python 3.12 zoneinfo compatibility issue.
This patches the _default_now method to work with zoneinfo.ZoneInfo instead of pytz.
"""
from django_celery_beat.schedulers import DatabaseScheduler, ModelEntry
class PatchedModelEntry(ModelEntry):
"""
Custom model entry that fixes the zoneinfo.ZoneInfo compatibility issue.
"""
def _default_now(self):
"""
Return the current time in the configured timezone.
This fixes the AttributeError: 'zoneinfo.ZoneInfo' object has no attribute 'localize'
"""
from django.utils import timezone
return timezone.now()
class PatchedDatabaseScheduler(DatabaseScheduler):
"""
Custom scheduler that fixes the zoneinfo.ZoneInfo compatibility issue
in django-celery-beat 2.1.0 with Python 3.12.
"""
Entry = PatchedModelEntry
def _default_now(self):
"""
Return the current time in the configured timezone.
This fixes the AttributeError: 'zoneinfo.ZoneInfo' object has no attribute 'localize'
"""
from django.utils import timezone
return timezone.now()

View File

@ -251,7 +251,7 @@ CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
# Celery Beat Schedule # Celery Beat Schedule
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' CELERY_BEAT_SCHEDULER = 'config.celery_scheduler:PatchedDatabaseScheduler'
# Logging Configuration # Logging Configuration
LOGGING = { LOGGING = {

View File

@ -25,8 +25,8 @@ urlpatterns = [
path('core/', include('apps.core.urls', namespace='core')), path('core/', include('apps.core.urls', namespace='core')),
# UI Pages # UI Pages
path('complaints/', include('apps.complaints.urls', namespace='complaints')), path('complaints/', include('apps.complaints.urls')),
path('physicians/', include('apps.physicians.urls')), path('physicians/', include('apps.physicians.urls', namespace='physicians')),
path('feedback/', include('apps.feedback.urls')), path('feedback/', include('apps.feedback.urls')),
path('actions/', include('apps.px_action_center.urls')), path('actions/', include('apps.px_action_center.urls')),
path('accounts/', include('apps.accounts.urls', namespace='accounts')), path('accounts/', include('apps.accounts.urls', namespace='accounts')),
@ -41,18 +41,18 @@ urlpatterns = [
path('ai-engine/', include('apps.ai_engine.urls')), path('ai-engine/', include('apps.ai_engine.urls')),
path('appreciation/', include('apps.appreciation.urls', namespace='appreciation')), path('appreciation/', include('apps.appreciation.urls', namespace='appreciation')),
path('notifications/', include('apps.notifications.urls', namespace='notifications')), path('notifications/', include('apps.notifications.urls', namespace='notifications')),
path('observations/', include('apps.observations.urls', namespace='observations')), path('observations/', include('apps.observations.urls')),
path('px-sources/', include('apps.px_sources.urls')), path('px-sources/', include('apps.px_sources.urls')),
path('references/', include('apps.references.urls', namespace='references')), path('references/', include('apps.references.urls', namespace='references')),
path('standards/', include('apps.standards.urls', namespace='standards')), path('standards/', include('apps.standards.urls')),
# API endpoints # API endpoints
path('api/auth/', include('apps.accounts.urls', namespace='api_auth')), path('api/auth/', include('apps.accounts.urls', namespace='api_auth')),
path('api/physicians/', include('apps.physicians.urls')), path('api/physicians/', include('apps.physicians.urls', namespace='api_physicians')),
path('api/integrations/', include('apps.integrations.urls')), path('api/integrations/', include('apps.integrations.urls')),
path('api/notifications/', include('apps.notifications.urls')), path('api/notifications/', include('apps.notifications.urls', namespace='api_notifications')),
path('api/v1/appreciation/', include('apps.appreciation.urls', namespace='api_appreciation')), path('api/v1/appreciation/', include('apps.appreciation.urls', namespace='api_appreciation')),
path('api/simulator/', include('apps.simulator.urls', namespace='simulator')), path('api/simulator/', include('apps.simulator.urls', namespace='api_simulator')),
# OpenAPI/Swagger documentation # OpenAPI/Swagger documentation
path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/', SpectacularAPIView.as_view(), name='schema'),

BIN
db.sqlite3.tar.gz Normal file

Binary file not shown.

View File

@ -5,6 +5,63 @@
{% block title %}{{ report.indicator_title }} - {% trans "KPI Report" %}{% endblock %} {% block title %}{{ report.indicator_title }} - {% trans "KPI Report" %}{% endblock %}
{% block extra_css %}
<style>
/* Button Styles */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
background: linear-gradient(to right, #005696, #007bbd);
color: white;
font-size: 0.875rem;
font-weight: 600;
border-radius: 0.75rem;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 6px -1px rgba(0, 86, 150, 0.2);
}
.btn-primary:hover {
opacity: 0.9;
box-shadow: 0 6px 8px -1px rgba(0, 86, 150, 0.3);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
background: white;
color: #475569;
font-size: 0.875rem;
font-weight: 600;
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: #f8fafc;
border-color: #cbd5e1;
}
/* Card styling */
.card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="p-6"> <div class="p-6">
<!-- Header --> <!-- Header -->
@ -227,6 +284,195 @@
</div> </div>
</div> </div>
<!-- AI Analysis Section -->
<div class="card mb-6" id="aiAnalysisSection">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-bold text-navy uppercase flex items-center gap-2">
<i data-lucide="brain" class="w-4 h-4"></i>
{% trans "AI-Generated Analysis" %}
{% if report.ai_analysis_generated_at %}
<span class="text-xs font-normal text-slate lowercase">
({% trans "Generated:" %} {{ report.ai_analysis_generated_at|date:"Y-m-d H:i" }})
</span>
{% endif %}
</h3>
<div class="flex gap-2">
{% if report.ai_analysis %}
<button id="editAiAnalysisBtn" class="btn-secondary flex items-center gap-2 text-xs"
data-report-id="{{ report.id }}">
<i data-lucide="edit" class="w-4 h-4"></i>
{% trans "Edit Analysis" %}
</button>
{% endif %}
<button id="generateAiAnalysisBtn" class="btn-primary flex items-center gap-2 text-xs"
data-report-id="{{ report.id }}">
<i data-lucide="sparkles" class="w-4 h-4"></i>
{% if report.ai_analysis %}{% trans "Regenerate" %}{% else %}{% trans "Generate" %}{% endif %}
</button>
</div>
</div>
<!-- View Mode -->
<div id="aiAnalysisContent">
{% if report.ai_analysis %}
{% with analysis=report.ai_analysis %}
<!-- Executive Summary -->
{% if analysis.executive_summary %}
<div class="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 class="text-sm font-bold text-blue mb-2">{% trans "Executive Summary" %}</h4>
<p class="text-sm text-navy">{{ analysis.executive_summary }}</p>
</div>
{% endif %}
<!-- Performance Analysis -->
{% if analysis.performance_analysis %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-2">{% trans "Performance Analysis" %}</h4>
<p class="text-sm text-slate">{{ analysis.performance_analysis }}</p>
</div>
{% endif %}
<!-- Key Findings -->
{% if analysis.key_findings %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-2">{% trans "Key Findings" %}</h4>
<ul class="list-disc list-inside text-sm text-slate space-y-1">
{% for finding in analysis.key_findings %}
<li>{{ finding }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Reasons for Delays -->
{% if analysis.reasons_for_delays %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-2">{% trans "Reasons for Delays" %}</h4>
<ul class="list-disc list-inside text-sm text-slate space-y-1">
{% for reason in analysis.reasons_for_delays %}
<li>{{ reason }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Resolution Time Analysis -->
{% if analysis.resolution_time_analysis %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-2">{% trans "Resolution Time Breakdown" %}</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
{% if analysis.resolution_time_analysis.within_24h %}
<div class="p-3 bg-green-50 border border-green-200 rounded text-center">
<p class="text-xs text-slate">{% trans "Within 24h" %}</p>
<p class="text-lg font-bold text-green-600">{{ analysis.resolution_time_analysis.within_24h.count }}</p>
<p class="text-xs text-green-600">{{ analysis.resolution_time_analysis.within_24h.percentage }}</p>
</div>
{% endif %}
{% if analysis.resolution_time_analysis.within_48h %}
<div class="p-3 bg-blue-50 border border-blue-200 rounded text-center">
<p class="text-xs text-slate">{% trans "Within 48h" %}</p>
<p class="text-lg font-bold text-blue-600">{{ analysis.resolution_time_analysis.within_48h.count }}</p>
<p class="text-xs text-blue-600">{{ analysis.resolution_time_analysis.within_48h.percentage }}</p>
</div>
{% endif %}
{% if analysis.resolution_time_analysis.within_72h %}
<div class="p-3 bg-yellow-50 border border-yellow-200 rounded text-center">
<p class="text-xs text-slate">{% trans "Within 72h" %}</p>
<p class="text-lg font-bold text-yellow-600">{{ analysis.resolution_time_analysis.within_72h.count }}</p>
<p class="text-xs text-yellow-600">{{ analysis.resolution_time_analysis.within_72h.percentage }}</p>
</div>
{% endif %}
{% if analysis.resolution_time_analysis.over_72h %}
<div class="p-3 bg-red-50 border border-red-200 rounded text-center">
<p class="text-xs text-slate">{% trans "Over 72h" %}</p>
<p class="text-lg font-bold text-red-600">{{ analysis.resolution_time_analysis.over_72h.count }}</p>
<p class="text-xs text-red-600">{{ analysis.resolution_time_analysis.over_72h.percentage }}</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Recommendations -->
{% if analysis.recommendations %}
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 class="text-sm font-bold text-green-800 mb-2">{% trans "Recommendations" %}</h4>
<ul class="list-disc list-inside text-sm text-green-700 space-y-1">
{% for rec in analysis.recommendations %}
<li>{{ rec }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
{% else %}
<div class="text-center py-8 text-slate" id="noAnalysisMessage">
<i data-lucide="sparkles" class="w-12 h-12 mx-auto mb-3 opacity-50"></i>
<p>{% trans "No AI analysis available yet. Click 'Generate Analysis' to create one." %}</p>
</div>
{% endif %}
</div>
<!-- Loading State -->
<div id="aiAnalysisLoading" class="hidden text-center py-8">
<div class="inline-block w-8 h-8 border-4 border-slate-200 border-t-blue rounded-full animate-spin mb-3"></div>
<p class="text-slate">{% trans "Generating AI analysis... This may take a moment." %}</p>
</div>
<!-- Edit Mode Form -->
<div id="aiAnalysisEditForm" class="hidden">
{% if report.ai_analysis %}
<div class="space-y-4">
<!-- Executive Summary -->
<div>
<label class="block text-sm font-bold text-navy mb-1">{% trans "Executive Summary" %}</label>
<textarea id="editExecutiveSummary" rows="3" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-blue">{{ report.ai_analysis.executive_summary|default:"" }}</textarea>
</div>
<!-- Performance Analysis -->
<div>
<label class="block text-sm font-bold text-navy mb-1">{% trans "Performance Analysis" %}</label>
<textarea id="editPerformanceAnalysis" rows="4" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-blue">{{ report.ai_analysis.performance_analysis|default:"" }}</textarea>
</div>
<!-- Key Findings -->
<div>
<label class="block text-sm font-bold text-navy mb-1">{% trans "Key Findings (one per line)" %}</label>
<textarea id="editKeyFindings" rows="4" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-blue">{% if report.ai_analysis.key_findings %}{% for finding in report.ai_analysis.key_findings %}{{ finding }}{% if not forloop.last %}
{% endif %}{% endfor %}{% endif %}</textarea>
</div>
<!-- Reasons for Delays -->
<div>
<label class="block text-sm font-bold text-navy mb-1">{% trans "Reasons for Delays (one per line)" %}</label>
<textarea id="editReasonsForDelays" rows="4" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-blue">{% if report.ai_analysis.reasons_for_delays %}{% for reason in report.ai_analysis.reasons_for_delays %}{{ reason }}{% if not forloop.last %}
{% endif %}{% endfor %}{% endif %}</textarea>
</div>
<!-- Recommendations -->
<div>
<label class="block text-sm font-bold text-navy mb-1">{% trans "Recommendations (one per line)" %}</label>
<textarea id="editRecommendations" rows="4" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-blue">{% if report.ai_analysis.recommendations %}{% for rec in report.ai_analysis.recommendations %}{{ rec }}{% if not forloop.last %}
{% endif %}{% endfor %}{% endif %}</textarea>
</div>
<!-- Action Buttons -->
<div class="flex gap-2 pt-4 border-t">
<button id="saveAiAnalysisBtn" class="btn-primary flex items-center gap-2 text-sm"
data-report-id="{{ report.id }}">
<i data-lucide="save" class="w-4 h-4"></i>
{% trans "Save Changes" %}
</button>
<button id="cancelEditBtn" class="btn-secondary flex items-center gap-2 text-sm">
<i data-lucide="x" class="w-4 h-4"></i>
{% trans "Cancel" %}
</button>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Report Info Footer --> <!-- Report Info Footer -->
<div class="text-xs text-slate text-center"> <div class="text-xs text-slate text-center">
<p> <p>
@ -361,6 +607,129 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Error rendering source chart:', e); console.error('Error rendering source chart:', e);
} }
} }
// AI Analysis Generation
const generateBtn = document.getElementById('generateAiAnalysisBtn');
if (generateBtn) {
generateBtn.addEventListener('click', async function() {
const reportId = this.dataset.reportId;
const contentDiv = document.getElementById('aiAnalysisContent');
const loadingDiv = document.getElementById('aiAnalysisLoading');
// Show loading, hide content
contentDiv.classList.add('hidden');
loadingDiv.classList.remove('hidden');
generateBtn.disabled = true;
try {
const response = await fetch(`/analytics/api/kpi-reports/${reportId}/ai-analysis/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1]
}
});
const data = await response.json();
if (data.success) {
// Reload page to show new analysis
window.location.reload();
} else {
alert(data.error || 'Failed to generate analysis');
contentDiv.classList.remove('hidden');
loadingDiv.classList.add('hidden');
generateBtn.disabled = false;
}
} catch (error) {
console.error('Error:', error);
alert('Failed to generate analysis. Please try again.');
contentDiv.classList.remove('hidden');
loadingDiv.classList.add('hidden');
generateBtn.disabled = false;
}
});
}
// Edit AI Analysis
const editBtn = document.getElementById('editAiAnalysisBtn');
const cancelEditBtn = document.getElementById('cancelEditBtn');
const saveBtn = document.getElementById('saveAiAnalysisBtn');
const contentDiv = document.getElementById('aiAnalysisContent');
const editForm = document.getElementById('aiAnalysisEditForm');
if (editBtn && contentDiv && editForm) {
// Enter edit mode
editBtn.addEventListener('click', function() {
contentDiv.classList.add('hidden');
editForm.classList.remove('hidden');
editBtn.classList.add('hidden');
if (typeof lucide !== 'undefined') lucide.createIcons();
});
}
if (cancelEditBtn && contentDiv && editForm) {
// Cancel edit mode
cancelEditBtn.addEventListener('click', function() {
editForm.classList.add('hidden');
contentDiv.classList.remove('hidden');
editBtn.classList.remove('hidden');
});
}
if (saveBtn && editForm) {
// Save edited analysis
saveBtn.addEventListener('click', async function() {
const reportId = this.dataset.reportId;
// Build analysis object from form fields
const analysis = {
executive_summary: document.getElementById('editExecutiveSummary')?.value || '',
performance_analysis: document.getElementById('editPerformanceAnalysis')?.value || '',
key_findings: (document.getElementById('editKeyFindings')?.value || '').split('\n').filter(f => f.trim()),
reasons_for_delays: (document.getElementById('editReasonsForDelays')?.value || '').split('\n').filter(r => r.trim()),
recommendations: (document.getElementById('editRecommendations')?.value || '').split('\n').filter(r => r.trim()),
// Preserve existing resolution_time_analysis and other fields
resolution_time_analysis: {{ report.ai_analysis.resolution_time_analysis|safe|default:"null" }},
department_analysis: {{ report.ai_analysis.department_analysis|safe|default:"null" }},
source_analysis: {{ report.ai_analysis.source_analysis|safe|default:"null" }}
};
// Disable button during save
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="inline-block w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></span>Saving...';
try {
const response = await fetch(`/analytics/api/kpi-reports/${reportId}/ai-analysis/save/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1]
},
body: JSON.stringify({ analysis: analysis })
});
const data = await response.json();
if (data.success) {
// Reload page to show updated analysis
window.location.reload();
} else {
alert(data.error || 'Failed to save analysis');
saveBtn.disabled = false;
saveBtn.innerHTML = '<i data-lucide="save" class="w-4 h-4"></i> Save Changes';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
} catch (error) {
console.error('Error:', error);
alert('Failed to save analysis. Please try again.');
saveBtn.disabled = false;
saveBtn.innerHTML = '<i data-lucide="save" class="w-4 h-4"></i> Save Changes';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -42,7 +42,7 @@
<!-- Statistics Cards --> <!-- Statistics Cards -->
<div class="grid grid-cols-4 gap-6 mb-6"> <div class="grid grid-cols-4 gap-6 mb-6">
<div class="bg-white p-4 rounded-2xl border shadow-sm flex items-center gap-4"> <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-4 hover:border-blue/30 hover:shadow-md transition-all">
<div class="p-3 bg-blue/10 rounded-xl"> <div class="p-3 bg-blue/10 rounded-xl">
<i data-lucide="bar-chart-3" class="text-blue w-5 h-5"></i> <i data-lucide="bar-chart-3" class="text-blue w-5 h-5"></i>
</div> </div>
@ -51,7 +51,7 @@
<p class="text-xl font-black text-navy leading-tight">{{ stats.total }}</p> <p class="text-xl font-black text-navy leading-tight">{{ stats.total }}</p>
</div> </div>
</div> </div>
<div class="bg-white p-4 rounded-2xl border shadow-sm flex items-center gap-4"> <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-4 hover:border-green-400/50 hover:shadow-md transition-all">
<div class="p-3 bg-green-50 rounded-xl"> <div class="p-3 bg-green-50 rounded-xl">
<i data-lucide="check-circle" class="text-green-600 w-5 h-5"></i> <i data-lucide="check-circle" class="text-green-600 w-5 h-5"></i>
</div> </div>
@ -60,7 +60,7 @@
<p class="text-xl font-black text-navy leading-tight">{{ stats.completed }}</p> <p class="text-xl font-black text-navy leading-tight">{{ stats.completed }}</p>
</div> </div>
</div> </div>
<div class="bg-white p-4 rounded-2xl border shadow-sm flex items-center gap-4"> <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-4 hover:border-yellow-400/50 hover:shadow-md transition-all">
<div class="p-3 bg-yellow-50 rounded-xl"> <div class="p-3 bg-yellow-50 rounded-xl">
<i data-lucide="clock" class="text-yellow-600 w-5 h-5"></i> <i data-lucide="clock" class="text-yellow-600 w-5 h-5"></i>
</div> </div>
@ -69,7 +69,7 @@
<p class="text-xl font-black text-navy leading-tight">{{ stats.pending }}</p> <p class="text-xl font-black text-navy leading-tight">{{ stats.pending }}</p>
</div> </div>
</div> </div>
<div class="bg-white p-4 rounded-2xl border shadow-sm flex items-center gap-4"> <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center gap-4 hover:border-red-400/50 hover:shadow-md transition-all">
<div class="p-3 bg-red-50 rounded-xl"> <div class="p-3 bg-red-50 rounded-xl">
<i data-lucide="alert-triangle" class="text-red-500 w-5 h-5"></i> <i data-lucide="alert-triangle" class="text-red-500 w-5 h-5"></i>
</div> </div>
@ -171,7 +171,7 @@
<div class="bg-white rounded-b-2xl shadow-sm border p-6"> <div class="bg-white rounded-b-2xl shadow-sm border p-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for report in reports %} {% for report in reports %}
<div class="card hover:shadow-lg hover:-translate-y-1 transition-all duration-200 cursor-pointer group" <div class="card bg-white p-5 border border-slate-200 rounded-2xl hover:shadow-lg hover:-translate-y-1 transition-all duration-200 cursor-pointer group"
onclick="window.location.href='{% url 'analytics:kpi_report_detail' report.id %}'"> onclick="window.location.href='{% url 'analytics:kpi_report_detail' report.id %}'">
<!-- Header --> <!-- Header -->
<div class="flex items-start justify-between mb-3"> <div class="flex items-start justify-between mb-3">

View File

@ -1,3 +1,6 @@
{% load i18n %}
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -5,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ report.indicator_title }} - {% trans "KPI Report" %}</title> <title>{{ report.indicator_title }} - {% trans "KPI Report" %}</title>
<!-- Tailwind CSS --> <!-- Tailwind CSS (for PDF preview only) -->
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons --> <!-- Lucide Icons -->
@ -33,6 +36,217 @@
</script> </script>
<style> <style>
/* Base styles that work without Tailwind */
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
background: white;
color: #334155;
line-height: 1.5;
}
/* Utility classes that match Tailwind */
.bg-white { background-color: white; }
.bg-navy { background-color: #005696; }
.bg-blue { background-color: #007bbd; }
.bg-light { background-color: #eef6fb; }
.text-white { color: white; }
.text-navy { color: #005696; }
.text-slate { color: #64748b; }
.text-slate-800 { color: #1e293b; }
.font-bold { font-weight: 700; }
.font-semibold { font-weight: 600; }
.text-2xl { font-size: 1.5rem; }
.text-3xl { font-size: 1.875rem; }
.text-sm { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; }
.text-xl { font-size: 1.25rem; }
.text-lg { font-size: 1.125rem; }
.font-black { font-weight: 900; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-2xl { border-radius: 1rem; }
.rounded-full { border-radius: 9999px; }
.border { border: 1px solid #e2e8f0; }
.shadow-sm { box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-5 { padding: 1.25rem; }
.p-6 { padding: 1.5rem; }
.p-8 { padding: 2rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-1_5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2_5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.m-0 { margin: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.mt-12 { margin-top: 3rem; }
.mr-1 { margin-right: 0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.inline { display: inline; }
.inline-block { display: inline-block; }
.block { display: block; }
.hidden { display: none; }
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.grid-cols-5 { grid-template-columns: repeat(5, 1fr); }
.w-full { width: 100%; }
.w-3 { width: 0.75rem; }
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-8 { width: 2rem; }
.h-3 { height: 0.75rem; }
.h-4 { height: 1rem; }
.h-5 { height: 1.25rem; }
.h-8 { height: 2rem; }
.h-10 { height: 2.5rem; }
.min-h-220 { min-height: 220px; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; }
.flex-col { flex-direction: column; }
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.top-4 { top: 1rem; }
.right-4 { right: 1rem; }
.z-50 { z-index: 50; }
.uppercase { text-transform: uppercase; }
.italic { font-style: italic; }
.whitespace-pre-line { white-space: pre-line; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.border-collapse { border-collapse: collapse; }
.border-b { border-bottom: 1px solid #e2e8f0; }
.border-b-2 { border-bottom: 2px solid; }
.border-t { border-top: 1px solid #e2e8f0; }
.border-t-2 { border-top: 2px solid; }
.border-l { border-left: 1px solid #e2e8f0; }
.border-slate-200 { border-color: #e2e8f0; }
.border-slate-300 { border-color: #cbd5e1; }
.border-slate-400 { border-color: #94a3b8; }
.border-navy { border-color: #005696; }
.border-green-200 { border-color: #bbf7d0; }
.border-red-200 { border-color: #fecaca; }
.border-blue-200 { border-color: #bfdbfe; }
.border-yellow-200 { border-color: #fde68a; }
.bg-green-50 { background-color: #f0fdf4; }
.bg-green-100 { background-color: #dcfce7; }
.bg-green-200 { background-color: #bbf7d0; }
.bg-green-600 { background-color: #16a34a; }
.bg-green-700 { background-color: #15803d; }
.bg-green-800 { background-color: #166534; }
.bg-red-50 { background-color: #fef2f2; }
.bg-red-100 { background-color: #fee2e2; }
.bg-red-200 { background-color: #fecaca; }
.bg-red-500 { background-color: #ef4444; }
.bg-red-600 { background-color: #dc2626; }
.bg-yellow-50 { background-color: #fefce8; }
.bg-yellow-100 { background-color: #fef9c3; }
.bg-yellow-200 { background-color: #fde68a; }
.bg-yellow-600 { background-color: #ca8a04; }
.bg-yellow-800 { background-color: #854d0e; }
.bg-blue-50 { background-color: #eff6ff; }
.bg-blue-100 { background-color: #dbeafe; }
.bg-blue-200 { background-color: #bfdbfe; }
.bg-slate-50 { background-color: #f8fafc; }
.bg-slate-100 { background-color: #f1f5f9; }
.bg-slate-200 { background-color: #e2e8f0; }
.bg-slate-300 { background-color: #cbd5e1; }
.bg-slate-400 { background-color: #94a3b8; }
.bg-slate-600 { background-color: #475569; }
.text-green-600 { color: #16a34a; }
.text-green-700 { color: #15803d; }
.text-green-800 { color: #166534; }
.text-red-500 { color: #ef4444; }
.text-red-600 { color: #dc2626; }
.text-red-800 { color: #991b1b; }
.text-yellow-600 { color: #ca8a04; }
.text-yellow-800 { color: #854d0e; }
.text-blue { color: #007bbd; }
.text-blue-600 { color: #2563eb; }
.text-blue-700 { color: #1d4ed8; }
.text-blue-800 { color: #1e40af; }
.hover\:opacity-90:hover { opacity: 0.9; }
.hover\:bg-blue:hover { background-color: #007bbd; }
.hover\:bg-slate-600:hover { background-color: #475569; }
.hover\:bg-slate-50:hover { background-color: #f8fafc; }
.hover\:-translate-y-1:hover { transform: translateY(-0.25rem); }
.hover\:shadow-lg:hover { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
.hover\:underline:hover { text-decoration: underline; }
.transition { transition: all 0.2s; }
.transition-all { transition: all 0.2s; }
.transition-colors { transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
.cursor-pointer { cursor: pointer; }
.cursor-not-allowed { cursor: not-allowed; }
.overflow-hidden { overflow: hidden; }
.overflow-x-auto { overflow-x: auto; }
.overflow-visible { overflow: visible; }
.list-disc { list-style-type: disc; }
.list-inside { list-style-position: inside; }
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-4 > * + * { margin-top: 1rem; }
.pt-1 { padding-top: 0.25rem; }
.pt-3 { padding-top: 0.75rem; }
.pt-4 { padding-top: 1rem; }
.pb-1 { padding-bottom: 0.25rem; }
.pb-2 { padding-bottom: 0.5rem; }
.pb-4 { padding-bottom: 1rem; }
.pb-8 { padding-bottom: 2rem; }
.pl-4 { padding-left: 1rem; }
.pr-4 { padding-right: 1rem; }
.tracking-wider { letter-spacing: 0.05em; }
.tracking-tight { letter-spacing: -0.025em; }
.leading-tight { line-height: 1.25; }
/* Animation */
.animate-spin { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* Print styles */
@media print { @media print {
.no-print { display: none !important; } .no-print { display: none !important; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
@ -43,6 +257,46 @@
.page-break { page-break-after: always; } .page-break { page-break-after: always; }
.no-break { page-break-inside: avoid; } .no-break { page-break-inside: avoid; }
/* Chart container styles */
#trendChart, #sourceChart {
max-width: 100% !important;
overflow: visible !important;
position: relative;
}
/* Ensure charts fit within PDF */
.apexcharts-canvas {
max-width: 100% !important;
}
/* Static chart images for PDF */
.chart-static-image {
max-width: 100% !important;
height: auto !important;
display: block;
background: white;
}
/* Ensure proper sizing for PDF capture */
.apexcharts-svg {
max-width: 100% !important;
}
#trendChart, #sourceChart {
min-height: 220px;
background: white;
}
/* Prevent horizontal overflow */
#report-content {
overflow-x: hidden !important;
}
/* Ensure proper sizing for PDF capture */
.apexcharts-svg {
max-width: 100% !important;
}
</style> </style>
</head> </head>
<body class="bg-white text-slate-800"> <body class="bg-white text-slate-800">
@ -63,7 +317,7 @@
</div> </div>
<!-- Report Container --> <!-- Report Container -->
<div id="report-content" class="max-w-[1200px] mx-auto p-8"> <div id="report-content" class="w-full mx-auto p-8">
<!-- Header with Logo --> <!-- Header with Logo -->
<div class="flex justify-between items-start mb-8 pb-4 border-b-2 border-navy no-break"> <div class="flex justify-between items-start mb-8 pb-4 border-b-2 border-navy no-break">
@ -184,24 +438,24 @@
</div> </div>
<!-- Charts --> <!-- Charts -->
<div class="grid grid-cols-3 gap-6 mb-8 no-break"> <div class="mb-8">
<!-- Trend Chart --> <!-- Trend Chart -->
<div class="col-span-2 border excel-border p-4 rounded-lg bg-slate-50/50"> <div class="border excel-border p-4 rounded-lg bg-slate-50/50 mb-6">
<p class="text-xs font-bold text-navy uppercase mb-4 flex items-center gap-2"> <p class="text-xs font-bold text-navy uppercase mb-4 flex items-center gap-2">
<i data-lucide="trending-up" class="w-3 h-3"></i> <i data-lucide="trending-up" class="w-3 h-3"></i>
{% trans "Monthly Performance Trend (%)" %} {% trans "Monthly Performance Trend (%)" %}
<span class="text-slate font-normal">[{% trans "Target:" %} {{ report.target_percentage }}%]</span> <span class="text-slate font-normal">[{% trans "Target:" %} {{ report.target_percentage }}%]</span>
</p> </p>
<div id="trendChart" style="height: 250px;"></div> <div id="trendChart" style="height: 220px; width: 100%;"></div>
</div> </div>
<!-- Source Chart --> <!-- Source Chart -->
<div class="border excel-border p-4 rounded-lg bg-slate-50/50"> <div class="border excel-border p-4 rounded-lg bg-slate-50/50" style="max-width: 400px; margin: 0 auto;">
<p class="text-xs font-bold text-navy uppercase mb-4 flex items-center gap-2"> <p class="text-xs font-bold text-navy uppercase mb-4 flex items-center gap-2 text-center">
<i data-lucide="pie-chart" class="w-3 h-3"></i> <i data-lucide="pie-chart" class="w-3 h-3"></i>
{% trans "Complaints by Source" %} {% trans "Complaints by Source" %}
</p> </p>
<div id="sourceChart" style="height: 250px;"></div> <div id="sourceChart" style="height: 220px; width: 100%;"></div>
</div> </div>
</div> </div>
@ -211,7 +465,7 @@
<i data-lucide="building" class="w-3 h-3"></i> <i data-lucide="building" class="w-3 h-3"></i>
{% trans "Department Breakdown" %} {% trans "Department Breakdown" %}
</h3> </h3>
<div class="grid grid-cols-2 border-t border-l excel-border"> <div class="grid grid-cols-2 border-t border-l excel-border">
{% for dept in department_breakdowns %} {% for dept in department_breakdowns %}
<div class="p-4 border-r border-b excel-border <div class="p-4 border-r border-b excel-border
@ -241,6 +495,166 @@
</div> </div>
</div> </div>
<!-- AI Analysis Section -->
{% if report.ai_analysis %}
<div class="page-break"></div>
<div class="mt-8 no-break">
<h3 class="text-lg font-bold text-navy mb-4 pb-2 border-b-2 border-navy">
<i data-lucide="brain" class="w-5 h-5 inline mr-2"></i>
{% trans "AI-Generated Analysis" %}
</h3>
{% with analysis=report.ai_analysis %}
<!-- Executive Summary -->
{% if analysis.executive_summary %}
<div class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded">
<h4 class="text-sm font-bold text-blue mb-1">{% trans "Executive Summary" %}</h4>
<p class="text-sm text-navy">{{ analysis.executive_summary }}</p>
</div>
{% endif %}
<!-- Performance Analysis -->
{% if analysis.performance_analysis %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Performance Analysis" %}</h4>
<p class="text-sm text-slate">{{ analysis.performance_analysis }}</p>
</div>
{% endif %}
<!-- Comparison to Target (for specific report types) -->
{% if analysis.comparison_to_target %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Comparison to Target" %}</h4>
<p class="text-sm text-slate">{{ analysis.comparison_to_target }}</p>
</div>
{% endif %}
<!-- Key Findings -->
{% if analysis.key_findings %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Key Findings" %}</h4>
<ul class="list-disc list-inside text-sm text-slate">
{% for finding in analysis.key_findings %}
<li>{{ finding }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Reasons for Delays -->
{% if analysis.reasons_for_delays %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Reasons for Delays" %}</h4>
<ul class="list-disc list-inside text-sm text-slate">
{% for reason in analysis.reasons_for_delays %}
<li>{{ reason }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Delay Reasons (alternative format) -->
{% if analysis.delay_reasons %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Reasons for Delays" %}</h4>
<ul class="list-disc list-inside text-sm text-slate">
{% for reason in analysis.delay_reasons %}
<li>{{ reason }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- Resolution Time Analysis -->
{% if analysis.resolution_time_analysis %}
<div class="mb-4">
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Resolution Time Breakdown" %}</h4>
<div class="grid grid-cols-4 gap-2 text-center text-sm">
{% if analysis.resolution_time_analysis.within_24h %}
<div class="p-2 bg-green-50 border border-green-200 rounded">
<p class="text-xs text-slate">{% trans "Within 24h" %}</p>
<p class="font-bold text-green-600">{{ analysis.resolution_time_analysis.within_24h.count }}</p>
<p class="text-xs text-green-600">{{ analysis.resolution_time_analysis.within_24h.percentage }}</p>
</div>
{% endif %}
{% if analysis.resolution_time_analysis.within_48h %}
<div class="p-2 bg-blue-50 border border-blue-200 rounded">
<p class="text-xs text-slate">{% trans "Within 48h" %}</p>
<p class="font-bold text-blue-600">{{ analysis.resolution_time_analysis.within_48h.count }}</p>
<p class="text-xs text-blue-600">{{ analysis.resolution_time_analysis.within_48h.percentage }}</p>
</div>
{% endif %}
{% if analysis.resolution_time_analysis.within_72h %}
<div class="p-2 bg-yellow-50 border border-yellow-200 rounded">
<p class="text-xs text-slate">{% trans "Within 72h" %}</p>
<p class="font-bold text-yellow-600">{{ analysis.resolution_time_analysis.within_72h.count }}</p>
<p class="text-xs text-yellow-600">{{ analysis.resolution_time_analysis.within_72h.percentage }}</p>
</div>
{% endif %}
{% if analysis.resolution_time_analysis.over_72h %}
<div class="p-2 bg-red-50 border border-red-200 rounded">
<p class="text-xs text-slate">{% trans "Over 72h" %}</p>
<p class="font-bold text-red-600">{{ analysis.resolution_time_analysis.over_72h.count }}</p>
<p class="text-xs text-red-600">{{ analysis.resolution_time_analysis.over_72h.percentage }}</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Recommendations -->
{% if analysis.recommendations %}
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded">
<h4 class="text-sm font-bold text-green-800 mb-1">{% trans "Recommendations" %}</h4>
<ul class="list-disc list-inside text-sm text-green-700">
{% for rec in analysis.recommendations %}
<li>{{ rec }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
</div>
{% endif %}
<!-- Review and Approval Section -->
<div class="page-break"></div>
<div class="mt-12 pt-8 border-t-2 border-slate-300">
<h3 class="text-lg font-bold text-navy mb-6">{% trans "Review and Approval" %}</h3>
<div class="grid grid-cols-3 gap-6">
<!-- Reviewed By -->
<div class="border-t border-slate-400 pt-4">
<p class="text-sm font-bold text-navy mb-8">{% trans "Reviewed By:" %}</p>
<div class="mt-12">
<p class="text-sm text-slate border-b border-slate-300 pb-1 mb-2">&nbsp;</p>
<p class="text-xs text-slate">{% trans "Name & Signature" %}</p>
<p class="text-xs text-slate mt-2">{% trans "Date:" %} _______________</p>
</div>
</div>
<!-- Reviewed and Approved By 1 -->
<div class="border-t border-slate-400 pt-4">
<p class="text-sm font-bold text-navy mb-8">{% trans "Reviewed and Approved By:" %}</p>
<div class="mt-12">
<p class="text-sm text-slate border-b border-slate-300 pb-1 mb-2">&nbsp;</p>
<p class="text-xs text-slate">{% trans "Name & Signature" %}</p>
<p class="text-xs text-slate mt-2">{% trans "Date:" %} _______________</p>
</div>
</div>
<!-- Reviewed and Approved By 2 -->
<div class="border-t border-slate-400 pt-4">
<p class="text-sm font-bold text-navy mb-8">{% trans "Reviewed and Approved By:" %}</p>
<div class="mt-12">
<p class="text-sm text-slate border-b border-slate-300 pb-1 mb-2">&nbsp;</p>
<p class="text-xs text-slate">{% trans "Name & Signature" %}</p>
<p class="text-xs text-slate mt-2">{% trans "Date:" %} _______________</p>
</div>
</div>
</div>
</div>
<!-- Footer --> <!-- Footer -->
<div class="text-xs text-slate text-center border-t pt-4 mt-8"> <div class="text-xs text-slate text-center border-t pt-4 mt-8">
<p> <p>
@ -278,7 +692,8 @@
}], }],
chart: { chart: {
type: 'line', type: 'line',
height: 250, height: 220,
width: '100%',
toolbar: { show: false }, toolbar: { show: false },
animations: { enabled: false } animations: { enabled: false }
}, },
@ -314,7 +729,7 @@
y: { formatter: function(val) { return val + '%'; } } y: { formatter: function(val) { return val + '%'; } }
} }
}; };
try { try {
const trendChart = new ApexCharts(trendChartEl, trendOptions); const trendChart = new ApexCharts(trendChartEl, trendOptions);
trendChart.render(); trendChart.render();
@ -331,7 +746,8 @@
labels: sourceData.labels, labels: sourceData.labels,
chart: { chart: {
type: 'donut', type: 'donut',
height: 250, height: 220,
width: '100%',
animations: { enabled: false } animations: { enabled: false }
}, },
colors: ['#005696', '#007bbd', '#64748b', '#94a3b8', '#cbd5e1'], colors: ['#005696', '#007bbd', '#64748b', '#94a3b8', '#cbd5e1'],
@ -343,7 +759,7 @@
y: { formatter: function(val) { return val + '%'; } } y: { formatter: function(val) { return val + '%'; } }
} }
}; };
try { try {
const sourceChart = new ApexCharts(sourceChartEl, sourceOptions); const sourceChart = new ApexCharts(sourceChartEl, sourceOptions);
sourceChart.render(); sourceChart.render();
@ -353,35 +769,144 @@
} }
// PDF Generation // PDF Generation
function generatePDF() { async function generatePDF() {
const element = document.getElementById('report-content'); const element = document.getElementById('report-content');
const opt = {
margin: [10, 10, 10, 10],
filename: '{{ report.kpi_id }}_{{ report.year }}_{{ report.month }}_{{ report.hospital.name|slugify }}.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'landscape'
}
};
// Show loading
const btn = document.querySelector('button[onclick="generatePDF()"]'); const btn = document.querySelector('button[onclick="generatePDF()"]');
const originalText = btn.innerHTML; const originalText = btn.innerHTML;
// Show loading
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> {% trans "Generating..." %}'; btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> {% trans "Generating..." %}';
btn.disabled = true; btn.disabled = true;
lucide.createIcons(); lucide.createIcons();
html2pdf().set(opt).from(element).save().then(() => { try {
// Wait for charts to fully render
await new Promise(resolve => setTimeout(resolve, 1000));
// Convert charts to images before PDF generation
await convertChartsToImages();
const opt = {
margin: [10, 10, 10, 10],
filename: '{{ report.kpi_id }}_{{ report.year }}_{{ report.month }}_{{ report.hospital.name|slugify }}.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: {
scale: 1.5,
useCORS: false, // Disable CORS to avoid tainted canvas issues
logging: false,
allowTaint: false, // Don't allow tainted canvas
letterRendering: true,
foreignObjectRendering: false, // Disable foreignObject rendering which causes issues
onclone: function(clonedDoc) {
// Hide any remaining SVGs in the clone, only show our static images
clonedDoc.querySelectorAll('.apexcharts-svg, .apexcharts-canvas').forEach(el => {
el.style.display = 'none';
});
}
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'landscape'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation error:', error);
alert('{% trans "Error generating PDF. Please try again." %}');
} finally {
// Restore charts after PDF generation (success or failure)
restoreChartsFromImages();
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false; btn.disabled = false;
lucide.createIcons(); lucide.createIcons();
}
}
// Convert ApexCharts to static images for PDF capture
async function convertChartsToImages() {
const chartContainers = ['trendChart', 'sourceChart'];
for (const containerId of chartContainers) {
const container = document.getElementById(containerId);
if (!container) continue;
try {
// Find the SVG element (ApexCharts renders SVG)
const svg = container.querySelector('.apexcharts-svg') || container.querySelector('svg');
if (svg) {
// Clone the SVG to modify it for export
const svgClone = svg.cloneNode(true);
// Get dimensions
const rect = svg.getBoundingClientRect();
const width = rect.width || 600;
const height = rect.height || 220;
// Ensure SVG has proper attributes for standalone rendering
svgClone.setAttribute('width', width);
svgClone.setAttribute('height', height);
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svgClone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
// Inline any computed styles by copying them
const computedStyles = window.getComputedStyle(svg);
svgClone.style.fontFamily = computedStyles.fontFamily;
// Convert SVG to XML string
const svgData = new XMLSerializer().serializeToString(svgClone);
// Create SVG data URL directly (no canvas needed!)
// Use TextEncoder for proper UTF-8 handling
const utf8Bytes = new TextEncoder().encode(svgData);
const binaryString = Array.from(utf8Bytes).map(b => String.fromCharCode(b)).join('');
const svgBase64 = btoa(binaryString);
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
// Create image element
const imgElement = document.createElement('img');
imgElement.src = dataUrl;
imgElement.style.width = width + 'px';
imgElement.style.height = height + 'px';
imgElement.style.maxWidth = '100%';
imgElement.className = 'chart-static-image';
imgElement.dataset.originalContainer = containerId;
// Wait for image to load
await new Promise((resolve) => {
imgElement.onload = resolve;
imgElement.onerror = resolve; // Continue on error
});
// Hide original chart and show image
svg.style.visibility = 'hidden';
container.appendChild(imgElement);
}
} catch (error) {
console.error(`Error converting ${containerId} to image:`, error);
// Continue without converting this chart
}
}
}
// Restore charts from images back to interactive charts
function restoreChartsFromImages() {
// Remove static images
document.querySelectorAll('.chart-static-image').forEach(img => {
img.remove();
});
// Show original SVGs
document.querySelectorAll('#trendChart svg, #sourceChart svg').forEach(svg => {
svg.style.visibility = '';
});
// Reset container positions
document.querySelectorAll('#trendChart, #sourceChart').forEach(container => {
container.style.position = '';
}); });
} }
</script> </script>

View File

@ -0,0 +1,276 @@
{% extends 'layouts/base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Call Records" %} - PX360{% endblock %}
{% block content %}
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-navy">{% trans "Call Records" %}</h1>
<p class="text-slate text-sm mt-1">{% trans "Manage and analyze imported call center recordings" %}</p>
</div>
<div class="flex items-center gap-3">
<a href="{% url 'callcenter:export_call_records_template' %}"
class="px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-light transition flex items-center gap-2">
<i data-lucide="download" class="w-4 h-4"></i>
{% trans "Download Template" %}
</a>
<a href="{% url 'callcenter:import_call_records' %}"
class="px-4 py-2 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center gap-2">
<i data-lucide="upload" class="w-4 h-4"></i>
{% trans "Import CSV" %}
</a>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Total Calls -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center">
<i data-lucide="phone" class="w-6 h-6 text-blue"></i>
</div>
<span class="text-xs font-bold text-slate uppercase">{% trans "All Calls" %}</span>
</div>
<p class="text-3xl font-bold text-navy">{{ stats.total_calls }}</p>
<p class="text-xs text-slate mt-1">{% trans "Total records" %}</p>
</div>
<!-- Inbound Calls -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center">
<i data-lucide="phone-incoming" class="w-6 h-6 text-green-600"></i>
</div>
<span class="text-xs font-bold text-slate uppercase">{% trans "Inbound" %}</span>
</div>
<p class="text-3xl font-bold text-navy">{{ stats.inbound_calls }}</p>
<p class="text-xs text-slate mt-1">{% trans "Incoming calls" %}</p>
</div>
<!-- Outbound Calls -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center">
<i data-lucide="phone-outgoing" class="w-6 h-6 text-orange-600"></i>
</div>
<span class="text-xs font-bold text-slate uppercase">{% trans "Outbound" %}</span>
</div>
<p class="text-3xl font-bold text-navy">{{ stats.outbound_calls }}</p>
<p class="text-xs text-slate mt-1">{% trans "Outgoing calls" %}</p>
</div>
<!-- Total Duration -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center">
<i data-lucide="clock" class="w-6 h-6 text-purple-600"></i>
</div>
<span class="text-xs font-bold text-slate uppercase">{% trans "Duration" %}</span>
</div>
<p class="text-3xl font-bold text-navy">{{ stats.total_duration_formatted }}</p>
<p class="text-xs text-slate mt-1">{% trans "Avg:" %} {{ stats.avg_duration_formatted }}</p>
</div>
<!-- Evaluated Calls -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center">
<i data-lucide="circle-check" class="w-6 h-6 text-indigo-600"></i>
</div>
<span class="text-xs font-bold text-slate uppercase">{% trans "Evaluated" %}</span>
</div>
<p class="text-3xl font-bold text-navy">{{ stats.evaluated_calls }}</p>
<p class="text-xs text-slate mt-1">{% trans "Not evaluated:" %} {{ stats.not_evaluated_calls }}</p>
</div>
</div>
<!-- Filters -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mb-6">
<form method="get" class="flex flex-wrap gap-4">
<div class="flex-1 min-w-48">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "Search" %}</label>
<input type="text" name="search" value="{{ filters.search }}"
placeholder="{% trans 'Name, department, extension...' %}"
class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm">
</div>
<div class="w-48">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "Call Type" %}</label>
<select name="call_type" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm bg-white">
<option value="">{% trans "All Calls" %}</option>
<option value="inbound" {% if filters.call_type == 'inbound' %}selected{% endif %}>{% trans "Inbound" %}</option>
<option value="outbound" {% if filters.call_type == 'outbound' %}selected{% endif %}>{% trans "Outbound" %}</option>
</select>
</div>
<div class="w-48">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "Evaluated" %}</label>
<select name="evaluated" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm bg-white">
<option value="">{% trans "All" %}</option>
<option value="true" {% if filters.evaluated == 'true' %}selected{% endif %}>{% trans "Yes" %}</option>
<option value="false" {% if filters.evaluated == 'false' %}selected{% endif %}>{% trans "No" %}</option>
</select>
</div>
<div class="w-48">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm bg-white">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div class="w-40">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "From Date" %}</label>
<input type="date" name="date_from" value="{{ filters.date_from }}"
class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm">
</div>
<div class="w-40">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "To Date" %}</label>
<input type="date" name="date_to" value="{{ filters.date_to }}"
class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm">
</div>
<div class="flex items-end">
<button type="submit" class="px-6 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center gap-2">
<i data-lucide="search" class="w-4 h-4"></i>
{% trans "Filter" %}
</button>
<a href="{% url 'callcenter:call_records_list' %}"
class="px-4 py-2.5 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-light transition ml-2">
<i data-lucide="x" class="w-4 h-4"></i>
</a>
</div>
</form>
</div>
<!-- Call Records Table -->
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "Date/Time" %}</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "Caller" %}</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "Department" %}</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "Type" %}</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "Duration" %}</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "Evaluated" %}</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-4 px-6">{% trans "File" %}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for record in call_records %}
<tr class="hover:bg-light/30 transition">
<td class="py-4 px-6">
<p class="text-sm font-semibold text-navy">{{ record.call_start|date:"M d, Y" }}</p>
<p class="text-xs text-slate">{{ record.call_start|date:"h:i A" }}</p>
</td>
<td class="py-4 px-6">
<p class="text-sm font-semibold text-navy">{{ record.caller_full_name|default:"-" }}</p>
{% if record.inbound_name %}
<p class="text-xs text-slate">{{ record.inbound_name }}</p>
{% endif %}
</td>
<td class="py-4 px-6">
<p class="text-sm font-semibold text-navy">{{ record.department|default:"-" }}</p>
{% if record.extension %}
<p class="text-xs text-slate">Ext: {{ record.extension }}</p>
{% endif %}
</td>
<td class="py-4 px-6">
{% if record.is_inbound %}
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-green-100 text-green-800">
<i data-lucide="phone-incoming" class="w-3 h-3 mr-1"></i>
{% trans "Inbound" %}
</span>
{% elif record.is_outbound %}
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-orange-100 text-orange-800">
<i data-lucide="phone-outgoing" class="w-3 h-3 mr-1"></i>
{% trans "Outbound" %}
</span>
{% else %}
<span class="text-xs text-slate">-</span>
{% endif %}
</td>
<td class="py-4 px-6">
<p class="text-sm font-mono text-navy">{{ record.call_length|default:"-" }}</p>
</td>
<td class="py-4 px-6">
{% if record.evaluated %}
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-green-100 text-green-800">
<i data-lucide="check" class="w-3 h-3 mr-1"></i>
{% trans "Yes" %}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 text-slate-600">
<i data-lucide="x" class="w-3 h-3 mr-1"></i>
{% trans "No" %}
</span>
{% endif %}
</td>
<td class="py-4 px-6">
<p class="text-xs text-slate truncate max-w-48" title="{{ record.file_name }}">{{ record.file_name|default:"-" }}</p>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="py-12 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 bg-slate-100 rounded-full mb-4">
<i data-lucide="phone-off" class="w-8 h-8 text-slate-400"></i>
</div>
<p class="text-slate font-medium">{% trans "No call records found" %}</p>
<p class="text-xs text-slate mt-1">{% trans "Import a CSV file to get started" %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div class="border-t border-slate-200 px-6 py-4">
<div class="flex items-center justify-between">
<p class="text-sm text-slate">
{% trans "Showing" %} {{ page_obj.start_index }}-{% if page_obj.has_next %}{{ page_obj.end_index }}{% else %}{{ page_obj.count }}{% endif %} {% trans "of" %} {{ page_obj.count }}
</p>
<div class="flex items-center gap-2">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}{% if filters %}{% for key, value in filters.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}{% endif %}"
class="px-3 py-2 border border-slate-200 rounded-lg text-sm font-semibold hover:bg-light transition">
{% trans "Previous" %}
</a>
{% endif %}
<span class="px-4 py-2 bg-navy text-white rounded-lg text-sm font-semibold">
{{ page_obj.number }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if filters %}{% for key, value in filters.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}{% endif %}"
class="px-3 py-2 border border-slate-200 rounded-lg text-sm font-semibold hover:bg-light transition">
{% trans "Next" %}
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>

View File

@ -0,0 +1,226 @@
{% extends 'layouts/base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Import Call Records" %} - PX360{% endblock %}
{% block extra_js %}
<script>
function updateFileName() {
const input = document.getElementById('csvFile');
const fileName = document.getElementById('fileName');
const importButton = document.getElementById('importButton');
if (input.files && input.files[0]) {
fileName.textContent = 'Selected: ' + input.files[0].name;
// Enable the import button
importButton.disabled = false;
importButton.classList.remove('bg-slate-300', 'text-slate-500', 'cursor-not-allowed');
importButton.classList.add('bg-navy', 'text-white', 'hover:bg-blue');
} else {
fileName.textContent = '';
// Disable the import button
importButton.disabled = true;
importButton.classList.add('bg-slate-300', 'text-slate-500', 'cursor-not-allowed');
importButton.classList.remove('bg-navy', 'text-white', 'hover:bg-blue');
}
}
// Drag and drop support
const dropZone = document.getElementById('dropZone');
const csvFile = document.getElementById('csvFile');
if (dropZone && csvFile) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-blue', 'bg-blue-50');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-blue', 'bg-blue-50');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-blue', 'bg-blue-50');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].name.endsWith('.csv')) {
csvFile.files = files;
updateFileName();
} else {
alert('Please upload a CSV file');
}
});
}
</script>
{% endblock %}
{% block content %}
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-2 text-sm text-slate mb-2">
<a href="{% url 'callcenter:call_records_list' %}" class="hover:text-navy">{% trans "Call Records" %}</a>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
<span class="font-bold text-navy">{% trans "Import CSV" %}</span>
</div>
<h1 class="text-2xl font-bold text-navy">{% trans "Import Call Records" %}</h1>
<p class="text-slate text-sm mt-1">{% trans "Upload call records from your call recording system" %}</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Upload Form -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<h2 class="text-lg font-bold text-navy mb-4 flex items-center gap-2">
<i data-lucide="upload" class="w-5 h-5 text-blue"></i>
{% trans "Upload CSV File" %}
</h2>
<form method="post" enctype="multipart/form-data" id="importForm">
{% csrf_token %}
<div class="mb-6">
<label class="block text-sm font-semibold text-slate mb-2">
{% trans "CSV File" %} <span class="text-red-500">*</span>
</label>
<div class="border-2 border-dashed border-slate-200 rounded-xl p-8 text-center hover:border-blue transition" id="dropZone">
<input type="file" name="csv_file" id="csvFile" accept=".csv" required
class="hidden" onchange="updateFileName()">
<label for="csvFile" class="cursor-pointer">
<div class="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="file-spreadsheet" class="w-8 h-8 text-blue"></i>
</div>
<p class="text-sm font-semibold text-navy mb-1">{% trans "Click to upload or drag and drop" %}</p>
<p class="text-xs text-slate">{% trans "CSV file (max. 100MB)" %}</p>
<p id="fileName" class="text-xs text-blue mt-2 font-medium"></p>
</label>
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">
<div class="flex items-start gap-3">
<i data-lucide="info" class="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0"></i>
<div>
<p class="text-sm font-semibold text-blue-800 mb-1">{% trans "Important Notes" %}</p>
<ul class="text-xs text-blue-700 space-y-1">
<li>• {% trans "File must be in CSV format with exact headers" %}</li>
<li>• {% trans "Duplicate records (by Media ID) will be skipped" %}</li>
<li>• {% trans "Invalid records will be skipped with error reporting" %}</li>
<li>• {% trans "Large files may take a few moments to process" %}</li>
</ul>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<button type="submit" id="importButton" disabled
class="px-6 py-3 bg-slate-300 text-slate-500 rounded-xl font-semibold cursor-not-allowed transition flex items-center gap-2">
<i data-lucide="upload" class="w-5 h-5"></i>
{% trans "Import Records" %}
</button>
<a href="{% url 'callcenter:call_records_list' %}"
class="px-6 py-3 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-light transition">
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
<!-- CSV Format Guide -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<h2 class="text-lg font-bold text-navy mb-4 flex items-center gap-2">
<i data-lucide="file-text" class="w-5 h-5 text-blue"></i>
{% trans "CSV Format Guide" %}
</h2>
<p class="text-sm text-slate mb-4">
{% trans "Your CSV file must contain the following headers:" %}
</p>
<div class="bg-slate-50 rounded-xl p-4 mb-6 max-h-80 overflow-y-auto">
<code class="text-xs text-slate font-mono">
{% for header in sample_headers %}
<span class="inline-block px-2 py-1 bg-white border border-slate-200 rounded mr-1 mb-1">{{ header }}</span>
{% endfor %}
</code>
</div>
<div class="space-y-3">
<h3 class="text-sm font-bold text-navy">{% trans "Required Fields" %}</h3>
<ul class="text-sm text-slate space-y-2">
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>Media ID</strong> - {% trans "Unique identifier (UUID format)" %}</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>Call Start</strong> - {% trans "Call start date/time (MM/DD/YYYY H:MM AM/PM)" %}</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>First Name</strong> - {% trans "Caller first name" %}</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>Last Name</strong> - {% trans "Caller last name" %}</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>Department</strong> - {% trans "Department name" %}</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>Length</strong> - {% trans "Call duration (HH:MM:SS)" %}</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0"></i>
<span><strong>File Name</strong> - {% trans "Recording file name" %}</span>
</li>
</ul>
</div>
<div class="mt-6 pt-6 border-t border-slate-200">
<a href="{% url 'callcenter:export_call_records_template' %}"
class="inline-flex items-center gap-2 text-blue font-semibold hover:underline text-sm">
<i data-lucide="download" class="w-4 h-4"></i>
{% trans "Download sample CSV template" %}
</a>
</div>
</div>
</div>
<!-- Example Table -->
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mt-6">
<h2 class="text-lg font-bold text-navy mb-4 flex items-center gap-2">
<i data-lucide="table" class="w-5 h-5 text-blue"></i>
{% trans "Example Data Format" %}
</h2>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">Media ID</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">Call Start</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">First Name</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">Last Name</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">Department</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">Length</th>
<th class="text-left text-xs font-bold text-slate uppercase tracking-wider py-3 px-4">File Name</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<tr>
<td class="py-3 px-4 font-mono text-xs text-slate">aade2430-2eb0-4e05-93eb-9567e2be07ae</td>
<td class="py-3 px-4 text-slate">10/30/2025 7:57:48 PM</td>
<td class="py-3 px-4 text-navy">Patient</td>
<td class="py-3 px-4 text-navy">Relation</td>
<td class="py-3 px-4 text-slate">Patient Relation</td>
<td class="py-3 px-4 font-mono text-slate">00:01:11</td>
<td class="py-3 px-4 text-slate text-xs">2025-10-30\x1379 19.57.48.467.mp3</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -1,18 +1,169 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Complaint Adverse Actions" %} - PX360{% endblock %} {% block title %}{% trans "Adverse Actions" %} - PX360{% endblock %}
{% block extra_css %}
<style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
--hh-purple: #8b5cf6;
}
.page-header {
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
color: white;
padding: 2rem 2.5rem;
border-radius: 1rem;
margin-bottom: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.filter-card, .data-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.data-table th {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 0.875rem 1rem;
text-align: left;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--hh-navy);
border-bottom: 2px solid #bae6fd;
}
.data-table td {
padding: 1rem;
border-bottom: 1px solid #f1f5f9;
color: #475569;
font-size: 0.875rem;
}
.data-table tbody tr {
transition: background-color 0.2s ease;
}
.data-table tbody tr:hover {
background-color: var(--hh-light);
cursor: pointer;
}
.severity-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
}
.severity-badge.low { background: linear-gradient(135deg, #dcfce7, #bbf7d0); color: #166534; }
.severity-badge.medium { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; }
.severity-badge.high { background: linear-gradient(135deg, #fee2e2, #fecaca); color: #991b1b; }
.severity-badge.critical { background: linear-gradient(135deg, #7f1d1d, #991b1b); color: white; }
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
}
.status-badge.reported { background: linear-gradient(135deg, #e0f2fe, #bae6fd); color: #075985; }
.status-badge.under_investigation { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; }
.status-badge.resolved { background: linear-gradient(135deg, #dcfce7, #bbf7d0); color: #166534; }
.status-badge.closed { background: linear-gradient(135deg, #f1f5f9, #e2e8f0); color: #475569; }
.btn-primary {
background: linear-gradient(135deg, var(--hh-navy) 0%, var(--hh-blue) 100%);
color: white;
padding: 0.625rem 1.25rem;
border-radius: 0.75rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 86, 150, 0.3);
}
.btn-secondary {
background: white;
color: #475569;
padding: 0.625rem 1.25rem;
border-radius: 0.75rem;
font-weight: 600;
border: 2px solid #e2e8f0;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-secondary:hover {
background: #f1f5f9;
border-color: #cbd5e1;
transform: translateY(-1px);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="p-6"> <div class="px-4 py-6">
<!-- Header --> <!-- Page Header -->
<div class="mb-6"> <div class="page-header animate-in">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="text-2xl font-bold text-[#005696]">{% trans "Complaint Adverse Actions" %}</h1> <h1 class="text-2xl font-bold mb-2">
<p class="text-[#64748b] mt-1">{% trans "Track and manage adverse actions or damages to patients related to complaints" %}</p> <i data-lucide="shield-alert" class="w-7 h-7 inline-block me-2"></i>
{% trans "Adverse Actions" %}
</h1>
<p class="text-white/90">{% trans "Track and manage adverse actions related to complaints" %}</p>
</div> </div>
<a href="{% url 'complaints:complaint_list' %}" class="px-4 py-2 border border-gray-200 rounded-lg text-gray-600 hover:bg-gray-50 transition flex items-center gap-2"> <a href="{% url 'complaints:complaint_list' %}" class="btn-secondary">
<i data-lucide="arrow-left" class="w-4 h-4"></i> <i data-lucide="arrow-left" class="w-4 h-4"></i>
{% trans "Back to Complaints" %} {% trans "Back to Complaints" %}
</a> </a>
@ -20,169 +171,192 @@
</div> </div>
<!-- Filters --> <!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-6"> <div class="filter-card mb-6 animate-in">
<form method="get" class="flex flex-wrap gap-4"> <div class="card-header">
<div class="flex-1 min-w-[200px]"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<input type="text" name="search" value="{{ filters.search|default:'' }}" <i data-lucide="filter" class="w-5 h-5"></i>
class="w-full px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-[#005696] focus:border-transparent" {% trans "Filters" %}
placeholder="{% trans 'Search by reference or description...' %}"> </h2>
</div> </div>
<div> <div class="p-6">
<select name="status" class="px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-[#005696] focus:border-transparent"> <form method="get" class="flex flex-wrap gap-4">
<option value="">{% trans "All Statuses" %}</option> <div class="flex-1 min-w-[250px]">
{% for value, label in status_choices %} <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Search" %}</label>
<option value="{{ value }}" {% if filters.status == value %}selected{% endif %}>{{ label }}</option> <input type="text" name="search" value="{{ filters.search|default:'' }}"
{% endfor %} placeholder="{% trans 'Reference or description...' %}"
</select> class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue focus:ring-2 focus:ring-blue/20">
</div> </div>
<div> <div>
<select name="severity" class="px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-[#005696] focus:border-transparent"> <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
<option value="">{% trans "All Severities" %}</option> <select name="status" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
{% for value, label in severity_choices %} <option value="">{% trans "All Statuses" %}</option>
<option value="{{ value }}" {% if filters.severity == value %}selected{% endif %}>{{ label }}</option> {% for value, label in status_choices %}
{% endfor %} <option value="{{ value }}" {% if filters.status == value %}selected{% endif %}>{{ label }}</option>
</select> {% endfor %}
</div> </select>
<div> </div>
<select name="action_type" class="px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-[#005696] focus:border-transparent"> <div>
<option value="">{% trans "All Types" %}</option> <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Severity" %}</label>
{% for value, label in action_type_choices %} <select name="severity" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<option value="{{ value }}" {% if filters.action_type == value %}selected{% endif %}>{{ label }}</option> <option value="">{% trans "All Severities" %}</option>
{% endfor %} {% for value, label in severity_choices %}
</select> <option value="{{ value }}" {% if filters.severity == value %}selected{% endif %}>{{ label }}</option>
</div> {% endfor %}
<button type="submit" class="px-4 py-2 bg-[#005696] text-white rounded-lg hover:bg-[#007bbd] transition flex items-center gap-2"> </select>
<i data-lucide="filter" class="w-4 h-4"></i> </div>
{% trans "Filter" %} <div>
</button> <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Type" %}</label>
</form> <select name="action_type" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<option value="">{% trans "All Types" %}</option>
{% for value, label in action_type_choices %}
<option value="{{ value }}" {% if filters.action_type == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="flex items-end">
<button type="submit" class="btn-primary h-[46px]">
<i data-lucide="search" class="w-4 h-4"></i>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
</div> </div>
<!-- Adverse Actions Table --> <!-- Adverse Actions Table -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> <div class="data-card animate-in">
<div class="overflow-x-auto"> <div class="card-header flex items-center justify-between">
<table class="w-full"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<thead class="bg-gray-50"> <i data-lucide="shield-alert" class="w-5 h-5"></i>
<tr> {% trans "All Adverse Actions" %} ({{ page_obj.paginator.count }})
<th class="px-6 py-3 text-left text-xs font-medium text-[#64748b] uppercase">{% trans "Complaint" %}</th> </h2>
<th class="px-6 py-3 text-left text-xs font-medium text-[#64748b] uppercase">{% trans "Type" %}</th> <a href="#" class="btn-primary">
<th class="px-6 py-3 text-left text-xs font-medium text-[#64748b] uppercase">{% trans "Severity" %}</th> <i data-lucide="plus" class="w-4 h-4"></i>
<th class="px-6 py-3 text-left text-xs font-medium text-[#64748b] uppercase">{% trans "Date" %}</th> {% trans "New Adverse Action" %}
<th class="px-6 py-3 text-left text-xs font-medium text-[#64748b] uppercase">{% trans "Status" %}</th> </a>
<th class="px-6 py-3 text-center text-xs font-medium text-[#64748b] uppercase">{% trans "Escalated" %}</th>
<th class="px-6 py-3 text-right text-xs font-medium text-[#64748b] uppercase">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for action in page_obj %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
<a href="{% url 'complaints:complaint_detail' action.complaint.id %}" class="font-medium text-[#005696] hover:text-[#007bbd]">
{{ action.complaint.reference_number }}
</a>
<p class="text-sm text-[#64748b] truncate max-w-[200px]">{{ action.complaint.title }}</p>
</td>
<td class="px-6 py-4">
<span class="text-sm text-gray-900">{{ action.get_action_type_display }}</span>
</td>
<td class="px-6 py-4">
{% if action.severity == 'critical' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{{ action.get_severity_display }}
</span>
{% elif action.severity == 'high' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
{{ action.get_severity_display }}
</span>
{% elif action.severity == 'medium' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
{{ action.get_severity_display }}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{{ action.get_severity_display }}
</span>
{% endif %}
</td>
<td class="px-6 py-4 text-sm text-gray-900">
{{ action.incident_date|date:"Y-m-d" }}
</td>
<td class="px-6 py-4">
{% if action.status == 'reported' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
{{ action.get_status_display }}
</span>
{% elif action.status == 'under_investigation' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ action.get_status_display }}
</span>
{% elif action.status == 'verified' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{{ action.get_status_display }}
</span>
{% elif action.status == 'resolved' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ action.get_status_display }}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600">
{{ action.get_status_display }}
</span>
{% endif %}
</td>
<td class="px-6 py-4 text-center">
{% if action.is_escalated %}
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-500 mx-auto"></i>
{% else %}
<span class="text-gray-300">-</span>
{% endif %}
</td>
<td class="px-6 py-4 text-right">
<a href="{% url 'complaints:complaint_detail' action.complaint.id %}" class="text-[#005696] hover:text-[#007bbd] font-medium text-sm">
{% trans "View" %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="px-6 py-8 text-center text-[#64748b]">
<i data-lucide="shield-check" class="w-12 h-12 mx-auto mb-3 text-gray-300"></i>
<p>{% trans "No adverse actions found." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div class="p-0">
<!-- Pagination --> {% if page_obj %}
{% if page_obj.has_other_pages %} <div class="overflow-x-auto">
<div class="px-6 py-4 border-t border-gray-100 flex items-center justify-between"> <table class="w-full data-table">
<p class="text-sm text-[#64748b]"> <thead>
{% blocktrans with page_obj.number as page and page_obj.paginator.num_pages as total %} <tr>
Page {{ page }} of {{ total }} <th>{% trans "Complaint" %}</th>
{% endblocktrans %} <th>{% trans "Type" %}</th>
</p> <th>{% trans "Severity" %}</th>
<div class="flex gap-2"> <th>{% trans "Date" %}</th>
{% if page_obj.has_previous %} <th class="text-center">{% trans "Status" %}</th>
<a href="?page={{ page_obj.previous_page_number }}&{{ filters.urlencode }}" class="px-3 py-1 border border-gray-200 rounded hover:bg-gray-50"> <th class="text-center">{% trans "Escalated" %}</th>
<i data-lucide="chevron-left" class="w-4 h-4"></i> <th class="text-right">{% trans "Actions" %}</th>
</a> </tr>
{% endif %} </thead>
{% if page_obj.has_next %} <tbody class="divide-y divide-slate-100">
<a href="?page={{ page_obj.next_page_number }}&{{ filters.urlencode }}" class="px-3 py-1 border border-gray-200 rounded hover:bg-gray-50"> {% for action in page_obj %}
<i data-lucide="chevron-right" class="w-4 h-4"></i> <tr onclick="window.location='{% url 'complaints:adverse_action_edit' action.pk %}'">
</a> <td>
{% endif %} <a href="{% url 'complaints:complaint_detail' action.complaint.id %}"
class="font-semibold text-navy hover:text-blue transition">
{{ action.complaint.reference_number|truncatechars:15 }}
</a>
<p class="text-xs text-slate mt-1">{{ action.complaint.title|truncatechars:30 }}</p>
</td>
<td>
<span class="text-sm font-medium text-slate-700">{{ action.get_action_type_display }}</span>
</td>
<td>
<span class="severity-badge {{ action.severity }}">
<i data-lucide="{% if action.severity == 'low' %}arrow-down{% elif action.severity == 'medium' %}minus{% elif action.severity == 'high' %}arrow-up{% else %}zap{% endif %}" class="w-3 h-3"></i>
{{ action.get_severity_display }}
</span>
</td>
<td>
<div class="text-sm text-slate">
<p>{{ action.incident_date|date:"Y-m-d" }}</p>
</div>
</td>
<td class="text-center">
<span class="status-badge {{ action.status }}">
<i data-lucide="{% if action.status == 'reported' %}circle{% elif action.status == 'under_investigation' %}clock{% elif action.status == 'resolved' %}check-circle{% else %}check{% endif %}" class="w-3 h-3"></i>
{{ action.get_status_display }}
</span>
</td>
<td class="text-center">
{% if action.is_escalated %}
<span class="inline-flex items-center gap-1 text-red-600 font-bold text-sm">
<i data-lucide="triangle-alert" class="w-4 h-4"></i>
{% trans "Yes" %}
</span>
{% else %}
<span class="text-slate-400 text-sm">{% trans "No" %}</span>
{% endif %}
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<a href="{% url 'complaints:adverse_action_edit' action.pk %}"
class="p-2 text-blue hover:bg-blue-50 rounded-lg transition"
title="{% trans 'Edit' %}">
<i data-lucide="edit" class="w-4 h-4"></i>
</a>
<a href="{% url 'complaints:adverse_action_delete' action.pk %}"
class="p-2 text-red-500 hover:bg-red-50 rounded-lg transition"
title="{% trans 'Delete' %}"
onclick="event.stopPropagation();">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="py-12 text-center">
<div class="flex flex-col items-center">
<div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-4">
<i data-lucide="shield-alert" class="w-8 h-8 text-slate-400"></i>
</div>
<p class="text-slate font-medium">{% trans "No adverse actions found" %}</p>
<p class="text-slate text-sm mt-1">{% trans "Create your first adverse action to get started" %}</p>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div class="p-4 border-t border-slate-200">
<div class="flex items-center justify-between">
<p class="text-sm text-slate">
{% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
Showing {{ start }} to {{ end }} of {{ total }} adverse actions
{% endblocktrans %}
</p>
<div class="flex gap-2">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.status %}&status={{ filters.status }}{% endif %}{% if filters.severity %}&severity={{ filters.severity }}{% endif %}{% if filters.action_type %}&action_type={{ filters.action_type }}{% endif %}"
class="px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-sm font-medium">
{% trans "Previous" %}
</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.status %}&status={{ filters.status }}{% endif %}{% if filters.severity %}&severity={{ filters.severity }}{% endif %}{% if filters.action_type %}&action_type={{ filters.action_type }}{% endif %}"
class="px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-sm font-medium">
{% trans "Next" %}
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons(); lucide.createIcons();
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -3,16 +3,143 @@
{% block title %}{% trans "Complaints Analytics" %} - PX360{% endblock %} {% block title %}{% trans "Complaints Analytics" %} - PX360{% endblock %}
{% block extra_css %}
<style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
}
.page-header {
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
color: white;
padding: 2rem 2.5rem;
border-radius: 1rem;
margin-bottom: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.stat-card {
background: white;
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid #e2e8f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.stat-card.blue::before { background: linear-gradient(90deg, #3b82f6, #2563eb); }
.stat-card.orange::before { background: linear-gradient(90deg, #f97316, #ea580c); }
.stat-card.red::before { background: linear-gradient(90deg, #ef4444, #dc2626); }
.stat-card.green::before { background: linear-gradient(90deg, #10b981, #059669); }
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.stat-icon.blue { background: linear-gradient(135deg, #dbeafe, #bfdbfe); }
.stat-icon.orange { background: linear-gradient(135deg, #ffedd5, #fed7aa); }
.stat-icon.red { background: linear-gradient(135deg, #fee2e2, #fecaca); }
.stat-icon.green { background: linear-gradient(135deg, #dcfce7, #bbf7d0); }
.chart-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.chart-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.chart-card .card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
}
.category-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 0;
border-bottom: 1px solid #f1f5f9;
}
.category-item:last-child {
border-bottom: none;
}
.category-badge {
width: 40px;
height: 40px;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.875rem;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="mb-8"> <div class="px-4 py-6">
<div class="flex justify-between items-center"> <!-- Page Header -->
<div> <div class="page-header animate-in">
<h1 class="text-3xl font-bold text-gray-800 mb-2">{% trans "Complaints Analytics" %}</h1> <div class="flex items-center justify-between">
<p class="text-gray-400">{% trans "Comprehensive complaints metrics and insights" %}</p> <div>
</div> <h1 class="text-2xl font-bold mb-2">
<div> <i data-lucide="circle-help" class="w-7 h-7 inline-block me-2"></i>
{% trans "Complaints Analytics" %}
</h1>
<p class="text-white/90">{% trans "Comprehensive complaints metrics and insights" %}</p>
</div>
<form method="get" class="inline-flex"> <form method="get" class="inline-flex">
<select name="date_range" class="px-4 py-3 border border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition" onchange="this.form.submit()"> <select name="date_range" class="px-4 py-2.5 bg-white/20 border border-white/30 rounded-xl text-white focus:ring-2 focus:ring-white/50 focus:border-transparent transition" onchange="this.form.submit()">
<option value="7" {% if date_range == 7 %}selected{% endif %}>{% trans "Last 7 Days" %}</option> <option value="7" {% if date_range == 7 %}selected{% endif %}>{% trans "Last 7 Days" %}</option>
<option value="30" {% if date_range == 30 %}selected{% endif %}>{% trans "Last 30 Days" %}</option> <option value="30" {% if date_range == 30 %}selected{% endif %}>{% trans "Last 30 Days" %}</option>
<option value="90" {% if date_range == 90 %}selected{% endif %}>{% trans "Last 90 Days" %}</option> <option value="90" {% if date_range == 90 %}selected{% endif %}>{% trans "Last 90 Days" %}</option>
@ -20,297 +147,291 @@
</form> </form>
</div> </div>
</div> </div>
</div>
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-blue-500 border border-gray-50 hover:shadow-md transition"> <div class="stat-card blue animate-in">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<div class="text-xs font-bold text-blue-600 uppercase mb-1">{% trans "Total Complaints" %}</div> <p class="text-xs font-bold text-blue-600 uppercase mb-1">{% trans "Total Complaints" %}</p>
<div class="text-3xl font-bold text-gray-800 mb-2">{{ dashboard_summary.status_counts.total }}</div> <p class="text-3xl font-black text-navy mb-2">{{ dashboard_summary.status_counts.total }}</p>
<small class="text-gray-500 flex items-center gap-1"> <div class="flex items-center gap-1 text-sm">
{% if dashboard_summary.trend.percentage_change > 0 %} {% if dashboard_summary.trend.percentage_change > 0 %}
<i data-lucide="trending-up" class="w-4 h-4 text-red-500"></i> +{{ dashboard_summary.trend.percentage_change }}% <i data-lucide="trending-up" class="w-4 h-4 text-red-500"></i>
{% elif dashboard_summary.trend.percentage_change < 0 %} <span class="text-red-500 font-bold">+{{ dashboard_summary.trend.percentage_change }}%</span>
<i data-lucide="trending-down" class="w-4 h-4 text-green-500"></i> {{ dashboard_summary.trend.percentage_change }}% {% elif dashboard_summary.trend.percentage_change < 0 %}
{% else %} <i data-lucide="trending-down" class="w-4 h-4 text-green-500"></i>
<i data-lucide="minus" class="w-4 h-4 text-gray-400"></i> 0% <span class="text-green-500 font-bold">{{ dashboard_summary.trend.percentage_change }}%</span>
{% endif %}
{% trans "vs last period" %}
</small>
</div>
<div class="bg-blue-100 p-3 rounded-xl">
<i data-lucide="activity" class="text-blue-500 w-6 h-6"></i>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-orange-500 border border-gray-50 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<div class="text-xs font-bold text-orange-600 uppercase mb-1">{% trans "Open" %}</div>
<div class="text-3xl font-bold text-gray-800">{{ dashboard_summary.status_counts.open }}</div>
</div>
<div class="bg-orange-100 p-3 rounded-xl">
<i data-lucide="folder-open" class="text-orange-500 w-6 h-6"></i>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-red-500 border border-gray-50 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<div class="text-xs font-bold text-red-600 uppercase mb-1">{% trans "Overdue" %}</div>
<div class="text-3xl font-bold text-red-500">{{ dashboard_summary.status_counts.overdue }}</div>
</div>
<div class="bg-red-100 p-3 rounded-xl">
<i data-lucide="alert-triangle" class="text-red-500 w-6 h-6"></i>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-green-500 border border-gray-50 hover:shadow-md transition">
<div class="flex justify-between items-start">
<div>
<div class="text-xs font-bold text-green-600 uppercase mb-1">{% trans "Resolved" %}</div>
<div class="text-3xl font-bold text-gray-800">{{ dashboard_summary.status_counts.resolved }}</div>
</div>
<div class="bg-green-100 p-3 rounded-xl">
<i data-lucide="check-circle" class="text-green-500 w-6 h-6"></i>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Complaints Trend -->
<div class="lg:col-span-2 bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="trending-up" class="w-5 h-5"></i> {% trans "Complaints Trend" %}
</h3>
<div id="trendChart"></div>
</div>
<!-- Top Categories -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="pie-chart" class="w-5 h-5"></i> {% trans "Top Categories" %}
</h3>
<div id="categoryChart"></div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- SLA Compliance -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="clock" class="w-5 h-5"></i> {% trans "SLA Compliance" %}
</h3>
<div class="text-center mb-6">
<h2 class="{% if sla_compliance.overall_compliance_rate >= 80 %}text-green-500{% elif sla_compliance.overall_compliance_rate >= 60 %}text-orange-500{% else %}text-red-500{% endif %} text-4xl font-bold mb-2">
{{ sla_compliance.overall_compliance_rate }}%
</h2>
<p class="text-gray-400">{% trans "Overall Compliance Rate" %}</p>
</div>
<div class="grid grid-cols-2 gap-4 text-center">
<div class="bg-green-50 rounded-xl p-4">
<h4 class="text-2xl font-bold text-green-600">{{ sla_compliance.on_time }}</h4>
<small class="text-gray-500">{% trans "On Time" %}</small>
</div>
<div class="bg-red-50 rounded-xl p-4">
<h4 class="text-2xl font-bold text-red-500">{{ sla_compliance.overdue }}</h4>
<small class="text-gray-500">{% trans "Overdue" %}</small>
</div>
</div>
</div>
<!-- Resolution Rate -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="check-square" class="w-5 h-5"></i> {% trans "Resolution Metrics" %}
</h3>
<div class="mb-6">
<div class="flex justify-between mb-2">
<span class="text-gray-600">{% trans "Resolution Rate" %}</span>
<strong class="text-gray-800">{{ resolution_rate.resolution_rate }}%</strong>
</div>
<div class="h-3 bg-gray-100 rounded-full overflow-hidden">
<div class="h-full bg-green-500 rounded-full" style="width: {{ resolution_rate.resolution_rate }}%"></div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-center">
<div>
<h4 class="text-2xl font-bold text-gray-800">{{ resolution_rate.resolved }}</h4>
<small class="text-gray-500">{% trans "Resolved" %}</small>
</div>
<div>
<h4 class="text-2xl font-bold text-gray-800">{{ resolution_rate.pending }}</h4>
<small class="text-gray-500">{% trans "Pending" %}</small>
</div>
</div>
{% if resolution_rate.avg_resolution_time_hours %}
<div class="mt-6 text-center bg-gray-50 rounded-xl p-4">
<p class="text-gray-600 mb-1">{% trans "Avg Resolution Time" %}</p>
<h5 class="text-xl font-bold text-gray-800">{{ resolution_rate.avg_resolution_time_hours }} {% trans "hours" %}</h5>
</div>
{% endif %}
</div>
</div>
<!-- Overdue Complaints -->
{% if overdue_complaints %}
<div class="bg-white rounded-2xl shadow-sm border border-gray-50">
<div class="p-6 border-b border-gray-100">
<h3 class="text-lg font-bold text-red-500 flex items-center gap-2">
<i data-lucide="alert-triangle" class="w-5 h-5"></i> {% trans "Overdue Complaints" %}
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "ID" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Source" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Title" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Patient" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Severity" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Due Date" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Assigned To" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{% for complaint in overdue_complaints %}
<tr class="hover:bg-gray-50 transition">
<td class="px-6 py-4">
<a href="{% url 'complaints:complaint_detail' complaint.id %}" class="text-navy hover:underline">
<code class="bg-gray-100 px-2 py-1 rounded text-sm font-semibold text-gray-700">{{ complaint.id|slice:8 }}</code>
</a>
</td>
<td class="px-6 py-4">
{% if complaint.source_name %}
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-blue-100 text-blue-600" title="{% trans 'PX Source' %}: {{ complaint.source_name }}">
<i data-lucide="cloud-arrow-down" class="w-3 h-3 inline mr-1"></i> {{ complaint.source_name|truncatechars:12 }}
</span>
{% elif complaint.complaint_source_type == 'internal' %}
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-indigo-100 text-indigo-600" title="{% trans 'Internal' %}">
<i data-lucide="building" class="w-3 h-3 inline mr-1"></i> {% trans "Internal" %}
</span>
{% else %} {% else %}
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-gray-100 text-gray-600" title="{% trans 'External' %}"> <i data-lucide="minus" class="w-4 h-4 text-slate-400"></i>
<i data-lucide="user" class="w-3 h-3 inline mr-1"></i> {% trans "Patient" %} <span class="text-slate-400 font-bold">0%</span>
</span>
{% endif %} {% endif %}
</td> <span class="text-slate-500">{% trans "vs last period" %}</span>
<td class="px-6 py-4"> </div>
<span class="text-gray-700">{{ complaint.title|truncatechars:50 }}</span> </div>
</td> <div class="stat-icon blue">
<td class="px-6 py-4"> <i data-lucide="activity" class="text-blue-600 w-6 h-6"></i>
<span class="text-gray-700">{{ complaint.patient_full_name }}</span> </div>
</td> </div>
<td class="px-6 py-4"> </div>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold {% if complaint.severity == 'critical' %}bg-red-500 text-white{% elif complaint.severity == 'high' %}bg-orange-100 text-orange-600{% else %}bg-gray-100 text-gray-600{% endif %}">
{{ complaint.severity }} <div class="stat-card orange animate-in">
</span> <div class="flex justify-between items-start">
</td> <div>
<td class="px-6 py-4"> <p class="text-xs font-bold text-orange-600 uppercase mb-1">{% trans "Open" %}</p>
<span class="text-red-500 font-semibold">{{ complaint.due_at|date:"Y-m-d H:i" }}</span> <p class="text-3xl font-black text-navy">{{ dashboard_summary.status_counts.open }}</p>
</td> </div>
<td class="px-6 py-4"> <div class="stat-icon orange">
<span class="text-gray-700">{{ complaint.assigned_to_full_name|default:"Unassigned" }}</span> <i data-lucide="folder-open" class="text-orange-600 w-6 h-6"></i>
</td> </div>
<td class="px-6 py-4"> </div>
<a href="{% url 'complaints:complaint_detail' complaint.id %}" class="p-2 bg-light text-navy rounded-lg hover:bg-blue-100 transition"> </div>
<i data-lucide="eye" class="w-4 h-4"></i>
</a> <div class="stat-card red animate-in">
</td> <div class="flex justify-between items-start">
</tr> <div>
{% endfor %} <p class="text-xs font-bold text-red-600 uppercase mb-1">{% trans "Overdue" %}</p>
</tbody> <p class="text-3xl font-black text-red-500">{{ dashboard_summary.status_counts.overdue }}</p>
</table> </div>
<div class="stat-icon red">
<i data-lucide="alert-triangle" class="text-red-600 w-6 h-6"></i>
</div>
</div>
</div>
<div class="stat-card green animate-in">
<div class="flex justify-between items-start">
<div>
<p class="text-xs font-bold text-green-600 uppercase mb-1">{% trans "Resolved" %}</p>
<p class="text-3xl font-black text-navy">{{ dashboard_summary.status_counts.resolved }}</p>
</div>
<div class="stat-icon green">
<i data-lucide="check-circle" class="text-green-600 w-6 h-6"></i>
</div>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<!-- Complaints Trend -->
<div class="lg:col-span-2 chart-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="trending-up" class="w-5 h-5"></i>
{% trans "Complaints Trend" %}
</h3>
</div>
<div class="p-6">
<div id="trendChart" style="min-height: 300px;"></div>
</div>
</div>
<!-- Top Categories -->
<div class="chart-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="pie-chart" class="w-5 h-5"></i>
{% trans "Top Categories" %}
</h3>
</div>
<div class="p-6">
<ul class="category-list">
{% for category in top_categories %}
<li class="category-item">
<div class="category-badge bg-blue-100 text-blue-600">
{{ forloop.counter }}
</div>
<div class="flex-1">
<p class="font-semibold text-navy text-sm">{{ category.category__name_en|default:"Uncategorized" }}</p>
<p class="text-xs text-slate-500">{{ category.count }} complaints</p>
</div>
<div class="text-right">
<p class="font-bold text-navy">{{ category.percentage|floatformat:1 }}%</p>
</div>
</li>
{% empty %}
<li class="text-center py-8 text-slate-500">
<i data-lucide="pie-chart" class="w-12 h-12 mx-auto mb-2 opacity-30"></i>
<p>{% trans "No category data available" %}</p>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<!-- Second Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Department Distribution -->
<div class="chart-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="building" class="w-5 h-5"></i>
{% trans "Department Distribution" %}
</h3>
</div>
<div class="p-6">
<div id="departmentChart" style="min-height: 250px;"></div>
</div>
</div>
<!-- Severity Breakdown -->
<div class="chart-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="circle-help" class="w-5 h-5"></i>
{% trans "Severity Breakdown" %}
</h3>
</div>
<div class="p-6">
<div id="severityChart" style="min-height: 250px;"></div>
</div>
</div>
</div>
<!-- Hospital Performance -->
<div class="chart-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="hospital" class="w-5 h-5"></i>
{% trans "Hospital Performance" %}
</h3>
</div>
<div class="p-0">
{% if hospital_performance %}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b-2 border-slate-200">
<th class="text-left py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</th>
<th class="text-center py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Total" %}</th>
<th class="text-center py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Open" %}</th>
<th class="text-center py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Resolved" %}</th>
<th class="text-center py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Overdue" %}</th>
<th class="text-center py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Resolution Rate" %}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for hospital in hospital_performance %}
<tr class="hover:bg-slate-50 transition">
<td class="py-3 px-4">
<span class="font-semibold text-navy">{{ hospital.hospital__name }}</span>
</td>
<td class="py-3 px-4 text-center">
<span class="font-bold text-navy">{{ hospital.total }}</span>
</td>
<td class="py-3 px-4 text-center">
<span class="px-2.5 py-1 bg-blue-100 text-blue-700 rounded-full text-xs font-bold">{{ hospital.open }}</span>
</td>
<td class="py-3 px-4 text-center">
<span class="px-2.5 py-1 bg-green-100 text-green-700 rounded-full text-xs font-bold">{{ hospital.resolved }}</span>
</td>
<td class="py-3 px-4 text-center">
<span class="px-2.5 py-1 bg-red-100 text-red-700 rounded-full text-xs font-bold">{{ hospital.overdue }}</span>
</td>
<td class="py-3 px-4 text-center">
<div class="flex items-center justify-center gap-2">
<div class="w-24 bg-slate-200 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: {{ hospital.resolution_rate }}%"></div>
</div>
<span class="font-bold text-navy text-sm">{{ hospital.resolution_rate|floatformat:1 }}%</span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-12">
<div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="hospital" class="w-8 h-8 text-slate-400"></i>
</div>
<p class="text-slate font-medium">{% trans "No hospital data available" %}</p>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endif %}
<!-- ApexCharts -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.1/dist/apexcharts.min.js"></script>
<script> <script>
// Trend Chart - ApexCharts document.addEventListener('DOMContentLoaded', function() {
var trendOptions = { lucide.createIcons();
series: [{
name: '{% trans "Complaints" %}',
data: {{ trends.data|safe }}
}],
chart: {
type: 'line',
height: 320,
toolbar: {
show: false
}
},
stroke: {
curve: 'smooth',
width: 3
},
colors: ['#4bc0c0'],
xaxis: {
categories: {{ trends.labels|safe }},
labels: {
style: {
fontSize: '12px'
}
}
},
yaxis: {
min: 0,
forceNiceScale: true,
labels: {
style: {
fontSize: '12px'
}
}
},
grid: {
borderColor: '#e7e7e7',
strokeDashArray: 5
},
tooltip: {
theme: 'light'
}
};
var trendChart = new ApexCharts(document.querySelector("#trendChart"), trendOptions);
trendChart.render();
// Category Chart - ApexCharts // Trend Chart
var categoryOptions = { const trendOptions = {
series: [{% for cat in top_categories.categories %}{{ cat.count }}{% if not forloop.last %},{% endif %}{% endfor %}], series: [{
chart: { name: '{% trans "Complaints" %}',
type: 'donut', data: {{ trend_data|safe }}
height: 360 }],
}, chart: {
labels: [{% for cat in top_categories.categories %}'{{ cat.category }}'{% if not forloop.last %},{% endif %}{% endfor %}], type: 'area',
colors: ['#ff6384', '#36a2eb', '#ffce56', '#4bc0c0', '#9966ff', '#ff9f40'], height: 300,
legend: { fontFamily: 'Inter, sans-serif',
position: 'bottom', toolbar: { show: false }
fontSize: '12px' },
}, colors: ['#007bbd'],
dataLabels: { dataLabels: { enabled: false },
enabled: true, stroke: { curve: 'smooth', width: 3 },
formatter: function (val) { fill: {
return val.toFixed(1) + "%" type: 'gradient',
} gradient: {
}, shadeIntensity: 1,
plotOptions: { opacityFrom: 0.4,
pie: { opacityTo: 0.1,
donut: {
size: '65%'
} }
} },
}, xaxis: {
tooltip: { categories: {{ trend_labels|safe }},
theme: 'light' labels: { style: { fontSize: '12px', colors: '#64748b' } }
} },
}; yaxis: {
var categoryChart = new ApexCharts(document.querySelector("#categoryChart"), categoryOptions); labels: { style: { fontSize: '12px', colors: '#64748b' } }
categoryChart.render(); },
grid: { borderColor: '#e2e8f0', strokeDashArray: 4 }
};
new ApexCharts(document.querySelector("#trendChart"), trendOptions).render();
{% if severity_data %}
// Severity Chart
const severityOptions = {
series: {{ severity_data|safe }},
chart: {
type: 'donut',
height: 250,
fontFamily: 'Inter, sans-serif',
toolbar: { show: false }
},
labels: ['Low', 'Medium', 'High', 'Critical'],
colors: ['#10b981', '#f59e0b', '#ef4444', '#dc2626'],
plotOptions: {
pie: {
donut: {
size: '65%',
labels: {
show: true,
name: { fontSize: '14px', fontWeight: 600 },
value: { fontSize: '18px', fontWeight: 700 },
total: {
show: true,
label: 'Total',
formatter: function(w) {
return w.globals.seriesTotals.reduce((a, b) => a + b, 0);
}
}
}
}
}
},
dataLabels: { enabled: false },
legend: { position: 'bottom', fontSize: '12px' }
};
new ApexCharts(document.querySelector("#severityChart"), severityOptions).render();
{% endif %}
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,15 +1,91 @@
{% extends 'layouts/base.html' %} {% extends 'layouts/base.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title %}Inquiry #{{ inquiry.id|slice:":8" }} - PX360{% endblock %} {% block title %}{% trans "Inquiry" %} #{{ inquiry.reference_number|truncatechars:15 }} - PX360{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-cyan: #06b6d4;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
}
.detail-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.detail-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.info-label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--hh-slate);
margin-bottom: 0.375rem;
}
.info-value {
font-size: 0.95rem;
color: #1e293b;
font-weight: 500;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.875rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
}
.status-badge.open { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #1565c0; }
.status-badge.in_progress { background: linear-gradient(135deg, #fff3e0, #ffe0b2); color: #e65100; }
.status-badge.resolved { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); color: #2e7d32; }
.status-badge.closed { background: linear-gradient(135deg, #f5f5f5, #e0e0e0); color: #616161; }
.priority-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.625rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.priority-badge.low { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); color: #2e7d32; }
.priority-badge.medium { background: linear-gradient(135deg, #fff3e0, #ffe0b2); color: #e65100; }
.priority-badge.high { background: linear-gradient(135deg, #ffebee, #ffcdd2); color: #c62828; }
.priority-badge.urgent { background: linear-gradient(135deg, #880e4f, #ad1457); color: white; }
.timeline { .timeline {
position: relative; position: relative;
padding-left: 30px; padding-left: 2rem;
} }
.timeline::before { .timeline::before {
content: ''; content: '';
position: absolute; position: absolute;
@ -17,509 +93,360 @@
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 2px; width: 2px;
background: #e5e7eb; background: #e2e8f0;
} }
.timeline-item { .timeline-item {
position: relative; position: relative;
padding-bottom: 30px; padding-bottom: 1.5rem;
} }
.timeline-item::before { .timeline-item::before {
content: ''; content: '';
position: absolute; position: absolute;
left: -26px; left: -1.625rem;
top: 5px; top: 4px;
width: 16px; width: 14px;
height: 16px; height: 14px;
border-radius: 50%; border-radius: 50%;
background: white; background: white;
border: 3px solid #17a2b8; border: 3px solid var(--hh-blue);
z-index: 1; z-index: 1;
} }
.timeline-item.status_change::before {
border-color: #f97316; .timeline-item.status_change::before { border-color: var(--hh-warning); }
} .timeline-item.response::before { border-color: var(--hh-success); }
.timeline-item.response::before { .timeline-item.note::before { border-color: var(--hh-navy); }
border-color: #22c55e;
} .btn-primary {
.timeline-item.note::before { background: linear-gradient(135deg, var(--hh-navy) 0%, var(--hh-blue) 100%);
border-color: #3b82f6; color: white;
} padding: 0.625rem 1.25rem;
border-radius: 0.75rem;
.info-label {
font-weight: 600; font-weight: 600;
color: #9ca3af; border: none;
font-size: 0.75rem; cursor: pointer;
text-transform: uppercase; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 0.05em; display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 86, 150, 0.3);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<!-- Back Button --> <div class="px-4 py-6">
<div class="mb-6"> <!-- Back Button -->
{% if source_user %} <div class="mb-6 animate-in">
<a href="{% url 'px_sources:source_user_inquiry_list' %}" class="inline-flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-xl text-gray-600 hover:bg-gray-50 transition text-sm font-semibold"> {% if source_user %}
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to My Inquiries" %} <a href="{% url 'px_sources:source_user_inquiry_list' %}" class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 rounded-xl text-slate-600 hover:bg-slate-50 transition text-sm font-semibold">
</a> <i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to My Inquiries" %}
{% else %} </a>
<a href="{% url 'complaints:inquiry_list' %}" class="inline-flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-xl text-gray-600 hover:bg-gray-50 transition text-sm font-semibold"> {% else %}
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Inquiries" %} <a href="{% url 'complaints:inquiry_list' %}" class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 rounded-xl text-slate-600 hover:bg-slate-50 transition text-sm font-semibold">
</a> <i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Inquiries" %}
{% endif %} </a>
</div>
<!-- Inquiry Header -->
<div class="bg-gradient-to-r from-cyan-500 to-teal-500 rounded-2xl p-6 text-white mb-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<div class="flex flex-wrap items-center gap-3 mb-4">
<h2 class="text-2xl font-bold">{{ inquiry.subject }}</h2>
<span class="px-3 py-1 bg-white/20 rounded-full text-sm font-semibold">
{% trans "Inquiry" %}
</span>
{% if inquiry.status == 'open' %}
<span class="px-3 py-1 bg-blue-500 rounded-full text-sm font-semibold">{% trans "Open" %}</span>
{% elif inquiry.status == 'in_progress' %}
<span class="px-3 py-1 bg-orange-500 rounded-full text-sm font-semibold">{% trans "In Progress" %}</span>
{% elif inquiry.status == 'resolved' %}
<span class="px-3 py-1 bg-green-500 rounded-full text-sm font-semibold">{% trans "Resolved" %}</span>
{% elif inquiry.status == 'closed' %}
<span class="px-3 py-1 bg-gray-500 rounded-full text-sm font-semibold">{% trans "Closed" %}</span>
{% endif %}
{% if inquiry.priority %}
{% if inquiry.priority == 'low' %}
<span class="px-3 py-1 bg-green-200 text-green-800 rounded-full text-sm font-semibold">{% trans "Low" %}</span>
{% elif inquiry.priority == 'medium' %}
<span class="px-3 py-1 bg-orange-200 text-orange-800 rounded-full text-sm font-semibold">{% trans "Medium" %}</span>
{% elif inquiry.priority == 'high' %}
<span class="px-3 py-1 bg-red-200 text-red-800 rounded-full text-sm font-semibold">{% trans "High" %}</span>
{% elif inquiry.priority == 'urgent' %}
<span class="px-3 py-1 bg-navy text-white rounded-full text-sm font-semibold">{% trans "Urgent" %}</span>
{% endif %}
{% endif %}
</div>
<div class="space-y-2 text-white/90">
<p class="flex items-center gap-2">
<i data-lucide="hash" class="w-4 h-4"></i>
<span><strong>{% trans "ID" %}:</strong> {{ inquiry.id|slice:":8" }}</span>
{% if inquiry.patient %}
<span class="mx-2">|</span>
<span><strong>{% trans "Patient" %}:</strong> {{ inquiry.patient.get_full_name }} ({% trans "MRN" %}: {{ inquiry.patient.mrn }})</span>
{% else %}
<span class="mx-2">|</span>
<span><strong>{% trans "Contact" %}:</strong> {{ inquiry.contact_name|default:inquiry.contact_email }}</span>
{% endif %}
</p>
<p class="flex items-center gap-2">
<i data-lucide="building-2" class="w-4 h-4"></i>
<span><strong>{% trans "Hospital" %}:</strong> {{ inquiry.hospital.name_en }}</span>
{% if inquiry.department %}
<span class="mx-2">|</span>
<span><strong>{% trans "Department" %}:</strong> {{ inquiry.department.name_en }}</span>
{% endif %}
</p>
</div>
</div>
{% if inquiry.due_date %}
<div class="bg-white/10 rounded-2xl p-5 text-center {% if inquiry.is_overdue %}bg-red-500/20{% endif %}">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="clock" class="w-5 h-5"></i>
<strong>{% trans "Due Date" %}</strong>
</div>
<h3 class="text-2xl font-bold mb-2">{{ inquiry.due_date|date:"M d, Y H:i" }}</h3>
{% if inquiry.is_overdue %}
<div class="text-red-200 font-bold">
<i data-lucide="alert-triangle" class="w-4 h-4 inline"></i>
{% trans "OVERDUE" %}
</div>
{% else %}
<small class="text-white/80">{{ inquiry.due_date|timeuntil }} {% trans "remaining" %}</small>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
</div>
<!-- Tab Navigation --> <!-- Inquiry Header -->
<div class="bg-white rounded-t-2xl border-b border-gray-100 px-6"> <div class="bg-gradient-to-r from-cyan-500 to-teal-500 rounded-2xl p-6 text-white mb-6 animate-in">
<div class="flex gap-1 overflow-x-auto" role="tablist"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<button class="tab-btn active" data-target="details" role="tab"> <div>
<i data-lucide="info" class="w-4 h-4"></i> {% trans "Details" %} <div class="flex flex-wrap items-center gap-3 mb-4">
</button> <h2 class="text-2xl font-bold">{{ inquiry.subject }}</h2>
<button class="tab-btn" data-target="timeline" role="tab"> <span class="px-3 py-1 bg-white/20 rounded-full text-sm font-semibold">
<i data-lucide="clock-history" class="w-4 h-4"></i> {% trans "Timeline" %} ({{ timeline.count }}) {% trans "Inquiry" %}
</button> </span>
<button class="tab-btn" data-target="attachments" role="tab"> <span class="status-badge {{ inquiry.status }}">
<i data-lucide="paperclip" class="w-4 h-4"></i> {% trans "Attachments" %} ({{ attachments.count }}) <i data-lucide="{% if inquiry.status == 'open' %}circle{% elif inquiry.status == 'in_progress' %}clock{% elif inquiry.status == 'resolved' %}check-circle{% else %}check{% endif %}" class="w-3 h-3"></i>
</button> {{ inquiry.get_status_display }}
</div> </span>
</div> {% if inquiry.priority %}
<span class="priority-badge {{ inquiry.priority }}">
<!-- Tab Content --> {% if inquiry.priority == 'low' %}
<div class="bg-white rounded-b-2xl border border-gray-50 shadow-sm mb-6"> <i data-lucide="arrow-down" class="w-3 h-3"></i>
<div class="p-6"> {% elif inquiry.priority == 'medium' %}
<i data-lucide="minus" class="w-3 h-3"></i>
<!-- Details Tab --> {% elif inquiry.priority == 'high' %}
<div class="tab-panel active" id="details"> <i data-lucide="arrow-up" class="w-3 h-3"></i>
<h3 class="text-xl font-bold text-gray-800 mb-6">{% trans "Inquiry Details" %}</h3>
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="info-label mb-2">{% trans "Category" %}</div>
<span class="px-3 py-1 bg-gray-100 text-gray-700 rounded-lg text-sm font-semibold">{{ inquiry.get_category_display }}</span>
</div>
<div>
<div class="info-label mb-2">{% trans "Source" %}</div>
{% if inquiry.source %}
<span class="px-3 py-1 bg-blue-100 text-blue-700 rounded-lg text-sm font-semibold">{{ inquiry.get_source_display }}</span>
{% else %} {% else %}
<span class="text-gray-400 text-sm">{% trans "N/A" %}</span> <i data-lucide="zap" class="w-3 h-3"></i>
{% endif %} {% endif %}
{{ inquiry.get_priority_display }}
</span>
{% endif %}
</div>
<p class="text-white/90 text-sm">{{ inquiry.message|truncatewords:30 }}</p>
</div>
<div class="lg:text-right">
<div class="flex flex-wrap gap-2 justify-start lg:justify-end mb-3">
<span class="px-3 py-1 bg-white/20 rounded-full text-sm">
<i data-lucide="hash" class="w-3 h-3 inline-block mr-1"></i>
{{ inquiry.reference_number|truncatechars:15 }}
</span>
<span class="px-3 py-1 bg-white/20 rounded-full text-sm">
<i data-lucide="calendar" class="w-3 h-3 inline-block mr-1"></i>
{{ inquiry.created_at|date:"Y-m-d" }}
</span>
</div>
{% if can_respond %}
<button onclick="showRespondModal()" class="bg-white text-cyan-600 px-4 py-2 rounded-xl font-bold hover:bg-cyan-50 transition text-sm inline-flex items-center gap-2">
<i data-lucide="message-square" class="w-4 h-4"></i>
{% trans "Respond" %}
</button>
{% endif %}
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Content -->
<div class="lg:col-span-2 space-y-6">
<!-- Inquiry Details -->
<div class="detail-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="file-text" class="w-5 h-5"></i>
{% trans "Inquiry Details" %}
</h3>
</div>
<div class="p-6">
<div class="prose prose-sm max-w-none">
<p class="text-slate-700 leading-relaxed">{{ inquiry.message|linebreaks }}</p>
</div> </div>
</div> </div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <!-- Response -->
<div class="info-label mb-2">{% trans "Channel" %}</div> {% if inquiry.response %}
{% if inquiry.channel %} <div class="detail-card animate-in">
<span class="text-gray-700 font-medium">{{ inquiry.get_channel_display }}</span> <div class="card-header bg-green-50 border-green-100">
{% else %} <h3 class="text-lg font-bold text-green-800 flex items-center gap-2 m-0">
<span class="text-gray-400 text-sm">{% trans "N/A" %}</span> <i data-lucide="circle-check" class="w-5 h-5"></i>
{% endif %} {% trans "Response" %}
</div> </h3>
</div>
<div> <div class="p-6">
<div class="info-label mb-2">{% trans "Assigned To" %}</div> <div class="flex items-center gap-3 mb-4 text-sm text-slate-600">
<span class="text-gray-700 font-medium"> <span class="flex items-center gap-1">
{% if inquiry.assigned_to %}{{ inquiry.assigned_to.get_full_name }}{% else %}<span class="text-gray-400">{% trans "Unassigned" %}</span>{% endif %} <i data-lucide="user" class="w-4 h-4"></i>
{{ inquiry.responded_by.get_full_name|default:inquiry.responded_by.email }}
</span>
<span></span>
<span class="flex items-center gap-1">
<i data-lucide="calendar" class="w-4 h-4"></i>
{{ inquiry.response_sent_at|date:"Y-m-d H:i" }}
</span> </span>
</div> </div>
</div> <div class="prose prose-sm max-w-none">
<p class="text-slate-700 leading-relaxed">{{ inquiry.response|linebreaks }}</p>
<hr class="border-gray-200">
<div>
<div class="info-label mb-2">{% trans "Message" %}</div>
<div class="bg-gray-50 rounded-xl p-4 text-gray-700">
{{ inquiry.message|linebreaks }}
</div> </div>
</div> </div>
{% if inquiry.response %}
<hr class="border-gray-200">
<div>
<div class="info-label mb-2">{% trans "Response" %}</div>
<div class="bg-green-50 border border-green-200 rounded-xl p-4">
<p class="text-gray-700 mb-3">{{ inquiry.response|linebreaks }}</p>
<small class="text-gray-500">
{% trans "Responded by" %} {{ inquiry.responded_by.get_full_name }} {% trans "on" %} {{ inquiry.responded_at|date:"M d, Y H:i" }}
</small>
</div>
</div>
{% endif %}
<hr class="border-gray-200">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="info-label mb-2">{% trans "Created" %}</div>
<span class="text-gray-700 font-medium">{{ inquiry.created_at|date:"M d, Y H:i" }}</span>
</div>
<div>
<div class="info-label mb-2">{% trans "Last Updated" %}</div>
<span class="text-gray-700 font-medium">{{ inquiry.updated_at|date:"M d, Y H:i" }}</span>
</div>
</div>
</div>
</div>
<!-- Timeline Tab -->
<div class="tab-panel hidden" id="timeline">
<h3 class="text-xl font-bold text-gray-800 mb-6">{% trans "Activity Timeline" %}</h3>
{% if timeline %}
<div class="timeline">
{% for update in timeline %}
<div class="timeline-item {{ update.update_type }}">
<div class="bg-white border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition">
<div class="flex justify-between items-start mb-2">
<div>
<span class="px-3 py-1 bg-cyan-500 text-white rounded-lg text-sm font-bold">{{ update.get_update_type_display }}</span>
{% if update.created_by %}
<span class="text-gray-500 text-sm ml-2">
by {{ update.created_by.get_full_name }}
</span>
{% endif %}
</div>
<small class="text-gray-400">
{{ update.created_at|date:"M d, Y H:i" }}
</small>
</div>
<p class="text-gray-700">{{ update.message }}</p>
{% if update.old_status and update.new_status %}
<div class="flex gap-2 mt-2">
<span class="px-2 py-1 bg-blue-100 text-blue-600 rounded-lg text-xs font-bold">{{ update.old_status }}</span>
<i data-lucide="arrow-right" class="w-4 h-4 text-gray-400"></i>
<span class="px-2 py-1 bg-green-100 text-green-600 rounded-lg text-xs font-bold">{{ update.new_status }}</span>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<i data-lucide="clock-history" class="w-16 h-16 mx-auto text-gray-300 mb-4"></i>
<p class="text-gray-500">{% trans "No timeline entries yet" %}</p>
</div> </div>
{% endif %} {% endif %}
</div>
<!-- Timeline -->
<!-- Attachments Tab --> <div class="detail-card animate-in">
<div class="tab-panel hidden" id="attachments"> <div class="card-header">
<h3 class="text-xl font-bold text-gray-800 mb-6">{% trans "Attachments" %}</h3> <h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="history" class="w-5 h-5"></i>
{% if attachments %} {% trans "Timeline" %}
<div class="space-y-3"> </h3>
{% for attachment in attachments %} </div>
<div class="bg-white border border-gray-200 rounded-xl p-4 flex justify-between items-center hover:shadow-md transition"> <div class="p-6">
<div> <div class="timeline">
<div class="flex items-center gap-2 font-semibold text-gray-800 mb-1"> <!-- Created -->
<i data-lucide="file" class="w-5 h-5"></i> {{ attachment.filename }} <div class="timeline-item">
<p class="info-label">{% trans "Inquiry Created" %}</p>
<p class="text-sm text-slate-700">
{% trans "Created by" %} {{ inquiry.created_by.get_full_name|default:inquiry.created_by.email }}
</p>
<p class="text-xs text-slate-500 mt-1">{{ inquiry.created_at|date:"Y-m-d H:i" }}</p>
</div>
<!-- Status Changes -->
{% for update in inquiry.updates.all %}
<div class="timeline-item status_change">
<p class="info-label">{% trans "Status Changed" %}</p>
<p class="text-sm text-slate-700">
{% trans "Status changed to" %} <strong>{{ update.new_status|title }}</strong>
</p>
<p class="text-xs text-slate-500 mt-1">{{ update.created_at|date:"Y-m-d H:i" }}</p>
</div>
{% endfor %}
<!-- Response -->
{% if inquiry.response_sent_at %}
<div class="timeline-item response">
<p class="info-label">{% trans "Response Sent" %}</p>
<p class="text-sm text-slate-700">
{% trans "Response sent by" %} {{ inquiry.responded_by.get_full_name|default:inquiry.responded_by.email }}
</p>
<p class="text-xs text-slate-500 mt-1">{{ inquiry.response_sent_at|date:"Y-m-d H:i" }}</p>
</div> </div>
<small class="text-gray-500">
{% trans "Uploaded by" %} {{ attachment.uploaded_by.get_full_name }}
{% trans "on" %} {{ attachment.created_at|date:"M d, Y H:i" }}
({{ attachment.file_size|filesizeformat }})
</small>
{% if attachment.description %}
<p class="text-sm text-gray-600 mt-1">{{ attachment.description }}</p>
{% endif %} {% endif %}
<!-- Notes -->
{% for note in inquiry.notes.all %}
<div class="timeline-item note">
<p class="info-label">{% trans "Note Added" %}</p>
<p class="text-sm text-slate-700">{{ note.content|truncatewords:20 }}</p>
<p class="text-xs text-slate-500 mt-1">{{ note.created_at|date:"Y-m-d H:i" }}</p>
</div>
{% endfor %}
</div> </div>
<a href="{{ attachment.file.url }}" class="p-2 bg-cyan-100 text-cyan-600 rounded-lg hover:bg-cyan-200 transition"> </div>
<i data-lucide="download" class="w-5 h-5"></i> </div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Contact Information -->
<div class="detail-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="user" class="w-5 h-5"></i>
{% trans "Contact Information" %}
</h3>
</div>
<div class="p-6 space-y-4">
<div>
<p class="info-label">{% trans "Name" %}</p>
<p class="info-value">{{ inquiry.contact_name|default:"-" }}</p>
</div>
<div>
<p class="info-label">{% trans "Phone" %}</p>
<p class="info-value">{{ inquiry.contact_phone|default:"-" }}</p>
</div>
<div>
<p class="info-label">{% trans "Email" %}</p>
<p class="info-value">{{ inquiry.contact_email|default:"-" }}</p>
</div>
</div>
</div>
<!-- Organization -->
<div class="detail-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="building" class="w-5 h-5"></i>
{% trans "Organization" %}
</h3>
</div>
<div class="p-6 space-y-4">
<div>
<p class="info-label">{% trans "Hospital" %}</p>
<p class="info-value">{{ inquiry.hospital.name }}</p>
</div>
{% if inquiry.department %}
<div>
<p class="info-label">{% trans "Department" %}</p>
<p class="info-value">{{ inquiry.department.name }}</p>
</div>
{% endif %}
<div>
<p class="info-label">{% trans "Category" %}</p>
<p class="info-value">{{ inquiry.get_category_display|default:"-" }}</p>
</div>
</div>
</div>
<!-- Actions -->
<div class="detail-card animate-in">
<div class="card-header">
<h3 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="settings" class="w-5 h-5"></i>
{% trans "Actions" %}
</h3>
</div>
<div class="p-4 space-y-2">
{% if can_respond %}
<button onclick="showRespondModal()" class="w-full btn-primary justify-center">
<i data-lucide="message-square" class="w-4 h-4"></i>
{% trans "Send Response" %}
</button>
{% endif %}
{% if inquiry.status != 'closed' %}
<a href="#" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-50 transition text-navy font-medium text-sm">
<i data-lucide="edit" class="w-4 h-4"></i>
{% trans "Edit Inquiry" %}
</a> </a>
{% endif %}
</div> </div>
{% endfor %}
</div> </div>
{% else %}
<div class="text-center py-12">
<i data-lucide="paperclip" class="w-16 h-16 mx-auto text-gray-300 mb-4"></i>
<p class="text-gray-500">{% trans "No attachments" %}</p>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- Sidebar Actions --> <!-- Respond Modal -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div id="respondModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center p-4">
<div class="lg:col-span-2"></div> <div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg animate-in">
<div class="p-6 border-b border-slate-200">
<div class="space-y-4"> <h3 class="text-xl font-bold text-navy flex items-center gap-2">
<!-- Quick Actions --> <i data-lucide="message-square" class="w-5 h-5"></i>
{% if can_edit %} {% trans "Send Response" %}
<div class="bg-white rounded-2xl border border-gray-50 shadow-sm p-6"> </h3>
<h4 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="zap" class="w-5 h-5"></i> {% trans "Quick Actions" %}
</h4>
<!-- Activate -->
<form method="post" action="{% url 'complaints:inquiry_activate' inquiry.id %}" class="mb-4">
{% csrf_token %}
{% if inquiry.assigned_to and inquiry.assigned_to == user %}
<button type="submit" class="w-full px-4 py-3 bg-green-500 text-white rounded-xl font-semibold flex items-center justify-center gap-2" disabled>
<i data-lucide="check-circle" class="w-5 h-5"></i> {% trans "Activated (Assigned to You)" %}
</button>
{% else %}
<button type="submit" class="w-full px-4 py-3 bg-cyan-500 text-white rounded-xl font-semibold hover:bg-cyan-600 transition flex items-center justify-center gap-2">
<i data-lucide="zap" class="w-5 h-5"></i> {% trans "Activate" %}
</button>
{% endif %}
</form>
<!-- Change Status -->
<form method="post" action="{% url 'complaints:inquiry_change_status' inquiry.id %}" class="mb-4">
{% csrf_token %}
<label class="block text-sm font-semibold text-gray-700 mb-2">{% trans "Change Status" %}</label>
<select name="status" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition mb-2" required>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if inquiry.status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<textarea name="note" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition mb-2" rows="2" placeholder="{% trans 'Optional note...' %}"></textarea>
<button type="submit" class="w-full px-4 py-3 bg-cyan-500 text-white rounded-xl font-semibold hover:bg-cyan-600 transition flex items-center justify-center gap-2">
<i data-lucide="refresh-cw" class="w-5 h-5"></i> {% trans "Update Status" %}
</button>
</form>
</div> </div>
{% endif %} <form method="post" action="{% url 'complaints:inquiry_respond' inquiry.pk %}">
{% csrf_token %}
<!-- Add Note --> <div class="p-6">
<div class="bg-white rounded-2xl border border-gray-50 shadow-sm p-6"> <div class="mb-4">
<h4 class="font-bold text-gray-800 mb-4 flex items-center gap-2"> <label class="block text-sm font-semibold text-navy mb-2">
<i data-lucide="message-circle" class="w-5 h-5"></i> {% trans "Add Note" %} {% trans "Response" %} <span class="text-red-500">*</span>
</h4> </label>
<textarea name="response" rows="5" required
<form method="post" action="{% url 'complaints:inquiry_add_note' inquiry.id %}"> class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue focus:ring-2 focus:ring-blue/20 resize-none"
{% csrf_token %} placeholder="{% trans 'Enter your response to the inquiry...' %}"></textarea>
<textarea name="note" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition mb-3" rows="3" placeholder="{% trans 'Enter your note...' %}" required></textarea>
<button type="submit" class="w-full px-4 py-3 bg-green-500 text-white rounded-xl font-semibold hover:bg-green-600 transition flex items-center justify-center gap-2">
<i data-lucide="plus-circle" class="w-5 h-5"></i> {% trans "Add Note" %}
</button>
</form>
</div>
<!-- Response Form -->
{% if can_edit and inquiry.status != 'resolved' and inquiry.status != 'closed' %}
<div class="bg-white rounded-2xl border border-gray-50 shadow-sm p-6">
<h4 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="reply" class="w-5 h-5"></i> {% trans "Respond to Inquiry" %}
</h4>
<form method="post" action="{% url 'complaints:inquiry_respond' inquiry.id %}">
{% csrf_token %}
<textarea name="response" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition mb-3" rows="4" placeholder="{% trans 'Enter your response...' %}" required></textarea>
<button type="submit" class="w-full px-4 py-3 bg-cyan-500 text-white rounded-xl font-semibold hover:bg-cyan-600 transition flex items-center justify-center gap-2">
<i data-lucide="send" class="w-5 h-5"></i> {% trans "Send Response" %}
</button>
</form>
</div>
{% endif %}
<!-- Contact Information -->
<div class="bg-white rounded-2xl border border-gray-50 shadow-sm p-6">
<h4 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
<i data-lucide="info" class="w-5 h-5"></i> {% trans "Contact Information" %}
</h4>
<div class="space-y-4">
{% if inquiry.patient %}
<div>
<div class="info-label mb-1">{% trans "Patient" %}</div>
<div class="text-gray-700 font-medium">
{{ inquiry.patient.get_full_name }}
<br>
<small class="text-gray-500">{% trans "MRN" %}: {{ inquiry.patient.mrn }}</small>
</div>
</div> </div>
{% if inquiry.patient.phone %}
<div>
<div class="info-label mb-1">{% trans "Phone" %}</div>
<div class="text-gray-700 font-medium">{{ inquiry.patient.phone }}</div>
</div>
{% endif %}
{% if inquiry.patient.email %}
<div>
<div class="info-label mb-1">{% trans "Email" %}</div>
<div class="text-gray-700 font-medium">{{ inquiry.patient.email }}</div>
</div>
{% endif %}
{% else %}
{% if inquiry.contact_name %}
<div>
<div class="info-label mb-1">{% trans "Name" %}</div>
<div class="text-gray-700 font-medium">{{ inquiry.contact_name }}</div>
</div>
{% endif %}
{% if inquiry.contact_phone %}
<div>
<div class="info-label mb-1">{% trans "Phone" %}</div>
<div class="text-gray-700 font-medium">{{ inquiry.contact_phone }}</div>
</div>
{% endif %}
{% if inquiry.contact_email %}
<div>
<div class="info-label mb-1">{% trans "Email" %}</div>
<div class="text-gray-700 font-medium">{{ inquiry.contact_email }}</div>
</div>
{% endif %}
{% endif %}
</div> </div>
</div> <div class="p-6 border-t border-slate-200 flex gap-3">
<button type="submit" class="btn-primary flex-1 justify-center">
<!-- Assignment Info --> <i data-lucide="send" class="w-4 h-4"></i>
<div class="bg-white rounded-2xl border border-gray-50 shadow-sm p-6"> {% trans "Send Response" %}
<h4 class="font-bold text-gray-800 mb-4 flex items-center gap-2"> </button>
<i data-lucide="user-check" class="w-5 h-5"></i> {% trans "Assignment Info" %} <button type="button" onclick="closeRespondModal()" class="btn-secondary">
</h4> {% trans "Cancel" %}
</button>
<div class="space-y-4">
<div>
<div class="info-label mb-1">{% trans "Assigned To" %}</div>
<div class="text-gray-700 font-medium">
{% if inquiry.assigned_to %}{{ inquiry.assigned_to.get_full_name }}{% else %}<span class="text-gray-400">{% trans "Unassigned" %}</span>{% endif %}
</div>
</div>
{% if inquiry.responded_by %}
<div>
<div class="info-label mb-1">{% trans "Responded By" %}</div>
<div class="text-gray-700 font-medium">
{{ inquiry.responded_by.get_full_name }}
<br>
<small class="text-gray-500">{{ inquiry.responded_at|date:"M d, Y H:i" }}</small>
</div>
</div>
{% endif %}
{% if inquiry.resolved_by %}
<div>
<div class="info-label mb-1">{% trans "Resolved By" %}</div>
<div class="text-gray-700 font-medium">
{{ inquiry.resolved_by.get_full_name }}
<br>
<small class="text-gray-500">{{ inquiry.resolved_at|date:"M d, Y H:i" }}</small>
</div>
</div>
{% endif %}
</div> </div>
</div> </form>
</div> </div>
</div> </div>
<script> <script>
// Tab switching document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-btn').forEach(btn => { lucide.createIcons();
btn.addEventListener('click', () => { });
// Remove active class from all tabs and panels
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); function showRespondModal() {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden')); document.getElementById('respondModal').classList.remove('hidden');
}
// Add active class to clicked tab
btn.classList.add('active'); function closeRespondModal() {
document.getElementById('respondModal').classList.add('hidden');
// Show corresponding panel }
const target = btn.dataset.target;
document.getElementById(target).classList.remove('hidden'); // Close modal on escape key
}); document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeRespondModal();
}
}); });
</script> </script>
{% endblock %}
<style>
.tab-btn {
@apply px-4 py-3 text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition border-b-2 border-transparent;
}
.tab-btn.active {
@apply text-cyan-500 border-cyan-500;
}
.tab-panel {
@apply block;
}
.tab-panel.hidden {
@apply hidden;
}
</style>
{% endblock %}

View File

@ -1,232 +1,372 @@
{% extends base_layout %} {% extends "layouts/base.html" %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title %}{{ _("New Inquiry")}} - PX360{% endblock %} {% block title %}{% trans "Create New Inquiry" %} - PX360{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.form-section { :root {
background: #fff; --hh-navy: #005696;
border: 1px solid #dee2e6; --hh-blue: #007bbd;
border-radius: 8px; --hh-light: #eef6fb;
padding: 25px; --hh-slate: #64748b;
margin-bottom: 20px;
} }
.form-section-title {
.form-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.form-section {
padding: 1.5rem;
border-bottom: 1px solid #f1f5f9;
}
.form-section:last-child {
border-bottom: none;
}
.section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700;
color: var(--hh-navy);
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600; font-weight: 600;
color: #495057; color: var(--hh-navy);
margin-bottom: 20px; font-size: 0.9rem;
padding-bottom: 10px; }
border-bottom: 2px solid #17a2b8;
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #cbd5e1;
border-radius: 0.75rem;
font-size: 0.95rem;
transition: all 0.2s;
background: white;
color: #1e293b;
font-family: 'Inter', sans-serif;
}
.form-input:focus {
outline: none;
border-color: var(--hh-blue);
box-shadow: 0 0 0 4px rgba(0, 123, 189, 0.1);
transform: translateY(-1px);
}
.form-input.error {
border-color: #ef4444;
}
.form-input.error:focus {
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1);
}
textarea.form-input {
resize: vertical;
min-height: 120px;
}
.form-help {
color: var(--hh-slate);
font-size: 0.825rem;
margin-top: 0.375rem;
}
.form-error {
color: #ef4444;
font-size: 0.825rem;
margin-top: 0.375rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--hh-navy) 0%, var(--hh-blue) 100%);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 86, 150, 0.3);
}
.btn-secondary {
background: white;
color: #475569;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 600;
border: 2px solid #e2e8f0;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-secondary:hover {
background: #f1f5f9;
border-color: #cbd5e1;
transform: translateY(-1px);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="px-4 py-6">
<!-- Breadcrumb -->
<nav class="mb-4 animate-in">
<ol class="flex items-center gap-2 text-sm text-slate">
<li><a href="{% url 'complaints:inquiry_list' %}" class="text-blue hover:text-navy font-medium">{% trans "Inquiries" %}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i></li>
<li class="text-navy font-semibold">{% trans "Create New Inquiry" %}</li>
</ol>
</nav>
<!-- Page Header --> <!-- Page Header -->
<div class="mb-4"> <div class="mb-6 animate-in">
{% if source_user %} <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<a href="{% url 'px_sources:source_user_inquiry_list' %}" class="btn btn-outline-secondary btn-sm mb-3"> <div class="w-10 h-10 bg-blue/10 rounded-xl flex items-center justify-center">
<i class="bi bi-arrow-left me-1"></i> {{ _("Back to My Inquiries")}} <i data-lucide="plus-circle" class="w-5 h-5 text-blue"></i>
</a> </div>
{% else %} {% trans "Create New Inquiry" %}
<a href="{% url 'complaints:inquiry_list' %}" class="btn btn-outline-secondary btn-sm mb-3"> </h1>
<i class="bi bi-arrow-left me-1"></i> {{ _("Back to Inquiries")}} <p class="text-slate mt-1">{% trans "Create a new patient inquiry or request" %}</p>
</a>
{% endif %}
<h2 class="mb-1">
<i class="bi bi-plus-circle text-info me-2"></i>
{{ _("Create New Inquiry")}}
</h2>
<p class="text-muted mb-0">{{ _("Create a new patient inquiry or request")}}</p>
</div> </div>
<form method="post" action="{% url 'complaints:inquiry_create' %}" id="inquiryForm"> <!-- Form Card -->
<form method="post" action="{% url 'complaints:inquiry_create' %}" id="inquiryForm" class="form-card max-w-5xl animate-in">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %} <div class="card-header">
<div class="alert alert-danger"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
{{ form.non_field_errors }} <i data-lucide="file-text" class="w-5 h-5"></i>
{% trans "Inquiry Information" %}
</h2>
</div> </div>
{% endif %}
<div class="p-0">
<div class="row"> <!-- Organization Section -->
<div class="col-lg-8"> <div class="form-section">
<!-- Organization Information --> <h3 class="section-title">
<div class="form-section"> <i data-lucide="building" class="w-5 h-5"></i>
<h5 class="form-section-title"> {% trans "Organization" %}
<i class="bi bi-hospital me-2"></i>{{ _("Organization") }} </h3>
</h5>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="mb-3"> <div class="form-group">
{{ form.hospital.label_tag }} <label for="{{ form.hospital.id_for_label }}" class="form-label">
{{ form.hospital.label }} <span class="text-red-500">*</span>
</label>
{{ form.hospital }} {{ form.hospital }}
{% if form.hospital.help_text %} {% if form.hospital.help_text %}
<small class="form-text text-muted">{{ form.hospital.help_text }}</small> <p class="form-help">{{ form.hospital.help_text }}</p>
{% endif %} {% endif %}
{% for error in form.hospital.errors %} {% for error in form.hospital.errors %}
<div class="invalid-feedback d-block">{{ error }}</div> <p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="mb-3"> <div class="form-group">
{{ form.department.label_tag }} <label for="{{ form.department.id_for_label }}" class="form-label">
{{ form.department.label }}
</label>
{{ form.department }} {{ form.department }}
{% for error in form.department.errors %} {% for error in form.department.errors %}
<div class="invalid-feedback d-block">{{ error }}</div> <p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
<!-- Contact/Patient Information --> <!-- Contact Information Section -->
<div class="form-section"> <div class="form-section">
<h5 class="form-section-title"> <h3 class="section-title">
<i class="bi bi-person-fill me-2"></i>{{ _("Contact Information")}} <i data-lucide="user" class="w-5 h-5"></i>
</h5> {% trans "Contact Information" %}
</h3>
<!-- Patient Field -->
<div class="mb-3">
{{ form.patient.label_tag }}
{{ form.patient }}
{% for error in form.patient.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
{{ form.contact_name.label_tag }} <div class="form-group">
<label for="{{ form.contact_name.id_for_label }}" class="form-label">
{{ form.contact_name.label }} <span class="text-red-500">*</span>
</label>
{{ form.contact_name }} {{ form.contact_name }}
{% for error in form.contact_name.errors %} {% for error in form.contact_name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div> <p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="row"> <div class="form-group">
<div class="col-md-6 mb-3"> <label for="{{ form.contact_phone.id_for_label }}" class="form-label">
{{ form.contact_phone.label_tag }} {{ form.contact_phone.label }} <span class="text-red-500">*</span>
{{ form.contact_phone }} </label>
{% for error in form.contact_phone.errors %} {{ form.contact_phone }}
<div class="invalid-feedback d-block">{{ error }}</div> {% for error in form.contact_phone.errors %}
{% endfor %} <p class="form-error">
</div> <i data-lucide="alert-circle" class="w-4 h-4"></i>
<div class="col-md-6 mb-3"> {{ error }}
{{ form.contact_email.label_tag }} </p>
{{ form.contact_email }} {% endfor %}
{% for error in form.contact_email.errors %} </div>
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %} <div class="form-group md:col-span-2">
</div> <label for="{{ form.contact_email.id_for_label }}" class="form-label">
{{ form.contact_email.label }}
</label>
{{ form.contact_email }}
{% for error in form.contact_email.errors %}
<p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %}
</div> </div>
</div> </div>
</div>
<!-- Inquiry Details --> <!-- Inquiry Details Section -->
<div class="form-section"> <div class="form-section">
<h5 class="form-section-title"> <h3 class="section-title">
<i class="bi bi-file-text me-2"></i>{{ _("Inquiry Details")}} <i data-lucide="help-circle" class="w-5 h-5"></i>
</h5> {% trans "Inquiry Details" %}
</h3>
<div class="mb-3">
{{ form.category.label_tag }} <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="form-group">
<label for="{{ form.category.id_for_label }}" class="form-label">
{{ form.category.label }} <span class="text-red-500">*</span>
</label>
{{ form.category }} {{ form.category }}
{% for error in form.category.errors %} {% for error in form.category.errors %}
<div class="invalid-feedback d-block">{{ error }}</div> <p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="mb-3"> <div class="form-group">
{{ form.subject.label_tag }} <label for="{{ form.priority.id_for_label }}" class="form-label">
{{ form.subject }} {{ form.priority.label }}
{% for error in form.subject.errors %} </label>
<div class="invalid-feedback d-block">{{ error }}</div> {{ form.priority }}
{% if form.priority.help_text %}
<p class="form-help">{{ form.priority.help_text }}</p>
{% endif %}
{% for error in form.priority.errors %}
<p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
<div class="mb-3"> <div class="form-group">
{{ form.message.label_tag }} <label for="{{ form.subject.id_for_label }}" class="form-label">
{{ form.message }} {{ form.subject.label }} <span class="text-red-500">*</span>
{% for error in form.message.errors %} </label>
<div class="invalid-feedback d-block">{{ error }}</div> {{ form.subject }}
{% endfor %} {% for error in form.subject.errors %}
</div> <p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %}
</div>
<div class="form-group">
<label for="{{ form.message.id_for_label }}" class="form-label">
{{ form.message.label }} <span class="text-red-500">*</span>
</label>
{{ form.message }}
{% for error in form.message.errors %}
<p class="form-error">
<i data-lucide="alert-circle" class="w-4 h-4"></i>
{{ error }}
</p>
{% endfor %}
</div> </div>
</div> </div>
</div>
<!-- Sidebar --> <!-- Form Actions -->
<div class="col-lg-4"> <div class="p-6 bg-slate-50 border-t border-slate-200 rounded-b-1rem flex gap-3">
<button type="submit" class="btn-primary">
<!-- Help Information --> <i data-lucide="check-circle" class="w-5 h-5"></i>
<div class="alert alert-info"> {% trans "Create Inquiry" %}
<h6 class="alert-heading"> </button>
<i class="bi bi-info-circle me-2"></i>{{ _("Help")}} <a href="{% url 'complaints:inquiry_list' %}" class="btn-secondary">
</h6> {% trans "Cancel" %}
<p class="mb-0 small"> </a>
{{ _("Use this form to create a new inquiry from a patient or visitor.")}}
</p>
<hr class="my-2">
<p class="mb-0 small">
{{ _("Fill in the inquiry details. Fields marked with * are required.")}}
</p>
</div>
<!-- Action Buttons -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle me-2"></i>{{ _("Create Inquiry")}}
</button>
{% if source_user %}
<a href="{% url 'px_sources:source_user_inquiry_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-2"></i>{{ _("Cancel") }}
</a>
{% else %}
<a href="{% url 'complaints:inquiry_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-2"></i>{{ _("Cancel") }}
</a>
{% endif %}
</div>
</div>
</div> </div>
</form> </form>
</div> </div>
{% endblock %}
{% block extra_js %}
<script> <script>
// Department loading document.addEventListener('DOMContentLoaded', function() {
document.getElementById('{{ form.hospital.id_for_label }}')?.addEventListener('change', function() { lucide.createIcons();
const hospitalId = this.value;
const departmentSelect = document.getElementById('{{ form.department.id_for_label }}');
if (!hospitalId) {
departmentSelect.innerHTML = '<option value="">{{ _("Select department")}}</option>';
return;
}
fetch(`/complaints/ajax/departments/?hospital_id=${hospitalId}`)
.then(response => response.json())
.then(data => {
departmentSelect.innerHTML = '<option value="">{{ _("Select department")}}</option>';
data.departments.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
option.textContent = dept.name_en || dept.name;
departmentSelect.appendChild(option);
});
})
.catch(error => console.error('Error loading departments:', error));
});
// Patient search (optional - for better UX) // Add error class to inputs with errors
const patientSelect = document.getElementById('{{ form.patient.id_for_label }}'); document.querySelectorAll('.form-error').forEach(function(errorEl) {
if (patientSelect) { const input = errorEl.parentElement.querySelector('.form-input');
patientSelect.addEventListener('change', function() { if (input) {
const selectedOption = this.options[this.selectedIndex]; input.classList.add('error');
if (!selectedOption || selectedOption.value === '') {
document.getElementById('patient-results').style.display = 'none';
} }
}); });
} });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,471 +1,367 @@
{% extends 'layouts/base.html' %} {% extends 'layouts/base.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title %}{{ _("Inquiries Console")}} - PX360{% endblock %} {% block title %}{% trans "Inquiries Console" %} - PX360{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.filter-panel { :root {
background: #f8f9fa; --hh-navy: #005696;
border: 1px solid #dee2e6; --hh-blue: #007bbd;
border-radius: 8px; --hh-light: #eef6fb;
padding: 20px; --hh-slate: #64748b;
margin-bottom: 20px; --hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
} }
.filter-panel.collapsed {
padding: 10px 20px; .page-header {
} background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
.filter-panel.collapsed .filter-body {
display: none;
}
.table-toolbar {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 500;
}
.status-open { background: #e3f2fd; color: #1976d2; }
.status-in_progress { background: #fff3e0; color: #f57c00; }
.status-resolved { background: #e8f5e9; color: #388e3c; }
.status-closed { background: #f5f5f5; color: #616161; }
.priority-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 500;
}
.priority-low { background: #e8f5e9; color: #388e3c; }
.priority-medium { background: #fff3e0; color: #f57c00; }
.priority-high { background: #ffebee; color: #d32f2f; }
.priority-urgent { background: #880e4f; color: #fff; }
.overdue-badge {
background: #d32f2f;
color: white; color: white;
padding: 2px 8px; padding: 2rem 2.5rem;
border-radius: 10px; border-radius: 1rem;
font-size: 0.75rem; margin-bottom: 2rem;
font-weight: 600; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
margin-left: 8px;
} }
.inquiry-row:hover { .data-card {
background: #f8f9fa; background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.data-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.data-table th {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 0.875rem 1rem;
text-align: left;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--hh-navy);
border-bottom: 2px solid #bae6fd;
}
.data-table td {
padding: 1rem;
border-bottom: 1px solid #f1f5f9;
color: #475569;
font-size: 0.875rem;
}
.data-table tbody tr {
transition: background-color 0.2s ease;
cursor: pointer; cursor: pointer;
} }
.stat-card { .data-table tbody tr:hover {
border-left: 4px solid; background-color: var(--hh-light);
transition: transform 0.2s;
} }
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.875rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
}
.status-badge.open { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #1565c0; }
.status-badge.in_progress { background: linear-gradient(135deg, #fff3e0, #ffe0b2); color: #e65100; }
.status-badge.resolved { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); color: #2e7d32; }
.status-badge.closed { background: linear-gradient(135deg, #f5f5f5, #e0e0e0); color: #616161; }
.priority-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.625rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.priority-badge.low { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); color: #2e7d32; }
.priority-badge.medium { background: linear-gradient(135deg, #fff3e0, #ffe0b2); color: #e65100; }
.priority-badge.high { background: linear-gradient(135deg, #ffebee, #ffcdd2); color: #c62828; }
.priority-badge.urgent { background: linear-gradient(135deg, #880e4f, #ad1457); color: white; }
.stat-card {
background: white;
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid #e2e8f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-card:hover { .stat-card:hover {
transform: translateY(-2px); transform: translateY(-4px);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.stat-icon.blue { background: linear-gradient(135deg, #e3f2fd, #bbdefb); }
.stat-icon.green { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); }
.stat-icon.orange { background: linear-gradient(135deg, #fff3e0, #ffe0b2); }
.stat-icon.slate { background: linear-gradient(135deg, #f1f5f9, #e2e8f0); }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="px-4 py-6">
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="page-header animate-in">
<div> <div class="flex items-center justify-between">
<h2 class="mb-1"> <div>
<i class="bi bi-question-circle-fill text-info me-2"></i> <h1 class="text-2xl font-bold mb-2">
{{ _("Inquiries Console")}} <i data-lucide="help-circle" class="w-7 h-7 inline-block me-2"></i>
</h2> {% trans "Inquiries Console" %}
<p class="text-muted mb-0">{{ _("Manage patient inquiries and requests")}}</p> </h1>
</div> <p class="text-white/90">{% trans "Manage patient inquiries and requests" %}</p>
<div> </div>
<a href="{% url 'complaints:inquiry_create' %}" class="btn btn-primary"> <a href="{% url 'complaints:inquiry_create' %}"
<i class="bi bi-plus-circle me-1"></i> {{ _("New Inquiry")}} class="inline-flex items-center gap-2 bg-white text-navy px-5 py-2.5 rounded-xl font-bold hover:bg-light transition shadow-lg">
<i data-lucide="plus" class="w-4 h-4"></i>
{% trans "New Inquiry" %}
</a> </a>
</div> </div>
</div> </div>
<!-- Statistics Cards --> <!-- Statistics Cards -->
<div class="row mb-4"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 animate-in">
<div class="col-md-3"> <div class="stat-card">
<div class="card stat-card border-primary"> <div class="stat-icon blue">
<div class="card-body"> <i data-lucide="inbox" class="w-6 h-6 text-blue-600"></i>
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">{% trans "Total Inquiries" %}</h6>
<h3 class="mb-0">{{ stats.total }}</h3>
</div>
<div class="text-primary">
<i class="bi bi-list-ul" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div> </div>
<p class="text-xs font-bold text-slate uppercase tracking-wider">{% trans "Total Inquiries" %}</p>
<p class="text-2xl font-black text-navy mt-1">{{ stats.total }}</p>
</div> </div>
<div class="col-md-3"> <div class="stat-card">
<div class="card stat-card border-info"> <div class="stat-icon green">
<div class="card-body"> <i data-lucide="check-circle" class="w-6 h-6 text-green-600"></i>
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">{% trans "Open" %}</h6>
<h3 class="mb-0">{{ stats.open }}</h3>
</div>
<div class="text-info">
<i class="bi bi-folder2-open" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div> </div>
<p class="text-xs font-bold text-slate uppercase tracking-wider">{% trans "Resolved" %}</p>
<p class="text-2xl font-black text-navy mt-1">{{ stats.resolved }} <span class="text-sm text-green-600">({{ stats.resolved_percentage|floatformat:1 }}%)</span></p>
</div> </div>
<div class="col-md-3"> <div class="stat-card">
<div class="card stat-card border-warning"> <div class="stat-icon orange">
<div class="card-body"> <i data-lucide="clock" class="w-6 h-6 text-orange-600"></i>
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">{% trans "In Progress" %}</h6>
<h3 class="mb-0">{{ stats.in_progress }}</h3>
</div>
<div class="text-warning">
<i class="bi bi-hourglass-split" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div> </div>
<p class="text-xs font-bold text-slate uppercase tracking-wider">{% trans "In Progress" %}</p>
<p class="text-2xl font-black text-navy mt-1">{{ stats.in_progress }}</p>
</div> </div>
<div class="col-md-3"> <div class="stat-card">
<div class="card stat-card border-success"> <div class="stat-icon slate">
<div class="card-body"> <i data-lucide="alert-triangle" class="w-6 h-6 text-red-500"></i>
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">{% trans "Resolved" %}</h6>
<h3 class="mb-0 text-success">{{ stats.resolved }}</h3>
</div>
<div class="text-success">
<i class="bi bi-check-circle-fill" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div> </div>
<p class="text-xs font-bold text-slate uppercase tracking-wider">{% trans "Overdue" %}</p>
<p class="text-2xl font-black text-navy mt-1">{{ stats.overdue }}</p>
</div> </div>
</div> </div>
<!-- Filter Panel --> <!-- Filters -->
<div class="filter-panel" id="filterPanel"> <div class="data-card mb-6 animate-in">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="card-header">
<h5 class="mb-0"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i class="bi bi-funnel me-2"></i>{{ _("Filters") }} <i data-lucide="filter" class="w-5 h-5"></i>
</h5> {% trans "Filters" %}
<button class="btn btn-sm btn-outline-secondary" onclick="toggleFilters()"> </h2>
<i class="bi bi-chevron-up" id="filterToggleIcon"></i>
</button>
</div> </div>
<div class="p-6">
<div class="filter-body"> <form method="get" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<form method="get" action="{% url 'complaints:inquiry_list' %}" id="filterForm"> <div>
<div class="row g-3"> <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Search" %}</label>
<!-- Search --> <input type="text" name="search" value="{{ filters.search }}"
<div class="col-md-4"> placeholder="{% trans 'Subject, contact name...' %}"
<label class="form-label">{% trans "Search" %}</label> class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue focus:ring-2 focus:ring-blue/20">
<input type="text" class="form-control" name="search"
placeholder="{% trans 'Subject, contact name...' %}"
value="{{ filters.search }}">
</div>
<!-- Status -->
<div class="col-md-4">
<label class="form-label">{% trans "Status" %}</label>
<select class="form-select" name="status">
<option value="">{{ _("All Statuses")}}</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if filters.status == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<!-- Priority -->
<div class="col-md-4">
<label class="form-label">{% trans "Priority" %}</label>
<select class="form-select" name="priority">
<option value="">{{ _("All Priorities")}}</option>
<option value="low" {% if filters.priority == 'low' %}selected{% endif %}>{{ _("Low") }}</option>
<option value="medium" {% if filters.priority == 'medium' %}selected{% endif %}>{{ _("Medium") }}</option>
<option value="high" {% if filters.priority == 'high' %}selected{% endif %}>{{ _("High") }}</option>
<option value="urgent" {% if filters.priority == 'urgent' %}selected{% endif %}>{{ _("Urgent") }}</option>
</select>
</div>
<!-- Category -->
<div class="col-md-4">
<label class="form-label">{% trans "Category" %}</label>
<select class="form-select" name="category">
<option value="">{{ _("All Categories")}}</option>
<option value="appointment" {% if filters.category == 'appointment' %}selected{% endif %}>{{ _("Appointment")}}</option>
<option value="billing" {% if filters.category == 'billing' %}selected{% endif %}>{{ _("Billing") }}</option>
<option value="medical_records" {% if filters.category == 'medical_records' %}selected{% endif %}>{{ _("Medical Records")}}</option>
<option value="pharmacy" {% if filters.category == 'pharmacy' %}selected{% endif %}>{{ _("Pharmacy")}}</option>
<option value="insurance" {% if filters.category == 'insurance' %}selected{% endif %}>{{ _("Insurance")}}</option>
<option value="feedback" {% if filters.category == 'feedback' %}selected{% endif %}>{{ _("Feedback")}}</option>
<option value="general" {% if filters.category == 'general' %}selected{% endif %}>{{ _("General")}}</option>
<option value="other" {% if filters.category == 'other' %}selected{% endif %}>{{ _("Other") }}</option>
</select>
</div>
<!-- Hospital -->
<div class="col-md-4">
<label class="form-label">{% trans "Hospital" %}</label>
<select class="form-select" name="hospital">
<option value="">{{ _("All Hospitals")}}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name_en }}
</option>
{% endfor %}
</select>
</div>
<!-- Assigned To -->
<div class="col-md-4">
<label class="form-label">{% trans "Assigned To" %}</label>
<select class="form-select" name="assigned_to">
<option value="">{{ _("All Users")}}</option>
{% for user_obj in assignable_users %}
<option value="{{ user_obj.id }}" {% if filters.assigned_to == user_obj.id|stringformat:"s" %}selected{% endif %}>
{{ user_obj.get_full_name }}
</option>
{% endfor %}
</select>
</div>
<!-- Date Range -->
<div class="col-md-3">
<label class="form-label">{% trans "Date From" %}</label>
<input type="date" class="form-control" name="date_from" value="{{ filters.date_from }}">
</div>
<div class="col-md-3">
<label class="form-label">{% trans "Date To" %}</label>
<input type="date" class="form-control" name="date_to" value="{{ filters.date_to }}">
</div>
</div> </div>
<div>
<div class="mt-3 d-flex gap-2"> <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
<button type="submit" class="btn btn-primary"> <select name="status" class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<i class="bi bi-search me-1"></i> {{ _("Apply Filters")}} <option value="">{% trans "All Status" %}</option>
<option value="open" {% if filters.status == 'open' %}selected{% endif %}>{% trans "Open" %}</option>
<option value="in_progress" {% if filters.status == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
<option value="resolved" {% if filters.status == 'resolved' %}selected{% endif %}>{% trans "Resolved" %}</option>
<option value="closed" {% if filters.status == 'closed' %}selected{% endif %}>{% trans "Closed" %}</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Category" %}</label>
<select name="category" class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<option value="">{% trans "All Categories" %}</option>
<option value="general" {% if filters.category == 'general' %}selected{% endif %}>{% trans "General" %}</option>
<option value="services" {% if filters.category == 'services' %}selected{% endif %}>{% trans "Services" %}</option>
<option value="appointments" {% if filters.category == 'appointments' %}selected{% endif %}>{% trans "Appointments" %}</option>
<option value="billing" {% if filters.category == 'billing' %}selected{% endif %}>{% trans "Billing" %}</option>
<option value="medical" {% if filters.category == 'medical' %}selected{% endif %}>{% trans "Medical Records" %}</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full px-6 py-2.5 bg-navy text-white rounded-xl font-bold hover:bg-blue transition shadow-lg shadow-navy/25">
<i data-lucide="search" class="w-4 h-4 inline-block me-2"></i>
{% trans "Filter" %}
</button> </button>
<a href="{% url 'complaints:inquiry_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i> {{ _("Clear") }}
</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Table Toolbar -->
<div class="table-toolbar">
<div>
<span class="text-muted">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} inquiries
</span>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary" onclick="exportData('csv')">
<i class="bi bi-file-earmark-spreadsheet me-1"></i> {{ _("Export CSV")}}
</button>
<button class="btn btn-sm btn-outline-primary" onclick="exportData('excel')">
<i class="bi bi-file-earmark-excel me-1"></i> {{ _("Export Excel")}}
</button>
</div>
</div>
<!-- Inquiries Table --> <!-- Inquiries Table -->
<div class="card"> <div class="data-card animate-in">
<div class="card-body p-0"> <div class="card-header">
<div class="table-responsive"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<table class="table table-hover mb-0"> <i data-lucide="help-circle" class="w-5 h-5"></i>
<thead class="table-light"> {% trans "All Inquiries" %} ({{ inquiries.paginator.count }})
</h2>
</div>
<div class="p-0">
{% if inquiries %}
<div class="overflow-x-auto">
<table class="w-full data-table">
<thead>
<tr> <tr>
<th style="width: 50px;"> <th>{% trans "Reference" %}</th>
<input type="checkbox" class="form-check-input" id="selectAll">
</th>
<th>{% trans "ID" %}</th>
<th>{% trans "Subject" %}</th> <th>{% trans "Subject" %}</th>
<th>{% trans "Contact" %}</th> <th>{% trans "Contact" %}</th>
<th>{% trans "Category" %}</th> <th>{% trans "Category" %}</th>
<th>{% trans "Status" %}</th> <th class="text-center">{% trans "Status" %}</th>
<th>{% trans "Priority" %}</th> <th class="text-center">{% trans "Priority" %}</th>
<th>{% trans "Hospital" %}</th> <th class="text-center">{% trans "Hospital" %}</th>
<th>{% trans "Assigned To" %}</th>
<th>{% trans "Due Date" %}</th>
<th>{% trans "Created" %}</th> <th>{% trans "Created" %}</th>
<th>{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-slate-100">
{% for inquiry in inquiries %} {% for inquiry in inquiries %}
<tr class="inquiry-row" onclick="window.location='{% url 'complaints:inquiry_detail' inquiry.id %}'"> <tr onclick="window.location='{% url 'complaints:inquiry_detail' inquiry.pk %}'">
<td onclick="event.stopPropagation();"> <td>
<input type="checkbox" class="form-check-input inquiry-checkbox" <span class="font-mono text-xs bg-slate-100 px-2 py-1 rounded">{{ inquiry.reference_number|truncatechars:15 }}</span>
value="{{ inquiry.id }}">
</td> </td>
<td> <td>
<small class="text-muted">#{{ inquiry.id|slice:":8" }}</small> <a href="{% url 'complaints:inquiry_detail' inquiry.pk %}"
class="font-semibold text-navy hover:text-blue transition">
{{ inquiry.subject|truncatechars:40 }}
</a>
</td> </td>
<td> <td>
<div class="d-flex align-items-center"> <div>
{{ inquiry.subject|truncatewords:8 }} <p class="font-medium text-slate-700">{{ inquiry.contact_name|default:"-" }}</p>
{% if inquiry.is_overdue %} <p class="text-xs text-slate">{{ inquiry.contact_phone|default:inquiry.contact_email|default:"-" }}</p>
<span class="overdue-badge">{{ _("OVERDUE") }}</span>
{% endif %}
</div> </div>
</td> </td>
<td> <td>
{% if inquiry.patient %} <span class="text-sm text-slate">{{ inquiry.get_category_display|default:"-" }}</span>
<strong>{{ inquiry.patient.get_full_name }}</strong><br>
<small class="text-muted">{{ _("MRN") }}: {{ inquiry.patient.mrn }}</small>
{% else %}
<strong>{{ inquiry.contact_name|default:inquiry.contact_email }}</strong><br>
<small class="text-muted">{{ inquiry.contact_email }}</small>
{% endif %}
</td> </td>
<td> <td class="text-center">
<span class="badge bg-secondary">{{ inquiry.get_category_display }}</span> <span class="status-badge {{ inquiry.status }}">
</td> <i data-lucide="{% if inquiry.status == 'open' %}circle{% elif inquiry.status == 'in_progress' %}clock{% elif inquiry.status == 'resolved' %}check-circle{% else %}check{% endif %}" class="w-3 h-3"></i>
<td>
<span class="status-badge status-{{ inquiry.status }}">
{{ inquiry.get_status_display }} {{ inquiry.get_status_display }}
</span> </span>
</td> </td>
<td> <td class="text-center">
{% if inquiry.priority %} <span class="priority-badge {{ inquiry.priority }}">
<span class="priority-badge priority-{{ inquiry.priority }}"> {% if inquiry.priority == 'low' %}
<i data-lucide="arrow-down" class="w-3 h-3"></i>
{% elif inquiry.priority == 'medium' %}
<i data-lucide="minus" class="w-3 h-3"></i>
{% elif inquiry.priority == 'high' %}
<i data-lucide="arrow-up" class="w-3 h-3"></i>
{% else %}
<i data-lucide="zap" class="w-3 h-3"></i>
{% endif %}
{{ inquiry.get_priority_display }} {{ inquiry.get_priority_display }}
</span> </span>
{% else %} </td>
<span class="text-muted"><em>-</em></span> <td class="text-center">
{% endif %} <span class="text-sm text-slate">{{ inquiry.hospital.name|truncatechars:15 }}</span>
</td> </td>
<td> <td>
<small>{{ inquiry.hospital.name_en|truncatewords:3 }}</small> <div class="text-sm text-slate">
</td> <p>{{ inquiry.created_at|date:"Y-m-d" }}</p>
<td> <p class="text-xs">{{ inquiry.created_at|date:"H:i" }}</p>
{% if inquiry.assigned_to %}
<small>{{ inquiry.assigned_to.get_full_name }}</small>
{% else %}
<span class="text-muted"><em>{{ _("Unassigned") }}</em></span>
{% endif %}
</td>
<td>
{% if inquiry.due_date %}
<small class="{% if inquiry.is_overdue %}text-danger fw-bold{% endif %}">
{{ inquiry.due_date|date:"M d, Y H:i" }}
</small>
{% else %}
<span class="text-muted"><em>-</em></span>
{% endif %}
</td>
<td>
<small class="text-muted">{{ inquiry.created_at|date:"M d, Y" }}</small>
</td>
<td onclick="event.stopPropagation();">
<div class="btn-group btn-group-sm">
<a href="{% url 'complaints:inquiry_detail' inquiry.id %}"
class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="bi bi-eye"></i>
</a>
</div> </div>
</td> </td>
</tr> </tr>
{% empty %}
<tr>
<td colspan="12" class="text-center py-5">
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
<p class="text-muted mt-3">{{ _("No inquiries found")}}</p>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Pagination -->
{% if inquiries.has_other_pages %}
<div class="p-4 border-t border-slate-200">
<div class="flex items-center justify-between">
<p class="text-sm text-slate">
{% blocktrans with start=inquiries.start_index end=inquiries.end_index total=inquiries.paginator.count %}
Showing {{ start }} to {{ end }} of {{ total }} inquiries
{% endblocktrans %}
</p>
<div class="flex gap-2">
{% if inquiries.has_previous %}
<a href="?page={{ inquiries.previous_page_number }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.status %}&status={{ filters.status }}{% endif %}{% if filters.category %}&category={{ filters.category }}{% endif %}"
class="px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-sm font-medium">
{% trans "Previous" %}
</a>
{% endif %}
{% if inquiries.has_next %}
<a href="?page={{ inquiries.next_page_number }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.status %}&status={{ filters.status }}{% endif %}{% if filters.category %}&category={{ filters.category }}{% endif %}"
class="px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-sm font-medium">
{% trans "Next" %}
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-12">
<div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="help-circle" class="w-8 h-8 text-slate-400"></i>
</div>
<p class="text-slate font-medium">{% trans "No inquiries found" %}</p>
<p class="text-slate text-sm mt-1">{% trans "Adjust your filters or create a new inquiry" %}</p>
</div>
{% endif %}
</div> </div>
</div> </div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Inquiries pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
<i class="bi bi-chevron-double-left"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
{{ num }}
</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
<i class="bi bi-chevron-double-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
{% endblock %}
{% block extra_js %}
<script> <script>
function toggleFilters() { document.addEventListener('DOMContentLoaded', function() {
const panel = document.getElementById('filterPanel'); lucide.createIcons();
const icon = document.getElementById('filterToggleIcon');
panel.classList.toggle('collapsed');
icon.classList.toggle('bi-chevron-up');
icon.classList.toggle('bi-chevron-down');
}
function exportData(format) {
const params = new URLSearchParams(window.location.search);
params.set('export', format);
window.location.href = '{% url "complaints:inquiry_list" %}?' + params.toString();
}
// Select all checkbox
document.getElementById('selectAll')?.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.inquiry-checkbox');
checkboxes.forEach(cb => cb.checked = this.checked);
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -11,6 +11,31 @@
{% if complaint.resolution %} {% if complaint.resolution %}
<p class="text-slate-700 mb-4">{{ complaint.resolution|linebreaks }}</p> <p class="text-slate-700 mb-4">{{ complaint.resolution|linebreaks }}</p>
{% endif %} {% endif %}
<!-- Resolution Outcome Display -->
{% if complaint.resolution_outcome %}
<div class="mb-4 pt-4 border-t border-green-200">
<p class="text-sm font-semibold text-green-800 mb-2">{% trans "Resolution Outcome" %}</p>
<div class="flex items-center gap-2">
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-bold
{% if complaint.resolution_outcome == 'patient' %}bg-blue-100 text-blue-800
{% elif complaint.resolution_outcome == 'hospital' %}bg-green-100 text-green-800
{% else %}bg-orange-100 text-orange-800{% endif %}">
{% if complaint.resolution_outcome == 'patient' %}
<i data-lucide="user" class="w-4 h-4 mr-1.5"></i>{% trans "Patient" %}
{% elif complaint.resolution_outcome == 'hospital' %}
<i data-lucide="building-2" class="w-4 h-4 mr-1.5"></i>{% trans "Hospital" %}
{% else %}
<i data-lucide="circle" class="w-4 h-4 mr-1.5"></i>{% trans "Other" %}
{% endif %}
</span>
{% if complaint.resolution_outcome == 'other' and complaint.resolution_outcome_other %}
<span class="text-sm text-slate">({{ complaint.resolution_outcome_other }})</span>
{% endif %}
</div>
</div>
{% endif %}
<div class="text-sm text-slate"> <div class="text-sm text-slate">
<p>{% trans "Resolved by:" %} {{ complaint.resolved_by.get_full_name|default:"-" }}</p> <p>{% trans "Resolved by:" %} {{ complaint.resolved_by.get_full_name|default:"-" }}</p>
<p>{% trans "Resolved at:" %} {{ complaint.resolved_at|date:"M d, Y H:i"|default:"-" }}</p> <p>{% trans "Resolved at:" %} {{ complaint.resolved_at|date:"M d, Y H:i"|default:"-" }}</p>
@ -76,6 +101,25 @@
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Resolution Notes" %}</label> <label class="block text-sm font-semibold text-slate mb-2">{% trans "Resolution Notes" %}</label>
<textarea name="resolution" id="resolutionTextarea" rows="6" class="w-full border border-slate-200 rounded-xl p-4 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Enter resolution details...' %}"></textarea> <textarea name="resolution" id="resolutionTextarea" rows="6" class="w-full border border-slate-200 rounded-xl p-4 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Enter resolution details...' %}"></textarea>
</div> </div>
<!-- Resolution Outcome - Who was in wrong/right -->
<div class="mb-4">
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Resolution Outcome" %}</label>
<p class="text-xs text-slate mb-2">{% trans "Who was in wrong / who was in right?" %}</p>
<select name="resolution_outcome" id="resolutionOutcome" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none bg-white">
<option value="">-- {% trans "Select Outcome" %} --</option>
<option value="patient">{% trans "Patient" %}</option>
<option value="hospital">{% trans "Hospital" %}</option>
<option value="other">{% trans "Other — please specify" %}</option>
</select>
</div>
<!-- Other specification field (shown when Other is selected) -->
<div class="mb-4 hidden" id="otherSpecificationDiv">
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Please Specify" %}</label>
<textarea name="resolution_outcome_other" id="resolutionOutcomeOther" rows="3" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Specify who was in wrong/right...' %}"></textarea>
</div>
<button type="submit" class="w-full px-6 py-3 bg-green-500 text-white rounded-xl font-bold hover:bg-green-600 transition flex items-center justify-center gap-2"> <button type="submit" class="w-full px-6 py-3 bg-green-500 text-white rounded-xl font-bold hover:bg-green-600 transition flex items-center justify-center gap-2">
<i data-lucide="check-circle" class="w-5 h-5"></i> {% trans "Mark as Resolved" %} <i data-lucide="check-circle" class="w-5 h-5"></i> {% trans "Mark as Resolved" %}
</button> </button>
@ -171,7 +215,7 @@ function selectResolution(lang) {
const textarea = document.getElementById('resolutionTextarea'); const textarea = document.getElementById('resolutionTextarea');
const radioEn = document.getElementById('resEn'); const radioEn = document.getElementById('resEn');
const radioAr = document.getElementById('resAr'); const radioAr = document.getElementById('resAr');
if (lang === 'en') { if (lang === 'en') {
radioEn.checked = true; radioEn.checked = true;
radioAr.checked = false; radioAr.checked = false;
@ -183,7 +227,7 @@ function selectResolution(lang) {
textarea.value = generatedResolutions.ar; textarea.value = generatedResolutions.ar;
textarea.dir = 'rtl'; textarea.dir = 'rtl';
} }
// Highlight selected card // Highlight selected card
const cards = document.querySelectorAll('#aiResolutionSelection > div'); const cards = document.querySelectorAll('#aiResolutionSelection > div');
cards.forEach((card, index) => { cards.forEach((card, index) => {
@ -194,4 +238,20 @@ function selectResolution(lang) {
} }
}); });
} }
// Show/hide "Other" specification field
document.addEventListener('DOMContentLoaded', function() {
const outcomeSelect = document.getElementById('resolutionOutcome');
const otherDiv = document.getElementById('otherSpecificationDiv');
if (outcomeSelect) {
outcomeSelect.addEventListener('change', function() {
if (this.value === 'other') {
otherDiv.classList.remove('hidden');
} else {
otherDiv.classList.add('hidden');
}
});
}
});
</script> </script>

View File

@ -3,210 +3,212 @@
{% block title %}{% trans "Track Your Complaint" %}{% endblock %} {% block title %}{% trans "Track Your Complaint" %}{% endblock %}
{% block extra_css %}
<style>
.glass-effect {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.timeline-dot::after {
content: '';
position: absolute;
width: 2px;
height: 100%;
background: #e2e8f0;
left: 50%;
transform: translateX(-50%);
top: 24px;
z-index: 0;
}
.timeline-item:last-child .timeline-dot::after {
display: none;
}
@keyframes subtle-float {
0% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0px); }
}
.float-icon { animation: subtle-float 3s ease-in-out infinite; }
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="min-h-screen bg-gradient-to-br from-light to-blue-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-4xl mx-auto px-4 py-8 md:py-12">
<div class="max-w-4xl mx-auto"> <a href="/" class="inline-flex items-center gap-2 text-navy/70 hover:text-navy mb-8 transition-all font-medium group">
<!-- Back Link --> <div class="p-2 rounded-full group-hover:bg-navy/5 transition-colors">
<a href="/" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 mb-8 transition font-medium"> <i data-lucide="arrow-left" class="w-5 h-5 group-hover:-translate-x-1 transition-transform"></i>
<i data-lucide="arrow-left" class="w-5 h-5"></i> </div>
{% trans "Back to Home" %} {% trans "Back to Home" %}
</a> </a>
<!-- Search Card --> <div class="glass-card rounded-[2rem] shadow-xl border border-white/50 p-8 md:p-12 mb-10 transition-all hover:shadow-2xl animate-fade-in overflow-hidden relative">
<div class="bg-white rounded-3xl shadow-xl p-8 md:p-12 mb-8"> <div class="absolute -top-24 -right-24 w-48 h-48 bg-blue/5 rounded-full blur-3xl"></div>
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-20 h-20 bg-blue-100 rounded-full mb-6"> <div class="text-center mb-10 relative">
<i data-lucide="search" class="w-10 h-10 text-blue-500"></i> <div class="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-tr from-navy via-navy to-blue rounded-3xl mb-6 shadow-xl rotate-3 float-icon">
</div> <i data-lucide="search" class="w-10 h-10 text-white -rotate-3"></i>
<h1 class="text-3xl font-bold text-gray-800 mb-2">
{% trans "Track Your Complaint" %}
</h1>
<p class="text-gray-500 text-lg">
{% trans "Enter your reference number to check the status of your complaint" %}
</p>
</div> </div>
<h1 class="text-4xl font-extrabold text-navy mb-3 tracking-tight">
<form method="POST" class="max-w-lg mx-auto"> {% trans "Track Your Complaint" %}
{% csrf_token %} </h1>
<div class="mb-4"> <p class="text-slate/80 text-lg max-w-md mx-auto">
<input {% trans "Enter your reference number below to see real-time updates on your request." %}
type="text"
name="reference_number"
class="w-full px-6 py-4 border-2 border-gray-200 rounded-xl text-gray-800 text-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
placeholder="{% trans 'e.g., CMP-20240101-123456' %}"
value="{{ reference_number }}"
required
>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-8 py-4 rounded-xl font-bold text-lg transition shadow-lg shadow-blue-200 hover:shadow-xl hover:-translate-y-0.5">
<i data-lucide="search" class="inline w-5 h-5 mr-2"></i>
{% trans "Track Complaint" %}
</button>
</form>
<p class="text-center text-gray-400 text-sm mt-4">
{% trans "Your reference number was provided when you submitted your complaint" %}
</p> </p>
</div> </div>
<!-- Error Message --> <form method="POST" class="max-w-lg mx-auto relative">
{% if error_message %} {% csrf_token %}
<div class="bg-yellow-50 border border-yellow-200 rounded-2xl p-6 mb-8 flex items-start gap-4"> <div class="relative group">
<div class="text-yellow-500 flex-shrink-0"> <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<i data-lucide="alert-triangle" class="w-6 h-6"></i> <i data-lucide="hash" class="w-5 h-5 text-slate/40 group-focus-within:text-blue transition-colors"></i>
</div>
<input
type="text"
name="reference_number"
class="w-full pl-12 pr-6 py-5 border-2 border-slate-100 rounded-2xl text-navy text-lg focus:ring-4 focus:ring-blue/10 focus:border-blue transition-all duration-300 bg-white/80 placeholder:text-slate/30"
placeholder="{% trans 'e.g., CMP-20240101-123456' %}"
value="{{ reference_number }}"
required
>
</div> </div>
<div> <button type="submit" class="w-full mt-4 bg-navy hover:bg-navy/90 text-white px-8 py-5 rounded-2xl font-bold text-lg transition-all duration-300 shadow-lg shadow-navy/20 hover:shadow-xl hover:-translate-y-1 flex items-center justify-center gap-3">
<strong class="text-yellow-800 block mb-1">{% trans "Not Found" %}</strong> <i data-lucide="crosshair" class="w-5 h-5"></i>
<span class="text-yellow-700">{{ error_message }}</span> {% trans "Track Status" %}
</button>
</form>
<p class="text-center text-slate/50 text-xs mt-6 uppercase tracking-widest font-semibold">
<i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
{% trans "Found in your confirmation email" %}
</p>
</div>
{% if error_message %}
<div class="bg-rose-50 border border-rose-100 rounded-2xl p-6 mb-8 flex items-center gap-4 animate-shake">
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center text-rose-600 shrink-0">
<i data-lucide="alert-circle" class="w-6 h-6"></i>
</div>
<div>
<h3 class="font-bold text-rose-900">{% trans "Reference Not Found" %}</h3>
<p class="text-rose-700/80 text-sm">{{ error_message }}</p>
</div>
</div>
{% endif %}
{% if complaint %}
<div class="animate-slide-up" style="animation-delay: 0.1s">
<div class="bg-white rounded-3xl shadow-lg border border-slate-100 p-6 md:p-8 mb-6 relative overflow-hidden">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<span class="text-xs font-bold text-slate/40 uppercase tracking-widest block mb-1">{% trans "Case Reference" %}</span>
<h2 class="text-3xl font-black text-navy">{{ complaint.reference_number }}</h2>
</div>
<div class="flex items-center gap-3">
<div class="text-right hidden md:block">
<span class="text-xs font-bold text-slate/40 uppercase tracking-widest block mb-1">{% trans "Current Status" %}</span>
<p class="font-bold text-navy">{{ complaint.get_status_display }}</p>
</div>
<div class="px-6 py-3 rounded-2xl text-sm font-black uppercase tracking-wider shadow-sm border-b-4
{% if complaint.status == 'open' %}bg-amber-50 text-amber-700 border-amber-200
{% elif complaint.status == 'in_progress' %}bg-blue-50 text-blue-700 border-blue-200
{% elif complaint.status == 'resolved' %}bg-emerald-50 text-emerald-700 border-emerald-200
{% else %}bg-slate-50 text-slate-700 border-slate-200{% endif %}">
{{ complaint.status }}
</div>
</div>
</div>
<div class="mt-8 h-2 w-full bg-slate-100 rounded-full overflow-hidden">
<div class="h-full bg-navy transition-all duration-1000"
style="width: {% if complaint.status == 'resolved' %}100%{% elif complaint.status == 'in_progress' %}50%{% else %}15%{% endif %}">
</div>
</div> </div>
</div> </div>
{% endif %}
<!-- Complaint Details --> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
{% if complaint %} <div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm transition-hover hover:shadow-md">
<div class="bg-white rounded-3xl shadow-xl p-8 md:p-12"> <i data-lucide="calendar" class="w-5 h-5 text-blue mb-3"></i>
<!-- Header --> <span class="block text-xs font-bold text-slate/50 uppercase">{% trans "Submitted" %}</span>
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6 pb-6 border-b border-gray-200"> <p class="font-bold text-navy">{{ complaint.created_at|date:"M d, Y" }}</p>
<div class="text-center md:text-left"> </div>
<h2 class="text-2xl font-bold text-gray-800 flex items-center gap-2 md:justify-start justify-center"> <div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm transition-hover hover:shadow-md">
<i data-lucide="hash" class="w-6 h-6 text-gray-400"></i> <i data-lucide="building" class="w-5 h-5 text-blue mb-3"></i>
{{ complaint.reference_number }} <span class="block text-xs font-bold text-slate/50 uppercase">{% trans "Department" %}</span>
</h2> <p class="font-bold text-navy truncate">{{ complaint.department.name|default:"General" }}</p>
</div> </div>
<div class="text-center md:text-right"> <div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm transition-hover hover:shadow-md relative overflow-hidden">
<span class="px-4 py-2 rounded-full text-sm font-bold uppercase tracking-wide <i data-lucide="clock" class="w-5 h-5 {% if complaint.is_overdue %}text-rose-500{% else %}text-blue{% endif %} mb-3"></i>
{% if complaint.status == 'open' %}bg-yellow-100 text-yellow-700 <span class="block text-xs font-bold text-slate/50 uppercase">{% trans "SLA Deadline" %}</span>
{% elif complaint.status == 'in_progress' %}bg-blue-100 text-blue-700 <p class="font-bold text-navy">{{ complaint.due_at|date:"M d, H:i" }}</p>
{% elif complaint.status == 'partially_resolved' %}bg-orange-100 text-orange-700 {% if complaint.is_overdue %}
{% elif complaint.status == 'resolved' %}bg-green-100 text-green-700 <span class="absolute top-2 right-2 flex h-2 w-2">
{% elif complaint.status == 'closed' %}bg-gray-100 text-gray-700 <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
{% elif complaint.status == 'cancelled' %}bg-red-100 text-red-700 <span class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
{% endif %}">
{{ complaint.get_status_display }}
</span> </span>
</div>
</div>
<!-- SLA Information -->
<div class="{% if complaint.is_overdue %}bg-red-50 border-l-4 border-red-500{% else %}bg-blue-50 border-l-4 border-blue-500{% endif %} rounded-xl p-6 mb-6">
<div class="flex items-center gap-4">
<div class="{% if complaint.is_overdue %}text-red-500{% else %}text-blue-500{% endif %} flex-shrink-0">
<i data-lucide="{% if complaint.is_overdue %}alert-circle{% else %}clock{% endif %}" class="w-8 h-8"></i>
</div>
<div class="flex-1">
<h4 class="font-bold text-gray-800 mb-1">
{% if complaint.is_overdue %}
{% trans "Response Overdue" %}
{% else %}
{% trans "Expected Response Time" %}
{% endif %}
</h4>
<p class="text-gray-600">
<strong>{% trans "Due:" %}</strong>
{{ complaint.due_at|date:"F j, Y" }} at {{ complaint.due_at|time:"g:i A" }}
</p>
</div>
</div>
</div>
<!-- Information Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
<div class="bg-gray-50 rounded-xl p-4">
<div class="flex items-center gap-2 text-gray-500 text-sm mb-1">
<i data-lucide="calendar" class="w-4 h-4"></i>
{% trans "Submitted On" %}
</div>
<div class="font-semibold text-gray-800">
{{ complaint.created_at|date:"F j, Y" }}
</div>
</div>
<div class="bg-gray-50 rounded-xl p-4">
<div class="flex items-center gap-2 text-gray-500 text-sm mb-1">
<i data-lucide="building" class="w-4 h-4"></i>
{% trans "Hospital" %}
</div>
<div class="font-semibold text-gray-800">
{{ complaint.hospital.name }}
</div>
</div>
{% if complaint.department %}
<div class="bg-gray-50 rounded-xl p-4">
<div class="flex items-center gap-2 text-gray-500 text-sm mb-1">
<i data-lucide="building-2" class="w-4 h-4"></i>
{% trans "Department" %}
</div>
<div class="font-semibold text-gray-800">
{{ complaint.department.name }}
</div>
</div>
{% endif %} {% endif %}
<div class="bg-gray-50 rounded-xl p-4">
<div class="flex items-center gap-2 text-gray-500 text-sm mb-1">
<i data-lucide="list" class="w-4 h-4"></i>
{% trans "Category" %}
</div>
<div class="font-semibold text-gray-800">
{% if complaint.category %}
{{ complaint.category.name_en }}
{% else %}
{% trans "Pending Classification" %}
{% endif %}
</div>
</div>
</div> </div>
</div>
<div class="bg-white rounded-3xl shadow-lg border border-slate-100 p-8 md:p-10">
<h3 class="text-2xl font-bold text-navy mb-10 flex items-center gap-3">
<div class="p-2 bg-navy text-white rounded-lg">
<i data-lucide="list-checks" class="w-5 h-5"></i>
</div>
{% trans "Resolution Journey" %}
</h3>
<!-- Timeline -->
{% if public_updates %} {% if public_updates %}
<div class="mt-8"> <div class="space-y-1">
<h3 class="text-xl font-bold text-gray-800 mb-6 flex items-center gap-2"> {% for update in public_updates %}
<i data-lucide="history" class="w-6 h-6 text-blue-500"></i> <div class="timeline-item flex gap-6 pb-10 relative">
{% trans "Complaint Timeline" %} <div class="timeline-dot shrink-0 relative z-10">
</h3> <div class="w-12 h-12 rounded-2xl flex items-center justify-center shadow-sm border-2 border-white
<div class="relative pl-8"> {% if update.update_type == 'status_change' %}bg-amber-100 text-amber-600
<!-- Timeline Line --> {% elif update.update_type == 'resolution' %}bg-emerald-100 text-emerald-600
<div class="absolute left-3 top-0 bottom-0 w-0.5 bg-gray-200"></div> {% else %}bg-blue-50 text-blue-600{% endif %}">
<i data-lucide="{% if update.update_type == 'status_change' %}refresh-cw{% elif update.update_type == 'resolution' %}check-circle-2{% else %}message-square{% endif %}" class="w-6 h-6"></i>
{% for update in public_updates %}
<div class="relative pb-6 last:pb-0">
<!-- Timeline Dot -->
<div class="absolute left-[-1.3rem] top-1 w-4 h-4 rounded-full border-2 border-white
{% if update.update_type == 'status_change' %}bg-orange-500 shadow-[0_0_0_2px_#f97316]
{% elif update.update_type == 'resolution' %}bg-green-500 shadow-[0_0_0_2px_#22c55e]
{% else %}bg-blue-500 shadow-[0_0_0_2px_#3b82f6]{% endif %}">
</div> </div>
</div>
<div class="text-sm text-gray-500 mb-1"> <div class="flex-1 pt-1">
{{ update.created_at|date:"F j, Y" }} at {{ update.created_at|time:"g:i A" }} <div class="flex flex-col md:flex-row md:items-center justify-between mb-2">
</div> <h4 class="font-black text-navy text-lg">
<div class="font-semibold text-gray-800 mb-2"> {% if update.update_type == 'status_change' %}{% trans "Status Updated" %}
{% if update.update_type == 'status_change' %} {% elif update.update_type == 'resolution' %}{% trans "Final Resolution" %}
{% trans "Status Updated" %} {% else %}{% trans "Update Received" %}{% endif %}
{% elif update.update_type == 'resolution' %} </h4>
{% trans "Resolution Added" %} <time class="text-sm font-medium text-slate/40">{{ update.created_at|date:"F j, Y • g:i A" }}</time>
{% elif update.update_type == 'communication' %}
{% trans "Update" %}
{% else %}
{{ update.get_update_type_display }}
{% endif %}
</div> </div>
{% if update.comments %} {% if update.comments %}
<div class="text-gray-600 leading-relaxed"> <div class="bg-slate-50/50 rounded-2xl p-5 border border-slate-100 text-slate-700 leading-relaxed shadow-inner">
{{ update.comments|linebreaks }} {{ update.comments|linebreaks }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endfor %}
</div> </div>
{% endfor %}
</div> </div>
{% else %} {% else %}
<div class="text-center py-8 px-6 bg-gray-50 rounded-xl"> <div class="text-center py-12">
<i data-lucide="info" class="w-8 h-8 text-gray-400 mx-auto mb-2"></i> <div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<p class="text-gray-500"> <i data-lucide="loader" class="w-8 h-8 text-slate/30 animate-spin"></i>
{% trans "No updates available yet. You will be notified when there is progress on your complaint." %} </div>
</p> <p class="text-slate/60 font-medium">{% trans "Your complaint is being reviewed. Updates will appear here." %}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,219 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% trans "Complaint Templates" %} - PX360{% endblock %}
{% block extra_css %}
<style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
}
.page-header {
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
color: white;
padding: 2rem 2.5rem;
border-radius: 1rem;
margin-bottom: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.template-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.template-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.1);
border-color: var(--hh-blue);
}
.usage-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
color: #1e40af;
}
.btn-primary {
background: linear-gradient(135deg, var(--hh-navy) 0%, var(--hh-blue) 100%);
color: white;
padding: 0.625rem 1.25rem;
border-radius: 0.75rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 86, 150, 0.3);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
}
</style>
{% endblock %}
{% block content %}
<div class="px-4 py-6">
<!-- Page Header -->
<div class="page-header animate-in">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold mb-2">
<i data-lucide="file-template" class="w-7 h-7 inline-block me-2"></i>
{% trans "Complaint Templates" %}
</h1>
<p class="text-white/90">{% trans "Pre-defined templates for common complaints" %}</p>
</div>
<a href="{% url 'complaints:template_create' %}" class="inline-flex items-center gap-2 bg-white text-navy px-5 py-2.5 rounded-xl font-bold hover:bg-light transition shadow-lg">
<i data-lucide="plus" class="w-4 h-4"></i>
{% trans "New Template" %}
</a>
</div>
</div>
<!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 animate-in">
<form method="get" class="flex flex-wrap gap-4">
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if hospital_filter == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
<select name="is_active" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<option value="">{% trans "All Status" %}</option>
<option value="true" {% if is_active_filter == 'true' %}selected{% endif %}>{% trans "Active" %}</option>
<option value="false" {% if is_active_filter == 'false' %}selected{% endif %}>{% trans "Inactive" %}</option>
</select>
</div>
<div class="flex-1 min-w-[250px]">
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Search" %}</label>
<input type="text" name="search" value="{{ search }}"
placeholder="{% trans 'Template name...' %}"
class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue">
</div>
<div class="flex items-end">
<button type="submit" class="btn-primary h-[46px]">
<i data-lucide="search" class="w-4 h-4"></i>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<!-- Templates Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-in">
{% for template in templates %}
<div class="template-card" onclick="window.location='{% url 'complaints:template_detail' template.pk %}'">
<div class="flex items-start justify-between mb-4">
<div class="w-12 h-12 bg-blue/10 rounded-xl flex items-center justify-center">
<i data-lucide="file-text" class="w-6 h-6 text-blue-600"></i>
</div>
{% if template.is_active %}
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-100 text-green-700 rounded-full text-xs font-bold">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
{% trans "Active" %}
</span>
{% else %}
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 text-slate-600 rounded-full text-xs font-bold">
<i data-lucide="x-circle" class="w-3.5 h-3.5"></i>
{% trans "Inactive" %}
</span>
{% endif %}
</div>
<h3 class="text-lg font-bold text-navy mb-2">{{ template.name }}</h3>
<p class="text-sm text-slate mb-4 line-clamp-2">{{ template.description|truncatewords:15 }}</p>
<div class="flex items-center gap-3 mb-4">
<div class="usage-badge">
<i data-lucide="trending-up" class="w-3.5 h-3.5"></i>
{{ template.usage_count }} {% trans "uses" %}
</div>
{% if template.category %}
<span class="text-xs text-slate font-medium">{{ template.category.name_en }}</span>
{% endif %}
</div>
<div class="flex items-center justify-between pt-4 border-t border-slate-100">
<div class="text-xs text-slate">
<i data-lucide="building" class="w-3 h-3 inline-block mr-1"></i>
{{ template.hospital.name }}
</div>
<div class="flex items-center gap-2">
<a href="{% url 'complaints:template_edit' template.pk %}"
class="p-2 text-blue hover:bg-blue-50 rounded-lg transition"
title="{% trans 'Edit' %}"
onclick="event.stopPropagation();">
<i data-lucide="edit" class="w-4 h-4"></i>
</a>
{% if user.is_px_admin %}
<a href="{% url 'complaints:template_delete' template.pk %}"
class="p-2 text-red-500 hover:bg-red-50 rounded-lg transition"
title="{% trans 'Delete' %}"
onclick="event.stopPropagation();">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</a>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="col-span-full">
<div class="text-center py-12">
<div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="file-template" class="w-8 h-8 text-slate-400"></i>
</div>
<p class="text-slate font-medium">{% trans "No templates found" %}</p>
<p class="text-slate text-sm mt-1">{% trans "Create your first template to get started" %}</p>
<a href="{% url 'complaints:template_create' %}" class="inline-flex items-center gap-2 mt-4 btn-primary">
<i data-lucide="plus" class="w-4 h-4"></i>
{% trans "Create Template" %}
</a>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -58,5 +58,31 @@
{% trans "Manage Hospitals" %} {% trans "Manage Hospitals" %}
</a> </a>
</div> </div>
<!-- On-Call Admin Schedules -->
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 text-center card-hover">
<div class="w-16 h-16 mx-auto mb-4 bg-purple-100 rounded-2xl flex items-center justify-center">
<i data-lucide="moon" class="w-8 h-8 text-purple-600"></i>
</div>
<h3 class="font-bold text-navy text-lg mb-2">{% trans "On-Call Schedules" %}</h3>
<p class="text-sm text-slate mb-4">{{ oncall_schedules_count }} {% trans "active schedules" %}</p>
<a href="{% url 'complaints:oncall_dashboard' %}" class="inline-flex items-center gap-2 bg-navy text-white px-5 py-2.5 rounded-xl text-sm font-bold shadow-lg shadow-navy/20 hover:bg-blue transition">
<i data-lucide="users" class="w-4 h-4"></i>
{% trans "Manage On-Call" %}
</a>
</div>
<!-- Call Records -->
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 text-center card-hover">
<div class="w-16 h-16 mx-auto mb-4 bg-pink-100 rounded-2xl flex items-center justify-center">
<i data-lucide="phone-call" class="w-8 h-8 text-pink-600"></i>
</div>
<h3 class="font-bold text-navy text-lg mb-2">{% trans "Call Records" %}</h3>
<p class="text-sm text-slate mb-4">{{ call_records_count }} {% trans "records" %}</p>
<a href="{% url 'callcenter:call_records_list' %}" class="inline-flex items-center gap-2 bg-navy text-white px-5 py-2.5 rounded-xl text-sm font-bold shadow-lg shadow-navy/20 hover:bg-blue transition">
<i data-lucide="headphones" class="w-4 h-4"></i>
{% trans "Manage Call Records" %}
</a>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -288,14 +288,14 @@
</div> </div>
<!-- Form Content --> <!-- Form Content -->
<div id="formContent" class="max-h-[600px] overflow-y-auto pr-2"></div> <div id="formContent" class="pr-2"></div>
</div> </div>
<!-- Track Submission Links --> <!-- Track Submission Links -->
<div class="glass-card rounded-2xl shadow-xl p-8 text-center"> <div class="glass-card rounded-2xl shadow-xl p-8 text-center">
<p class="text-slate mb-4 font-medium">{% trans "Already submitted something?" %}</p> <p class="text-slate mb-4 font-medium">{% trans "Already submitted something?" %}</p>
<div class="flex justify-center gap-3 flex-wrap"> <div class="flex justify-center gap-3 flex-wrap">
<a href="{% url 'complaints:public_complaint_success' 'lookup' %}" <a href="{% url 'complaints:public_complaint_track' %}"
class="inline-flex items-center gap-2 px-6 py-3 bg-navy text-white rounded-xl hover:bg-blue transition font-semibold shadow-lg shadow-navy/30 btn-hover"> class="inline-flex items-center gap-2 px-6 py-3 bg-navy text-white rounded-xl hover:bg-blue transition font-semibold shadow-lg shadow-navy/30 btn-hover">
<i data-lucide="search" class="w-5 h-5"></i> <i data-lucide="search" class="w-5 h-5"></i>
{% trans "Track Complaint" %} {% trans "Track Complaint" %}

View File

@ -1,14 +1,14 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load i18n %} {% load i18n static %}
{% block title %}{% trans "Select Hospital" %} - PX360{% endblock %} {% block title %}{% trans "Select Hospital" %} - PX360{% endblock %}
{% block content %} {% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header --> <!-- Header -->
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue-500 to-navy rounded-2xl mb-4 shadow-lg"> <div class="mb-4">
<i data-lucide="building-2" class="w-10 h-10 text-white"></i> <img src="{% static 'img/hh-logo.png' %}" alt="Al Hammadi Hospital" class="h-20 mx-auto">
</div> </div>
<h1 class="text-3xl font-bold text-navy mb-3"> <h1 class="text-3xl font-bold text-navy mb-3">
{% trans "Select Hospital" %} {% trans "Select Hospital" %}
@ -19,89 +19,142 @@
</div> </div>
<!-- Hospital Selection Form --> <!-- Hospital Selection Form -->
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden"> <form method="post" class="space-y-6" id="hospitalForm">
<form method="post"> {% csrf_token %}
{% csrf_token %} <input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="next" value="{{ next }}">
<div class="divide-y divide-slate-100"> {% if hospitals %}
{% for hospital in hospitals %} <!-- Hospital Cards Grid -->
<div class="block cursor-pointer hover:bg-light/30 transition group relative"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<input type="radio" {% for hospital in hospitals %}
id="hospital_{{ hospital.id }}" {% with hospital_id_str=hospital.id|stringformat:"s" %}
name="hospital_id" <div
value="{{ hospital.id }}" class="hospital-card group relative cursor-pointer rounded-2xl border-2 transition-all duration-200
{% if hospital.id == selected_hospital_id %}checked{% endif %} {% if hospital_id_str == selected_hospital_id %}
class="peer" border-blue bg-blue-50/50 shadow-md selected
style="position: absolute; opacity: 0; width: 0; height: 0;"> {% else %}
border-slate-200 bg-white hover:border-blue/50 hover:-translate-y-1
<label for="hospital_{{ hospital.id }}" class="block p-6"> {% endif %}"
<div class="flex items-start gap-4"> onclick="selectHospital('{{ hospital_id_str }}')"
<!-- Radio Button --> data-hospital-id="{{ hospital_id_str }}"
<div class="flex-shrink-0 mt-1"> >
<div class="w-5 h-5 rounded-full border-2 border-slate-300 peer-checked:border-blue peer-checked:bg-blue flex items-center justify-center transition-all"> <input
<div class="w-2.5 h-2.5 bg-white rounded-full opacity-0 peer-checked:opacity-100 transition-opacity"></div> type="radio"
</div> id="hospital_{{ hospital.id }}"
</div> name="hospital_id"
value="{{ hospital.id }}"
<!-- Hospital Info --> {% if hospital_id_str == selected_hospital_id %}checked{% endif %}
<div class="flex-1 min-w-0"> class="hospital-radio sr-only"
<div class="flex items-start justify-between gap-4"> >
<div class="flex-1">
<h3 class="text-lg font-bold text-navy mb-1 group-hover:text-blue transition"> <!-- Card Content -->
{{ hospital.name }} <div class="p-6">
</h3> <!-- Selection Indicator -->
{% if hospital.city %} <div class="absolute top-4 right-4">
<p class="text-slate text-sm flex items-center gap-1.5"> <div class="selection-indicator w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all
<i data-lucide="map-pin" class="w-3.5 h-3.5"></i> {% if hospital_id_str == selected_hospital_id %}
{{ hospital.city }} border-blue bg-blue
{% if hospital.country %}, {{ hospital.country }}{% endif %} {% else %}
</p> border-slate-300
{% endif %} {% endif %}">
</div> <i data-lucide="check" class="w-3.5 h-3.5 text-white transition-opacity
{% if hospital_id_str == selected_hospital_id %}
<!-- Selected Badge --> opacity-100
{% if hospital.id == selected_hospital_id %} {% else %}
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-bold bg-green-100 text-green-800 flex-shrink-0"> opacity-0
<i data-lucide="check-circle-2" class="w-3.5 h-3.5 mr-1.5"></i> {% endif %}"></i>
{% trans "Selected" %}
</span>
{% endif %}
</div>
</div>
</div> </div>
</label>
</div>
{% empty %}
<div class="p-12 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 bg-amber-50 rounded-full mb-4">
<i data-lucide="alert-triangle" class="w-8 h-8 text-amber-500"></i>
</div> </div>
<h3 class="text-lg font-semibold text-navy mb-2">
{% trans "No Hospitals Available" %}
</h3>
<p class="text-slate text-sm">
{% trans "No hospitals found in the system. Please contact your administrator." %}
</p>
</div>
{% endfor %}
</div>
<!-- Action Buttons --> <!-- Hospital Icon -->
<div class="p-6 bg-slate-50 border-t border-slate-200"> <div class="hospital-icon w-14 h-14 rounded-xl flex items-center justify-center mb-4 transition-all
<div class="flex flex-col sm:flex-row justify-between items-center gap-4"> {% if hospital_id_str == selected_hospital_id %}
<a href="/" class="w-full sm:w-auto px-6 py-3 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-white transition flex items-center justify-center gap-2"> bg-gradient-to-br from-blue to-navy ring-4 ring-blue/20
<i data-lucide="arrow-left" class="w-5 h-5"></i> {% else %}
{% trans "Back to Dashboard" %} bg-slate-100 group-hover:bg-blue-50
</a> {% endif %}">
<button type="submit" class="w-full sm:w-auto px-8 py-3 bg-gradient-to-r from-blue to-navy text-white rounded-xl font-semibold hover:from-navy hover:to-blue transition flex items-center justify-center gap-2 shadow-lg shadow-blue/20"> <i data-lucide="building-2" class="w-7 h-7 transition-colors
<i data-lucide="check" class="w-5 h-5"></i> {% if hospital_id_str == selected_hospital_id %}
{% trans "Continue" %} text-white
</button> {% else %}
text-slate-500 group-hover:text-blue
{% endif %}"></i>
</div>
<!-- Hospital Name -->
<h3 class="hospital-name text-lg font-bold mb-2 transition-colors pr-8
{% if hospital_id_str == selected_hospital_id %}
text-blue
{% else %}
text-navy group-hover:text-blue
{% endif %}">
{{ hospital.name }}
</h3>
<!-- Location -->
{% if hospital.city %}
<div class="flex items-center gap-2 text-slate text-sm">
<i data-lucide="map-pin" class="w-4 h-4 text-slate/70"></i>
<span>
{{ hospital.city }}
{% if hospital.country %}, {{ hospital.country }}{% endif %}
</span>
</div>
{% else %}
<div class="flex items-center gap-2 text-slate/50 text-sm">
<i data-lucide="map-pin" class="w-4 h-4"></i>
<span>{% trans "Location not specified" %}</span>
</div>
{% endif %}
<!-- Selected Badge -->
<div class="selected-badge mt-4 pt-4 border-t transition-all
{% if hospital_id_str == selected_hospital_id %}
block border-blue/20
{% else %}
hidden border-slate-100
{% endif %}">
<span class="inline-flex items-center gap-1.5 text-sm font-semibold text-blue">
<i data-lucide="check-circle-2" class="w-4 h-4"></i>
{% trans "Currently Selected" %}
</span>
</div>
</div> </div>
</div> </div>
</form> {% endwith %}
</div> {% endfor %}
</div>
{% else %}
<!-- Empty State -->
<div class="bg-white rounded-2xl border border-slate-200 p-12 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 bg-amber-50 rounded-full mb-4">
<i data-lucide="alert-triangle" class="w-8 h-8 text-amber-500"></i>
</div>
<h3 class="text-lg font-semibold text-navy mb-2">
{% trans "No Hospitals Available" %}
</h3>
<p class="text-slate text-sm">
{% trans "No hospitals found in the system. Please contact your administrator." %}
</p>
</div>
{% endif %}
<!-- Action Buttons -->
{% if hospitals %}
<div class="bg-white rounded-2xl border border-slate-200 p-6 mt-8">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<a href="/" class="w-full sm:w-auto px-6 py-3 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition flex items-center justify-center gap-2">
<i data-lucide="arrow-left" class="w-5 h-5"></i>
{% trans "Back to Dashboard" %}
</a>
<button type="submit" class="w-full sm:w-auto px-8 py-3 bg-gradient-to-r from-blue to-navy text-white rounded-xl font-semibold hover:from-navy hover:to-blue transition flex items-center justify-center gap-2 shadow-lg shadow-blue/20">
<i data-lucide="check" class="w-5 h-5"></i>
{% trans "Continue" %}
</button>
</div>
</div>
{% endif %}
</form>
<!-- Info Banner --> <!-- Info Banner -->
<div class="bg-blue-50 border border-blue-200 rounded-2xl p-4 mt-6"> <div class="bg-blue-50 border border-blue-200 rounded-2xl p-4 mt-6">
@ -123,5 +176,103 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons(); lucide.createIcons();
}); });
function selectHospital(hospitalId) {
// Check the radio button
const radio = document.getElementById('hospital_' + hospitalId);
if (radio) {
radio.checked = true;
}
// Update all cards visual state
document.querySelectorAll('.hospital-card').forEach(card => {
const cardHospitalId = card.dataset.hospitalId;
const isSelected = cardHospitalId === hospitalId;
// Update card border and background
if (isSelected) {
card.classList.remove('border-slate-200', 'bg-white');
card.classList.add('border-blue', 'bg-blue-50/50', 'shadow-md', 'selected');
} else {
card.classList.remove('border-blue', 'bg-blue-50/50', 'shadow-md', 'selected');
card.classList.add('border-slate-200', 'bg-white');
}
// Update selection indicator (check circle)
const indicator = card.querySelector('.selection-indicator');
if (indicator) {
if (isSelected) {
indicator.classList.remove('border-slate-300');
indicator.classList.add('border-blue', 'bg-blue');
} else {
indicator.classList.remove('border-blue', 'bg-blue');
indicator.classList.add('border-slate-300');
}
}
// Update check icon
const checkIcon = card.querySelector('.selection-indicator i');
if (checkIcon) {
if (isSelected) {
checkIcon.classList.remove('opacity-0');
checkIcon.classList.add('opacity-100');
} else {
checkIcon.classList.remove('opacity-100');
checkIcon.classList.add('opacity-0');
}
}
// Update hospital icon
const iconContainer = card.querySelector('.hospital-icon');
if (iconContainer) {
if (isSelected) {
iconContainer.classList.remove('bg-slate-100');
iconContainer.classList.add('bg-gradient-to-br', 'from-blue', 'to-navy', 'ring-4', 'ring-blue/20');
} else {
iconContainer.classList.remove('bg-gradient-to-br', 'from-blue', 'to-navy', 'ring-4', 'ring-blue/20');
iconContainer.classList.add('bg-slate-100');
}
}
// Update icon color
const icon = card.querySelector('.hospital-icon i');
if (icon) {
if (isSelected) {
icon.classList.remove('text-slate-500');
icon.classList.add('text-white');
} else {
icon.classList.remove('text-white');
icon.classList.add('text-slate-500');
}
}
// Update hospital name color
const name = card.querySelector('.hospital-name');
if (name) {
if (isSelected) {
name.classList.remove('text-navy');
name.classList.add('text-blue');
} else {
name.classList.remove('text-blue');
name.classList.add('text-navy');
}
}
// Update selected badge
const badge = card.querySelector('.selected-badge');
if (badge) {
if (isSelected) {
badge.classList.remove('hidden');
badge.classList.add('block', 'border-blue/20');
} else {
badge.classList.remove('block', 'border-blue/20');
badge.classList.add('hidden');
}
}
});
// Re-render icons
lucide.createIcons();
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -7,16 +7,23 @@
{% block content %} {% block content %}
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Page Header -->
<div class="flex justify-between items-start"> <header class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-bold mb-2">{% trans "Admin Evaluation Dashboard" %}</h1> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<p class="text-gray-500">{% trans "Staff performance analysis for complaints and inquiries" %}</p> <i data-lucide="shield-check" class="w-7 h-7"></i>
{% trans "Admin Evaluation Dashboard" %}
</h1>
<p class="text-sm text-slate mt-1">{% trans "Staff performance analysis for complaints and inquiries" %}</p>
</div> </div>
</div> <div class="text-right">
<p class="text-xs text-slate uppercase tracking-wider">{% trans "Last Updated" %}</p>
<p class="text-sm font-bold text-navy">{% now "j M Y, H:i" %}</p>
</div>
</header>
<!-- Filters --> <!-- Filters -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6"> <div class="card">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Date Range --> <!-- Date Range -->
<div> <div>
@ -75,99 +82,134 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<a href="{% url 'dashboard:department_benchmarks' %}?date_range={{ date_range }}" class="inline-flex items-center gap-2 px-4 py-2.5 border-2 border-navy text-navy rounded-xl font-semibold hover:bg-light transition"> <a href="{% url 'dashboard:department_benchmarks' %}?date_range={{ date_range }}" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-navy text-navy rounded-lg font-semibold hover:bg-light transition">
<i data-lucide="bar-chart-2" class="w-4 h-4"></i> <i data-lucide="bar-chart-2" class="w-4 h-4"></i>
{% trans "Department Benchmarks" %} {% trans "Department Benchmarks" %}
</a> </a>
<button onclick="exportReport('csv')" class="inline-flex items-center gap-2 px-4 py-2.5 border-2 border-green-500 text-green-500 rounded-xl font-semibold hover:bg-green-50 transition"> <button onclick="exportReport('csv')" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-green-500 text-green-500 rounded-lg font-semibold hover:bg-green-50 transition">
<i data-lucide="download" class="w-4 h-4"></i> <i data-lucide="download" class="w-4 h-4"></i>
{% trans "Export CSV" %} {% trans "Export CSV" %}
</button> </button>
<button onclick="exportReport('json')" class="inline-flex items-center gap-2 px-4 py-2.5 border-2 border-blue-500 text-blue-500 rounded-xl font-semibold hover:bg-blue-50 transition"> <button onclick="exportReport('json')" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-blue-500 text-blue-500 rounded-lg font-semibold hover:bg-blue-50 transition">
<i data-lucide="file-code" class="w-4 h-4"></i> <i data-lucide="file-code" class="w-4 h-4"></i>
{% trans "Export JSON" %} {% trans "Export JSON" %}
</button> </button>
</div> </div>
<!-- Summary Cards --> <!-- Summary Cards -->
<div id="summaryCards" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div id="summaryCards" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{% if performance_data.staff_metrics %} {% if performance_data.staff_metrics %}
<div class="bg-gradient-to-br from-navy to-navy rounded-2xl p-6 text-white shadow-lg shadow-blue-200"> <div class="card stat-card">
<div class="flex items-center gap-3 mb-4"> <div class="flex items-start justify-between">
<div class="bg-white/20 p-3 rounded-xl"> <div>
<i data-lucide="users" class="w-6 h-6"></i> <p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Staff" %}</p>
<p class="text-3xl font-bold text-navy">{{ performance_data.staff_metrics|length }}</p>
<div class="flex items-center gap-1.5 mt-2">
<i data-lucide="building" class="w-4 h-4 text-blue"></i>
<span class="text-sm text-slate">{% trans "Active Staff" %}</span>
</div>
</div>
<div class="p-3 bg-blue-50 rounded-xl">
<i data-lucide="users" class="w-6 h-6 text-blue"></i>
</div> </div>
<h3 class="text-sm font-medium opacity-90">{% trans "Total Staff" %}</h3>
</div> </div>
<div class="text-4xl font-bold">{{ performance_data.staff_metrics|length }}</div>
</div> </div>
<div class="bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl p-6 text-white shadow-lg shadow-indigo-200">
<div class="flex items-center gap-3 mb-4"> <div class="card stat-card">
<div class="bg-white/20 p-3 rounded-xl"> <div class="flex items-start justify-between">
<i data-lucide="alert-triangle" class="w-6 h-6"></i> <div>
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Complaints" %}</p>
<p class="text-3xl font-bold text-navy" id="totalComplaints">0</p>
<div class="flex items-center gap-1.5 mt-2">
<i data-lucide="trending-up" class="w-4 h-4 text-red-500"></i>
<span class="text-xs text-slate">{% trans "Requires Attention" %}</span>
</div>
</div>
<div class="p-3 bg-red-50 rounded-xl">
<i data-lucide="alert-triangle" class="w-6 h-6 text-red-500"></i>
</div> </div>
<h3 class="text-sm font-medium opacity-90">{% trans "Total Complaints" %}</h3>
</div> </div>
<div class="text-4xl font-bold" id="totalComplaints">0</div>
</div> </div>
<div class="bg-gradient-to-br from-orange-500 to-orange-600 rounded-2xl p-6 text-white shadow-lg shadow-orange-200">
<div class="flex items-center gap-3 mb-4"> <div class="card stat-card">
<div class="bg-white/20 p-3 rounded-xl"> <div class="flex items-start justify-between">
<i data-lucide="message-circle" class="w-6 h-6"></i> <div>
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Inquiries" %}</p>
<p class="text-3xl font-bold text-navy" id="totalInquiries">0</p>
<div class="flex items-center gap-1.5 mt-2">
<i data-lucide="message-circle" class="w-4 h-4 text-blue"></i>
<span class="text-xs text-slate">{% trans "Open Requests" %}</span>
</div>
</div>
<div class="p-3 bg-blue-50 rounded-xl">
<i data-lucide="message-circle" class="w-6 h-6 text-blue"></i>
</div> </div>
<h3 class="text-sm font-medium opacity-90">{% trans "Total Inquiries" %}</h3>
</div> </div>
<div class="text-4xl font-bold" id="totalInquiries">0</div>
</div> </div>
<div class="bg-gradient-to-br from-emerald-500 to-emerald-600 rounded-2xl p-6 text-white shadow-lg shadow-emerald-200">
<div class="flex items-center gap-3 mb-4"> <div class="card stat-card">
<div class="bg-white/20 p-3 rounded-xl"> <div class="flex items-start justify-between">
<i data-lucide="check-circle" class="w-6 h-6"></i> <div>
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Resolution Rate" %}</p>
<p class="text-3xl font-bold text-navy" id="resolutionRate">0%</p>
<div class="flex items-center gap-1.5 mt-2">
<i data-lucide="trending-up" class="w-4 h-4 text-green-500"></i>
<span class="text-sm font-bold text-green-500">{% trans "Performance" %}</span>
</div>
</div>
<div class="p-3 bg-green-50 rounded-xl">
<i data-lucide="check-circle" class="w-6 h-6 text-green-500"></i>
</div> </div>
<h3 class="text-sm font-medium opacity-90">{% trans "Resolution Rate" %}</h3>
</div> </div>
<div class="text-4xl font-bold" id="resolutionRate">0%</div>
</div> </div>
{% else %} {% else %}
<div class="col-span-full bg-blue-50 border border-blue-200 rounded-2xl p-6 text-center"> <div class="col-span-full card">
<i data-lucide="info" class="w-6 h-6 text-blue-500 mx-auto mb-2"></i> <div class="flex flex-col items-center justify-center py-8 text-center">
<p class="text-blue-700">{% trans "No staff members with assigned complaints or inquiries found in the selected time period." %}</p> <div class="bg-blue-50 w-16 h-16 rounded-full flex items-center justify-center mb-4">
<i data-lucide="info" class="w-8 h-8 text-blue"></i>
</div>
<p class="text-sm text-slate">{% trans "No staff members with assigned complaints or inquiries found in the selected time period." %}</p>
</div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Tabs --> <!-- Tabs -->
{% if performance_data.staff_metrics %} {% if performance_data.staff_metrics %}
<div class="bg-white rounded-2xl shadow-sm border border-gray-50"> <div class="card">
<div class="border-b border-gray-100"> <div class="border-b border-slate-100">
<nav class="flex gap-1 p-2" id="evaluationTabs"> <nav class="flex gap-1" id="evaluationTabs">
<button class="px-6 py-3 rounded-xl font-semibold bg-light0 text-white" data-tab="complaints" id="complaints-tab"> <button class="px-6 py-3 rounded-lg font-semibold bg-navy text-white" data-tab="complaints" id="complaints-tab">
{% trans "Complaints" %} {% trans "Complaints" %}
</button> </button>
<button class="px-6 py-3 rounded-xl font-semibold text-gray-500 hover:bg-gray-100 transition" data-tab="inquiries" id="inquiries-tab"> <button class="px-6 py-3 rounded-lg font-semibold text-slate hover:text-navy hover:bg-light transition" data-tab="inquiries" id="inquiries-tab">
{% trans "Inquiries" %} {% trans "Inquiries" %}
</button> </button>
</nav> </nav>
</div> </div>
<!-- Complaints Tab Content --> <!-- Complaints Tab Content -->
<div id="complaints-content" class="tab-content p-6"> <div id="complaints-content" class="tab-content">
<!-- Charts Row 1 --> <!-- Charts Row 1 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="border border-gray-100 rounded-2xl p-6"> <div class="card">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2"> <div class="card-header">
<i data-lucide="pie-chart" class="w-5 h-5 text-navy"></i> <h3 class="card-title flex items-center gap-2">
{% trans "Complaint Source Breakdown" %} <i data-lucide="pie-chart" class="w-4 h-4"></i>
</h3> {% trans "Complaint Source Breakdown" %}
</h3>
</div>
<div class="h-[320px]"> <div class="h-[320px]">
<canvas id="complaintSourceChart"></canvas> <canvas id="complaintSourceChart"></canvas>
</div> </div>
</div> </div>
<div class="border border-gray-100 rounded-2xl p-6"> <div class="card">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2"> <div class="card-header">
<i data-lucide="bar-chart-2" class="w-5 h-5 text-navy"></i> <h3 class="card-title flex items-center gap-2">
{% trans "Complaint Status Distribution" %} <i data-lucide="bar-chart-2" class="w-4 h-4"></i>
</h3> {% trans "Complaint Status Distribution" %}
</h3>
</div>
<div class="h-[320px]"> <div class="h-[320px]">
<canvas id="complaintStatusChart"></canvas> <canvas id="complaintStatusChart"></canvas>
</div> </div>
@ -176,22 +218,26 @@
<!-- Charts Row 2 --> <!-- Charts Row 2 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="border border-gray-100 rounded-2xl p-6"> <div class="card">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2"> <div class="card-header">
<i data-lucide="clock" class="w-5 h-5 text-navy"></i> <h3 class="card-title flex items-center gap-2">
{% trans "Complaint Activation Time" %} <i data-lucide="clock" class="w-4 h-4"></i>
</h3> {% trans "Complaint Activation Time" %}
<p class="text-sm text-gray-500 mb-4">{% trans "Time from creation to assignment" %}</p> </h3>
<p class="text-sm text-slate">{% trans "Time from creation to assignment" %}</p>
</div>
<div class="h-[320px]"> <div class="h-[320px]">
<canvas id="complaintActivationChart"></canvas> <canvas id="complaintActivationChart"></canvas>
</div> </div>
</div> </div>
<div class="border border-gray-100 rounded-2xl p-6"> <div class="card">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2"> <div class="card-header">
<i data-lucide="gauge" class="w-5 h-5 text-navy"></i> <h3 class="card-title flex items-center gap-2">
{% trans "Complaint Response Time" %} <i data-lucide="gauge" class="w-4 h-4"></i>
</h3> {% trans "Complaint Response Time" %}
<p class="text-sm text-gray-500 mb-4">{% trans "Time to first response/update" %}</p> </h3>
<p class="text-sm text-slate">{% trans "Time to first response/update" %}</p>
</div>
<div class="h-[320px]"> <div class="h-[320px]">
<canvas id="complaintResponseChart"></canvas> <canvas id="complaintResponseChart"></canvas>
</div> </div>
@ -199,47 +245,47 @@
</div> </div>
<!-- Staff Comparison Table --> <!-- Staff Comparison Table -->
<div class="border border-gray-100 rounded-2xl overflow-hidden"> <div class="card">
<div class="bg-gradient-to-r from-light to-blue-50 px-6 py-4 border-b border-gray-100"> <div class="card-header">
<h3 class="font-bold text-lg flex items-center gap-2"> <h3 class="card-title flex items-center gap-2">
<i data-lucide="users" class="w-5 h-5 text-navy"></i> <i data-lucide="users" class="w-4 h-4"></i>
{% trans "Staff Complaint Performance" %} {% trans "Staff Complaint Performance" %}
</h3> </h3>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full"> <table class="w-full">
<thead class="bg-gray-50"> <thead class="bg-light">
<tr> <tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Staff Name" %}</th> <th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Staff Name" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Hospital" %}</th> <th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Hospital" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Department" %}</th> <th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Department" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Total" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Total" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Internal" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Internal" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "External" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "External" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Open" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Open" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Resolved" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Resolved" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Activation ≤2h" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Activation ≤2h" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Response ≤24h" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤24h" %}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-50"> <tbody class="divide-y divide-slate-100">
{% for staff in performance_data.staff_metrics %} {% for staff in performance_data.staff_metrics %}
<tr class="hover:bg-gray-50 transition"> <tr class="hover:bg-light transition">
<td class="px-6 py-4"> <td class="px-6 py-4">
<a href="{% url 'dashboard:staff_performance_detail' staff.id %}?date_range={{ date_range }}" class="font-semibold text-navy hover:text-navy"> <a href="{% url 'dashboard:staff_performance_detail' staff.id %}?date_range={{ date_range }}" class="font-bold text-navy hover:text-blue transition group">
{{ staff.name }} {{ staff.name }}
</a> </a>
</td> </td>
<td class="px-6 py-4 text-sm text-gray-600">{{ staff.hospital|default:"-" }}</td> <td class="px-6 py-4 text-sm text-slate">{{ staff.hospital|default:"-" }}</td>
<td class="px-6 py-4 text-sm text-gray-600">{{ staff.department|default:"-" }}</td> <td class="px-6 py-4 text-sm text-slate">{{ staff.department|default:"-" }}</td>
<td class="px-6 py-4 text-center font-bold">{{ staff.complaints.total }}</td> <td class="px-6 py-4 text-center font-bold text-navy">{{ staff.complaints.total }}</td>
<td class="px-6 py-4 text-center">{{ staff.complaints.internal }}</td> <td class="px-6 py-4 text-center">{{ staff.complaints.internal }}</td>
<td class="px-6 py-4 text-center">{{ staff.complaints.external }}</td> <td class="px-6 py-4 text-center">{{ staff.complaints.external }}</td>
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-yellow-100 text-yellow-600">{{ staff.complaints.status.open }}</span> <span class="px-3 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-600">{{ staff.complaints.status.open }}</span>
</td> </td>
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-green-100 text-green-600">{{ staff.complaints.status.resolved }}</span> <span class="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-600">{{ staff.complaints.status.resolved }}</span>
</td> </td>
<td class="px-6 py-4 text-center">{{ staff.complaints.activation_time.within_2h }}</td> <td class="px-6 py-4 text-center">{{ staff.complaints.activation_time.within_2h }}</td>
<td class="px-6 py-4 text-center">{{ staff.complaints.response_time.within_24h }}</td> <td class="px-6 py-4 text-center">{{ staff.complaints.response_time.within_24h }}</td>
@ -252,24 +298,28 @@
</div> </div>
<!-- Inquiries Tab Content --> <!-- Inquiries Tab Content -->
<div id="inquiries-content" class="tab-content p-6 hidden"> <div id="inquiries-content" class="tab-content hidden">
<!-- Charts Row --> <!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="border border-gray-100 rounded-2xl p-6"> <div class="card">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2"> <div class="card-header">
<i data-lucide="bar-chart-2" class="w-5 h-5 text-navy"></i> <h3 class="card-title flex items-center gap-2">
{% trans "Inquiry Status Distribution" %} <i data-lucide="bar-chart-2" class="w-4 h-4"></i>
</h3> {% trans "Inquiry Status Distribution" %}
</h3>
</div>
<div class="h-[320px]"> <div class="h-[320px]">
<canvas id="inquiryStatusChart"></canvas> <canvas id="inquiryStatusChart"></canvas>
</div> </div>
</div> </div>
<div class="border border-gray-100 rounded-2xl p-6"> <div class="card">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2"> <div class="card-header">
<i data-lucide="gauge" class="w-5 h-5 text-navy"></i> <h3 class="card-title flex items-center gap-2">
{% trans "Inquiry Response Time" %} <i data-lucide="gauge" class="w-4 h-4"></i>
</h3> {% trans "Inquiry Response Time" %}
<p class="text-sm text-gray-500 mb-4">{% trans "Time to first response/update" %}</p> </h3>
<p class="text-sm text-slate">{% trans "Time to first response/update" %}</p>
</div>
<div class="h-[320px]"> <div class="h-[320px]">
<canvas id="inquiryResponseChart"></canvas> <canvas id="inquiryResponseChart"></canvas>
</div> </div>
@ -277,40 +327,40 @@
</div> </div>
<!-- Staff Comparison Table --> <!-- Staff Comparison Table -->
<div class="border border-gray-100 rounded-2xl overflow-hidden"> <div class="card">
<div class="bg-gradient-to-r from-light to-blue-50 px-6 py-4 border-b border-gray-100"> <div class="card-header">
<h3 class="font-bold text-lg flex items-center gap-2"> <h3 class="card-title flex items-center gap-2">
<i data-lucide="users" class="w-5 h-5 text-navy"></i> <i data-lucide="users" class="w-4 h-4"></i>
{% trans "Staff Inquiry Performance" %} {% trans "Staff Inquiry Performance" %}
</h3> </h3>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full"> <table class="w-full">
<thead class="bg-gray-50"> <thead class="bg-light">
<tr> <tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Staff Name" %}</th> <th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Staff Name" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Hospital" %}</th> <th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Hospital" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Department" %}</th> <th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Department" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Total" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Total" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Open" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Open" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Resolved" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Resolved" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Response ≤24h" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤24h" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Response ≤48h" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤48h" %}</th>
<th class="px-6 py-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Response ≤72h" %}</th> <th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤72h" %}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-50"> <tbody class="divide-y divide-slate-100">
{% for staff in performance_data.staff_metrics %} {% for staff in performance_data.staff_metrics %}
<tr class="hover:bg-gray-50 transition"> <tr class="hover:bg-light transition">
<td class="px-6 py-4 font-semibold text-gray-800">{{ staff.name }}</td> <td class="px-6 py-4 font-bold text-navy">{{ staff.name }}</td>
<td class="px-6 py-4 text-sm text-gray-600">{{ staff.hospital|default:"-" }}</td> <td class="px-6 py-4 text-sm text-slate">{{ staff.hospital|default:"-" }}</td>
<td class="px-6 py-4 text-sm text-gray-600">{{ staff.department|default:"-" }}</td> <td class="px-6 py-4 text-sm text-slate">{{ staff.department|default:"-" }}</td>
<td class="px-6 py-4 text-center font-bold">{{ staff.inquiries.total }}</td> <td class="px-6 py-4 text-center font-bold text-navy">{{ staff.inquiries.total }}</td>
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-yellow-100 text-yellow-600">{{ staff.inquiries.status.open }}</span> <span class="px-3 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-600">{{ staff.inquiries.status.open }}</span>
</td> </td>
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-green-100 text-green-600">{{ staff.inquiries.status.resolved }}</span> <span class="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-600">{{ staff.inquiries.status.resolved }}</span>
</td> </td>
<td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_24h }}</td> <td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_24h }}</td>
<td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_48h }}</td> <td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_48h }}</td>

View File

@ -81,6 +81,15 @@
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Dashboard" %}</span> <span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Dashboard" %}</span>
</a> </a>
<!-- Admin Evaluation (Admin only) -->
{% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'dashboard:admin_evaluation' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'admin-evaluation' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="shield-check" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap">{% trans "Admin Evaluation" %}</span>
</a>
{% endif %}
<!-- Complaints --> <!-- Complaints -->
<a href="{% url 'complaints:complaint_list' %}" <a href="{% url 'complaints:complaint_list' %}"
class="flex items-center gap-3 p-3 rounded-lg transition {% if 'complaints' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}"> class="flex items-center gap-3 p-3 rounded-lg transition {% if 'complaints' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
@ -263,6 +272,38 @@
</div> </div>
</div> </div>
<!-- PX Sources (with submenu) -->
<div class="px-sources-nav-container">
<button onclick="togglePXSourcesMenu(event)"
class="w-full flex items-center gap-3 p-3 rounded-lg transition {% if 'px-sources' in request.path %}nav-item-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="radio" class="w-5 h-5 flex-shrink-0"></i>
<span class="sidebar-text text-sm font-semibold whitespace-nowrap flex-1 text-left">{% trans "PX Sources" %}</span>
<i data-lucide="chevron-down" class="sidebar-text w-4 h-4 flex-shrink-0 transform transition-transform duration-200 {% if 'px-sources' in request.path %}rotate-180{% endif %}" id="px-sources-chevron"></i>
</button>
<div class="submenu {% if 'px-sources' in request.path %}open{% endif %} space-y-1 mt-1" id="px-sources-submenu">
<a href="{% url 'px_sources:source_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if request.resolver_match.url_name == 'source_list' %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="list" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "All Sources" %}</span>
</a>
<a href="{% url 'px_sources:source_user_dashboard' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'dashboard' in request.path and 'px-sources' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="layout-dashboard" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Dashboard" %}</span>
</a>
<a href="{% url 'px_sources:source_user_complaint_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'complaints' in request.path and 'px-sources' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="file-text" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Source Complaints" %}</span>
</a>
<a href="{% url 'px_sources:source_user_inquiry_list' %}"
class="flex items-center gap-2 p-2 rounded-lg transition {% if 'inquiries' in request.path and 'px-sources' in request.path %}nav-item-sublink-active{% else %}opacity-70 hover:opacity-100 hover:bg-white/10{% endif %}">
<i data-lucide="help-circle" class="w-4 h-4 flex-shrink-0"></i>
<span class="sidebar-text whitespace-nowrap">{% trans "Source Inquiries" %}</span>
</a>
</div>
</div>
<!-- Send SMS (Admin only) --> <!-- Send SMS (Admin only) -->
{% if user.is_px_admin or user.is_hospital_admin %} {% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'notifications:send_sms_direct' %}" <a href="{% url 'notifications:send_sms_direct' %}"
@ -350,6 +391,14 @@ function toggleStaffMenu(event) {
chevron.classList.toggle('rotate-180'); chevron.classList.toggle('rotate-180');
} }
function togglePXSourcesMenu(event) {
event.preventDefault();
const submenu = document.getElementById('px-sources-submenu');
const chevron = document.getElementById('px-sources-chevron');
submenu.classList.toggle('open');
chevron.classList.toggle('rotate-180');
}
function logout() { function logout() {
if (confirm('{% trans "Are you sure you want to logout?" %}')) { if (confirm('{% trans "Are you sure you want to logout?" %}')) {
document.querySelector('form[action="{% url 'accounts:logout' %}"]').submit(); document.querySelector('form[action="{% url 'accounts:logout' %}"]').submit();

View File

@ -4,110 +4,141 @@
{% block title %}{% trans "Delete Source" %}{% endblock %} {% block title %}{% trans "Delete Source" %}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="p-6">
<!-- Breadcrumb -->
<nav class="mb-4">
<ol class="flex items-center gap-2 text-sm text-slate">
<li><a href="{% url 'px_sources:source_list' %}" class="text-blue hover:text-navy">{% trans "PX Sources" %}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4"></i></li>
<li><a href="{% url 'px_sources:source_detail' source.pk %}" class="text-blue hover:text-navy">{{ source.name_en }}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4"></i></li>
<li class="text-navy font-semibold">{% trans "Delete" %}</li>
</ol>
</nav>
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<div> <div>
<nav aria-label="breadcrumb"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<ol class="breadcrumb mb-2"> <i data-lucide="alert-triangle" class="w-8 h-8 text-red-500"></i>
<li class="breadcrumb-item">
<a href="{% url 'px_sources:source_list' %}">{% trans "PX Sources" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'px_sources:source_detail' source.pk %}">{{ source.name_en }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
{% trans "Delete" %}
</li>
</ol>
</nav>
<h2 class="mb-1">
<i class="bi bi-exclamation-triangle-fill text-danger me-2"></i>
{% trans "Delete Source" %} {% trans "Delete Source" %}
</h2> </h1>
<p class="text-muted mb-0">{{ source.name_en }}</p> <p class="text-slate mt-1">{{ source.name_en }}</p>
</div> </div>
<div> <div>
<a href="{% url 'px_sources:source_detail' source.pk %}" class="btn btn-outline-secondary"> <a href="{% url 'px_sources:source_detail' source.pk %}"
<i class="bi bi-x-circle me-1"></i> {% trans "Cancel" %} class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
<i data-lucide="x" class="w-4 h-4"></i> {% trans "Cancel" %}
</a> </a>
</div> </div>
</div> </div>
<!-- Delete Confirmation Card --> <!-- Delete Confirmation Card -->
<div class="card"> <div class="bg-white rounded-xl shadow-sm border border-slate-200">
<div class="card-header"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl">
<h5 class="card-title mb-0"> <h2 class="text-lg font-semibold text-navy flex items-center gap-2">
<i class="bi bi-exclamation-circle me-2"></i>{% trans "Confirm Deletion" %} <i data-lucide="alert-circle" class="w-5 h-5 text-slate"></i>
</h5> {% trans "Confirm Deletion" %}
</h2>
</div> </div>
<div class="card-body"> <div class="p-6">
<div class="alert alert-warning"> <!-- Warning Alert -->
<h4><i class="fas fa-exclamation-circle"></i> {% trans "Warning" %}</h4> <div class="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p>{% trans "Are you sure you want to delete this source? This action cannot be undone." %}</p> <div class="flex items-start gap-3">
<i data-lucide="alert-circle" class="w-5 h-5 text-amber-600 mt-0.5"></i>
<div>
<h4 class="font-semibold text-amber-800 mb-1">{% trans "Warning" %}</h4>
<p class="text-amber-700 text-sm">{% trans "Are you sure you want to delete this source? This action cannot be undone." %}</p>
</div>
</div>
</div> </div>
<div class="table-responsive mb-4"> <!-- Source Info Table -->
<table class="table table-bordered"> <div class="overflow-x-auto mb-6">
<tr> <table class="w-full border border-slate-200 rounded-lg">
<th width="30%">{% trans "Name (English)" %}</th> <tbody class="divide-y divide-slate-200">
<td><strong>{{ source.name_en }}</strong></td> <tr>
</tr> <th class="py-3 px-4 text-left text-sm font-semibold text-navy bg-slate-50 w-1/3">{% trans "Name (English)" %}</th>
<tr> <td class="py-3 px-4 text-navy font-semibold">{{ source.name_en }}</td>
<th>{% trans "Name (Arabic)" %}</th> </tr>
<td dir="rtl">{{ source.name_ar|default:"-" }}</td> <tr>
</tr> <th class="py-3 px-4 text-left text-sm font-semibold text-navy bg-slate-50">{% trans "Name (Arabic)" %}</th>
<tr> <td class="py-3 px-4 text-navy" dir="rtl">{{ source.name_ar|default:"-" }}</td>
<th>{% trans "Description" %}</th> </tr>
<td>{{ source.description|default:"-"|truncatewords:20 }}</td> <tr>
</tr> <th class="py-3 px-4 text-left text-sm font-semibold text-navy bg-slate-50">{% trans "Description" %}</th>
<tr> <td class="py-3 px-4 text-slate">{{ source.description|default:"-"|truncatewords:20 }}</td>
<th>{% trans "Status" %}</th> </tr>
<td> <tr>
{% if source.is_active %} <th class="py-3 px-4 text-left text-sm font-semibold text-navy bg-slate-50">{% trans "Status" %}</th>
<span class="badge bg-success">{% trans "Active" %}</span> <td class="py-3 px-4">
{% else %} {% if source.is_active %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span> <span class="inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-700 rounded-full text-xs font-semibold">
{% endif %} <i data-lucide="check-circle" class="w-3 h-3"></i>
</td> {% trans "Active" %}
</tr> </span>
<tr> {% else %}
<th>{% trans "Usage Count" %}</th> <span class="inline-flex items-center gap-1 px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full text-xs font-semibold">
<td> <i data-lucide="x-circle" class="w-3 h-3"></i>
{% if usage_count > 0 %} {% trans "Inactive" %}
<span class="badge bg-danger">{{ usage_count }}</span> </span>
{% else %} {% endif %}
<span class="badge bg-success">0</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th class="py-3 px-4 text-left text-sm font-semibold text-navy bg-slate-50">{% trans "Usage Count" %}</th>
<td class="py-3 px-4">
{% if usage_count > 0 %}
<span class="inline-flex items-center gap-1 px-2.5 py-1 bg-red-100 text-red-700 rounded-full text-xs font-semibold">{{ usage_count }}</span>
{% else %}
<span class="inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-700 rounded-full text-xs font-semibold">0</span>
{% endif %}
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
{% if usage_count > 0 %} {% if usage_count > 0 %}
<div class="alert alert-danger"> <!-- Cannot Delete Alert -->
<h5><i class="fas fa-exclamation-triangle"></i> {% trans "Cannot Delete" %}</h5> <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p>{% trans "This source has been used in {{ usage_count }} record(s). You cannot delete sources that have usage records." %}</p> <div class="flex items-start gap-3">
<p><strong>{% trans "Recommended action:" %}</strong> {% trans "Deactivate this source instead by editing it and unchecking the 'Active' checkbox." %}</p> <i data-lucide="alert-triangle" class="w-5 h-5 text-red-600 mt-0.5"></i>
<div>
<h5 class="font-semibold text-red-800 mb-1">{% trans "Cannot Delete" %}</h5>
<p class="text-red-700 text-sm mb-2">{% trans "This source has been used in {{ usage_count }} record(s). You cannot delete sources that have usage records." %}</p>
<p class="text-red-700 text-sm"><strong>{% trans "Recommended action:" %}</strong> {% trans "Deactivate this source instead by editing it and unchecking the 'Active' checkbox." %}</p>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
<form method="post"> <!-- Action Buttons -->
<form method="post" class="flex gap-3">
{% csrf_token %} {% csrf_token %}
{% if usage_count == 0 %} {% if usage_count == 0 %}
<button type="submit" class="btn btn-danger"> <button type="submit"
<i class="fas fa-trash"></i> {% trans "Yes, Delete" %} class="inline-flex items-center gap-2 px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition shadow-lg shadow-red-500/20">
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Yes, Delete" %}
</button> </button>
{% else %} {% else %}
<button type="button" class="btn btn-danger" disabled> <button type="button" disabled
<i class="fas fa-trash"></i> {% trans "Cannot Delete" %} class="inline-flex items-center gap-2 px-6 py-2 bg-red-300 text-white rounded-lg cursor-not-allowed">
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Cannot Delete" %}
</button> </button>
{% endif %} {% endif %}
<a href="{% url 'px_sources:source_detail' source.pk %}" class="btn btn-secondary"> <a href="{% url 'px_sources:source_detail' source.pk %}"
class="inline-flex items-center gap-2 px-6 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -1,237 +1,363 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{{ source.name_en }} - {% trans "PX Source" %}{% endblock %} {% block title %}{{ source.name_en }} - {% trans "PX Source" %} - PX360{% endblock %}
{% block extra_css %}
<style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
}
.info-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.info-table th {
padding: 1rem;
text-align: left;
font-size: 0.875rem;
font-weight: 600;
color: var(--hh-navy);
width: 35%;
border-bottom: 1px solid #f1f5f9;
}
.info-table td {
padding: 1rem;
color: #475569;
border-bottom: 1px solid #f1f5f9;
}
.info-table tr:last-child th,
.info-table tr:last-child td {
border-bottom: none;
}
.stat-card {
background: linear-gradient(135deg, var(--hh-navy), var(--hh-blue));
color: white;
padding: 1.5rem;
border-radius: 1rem;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 800;
}
.stat-label {
font-size: 0.875rem;
opacity: 0.9;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="px-4 py-6">
<!-- Breadcrumb -->
<nav class="mb-4 animate-in">
<ol class="flex items-center gap-2 text-sm text-slate">
<li><a href="{% url 'px_sources:source_list' %}" class="text-blue hover:text-navy font-medium">{% trans "PX Sources" %}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i></li>
<li class="text-navy font-semibold">{{ source.name_en }}</li>
</ol>
</nav>
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-start gap-4 mb-6 animate-in">
<div> <div>
<nav aria-label="breadcrumb"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<ol class="breadcrumb mb-2"> <div class="w-10 h-10 bg-blue/10 rounded-xl flex items-center justify-center">
<li class="breadcrumb-item"> <i data-lucide="radio" class="w-6 h-6 text-blue"></i>
<a href="{% url 'px_sources:source_list' %}">{% trans "PX Sources" %}</a> </div>
</li>
<li class="breadcrumb-item active" aria-current="page">
{{ source.name_en }}
</li>
</ol>
</nav>
<h2 class="mb-1">
<i class="bi bi-lightning-fill text-warning me-2"></i>
{{ source.name_en }} {{ source.name_en }}
</h2> </h1>
<p class="text-muted mb-0"> <div class="mt-2 flex items-center gap-3">
{% if source.is_active %} <span class="inline-flex items-center gap-1.5 px-3 py-1.5 {% if source.is_active %}bg-green-100 text-green-700{% else %}bg-slate-100 text-slate-600{% endif %} rounded-full text-xs font-bold">
<span class="badge bg-success">{% trans "Active" %}</span> <i data-lucide="{% if source.is_active %}check-circle{% else %}x-circle{% endif %}" class="w-3.5 h-3.5"></i>
{% else %} {% if source.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span> </span>
{% endif %} <span class="font-mono text-xs bg-slate-100 px-2 py-1 rounded">{{ source.code }}</span>
</p> <span class="source-type-badge inline-flex items-center gap-1.5 px-3 py-1.5 bg-blue-100 text-blue-700 rounded-full text-xs font-bold">
<i data-lucide="tag" class="w-3.5 h-3.5"></i>
{{ source.get_source_type_display }}
</span>
</div>
</div> </div>
<div> <div class="flex gap-2">
<a href="{% url 'px_sources:source_list' %}" class="btn btn-outline-secondary me-2"> <a href="{% url 'px_sources:source_list' %}"
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to List" %} class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
{% trans "Back" %}
</a> </a>
{% if request.user.is_px_admin or request.user.is_hospital_admin %}
<a href="{% url 'px_sources:source_edit' source.pk %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition shadow-lg shadow-navy/20">
<i data-lucide="edit" class="w-4 h-4"></i>
{% trans "Edit" %}
</a>
{% endif %}
{% if request.user.is_px_admin %} {% if request.user.is_px_admin %}
<a href="{% url 'px_sources:source_edit' source.pk %}" class="btn btn-primary me-2"> <a href="{% url 'px_sources:source_delete' source.pk %}"
<i class="bi bi-pencil me-1"></i> {% trans "Edit" %} class="inline-flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition">
</a> <i data-lucide="trash-2" class="w-4 h-4"></i>
<a href="{% url 'px_sources:source_delete' source.pk %}" class="btn btn-danger"> {% trans "Delete" %}
<i class="bi bi-trash me-1"></i> {% trans "Delete" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<!-- Detail Cards --> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="row"> <!-- Source Details -->
<div class="col-12"> <div class="lg:col-span-2 space-y-6">
<div class="card"> <!-- Source Information -->
<div class="info-card animate-in">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i class="bi bi-info-circle me-2"></i>{% trans "Source Details" %} <i data-lucide="info" class="w-5 h-5"></i>
</h5> {% trans "Source Information" %}
</h2>
</div> </div>
<div class="card-body"> <div class="p-0">
<div class="row"> <table class="w-full info-table">
<div class="col-md-8"> <tbody>
<h5>{% trans "Source Details" %}</h5> <tr>
<table class="table table-borderless"> <th>{% trans "Name (English)" %}</th>
<tr> <td class="font-semibold">{{ source.name_en }}</td>
<th width="30%">{% trans "Name (English)" %}</th> </tr>
<td><strong>{{ source.name_en }}</strong></td> <tr>
</tr> <th>{% trans "Name (Arabic)" %}</th>
<tr> <td dir="rtl">{{ source.name_ar|default:"-" }}</td>
<th>{% trans "Name (Arabic)" %}</th> </tr>
<td dir="rtl">{{ source.name_ar|default:"-" }}</td> <tr>
</tr> <th>{% trans "Description" %}</th>
<tr> <td>{{ source.description|default:"-"|linebreaks }}</td>
<th>{% trans "Description" %}</th> </tr>
<td>{{ source.description|default:"-"|linebreaks }}</td> {% if source.contact_email or source.contact_phone %}
</tr> <tr>
<tr> <th>{% trans "Contact" %}</th>
<th>{% trans "Status" %}</th> <td>
<td> {% if source.contact_email %}
{% if source.is_active %} <div class="flex items-center gap-2">
<span class="badge bg-success">{% trans "Active" %}</span> <i data-lucide="mail" class="w-4 h-4 text-slate"></i>
{% else %} <a href="mailto:{{ source.contact_email }}" class="text-blue hover:underline">{{ source.contact_email }}</a>
<span class="badge bg-secondary">{% trans "Inactive" %}</span> </div>
{% endif %} {% endif %}
</td> {% if source.contact_phone %}
</tr> <div class="flex items-center gap-2 mt-1">
<tr> <i data-lucide="phone" class="w-4 h-4 text-slate"></i>
<th>{% trans "Created" %}</th> <a href="tel:{{ source.contact_phone }}" class="text-blue hover:underline">{{ source.contact_phone }}</a>
<td>{{ source.created_at|date:"Y-m-d H:i" }}</td> </div>
</tr> {% endif %}
<tr> </td>
<th>{% trans "Last Updated" %}</th> </tr>
<td>{{ source.updated_at|date:"Y-m-d H:i" }}</td> {% endif %}
</tr> <tr>
</table> <th>{% trans "Created" %}</th>
</div> <td class="text-sm">{{ source.created_at|date:"Y-m-d H:i" }}</td>
</tr>
<tr>
<th>{% trans "Last Updated" %}</th>
<td class="text-sm">{{ source.updated_at|date:"Y-m-d H:i" }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-4"> <!-- Usage Statistics -->
<h5>{% trans "Quick Actions" %}</h5> <div class="info-card animate-in">
<div class="list-group"> <div class="card-header">
{% if request.user.is_px_admin %} <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<a href="{% url 'px_sources:source_edit' source.pk %}" class="list-group-item list-group-item-action"> <i data-lucide="circle-help" class="w-5 h-5"></i>
<i class="fas fa-edit"></i> {% trans "Edit Source" %} {% trans "Usage Statistics (Last 30 Days)" %}
</a> </h2>
<a href="{% url 'px_sources:source_delete' source.pk %}" class="list-group-item list-group-item-action list-group-item-danger"> </div>
<i class="fas fa-trash"></i> {% trans "Delete Source" %} <div class="p-6">
</a> <div class="grid grid-cols-3 gap-4">
<div class="stat-card">
<div class="stat-value">{{ usage_stats.total_usage }}</div>
<div class="stat-label">{% trans "Total Usage" %}</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #10b981, #34d399);">
<div class="stat-value">{{ usage_stats.complaints }}</div>
<div class="stat-label">{% trans "Complaints" %}</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #f59e0b, #fbbf24);">
<div class="stat-value">{{ usage_stats.inquiries }}</div>
<div class="stat-label">{% trans "Inquiries" %}</div>
</div>
</div>
</div>
</div>
<!-- Usage Records -->
<div class="info-card animate-in">
<div class="card-header">
<h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="activity" class="w-5 h-5"></i>
{% trans "Recent Activity" %} ({{ usage_records|length }})
</h2>
</div>
<div class="p-0">
{% if usage_records %}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b-2 border-slate-200">
<th class="text-left py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Date" %}</th>
<th class="text-left py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Type" %}</th>
<th class="text-left py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Reference" %}</th>
<th class="text-left py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</th>
<th class="text-left py-3 px-4 text-xs font-bold text-slate uppercase">{% trans "User" %}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for record in usage_records %}
<tr class="hover:bg-slate-50 transition">
<td class="py-3 px-4 text-sm text-slate">{{ record.created_at|date:"Y-m-d H:i" }}</td>
<td class="py-3 px-4">
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 {% if record.content_type.model == 'complaint' %}bg-red-100 text-red-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded-full text-xs font-bold">
<i data-lucide="{% if record.content_type.model == 'complaint' %}file-text{% else %}help-circle{% endif %}" class="w-3 h-3"></i>
{{ record.content_type.model|title }}
</span>
</td>
<td class="py-3 px-4 text-sm font-mono text-slate">{{ record.object_id|truncatechars:12 }}</td>
<td class="py-3 px-4 text-sm text-slate">{{ record.hospital.name|default:"-" }}</td>
<td class="py-3 px-4 text-sm text-navy">{{ record.user.get_full_name|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-12">
<div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="activity" class="w-8 h-8 text-slate-400"></i>
</div>
<p class="text-slate font-medium">{% trans "No usage records found" %}</p>
<p class="text-slate text-sm mt-1">{% trans "Activity will appear here once feedback is submitted" %}</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Quick Stats -->
<div class="info-card animate-in">
<div class="card-header">
<h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i data-lucide="trending-up" class="w-5 h-5"></i>
{% trans "Quick Stats" %}
</h2>
</div>
<div class="p-6 space-y-4">
<div class="flex items-center justify-between">
<span class="text-slate text-sm">{% trans "Total Complaints" %}</span>
<span class="font-bold text-navy text-lg">{{ source.total_complaints }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate text-sm">{% trans "Total Inquiries" %}</span>
<span class="font-bold text-navy text-lg">{{ source.total_inquiries }}</span>
</div>
<div class="pt-4 border-t border-slate-200">
<div class="flex items-center justify-between mb-2">
<span class="text-slate text-sm">{% trans "Source Users" %}</span>
<span class="font-bold text-navy text-lg">{{ source_users.count }}</span>
</div>
{% if source_users %}
<div class="space-y-2 mt-3">
{% for su in source_users|slice:":5" %}
<div class="flex items-center gap-2 text-sm">
<div class="w-8 h-8 rounded-full bg-blue/10 flex items-center justify-center">
<i data-lucide="user" class="w-4 h-4 text-blue"></i>
</div>
<div class="flex-1">
<p class="font-medium text-navy">{{ su.user.get_full_name }}</p>
<p class="text-xs text-slate">{{ su.user.email }}</p>
</div>
{% if su.is_active %}
<i data-lucide="check-circle" class="w-4 h-4 text-green-600"></i>
{% else %}
<i data-lucide="x-circle" class="w-4 h-4 text-slate-400"></i>
{% endif %} {% endif %}
</div> </div>
{% endfor %}
</div> </div>
{% endif %}
</div> </div>
<hr>
<h5>{% trans "Recent Usage" %} ({{ usage_records|length }})</h5>
{% if usage_records %}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Content Type" %}</th>
<th>{% trans "Object ID" %}</th>
<th>{% trans "Hospital" %}</th>
<th>{% trans "User" %}</th>
</tr>
</thead>
<tbody>
{% for record in usage_records %}
<tr>
<td>{{ record.created_at|date:"Y-m-d H:i" }}</td>
<td><code>{{ record.content_type.model }}</code></td>
<td>{{ record.object_id|truncatechars:20 }}</td>
<td>{{ record.hospital.name_en|default:"-" }}</td>
<td>{{ record.user.get_full_name|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">{% trans "No usage records found for this source." %}</p>
{% endif %}
</div> </div>
</div> </div>
</div>
</div>
<!-- Source Users Section (PX Admin only) --> <!-- Actions -->
{% comment %} {% if request.user.is_px_admin %} {% endcomment %} <div class="info-card animate-in">
<div class="row mt-4"> <div class="card-header">
<div class="col-12"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<div class="card"> <i data-lucide="settings" class="w-5 h-5"></i>
<div class="card-header d-flex justify-content-between align-items-center"> {% trans "Manage" %}
<h5 class="card-title mb-0"> </h2>
<i class="bi bi-people-fill me-2"></i> </div>
{% trans "Source Users" %} ({{ source_users|length }}) <div class="p-4 space-y-2">
</h5> {% if request.user.is_px_admin or request.user.is_hospital_admin %}
<a href="{% url 'px_sources:source_user_create' source.pk %}" class="btn btn-sm btn-primary"> <a href="{% url 'px_sources:source_user_create' source.pk %}"
<i class="bi bi-plus-lg me-1"></i>{% trans "Add Source User" %} class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-50 transition text-navy font-medium">
<i data-lucide="user-plus" class="w-4 h-4"></i>
{% trans "Add Source User" %}
</a>
{% endif %}
<a href="{% url 'px_sources:source_edit' source.pk %}"
class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-50 transition text-navy font-medium">
<i data-lucide="edit" class="w-4 h-4"></i>
{% trans "Edit Source" %}
</a>
{% if source.is_active %}
<a href="{% url 'px_sources:source_toggle_status' source.pk %}"
class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-50 transition text-yellow-600 font-medium">
<i data-lucide="pause" class="w-4 h-4"></i>
{% trans "Deactivate" %}
</a> </a>
</div>
<div class="card-body">
{% if source_users %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Permissions" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for su in source_users %}
<tr>
<td>
<strong>{{ su.user.get_full_name|default:"-" }}</strong>
</td>
<td>{{ su.user.email }}</td>
<td>
{% if su.is_active %}
<span class="badge bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
{% endif %}
</td>
<td>
{% if su.can_create_complaints %}
<span class="badge bg-primary me-1">{% trans "Complaints" %}</span>
{% endif %}
{% if su.can_create_inquiries %}
<span class="badge bg-info">{% trans "Inquiries" %}</span>
{% endif %}
{% if not su.can_create_complaints and not su.can_create_inquiries %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</td>
<td>{{ su.created_at|date:"Y-m-d" }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'px_sources:source_user_edit' source.pk su.pk %}"
class="btn btn-outline-primary"
title="{% trans 'Edit' %}">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'px_sources:source_user_delete' source.pk su.pk %}"
class="btn btn-outline-danger"
title="{% trans 'Delete' %}">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %} {% else %}
<div class="text-center py-5"> <a href="{% url 'px_sources:source_toggle_status' source.pk %}"
<i class="bi bi-people fs-1 text-muted mb-3"></i> class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-50 transition text-green-600 font-medium">
<p class="text-muted mb-0"> <i data-lucide="play" class="w-4 h-4"></i>
{% trans "No source users assigned yet." %} {% trans "Activate" %}
<a href="{% url 'px_sources:source_user_create' source.pk %}" class="text-primary"> </a>
{% trans "Add a source user" %}
</a>
{% trans "to get started." %}
</p>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% comment %} {% endif %} {% endcomment %}
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,101 +1,289 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% if source %}{% trans "Edit Source" %}{% else %}{% trans "Create Source" %}{% endif %}{% endblock %} {% block title %}{% if source %}{% trans "Edit Source" %}{% else %}{% trans "Create Source" %}{% endif %} - PX360{% endblock %}
{% block extra_css %}
<style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
}
.form-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--hh-navy);
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #cbd5e1;
border-radius: 0.75rem;
font-size: 0.95rem;
transition: all 0.2s;
background: white;
color: #1e293b;
font-family: 'Inter', sans-serif;
}
.form-input:focus {
outline: none;
border-color: var(--hh-blue);
box-shadow: 0 0 0 4px rgba(0, 123, 189, 0.1);
transform: translateY(-1px);
}
.form-input[readonly] {
background: #f1f5f9;
cursor: not-allowed;
}
textarea.form-input {
resize: vertical;
min-height: 120px;
}
.btn-primary {
background: linear-gradient(135deg, var(--hh-navy) 0%, var(--hh-blue) 100%);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 86, 150, 0.3);
}
.btn-secondary {
background: white;
color: #475569;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 600;
border: 2px solid #e2e8f0;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-secondary:hover {
background: #f1f5f9;
border-color: #cbd5e1;
transform: translateY(-1px);
}
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--hh-light);
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.checkbox-wrapper:hover {
background: #e0f2fe;
}
.checkbox-wrapper input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="px-4 py-6">
<!-- Page Header --> <!-- Breadcrumb -->
<div class="d-flex justify-content-between align-items-center mb-4"> <nav class="mb-4 animate-in">
<div> <ol class="flex items-center gap-2 text-sm text-slate">
<nav aria-label="breadcrumb"> <li><a href="{% url 'px_sources:source_list' %}" class="text-blue hover:text-navy font-medium">{% trans "PX Sources" %}</a></li>
<ol class="breadcrumb mb-2"> <li><i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i></li>
<li class="breadcrumb-item"> <li class="text-navy font-semibold">
<a href="{% url 'px_sources:source_list' %}">{% trans "PX Sources" %}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
{% if source %}{% trans "Edit Source" %}{% else %}{% trans "Create Source" %}{% endif %}
</li>
</ol>
</nav>
<h2 class="mb-1">
<i class="bi bi-{% if source %}pencil-square{% else %}plus-circle{% endif %} text-warning me-2"></i>
{% if source %}{% trans "Edit Source" %}{% else %}{% trans "Create Source" %}{% endif %} {% if source %}{% trans "Edit Source" %}{% else %}{% trans "Create Source" %}{% endif %}
</h2> </li>
</ol>
</nav>
<!-- Page Header -->
<div class="flex flex-wrap justify-between items-center gap-4 mb-6 animate-in">
<div>
<h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<div class="w-10 h-10 bg-blue/10 rounded-xl flex items-center justify-center">
<i data-lucide="{% if source %}edit{% else %}plus-circle{% endif %}" class="w-5 h-5 text-blue"></i>
</div>
{% if source %}{% trans "Edit Source" %}{% else %}{% trans "Create Source" %}{% endif %}
</h1>
{% if source %} {% if source %}
<p class="text-muted mb-0">{{ source.name_en }}</p> <p class="text-slate mt-1">{{ source.name_en }} <span class="text-slate-400">({{ source.code }})</span></p>
{% endif %} {% endif %}
</div> </div>
<div> <a href="{% url 'px_sources:source_list' %}"
<a href="{% url 'px_sources:source_list' %}" class="btn btn-outline-secondary"> class="btn-secondary">
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to List" %} <i data-lucide="arrow-left" class="w-4 h-4"></i>
</a> {% trans "Back to List" %}
</div> </a>
</div> </div>
<!-- Form Card --> <!-- Form Card -->
<div class="card"> <div class="form-card max-w-4xl animate-in">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
<i class="bi bi-form me-2"></i>{% trans "Source Information" %} <i data-lucide="file-text" class="w-5 h-5"></i>
</h5> {% trans "Source Information" %}
</h2>
</div> </div>
<div class="card-body"> <div class="p-6">
<form method="post" enctype="multipart/form-data"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-5">
<div class="col-md-6"> <div class="md:col-span-1">
<div class="mb-3"> <label for="code" class="form-label">
<label for="name_en" class="form-label"> {% trans "Source Code" %}
{% trans "Name (English)" %} <span class="text-danger">*</span> </label>
</label> <input type="text" id="code" name="code"
<input type="text" class="form-control" id="name_en" name="name_en" value="{{ source.code|default:'' }}"
value="{{ source.name_en|default:'' }}" required placeholder="{% trans 'Auto-generated' %}"
placeholder="{% trans 'e.g., Patient Portal' %}"> class="form-input"
</div> {% if source %}readonly{% endif %}>
{% if not source %}
<p class="text-xs text-slate mt-1">{% trans "Auto-generated from name if left blank" %}</p>
{% endif %}
</div> </div>
<div class="col-md-6"> <div class="md:col-span-2">
<div class="mb-3"> <label for="name_en" class="form-label">
<label for="name_ar" class="form-label"> {% trans "Name (English)" %} <span class="text-red-500">*</span>
{% trans "Name (Arabic)" %} </label>
</label> <input type="text" id="name_en" name="name_en"
<input type="text" class="form-control" id="name_ar" name="name_ar" value="{{ source.name_en|default:'' }}" required
value="{{ source.name_ar|default:'' }}" dir="rtl" placeholder="{% trans 'e.g., Patient Portal' %}"
placeholder="{% trans 'e.g., بوابة المرضى' %}"> class="form-input">
</div>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-5">
<div>
<label for="name_ar" class="form-label">
{% trans "Name (Arabic)" %}
</label>
<input type="text" id="name_ar" name="name_ar"
value="{{ source.name_ar|default:'' }}"
placeholder="{% trans 'e.g., بوابة المريض' %}"
dir="rtl"
class="form-input">
</div>
<div>
<label for="source_type" class="form-label">
{% trans "Source Type" %}
</label>
<select id="source_type" name="source_type" class="form-input">
{% for value, label in source_types %}
<option value="{{ value }}" {% if source.source_type == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label for="description" class="form-label"> <label for="description" class="form-label">
{% trans "Description" %} {% trans "Description" %}
</label> </label>
<textarea class="form-control" id="description" name="description" <textarea id="description" name="description" rows="4"
rows="4" placeholder="{% trans 'Describe this source channel...' %}">{{ source.description|default:'' }}</textarea> placeholder="{% trans 'Enter source description...' %}"
<small class="form-text text-muted"> class="form-input">{{ source.description|default:'' }}</textarea>
{% trans "Optional: Additional details about this source" %}
</small>
</div> </div>
<div class="mb-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-5">
<div class="form-check form-switch"> <div>
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" <label for="contact_email" class="form-label">
{% if source.is_active|default:True %}checked{% endif %}> {% trans "Contact Email" %}
<label class="form-check-label" for="is_active">
{% trans "Active" %}
</label> </label>
<input type="email" id="contact_email" name="contact_email"
value="{{ source.contact_email|default:'' }}"
placeholder="contact@example.com"
class="form-input">
</div>
<div>
<label for="contact_phone" class="form-label">
{% trans "Contact Phone" %}
</label>
<input type="text" id="contact_phone" name="contact_phone"
value="{{ source.contact_phone|default:'' }}"
placeholder="+966 XX XXX XXXX"
class="form-input">
</div> </div>
<small class="form-text text-muted">
{% trans "Uncheck to deactivate this source (it won't appear in dropdowns)" %}
</small>
</div> </div>
<div class="d-flex gap-2"> <div class="form-group">
<button type="submit" class="btn btn-primary"> <label class="checkbox-wrapper">
<i class="fas fa-save"></i> {% trans "Save" %} <input type="checkbox" id="is_active" name="is_active" value="true"
{% if source.is_active|default_if_none:True %}checked{% endif %}>
<div>
<span class="text-navy font-semibold">{% trans "Active" %}</span>
<p class="text-slate text-sm m-0">{% trans "Source is available for selection" %}</p>
</div>
</label>
</div>
<div class="mt-8 pt-6 border-t border-slate-200 flex gap-3">
<button type="submit" class="btn-primary">
<i data-lucide="save" class="w-4 h-4"></i>
{% if source %}{% trans "Save Changes" %}{% else %}{% trans "Create Source" %}{% endif %}
</button> </button>
<a href="{% url 'px_sources:source_list' %}" class="btn btn-secondary"> <a href="{% url 'px_sources:source_list' %}" class="btn-secondary">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
</div> </div>
@ -103,4 +291,30 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
// Auto-generate code preview from name
const nameEnInput = document.getElementById('name_en');
const codeInput = document.getElementById('code');
if (nameEnInput && codeInput && !codeInput.value) {
nameEnInput.addEventListener('blur', function() {
const name = this.value.trim().toUpperCase();
if (name && !codeInput.value) {
const words = name.split(' ');
let code;
if (words.length >= 2) {
code = words.slice(0, 3).map(w => w.substring(0, 4)).join('-');
} else {
code = name.substring(0, 10).replace(' ', '-');
}
codeInput.value = code;
}
});
}
});
</script>
{% endblock %}

View File

@ -1,44 +1,181 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load i18n action_icons %} {% load i18n %}
{% block title %}{% trans "PX Sources" %}{% endblock %} {% block title %}{% trans "PX Sources" %} - PX360{% endblock %}
{% block extra_css %}
<style>
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
}
.page-header {
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
color: white;
padding: 2rem 2.5rem;
border-radius: 1rem;
margin-bottom: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
}
.data-card {
background: white;
border-radius: 1rem;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.data-card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 1.25rem 1.75rem;
border-bottom: 1px solid #bae6fd;
border-radius: 1rem 1rem 0 0;
}
.data-table th {
background: linear-gradient(135deg, var(--hh-light), #e0f2fe);
padding: 0.875rem 1rem;
text-align: left;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--hh-navy);
border-bottom: 2px solid #bae6fd;
}
.data-table td {
padding: 1rem;
border-bottom: 1px solid #f1f5f9;
color: #475569;
font-size: 0.875rem;
}
.data-table tbody tr {
transition: background-color 0.2s ease;
}
.data-table tbody tr:hover {
background-color: var(--hh-light);
}
.source-type-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.source-type-badge.internal {
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
color: #1e40af;
}
.source-type-badge.external {
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
color: #166534;
}
.source-type-badge.partner {
background: linear-gradient(135deg, #fef3c7, #fde68a);
color: #92400e;
}
.source-type-badge.government {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
color: #3730a3;
}
.usage-stat {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: var(--hh-slate);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.5s ease-out forwards;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="px-4 py-6">
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="page-header animate-in">
<div> <div class="flex items-center justify-between">
<h2 class="mb-1"> <div>
<i class="bi bi-lightning-fill text-warning me-2"></i> <h1 class="text-2xl font-bold mb-2">
{% trans "PX Sources" %} <i data-lucide="radio" class="w-7 h-7 inline-block me-2"></i>
</h2> {% trans "PX Sources" %}
<p class="text-muted mb-0">{% trans "Manage patient experience source channels" %}</p> </h1>
</div> <p class="text-white/90">{% trans "Manage patient experience source channels" %}</p>
<div> </div>
{% comment %} {% if request.user.is_px_admin %} {% endcomment %} <a href="{% url 'px_sources:source_create' %}"
<a href="{% url 'px_sources:source_create' %}" class="btn btn-primary"> class="inline-flex items-center gap-2 bg-white text-navy px-5 py-2.5 rounded-xl font-bold hover:bg-light transition shadow-lg">
{% action_icon 'create' %} {% trans "Add Source" %} <i data-lucide="plus" class="w-4 h-4"></i>
{% trans "Add Source" %}
</a> </a>
{% comment %} {% endif %} {% endcomment %}
</div> </div>
</div> </div>
<!-- Sources Card --> <!-- Sources Card -->
<div class="card"> <div class="data-card animate-in">
<div class="card-header"> <div class="card-header">
<h5 class="card-title mb-0"> <h2 class="text-lg font-bold text-navy flex items-center gap-2 m-0">
{% action_icon 'filter' %} {% trans "Sources" %} <i data-lucide="database" class="w-5 h-5"></i>
</h5> {% trans "All Sources" %}
</h2>
</div> </div>
<div class="card-body">
<div class="p-6">
<!-- Filters --> <!-- Filters -->
<div class="row mb-3"> <div class="flex flex-wrap gap-3 mb-6">
<div class="col-md-4"> <div class="flex-1 min-w-[250px]">
<input type="text" id="search-input" class="form-control" <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Search" %}</label>
placeholder="{% trans 'Search...' %}" value="{{ search }}"> <div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate"></i>
<input type="text" id="search-input"
class="w-full pl-10 pr-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue focus:ring-2 focus:ring-blue/20 transition"
placeholder="{% trans 'Search by name, code, or description...' %}" value="{{ search }}">
</div>
</div> </div>
<div class="col-md-3"> <div class="w-48">
<select id="status-filter" class="form-select"> <label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
<select id="status-filter"
class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white transition">
<option value="">{% trans "All Status" %}</option> <option value="">{% trans "All Status" %}</option>
<option value="true" {% if is_active == 'true' %}selected{% endif %}> <option value="true" {% if is_active == 'true' %}selected{% endif %}>
{% trans "Active" %} {% trans "Active" %}
@ -48,62 +185,125 @@
</option> </option>
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="flex items-end">
<button id="apply-filters" class="btn btn-secondary w-100"> <button id="apply-filters"
{% action_icon 'filter' %} {% trans "Filter" %} class="inline-flex items-center gap-2 px-6 py-2.5 bg-navy text-white rounded-xl font-bold hover:bg-blue transition shadow-lg shadow-navy/25">
<i data-lucide="filter" class="w-4 h-4"></i>
{% trans "Filter" %}
</button> </button>
</div> </div>
</div> </div>
<!-- Sources Table --> <!-- Sources Table -->
<div class="table-responsive"> <div class="overflow-x-auto">
<table class="table table-hover"> <table class="w-full data-table">
<thead class="table-light"> <thead>
<tr> <tr>
<th>{% trans "Code" %}</th>
<th>{% trans "Name (EN)" %}</th> <th>{% trans "Name (EN)" %}</th>
<th>{% trans "Name (AR)" %}</th> <th>{% trans "Name (AR)" %}</th>
<th>{% trans "Description" %}</th> <th>{% trans "Type" %}</th>
<th>{% trans "Status" %}</th> <th>{% trans "Usage" %}</th>
<th>{% trans "Actions" %}</th> <th class="text-center">{% trans "Status" %}</th>
<th class="text-center">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for source in sources %} {% for source in sources %}
<tr> <tr>
<td><strong>{{ source.name_en }}</strong></td>
<td dir="rtl">{{ source.name_ar|default:"-" }}</td>
<td class="text-muted">{{ source.description|default:"-"|truncatewords:10 }}</td>
<td> <td>
{% if source.is_active %} <span class="font-mono text-xs bg-slate-100 px-2 py-1 rounded">{{ source.code }}</span>
<span class="badge bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
{% endif %}
</td> </td>
<td> <td>
<a href="{% url 'px_sources:source_detail' source.pk %}" <a href="{% url 'px_sources:source_detail' source.pk %}"
class="btn btn-sm btn-info" title="{% trans 'View' %}"> class="font-semibold text-navy hover:text-blue transition">
{% action_icon 'view' %} {{ source.name_en }}
</a>
{% if request.user.is_px_admin %}
<a href="{% url 'px_sources:source_edit' source.pk %}"
class="btn btn-sm btn-warning" title="{% trans 'Edit' %}">
{% action_icon 'edit' %}
</a>
<a href="{% url 'px_sources:source_delete' source.pk %}"
class="btn btn-sm btn-danger" title="{% trans 'Delete' %}">
{% action_icon 'delete' %}
</a> </a>
</td>
<td dir="rtl">
<span class="text-slate">{{ source.name_ar|default:"-" }}</span>
</td>
<td>
<span class="source-type-badge {{ source.source_type }}">
{% if source.source_type == 'internal' %}
<i data-lucide="building" class="w-3 h-3"></i>
{% elif source.source_type == 'external' %}
<i data-lucide="globe" class="w-3 h-3"></i>
{% elif source.source_type == 'partner' %}
<i data-lucide="handshake" class="w-3 h-3"></i>
{% elif source.source_type == 'government' %}
<i data-lucide="landmark" class="w-3 h-3"></i>
{% else %}
<i data-lucide="circle" class="w-3 h-3"></i>
{% endif %}
{{ source.get_source_type_display }}
</span>
</td>
<td>
<div class="flex flex-col gap-1">
<span class="usage-stat">
<i data-lucide="file-text" class="w-3 h-3"></i>
{{ source.total_complaints }} {% trans "complaints" %}
</span>
<span class="usage-stat">
<i data-lucide="help-circle" class="w-3 h-3"></i>
{{ source.total_inquiries }} {% trans "inquiries" %}
</span>
</div>
</td>
<td class="text-center">
{% if source.is_active %}
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-100 text-green-700 rounded-full text-xs font-bold">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
{% trans "Active" %}
</span>
{% else %}
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 text-slate-600 rounded-full text-xs font-bold">
<i data-lucide="x-circle" class="w-3.5 h-3.5"></i>
{% trans "Inactive" %}
</span>
{% endif %} {% endif %}
</td> </td>
<td class="text-center">
<div class="flex items-center justify-center gap-1.5">
<a href="{% url 'px_sources:source_detail' source.pk %}"
class="p-2 text-blue hover:bg-blue-50 rounded-lg transition" title="{% trans 'View' %}">
<i data-lucide="eye" class="w-4 h-4"></i>
</a>
{% if user.is_px_admin or user.is_hospital_admin %}
<a href="{% url 'px_sources:source_edit' source.pk %}"
class="p-2 text-navy hover:bg-navy/10 rounded-lg transition" title="{% trans 'Edit' %}">
<i data-lucide="edit" class="w-4 h-4"></i>
</a>
<a href="{% url 'px_sources:source_toggle_status' source.pk %}"
class="p-2 hover:bg-slate-100 rounded-lg transition"
title="{% if source.is_active %}{% trans 'Deactivate' %}{% else %}{% trans 'Activate' %}{% endif %}">
{% if source.is_active %}
<i data-lucide="pause" class="w-4 h-4 text-yellow-600"></i>
{% else %}
<i data-lucide="play" class="w-4 h-4 text-green-600"></i>
{% endif %}
</a>
{% if user.is_px_admin %}
<a href="{% url 'px_sources:source_delete' source.pk %}"
class="p-2 text-red-500 hover:bg-red-50 rounded-lg transition" title="{% trans 'Delete' %}">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</a>
{% endif %}
{% endif %}
</div>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5" class="text-center py-4"> <td colspan="7" class="py-12 text-center">
<p class="text-muted mb-2"> <div class="flex flex-col items-center">
<i class="bi bi-inbox fs-1"></i> <div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-4">
</p> <i data-lucide="database" class="w-8 h-8 text-slate-400"></i>
<p>{% trans "No sources found. Click 'Add Source' to create one." %}</p> </div>
<p class="text-slate font-medium">{% trans "No sources found" %}</p>
<p class="text-slate text-sm mt-1">{% trans "Add your first source to get started" %}</p>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -116,22 +316,24 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Apply filters button lucide.createIcons();
// Filter functionality
document.getElementById('apply-filters').addEventListener('click', function() { document.getElementById('apply-filters').addEventListener('click', function() {
const search = document.getElementById('search-input').value; const search = document.getElementById('search-input').value;
const isActive = document.getElementById('status-filter').value; const status = document.getElementById('status-filter').value;
let url = new URL(window.location.href); let url = new URL(window.location.href);
if (search) url.searchParams.set('search', search); url.searchParams.set('search', search);
else url.searchParams.delete('search'); if (status) {
url.searchParams.set('is_active', status);
if (isActive) url.searchParams.set('is_active', isActive); } else {
else url.searchParams.delete('is_active'); url.searchParams.delete('is_active');
}
window.location.href = url.toString(); window.location.href = url.toString();
}); });
// Enter key on search input // Enter key to search
document.getElementById('search-input').addEventListener('keypress', function(e) { document.getElementById('search-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
document.getElementById('apply-filters').click(); document.getElementById('apply-filters').click();

View File

@ -4,42 +4,53 @@
{% block title %}{% trans "My Complaints" %} - {{ source.name_en }}{% endblock %} {% block title %}{% trans "My Complaints" %} - {{ source.name_en }}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="p-6">
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<div> <div>
<h2 class="mb-1"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<i class="bi bi-exclamation-triangle-fill text-warning me-2"></i> <i data-lucide="alert-triangle" class="w-8 h-8 text-yellow-500"></i>
{% trans "My Complaints" %} {% trans "My Complaints" %}
<span class="badge bg-primary">{{ complaints_count }}</span> <span class="inline-flex items-center px-3 py-1 bg-navy text-white rounded-full text-sm font-medium">{{ complaints_count }}</span>
</h2> </h1>
<p class="text-muted mb-0"> <p class="text-slate mt-1">
{% trans "View all complaints from your source" %} {% trans "View all complaints from your source" %}
</p> </p>
</div> </div>
{% if source_user.can_create_complaints %} {% if source_user.can_create_complaints %}
<a href="{% url 'complaints:complaint_create' %}" class="btn btn-primary"> <a href="{% url 'complaints:complaint_create' %}"
<i class="bi bi-plus-circle me-1"></i> {% trans "Create Complaint" %} class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition shadow-lg shadow-navy/20">
<i data-lucide="plus-circle" class="w-4 h-4"></i>
{% trans "Create Complaint" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
<!-- Filter Panel --> <!-- Filter Panel -->
<div class="card mb-4"> <div class="bg-white rounded-xl shadow-sm border border-slate-200 mb-6">
<div class="card-body"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl">
<form method="get" class="row g-3"> <h2 class="text-sm font-semibold text-navy flex items-center gap-2">
<!-- Search --> <i data-lucide="filter" class="w-4 h-4 text-slate"></i>
<div class="col-md-4"> {% trans "Filters" %}
<label class="form-label">{% trans "Search" %}</label> </h2>
<input type="text" class="form-control" name="search" </div>
placeholder="{% trans 'Title, patient name...' %}" <div class="p-4">
value="{{ search|default:'' }}"> <form method="get" class="grid grid-cols-1 md:grid-cols-12 gap-4">
</div> <!-- Search -->
<div class="md:col-span-4">
<label class="block text-sm font-medium text-slate mb-1">{% trans "Search" %}</label>
<input type="text"
class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition"
name="search"
placeholder="{% trans 'Title, patient name...' %}"
value="{{ search|default:'' }}">
</div>
<!-- Status --> <!-- Status -->
<div class="col-md-2"> <div class="md:col-span-2">
<label class="form-label">{% trans "Status" %}</label> <label class="block text-sm font-medium text-slate mb-1">{% trans "Status" %}</label>
<select class="form-select" name="status"> <select class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition bg-white"
name="status">
<option value="">{% trans "All Statuses" %}</option> <option value="">{% trans "All Statuses" %}</option>
<option value="open" {% if status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option> <option value="open" {% if status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option>
<option value="in_progress" {% if status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option> <option value="in_progress" {% if status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
@ -49,9 +60,10 @@
</div> </div>
<!-- Priority --> <!-- Priority -->
<div class="col-md-2"> <div class="md:col-span-2">
<label class="form-label">{% trans "Priority" %}</label> <label class="block text-sm font-medium text-slate mb-1">{% trans "Priority" %}</label>
<select class="form-select" name="priority"> <select class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition bg-white"
name="priority">
<option value="">{% trans "All Priorities" %}</option> <option value="">{% trans "All Priorities" %}</option>
<option value="low" {% if priority_filter == 'low' %}selected{% endif %}>{% trans "Low" %}</option> <option value="low" {% if priority_filter == 'low' %}selected{% endif %}>{% trans "Low" %}</option>
<option value="medium" {% if priority_filter == 'medium' %}selected{% endif %}>{% trans "Medium" %}</option> <option value="medium" {% if priority_filter == 'medium' %}selected{% endif %}>{% trans "Medium" %}</option>
@ -60,9 +72,10 @@
</div> </div>
<!-- Category --> <!-- Category -->
<div class="col-md-2"> <div class="md:col-span-2">
<label class="form-label">{% trans "Category" %}</label> <label class="block text-sm font-medium text-slate mb-1">{% trans "Category" %}</label>
<select class="form-select" name="category"> <select class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition bg-white"
name="category">
<option value="">{% trans "All Categories" %}</option> <option value="">{% trans "All Categories" %}</option>
<option value="clinical_care" {% if category_filter == 'clinical_care' %}selected{% endif %}>{% trans "Clinical Care" %}</option> <option value="clinical_care" {% if category_filter == 'clinical_care' %}selected{% endif %}>{% trans "Clinical Care" %}</option>
<option value="staff_behavior" {% if category_filter == 'staff_behavior' %}selected{% endif %}>{% trans "Staff Behavior" %}</option> <option value="staff_behavior" {% if category_filter == 'staff_behavior' %}selected{% endif %}>{% trans "Staff Behavior" %}</option>
@ -75,14 +88,16 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="col-md-2 d-flex align-items-end"> <div class="md:col-span-2 flex items-end">
<div class="d-flex gap-2 w-100"> <div class="flex gap-2 w-full">
<button type="submit" class="btn btn-primary flex-grow-1"> <button type="submit"
<i class="bi bi-search me-1"></i> {% trans "Filter" %} class="inline-flex items-center justify-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition flex-grow">
<i data-lucide="search" class="w-4 h-4"></i>
{% trans "Filter" %}
</button> </button>
<a href="{% url 'px_sources:source_user_complaint_list' %}" <a href="{% url 'px_sources:source_user_complaint_list' %}"
class="btn btn-outline-secondary"> class="inline-flex items-center justify-center p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate">
<i class="bi bi-x-circle"></i> <i data-lucide="x-circle" class="w-4 h-4"></i>
</a> </a>
</div> </div>
</div> </div>
@ -91,117 +106,135 @@
</div> </div>
<!-- Complaints Table --> <!-- Complaints Table -->
<div class="card"> <div class="bg-white rounded-xl shadow-sm border border-slate-200">
<div class="card-body p-0"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl">
<div class="table-responsive"> <h2 class="text-lg font-semibold text-navy flex items-center gap-2">
<table class="table table-hover mb-0"> <i data-lucide="file-text" class="w-5 h-5 text-slate"></i>
<thead class="table-light"> {% trans "Complaints List" %}
<tr> </h2>
<th>{% trans "ID" %}</th> </div>
<th>{% trans "Title" %}</th> <div class="overflow-x-auto">
<th>{% trans "Patient" %}</th> <table class="w-full">
<th>{% trans "Category" %}</th> <thead>
<th>{% trans "Status" %}</th> <tr class="border-b border-slate-200">
<th>{% trans "Priority" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "ID" %}</th>
<th>{% trans "Assigned To" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Title" %}</th>
<th>{% trans "Created" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Patient" %}</th>
<th>{% trans "Actions" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Category" %}</th>
</tr> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Status" %}</th>
</thead> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Priority" %}</th>
<tbody> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Assigned To" %}</th>
{% for complaint in complaints %} <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Created" %}</th>
<tr> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Actions" %}</th>
<td><code>{{ complaint.id|slice:":8" }}</code></td> </tr>
<td>{{ complaint.title|truncatewords:8 }}</td> </thead>
<td> <tbody class="divide-y divide-slate-100">
{% if complaint.patient %} {% for complaint in complaints %}
<strong>{{ complaint.patient.get_full_name }}</strong><br> <tr class="hover:bg-slate-50 transition">
<small class="text-muted">{% trans "MRN" %}: {{ complaint.patient.mrn }}</small> <td class="py-3 px-4 text-sm font-mono text-slate">{{ complaint.id|slice:":8" }}</td>
{% else %} <td class="py-3 px-4">
<em class="text-muted">{% trans "Not specified" %}</em> <a href="{% url 'complaints:complaint_detail' complaint.pk %}"
{% endif %} class="text-navy font-semibold hover:text-blue transition">
</td> {{ complaint.title|truncatewords:8 }}
<td><span class="badge bg-secondary">{{ complaint.get_category_display }}</span></td> </a>
<td> </td>
{% if complaint.status == 'open' %} <td class="py-3 px-4">
<span class="badge bg-danger">{% trans "Open" %}</span> {% if complaint.patient %}
{% elif complaint.status == 'in_progress' %} <div class="font-medium text-slate">{{ complaint.patient.get_full_name }}</div>
<span class="badge bg-warning text-dark">{% trans "In Progress" %}</span> <div class="text-xs text-slate/70">{% trans "MRN" %}: {{ complaint.patient.mrn }}</div>
{% elif complaint.status == 'resolved' %} {% else %}
<span class="badge bg-success">{% trans "Resolved" %}</span> <span class="text-slate/60 italic">{% trans "Not specified" %}</span>
{% else %} {% endif %}
<span class="badge bg-secondary">{% trans "Closed" %}</span> </td>
{% endif %} <td class="py-3 px-4">
</td> <span class="inline-flex items-center px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">
<td> {{ complaint.get_category_display }}
{% if complaint.priority == 'high' %} </span>
<span class="badge bg-danger">{% trans "High" %}</span> </td>
{% elif complaint.priority == 'medium' %} <td class="py-3 px-4 text-center">
<span class="badge bg-warning text-dark">{% trans "Medium" %}</span> {% if complaint.status == 'open' %}
{% else %} <span class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">{% trans "Open" %}</span>
<span class="badge bg-success">{% trans "Low" %}</span> {% elif complaint.status == 'in_progress' %}
{% endif %} <span class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium">{% trans "In Progress" %}</span>
</td> {% elif complaint.status == 'resolved' %}
<td> <span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">{% trans "Resolved" %}</span>
{% if complaint.assigned_to %} {% else %}
{{ complaint.assigned_to.get_full_name }} <span class="inline-flex items-center px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">{% trans "Closed" %}</span>
{% else %} {% endif %}
<span class="text-muted"><em>{% trans "Unassigned" %}</em></span> </td>
{% endif %} <td class="py-3 px-4 text-center">
</td> {% if complaint.priority == 'high' %}
<td><small class="text-muted">{{ complaint.created_at|date:"Y-m-d" }}</small></td> <span class="inline-flex items-center px-2 py-1 bg-red-100 text-red-700 rounded text-xs font-medium">{% trans "High" %}</span>
<td> {% elif complaint.priority == 'medium' %}
<a href="{% url 'complaints:complaint_detail' complaint.pk %}" <span class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">{% trans "Medium" %}</span>
class="btn btn-sm btn-info" {% else %}
title="{% trans 'View' %}"> <span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">{% trans "Low" %}</span>
<i class="bi bi-eye"></i> {% endif %}
</a> </td>
</td> <td class="py-3 px-4 text-sm text-slate">
</tr> {% if complaint.assigned_to %}
{% empty %} {{ complaint.assigned_to.get_full_name }}
<tr> {% else %}
<td colspan="9" class="text-center py-5"> <span class="text-slate/60 italic">{% trans "Unassigned" %}</span>
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i> {% endif %}
<p class="text-muted mt-3"> </td>
{% trans "No complaints found for your source." %} <td class="py-3 px-4 text-sm text-slate">{{ complaint.created_at|date:"Y-m-d" }}</td>
</p> <td class="py-3 px-4 text-center">
{% if source_user.can_create_complaints %} <a href="{% url 'complaints:complaint_detail' complaint.pk %}"
<a href="{% url 'complaints:complaint_create' %}" class="btn btn-primary"> class="inline-flex items-center justify-center p-2 bg-blue text-white rounded-lg hover:bg-navy transition"
<i class="bi bi-plus-circle me-1"></i> {% trans "Create Complaint" %} title="{% trans 'View' %}">
</a> <i data-lucide="eye" class="w-4 h-4"></i>
{% endif %} </a>
</td> </td>
</tr> </tr>
{% endfor %} {% empty %}
</tbody> <tr>
</table> <td colspan="9" class="text-center py-12">
</div> <i data-lucide="inbox" class="w-16 h-16 mx-auto text-slate/30 mb-4"></i>
<p class="text-slate mb-4">{% trans "No complaints found for your source." %}</p>
{% if source_user.can_create_complaints %}
<a href="{% url 'complaints:complaint_create' %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition">
<i data-lucide="plus-circle" class="w-4 h-4"></i>
{% trans "Create Complaint" %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{% if complaints.has_other_pages %} {% if complaints.has_other_pages %}
<nav aria-label="Complaints pagination" class="mt-4"> <nav aria-label="Complaints pagination" class="mt-6">
<ul class="pagination justify-content-center"> <ul class="flex justify-center items-center gap-2">
{% if complaints.has_previous %} {% if complaints.has_previous %}
<li class="page-item"> <li>
<a class="page-link" href="?page=1&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-double-left"></i> href="?page=1&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}">
<i data-lucide="chevrons-left" class="w-4 h-4"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li>
<a class="page-link" href="?page={{ complaints.previous_page_number }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-left"></i> href="?page={{ complaints.previous_page_number }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% for num in complaints.paginator.page_range %} {% for num in complaints.paginator.page_range %}
{% if complaints.number == num %} {% if complaints.number == num %}
<li class="page-item active"><span class="page-link">{{ num }}</span></li> <li>
<span class="inline-flex items-center justify-center w-10 h-10 bg-navy text-white rounded-lg font-medium">{{ num }}</span>
</li>
{% elif num > complaints.number|add:'-3' and num < complaints.number|add:'3' %} {% elif num > complaints.number|add:'-3' and num < complaints.number|add:'3' %}
<li class="page-item"> <li>
<a class="page-link" href="?page={{ num }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
href="?page={{ num }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}">
{{ num }} {{ num }}
</a> </a>
</li> </li>
@ -209,14 +242,16 @@
{% endfor %} {% endfor %}
{% if complaints.has_next %} {% if complaints.has_next %}
<li class="page-item"> <li>
<a class="page-link" href="?page={{ complaints.next_page_number }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-right"></i> href="?page={{ complaints.next_page_number }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}">
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li>
<a class="page-link" href="?page={{ complaints.paginator.num_pages }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-double-right"></i> href="?page={{ complaints.paginator.num_pages }}&search={{ search }}&status={{ status_filter }}&priority={{ priority_filter }}&category={{ category_filter }}">
<i data-lucide="chevrons-right" class="w-4 h-4"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}
@ -224,4 +259,10 @@
</nav> </nav>
{% endif %} {% endif %}
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -4,117 +4,139 @@
{% block title %}{% trans "Delete Source User" %} - {{ source.name_en }}{% endblock %} {% block title %}{% trans "Delete Source User" %} - {{ source.name_en }}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="p-6">
<!-- Breadcrumb -->
<nav class="mb-4">
<ol class="flex items-center gap-2 text-sm text-slate">
<li><a href="{% url 'px_sources:source_list' %}" class="text-blue hover:text-navy">{% trans "PX Sources" %}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4"></i></li>
<li><a href="{% url 'px_sources:source_detail' source.pk %}" class="text-blue hover:text-navy">{{ source.name_en }}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4"></i></li>
<li class="text-navy font-semibold">{% trans "Delete Source User" %}</li>
</ol>
</nav>
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<div> <div>
<nav aria-label="breadcrumb"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<ol class="breadcrumb mb-2"> <i data-lucide="alert-triangle" class="w-8 h-8 text-red-500"></i>
<li class="breadcrumb-item">
<a href="{% url 'px_sources:source_list' %}">{% trans "PX Sources" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'px_sources:source_detail' source.pk %}">{{ source.name_en }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
{% trans "Delete Source User" %}
</li>
</ol>
</nav>
<h2 class="mb-1">
<i class="bi bi-exclamation-triangle text-danger me-2"></i>
{% trans "Delete Source User" %} {% trans "Delete Source User" %}
</h2> </h1>
<p class="text-muted mb-0"> <p class="text-slate mt-1">{{ source.name_en }}</p>
{{ source.name_en }}
</p>
</div> </div>
<div> <div>
<a href="{% url 'px_sources:source_detail' source.pk %}" class="btn btn-outline-secondary"> <a href="{% url 'px_sources:source_detail' source.pk %}"
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to Source" %} class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Source" %}
</a> </a>
</div> </div>
</div> </div>
<!-- Confirmation Card --> <!-- Confirmation Card -->
<div class="row"> <div class="bg-white rounded-xl shadow-sm border border-red-200">
<div class="col-12"> <div class="p-4 border-b border-red-200 bg-red-500 rounded-t-xl">
<div class="card border-danger"> <h2 class="text-lg font-semibold text-white flex items-center gap-2">
<div class="card-header bg-danger text-white"> <i data-lucide="alert-triangle" class="w-5 h-5"></i>
<h5 class="card-title mb-0"> {% trans "Confirm Deletion" %}
<i class="bi bi-exclamation-triangle me-2"></i> </h2>
{% trans "Confirm Deletion" %} </div>
</h5> <div class="p-6">
</div> <!-- Warning Alert -->
<div class="card-body"> <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div class="alert alert-danger"> <div class="flex items-center gap-3">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <i data-lucide="alert-triangle" class="w-5 h-5 text-red-600"></i>
<strong>{% trans "Warning:" %}</strong> {% trans "This action cannot be undone!" %} <strong class="text-red-800">{% trans "Warning:" %}</strong>
</div> <span class="text-red-700">{% trans "This action cannot be undone!" %}</span>
<div class="mb-4">
<p>{% trans "Are you sure you want to remove the following source user?" %}</p>
<div class="card bg-light">
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-3">{% trans "User" %}:</dt>
<dd class="col-sm-9">
<strong>{{ source_user.user.email }}</strong>
{% if source_user.user.get_full_name %}
<br><small class="text-muted">{{ source_user.user.get_full_name }}</small>
{% endif %}
</dd>
<dt class="col-sm-3">{% trans "Source" %}:</dt>
<dd class="col-sm-9">{{ source.name_en }}</dd>
<dt class="col-sm-3">{% trans "Status" %}:</dt>
<dd class="col-sm-9">
{% if source_user.is_active %}
<span class="badge bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
{% endif %}
</dd>
<dt class="col-sm-3">{% trans "Permissions" %}:</dt>
<dd class="col-sm-9">
{% if source_user.can_create_complaints %}
<span class="badge bg-primary">{% trans "Complaints" %}</span>
{% endif %}
{% if source_user.can_create_inquiries %}
<span class="badge bg-info">{% trans "Inquiries" %}</span>
{% endif %}
{% if not source_user.can_create_complaints and not source_user.can_create_inquiries %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</dd>
</dl>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{% trans "The user will lose access to the source dashboard and will not be able to create complaints or inquiries from this source." %}
</div>
<form method="POST" novalidate>
{% csrf_token %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash me-1"></i> {% trans "Yes, Delete" %}
</button>
<a href="{% url 'px_sources:source_detail' source.pk %}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg me-1"></i> {% trans "Cancel" %}
</a>
</div>
</form>
</div> </div>
</div> </div>
<!-- User Details -->
<div class="mb-6">
<p class="text-navy mb-4">{% trans "Are you sure you want to remove the following source user?" %}</p>
<div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
<dl class="space-y-3 mb-0">
<div class="flex">
<dt class="w-32 text-sm font-semibold text-navy">{% trans "User" %}:</dt>
<dd class="flex-1">
<strong class="text-navy">{{ source_user.user.email }}</strong>
{% if source_user.user.get_full_name %}
<br><small class="text-slate">{{ source_user.user.get_full_name }}</small>
{% endif %}
</dd>
</div>
<div class="flex">
<dt class="w-32 text-sm font-semibold text-navy">{% trans "Source" %}:</dt>
<dd class="flex-1 text-navy">{{ source.name_en }}</dd>
</div>
<div class="flex">
<dt class="w-32 text-sm font-semibold text-navy">{% trans "Status" %}:</dt>
<dd class="flex-1">
{% if source_user.is_active %}
<span class="inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-700 rounded-full text-xs font-semibold">
<i data-lucide="check-circle" class="w-3 h-3"></i>
{% trans "Active" %}
</span>
{% else %}
<span class="inline-flex items-center gap-1 px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full text-xs font-semibold">
<i data-lucide="x-circle" class="w-3 h-3"></i>
{% trans "Inactive" %}
</span>
{% endif %}
</dd>
</div>
<div class="flex">
<dt class="w-32 text-sm font-semibold text-navy">{% trans "Permissions" %}:</dt>
<dd class="flex-1">
{% if source_user.can_create_complaints %}
<span class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium mr-1">{% trans "Complaints" %}</span>
{% endif %}
{% if source_user.can_create_inquiries %}
<span class="inline-flex items-center px-2 py-1 bg-cyan-100 text-cyan-700 rounded text-xs font-medium">{% trans "Inquiries" %}</span>
{% endif %}
{% if not source_user.can_create_complaints and not source_user.can_create_inquiries %}
<span class="text-slate text-sm">{% trans "None" %}</span>
{% endif %}
</dd>
</div>
</dl>
</div>
</div>
<!-- Info Alert -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-center gap-3">
<i data-lucide="info" class="w-5 h-5 text-blue-600"></i>
<span class="text-blue-700 text-sm">{% trans "The user will lose access to the source dashboard and will not be able to create complaints or inquiries from this source." %}</span>
</div>
</div>
<!-- Action Buttons -->
<form method="POST" novalidate>
{% csrf_token %}
<div class="flex gap-3">
<button type="submit"
class="inline-flex items-center gap-2 px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition shadow-lg shadow-red-500/20">
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Yes, Delete" %}
</button>
<a href="{% url 'px_sources:source_detail' source.pk %}"
class="inline-flex items-center gap-2 px-6 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
<i data-lucide="x" class="w-4 h-4"></i> {% trans "Cancel" %}
</a>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -1,212 +1,205 @@
{% extends "layouts/source_user_base.html" %} {% extends "layouts/source_user_base.html" %}
{% load i18n action_icons %} {% load i18n %}
{% block title %}{% trans "Source User Dashboard" %} - {{ source.name_en }}{% endblock %} {% block title %}{% trans "Source User Dashboard" %} - {{ source.name_en }} - PX360{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="p-6">
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<div> <div>
<h2 class="mb-1"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<i class="bi bi-lightning-fill text-warning me-2"></i> <i data-lucide="radio" class="w-8 h-8 text-blue"></i>
{{ source.name_en }} {{ source.name_en }}
</h2> </h1>
<p class="text-muted mb-0"> <p class="text-slate mt-1">
{% trans "Welcome" %}, {{ request.user.get_full_name }}! {% trans "Welcome" %}, {{ request.user.get_full_name }}!
{% trans "You're managing feedback from this source." %} {% trans "You're managing feedback from this source." %}
</p> </p>
</div> </div>
<div class="flex gap-2">
<a href="{% url 'px_sources:source_user_complaint_list' %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition shadow-lg shadow-navy/20">
<i data-lucide="file-text" class="w-4 h-4"></i>
{% trans "All Complaints" %}
</a>
<a href="{% url 'px_sources:source_user_inquiry_list' %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue text-white rounded-lg hover:bg-navy transition">
<i data-lucide="help-circle" class="w-4 h-4"></i>
{% trans "All Inquiries" %}
</a>
</div>
</div> </div>
<!-- Statistics Cards --> <!-- Statistics Cards -->
<div class="row mb-4"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="col-md-3"> <div class="bg-gradient-to-br from-navy to-blue rounded-xl p-4 text-white shadow-lg shadow-navy/20">
<div class="card bg-primary text-white"> <div class="flex items-center justify-between">
<div class="card-body"> <div>
<h6 class="card-title">{% trans "Total Complaints" %}</h6> <p class="text-white/80 text-sm">{% trans "Total Complaints" %}</p>
<h2 class="mb-0">{{ total_complaints }}</h2> <p class="text-2xl font-bold">{{ total_complaints }}</p>
</div> </div>
<i data-lucide="file-text" class="w-10 h-10 text-white/30"></i>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="bg-gradient-to-br from-yellow-500 to-orange-500 rounded-xl p-4 text-white shadow-lg shadow-yellow/20">
<div class="card bg-warning text-dark"> <div class="flex items-center justify-between">
<div class="card-body"> <div>
<h6 class="card-title">{% trans "Open Complaints" %}</h6> <p class="text-white/80 text-sm">{% trans "Open Complaints" %}</p>
<h2 class="mb-0">{{ open_complaints }}</h2> <p class="text-2xl font-bold">{{ open_complaints }}</p>
</div> </div>
<i data-lucide="alert-circle" class="w-10 h-10 text-white/30"></i>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="bg-gradient-to-br from-cyan-500 to-blue-500 rounded-xl p-4 text-white shadow-lg shadow-cyan/20">
<div class="card bg-info text-white"> <div class="flex items-center justify-between">
<div class="card-body"> <div>
<h6 class="card-title">{% trans "Total Inquiries" %}</h6> <p class="text-white/80 text-sm">{% trans "Total Inquiries" %}</p>
<h2 class="mb-0">{{ total_inquiries }}</h2> <p class="text-2xl font-bold">{{ total_inquiries }}</p>
</div> </div>
<i data-lucide="help-circle" class="w-10 h-10 text-white/30"></i>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="bg-gradient-to-br from-slate-500 to-slate-600 rounded-xl p-4 text-white shadow-lg shadow-slate/20">
<div class="card bg-secondary text-white"> <div class="flex items-center justify-between">
<div class="card-body"> <div>
<h6 class="card-title">{% trans "Open Inquiries" %}</h6> <p class="text-white/80 text-sm">{% trans "Open Inquiries" %}</p>
<h2 class="mb-0">{{ open_inquiries }}</h2> <p class="text-2xl font-bold">{{ open_inquiries }}</p>
</div> </div>
<i data-lucide="message-circle" class="w-10 h-10 text-white/30"></i>
</div> </div>
</div> </div>
</div> </div>
<!-- Recent Complaints -->
<!-- Complaints Table --> <div class="bg-white rounded-xl shadow-sm border border-slate-200 mb-6">
<div class="row mb-4"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl flex justify-between items-center">
<div class="col-12"> <h2 class="text-lg font-semibold text-navy flex items-center gap-2">
<div class="card"> <i data-lucide="file-text" class="w-5 h-5 text-slate"></i>
<div class="card-header"> {% trans "Recent Complaints" %} ({{ complaints|length }})
<h5 class="card-title mb-0"> </h2>
{% action_icon 'filter' %} {% trans "Recent Complaints" %} ({{ complaints|length }}) </div>
</h5> <div class="p-4">
</div> {% if complaints %}
<div class="card-body"> <div class="overflow-x-auto">
<div class="table-responsive"> <table class="w-full">
<table class="table table-hover"> <thead>
<thead class="table-light"> <tr class="border-b border-slate-200">
<tr> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "ID" %}</th>
<th>{% trans "ID" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Title" %}</th>
<th>{% trans "Title" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Patient" %}</th>
<th>{% trans "Patient" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Category" %}</th>
<th>{% trans "Category" %}</th> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Status" %}</th>
<th>{% trans "Status" %}</th> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Priority" %}</th>
<th>{% trans "Priority" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Created" %}</th>
<th>{% trans "Created" %}</th> </tr>
<th>{% trans "Actions" %}</th> </thead>
</tr> <tbody class="divide-y divide-slate-100">
</thead> {% for complaint in complaints|slice:":10" %}
<tbody> <tr class="hover:bg-slate-50 transition">
{% for complaint in complaints %} <td class="py-3 px-4 text-sm font-mono text-slate">{{ complaint.reference_number|default:complaint.id|truncatechars:12 }}</td>
<tr> <td class="py-3 px-4">
<td><code>{{ complaint.id|slice:":8" }}</code></td> <a href="{% url 'complaints:complaint_detail' complaint.pk %}"
<td>{{ complaint.title|truncatewords:8 }}</td> class="text-navy font-semibold hover:text-blue transition">
<td>{{ complaint.patient.get_full_name }}</td> {{ complaint.title|truncatechars:40 }}
<td>{{ complaint.get_category_display }}</td> </a>
<td> </td>
{% if complaint.status == 'open' %} <td class="py-3 px-4 text-sm text-slate">{{ complaint.patient.get_full_name|default:"-" }}</td>
<span class="badge bg-danger">{% trans "Open" %}</span> <td class="py-3 px-4 text-sm text-slate">{{ complaint.category|default:"-" }}</td>
{% elif complaint.status == 'in_progress' %} <td class="py-3 px-4 text-center">
<span class="badge bg-warning text-dark">{% trans "In Progress" %}</span> {% if complaint.status == 'open' %}
{% elif complaint.status == 'resolved' %} <span class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">{% trans "Open" %}</span>
<span class="badge bg-success">{% trans "Resolved" %}</span> {% elif complaint.status == 'in_progress' %}
{% else %} <span class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium">{% trans "In Progress" %}</span>
<span class="badge bg-secondary">{% trans "Closed" %}</span> {% elif complaint.status == 'resolved' %}
{% endif %} <span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">{% trans "Resolved" %}</span>
</td> {% else %}
<td> <span class="inline-flex items-center px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">{{ complaint.get_status_display }}</span>
{% if complaint.priority == 'high' %} {% endif %}
<span class="badge bg-danger">{% trans "High" %}</span> </td>
{% elif complaint.priority == 'medium' %} <td class="py-3 px-4 text-center">
<span class="badge bg-warning text-dark">{% trans "Medium" %}</span> {% if complaint.priority == 'high' %}
{% else %} <span class="inline-flex items-center px-2 py-1 bg-red-100 text-red-700 rounded text-xs font-medium">{% trans "High" %}</span>
<span class="badge bg-success">{% trans "Low" %}</span> {% elif complaint.priority == 'medium' %}
{% endif %} <span class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">{% trans "Medium" %}</span>
</td> {% else %}
<td>{{ complaint.created_at|date:"Y-m-d" }}</td> <span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">{% trans "Low" %}</span>
<td> {% endif %}
<a href="{% url 'complaints:complaint_detail' complaint.pk %}" </td>
class="btn btn-sm btn-info" <td class="py-3 px-4 text-sm text-slate">{{ complaint.created_at|date:"Y-m-d" }}</td>
title="{% trans 'View' %}"> </tr>
{% action_icon 'view' %} {% endfor %}
</a> </tbody>
</td> </table>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-4">
<p class="text-muted mb-2">
<i class="bi bi-inbox fs-1"></i>
</p>
<p>{% trans "No complaints found for this source." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div>
{% else %}
<div class="text-center py-8 text-slate">
<i data-lucide="file-text" class="w-12 h-12 mx-auto mb-3 opacity-30"></i>
<p>{% trans "No complaints found from this source." %}</p>
</div>
{% endif %}
</div> </div>
</div> </div>
<!-- Inquiries Table --> <!-- Recent Inquiries -->
<div class="row"> <div class="bg-white rounded-xl shadow-sm border border-slate-200">
<div class="col-12"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl flex justify-between items-center">
<div class="card"> <h2 class="text-lg font-semibold text-navy flex items-center gap-2">
<div class="card-header"> <i data-lucide="help-circle" class="w-5 h-5 text-slate"></i>
<h5 class="card-title mb-0"> {% trans "Recent Inquiries" %} ({{ inquiries|length }})
{% action_icon 'filter' %} {% trans "Recent Inquiries" %} ({{ inquiries|length }}) </h2>
</h5> </div>
</div> <div class="p-4">
<div class="card-body"> {% if inquiries %}
<div class="table-responsive"> <div class="overflow-x-auto">
<table class="table table-hover"> <table class="w-full">
<thead class="table-light"> <thead>
<tr> <tr class="border-b border-slate-200">
<th>{% trans "ID" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "ID" %}</th>
<th>{% trans "Subject" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Subject" %}</th>
<th>{% trans "Patient" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "From" %}</th>
<th>{% trans "Category" %}</th> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Status" %}</th>
<th>{% trans "Status" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Created" %}</th>
<th>{% trans "Created" %}</th> </tr>
<th>{% trans "Actions" %}</th> </thead>
</tr> <tbody class="divide-y divide-slate-100">
</thead> {% for inquiry in inquiries|slice:":10" %}
<tbody> <tr class="hover:bg-slate-50 transition">
{% for inquiry in inquiries %} <td class="py-3 px-4 text-sm font-mono text-slate">{{ inquiry.reference_number|default:inquiry.id|truncatechars:12 }}</td>
<tr> <td class="py-3 px-4">
<td><code>{{ inquiry.id|slice:":8" }}</code></td> <span class="text-navy font-medium">{{ inquiry.subject|truncatechars:40 }}</span>
<td>{{ inquiry.subject|truncatewords:8 }}</td> </td>
<td> <td class="py-3 px-4 text-sm text-slate">{{ inquiry.name }}</td>
{% if inquiry.patient %} <td class="py-3 px-4 text-center">
{{ inquiry.patient.get_full_name }} {% if inquiry.status == 'open' %}
{% else %} <span class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">{% trans "Open" %}</span>
{{ inquiry.contact_name|default:"-" }} {% elif inquiry.status == 'in_progress' %}
{% endif %} <span class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium">{% trans "In Progress" %}</span>
</td> {% elif inquiry.status == 'resolved' %}
<td>{{ inquiry.get_category_display }}</td> <span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">{% trans "Resolved" %}</span>
<td> {% else %}
{% if inquiry.status == 'open' %} <span class="inline-flex items-center px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">{{ inquiry.get_status_display }}</span>
<span class="badge bg-danger">{% trans "Open" %}</span> {% endif %}
{% elif inquiry.status == 'in_progress' %} </td>
<span class="badge bg-warning text-dark">{% trans "In Progress" %}</span> <td class="py-3 px-4 text-sm text-slate">{{ inquiry.created_at|date:"Y-m-d" }}</td>
{% elif inquiry.status == 'resolved' %} </tr>
<span class="badge bg-success">{% trans "Resolved" %}</span> {% endfor %}
{% else %} </tbody>
<span class="badge bg-secondary">{% trans "Closed" %}</span> </table>
{% endif %}
</td>
<td>{{ inquiry.created_at|date:"Y-m-d" }}</td>
<td>
<a href="{% url 'complaints:inquiry_detail' inquiry.pk %}"
class="btn btn-sm btn-info"
title="{% trans 'View' %}">
{% action_icon 'view' %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-4">
<p class="text-muted mb-2">
<i class="bi bi-inbox fs-1"></i>
</p>
<p>{% trans "No inquiries found for this source." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div>
{% else %}
<div class="text-center py-8 text-slate">
<i data-lucide="help-circle" class="w-12 h-12 mx-auto mb-3 opacity-30"></i>
<p>{% trans "No inquiries found from this source." %}</p>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -4,142 +4,147 @@
{% block title %}{% if source_user %}{% trans "Edit Source User" %}{% else %}{% trans "Create Source User" %}{% endif %} - {{ source.name_en }}{% endblock %} {% block title %}{% if source_user %}{% trans "Edit Source User" %}{% else %}{% trans "Create Source User" %}{% endif %} - {{ source.name_en }}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="p-6">
<!-- Breadcrumb -->
<nav class="mb-4">
<ol class="flex items-center gap-2 text-sm text-slate">
<li><a href="{% url 'px_sources:source_list' %}" class="text-blue hover:text-navy">{% trans "PX Sources" %}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4"></i></li>
<li><a href="{% url 'px_sources:source_detail' source.pk %}" class="text-blue hover:text-navy">{{ source.name_en }}</a></li>
<li><i data-lucide="chevron-right" class="w-4 h-4"></i></li>
<li class="text-navy font-semibold">
{% if source_user %}{% trans "Edit Source User" %}{% else %}{% trans "Create Source User" %}{% endif %}
</li>
</ol>
</nav>
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<div> <div>
<nav aria-label="breadcrumb"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<ol class="breadcrumb mb-2">
<li class="breadcrumb-item">
<a href="{% url 'px_sources:source_list' %}">{% trans "PX Sources" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'px_sources:source_detail' source.pk %}">{{ source.name_en }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
{% if source_user %}{% trans "Edit Source User" %}{% else %}{% trans "Create Source User" %}{% endif %}
</li>
</ol>
</nav>
<h2 class="mb-1">
{% if source_user %} {% if source_user %}
<i class="bi bi-person-gear me-2"></i>{% trans "Edit Source User" %} <i data-lucide="user-cog" class="w-8 h-8 text-blue"></i>{% trans "Edit Source User" %}
{% else %} {% else %}
<i class="bi bi-person-plus me-2"></i>{% trans "Create Source User" %} <i data-lucide="user-plus" class="w-8 h-8 text-blue"></i>{% trans "Create Source User" %}
{% endif %} {% endif %}
</h2> </h1>
<p class="text-muted mb-0"> <p class="text-slate mt-1">{{ source.name_en }}</p>
{{ source.name_en }}
</p>
</div> </div>
<div> <div>
<a href="{% url 'px_sources:source_detail' source.pk %}" class="btn btn-outline-secondary"> <a href="{% url 'px_sources:source_detail' source.pk %}"
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to Source" %} class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Source" %}
</a> </a>
</div> </div>
</div> </div>
<!-- Form Card --> <!-- Form Card -->
<div class="row"> <div class="bg-white rounded-xl shadow-sm border border-slate-200 max-w-3xl">
<div class="col-12"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl">
<div class="card"> <h2 class="text-lg font-semibold text-navy flex items-center gap-2">
<div class="card-header"> <i data-lucide="settings" class="w-5 h-5 text-slate"></i>
<h5 class="card-title mb-0"> {% trans "Source User Details" %}
<i class="bi bi-gear me-2"></i>{% trans "Source User Details" %} </h2>
</h5> </div>
<div class="p-6">
<form method="POST" novalidate>
{% csrf_token %}
{% if not source_user %}
<!-- User Selection (only for new source users) -->
<div class="mb-4">
<label for="id_user" class="block text-sm font-semibold text-navy mb-2">
{% trans "User" %} <span class="text-red-500">*</span>
</label>
<select name="user" id="id_user" required
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:border-blue focus:ring-2 focus:ring-blue/20 bg-white">
<option value="">{% trans "Select a user" %}</option>
{% for user in available_users %}
<option value="{{ user.id }}" {% if form.user.value == user.id %}selected{% endif %}>
{{ user.email }} {% if user.get_full_name %}({{ user.get_full_name }}){% endif %}
</option>
{% endfor %}
</select>
<p class="text-slate text-sm mt-1">
{% trans "Select a user to assign as source user. A user can only manage one source." %}
</p>
</div> </div>
<div class="card-body"> {% else %}
<form method="POST" novalidate> <!-- User Display (for editing) -->
{% csrf_token %} <div class="mb-4">
<label class="block text-sm font-semibold text-navy mb-2">{% trans "User" %}</label>
{% if not source_user %} <input type="text"
<!-- User Selection (only for new source users) --> value="{{ source_user.user.email }} {% if source_user.user.get_full_name %}({{ source_user.user.get_full_name }}){% endif %}"
<div class="row mb-3"> readonly
<div class="col-md-6"> class="w-full px-4 py-2 border border-slate-200 rounded-lg bg-slate-100 text-slate-600 cursor-not-allowed">
<label for="id_user" class="form-label">{% trans "User" %} <span class="text-danger">*</span></label>
<select name="user" id="id_user" class="form-select" required>
<option value="">{% trans "Select a user" %}</option>
{% for user in available_users %}
<option value="{{ user.id }}" {% if form.user.value == user.id %}selected{% endif %}>
{{ user.email }} {% if user.get_full_name %}({{ user.get_full_name }}){% endif %}
</option>
{% endfor %}
</select>
<div class="form-text">
{% trans "Select a user to assign as source user. A user can only manage one source." %}
</div>
</div>
</div>
{% else %}
<!-- User Display (for editing) -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">{% trans "User" %}</label>
<input type="text" class="form-control" value="{{ source_user.user.email }} {% if source_user.user.get_full_name %}({{ source_user.user.get_full_name }}){% endif %}" readonly>
</div>
</div>
{% endif %}
<!-- Status -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">{% trans "Status" %}</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="id_is_active" {% if source_user.is_active|default:True %}checked{% endif %}>
<label class="form-check-label" for="id_is_active">
{% trans "Active" %}
</label>
</div>
<div class="form-text">
{% trans "Inactive users will not be able to access their dashboard." %}
</div>
</div>
</div>
<hr>
<!-- Permissions -->
<h5 class="mb-3">{% trans "Permissions" %}</h5>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="can_create_complaints" id="id_can_create_complaints" {% if source_user.can_create_complaints|default:True %}checked{% endif %}>
<label class="form-check-label" for="id_can_create_complaints">
{% trans "Can create complaints" %}
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="can_create_inquiries" id="id_can_create_inquiries" {% if source_user.can_create_inquiries|default:True %}checked{% endif %}>
<label class="form-check-label" for="id_can_create_inquiries">
{% trans "Can create inquiries" %}
</label>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{% trans "Permissions control what the source user can do in their dashboard. Uncheck to restrict access." %}
</div>
<!-- Submit Buttons -->
<hr>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i> {% trans "Save" %}
</button>
<a href="{% url 'px_sources:source_detail' source.pk %}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg me-1"></i> {% trans "Cancel" %}
</a>
</div>
</form>
</div> </div>
</div> {% endif %}
<!-- Status -->
<div class="mb-6">
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Status" %}</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="is_active" id="id_is_active"
{% if source_user.is_active|default:True %}checked{% endif %}
class="w-5 h-5 text-navy border-slate-300 rounded focus:ring-blue">
<span class="text-navy font-medium">{% trans "Active" %}</span>
</label>
<p class="text-slate text-sm mt-1">
{% trans "Inactive users will not be able to access their dashboard." %}
</p>
</div>
<div class="border-t border-slate-200 my-6"></div>
<!-- Permissions -->
<h5 class="text-lg font-semibold text-navy mb-4">{% trans "Permissions" %}</h5>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="can_create_complaints" id="id_can_create_complaints"
{% if source_user.can_create_complaints|default:True %}checked{% endif %}
class="w-5 h-5 text-navy border-slate-300 rounded focus:ring-blue">
<span class="text-navy">{% trans "Can create complaints" %}</span>
</label>
</div>
<div>
<label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="can_create_inquiries" id="id_can_create_inquiries"
{% if source_user.can_create_inquiries|default:True %}checked{% endif %}
class="w-5 h-5 text-navy border-slate-300 rounded focus:ring-blue">
<span class="text-navy">{% trans "Can create inquiries" %}</span>
</label>
</div>
</div>
<!-- Info Alert -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-center gap-3">
<i data-lucide="info" class="w-5 h-5 text-blue-600"></i>
<span class="text-blue-700 text-sm">{% trans "Permissions control what the source user can do in their dashboard. Uncheck to restrict access." %}</span>
</div>
</div>
<!-- Submit Buttons -->
<div class="border-t border-slate-200 pt-4 flex gap-3">
<button type="submit"
class="inline-flex items-center gap-2 px-6 py-2 bg-navy text-white rounded-lg hover:bg-blue transition shadow-lg shadow-navy/20">
<i data-lucide="check" class="w-4 h-4"></i> {% trans "Save" %}
</button>
<a href="{% url 'px_sources:source_detail' source.pk %}"
class="inline-flex items-center gap-2 px-6 py-2 border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition">
<i data-lucide="x" class="w-4 h-4"></i> {% trans "Cancel" %}
</a>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -4,42 +4,53 @@
{% block title %}{% trans "My Inquiries" %} - {{ source.name_en }}{% endblock %} {% block title %}{% trans "My Inquiries" %} - {{ source.name_en }}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="p-6">
<!-- Page Header --> <!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<div> <div>
<h2 class="mb-1"> <h1 class="text-2xl font-bold text-navy flex items-center gap-3">
<i class="bi bi-question-circle-fill text-info me-2"></i> <i data-lucide="help-circle" class="w-8 h-8 text-cyan-500"></i>
{% trans "My Inquiries" %} {% trans "My Inquiries" %}
<span class="badge bg-info">{{ inquiries_count }}</span> <span class="inline-flex items-center px-3 py-1 bg-blue text-white rounded-full text-sm font-medium">{{ inquiries_count }}</span>
</h2> </h1>
<p class="text-muted mb-0"> <p class="text-slate mt-1">
{% trans "View all inquiries from your source" %} {% trans "View all inquiries from your source" %}
</p> </p>
</div> </div>
{% if source_user.can_create_inquiries %} {% if source_user.can_create_inquiries %}
<a href="{% url 'complaints:inquiry_create' %}" class="btn btn-primary"> <a href="{% url 'complaints:inquiry_create' %}"
<i class="bi bi-plus-circle me-1"></i> {% trans "Create Inquiry" %} class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition shadow-lg shadow-navy/20">
<i data-lucide="plus-circle" class="w-4 h-4"></i>
{% trans "Create Inquiry" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
<!-- Filter Panel --> <!-- Filter Panel -->
<div class="card mb-4"> <div class="bg-white rounded-xl shadow-sm border border-slate-200 mb-6">
<div class="card-body"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl">
<form method="get" class="row g-3"> <h2 class="text-sm font-semibold text-navy flex items-center gap-2">
<!-- Search --> <i data-lucide="filter" class="w-4 h-4 text-slate"></i>
<div class="col-md-5"> {% trans "Filters" %}
<label class="form-label">{% trans "Search" %}</label> </h2>
<input type="text" class="form-control" name="search" </div>
placeholder="{% trans 'Subject, contact name...' %}" <div class="p-4">
value="{{ search|default:'' }}"> <form method="get" class="grid grid-cols-1 md:grid-cols-12 gap-4">
</div> <!-- Search -->
<div class="md:col-span-5">
<label class="block text-sm font-medium text-slate mb-1">{% trans "Search" %}</label>
<input type="text"
class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition"
name="search"
placeholder="{% trans 'Subject, contact name...' %}"
value="{{ search|default:'' }}">
</div>
<!-- Status --> <!-- Status -->
<div class="col-md-3"> <div class="md:col-span-3">
<label class="form-label">{% trans "Status" %}</label> <label class="block text-sm font-medium text-slate mb-1">{% trans "Status" %}</label>
<select class="form-select" name="status"> <select class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition bg-white"
name="status">
<option value="">{% trans "All Statuses" %}</option> <option value="">{% trans "All Statuses" %}</option>
<option value="open" {% if status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option> <option value="open" {% if status_filter == 'open' %}selected{% endif %}>{% trans "Open" %}</option>
<option value="in_progress" {% if status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option> <option value="in_progress" {% if status_filter == 'in_progress' %}selected{% endif %}>{% trans "In Progress" %}</option>
@ -49,9 +60,10 @@
</div> </div>
<!-- Category --> <!-- Category -->
<div class="col-md-2"> <div class="md:col-span-2">
<label class="form-label">{% trans "Category" %}</label> <label class="block text-sm font-medium text-slate mb-1">{% trans "Category" %}</label>
<select class="form-select" name="category"> <select class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue focus:border-blue outline-none transition bg-white"
name="category">
<option value="">{% trans "All Categories" %}</option> <option value="">{% trans "All Categories" %}</option>
<option value="clinical_care" {% if category_filter == 'clinical_care' %}selected{% endif %}>{% trans "Clinical Care" %}</option> <option value="clinical_care" {% if category_filter == 'clinical_care' %}selected{% endif %}>{% trans "Clinical Care" %}</option>
<option value="staff_behavior" {% if category_filter == 'staff_behavior' %}selected{% endif %}>{% trans "Staff Behavior" %}</option> <option value="staff_behavior" {% if category_filter == 'staff_behavior' %}selected{% endif %}>{% trans "Staff Behavior" %}</option>
@ -64,14 +76,16 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="col-md-2 d-flex align-items-end"> <div class="md:col-span-2 flex items-end">
<div class="d-flex gap-2 w-100"> <div class="flex gap-2 w-full">
<button type="submit" class="btn btn-primary flex-grow-1"> <button type="submit"
<i class="bi bi-search me-1"></i> {% trans "Filter" %} class="inline-flex items-center justify-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition flex-grow">
<i data-lucide="search" class="w-4 h-4"></i>
{% trans "Filter" %}
</button> </button>
<a href="{% url 'px_sources:source_user_inquiry_list' %}" <a href="{% url 'px_sources:source_user_inquiry_list' %}"
class="btn btn-outline-secondary"> class="inline-flex items-center justify-center p-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate">
<i class="bi bi-x-circle"></i> <i data-lucide="x-circle" class="w-4 h-4"></i>
</a> </a>
</div> </div>
</div> </div>
@ -80,108 +94,126 @@
</div> </div>
<!-- Inquiries Table --> <!-- Inquiries Table -->
<div class="card"> <div class="bg-white rounded-xl shadow-sm border border-slate-200">
<div class="card-body p-0"> <div class="p-4 border-b border-slate-200 bg-slate-50/50 rounded-t-xl">
<div class="table-responsive"> <h2 class="text-lg font-semibold text-navy flex items-center gap-2">
<table class="table table-hover mb-0"> <i data-lucide="help-circle" class="w-5 h-5 text-slate"></i>
<thead class="table-light"> {% trans "Inquiries List" %}
<tr> </h2>
<th>{% trans "ID" %}</th> </div>
<th>{% trans "Subject" %}</th> <div class="overflow-x-auto">
<th>{% trans "Contact" %}</th> <table class="w-full">
<th>{% trans "Category" %}</th> <thead>
<th>{% trans "Status" %}</th> <tr class="border-b border-slate-200">
<th>{% trans "Assigned To" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "ID" %}</th>
<th>{% trans "Created" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Subject" %}</th>
<th>{% trans "Actions" %}</th> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Contact" %}</th>
</tr> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Category" %}</th>
</thead> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Status" %}</th>
<tbody> <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Assigned To" %}</th>
{% for inquiry in inquiries %} <th class="text-left py-3 px-4 text-sm font-semibold text-navy">{% trans "Created" %}</th>
<tr> <th class="text-center py-3 px-4 text-sm font-semibold text-navy">{% trans "Actions" %}</th>
<td><code>{{ inquiry.id|slice:":8" }}</code></td> </tr>
<td>{{ inquiry.subject|truncatewords:8 }}</td> </thead>
<td> <tbody class="divide-y divide-slate-100">
{% if inquiry.patient %} {% for inquiry in inquiries %}
<strong>{{ inquiry.patient.get_full_name }}</strong><br> <tr class="hover:bg-slate-50 transition">
<small class="text-muted">{% trans "MRN" %}: {{ inquiry.patient.mrn }}</small> <td class="py-3 px-4 text-sm font-mono text-slate">{{ inquiry.id|slice:":8" }}</td>
{% else %} <td class="py-3 px-4">
{{ inquiry.contact_name|default:"-" }}<br> <a href="{% url 'complaints:inquiry_detail' inquiry.pk %}"
<small class="text-muted">{{ inquiry.contact_email|default:"-" }}</small> class="text-navy font-semibold hover:text-blue transition">
{% endif %} {{ inquiry.subject|truncatewords:8 }}
</td> </a>
<td><span class="badge bg-secondary">{{ inquiry.get_category_display }}</span></td> </td>
<td> <td class="py-3 px-4">
{% if inquiry.status == 'open' %} {% if inquiry.patient %}
<span class="badge bg-danger">{% trans "Open" %}</span> <div class="font-medium text-slate">{{ inquiry.patient.get_full_name }}</div>
{% elif inquiry.status == 'in_progress' %} <div class="text-xs text-slate/70">{% trans "MRN" %}: {{ inquiry.patient.mrn }}</div>
<span class="badge bg-warning text-dark">{% trans "In Progress" %}</span> {% else %}
{% elif inquiry.status == 'resolved' %} <div class="font-medium text-slate">{{ inquiry.contact_name|default:"-" }}</div>
<span class="badge bg-success">{% trans "Resolved" %}</span> <div class="text-xs text-slate/70">{{ inquiry.contact_email|default:"-" }}</div>
{% else %} {% endif %}
<span class="badge bg-secondary">{% trans "Closed" %}</span> </td>
{% endif %} <td class="py-3 px-4">
</td> <span class="inline-flex items-center px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">
<td> {{ inquiry.get_category_display }}
{% if inquiry.assigned_to %} </span>
{{ inquiry.assigned_to.get_full_name }} </td>
{% else %} <td class="py-3 px-4 text-center">
<span class="text-muted"><em>{% trans "Unassigned" %}</em></span> {% if inquiry.status == 'open' %}
{% endif %} <span class="inline-flex items-center px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs font-medium">{% trans "Open" %}</span>
</td> {% elif inquiry.status == 'in_progress' %}
<td><small class="text-muted">{{ inquiry.created_at|date:"Y-m-d" }}</small></td> <span class="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium">{% trans "In Progress" %}</span>
<td> {% elif inquiry.status == 'resolved' %}
<a href="{% url 'complaints:inquiry_detail' inquiry.pk %}" <span class="inline-flex items-center px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-medium">{% trans "Resolved" %}</span>
class="btn btn-sm btn-info" {% else %}
title="{% trans 'View' %}"> <span class="inline-flex items-center px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">{% trans "Closed" %}</span>
<i class="bi bi-eye"></i> {% endif %}
</a> </td>
</td> <td class="py-3 px-4 text-sm text-slate">
</tr> {% if inquiry.assigned_to %}
{% empty %} {{ inquiry.assigned_to.get_full_name }}
<tr> {% else %}
<td colspan="8" class="text-center py-5"> <span class="text-slate/60 italic">{% trans "Unassigned" %}</span>
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i> {% endif %}
<p class="text-muted mt-3"> </td>
{% trans "No inquiries found for your source." %} <td class="py-3 px-4 text-sm text-slate">{{ inquiry.created_at|date:"Y-m-d" }}</td>
</p> <td class="py-3 px-4 text-center">
{% if source_user.can_create_inquiries %} <a href="{% url 'complaints:inquiry_detail' inquiry.pk %}"
<a href="{% url 'complaints:inquiry_create' %}" class="btn btn-primary"> class="inline-flex items-center justify-center p-2 bg-blue text-white rounded-lg hover:bg-navy transition"
<i class="bi bi-plus-circle me-1"></i> {% trans "Create Inquiry" %} title="{% trans 'View' %}">
</a> <i data-lucide="eye" class="w-4 h-4"></i>
{% endif %} </a>
</td> </td>
</tr> </tr>
{% endfor %} {% empty %}
</tbody> <tr>
</table> <td colspan="8" class="text-center py-12">
</div> <i data-lucide="inbox" class="w-16 h-16 mx-auto text-slate/30 mb-4"></i>
<p class="text-slate mb-4">{% trans "No inquiries found for your source." %}</p>
{% if source_user.can_create_inquiries %}
<a href="{% url 'complaints:inquiry_create' %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-lg hover:bg-blue transition">
<i data-lucide="plus-circle" class="w-4 h-4"></i>
{% trans "Create Inquiry" %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{% if inquiries.has_other_pages %} {% if inquiries.has_other_pages %}
<nav aria-label="Inquiries pagination" class="mt-4"> <nav aria-label="Inquiries pagination" class="mt-6">
<ul class="pagination justify-content-center"> <ul class="flex justify-center items-center gap-2">
{% if inquiries.has_previous %} {% if inquiries.has_previous %}
<li class="page-item"> <li>
<a class="page-link" href="?page=1&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-double-left"></i> href="?page=1&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}">
<i data-lucide="chevrons-left" class="w-4 h-4"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li>
<a class="page-link" href="?page={{ inquiries.previous_page_number }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-left"></i> href="?page={{ inquiries.previous_page_number }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% for num in inquiries.paginator.page_range %} {% for num in inquiries.paginator.page_range %}
{% if inquiries.number == num %} {% if inquiries.number == num %}
<li class="page-item active"><span class="page-link">{{ num }}</span></li> <li>
<span class="inline-flex items-center justify-center w-10 h-10 bg-navy text-white rounded-lg font-medium">{{ num }}</span>
</li>
{% elif num > inquiries.number|add:'-3' and num < inquiries.number|add:'3' %} {% elif num > inquiries.number|add:'-3' and num < inquiries.number|add:'3' %}
<li class="page-item"> <li>
<a class="page-link" href="?page={{ num }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
href="?page={{ num }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}">
{{ num }} {{ num }}
</a> </a>
</li> </li>
@ -189,14 +221,16 @@
{% endfor %} {% endfor %}
{% if inquiries.has_next %} {% if inquiries.has_next %}
<li class="page-item"> <li>
<a class="page-link" href="?page={{ inquiries.next_page_number }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-right"></i> href="?page={{ inquiries.next_page_number }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}">
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li>
<a class="page-link" href="?page={{ inquiries.paginator.num_pages }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}"> <a class="inline-flex items-center justify-center w-10 h-10 border border-slate-200 rounded-lg hover:bg-slate-50 transition text-slate"
<i class="bi bi-chevron-double-right"></i> href="?page={{ inquiries.paginator.num_pages }}&search={{ search }}&status={{ status_filter }}&category={{ category_filter }}">
<i data-lucide="chevrons-right" class="w-4 h-4"></i>
</a> </a>
</li> </li>
{% endif %} {% endif %}
@ -204,4 +238,10 @@
</nav> </nav>
{% endif %} {% endif %}
</div> </div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
</script>
{% endblock %}

View File

@ -111,7 +111,7 @@
{% if response.numeric_value %} {% if response.numeric_value %}
<div class="text-right"> <div class="text-right">
<div class="text-3xl font-bold {% if response.numeric_value >= 4 %}text-green-600{% elif response.numeric_value >= 3 %}text-orange-500{% else %}text-red-500{% endif %}"> <div class="text-3xl font-bold {% if response.numeric_value >= 4 %}text-green-600{% elif response.numeric_value >= 3 %}text-orange-500{% else %}text-red-500{% endif %}">
{{ response.numeric_value }} {{ response.numeric_value }}
</div> </div>
<div class="text-slate text-sm">{% trans "out of" %} 5</div> <div class="text-slate text-sm">{% trans "out of" %} 5</div>
</div> </div>
@ -159,7 +159,7 @@
{% if response.question.question_type in 'multiple_choice,single_choice' %} {% if response.question.question_type in 'multiple_choice,single_choice' %}
<div class="mb-4"> <div class="mb-4">
<div class="bg-blue-50 rounded-xl p-4 mb-3"> <div class="bg-blue-50 rounded-xl p-4 mb-3">
<strong class="text-blue-700">{% trans "Response" %}:</strong> {{ response.choice_value }} <strong class="text-blue-700">{% trans "Response" %}:</strong> {{ response.choice_value }} {{ response.text_value }}
</div> </div>
{% if response.question.id in question_stats and question_stats|get_item:response.question.id.type == 'choice' %} {% if response.question.id in question_stats and question_stats|get_item:response.question.id.type == 'choice' %}