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=admin/
# Integration APIs (Stubs - Replace with actual credentials)
HIS_API_URL=
# Integration APIs
# 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=
# Other Integration APIs (Stubs - Replace with actual credentials)
MOH_API_URL=
MOH_API_KEY=
CHI_API_URL=

View File

@ -1,6 +1,7 @@
"""
Context processors for global template variables
"""
from django.db.models import Q
@ -25,59 +26,36 @@ def sidebar_counts(request):
# Source Users only see their own created complaints
if user.is_source_user():
complaint_count = Complaint.objects.filter(
created_by=user,
status__in=['open', 'in_progress']
).count()
complaint_count = Complaint.objects.filter(created_by=user, status__in=["open", "in_progress"]).count()
return {
'complaint_count': complaint_count,
'feedback_count': 0,
'action_count': 0,
'current_hospital': None,
'is_px_admin': False,
'is_source_user': True,
"complaint_count": complaint_count,
"feedback_count": 0,
"action_count": 0,
"current_hospital": None,
"is_px_admin": False,
"is_source_user": True,
}
# Filter based on user role and tenant_hospital
if user.is_px_admin():
# PX Admins use their selected hospital from session
hospital = getattr(request, 'tenant_hospital', None)
hospital = getattr(request, "tenant_hospital", None)
if hospital:
complaint_count = Complaint.objects.filter(
hospital=hospital,
status__in=['open', 'in_progress']
).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()
complaint_count = Complaint.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).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:
complaint_count = 0
feedback_count = 0
action_count = 0
# Count provisional users for PX Admin
from apps.accounts.models import User
provisional_user_count = User.objects.filter(
is_provisional=True,
acknowledgement_completed=False
).count()
provisional_user_count = User.objects.filter(is_provisional=True, acknowledgement_completed=False).count()
elif user.hospital:
complaint_count = Complaint.objects.filter(
hospital=user.hospital,
status__in=['open', 'in_progress']
).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()
complaint_count = Complaint.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).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
else:
complaint_count = 0
@ -85,12 +63,12 @@ def sidebar_counts(request):
action_count = 0
return {
'complaint_count': complaint_count,
'feedback_count': feedback_count,
'action_count': action_count,
'current_hospital': getattr(request, 'tenant_hospital', None),
'is_px_admin': request.user.is_authenticated and request.user.is_px_admin(),
'is_source_user': False,
"complaint_count": complaint_count,
"feedback_count": feedback_count,
"action_count": action_count,
"current_hospital": getattr(request, "tenant_hospital", None),
"is_px_admin": request.user.is_authenticated and request.user.is_px_admin(),
"is_source_user": False,
}
@ -103,25 +81,24 @@ def hospital_context(request):
if not request.user.is_authenticated:
return {}
hospital = getattr(request, 'tenant_hospital', None)
hospital = getattr(request, "tenant_hospital", None)
# Get list of hospitals for PX Admin switcher
hospitals_list = []
if request.user.is_px_admin():
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
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 {
'current_hospital': hospital,
'is_px_admin': request.user.is_px_admin(),
'is_source_user': is_source_user,
'source_user_profile': source_user_profile,
'hospitals_list': hospitals_list,
# 'provisional_user_count': provisional_user_count,
"current_hospital": hospital,
"is_px_admin": request.user.is_px_admin(),
"is_source_user": is_source_user,
"source_user_profile": source_user_profile,
"hospitals_list": hospitals_list,
"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"
)
# Delay configuration
send_delay_hours = models.IntegerField(
default=1,
help_text="Hours after discharge to send survey"
)
class Meta:
ordering = ['hospital', 'patient_type']
indexes = [

View File

@ -7,11 +7,13 @@ internal format for sending surveys based on PatientType.
Simplified Flow:
1. Parse HIS patient data
2. Determine survey type from PatientType
3. Create survey instance
4. Send survey via SMS
3. Create survey instance with PENDING status
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
import logging
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.integrations.models import InboundEvent
logger = logging.getLogger(__name__)
class HISAdapter:
"""
@ -191,6 +195,54 @@ class HISAdapter:
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
def create_and_send_survey(
patient: Patient,
@ -199,7 +251,9 @@ class HISAdapter:
survey_template: SurveyTemplate
) -> 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:
patient: Patient instance
@ -210,9 +264,11 @@ class HISAdapter:
Returns:
SurveyInstance or None if failed
"""
from apps.surveys.tasks import send_scheduled_survey
admission_id = patient_data.get("AdmissionID")
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
existing_survey = SurveyInstance.objects.filter(
@ -222,43 +278,49 @@ class HISAdapter:
).first()
if existing_survey:
logger.info(f"Survey already exists for admission {admission_id}")
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_template=survey_template,
patient=patient,
hospital=hospital,
status=SurveyStatus.SENT, # Set to SENT as it will be sent immediately
delivery_channel="SMS", # Send via SMS
status=SurveyStatus.PENDING, # Changed from SENT
delivery_channel="SMS",
recipient_phone=patient.phone,
recipient_email=patient.email,
scheduled_send_at=scheduled_send_at,
metadata={
'admission_id': admission_id,
'patient_type': patient_data.get("PatientType"),
'patient_type': patient_type,
'hospital_id': patient_data.get("HospitalID"),
'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
try:
from apps.surveys.services import SurveyDeliveryService
delivery_success = SurveyDeliveryService.deliver_survey(survey)
# Queue delayed send task
send_scheduled_survey.apply_async(
args=[str(survey.id)],
countdown=delay_hours * 3600 # Convert to seconds
)
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
logger.info(
f"Survey {survey.id} created for {patient_type}, "
f"will send in {delay_hours}h at {scheduled_send_at}"
)
return survey
@staticmethod
def process_his_data(his_data: Dict) -> Dict:
@ -269,7 +331,8 @@ class HISAdapter:
1. Extract patient data
2. Get or create patient and hospital
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:
his_data: HIS data in real format
@ -282,7 +345,7 @@ class HISAdapter:
'message': '',
'patient': None,
'survey': None,
'survey_sent': False
'survey_queued': False
}
try:
@ -327,16 +390,16 @@ class HISAdapter:
result['message'] = f"No survey template found for patient type '{patient_type}'"
return result
# Create and send survey
# Create and queue survey (delayed sending)
survey = HISAdapter.create_and_send_survey(
patient, hospital, patient_data, survey_template
)
if survey:
from apps.surveys.models import SurveyStatus
survey_sent = survey.status == SurveyStatus.SENT
# Survey is queued with PENDING status
survey_queued = survey.status == SurveyStatus.PENDING
else:
survey_sent = False
survey_queued = False
result.update({
'success': True,
@ -344,13 +407,12 @@ class HISAdapter:
'patient': patient,
'patient_type': patient_type,
'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
})
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error processing HIS data: {str(e)}", exc_info=True)
result['message'] = f"Error processing HIS data: {str(e)}"
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
def fetch_his_surveys():
"""

View File

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

View File

@ -237,6 +237,12 @@ class SurveyInstance(UUIDModel, TimeStampedModel, TenantModel):
# Timestamps
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)
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))
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
user = request.user
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:
queryset = queryset.filter(hospital=user.hospital)
else:

View File

@ -35,6 +35,16 @@ app.conf.beat_schedule = {
'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-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>
<!-- 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 -->
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
@ -463,7 +449,7 @@ function handleDateRangeChange() {
function updateFilters() {
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.kpi_category = document.getElementById('kpiCategoryFilter').value;
currentFilters.custom_start = document.getElementById('customStart').value;
@ -669,7 +655,6 @@ function refreshDashboard() {
function resetFilters() {
document.getElementById('dateRange').value = '30d';
document.getElementById('hospitalFilter').value = '';
document.getElementById('departmentFilter').value = '';
document.getElementById('kpiCategoryFilter').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>
</div>
<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' %}">
<i data-lucide="refresh-cw" class="w-5 h-5 text-white"></i>
</button>

View File

@ -58,19 +58,7 @@
</p>
</div>
<!-- Hospital -->
<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>
<input type="hidden" name="hospital" value="{{ current_hospital.id }}">
<!-- Year and Month -->
<div class="grid grid-cols-2 gap-4">

View File

@ -123,20 +123,6 @@
</select>
</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">
<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">

View File

@ -39,15 +39,9 @@
</select>
</div>
<div class="col-md-6">
<label for="hospital_id" class="form-label">
{% trans "Hospital" %} <span class="text-danger">*</span>
</label>
<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>
<input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
<label class="form-label">{% trans "Hospital" %}</label>
<input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
</div>
</div>
@ -240,31 +234,26 @@
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const hospitalSelect = document.getElementById('hospital_id');
const recipientTypeSelect = document.getElementById('recipient_type');
const recipientSelect = document.getElementById('recipient_id');
const departmentSelect = document.getElementById('department_id');
const recipientHelp = document.getElementById('recipientHelp');
const currentHospitalId = '{{ current_hospital.id|default:"" }}';
let recipientData = [];
// Load recipients when hospital changes
hospitalSelect.addEventListener('change', function() {
const hospitalId = this.value;
// Load recipients and departments on page load
function loadRecipientsAndDepartments() {
const hospitalId = currentHospitalId;
const recipientType = recipientTypeSelect.value;
if (!hospitalId) return;
// Load recipients
recipientSelect.disabled = true;
recipientSelect.innerHTML = '<option value="">Loading...</option>';
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'
? "{% url 'appreciation:get_users_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>';
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>';
if (!hospitalId) return;
fetch("{% url 'appreciation:get_departments_by_hospital' %}?hospital_id=" + hospitalId)
.then(response => response.json())
.then(data => {
@ -313,14 +296,13 @@ document.addEventListener('DOMContentLoaded', function() {
.catch(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>
{% endblock %}

View File

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

View File

@ -116,18 +116,6 @@
</select>
</div>
<div class="w-48">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm bg-white">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div class="w-40">
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "From Date" %}</label>
<input type="date" name="date_from" value="{{ filters.date_from }}"

View File

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

View File

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

View File

@ -33,20 +33,6 @@
</div>
<div class="card-body">
<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">
<label class="form-label">{% translate "Threshold Type" %}</label>
<select name="threshold_type" class="form-select">

View File

@ -33,20 +33,6 @@
</div>
<div class="card-body">
<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">
<label class="form-label">{% translate "Escalation Level" %}</label>
<select name="escalation_level" class="form-select">

View File

@ -33,20 +33,6 @@
</div>
<div class="card-body">
<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">
<label class="form-label">{% translate "Severity" %}</label>
<select name="severity" class="form-select">

View File

@ -102,17 +102,6 @@
<!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 animate-in">
<form method="get" class="flex flex-wrap gap-4">
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Hospital" %}</label>
<select name="hospital" class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if hospital_filter == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
<select name="is_active" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">

View File

@ -251,25 +251,10 @@
</select>
</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 -->
<div>
<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>
{% for department in departments %}
<option value="{{ department.id }}" {% if selected_department_id == department.id|stringformat:"s" %}selected{% endif %}>
@ -730,7 +715,7 @@ function updateSummaryCards(data) {
function exportReport(format) {
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 params = new URLSearchParams({

View File

@ -244,19 +244,6 @@
</select>
</div>
<!-- Hospital -->
<div class="col-md-4">
<label class="form-label">{% trans "Hospital" %}</label>
<select class="form-select" name="hospital">
<option value="">All Hospitals</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
{{ hospital.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Rating Range -->
<div class="col-md-3">
<label class="form-label">{% trans "Min Rating" %}</label>

View File

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

View File

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

View File

@ -68,20 +68,6 @@
</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>
<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">

View File

@ -196,17 +196,6 @@
<div class="card mb-4">
<div class="card-body">
<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">
<label class="form-label">{% trans "Department" %}</label>
<select name="department" class="form-select">

View File

@ -50,17 +50,6 @@
{% endfor %}
</select>
</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">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search me-2"></i>{% trans "Filter" %}

View File

@ -64,18 +64,6 @@
<!-- Filters -->
<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">
<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>
<label class="field-label">{% trans "Doctor ID" %}</label>
<input type="text" name="doctor_id"

View File

@ -129,18 +129,6 @@
</select>
</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>
<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">

View File

@ -105,18 +105,6 @@
</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>
<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">

View File

@ -275,20 +275,9 @@
</option>
{% endfor %}
</select>
</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>
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
</div>
<div>
<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">
<option value="">{% trans "All Departments" %}</option>
{% for department in departments %}

View File

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

View File

@ -50,17 +50,6 @@
{% endfor %}
</select>
</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">
<button type="submit" class="btn btn-primary w-100">
<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 %}">
{% trans "Completed" %}
</a>
{% if user.is_px_admin %}
<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>
<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 = '?';
if (value) url += 'search=' + encodeURIComponent(value);
{% if filters.status %}url += '&status={{ filters.status }}';{% endif %}
{% if filters.hospital %}url += '&hospital={{ filters.hospital }}';{% endif %}
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>
{% endblock %}

View File

@ -76,17 +76,6 @@
</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 -->
<div class="mb-6">
<label class="block text-sm font-bold text-navy mb-2">Department</label>
@ -225,7 +214,6 @@ const CSRF_TOKEN = '{{ csrf_token }}';
// DOM elements
let dataSourceSelect = null;
let dateRangeSelect = null;
let hospitalFilter = null;
let departmentFilter = null;
let statusFilter = null;
let previewBtn = null;
@ -244,7 +232,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize DOM element references
dataSourceSelect = document.getElementById('dataSource');
dateRangeSelect = document.getElementById('dateRange');
hospitalFilter = document.getElementById('hospitalFilter');
departmentFilter = document.getElementById('departmentFilter');
statusFilter = document.getElementById('statusFilter');
previewBtn = document.getElementById('previewBtn');
@ -261,8 +248,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Event listeners
if (dataSourceSelect) dataSourceSelect.addEventListener('change', loadFilterOptions);
if (dateRangeSelect) dateRangeSelect.addEventListener('change', toggleCustomDateRange);
if (hospitalFilter) hospitalFilter.addEventListener('change', loadDepartments);
if (previewBtn) previewBtn.addEventListener('click', generateReport);
// Load departments for current hospital on page load
loadDepartments();
if (saveBtn) saveBtn.addEventListener('click', showSaveModal);
if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', hideSaveModal);
if (confirmSaveBtn) confirmSaveBtn.addEventListener('click', saveReport);
@ -385,7 +374,7 @@ function getSelectedColumns() {
}
async function loadDepartments() {
const hospitalId = hospitalFilter.value;
const hospitalId = '{{ current_hospital.id|default:"" }}';
if (!hospitalId) {
departmentFilter.innerHTML = '<option value="">All Departments</option>';
return;
@ -437,7 +426,7 @@ async function generateReport() {
date_range: dateRangeSelect.value,
date_start: dateRange.start,
date_end: dateRange.end,
hospital: hospitalFilter.value,
hospital: '{{ current_hospital.id|default:"" }}',
department: departmentFilter.value,
status: statusFilter.value,
},
@ -983,7 +972,7 @@ async function saveReport() {
date_range: dateRangeSelect.value,
date_start: dateRange.start,
date_end: dateRange.end,
hospital: hospitalFilter.value,
hospital: '{{ current_hospital.id|default:"" }}',
department: departmentFilter.value,
status: statusFilter.value,
},

View File

@ -132,20 +132,6 @@
</div>
<!-- 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 -->
<div>
<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>
</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">
<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>

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">
<i data-lucide="clipboard-list" class="w-10 h-10 text-slate-400"></i>
</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>
<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">
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Create Survey Template" %}
</a>
{% endif %}
</div>
{% endif %}
</div>