Remove hospital dropdowns from templates and fix JavaScript dependencies
This commit is contained in:
parent
01fa26c59a
commit
c16e410fdd
@ -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=
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
2267
apps/core/management/commands/setup_dev_environment.py
Normal file
2267
apps/core/management/commands/setup_dev_environment.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 = [
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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():
|
||||
"""
|
||||
|
||||
@ -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] = []
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
245
docs/SETUP_COMPLETE.md
Normal 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
362
docs/SETUP_GUIDE.md
Normal 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
|
||||
@ -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 = '';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 }}"
|
||||
|
||||
@ -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>';
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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>';
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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 %}
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user