Remove hospital dropdowns from templates and fix JavaScript dependencies

This commit is contained in:
ismail 2026-03-11 00:17:53 +03:00
parent 01fa26c59a
commit c16e410fdd
46 changed files with 3619 additions and 838 deletions

View File

@ -68,9 +68,14 @@ SMS_API_RETRY_DELAY=2
# Admin URL (change in production) # Admin URL (change in production)
ADMIN_URL=admin/ ADMIN_URL=admin/
# Integration APIs (Stubs - Replace with actual credentials) # Integration APIs
HIS_API_URL= # HIS API - Hospital Information System for fetching patient discharge data
HIS_API_URL=https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps
HIS_API_USERNAME=your_his_username
HIS_API_PASSWORD=your_his_password
HIS_API_KEY= HIS_API_KEY=
# Other Integration APIs (Stubs - Replace with actual credentials)
MOH_API_URL= MOH_API_URL=
MOH_API_KEY= MOH_API_KEY=
CHI_API_URL= CHI_API_URL=

View File

@ -1,6 +1,7 @@
""" """
Context processors for global template variables Context processors for global template variables
""" """
from django.db.models import Q from django.db.models import Q
@ -25,59 +26,36 @@ def sidebar_counts(request):
# Source Users only see their own created complaints # Source Users only see their own created complaints
if user.is_source_user(): if user.is_source_user():
complaint_count = Complaint.objects.filter( complaint_count = Complaint.objects.filter(created_by=user, status__in=["open", "in_progress"]).count()
created_by=user,
status__in=['open', 'in_progress']
).count()
return { return {
'complaint_count': complaint_count, "complaint_count": complaint_count,
'feedback_count': 0, "feedback_count": 0,
'action_count': 0, "action_count": 0,
'current_hospital': None, "current_hospital": None,
'is_px_admin': False, "is_px_admin": False,
'is_source_user': True, "is_source_user": True,
} }
# Filter based on user role and tenant_hospital # Filter based on user role and tenant_hospital
if user.is_px_admin(): if user.is_px_admin():
# PX Admins use their selected hospital from session # PX Admins use their selected hospital from session
hospital = getattr(request, 'tenant_hospital', None) hospital = getattr(request, "tenant_hospital", None)
if hospital: if hospital:
complaint_count = Complaint.objects.filter( complaint_count = Complaint.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).count()
hospital=hospital, feedback_count = Feedback.objects.filter(hospital=hospital, status__in=["submitted", "reviewed"]).count()
status__in=['open', 'in_progress'] action_count = PXAction.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).count()
).count()
feedback_count = Feedback.objects.filter(
hospital=hospital,
status__in=['submitted', 'reviewed']
).count()
action_count = PXAction.objects.filter(
hospital=hospital,
status__in=['open', 'in_progress']
).count()
else: else:
complaint_count = 0 complaint_count = 0
feedback_count = 0 feedback_count = 0
action_count = 0 action_count = 0
# Count provisional users for PX Admin # Count provisional users for PX Admin
from apps.accounts.models import User from apps.accounts.models import User
provisional_user_count = User.objects.filter(
is_provisional=True, provisional_user_count = User.objects.filter(is_provisional=True, acknowledgement_completed=False).count()
acknowledgement_completed=False
).count()
elif user.hospital: elif user.hospital:
complaint_count = Complaint.objects.filter( complaint_count = Complaint.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).count()
hospital=user.hospital, feedback_count = Feedback.objects.filter(hospital=user.hospital, status__in=["submitted", "reviewed"]).count()
status__in=['open', 'in_progress'] action_count = PXAction.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).count()
).count()
feedback_count = Feedback.objects.filter(
hospital=user.hospital,
status__in=['submitted', 'reviewed']
).count()
action_count = PXAction.objects.filter(
hospital=user.hospital,
status__in=['open', 'in_progress']
).count()
# provisional_user_count = 0 # provisional_user_count = 0
else: else:
complaint_count = 0 complaint_count = 0
@ -85,12 +63,12 @@ def sidebar_counts(request):
action_count = 0 action_count = 0
return { return {
'complaint_count': complaint_count, "complaint_count": complaint_count,
'feedback_count': feedback_count, "feedback_count": feedback_count,
'action_count': action_count, "action_count": action_count,
'current_hospital': getattr(request, 'tenant_hospital', None), "current_hospital": getattr(request, "tenant_hospital", None),
'is_px_admin': request.user.is_authenticated and request.user.is_px_admin(), "is_px_admin": request.user.is_authenticated and request.user.is_px_admin(),
'is_source_user': False, "is_source_user": False,
} }
@ -103,25 +81,24 @@ def hospital_context(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return {} return {}
hospital = getattr(request, 'tenant_hospital', None) hospital = getattr(request, "tenant_hospital", None)
# Get list of hospitals for PX Admin switcher # Get list of hospitals for PX Admin switcher
hospitals_list = [] hospitals_list = []
if request.user.is_px_admin(): if request.user.is_px_admin():
from apps.organizations.models import Hospital from apps.organizations.models import Hospital
hospitals_list = list(
Hospital.objects.filter(status='active').order_by('name').values('id', 'name', 'code') hospitals_list = list(Hospital.objects.filter(status="active").order_by("name").values("id", "name", "code"))
)
# Source user context # Source user context
is_source_user = request.user.is_source_user() is_source_user = request.user.is_source_user()
source_user_profile = getattr(request, 'source_user_profile', None) source_user_profile = getattr(request, "source_user_profile", None)
return { return {
'current_hospital': hospital, "current_hospital": hospital,
'is_px_admin': request.user.is_px_admin(), "is_px_admin": request.user.is_px_admin(),
'is_source_user': is_source_user, "is_source_user": is_source_user,
'source_user_profile': source_user_profile, "source_user_profile": source_user_profile,
'hospitals_list': hospitals_list, "hospitals_list": hospitals_list,
# 'provisional_user_count': provisional_user_count, "show_hospital_selector": False,
} }

File diff suppressed because it is too large Load Diff

View File

@ -258,6 +258,12 @@ class SurveyTemplateMapping(UUIDModel, TimeStampedModel):
help_text="Whether this mapping is active" help_text="Whether this mapping is active"
) )
# Delay configuration
send_delay_hours = models.IntegerField(
default=1,
help_text="Hours after discharge to send survey"
)
class Meta: class Meta:
ordering = ['hospital', 'patient_type'] ordering = ['hospital', 'patient_type']
indexes = [ indexes = [

View File

@ -7,11 +7,13 @@ internal format for sending surveys based on PatientType.
Simplified Flow: Simplified Flow:
1. Parse HIS patient data 1. Parse HIS patient data
2. Determine survey type from PatientType 2. Determine survey type from PatientType
3. Create survey instance 3. Create survey instance with PENDING status
4. Send survey via SMS 4. Queue delayed send task
5. Survey sent after delay (e.g., 1 hour for OPD)
""" """
from datetime import datetime from datetime import datetime, timedelta
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
import logging
from django.utils import timezone from django.utils import timezone
@ -19,6 +21,8 @@ from apps.organizations.models import Hospital, Patient
from apps.surveys.models import SurveyTemplate, SurveyInstance, SurveyStatus from apps.surveys.models import SurveyTemplate, SurveyInstance, SurveyStatus
from apps.integrations.models import InboundEvent from apps.integrations.models import InboundEvent
logger = logging.getLogger(__name__)
class HISAdapter: class HISAdapter:
""" """
@ -191,6 +195,54 @@ class HISAdapter:
return survey_template return survey_template
@staticmethod
def get_delay_for_patient_type(patient_type: str, hospital) -> int:
"""
Get delay hours from SurveyTemplateMapping.
Falls back to default delays if no mapping found.
Args:
patient_type: HIS PatientType code (1, 2, 3, 4, O, E)
hospital: Hospital instance
Returns:
Delay in hours
"""
from apps.integrations.models import SurveyTemplateMapping
# Try to get mapping with delay (hospital-specific)
mapping = SurveyTemplateMapping.objects.filter(
patient_type=patient_type,
hospital=hospital,
is_active=True
).first()
if mapping and mapping.send_delay_hours:
return mapping.send_delay_hours
# Fallback to global mapping
mapping = SurveyTemplateMapping.objects.filter(
patient_type=patient_type,
hospital__isnull=True,
is_active=True
).first()
if mapping and mapping.send_delay_hours:
return mapping.send_delay_hours
# Default delays by patient type
default_delays = {
'1': 24, # Inpatient - 24 hours
'2': 1, # OPD - 1 hour
'3': 2, # EMS - 2 hours
'O': 1, # OPD - 1 hour
'E': 2, # EMS - 2 hours
'4': 4, # Daycase - 4 hours
}
return default_delays.get(patient_type, 1) # Default 1 hour
@staticmethod @staticmethod
def create_and_send_survey( def create_and_send_survey(
patient: Patient, patient: Patient,
@ -199,7 +251,9 @@ class HISAdapter:
survey_template: SurveyTemplate survey_template: SurveyTemplate
) -> Optional[SurveyInstance]: ) -> Optional[SurveyInstance]:
""" """
Create survey instance and send via SMS. Create survey instance and queue for delayed sending.
NEW: Survey is created with PENDING status and sent after delay.
Args: Args:
patient: Patient instance patient: Patient instance
@ -210,9 +264,11 @@ class HISAdapter:
Returns: Returns:
SurveyInstance or None if failed SurveyInstance or None if failed
""" """
from apps.surveys.tasks import send_scheduled_survey
admission_id = patient_data.get("AdmissionID") admission_id = patient_data.get("AdmissionID")
discharge_date_str = patient_data.get("DischargeDate") discharge_date_str = patient_data.get("DischargeDate")
discharge_date = HISAdapter.parse_date(discharge_date_str) if discharge_date_str else None patient_type = patient_data.get("PatientType")
# Check if survey already sent for this admission # Check if survey already sent for this admission
existing_survey = SurveyInstance.objects.filter( existing_survey = SurveyInstance.objects.filter(
@ -222,42 +278,48 @@ class HISAdapter:
).first() ).first()
if existing_survey: if existing_survey:
logger.info(f"Survey already exists for admission {admission_id}")
return existing_survey return existing_survey
# Create survey instance # Get delay from SurveyTemplateMapping
delay_hours = HISAdapter.get_delay_for_patient_type(patient_type, hospital)
# Calculate scheduled send time
scheduled_send_at = timezone.now() + timedelta(hours=delay_hours)
# Create survey with PENDING status (NOT SENT)
survey = SurveyInstance.objects.create( survey = SurveyInstance.objects.create(
survey_template=survey_template, survey_template=survey_template,
patient=patient, patient=patient,
hospital=hospital, hospital=hospital,
status=SurveyStatus.SENT, # Set to SENT as it will be sent immediately status=SurveyStatus.PENDING, # Changed from SENT
delivery_channel="SMS", # Send via SMS delivery_channel="SMS",
recipient_phone=patient.phone, recipient_phone=patient.phone,
recipient_email=patient.email, recipient_email=patient.email,
scheduled_send_at=scheduled_send_at,
metadata={ metadata={
'admission_id': admission_id, 'admission_id': admission_id,
'patient_type': patient_data.get("PatientType"), 'patient_type': patient_type,
'hospital_id': patient_data.get("HospitalID"), 'hospital_id': patient_data.get("HospitalID"),
'insurance_company': patient_data.get("InsuranceCompanyName"), 'insurance_company': patient_data.get("InsuranceCompanyName"),
'is_vip': patient_data.get("IsVIP") == "1" 'is_vip': patient_data.get("IsVIP") == "1",
'discharge_date': discharge_date_str,
'scheduled_send_at': scheduled_send_at.isoformat(),
'delay_hours': delay_hours,
} }
) )
# Send survey via SMS # Queue delayed send task
try: send_scheduled_survey.apply_async(
from apps.surveys.services import SurveyDeliveryService args=[str(survey.id)],
delivery_success = SurveyDeliveryService.deliver_survey(survey) countdown=delay_hours * 3600 # Convert to seconds
)
logger.info(
f"Survey {survey.id} created for {patient_type}, "
f"will send in {delay_hours}h at {scheduled_send_at}"
)
if delivery_success:
return survey
else:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Survey created but SMS delivery failed for survey {survey.id}")
return survey
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error sending survey SMS: {str(e)}", exc_info=True)
return survey return survey
@staticmethod @staticmethod
@ -269,7 +331,8 @@ class HISAdapter:
1. Extract patient data 1. Extract patient data
2. Get or create patient and hospital 2. Get or create patient and hospital
3. Determine survey type from PatientType 3. Determine survey type from PatientType
4. Create and send survey via SMS 4. Create survey with PENDING status
5. Queue delayed send task
Args: Args:
his_data: HIS data in real format his_data: HIS data in real format
@ -282,7 +345,7 @@ class HISAdapter:
'message': '', 'message': '',
'patient': None, 'patient': None,
'survey': None, 'survey': None,
'survey_sent': False 'survey_queued': False
} }
try: try:
@ -327,16 +390,16 @@ class HISAdapter:
result['message'] = f"No survey template found for patient type '{patient_type}'" result['message'] = f"No survey template found for patient type '{patient_type}'"
return result return result
# Create and send survey # Create and queue survey (delayed sending)
survey = HISAdapter.create_and_send_survey( survey = HISAdapter.create_and_send_survey(
patient, hospital, patient_data, survey_template patient, hospital, patient_data, survey_template
) )
if survey: if survey:
from apps.surveys.models import SurveyStatus # Survey is queued with PENDING status
survey_sent = survey.status == SurveyStatus.SENT survey_queued = survey.status == SurveyStatus.PENDING
else: else:
survey_sent = False survey_queued = False
result.update({ result.update({
'success': True, 'success': True,
@ -344,13 +407,12 @@ class HISAdapter:
'patient': patient, 'patient': patient,
'patient_type': patient_type, 'patient_type': patient_type,
'survey': survey, 'survey': survey,
'survey_sent': survey_sent, 'survey_queued': survey_queued,
'scheduled_send_at': survey.scheduled_send_at.isoformat() if survey and survey.scheduled_send_at else None,
'survey_url': survey.get_survey_url() if survey else None 'survey_url': survey.get_survey_url() if survey else None
}) })
except Exception as e: except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error processing HIS data: {str(e)}", exc_info=True) logger.error(f"Error processing HIS data: {str(e)}", exc_info=True)
result['message'] = f"Error processing HIS data: {str(e)}" result['message'] = f"Error processing HIS data: {str(e)}"
result['success'] = False result['success'] = False

View File

@ -208,6 +208,127 @@ def process_pending_events():
# ============================================================================= # =============================================================================
@shared_task
def test_fetch_his_surveys_from_json():
"""
TEST TASK - Fetch surveys from local JSON file instead of HIS API.
This is a clone of fetch_his_surveys for testing purposes.
Reads from /home/ismail/projects/HH/data.json
TODO: Remove this task after testing is complete.
Returns:
dict: Summary of fetched and processed surveys
"""
import json
from pathlib import Path
from apps.integrations.services.his_adapter import HISAdapter
logger.info("Starting TEST HIS survey fetch from JSON file")
result = {
"success": False,
"patients_fetched": 0,
"surveys_created": 0,
"surveys_queued": 0,
"errors": [],
"details": [],
}
try:
# Read JSON file
json_path = Path("/home/ismail/projects/HH/data.json")
if not json_path.exists():
error_msg = f"JSON file not found: {json_path}"
logger.error(error_msg)
result["errors"].append(error_msg)
return result
with open(json_path, 'r') as f:
his_data = json.load(f)
# Extract patient list
patient_list = his_data.get("FetchPatientDataTimeStampList", [])
if not patient_list:
logger.warning("No patient data found in JSON file")
result["errors"].append("No patient data found")
return result
logger.info(f"Found {len(patient_list)} patients in JSON file")
result["patients_fetched"] = len(patient_list)
# Process each patient
for patient_data in patient_list:
try:
# Wrap in proper format for HISAdapter
patient_payload = {
"FetchPatientDataTimeStampList": [patient_data],
"FetchPatientDataTimeStampVisitDataList": [],
"Code": 200,
"Status": "Success",
}
# Process using HISAdapter
process_result = HISAdapter.process_his_data(patient_payload)
if process_result["success"]:
result["surveys_created"] += 1
if process_result.get("survey_queued"):
result["surveys_queued"] += 1
# Log survey details
survey = process_result.get("survey")
if survey:
logger.info(
f"Survey queued for {patient_data.get('PatientName')}: "
f"Type={patient_data.get('PatientType')}, "
f"Scheduled={survey.scheduled_send_at}, "
f"Delay={process_result.get('metadata', {}).get('delay_hours', 'N/A')}h"
)
else:
logger.info(
f"Survey created but not queued for {patient_data.get('PatientName')}"
)
else:
# Not an error - patient may not be discharged
if "not discharged" in process_result.get("message", ""):
logger.debug(
f"Skipping {patient_data.get('PatientName')}: Not discharged"
)
else:
logger.warning(
f"Failed to process {patient_data.get('PatientName')}: "
f"{process_result.get('message', 'Unknown error')}"
)
result["errors"].append(
f"{patient_data.get('PatientName')}: {process_result.get('message')}"
)
except Exception as e:
error_msg = f"Error processing patient {patient_data.get('PatientName', 'Unknown')}: {str(e)}"
logger.error(error_msg, exc_info=True)
result["errors"].append(error_msg)
result["success"] = True
logger.info(
f"TEST HIS survey fetch completed: "
f"{result['patients_fetched']} patients, "
f"{result['surveys_created']} surveys created, "
f"{result['surveys_queued']} surveys queued"
)
except Exception as e:
error_msg = f"Fatal error in test_fetch_his_surveys_from_json: {str(e)}"
logger.error(error_msg, exc_info=True)
result["errors"].append(error_msg)
return result
@shared_task @shared_task
def fetch_his_surveys(): def fetch_his_surveys():
""" """

View File

@ -34,10 +34,14 @@ class SurveyTemplateMappingViewSet(viewsets.ModelViewSet):
queryset = super().get_queryset() queryset = super().get_queryset()
user = self.request.user user = self.request.user
# If user is not superuser, filter by their hospital # Superusers and PX Admins see all mappings
if not user.is_superuser and user.hospital: if user.is_superuser or user.is_px_admin():
return queryset
# Hospital users filter by their assigned hospital
if user.hospital:
queryset = queryset.filter(hospital=user.hospital) queryset = queryset.filter(hospital=user.hospital)
elif not user.is_superuser and not user.hospital: else:
# User without hospital assignment - no access # User without hospital assignment - no access
queryset = queryset.none() queryset = queryset.none()
@ -149,19 +153,28 @@ def survey_mapping_settings(request):
# Get user's accessible hospitals based on role # Get user's accessible hospitals based on role
if user.is_superuser: if user.is_superuser:
# Superusers can see all hospitals # Superusers can see all hospitals
hospitals = Hospital.objects.all() hospitals = Hospital.objects.filter(status='active')
elif user.is_px_admin():
# PX Admins see all active hospitals for the dropdown
# They use session-based hospital selection (request.tenant_hospital)
hospitals = Hospital.objects.filter(status='active')
elif user.hospital: elif user.hospital:
# Regular users can only see their assigned hospital # Regular users can only see their assigned hospital
hospitals = Hospital.objects.filter(id=user.hospital.id) hospitals = Hospital.objects.filter(id=user.hospital.id)
else: else:
# User without hospital assignment - no access # User without hospital assignment - no access
hospitals = [] hospitals = Hospital.objects.none()
# Get all mappings # Get all mappings based on user role
if user.is_superuser: if user.is_superuser:
mappings = SurveyTemplateMapping.objects.select_related( mappings = SurveyTemplateMapping.objects.select_related(
'hospital', 'survey_template' 'hospital', 'survey_template'
).all() ).all()
elif user.is_px_admin():
# PX Admins see mappings for all hospitals (they manage all)
mappings = SurveyTemplateMapping.objects.select_related(
'hospital', 'survey_template'
).all()
else: else:
mappings = SurveyTemplateMapping.objects.filter( mappings = SurveyTemplateMapping.objects.filter(
hospital__in=hospitals hospital__in=hospitals
@ -170,6 +183,9 @@ def survey_mapping_settings(request):
# Group mappings by hospital # Group mappings by hospital
mappings_by_hospital = {} mappings_by_hospital = {}
for mapping in mappings: for mapping in mappings:
# Skip mappings with missing hospital (orphaned records)
if mapping.hospital is None:
continue
hospital_name = mapping.hospital.name hospital_name = mapping.hospital.name
if hospital_name not in mappings_by_hospital: if hospital_name not in mappings_by_hospital:
mappings_by_hospital[hospital_name] = [] mappings_by_hospital[hospital_name] = []

View File

@ -237,6 +237,12 @@ class SurveyInstance(UUIDModel, TimeStampedModel, TenantModel):
# Timestamps # Timestamps
sent_at = models.DateTimeField(null=True, blank=True, db_index=True) sent_at = models.DateTimeField(null=True, blank=True, db_index=True)
scheduled_send_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
help_text="When this survey should be sent (for delayed sending)"
)
opened_at = models.DateTimeField(null=True, blank=True) opened_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True)

View File

@ -676,3 +676,86 @@ def send_bulk_surveys(self, job_id):
raise self.retry(countdown=60 * (self.request.retries + 1)) raise self.retry(countdown=60 * (self.request.retries + 1))
return {'status': 'error', 'error': str(e)} return {'status': 'error', 'error': str(e)}
@shared_task
def send_scheduled_survey(survey_instance_id):
"""
Send a scheduled survey.
This task is called after the delay period expires.
It sends the survey via the configured delivery channel (SMS/Email).
Args:
survey_instance_id: UUID of the SurveyInstance to send
Returns:
dict: Result with status and details
"""
from apps.surveys.models import SurveyInstance, SurveyStatus
from apps.surveys.services import SurveyDeliveryService
try:
survey = SurveyInstance.objects.get(id=survey_instance_id)
# Check if already sent
if survey.status != SurveyStatus.PENDING:
logger.warning(f"Survey {survey.id} already sent/cancelled (status: {survey.status})")
return {'status': 'skipped', 'reason': 'already_sent', 'survey_id': survey.id}
# Check if scheduled time has passed
if survey.scheduled_send_at and survey.scheduled_send_at > timezone.now():
logger.warning(f"Survey {survey.id} not due yet (scheduled: {survey.scheduled_send_at})")
return {'status': 'delayed', 'scheduled_at': survey.scheduled_send_at.isoformat(), 'survey_id': survey.id}
# Send survey
success = SurveyDeliveryService.deliver_survey(survey)
if success:
survey.status = SurveyStatus.SENT
survey.sent_at = timezone.now()
survey.save()
logger.info(f"Scheduled survey {survey.id} sent successfully")
return {'status': 'sent', 'survey_id': survey.id}
else:
survey.status = SurveyStatus.FAILED
survey.save()
logger.error(f"Scheduled survey {survey.id} delivery failed")
return {'status': 'failed', 'survey_id': survey.id, 'reason': 'delivery_failed'}
except SurveyInstance.DoesNotExist:
logger.error(f"Survey {survey_instance_id} not found")
return {'status': 'error', 'reason': 'not_found'}
except Exception as e:
logger.error(f"Error sending scheduled survey: {e}", exc_info=True)
return {'status': 'error', 'reason': str(e)}
@shared_task
def send_pending_scheduled_surveys():
"""
Periodic task to send any overdue scheduled surveys.
Runs every 10 minutes as a safety net to catch any surveys
that weren't sent due to task failures or delays.
Returns:
dict: Result with count of queued surveys
"""
from apps.surveys.models import SurveyInstance, SurveyStatus
# Find surveys that should have been sent but weren't
overdue_surveys = SurveyInstance.objects.filter(
status=SurveyStatus.PENDING,
scheduled_send_at__lte=timezone.now()
)[:50] # Max 50 at a time
sent_count = 0
for survey in overdue_surveys:
send_scheduled_survey.delay(str(survey.id))
sent_count += 1
if sent_count > 0:
logger.info(f"Queued {sent_count} overdue scheduled surveys")
return {'queued': sent_count}

View File

@ -226,7 +226,13 @@ def survey_template_list(request):
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
if user.is_px_admin(): if user.is_px_admin():
pass # PX Admins see templates for their selected hospital (from session)
tenant_hospital = getattr(request, 'tenant_hospital', None)
if tenant_hospital:
queryset = queryset.filter(hospital=tenant_hospital)
else:
# If no hospital selected, show none (user needs to select a hospital)
queryset = queryset.none()
elif user.hospital: elif user.hospital:
queryset = queryset.filter(hospital=user.hospital) queryset = queryset.filter(hospital=user.hospital)
else: else:

View File

@ -35,6 +35,16 @@ app.conf.beat_schedule = {
'expires': 240, # Task expires after 4 minutes if not picked up 'expires': 240, # Task expires after 4 minutes if not picked up
} }
}, },
# TEST TASK - Fetch from JSON file (uncomment for testing, remove when done)
# 'test-fetch-his-surveys-from-json': {
# 'task': 'apps.integrations.tasks.test_fetch_his_surveys_from_json',
# 'schedule': crontab(minute='*/5'), # Every 5 minutes
# },
# Send pending scheduled surveys every 10 minutes
'send-pending-scheduled-surveys': {
'task': 'apps.surveys.tasks.send_pending_scheduled_surveys',
'schedule': crontab(minute='*/10'), # Every 10 minutes
},
# Check for overdue complaints every 15 minutes # Check for overdue complaints every 15 minutes
'check-overdue-complaints': { 'check-overdue-complaints': {
'task': 'apps.complaints.tasks.check_overdue_complaints', 'task': 'apps.complaints.tasks.check_overdue_complaints',

245
docs/SETUP_COMPLETE.md Normal file
View File

@ -0,0 +1,245 @@
# Development Environment Setup - Complete! ✅
## Setup Summary
The development environment has been successfully created with the following components:
### 📊 Database Objects Created
| Component | Count |
|-----------|-------|
| Organizations | 2 |
| Hospitals | 6 |
| Departments | 78 |
| Roles | 9 |
| PX Sources | 13 |
| Survey Templates | 22 |
| Survey Questions | 117 |
| Journey Templates | 15 |
| Journey Stages | 37 |
| Observation Categories | 15 |
| Notification Templates | 10 |
| Standard Sources | 4 |
| Standard Categories | 8 |
### 🏥 Hospitals Created
1. **NUZHA-DEV** - Al Hammadi Hospital - Nuzha (Development)
- 8 departments (ED, OPD, IP, ICU, Pharmacy, Lab, Radiology, Admin)
- 4 survey templates (Inpatient, OPD, EMS, Day Case)
- Journey templates for all patient types
- SLA configs, escalation rules, thresholds
2. **OLAYA-DEV** - Al Hammadi Hospital - Olaya (Development)
- Same configuration as NUZHA-DEV
3. **SUWAIDI-DEV** - Al Hammadi Hospital - Suwaidi (Development)
- Same configuration as NUZHA-DEV
### 📝 Survey Templates
**4 Template Types** per hospital:
1. **Inpatient Post-Discharge Survey**
- 7 questions (nursing, doctor, cleanliness, food, information, NPS, comments)
2. **OPD Patient Experience Survey**
- 6 questions (registration, waiting, consultation, pharmacy, NPS, comments)
3. **EMS Emergency Services Survey**
- 6 questions (response time, paramedic, ED care, communication, NPS, comments)
4. **Day Case Patient Survey**
- 6 questions (pre-procedure, procedure, post-procedure, discharge, NPS, comments)
### 🔄 Journey Templates
**OPD Journey Stages** (with HIS integration):
1. Registration (trigger: REGISTRATION)
2. Waiting (trigger: WAITING)
3. MD Consultation (trigger: Consultation)
4. MD Visit (trigger: Doctor Visited)
5. Clinical Assessment (trigger: Clinical Condtion)
6. Patient Assessment (trigger: ChiefComplaint)
7. Pharmacy (trigger: Prescribed Drugs)
8. Discharge (trigger: DISCHARGED)
**Other Journey Types**:
- Inpatient Journey
- EMS Journey
- Day Case Journey
### 🎭 Roles & Permissions
| Role | Level | Description |
|------|-------|-------------|
| PX Admin | 100 | Full system access |
| Hospital Admin | 80 | Hospital-level access |
| Department Manager | 60 | Department-level access |
| PX Coordinator | 50 | PX actions & complaints |
| Physician | 40 | View feedback |
| Nurse | 30 | View department feedback |
| Staff | 20 | Basic access |
| Viewer | 10 | Read-only |
| PX Source User | 5 | External sources |
### 📬 PX Sources
**Internal Sources:**
- Patient
- Family Member
- Staff
- Survey
**Government Sources:**
- Ministry of Health (MOH)
- Council of Cooperative Health Insurance (CCHI)
### ⚙️ SLA Configurations
| Source | SLA | 1st Reminder | 2nd Reminder | Escalation |
|--------|-----|--------------|--------------|------------|
| MOH | 24h | 12h | 18h | 24h |
| CCHI | 48h | 24h | 36h | 48h |
| Internal | 72h | 24h | 48h | 72h |
### 🔔 Escalation Rules
1. **Default**: Department Manager (immediate on overdue)
2. **Critical**: Hospital Admin (4h overdue)
3. **Final**: PX Admin (24h overdue)
### 👁️ Observation Categories
15 categories including:
- Patient Safety
- Clinical Quality
- Infection Control
- Medication Safety
- Equipment & Devices
- Facility & Environment
- Staff Behavior
- Communication
- Documentation
- Process & Workflow
- Security
- IT & Systems
- Housekeeping
- Food Services
- Other
### 📨 Notification Templates
10 templates for:
- Onboarding (Invitation, Reminder, Completion)
- Surveys (Invitation, Reminder)
- Complaints (Acknowledgment, Update)
- Actions (Assignment)
- SLA (Reminder, Breach)
### ✅ Standards Setup
**Standard Sources:**
- CBAHI (Saudi Central Board for Accreditation)
- JCI (Joint Commission International)
- ISO (International Organization for Standardization)
- SFDA (Saudi Food & Drug Authority)
**Standard Categories:**
- Patient Safety
- Quality Management
- Infection Control
- Medication Safety
- Environment of Care
- Leadership
- Information Management
- Facility Management
### 🔌 HIS Integration
**API Configuration:**
- URL: `https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps`
- Auth: Basic (username/password from .env)
- Schedule: Every 5 minutes (Celery Beat)
**Event Mappings:**
- Consultation → OPD_CONSULTATION
- Doctor Visited → OPD_DOCTOR_VISITED
- Clinical Condtion → CLINICAL_ASSESSMENT
- ChiefComplaint → PATIENT_ASSESSMENT
- Prescribed Drugs → PHARMACY
- DISCHARGED → PATIENT_DISCHARGED
## 🚀 Next Steps
### 1. Create Admin Users
```bash
python manage.py createsuperuser
```
### 2. Start Services
```bash
# Start Redis
redis-server
# Start Celery Worker
celery -A config worker -l info
# Start Celery Beat
celery -A config beat -l info
# Start Django Server
python manage.py runserver
```
### 3. Verify Setup
Visit http://localhost:8000 and login with your superuser account.
### 4. Load SHCT Taxonomy (Optional)
```bash
python manage.py load_shct_taxonomy
```
### 5. Import Staff Data (Optional)
```bash
python manage.py import_staff_csv path/to/staff.csv
```
## 📚 Documentation
See `/docs/SETUP_GUIDE.md` for complete documentation.
## 🔧 Useful Commands
```bash
# Preview changes
python manage.py setup_dev_environment --dry-run
# Setup specific hospital
python manage.py setup_dev_environment --hospital-code NUZHA-DEV
# Skip specific components
python manage.py setup_dev_environment --skip-surveys --skip-integration
# Reset and recreate (delete database first)
rm db.sqlite3
python manage.py migrate
python manage.py setup_dev_environment
```
## ✨ Features Ready to Use
1. ✅ Multi-hospital structure
2. ✅ Role-based access control
3. ✅ Survey system with 4 template types
4. ✅ HIS integration with 5-minute polling
5. ✅ Patient journey tracking
6. ✅ Complaint management with SLA
7. ✅ Observation system
8. ✅ Notification system
9. ✅ Standards compliance tracking
10. ✅ Escalation workflows
---
**Environment is ready for development!** 🎉

362
docs/SETUP_GUIDE.md Normal file
View File

@ -0,0 +1,362 @@
# PX360 Development Environment Setup Guide
## Overview
This guide explains how to set up a complete development environment for PX360 using the `setup_dev_environment` management command.
## What Gets Created
### 1. Organization Structure
- **Organization**: Al Hammadi Healthcare Group (DEV)
- **3 Hospitals**: NUZHA-DEV, OLAYA-DEV, SUWAIDI-DEV
- **Departments**: Emergency, OPD, Inpatient, ICU, Pharmacy, Laboratory, Radiology, Administration
### 2. User Roles & Permissions
- PX Admin (Full system access)
- Hospital Admin (Hospital-level access)
- Department Manager (Department-level access)
- PX Coordinator (PX actions & complaints)
- Physician (View feedback)
- Nurse (View department feedback)
- Staff (Basic access)
- Viewer (Read-only)
- PX Source User (External source users)
### 3. Survey Templates (4 types)
1. **Inpatient Post-Discharge Survey**
- Nursing care
- Doctor's care
- Room cleanliness
- Food quality
- Treatment information
- NPS question
- Comments
2. **OPD Patient Experience Survey**
- Registration process
- Waiting time
- Doctor consultation
- Pharmacy service
- NPS question
- Comments
3. **EMS Emergency Services Survey**
- Ambulance response time
- Paramedic care
- Emergency department care
- Communication
- NPS question
- Comments
4. **Day Case Patient Survey**
- Pre-procedure preparation
- Procedure quality
- Post-procedure care
- Discharge process
- NPS question
- Comments
### 4. Complaint System Configuration
- **Complaint Categories**: Clinical Care, Management, Relationships, Facility, Communication, Access, Billing, Other
- **PX Sources**: Patient, Family Member, Staff, Survey, MOH, CCHI
- **SLA Configurations** (per hospital):
- MOH: 24 hours (reminders at 12h/18h)
- CCHI: 48 hours (reminders at 24h/36h)
- Internal: 72 hours (reminders at 24h/48h)
- **Escalation Rules** (per hospital):
- Default: Department Manager (immediate)
- Critical: Hospital Admin (4h overdue)
- Final: PX Admin (24h overdue)
- **Thresholds**: Resolution survey < 50% Create PX Action
- **Explanation SLA**: 48 hours response time
### 5. Journey Templates
**OPD Journey Stages**:
1. Registration (trigger: REGISTRATION)
2. Waiting (trigger: WAITING)
3. MD Consultation (trigger: Consultation)
4. MD Visit (trigger: Doctor Visited)
5. Clinical Assessment (trigger: Clinical Condition)
6. Patient Assessment (trigger: ChiefComplaint)
7. Pharmacy (trigger: Prescribed Drugs)
8. Discharge (trigger: DISCHARGED)
**Other Journey Types**:
- Inpatient Journey
- EMS Journey
- Day Case Journey
### 6. Survey Mappings
- Patient Type 1 (Inpatient) → Inpatient Survey
- Patient Type 2 (Outpatient) → OPD Survey
- Patient Type 3 (Emergency) → EMS Survey
- Patient Type 4 (Day Case) → Day Case Survey
### 7. Observation Categories (15)
1. Patient Safety
2. Clinical Quality
3. Infection Control
4. Medication Safety
5. Equipment & Devices
6. Facility & Environment
7. Staff Behavior
8. Communication
9. Documentation
10. Process & Workflow
11. Security
12. IT & Systems
13. Housekeeping
14. Food Services
15. Other
### 8. Notification Templates
- Onboarding Invitation
- Onboarding Reminder
- Onboarding Completion
- Survey Invitation
- Survey Reminder
- Complaint Acknowledgment
- Complaint Update
- Action Assignment
- SLA Reminder
- SLA Breach
### 9. Standards Setup
**Standard Sources**:
- CBAHI (Saudi Central Board for Accreditation)
- JCI (Joint Commission International)
- ISO (International Organization for Standardization)
**Standard Categories**:
- Patient Safety
- Quality Management
- Infection Control
- Medication Safety
- Environment of Care
- Leadership
- Information Management
### 10. HIS Integration
- API URL: From `.env` (HIS_API_URL)
- Username: From `.env` (HIS_API_USERNAME)
- Password: From `.env` (HIS_API_PASSWORD)
- Event Mappings configured for OPD workflow
## Usage
### Basic Setup (All Components)
```bash
python manage.py setup_dev_environment
```
### Dry Run (Preview Only)
```bash
python manage.py setup_dev_environment --dry-run
```
### Setup Specific Hospital
```bash
python manage.py setup_dev_environment --hospital-code NUZHA-DEV
```
### Skip Specific Components
```bash
# Skip surveys
python manage.py setup_dev_environment --skip-surveys
# Skip complaints
python manage.py setup_dev_environment --skip-complaints
# Skip journeys
python manage.py setup_dev_environment --skip-journeys
# Skip HIS integration
python manage.py setup_dev_environment --skip-integration
# Combine multiple skips
python manage.py setup_dev_environment --skip-surveys --skip-integration
```
## Environment Variables Required
Add these to your `.env` file:
```env
# HIS Integration
HIS_API_URL=https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps
HIS_API_USERNAME=AlhhSUNZHippo
HIS_API_PASSWORD=*#$@PAlhh^2106
# Database
DATABASE_URL=sqlite:///db.sqlite3
# Redis/Celery
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0
# SMS Gateway
SMS_API_URL=http://localhost:8000/api/simulator/send-sms/
SMS_API_KEY=simulator-test-key
SMS_ENABLED=True
SMS_PROVIDER=console
# Email Gateway
EMAIL_API_URL=http://localhost:8000/api/simulator/send-email/
EMAIL_API_KEY=simulator-test-key
EMAIL_ENABLED=True
EMAIL_PROVIDER=console
# AI Configuration
OPENROUTER_API_KEY=your-api-key-here
AI_MODEL=stepfun/step-3.5-flash:free
AI_TEMPERATURE=0.3
AI_MAX_TOKENS=500
```
## Post-Setup Steps
### 1. Create Admin Users
After running the setup, create admin users for each hospital:
```bash
python manage.py createsuperuser
```
Then assign them to hospitals via the admin interface or:
```python
from apps.accounts.models import HospitalUser
from apps.organizations.models import Hospital
from django.contrib.auth import get_user
User = get_user_model()
hospital = Hospital.objects.get(code='NUZHA-DEV')
user = User.objects.get(email='admin@example.com')
HospitalUser.objects.create(
user=user,
hospital=hospital,
role='hospital_admin'
)
```
### 2. Start Celery Workers
```bash
# Start Celery worker
celery -A config worker -l info
# Start Celery beat (for scheduled tasks)
celery -A config beat -l info
```
### 3. Verify Setup
```bash
# Check organization
python manage.py shell
>>> from apps.organizations.models import Organization
>>> Organization.objects.count()
1
# Check hospitals
>>> from apps.organizations.models import Hospital
>>> Hospital.objects.count()
3
# Check survey templates
>>> from apps.surveys.models import SurveyTemplate
>>> SurveyTemplate.objects.count()
12 # 4 templates × 3 hospitals
```
### 4. Test HIS Integration
```bash
python manage.py test_his_connection
```
### 5. Run Initial HIS Sync
```bash
python manage.py fetch_his_surveys
```
## Idempotent Operation
The command is **idempotent** - it can be run multiple times safely:
- Uses `get_or_create()` for all models
- Won't create duplicates
- Updates existing records if needed
- Safe to re-run after errors
## Troubleshooting
### Issue: "No module named 'django'"
**Solution**: Activate virtual environment
```bash
source .venv/bin/activate
```
### Issue: "Command not found"
**Solution**: Run from project root
```bash
cd /path/to/HH
python manage.py setup_dev_environment
```
### Issue: Database locked
**Solution**: Stop all running processes and try again
```bash
pkill -f celery
pkill -f python
python manage.py setup_dev_environment
```
### Issue: Permission denied
**Solution**: Check file permissions
```bash
chmod +x manage.py
```
## Next Steps After Setup
1. **Configure SMS Gateway** (for production)
2. **Configure Email Gateway** (for production)
3. **Load SHCT Taxonomy** (detailed complaint categories)
4. **Import Staff Data** (via CSV import commands)
5. **Set Up Department Managers** (via admin interface)
6. **Configure HIS Integration** (fine-tune event mappings)
7. **Create Additional Survey Templates** (as needed)
8. **Set Up Standards** (add CBAHI/JCI standards)
9. **Configure Notification Templates** (add SMS/email content)
10. **Test Complete Workflow** (create test complaint → resolve → survey)
## Related Management Commands
```bash
# Load SHCT complaint taxonomy
python manage.py load_shct_taxonomy
# Seed departments
python manage.py seed_departments
# Import staff from CSV
python manage.py import_staff_csv path/to/staff.csv
# Create notification templates
python manage.py init_notification_templates
# Create appreciation category
python manage.py create_patient_feedback_category
# Seed observation categories
python manage.py seed_observation_categories
# Seed acknowledgement categories
python manage.py seed_acknowledgements
```
## Support
For issues or questions:
1. Check logs: `tail -f logs/debug.log`
2. Check Celery logs
3. Review environment variables
4. Check database integrity
5. Contact: support@px360.sa

View File

@ -111,20 +111,6 @@
</div> </div>
</div> </div>
<!-- Hospital -->
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</label>
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"
name="hospital" id="hospitalFilter" onchange="loadDepartments()">
<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_en|default:hospital.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Department --> <!-- Department -->
<div> <div>
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label> <label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
@ -463,7 +449,7 @@ function handleDateRangeChange() {
function updateFilters() { function updateFilters() {
currentFilters.date_range = document.getElementById('dateRange').value; currentFilters.date_range = document.getElementById('dateRange').value;
currentFilters.hospital = document.getElementById('hospitalFilter').value; currentFilters.hospital = '{{ current_hospital.id|default:"" }}';
currentFilters.department = document.getElementById('departmentFilter').value; currentFilters.department = document.getElementById('departmentFilter').value;
currentFilters.kpi_category = document.getElementById('kpiCategoryFilter').value; currentFilters.kpi_category = document.getElementById('kpiCategoryFilter').value;
currentFilters.custom_start = document.getElementById('customStart').value; currentFilters.custom_start = document.getElementById('customStart').value;
@ -669,7 +655,6 @@ function refreshDashboard() {
function resetFilters() { function resetFilters() {
document.getElementById('dateRange').value = '30d'; document.getElementById('dateRange').value = '30d';
document.getElementById('hospitalFilter').value = '';
document.getElementById('departmentFilter').value = ''; document.getElementById('departmentFilter').value = '';
document.getElementById('kpiCategoryFilter').value = ''; document.getElementById('kpiCategoryFilter').value = '';
document.getElementById('customStart').value = ''; document.getElementById('customStart').value = '';

View File

@ -240,14 +240,6 @@
<p class="mt-1 opacity-90">{% trans "Comprehensive overview of patient experience metrics" %}</p> <p class="mt-1 opacity-90">{% trans "Comprehensive overview of patient experience metrics" %}</p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<select class="form-select-px360" onchange="window.location.href='?hospital='+this.value">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if selected_hospital and selected_hospital.id == hospital.id %}selected{% endif %}>
{{ hospital.name_en }}
</option>
{% endfor %}
</select>
<button onclick="refreshDashboard()" class="p-2.5 bg-white/20 hover:bg-white/30 rounded-xl transition" title="{% trans 'Refresh' %}"> <button onclick="refreshDashboard()" class="p-2.5 bg-white/20 hover:bg-white/30 rounded-xl transition" title="{% trans 'Refresh' %}">
<i data-lucide="refresh-cw" class="w-5 h-5 text-white"></i> <i data-lucide="refresh-cw" class="w-5 h-5 text-white"></i>
</button> </button>

View File

@ -58,19 +58,7 @@
</p> </p>
</div> </div>
<!-- Hospital --> <input type="hidden" name="hospital" value="{{ current_hospital.id }}">
<div>
<label for="hospital" class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">
{% trans "Hospital" %} <span class="text-red-500">*</span>
</label>
<select name="hospital" id="hospital" required
class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:bg-white focus:border-navy focus:ring-2 focus:ring-navy/10 outline-none transition">
<option value="">{% trans "Select Hospital" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
{% endfor %}
</select>
</div>
<!-- Year and Month --> <!-- Year and Month -->
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">

View File

@ -123,20 +123,6 @@
</select> </select>
</div> </div>
{% if request.user.is_px_admin %}
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</label>
<select name="hospital" class="px-3 py-1.5 bg-white border rounded-lg text-xs">
<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>
{% endif %}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate uppercase">{% trans "Year" %}</label> <label class="text-xs font-bold text-slate uppercase">{% trans "Year" %}</label>
<select name="year" class="px-3 py-1.5 bg-white border rounded-lg text-xs"> <select name="year" class="px-3 py-1.5 bg-white border rounded-lg text-xs">

View File

@ -39,15 +39,9 @@
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="hospital_id" class="form-label"> <input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
{% trans "Hospital" %} <span class="text-danger">*</span> <label class="form-label">{% trans "Hospital" %}</label>
</label> <input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
<select class="form-select" id="hospital_id" name="hospital_id" required>
<option value="">-- {% trans "Select Hospital" %} --</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
{% endfor %}
</select>
</div> </div>
</div> </div>
@ -240,31 +234,26 @@
{{ block.super }} {{ block.super }}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const hospitalSelect = document.getElementById('hospital_id');
const recipientTypeSelect = document.getElementById('recipient_type'); const recipientTypeSelect = document.getElementById('recipient_type');
const recipientSelect = document.getElementById('recipient_id'); const recipientSelect = document.getElementById('recipient_id');
const departmentSelect = document.getElementById('department_id'); const departmentSelect = document.getElementById('department_id');
const recipientHelp = document.getElementById('recipientHelp'); const recipientHelp = document.getElementById('recipientHelp');
const currentHospitalId = '{{ current_hospital.id|default:"" }}';
let recipientData = []; let recipientData = [];
// Load recipients when hospital changes // Load recipients and departments on page load
hospitalSelect.addEventListener('change', function() { function loadRecipientsAndDepartments() {
const hospitalId = this.value; const hospitalId = currentHospitalId;
const recipientType = recipientTypeSelect.value; const recipientType = recipientTypeSelect.value;
if (!hospitalId) return;
// Load recipients
recipientSelect.disabled = true; recipientSelect.disabled = true;
recipientSelect.innerHTML = '<option value="">Loading...</option>'; recipientSelect.innerHTML = '<option value="">Loading...</option>';
recipientHelp.textContent = 'Loading recipients...'; recipientHelp.textContent = 'Loading recipients...';
if (!hospitalId) {
recipientSelect.innerHTML = '<option value="">-- Select Recipient --</option>';
recipientSelect.disabled = true;
recipientHelp.textContent = 'Select a hospital first';
return;
}
// Fetch recipients
const url = recipientType === 'user' const url = recipientType === 'user'
? "{% url 'appreciation:get_users_by_hospital' %}?hospital_id=" + hospitalId ? "{% url 'appreciation:get_users_by_hospital' %}?hospital_id=" + hospitalId
: "{% url 'appreciation:get_physicians_by_hospital' %}?hospital_id=" + hospitalId; : "{% url 'appreciation:get_physicians_by_hospital' %}?hospital_id=" + hospitalId;
@ -290,16 +279,10 @@ document.addEventListener('DOMContentLoaded', function() {
recipientSelect.innerHTML = '<option value="">Error loading recipients</option>'; recipientSelect.innerHTML = '<option value="">Error loading recipients</option>';
recipientHelp.textContent = 'Error loading recipients'; recipientHelp.textContent = 'Error loading recipients';
}); });
});
// Load departments when hospital changes
hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value;
// Load departments
departmentSelect.innerHTML = '<option value="">-- Select Department --</option>'; departmentSelect.innerHTML = '<option value="">-- Select Department --</option>';
if (!hospitalId) return;
fetch("{% url 'appreciation:get_departments_by_hospital' %}?hospital_id=" + hospitalId) fetch("{% url 'appreciation:get_departments_by_hospital' %}?hospital_id=" + hospitalId)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@ -313,14 +296,13 @@ document.addEventListener('DOMContentLoaded', function() {
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
}); });
});
// Refresh recipients when recipient type changes
recipientTypeSelect.addEventListener('change', function() {
if (hospitalSelect.value) {
hospitalSelect.dispatchEvent(new Event('change'));
} }
});
// Load on page load
loadRecipientsAndDepartments();
// Refresh when recipient type changes
recipientTypeSelect.addEventListener('change', loadRecipientsAndDepartments);
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -47,17 +47,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-3">
<label for="hospital" class="form-label">{% trans "Hospital" %}</label>
<select class="form-select" id="hospital" 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 }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3"> <div class="col-md-3">
<label for="department" class="form-label">{% trans "Department" %}</label> <label for="department" class="form-label">{% trans "Department" %}</label>
<select class="form-select" id="department" name="department"> <select class="form-select" id="department" name="department">

View File

@ -116,18 +116,6 @@
</select> </select>
</div> </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"> <div class="w-40">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "From Date" %}</label> <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 }}" <input type="date" name="date_from" value="{{ filters.date_from }}"

View File

@ -130,13 +130,9 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label required-field">{% trans "Hospital" %}</label> <label class="form-label">{% trans "Hospital" %}</label>
<select name="hospital_id" class="form-select" id="hospitalSelect" required> <input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
<option value="">{% trans "Select hospital..." %}</option> <input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
{% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name_en }}</option>
{% endfor %}
</select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
@ -283,7 +279,6 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const hospitalSelect = document.getElementById('hospitalSelect');
const departmentSelect = document.getElementById('departmentSelect'); const departmentSelect = document.getElementById('departmentSelect');
const physicianSelect = document.getElementById('physicianSelect'); const physicianSelect = document.getElementById('physicianSelect');
const patientSearch = document.getElementById('patientSearch'); const patientSearch = document.getElementById('patientSearch');
@ -293,19 +288,15 @@ document.addEventListener('DOMContentLoaded', function() {
const callerNameInput = document.getElementById('callerName'); const callerNameInput = document.getElementById('callerName');
const callerPhoneInput = document.getElementById('callerPhone'); const callerPhoneInput = document.getElementById('callerPhone');
// Hospital change handler - load departments and physicians const currentHospitalId = '{{ current_hospital.id|default:"" }}';
hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value;
// Clear department and physician // Load departments and physicians on page load
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>'; if (currentHospitalId) {
physicianSelect.innerHTML = '<option value="">{% trans "Select physician..." %}</option>';
if (hospitalId) {
// Load departments // Load departments
fetch(`/callcenter/ajax/departments/?hospital_id=${hospitalId}`) fetch(`/callcenter/ajax/departments/?hospital_id=${currentHospitalId}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>';
data.departments.forEach(dept => { data.departments.forEach(dept => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = dept.id; option.value = dept.id;
@ -316,9 +307,10 @@ document.addEventListener('DOMContentLoaded', function() {
.catch(error => console.error('Error loading departments:', error)); .catch(error => console.error('Error loading departments:', error));
// Load physicians // Load physicians
fetch(`/callcenter/ajax/physicians/?hospital_id=${hospitalId}`) fetch(`/callcenter/ajax/physicians/?hospital_id=${currentHospitalId}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
physicianSelect.innerHTML = '<option value="">{% trans "Select physician..." %}</option>';
data.physicians.forEach(physician => { data.physicians.forEach(physician => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = physician.id; option.value = physician.id;
@ -328,12 +320,11 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.catch(error => console.error('Error loading physicians:', error)); .catch(error => console.error('Error loading physicians:', error));
} }
});
// Patient search // Patient search
function searchPatients() { function searchPatients() {
const query = patientSearch.value.trim(); const query = patientSearch.value.trim();
const hospitalId = hospitalSelect.value; const hospitalId = currentHospitalId;
if (query.length < 2) { if (query.length < 2) {
patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>'; patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>';

View File

@ -110,17 +110,6 @@
<option value="low" {% if filters.severity == 'low' %}selected{% endif %}>{% trans "Low" %}</option> <option value="low" {% if filters.severity == 'low' %}selected{% endif %}>{% trans "Low" %}</option>
</select> </select>
</div> </div>
<div>
<label class="block text-xs font-bold text-slate uppercase mb-2">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy focus:border-transparent transition">
<option value="">{% trans "All" %}</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>
<div class="flex items-end"> <div class="flex items-end">
<button type="submit" class="w-full bg-navy text-white px-4 py-2.5 rounded-xl text-sm font-bold hover:bg-blue transition flex items-center justify-center gap-2"> <button type="submit" class="w-full bg-navy text-white px-4 py-2.5 rounded-xl text-sm font-bold hover:bg-blue transition flex items-center justify-center gap-2">
<i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %} <i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %}

View File

@ -134,13 +134,9 @@
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label required-field">{% trans "Hospital" %}</label> <label class="form-label">{% trans "Hospital" %}</label>
<select name="hospital_id" class="form-select" id="hospitalSelect" required> <input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
<option value="">{% trans "Select hospital..." %}</option> <input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
{% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name_en }}</option>
{% endfor %}
</select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
@ -242,7 +238,6 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const hospitalSelect = document.getElementById('hospitalSelect');
const departmentSelect = document.getElementById('departmentSelect'); const departmentSelect = document.getElementById('departmentSelect');
const patientSearch = document.getElementById('patientSearch'); const patientSearch = document.getElementById('patientSearch');
const searchBtn = document.getElementById('searchBtn'); const searchBtn = document.getElementById('searchBtn');
@ -252,18 +247,14 @@ document.addEventListener('DOMContentLoaded', function() {
const contactPhoneInput = document.getElementById('contactPhone'); const contactPhoneInput = document.getElementById('contactPhone');
const contactEmailInput = document.getElementById('contactEmail'); const contactEmailInput = document.getElementById('contactEmail');
// Hospital change handler - load departments const currentHospitalId = '{{ current_hospital.id|default:"" }}';
hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value;
// Clear department // Load departments on page load
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>'; if (currentHospitalId) {
fetch(`/callcenter/ajax/departments/?hospital_id=${currentHospitalId}`)
if (hospitalId) {
// Load departments
fetch(`/callcenter/ajax/departments/?hospital_id=${hospitalId}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>';
data.departments.forEach(dept => { data.departments.forEach(dept => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = dept.id; option.value = dept.id;
@ -273,12 +264,11 @@ document.addEventListener('DOMContentLoaded', function() {
}) })
.catch(error => console.error('Error loading departments:', error)); .catch(error => console.error('Error loading departments:', error));
} }
});
// Patient search // Patient search
function searchPatients() { function searchPatients() {
const query = patientSearch.value.trim(); const query = patientSearch.value.trim();
const hospitalId = hospitalSelect.value; const hospitalId = currentHospitalId;
if (query.length < 2) { if (query.length < 2) {
patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>'; patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>';

View File

@ -107,17 +107,6 @@
<option value="other" {% if filters.category == 'other' %}selected{% endif %}>{% trans "Other" %}</option> <option value="other" {% if filters.category == 'other' %}selected{% endif %}>{% trans "Other" %}</option>
</select> </select>
</div> </div>
<div>
<label class="block text-xs font-bold text-slate uppercase mb-2">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy focus:border-transparent transition">
<option value="">{% trans "All" %}</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>
<div class="md:col-span-5 flex justify-end"> <div class="md:col-span-5 flex justify-end">
<button type="submit" class="bg-cyan-500 text-white px-5 py-2.5 rounded-xl text-sm font-bold hover:bg-cyan-600 transition flex items-center gap-2"> <button type="submit" class="bg-cyan-500 text-white px-5 py-2.5 rounded-xl text-sm font-bold hover:bg-cyan-600 transition flex items-center gap-2">
<i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %} <i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %}

View File

@ -33,20 +33,6 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
{% if request.user.is_px_admin %}
<div class="col-md-4">
<label class="form-label">{% translate "Hospital" %}</label>
<select name="hospital" class="form-select">
<option value="">{% translate "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>
{% endif %}
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">{% translate "Threshold Type" %}</label> <label class="form-label">{% translate "Threshold Type" %}</label>
<select name="threshold_type" class="form-select"> <select name="threshold_type" class="form-select">

View File

@ -33,20 +33,6 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
{% if request.user.is_px_admin %}
<div class="col-md-4">
<label class="form-label">{% translate "Hospital" %}</label>
<select name="hospital" class="form-select">
<option value="">{% translate "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>
{% endif %}
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">{% translate "Escalation Level" %}</label> <label class="form-label">{% translate "Escalation Level" %}</label>
<select name="escalation_level" class="form-select"> <select name="escalation_level" class="form-select">

View File

@ -33,20 +33,6 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
{% if request.user.is_px_admin %}
<div class="col-md-3">
<label class="form-label">{% translate "Hospital" %}</label>
<select name="hospital" class="form-select">
<option value="">{% translate "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>
{% endif %}
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">{% translate "Severity" %}</label> <label class="form-label">{% translate "Severity" %}</label>
<select name="severity" class="form-select"> <select name="severity" class="form-select">

View File

@ -102,17 +102,6 @@
<!-- Filters --> <!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 animate-in"> <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"> <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> <div>
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label> <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"> <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">

View File

@ -251,25 +251,10 @@
</select> </select>
</div> </div>
<!-- Hospital Filter -->
{% if hospitals.exists %}
<div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Hospital" %}</label>
<select class="form-select-px360 w-full" id="hospitalFilter">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if selected_hospital_id == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<!-- Department Filter --> <!-- Department Filter -->
<div> <div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Department" %}</label> <label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Department" %}</label>
<select class="form-select-px360 w-full" id="departmentFilter" {% if not selected_hospital_id and not request.user.hospital %}disabled{% endif %}> <select class="form-select-px360 w-full" id="departmentFilter">
<option value="">{% trans "All Departments" %}</option> <option value="">{% trans "All Departments" %}</option>
{% for department in departments %} {% for department in departments %}
<option value="{{ department.id }}" {% if selected_department_id == department.id|stringformat:"s" %}selected{% endif %}> <option value="{{ department.id }}" {% if selected_department_id == department.id|stringformat:"s" %}selected{% endif %}>
@ -730,7 +715,7 @@ function updateSummaryCards(data) {
function exportReport(format) { function exportReport(format) {
const dateRange = document.getElementById('dateRange').value; const dateRange = document.getElementById('dateRange').value;
const hospital = document.getElementById('hospitalFilter')?.value || ''; const hospital = '{{ current_hospital.id|default:"" }}';
const department = document.getElementById('departmentFilter')?.value || ''; const department = document.getElementById('departmentFilter')?.value || '';
const params = new URLSearchParams({ const params = new URLSearchParams({

View File

@ -244,19 +244,6 @@
</select> </select>
</div> </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 }}
</option>
{% endfor %}
</select>
</div>
<!-- Rating Range --> <!-- Rating Range -->
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">{% trans "Min Rating" %}</label> <label class="form-label">{% trans "Min Rating" %}</label>

View File

@ -1,79 +1,77 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Survey Template Mappings" %} - {{ block.super }}{% endblock %} {% block title %}{% trans "Survey Template Mappings" %} - PX360{% endblock %}
{% block content %} {% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <!-- Page Header -->
<!-- Page Header --> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<div> <div>
<h2 class="text-3xl font-bold text-gray-800 mb-2 flex items-center gap-2"> <h1 class="text-2xl font-bold text-navy flex items-center gap-2">
<i data-lucide="layers" class="w-8 h-8 text-navy"></i> <i data-lucide="layers" class="w-7 h-7 text-blue"></i>
{% trans "Survey Template Mappings" %} {% trans "Survey Template Mappings" %}
</h2> </h1>
<p class="text-gray-500"> <p class="text-slate mt-1">{% trans "Configure which survey templates are sent for each patient type at each hospital" %}</p>
{% trans "Configure which survey templates are sent for each patient type at each hospital." %}
</p>
</div> </div>
<button id="addMappingBtn" class="bg-light0 text-white px-6 py-3 rounded-xl font-bold hover:bg-navy transition flex items-center gap-2 shadow-lg shadow-blue-200 hover:shadow-xl hover:-translate-y-0.5"> <button id="addMappingBtn" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center gap-2">
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Add Mapping" %} <i data-lucide="plus" class="w-5 h-5"></i> {% trans "Add Mapping" %}
</button> </button>
</div> </div>
<!-- Mappings List --> <!-- Mappings List Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 overflow-hidden"> <div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
{% if mappings %} {% if mappings %}
<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-slate-50 border-b border-slate-100">
<tr> <tr class="text-xs font-bold text-slate uppercase tracking-wider">
<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">{% trans "Hospital" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Patient Type" %}</th> <th class="px-6 py-4 text-left">{% trans "Patient Type" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Survey Template" %}</th> <th class="px-6 py-4 text-left">{% trans "Survey Template" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Status" %}</th> <th class="px-6 py-4 text-left">{% trans "Status" %}</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th> <th class="px-6 py-4 text-left">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100"> <tbody class="divide-y divide-slate-100">
{% for mapping in mappings %} {% for mapping in mappings %}
<tr class="hover:bg-gray-50 transition"> <tr class="hover:bg-light/30 transition">
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="font-semibold text-gray-800">{{ mapping.hospital.name }}</div> <div class="font-semibold text-navy">
{% if mapping.hospital %}
{{ mapping.hospital.name }}
{% else %}
<span class="text-slate italic">{% trans "All Hospitals" %}</span>
{% endif %}
</div>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-blue-100 text-blue-700"> <span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-blue-100 text-blue-700">
{{ mapping.patient_type_display }} {{ mapping.get_patient_type_display }}
</span> </span>
<div class="text-xs text-gray-400 mt-1">({{ mapping.patient_type }})</div>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="font-semibold text-gray-800">{{ mapping.survey_template.name }}</div> <div class="font-semibold text-navy">{{ mapping.survey_template.name }}</div>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{% if mapping.is_active %} {% if mapping.is_active %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700"> <span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-green-100 text-green-700">{% trans "Active" %}</span>
{% trans "Active" %}
</span>
{% else %} {% else %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600"> <span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 text-slate-600">{% trans "Inactive" %}</span>
{% trans "Inactive" %}
</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex gap-2"> <div class="flex gap-2">
<button class="edit-mapping px-3 py-2 text-navy bg-light rounded-lg hover:bg-light transition font-medium text-sm flex items-center gap-1" <button class="edit-mapping px-3 py-2 text-navy bg-light rounded-lg hover:bg-light/80 transition font-medium text-sm flex items-center gap-1"
data-id="{{ mapping.id }}" data-id="{{ mapping.id }}"
data-hospital="{{ mapping.hospital.id }}" data-hospital="{{ mapping.hospital.id|default:'' }}"
data-patient-type="{{ mapping.patient_type }}" data-patient-type="{{ mapping.patient_type }}"
data-survey-template="{{ mapping.survey_template.id }}" data-survey-template="{{ mapping.survey_template.id }}"
data-active="{{ mapping.is_active }}"> data-active="{{ mapping.is_active }}">
<i data-lucide="edit" class="w-4 h-4"></i> {% trans "Edit" %} <i data-lucide="pencil" class="w-4 h-4"></i> {% trans "Edit" %}
</button> </button>
<button class="delete-mapping px-3 py-2 text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition font-medium text-sm flex items-center gap-1" <button class="delete-mapping px-3 py-2 text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition font-medium text-sm flex items-center gap-1"
data-id="{{ mapping.id }}" data-id="{{ mapping.id }}"
data-name="{{ mapping.patient_type_display }} - {{ mapping.survey_template.name }}"> data-name="{{ mapping.get_patient_type_display }} - {{ mapping.survey_template.name }}">
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Delete" %} <i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Delete" %}
</button> </button>
</div> </div>
@ -84,38 +82,49 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="p-12 text-center"> <!-- Empty State -->
<div class="inline-flex items-center justify-center w-20 h-20 bg-blue-100 rounded-full mb-4"> <div class="text-center py-16">
<i data-lucide="layers" class="w-10 h-10 text-blue-500"></i> <div class="inline-flex items-center justify-center w-20 h-20 bg-slate-100 rounded-full mb-4">
<i data-lucide="layers" class="w-10 h-10 text-slate-400"></i>
</div> </div>
<h3 class="text-xl font-bold text-gray-800 mb-2">{% trans "No Mappings Configured" %}</h3> <h3 class="text-xl font-bold text-navy mb-2">{% trans "No Mappings Configured" %}</h3>
<p class="text-gray-500 mb-4">{% trans "No survey template mappings configured yet. Click 'Add Mapping' to create your first mapping." %}</p> <p class="text-slate mb-6">{% trans "No survey template mappings configured yet. Click 'Add Mapping' to create your first mapping." %}</p>
<button id="addMappingBtnEmpty" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition inline-flex items-center gap-2">
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Add Mapping" %}
</button>
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
<!-- Add/Edit Mapping Modal --> <!-- Add/Edit Mapping Modal -->
<div id="addMappingModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"> <div id="addMappingModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
<div class="bg-white rounded-2xl shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto"> <div class="flex items-center justify-center min-h-screen px-4">
<div class="sticky top-0 bg-white border-b border-gray-200 px-8 py-6 flex justify-between items-center"> <div class="fixed inset-0 bg-black/50 modal-backdrop"></div>
<h3 id="mappingModalTitle" class="text-2xl font-bold text-gray-800">{% trans "Add Survey Template Mapping" %}</h3> <div class="relative bg-white rounded-2xl max-w-2xl w-full shadow-xl overflow-hidden">
<button id="closeAddModal" class="text-gray-400 hover:text-gray-600 transition"> <!-- Modal Header -->
<i data-lucide="x" class="w-6 h-6"></i> <div class="bg-slate-50 border-b border-slate-100 px-6 py-4 flex justify-between items-center">
<h3 id="mappingModalTitle" class="text-xl font-bold text-navy flex items-center gap-2">
<i data-lucide="layers" class="w-6 h-6 text-blue"></i>
{% trans "Add Survey Template Mapping" %}
</h3>
<button id="closeAddModal" class="text-slate hover:text-navy transition p-1 hover:bg-light rounded-lg">
<i data-lucide="x" class="w-5 h-5"></i>
</button> </button>
</div> </div>
<div class="p-8">
{% csrf_token %} <!-- Modal Body -->
<form id="mappingForm" class="space-y-6"> <div class="p-6">
<form id="mappingForm" class="space-y-5">
<input type="hidden" id="mappingId" name="id"> <input type="hidden" id="mappingId" name="id">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Hospital -->
<div> <div>
<label for="hospital" class="block text-sm font-bold text-gray-700 mb-2"> <label for="hospital" class="block text-sm font-bold text-slate mb-2">
{% trans "Hospital" %} <span class="text-red-500">*</span> {% trans "Hospital" %} <span class="text-red-500">*</span>
</label> </label>
<select id="hospital" name="hospital" required <select id="hospital" name="hospital" required
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"> class="form-select w-full px-4 py-2.5 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white">
<option value="">{% trans "Select Hospital" %}</option> <option value="">{% trans "Select Hospital" %}</option>
{% for hospital in hospitals %} {% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name }}</option> <option value="{{ hospital.id }}">{{ hospital.name }}</option>
@ -123,79 +132,100 @@
</select> </select>
</div> </div>
<!-- Survey Template -->
<div> <div>
<label for="surveyTemplate" class="block text-sm font-bold text-gray-700 mb-2"> <label for="surveyTemplate" class="block text-sm font-bold text-slate mb-2">
{% trans "Survey Template" %} <span class="text-red-500">*</span> {% trans "Survey Template" %} <span class="text-red-500">*</span>
</label> </label>
<select id="surveyTemplate" name="survey_template" required <select id="surveyTemplate" name="survey_template" required
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"> class="form-select w-full px-4 py-2.5 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white">
<option value="">{% trans "Select Survey Template" %}</option> <option value="">{% trans "Select Survey Template" %}</option>
</select> </select>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Patient Type -->
<div> <div>
<label for="patientType" class="block text-sm font-bold text-gray-700 mb-2"> <label for="patientType" class="block text-sm font-bold text-slate mb-2">
{% trans "Patient Type" %} <span class="text-red-500">*</span> {% trans "Patient Type" %} <span class="text-red-500">*</span>
</label> </label>
<select id="patientType" name="patient_type" required <select id="patientType" name="patient_type" required
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"> class="form-select w-full px-4 py-2.5 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white">
<option value="">{% trans "Select Patient Type" %}</option> <option value="">{% trans "Select Patient Type" %}</option>
<option value="1">1 - {% trans "Inpatient (Type 1)" %}</option> <option value="1">{% trans "Inpatient (Type 1)" %}</option>
<option value="2">2 - {% trans "Outpatient (Type 2)" %}</option> <option value="2">{% trans "Outpatient (Type 2)" %}</option>
<option value="3">3 - {% trans "Emergency (Type 3)" %}</option> <option value="3">{% trans "Emergency (Type 3)" %}</option>
<option value="4">4 - {% trans "Day Case (Type 4)" %}</option> <option value="4">{% trans "Day Case (Type 4)" %}</option>
<option value="APPOINTMENT">APPOINTMENT - {% trans "Appointment" %}</option> <option value="APPOINTMENT">{% trans "Appointment" %}</option>
</select> </select>
</div> </div>
<!-- Status -->
<div> <div>
<label for="isActive" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Status" %}</label> <label class="block text-sm font-bold text-slate mb-2">{% trans "Status" %}</label>
<div class="flex items-center gap-3 pt-3"> <label class="flex items-center gap-3 p-3 bg-light/50 rounded-xl border border-slate-100 cursor-pointer hover:bg-light transition">
<input type="checkbox" id="isActive" name="is_active" checked <input type="checkbox" id="isActive" name="is_active" checked
class="w-5 h-5 text-navy border-2 border-gray-300 rounded focus:ring-navy focus:ring-offset-2"> class="w-5 h-5 text-navy border-2 border-slate-300 rounded focus:ring-navy focus:ring-offset-2">
<label for="isActive" class="text-gray-700 font-medium">{% trans "Active" %}</label> <span class="text-navy font-medium">{% trans "Active" %}</span>
</div> </label>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<div class="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-8 py-6 flex justify-end gap-3">
<button id="cancelAddModal" class="border-2 border-gray-300 text-gray-700 px-6 py-3 rounded-xl font-bold hover:bg-gray-100 transition flex items-center gap-2"> <!-- Modal Footer -->
<i data-lucide="x" class="w-5 h-5"></i> {% trans "Cancel" %} <div class="bg-slate-50 border-t border-slate-100 px-6 py-4 flex justify-end gap-3">
<button id="cancelAddModal" class="px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-white hover:text-navy transition">
{% trans "Cancel" %}
</button> </button>
<button id="saveMapping" class="bg-light0 text-white px-6 py-3 rounded-xl font-bold hover:bg-navy transition flex items-center gap-2 shadow-lg shadow-blue-200"> <button id="saveMapping" 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="save" class="w-5 h-5"></i> {% trans "Save Mapping" %} <i data-lucide="save" class="w-4 h-4"></i> {% trans "Save Mapping" %}
</button> </button>
</div> </div>
</div> </div>
</div>
</div> </div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"> <div id="deleteModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4"> <div class="flex items-center justify-center min-h-screen px-4">
<div class="p-8"> <div class="fixed inset-0 bg-black/50 modal-backdrop"></div>
<div class="text-center mb-6"> <div class="relative bg-white rounded-2xl max-w-md w-full shadow-xl overflow-hidden">
<div class="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4"> <!-- Modal Header -->
<i data-lucide="alert-triangle" class="w-8 h-8 text-red-500"></i> <div class="bg-red-50 border-b border-red-100 px-6 py-4 flex justify-between items-center">
</div> <h3 class="text-xl font-bold text-red-700 flex items-center gap-2">
<h3 class="text-xl font-bold text-gray-800 mb-2">{% trans "Confirm Delete" %}</h3> <i data-lucide="alert-triangle" class="w-6 h-6 text-red-600"></i>
<p class="text-gray-500">{% trans "Are you sure you want to delete this mapping?" %}</p> {% trans "Confirm Delete" %}
<p id="deleteMappingName" class="font-bold text-gray-800 mt-2"></p> </h3>
</div> <button id="cancelDeleteModal" class="text-red-400 hover:text-red-700 transition p-1 hover:bg-red-100 rounded-lg">
<div class="flex justify-center gap-3"> <i data-lucide="x" class="w-5 h-5"></i>
<button id="cancelDeleteModal" class="border-2 border-gray-300 text-gray-700 px-6 py-3 rounded-xl font-bold hover:bg-gray-100 transition flex items-center gap-2">
<i data-lucide="x" class="w-5 h-5"></i> {% trans "Cancel" %}
</button> </button>
<button id="confirmDelete" class="bg-red-600 text-white px-6 py-3 rounded-xl font-bold hover:bg-red-700 transition flex items-center gap-2 shadow-lg shadow-red-200"> </div>
<i data-lucide="trash-2" class="w-5 h-5"></i> {% trans "Delete" %}
<!-- Modal Body -->
<div class="p-6">
<div class="text-center">
<p class="text-slate mb-4">{% trans "Are you sure you want to delete this mapping?" %}</p>
<p id="deleteMappingName" class="font-bold text-navy bg-light/50 p-3 rounded-xl inline-block"></p>
</div>
</div>
<!-- Modal Footer -->
<div class="bg-slate-50 border-t border-slate-100 px-6 py-4 flex justify-end gap-3">
<button id="cancelDeleteBtn" class="px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-white hover:text-navy transition">
{% trans "Cancel" %}
</button>
<button id="confirmDelete" class="px-4 py-2 bg-red-600 text-white rounded-xl font-semibold hover:bg-red-700 transition flex items-center gap-2">
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Delete" %}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const API_URL = '/api/integrations/survey-template-mappings/'; const API_URL = '/api/integrations/survey-template-mappings/';
@ -205,33 +235,36 @@ document.addEventListener('DOMContentLoaded', function() {
const addModal = document.getElementById('addMappingModal'); const addModal = document.getElementById('addMappingModal');
const deleteModal = document.getElementById('deleteModal'); const deleteModal = document.getElementById('deleteModal');
const addMappingBtn = document.getElementById('addMappingBtn'); const addMappingBtn = document.getElementById('addMappingBtn');
const addMappingBtnEmpty = document.getElementById('addMappingBtnEmpty');
const closeAddModal = document.getElementById('closeAddModal'); const closeAddModal = document.getElementById('closeAddModal');
const cancelAddModal = document.getElementById('cancelAddModal'); const cancelAddModal = document.getElementById('cancelAddModal');
const closeDeleteModal = document.getElementById('cancelDeleteModal'); const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
const cancelDeleteModal = document.getElementById('cancelDeleteModal');
// Show/Hide modal helper functions // Show/Hide modal helper functions
function showModal(modal) { function showModal(modal) {
modal.classList.remove('hidden'); modal.classList.remove('hidden');
modal.classList.add('flex'); lucide.createIcons();
} }
function hideModal(modal) { function hideModal(modal) {
modal.classList.add('hidden'); modal.classList.add('hidden');
modal.classList.remove('flex');
} }
// Event listeners for modal controls // Event listeners for modal controls
addMappingBtn.addEventListener('click', () => showModal(addModal)); if (addMappingBtn) addMappingBtn.addEventListener('click', () => showModal(addModal));
if (addMappingBtnEmpty) addMappingBtnEmpty.addEventListener('click', () => showModal(addModal));
closeAddModal.addEventListener('click', () => hideModal(addModal)); closeAddModal.addEventListener('click', () => hideModal(addModal));
cancelAddModal.addEventListener('click', () => hideModal(addModal)); cancelAddModal.addEventListener('click', () => hideModal(addModal));
closeDeleteModal.addEventListener('click', () => hideModal(deleteModal)); cancelDeleteBtn.addEventListener('click', () => hideModal(deleteModal));
cancelDeleteModal.addEventListener('click', () => hideModal(deleteModal));
// Close modal when clicking outside // Close modal when clicking outside
addModal.addEventListener('click', (e) => { addModal.addEventListener('click', (e) => {
if (e.target === addModal) hideModal(addModal); if (e.target === addModal.querySelector('.modal-backdrop')) hideModal(addModal);
}); });
deleteModal.addEventListener('click', (e) => { deleteModal.addEventListener('click', (e) => {
if (e.target === deleteModal) hideModal(deleteModal); if (e.target === deleteModal.querySelector('.modal-backdrop')) hideModal(deleteModal);
}); });
// Load survey templates when hospital changes // Load survey templates when hospital changes
@ -239,12 +272,12 @@ document.addEventListener('DOMContentLoaded', function() {
const hospitalId = this.value; const hospitalId = this.value;
const surveyTemplateSelect = document.getElementById('surveyTemplate'); const surveyTemplateSelect = document.getElementById('surveyTemplate');
// Get any pending value BEFORE clearing options
const pendingValue = surveyTemplateSelect.dataset.pendingValue;
// Clear existing options // Clear existing options
surveyTemplateSelect.innerHTML = '<option value="">{% trans "Select Survey Template" %}</option>'; surveyTemplateSelect.innerHTML = '<option value="">{% trans "Select Survey Template" %}</option>';
// Store of pending survey template ID to set after loading
surveyTemplateSelect.dataset.pendingValue = '';
if (hospitalId) { if (hospitalId) {
// Fetch survey templates for this hospital // Fetch survey templates for this hospital
fetch(`/surveys/api/templates/?hospital=${hospitalId}`) fetch(`/surveys/api/templates/?hospital=${hospitalId}`)
@ -260,15 +293,14 @@ document.addEventListener('DOMContentLoaded', function() {
surveyTemplateSelect.appendChild(option); surveyTemplateSelect.appendChild(option);
}); });
// Set of pending value after loading // Set pending value after loading (if it was set before the fetch)
if (surveyTemplateSelect.dataset.pendingValue) { if (pendingValue) {
surveyTemplateSelect.value = surveyTemplateSelect.dataset.pendingValue; surveyTemplateSelect.value = pendingValue;
surveyTemplateSelect.dataset.pendingValue = ''; surveyTemplateSelect.dataset.pendingValue = '';
} }
}) })
.catch(error => { .catch(error => {
console.error('Error loading survey templates:', error); console.error('Error loading survey templates:', error);
console.error('Response data:', error.message);
}); });
} }
}); });
@ -288,10 +320,11 @@ document.addEventListener('DOMContentLoaded', function() {
surveyTemplateSelect.dataset.pendingValue = surveyTemplateId; surveyTemplateSelect.dataset.pendingValue = surveyTemplateId;
// Update modal title // Update modal title
document.getElementById('mappingModalTitle').textContent = '{% trans "Edit Survey Template Mapping" %}'; document.getElementById('mappingModalTitle').innerHTML = `
<i data-lucide="layers" class="w-6 h-6 text-blue"></i> {% trans "Edit Survey Template Mapping" %}
`;
// Trigger hospital change to load survey templates // Trigger hospital change to load survey templates
// The survey template value will be set after loading completes
document.getElementById('hospital').dispatchEvent(new Event('change')); document.getElementById('hospital').dispatchEvent(new Event('change'));
showModal(addModal); showModal(addModal);
@ -310,8 +343,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Confirm delete // Confirm delete
document.getElementById('confirmDelete').addEventListener('click', function() { document.getElementById('confirmDelete').addEventListener('click', function() {
if (deleteMappingId) { if (deleteMappingId) {
// Get CSRF token const csrfToken = getCookie('csrftoken');
const csrfToken = getCSRFToken();
if (!csrfToken) { if (!csrfToken) {
alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}'); alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}');
return; return;
@ -339,12 +371,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Save mapping // Save mapping
document.getElementById('saveMapping').addEventListener('click', function(event) { document.getElementById('saveMapping').addEventListener('click', function(event) {
// Prevent form submission
event.preventDefault(); event.preventDefault();
const form = document.getElementById('mappingForm'); const form = document.getElementById('mappingForm');
// Validate required fields
if (!form.checkValidity()) { if (!form.checkValidity()) {
form.reportValidity(); form.reportValidity();
return; return;
@ -359,27 +389,24 @@ document.addEventListener('DOMContentLoaded', function() {
is_active: document.getElementById('isActive').checked is_active: document.getElementById('isActive').checked
}; };
console.log('Saving mapping:', data);
const url = mappingId ? `${API_URL}${mappingId}/` : API_URL; const url = mappingId ? `${API_URL}${mappingId}/` : API_URL;
const method = mappingId ? 'PUT' : 'POST'; const method = mappingId ? 'PUT' : 'POST';
// Disable button to prevent double submission
const saveButton = this; const saveButton = this;
const originalContent = saveButton.innerHTML;
saveButton.disabled = true; saveButton.disabled = true;
saveButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>{% trans "Saving..." %}'; saveButton.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> {% trans "Saving..." %}';
lucide.createIcons();
// Get CSRF token const csrfToken = getCookie('csrftoken');
const csrfToken = getCSRFToken();
if (!csrfToken) { if (!csrfToken) {
alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}'); alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}');
saveButton.disabled = false; saveButton.disabled = false;
saveButton.innerHTML = '<i data-lucide="save" class="w-5 h-5"></i> {% trans "Save Mapping" %}'; saveButton.innerHTML = originalContent;
lucide.createIcons();
return; return;
} }
console.log('CSRF Token:', csrfToken.substring(0, 20) + '...');
fetch(url, { fetch(url, {
method: method, method: method,
headers: { headers: {
@ -389,7 +416,6 @@ document.addEventListener('DOMContentLoaded', function() {
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
.then(response => { .then(response => {
console.log('Response status:', response.status);
if (!response.ok) { if (!response.ok) {
return response.json().then(err => { return response.json().then(err => {
throw new Error(JSON.stringify(err)); throw new Error(JSON.stringify(err));
@ -398,17 +424,15 @@ document.addEventListener('DOMContentLoaded', function() {
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
console.log('Success:', data);
// Hide modal and reload page
hideModal(addModal); hideModal(addModal);
location.reload(); location.reload();
}) })
.catch(error => { .catch(error => {
console.error('Error saving mapping:', error); console.error('Error saving mapping:', error);
alert('{% trans "Error saving mapping" %}: ' + error.message); alert('{% trans "Error saving mapping" %}: ' + error.message);
// Re-enable button
saveButton.disabled = false; saveButton.disabled = false;
saveButton.innerHTML = '<i data-lucide="save" class="w-5 h-5"></i> {% trans "Save Mapping" %}'; saveButton.innerHTML = originalContent;
lucide.createIcons();
}); });
}); });
@ -416,37 +440,12 @@ document.addEventListener('DOMContentLoaded', function() {
addModal.addEventListener('hidden', function() { addModal.addEventListener('hidden', function() {
document.getElementById('mappingForm').reset(); document.getElementById('mappingForm').reset();
document.getElementById('mappingId').value = ''; document.getElementById('mappingId').value = '';
document.getElementById('mappingModalTitle').textContent = '{% trans "Add Survey Template Mapping" %}'; document.getElementById('mappingModalTitle').innerHTML = `
<i data-lucide="layers" class="w-6 h-6 text-blue"></i> {% trans "Add Survey Template Mapping" %}
`;
lucide.createIcons();
}); });
// CSRF token helper - multiple methods for reliability
function getCSRFToken() {
// Method 1: Try to get from hidden input (most reliable)
const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
if (csrfInput) {
return csrfInput.value;
}
// Method 2: Try to get from cookie (case-insensitive)
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Check both 'csrftoken' and 'csrfToken' (case-insensitive)
if (cookie.toLowerCase().startsWith('csrftoken=')) {
return decodeURIComponent(cookie.substring(10));
}
}
// Method 3: Check for Django's meta tag
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
console.error('Unable to find CSRF token');
return null;
}
// Initialize Lucide icons // Initialize Lucide icons
lucide.createIcons(); lucide.createIcons();
}); });

View File

@ -150,18 +150,6 @@
</select> </select>
</div> </div>
<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>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">{% trans "Department" %}</label> <label class="form-label">{% trans "Department" %}</label>
<select class="form-select" name="department"> <select class="form-select" name="department">

View File

@ -68,20 +68,6 @@
</div> </div>
</div> </div>
{% if request.user.is_px_admin %}
<div>
<label class="field-label">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
<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>
{% endif %}
<div> <div>
<label class="field-label">{% trans "Status" %}</label> <label class="field-label">{% trans "Status" %}</label>
<select name="status" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition"> <select name="status" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">

View File

@ -196,17 +196,6 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-3"> <form method="get" class="row g-3">
<div class="col-md-3">
<label class="form-label">{% trans "Hospital" %}</label>
<select name="hospital" class="form-select">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if request.GET.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">{% trans "Department" %}</label> <label class="form-label">{% trans "Department" %}</label>
<select name="department" class="form-select"> <select name="department" class="form-select">

View File

@ -50,17 +50,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-4">
<label class="form-label">{% trans "Hospital" %}</label>
<select name="hospital" class="form-select">
<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="col-md-2 d-flex align-items-end"> <div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100"> <button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search me-2"></i>{% trans "Filter" %} <i class="bi bi-search me-2"></i>{% trans "Filter" %}

View File

@ -64,18 +64,6 @@
<!-- Filters --> <!-- Filters -->
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mb-6"> <section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mb-6">
<form method="get" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4"> <form method="get" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div>
<label class="field-label">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
<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> <div>
<label class="field-label">{% trans "Doctor ID" %}</label> <label class="field-label">{% trans "Doctor ID" %}</label>
<input type="text" name="doctor_id" <input type="text" name="doctor_id"

View File

@ -129,18 +129,6 @@
</select> </select>
</div> </div>
<div>
<label class="field-label">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
<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> <div>
<label class="field-label">{% trans "Department" %}</label> <label class="field-label">{% trans "Department" %}</label>
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition"> <select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">

View File

@ -105,18 +105,6 @@
</div> </div>
</div> </div>
<div>
<label class="field-label">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
<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> <div>
<label class="field-label">{% trans "Department" %}</label> <label class="field-label">{% trans "Department" %}</label>
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition"> <select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">

View File

@ -276,17 +276,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
<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> <div>
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label> <label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition"> <select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">

View File

@ -57,17 +57,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2">
<label class="form-label">{% trans "Hospital" %}</label>
<select name="hospital" class="form-select">
<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="col-md-3"> <div class="col-md-3">
<label class="form-label">{% trans "Department" %}</label> <label class="form-label">{% trans "Department" %}</label>
<select name="department" class="form-select"> <select name="department" class="form-select">

View File

@ -50,17 +50,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-4">
<label class="form-label">{% trans "Hospital" %}</label>
<select name="hospital" class="form-select">
<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="col-md-2 d-flex align-items-end"> <div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100"> <button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search me-2"></i>{% trans "Filter" %} <i class="bi bi-search me-2"></i>{% trans "Filter" %}

View File

@ -105,20 +105,7 @@
<a href="?status=completed" class="filter-btn px-4 py-1.5 rounded-full text-xs font-semibold transition {% if filters.status == 'completed' %}active{% endif %}"> <a href="?status=completed" class="filter-btn px-4 py-1.5 rounded-full text-xs font-semibold transition {% if filters.status == 'completed' %}active{% endif %}">
{% trans "Completed" %} {% trans "Completed" %}
</a> </a>
{% if user.is_px_admin %}
<div class="h-4 w-[1px] bg-slate-200 mx-2"></div> <div class="h-4 w-[1px] bg-slate-200 mx-2"></div>
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</label>
<select id="hospitalFilter" class="px-3 py-1.5 bg-white border rounded-lg text-xs">
<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>
{% endif %}
</div> </div>
</div> </div>
<p class="px-6 py-2 text-[10px] font-bold text-slate uppercase bg-slate-50 border-b-2 border-slate-200"> <p class="px-6 py-2 text-[10px] font-bold text-slate uppercase bg-slate-50 border-b-2 border-slate-200">
@ -297,18 +284,8 @@ document.getElementById('searchInput')?.addEventListener('keypress', function(e)
let url = '?'; let url = '?';
if (value) url += 'search=' + encodeURIComponent(value); if (value) url += 'search=' + encodeURIComponent(value);
{% if filters.status %}url += '&status={{ filters.status }}';{% endif %} {% if filters.status %}url += '&status={{ filters.status }}';{% endif %}
{% if filters.hospital %}url += '&hospital={{ filters.hospital }}';{% endif %}
window.location.href = url; window.location.href = url;
} }
}); });
// Hospital filter
document.getElementById('hospitalFilter')?.addEventListener('change', function() {
let url = '?';
if (this.value) url += 'hospital=' + this.value;
{% if filters.search %}url += '&search={{ filters.search }}';{% endif %}
{% if filters.status %}url += '&status={{ filters.status }}';{% endif %}
window.location.href = url;
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -76,17 +76,6 @@
</div> </div>
</div> </div>
<!-- Hospital Filter -->
<div class="mb-6">
<label class="block text-sm font-bold text-navy mb-2">Hospital</label>
<select id="hospitalFilter" class="w-full px-4 py-2.5 border-2 border-blue-100 rounded-xl text-navy focus:ring-2 focus:ring-blue focus:border-transparent transition bg-white">
<option value="">All Hospitals</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
{% endfor %}
</select>
</div>
<!-- Department Filter --> <!-- Department Filter -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm font-bold text-navy mb-2">Department</label> <label class="block text-sm font-bold text-navy mb-2">Department</label>
@ -225,7 +214,6 @@ const CSRF_TOKEN = '{{ csrf_token }}';
// DOM elements // DOM elements
let dataSourceSelect = null; let dataSourceSelect = null;
let dateRangeSelect = null; let dateRangeSelect = null;
let hospitalFilter = null;
let departmentFilter = null; let departmentFilter = null;
let statusFilter = null; let statusFilter = null;
let previewBtn = null; let previewBtn = null;
@ -244,7 +232,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize DOM element references // Initialize DOM element references
dataSourceSelect = document.getElementById('dataSource'); dataSourceSelect = document.getElementById('dataSource');
dateRangeSelect = document.getElementById('dateRange'); dateRangeSelect = document.getElementById('dateRange');
hospitalFilter = document.getElementById('hospitalFilter');
departmentFilter = document.getElementById('departmentFilter'); departmentFilter = document.getElementById('departmentFilter');
statusFilter = document.getElementById('statusFilter'); statusFilter = document.getElementById('statusFilter');
previewBtn = document.getElementById('previewBtn'); previewBtn = document.getElementById('previewBtn');
@ -261,8 +248,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Event listeners // Event listeners
if (dataSourceSelect) dataSourceSelect.addEventListener('change', loadFilterOptions); if (dataSourceSelect) dataSourceSelect.addEventListener('change', loadFilterOptions);
if (dateRangeSelect) dateRangeSelect.addEventListener('change', toggleCustomDateRange); if (dateRangeSelect) dateRangeSelect.addEventListener('change', toggleCustomDateRange);
if (hospitalFilter) hospitalFilter.addEventListener('change', loadDepartments);
if (previewBtn) previewBtn.addEventListener('click', generateReport); if (previewBtn) previewBtn.addEventListener('click', generateReport);
// Load departments for current hospital on page load
loadDepartments();
if (saveBtn) saveBtn.addEventListener('click', showSaveModal); if (saveBtn) saveBtn.addEventListener('click', showSaveModal);
if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', hideSaveModal); if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', hideSaveModal);
if (confirmSaveBtn) confirmSaveBtn.addEventListener('click', saveReport); if (confirmSaveBtn) confirmSaveBtn.addEventListener('click', saveReport);
@ -385,7 +374,7 @@ function getSelectedColumns() {
} }
async function loadDepartments() { async function loadDepartments() {
const hospitalId = hospitalFilter.value; const hospitalId = '{{ current_hospital.id|default:"" }}';
if (!hospitalId) { if (!hospitalId) {
departmentFilter.innerHTML = '<option value="">All Departments</option>'; departmentFilter.innerHTML = '<option value="">All Departments</option>';
return; return;
@ -437,7 +426,7 @@ async function generateReport() {
date_range: dateRangeSelect.value, date_range: dateRangeSelect.value,
date_start: dateRange.start, date_start: dateRange.start,
date_end: dateRange.end, date_end: dateRange.end,
hospital: hospitalFilter.value, hospital: '{{ current_hospital.id|default:"" }}',
department: departmentFilter.value, department: departmentFilter.value,
status: statusFilter.value, status: statusFilter.value,
}, },
@ -983,7 +972,7 @@ async function saveReport() {
date_range: dateRangeSelect.value, date_range: dateRangeSelect.value,
date_start: dateRange.start, date_start: dateRange.start,
date_end: dateRange.end, date_end: dateRange.end,
hospital: hospitalFilter.value, hospital: '{{ current_hospital.id|default:"" }}',
department: departmentFilter.value, department: departmentFilter.value,
status: statusFilter.value, status: statusFilter.value,
}, },

View File

@ -132,20 +132,6 @@
</div> </div>
<!-- Hospital --> <!-- Hospital -->
<div>
<label for="hospital" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</label>
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"
id="hospital" name="hospital">
<option value="">{% trans "All" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}"
{% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Patient Type --> <!-- Patient Type -->
<div> <div>
<label for="patient_type" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Patient Type" %}</label> <label for="patient_type" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Patient Type" %}</label>

View File

@ -60,18 +60,6 @@
</select> </select>
</div> </div>
<div class="flex items-center gap-2">
<label class="text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</label>
<select name="hospital" class="px-3 py-1.5 bg-white border border-slate-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-navy">
<option value="">{% trans "All" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if filters.hospital|add:"0" == hospital.id %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="submit" class="px-4 py-1.5 bg-navy text-white rounded-lg text-xs font-bold hover:bg-blue transition">{% trans "Apply" %}</button> <button type="submit" class="px-4 py-1.5 bg-navy text-white rounded-lg text-xs font-bold hover:bg-blue transition">{% trans "Apply" %}</button>
<a href="{% url 'surveys:instance_list' %}" class="px-4 py-1.5 border border-slate-200 rounded-lg text-xs font-semibold text-slate hover:bg-white transition">{% trans "Clear" %}</a> <a href="{% url 'surveys:instance_list' %}" class="px-4 py-1.5 border border-slate-200 rounded-lg text-xs font-semibold text-slate hover:bg-white transition">{% trans "Clear" %}</a>

View File

@ -126,11 +126,16 @@
<div class="inline-flex items-center justify-center w-20 h-20 bg-slate-100 rounded-full mb-4"> <div class="inline-flex items-center justify-center w-20 h-20 bg-slate-100 rounded-full mb-4">
<i data-lucide="clipboard-list" class="w-10 h-10 text-slate-400"></i> <i data-lucide="clipboard-list" class="w-10 h-10 text-slate-400"></i>
</div> </div>
{% if is_px_admin and not current_hospital %}
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Hospital Selected" %}</h3>
<p class="text-slate mb-6">{% trans "Please select a hospital from the dropdown in the header to view survey templates." %}</p>
{% else %}
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Templates Found" %}</h3> <h3 class="text-xl font-bold text-navy mb-2">{% trans "No Templates Found" %}</h3>
<p class="text-slate mb-6">{% trans "Get started by creating your first survey template." %}</p> <p class="text-slate mb-6">{% trans "Get started by creating your first survey template." %}</p>
<a href="{% url 'surveys:template_create' %}" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition inline-flex items-center gap-2"> <a href="{% url 'surveys:template_create' %}" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition inline-flex items-center gap-2">
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Create Survey Template" %} <i data-lucide="plus" class="w-5 h-5"></i> {% trans "Create Survey Template" %}
</a> </a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>