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 (change in production)
|
||||||
ADMIN_URL=admin/
|
ADMIN_URL=admin/
|
||||||
|
|
||||||
# Integration APIs (Stubs - Replace with actual credentials)
|
# Integration APIs
|
||||||
HIS_API_URL=
|
# HIS API - Hospital Information System for fetching patient discharge data
|
||||||
|
HIS_API_URL=https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps
|
||||||
|
HIS_API_USERNAME=your_his_username
|
||||||
|
HIS_API_PASSWORD=your_his_password
|
||||||
HIS_API_KEY=
|
HIS_API_KEY=
|
||||||
|
|
||||||
|
# Other Integration APIs (Stubs - Replace with actual credentials)
|
||||||
MOH_API_URL=
|
MOH_API_URL=
|
||||||
MOH_API_KEY=
|
MOH_API_KEY=
|
||||||
CHI_API_URL=
|
CHI_API_URL=
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Context processors for global template variables
|
Context processors for global template variables
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
@ -25,59 +26,36 @@ def sidebar_counts(request):
|
|||||||
|
|
||||||
# Source Users only see their own created complaints
|
# Source Users only see their own created complaints
|
||||||
if user.is_source_user():
|
if user.is_source_user():
|
||||||
complaint_count = Complaint.objects.filter(
|
complaint_count = Complaint.objects.filter(created_by=user, status__in=["open", "in_progress"]).count()
|
||||||
created_by=user,
|
|
||||||
status__in=['open', 'in_progress']
|
|
||||||
).count()
|
|
||||||
return {
|
return {
|
||||||
'complaint_count': complaint_count,
|
"complaint_count": complaint_count,
|
||||||
'feedback_count': 0,
|
"feedback_count": 0,
|
||||||
'action_count': 0,
|
"action_count": 0,
|
||||||
'current_hospital': None,
|
"current_hospital": None,
|
||||||
'is_px_admin': False,
|
"is_px_admin": False,
|
||||||
'is_source_user': True,
|
"is_source_user": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Filter based on user role and tenant_hospital
|
# Filter based on user role and tenant_hospital
|
||||||
if user.is_px_admin():
|
if user.is_px_admin():
|
||||||
# PX Admins use their selected hospital from session
|
# PX Admins use their selected hospital from session
|
||||||
hospital = getattr(request, 'tenant_hospital', None)
|
hospital = getattr(request, "tenant_hospital", None)
|
||||||
if hospital:
|
if hospital:
|
||||||
complaint_count = Complaint.objects.filter(
|
complaint_count = Complaint.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).count()
|
||||||
hospital=hospital,
|
feedback_count = Feedback.objects.filter(hospital=hospital, status__in=["submitted", "reviewed"]).count()
|
||||||
status__in=['open', 'in_progress']
|
action_count = PXAction.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).count()
|
||||||
).count()
|
|
||||||
feedback_count = Feedback.objects.filter(
|
|
||||||
hospital=hospital,
|
|
||||||
status__in=['submitted', 'reviewed']
|
|
||||||
).count()
|
|
||||||
action_count = PXAction.objects.filter(
|
|
||||||
hospital=hospital,
|
|
||||||
status__in=['open', 'in_progress']
|
|
||||||
).count()
|
|
||||||
else:
|
else:
|
||||||
complaint_count = 0
|
complaint_count = 0
|
||||||
feedback_count = 0
|
feedback_count = 0
|
||||||
action_count = 0
|
action_count = 0
|
||||||
# Count provisional users for PX Admin
|
# Count provisional users for PX Admin
|
||||||
from apps.accounts.models import User
|
from apps.accounts.models import User
|
||||||
provisional_user_count = User.objects.filter(
|
|
||||||
is_provisional=True,
|
provisional_user_count = User.objects.filter(is_provisional=True, acknowledgement_completed=False).count()
|
||||||
acknowledgement_completed=False
|
|
||||||
).count()
|
|
||||||
elif user.hospital:
|
elif user.hospital:
|
||||||
complaint_count = Complaint.objects.filter(
|
complaint_count = Complaint.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).count()
|
||||||
hospital=user.hospital,
|
feedback_count = Feedback.objects.filter(hospital=user.hospital, status__in=["submitted", "reviewed"]).count()
|
||||||
status__in=['open', 'in_progress']
|
action_count = PXAction.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).count()
|
||||||
).count()
|
|
||||||
feedback_count = Feedback.objects.filter(
|
|
||||||
hospital=user.hospital,
|
|
||||||
status__in=['submitted', 'reviewed']
|
|
||||||
).count()
|
|
||||||
action_count = PXAction.objects.filter(
|
|
||||||
hospital=user.hospital,
|
|
||||||
status__in=['open', 'in_progress']
|
|
||||||
).count()
|
|
||||||
# provisional_user_count = 0
|
# provisional_user_count = 0
|
||||||
else:
|
else:
|
||||||
complaint_count = 0
|
complaint_count = 0
|
||||||
@ -85,12 +63,12 @@ def sidebar_counts(request):
|
|||||||
action_count = 0
|
action_count = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'complaint_count': complaint_count,
|
"complaint_count": complaint_count,
|
||||||
'feedback_count': feedback_count,
|
"feedback_count": feedback_count,
|
||||||
'action_count': action_count,
|
"action_count": action_count,
|
||||||
'current_hospital': getattr(request, 'tenant_hospital', None),
|
"current_hospital": getattr(request, "tenant_hospital", None),
|
||||||
'is_px_admin': request.user.is_authenticated and request.user.is_px_admin(),
|
"is_px_admin": request.user.is_authenticated and request.user.is_px_admin(),
|
||||||
'is_source_user': False,
|
"is_source_user": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -103,25 +81,24 @@ def hospital_context(request):
|
|||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
hospital = getattr(request, 'tenant_hospital', None)
|
hospital = getattr(request, "tenant_hospital", None)
|
||||||
|
|
||||||
# Get list of hospitals for PX Admin switcher
|
# Get list of hospitals for PX Admin switcher
|
||||||
hospitals_list = []
|
hospitals_list = []
|
||||||
if request.user.is_px_admin():
|
if request.user.is_px_admin():
|
||||||
from apps.organizations.models import Hospital
|
from apps.organizations.models import Hospital
|
||||||
hospitals_list = list(
|
|
||||||
Hospital.objects.filter(status='active').order_by('name').values('id', 'name', 'code')
|
hospitals_list = list(Hospital.objects.filter(status="active").order_by("name").values("id", "name", "code"))
|
||||||
)
|
|
||||||
|
|
||||||
# Source user context
|
# Source user context
|
||||||
is_source_user = request.user.is_source_user()
|
is_source_user = request.user.is_source_user()
|
||||||
source_user_profile = getattr(request, 'source_user_profile', None)
|
source_user_profile = getattr(request, "source_user_profile", None)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'current_hospital': hospital,
|
"current_hospital": hospital,
|
||||||
'is_px_admin': request.user.is_px_admin(),
|
"is_px_admin": request.user.is_px_admin(),
|
||||||
'is_source_user': is_source_user,
|
"is_source_user": is_source_user,
|
||||||
'source_user_profile': source_user_profile,
|
"source_user_profile": source_user_profile,
|
||||||
'hospitals_list': hospitals_list,
|
"hospitals_list": hospitals_list,
|
||||||
# 'provisional_user_count': provisional_user_count,
|
"show_hospital_selector": False,
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
help_text="Whether this mapping is active"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Delay configuration
|
||||||
|
send_delay_hours = models.IntegerField(
|
||||||
|
default=1,
|
||||||
|
help_text="Hours after discharge to send survey"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['hospital', 'patient_type']
|
ordering = ['hospital', 'patient_type']
|
||||||
indexes = [
|
indexes = [
|
||||||
|
|||||||
@ -7,11 +7,13 @@ internal format for sending surveys based on PatientType.
|
|||||||
Simplified Flow:
|
Simplified Flow:
|
||||||
1. Parse HIS patient data
|
1. Parse HIS patient data
|
||||||
2. Determine survey type from PatientType
|
2. Determine survey type from PatientType
|
||||||
3. Create survey instance
|
3. Create survey instance with PENDING status
|
||||||
4. Send survey via SMS
|
4. Queue delayed send task
|
||||||
|
5. Survey sent after delay (e.g., 1 hour for OPD)
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -19,6 +21,8 @@ from apps.organizations.models import Hospital, Patient
|
|||||||
from apps.surveys.models import SurveyTemplate, SurveyInstance, SurveyStatus
|
from apps.surveys.models import SurveyTemplate, SurveyInstance, SurveyStatus
|
||||||
from apps.integrations.models import InboundEvent
|
from apps.integrations.models import InboundEvent
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HISAdapter:
|
class HISAdapter:
|
||||||
"""
|
"""
|
||||||
@ -191,6 +195,54 @@ class HISAdapter:
|
|||||||
|
|
||||||
return survey_template
|
return survey_template
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_delay_for_patient_type(patient_type: str, hospital) -> int:
|
||||||
|
"""
|
||||||
|
Get delay hours from SurveyTemplateMapping.
|
||||||
|
|
||||||
|
Falls back to default delays if no mapping found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
patient_type: HIS PatientType code (1, 2, 3, 4, O, E)
|
||||||
|
hospital: Hospital instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Delay in hours
|
||||||
|
"""
|
||||||
|
from apps.integrations.models import SurveyTemplateMapping
|
||||||
|
|
||||||
|
# Try to get mapping with delay (hospital-specific)
|
||||||
|
mapping = SurveyTemplateMapping.objects.filter(
|
||||||
|
patient_type=patient_type,
|
||||||
|
hospital=hospital,
|
||||||
|
is_active=True
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if mapping and mapping.send_delay_hours:
|
||||||
|
return mapping.send_delay_hours
|
||||||
|
|
||||||
|
# Fallback to global mapping
|
||||||
|
mapping = SurveyTemplateMapping.objects.filter(
|
||||||
|
patient_type=patient_type,
|
||||||
|
hospital__isnull=True,
|
||||||
|
is_active=True
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if mapping and mapping.send_delay_hours:
|
||||||
|
return mapping.send_delay_hours
|
||||||
|
|
||||||
|
# Default delays by patient type
|
||||||
|
default_delays = {
|
||||||
|
'1': 24, # Inpatient - 24 hours
|
||||||
|
'2': 1, # OPD - 1 hour
|
||||||
|
'3': 2, # EMS - 2 hours
|
||||||
|
'O': 1, # OPD - 1 hour
|
||||||
|
'E': 2, # EMS - 2 hours
|
||||||
|
'4': 4, # Daycase - 4 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
return default_delays.get(patient_type, 1) # Default 1 hour
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_and_send_survey(
|
def create_and_send_survey(
|
||||||
patient: Patient,
|
patient: Patient,
|
||||||
@ -199,7 +251,9 @@ class HISAdapter:
|
|||||||
survey_template: SurveyTemplate
|
survey_template: SurveyTemplate
|
||||||
) -> Optional[SurveyInstance]:
|
) -> Optional[SurveyInstance]:
|
||||||
"""
|
"""
|
||||||
Create survey instance and send via SMS.
|
Create survey instance and queue for delayed sending.
|
||||||
|
|
||||||
|
NEW: Survey is created with PENDING status and sent after delay.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patient: Patient instance
|
patient: Patient instance
|
||||||
@ -210,9 +264,11 @@ class HISAdapter:
|
|||||||
Returns:
|
Returns:
|
||||||
SurveyInstance or None if failed
|
SurveyInstance or None if failed
|
||||||
"""
|
"""
|
||||||
|
from apps.surveys.tasks import send_scheduled_survey
|
||||||
|
|
||||||
admission_id = patient_data.get("AdmissionID")
|
admission_id = patient_data.get("AdmissionID")
|
||||||
discharge_date_str = patient_data.get("DischargeDate")
|
discharge_date_str = patient_data.get("DischargeDate")
|
||||||
discharge_date = HISAdapter.parse_date(discharge_date_str) if discharge_date_str else None
|
patient_type = patient_data.get("PatientType")
|
||||||
|
|
||||||
# Check if survey already sent for this admission
|
# Check if survey already sent for this admission
|
||||||
existing_survey = SurveyInstance.objects.filter(
|
existing_survey = SurveyInstance.objects.filter(
|
||||||
@ -222,42 +278,48 @@ class HISAdapter:
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing_survey:
|
if existing_survey:
|
||||||
|
logger.info(f"Survey already exists for admission {admission_id}")
|
||||||
return existing_survey
|
return existing_survey
|
||||||
|
|
||||||
# Create survey instance
|
# Get delay from SurveyTemplateMapping
|
||||||
|
delay_hours = HISAdapter.get_delay_for_patient_type(patient_type, hospital)
|
||||||
|
|
||||||
|
# Calculate scheduled send time
|
||||||
|
scheduled_send_at = timezone.now() + timedelta(hours=delay_hours)
|
||||||
|
|
||||||
|
# Create survey with PENDING status (NOT SENT)
|
||||||
survey = SurveyInstance.objects.create(
|
survey = SurveyInstance.objects.create(
|
||||||
survey_template=survey_template,
|
survey_template=survey_template,
|
||||||
patient=patient,
|
patient=patient,
|
||||||
hospital=hospital,
|
hospital=hospital,
|
||||||
status=SurveyStatus.SENT, # Set to SENT as it will be sent immediately
|
status=SurveyStatus.PENDING, # Changed from SENT
|
||||||
delivery_channel="SMS", # Send via SMS
|
delivery_channel="SMS",
|
||||||
recipient_phone=patient.phone,
|
recipient_phone=patient.phone,
|
||||||
recipient_email=patient.email,
|
recipient_email=patient.email,
|
||||||
|
scheduled_send_at=scheduled_send_at,
|
||||||
metadata={
|
metadata={
|
||||||
'admission_id': admission_id,
|
'admission_id': admission_id,
|
||||||
'patient_type': patient_data.get("PatientType"),
|
'patient_type': patient_type,
|
||||||
'hospital_id': patient_data.get("HospitalID"),
|
'hospital_id': patient_data.get("HospitalID"),
|
||||||
'insurance_company': patient_data.get("InsuranceCompanyName"),
|
'insurance_company': patient_data.get("InsuranceCompanyName"),
|
||||||
'is_vip': patient_data.get("IsVIP") == "1"
|
'is_vip': patient_data.get("IsVIP") == "1",
|
||||||
|
'discharge_date': discharge_date_str,
|
||||||
|
'scheduled_send_at': scheduled_send_at.isoformat(),
|
||||||
|
'delay_hours': delay_hours,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send survey via SMS
|
# Queue delayed send task
|
||||||
try:
|
send_scheduled_survey.apply_async(
|
||||||
from apps.surveys.services import SurveyDeliveryService
|
args=[str(survey.id)],
|
||||||
delivery_success = SurveyDeliveryService.deliver_survey(survey)
|
countdown=delay_hours * 3600 # Convert to seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Survey {survey.id} created for {patient_type}, "
|
||||||
|
f"will send in {delay_hours}h at {scheduled_send_at}"
|
||||||
|
)
|
||||||
|
|
||||||
if delivery_success:
|
|
||||||
return survey
|
|
||||||
else:
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.warning(f"Survey created but SMS delivery failed for survey {survey.id}")
|
|
||||||
return survey
|
|
||||||
except Exception as e:
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.error(f"Error sending survey SMS: {str(e)}", exc_info=True)
|
|
||||||
return survey
|
return survey
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -269,7 +331,8 @@ class HISAdapter:
|
|||||||
1. Extract patient data
|
1. Extract patient data
|
||||||
2. Get or create patient and hospital
|
2. Get or create patient and hospital
|
||||||
3. Determine survey type from PatientType
|
3. Determine survey type from PatientType
|
||||||
4. Create and send survey via SMS
|
4. Create survey with PENDING status
|
||||||
|
5. Queue delayed send task
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
his_data: HIS data in real format
|
his_data: HIS data in real format
|
||||||
@ -282,7 +345,7 @@ class HISAdapter:
|
|||||||
'message': '',
|
'message': '',
|
||||||
'patient': None,
|
'patient': None,
|
||||||
'survey': None,
|
'survey': None,
|
||||||
'survey_sent': False
|
'survey_queued': False
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -327,16 +390,16 @@ class HISAdapter:
|
|||||||
result['message'] = f"No survey template found for patient type '{patient_type}'"
|
result['message'] = f"No survey template found for patient type '{patient_type}'"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Create and send survey
|
# Create and queue survey (delayed sending)
|
||||||
survey = HISAdapter.create_and_send_survey(
|
survey = HISAdapter.create_and_send_survey(
|
||||||
patient, hospital, patient_data, survey_template
|
patient, hospital, patient_data, survey_template
|
||||||
)
|
)
|
||||||
|
|
||||||
if survey:
|
if survey:
|
||||||
from apps.surveys.models import SurveyStatus
|
# Survey is queued with PENDING status
|
||||||
survey_sent = survey.status == SurveyStatus.SENT
|
survey_queued = survey.status == SurveyStatus.PENDING
|
||||||
else:
|
else:
|
||||||
survey_sent = False
|
survey_queued = False
|
||||||
|
|
||||||
result.update({
|
result.update({
|
||||||
'success': True,
|
'success': True,
|
||||||
@ -344,13 +407,12 @@ class HISAdapter:
|
|||||||
'patient': patient,
|
'patient': patient,
|
||||||
'patient_type': patient_type,
|
'patient_type': patient_type,
|
||||||
'survey': survey,
|
'survey': survey,
|
||||||
'survey_sent': survey_sent,
|
'survey_queued': survey_queued,
|
||||||
|
'scheduled_send_at': survey.scheduled_send_at.isoformat() if survey and survey.scheduled_send_at else None,
|
||||||
'survey_url': survey.get_survey_url() if survey else None
|
'survey_url': survey.get_survey_url() if survey else None
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.error(f"Error processing HIS data: {str(e)}", exc_info=True)
|
logger.error(f"Error processing HIS data: {str(e)}", exc_info=True)
|
||||||
result['message'] = f"Error processing HIS data: {str(e)}"
|
result['message'] = f"Error processing HIS data: {str(e)}"
|
||||||
result['success'] = False
|
result['success'] = False
|
||||||
|
|||||||
@ -208,6 +208,127 @@ def process_pending_events():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def test_fetch_his_surveys_from_json():
|
||||||
|
"""
|
||||||
|
TEST TASK - Fetch surveys from local JSON file instead of HIS API.
|
||||||
|
|
||||||
|
This is a clone of fetch_his_surveys for testing purposes.
|
||||||
|
Reads from /home/ismail/projects/HH/data.json
|
||||||
|
|
||||||
|
TODO: Remove this task after testing is complete.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Summary of fetched and processed surveys
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from apps.integrations.services.his_adapter import HISAdapter
|
||||||
|
|
||||||
|
logger.info("Starting TEST HIS survey fetch from JSON file")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"patients_fetched": 0,
|
||||||
|
"surveys_created": 0,
|
||||||
|
"surveys_queued": 0,
|
||||||
|
"errors": [],
|
||||||
|
"details": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read JSON file
|
||||||
|
json_path = Path("/home/ismail/projects/HH/data.json")
|
||||||
|
if not json_path.exists():
|
||||||
|
error_msg = f"JSON file not found: {json_path}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
result["errors"].append(error_msg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
his_data = json.load(f)
|
||||||
|
|
||||||
|
# Extract patient list
|
||||||
|
patient_list = his_data.get("FetchPatientDataTimeStampList", [])
|
||||||
|
|
||||||
|
if not patient_list:
|
||||||
|
logger.warning("No patient data found in JSON file")
|
||||||
|
result["errors"].append("No patient data found")
|
||||||
|
return result
|
||||||
|
|
||||||
|
logger.info(f"Found {len(patient_list)} patients in JSON file")
|
||||||
|
result["patients_fetched"] = len(patient_list)
|
||||||
|
|
||||||
|
# Process each patient
|
||||||
|
for patient_data in patient_list:
|
||||||
|
try:
|
||||||
|
# Wrap in proper format for HISAdapter
|
||||||
|
patient_payload = {
|
||||||
|
"FetchPatientDataTimeStampList": [patient_data],
|
||||||
|
"FetchPatientDataTimeStampVisitDataList": [],
|
||||||
|
"Code": 200,
|
||||||
|
"Status": "Success",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process using HISAdapter
|
||||||
|
process_result = HISAdapter.process_his_data(patient_payload)
|
||||||
|
|
||||||
|
if process_result["success"]:
|
||||||
|
result["surveys_created"] += 1
|
||||||
|
|
||||||
|
if process_result.get("survey_queued"):
|
||||||
|
result["surveys_queued"] += 1
|
||||||
|
|
||||||
|
# Log survey details
|
||||||
|
survey = process_result.get("survey")
|
||||||
|
if survey:
|
||||||
|
logger.info(
|
||||||
|
f"Survey queued for {patient_data.get('PatientName')}: "
|
||||||
|
f"Type={patient_data.get('PatientType')}, "
|
||||||
|
f"Scheduled={survey.scheduled_send_at}, "
|
||||||
|
f"Delay={process_result.get('metadata', {}).get('delay_hours', 'N/A')}h"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Survey created but not queued for {patient_data.get('PatientName')}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Not an error - patient may not be discharged
|
||||||
|
if "not discharged" in process_result.get("message", ""):
|
||||||
|
logger.debug(
|
||||||
|
f"Skipping {patient_data.get('PatientName')}: Not discharged"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to process {patient_data.get('PatientName')}: "
|
||||||
|
f"{process_result.get('message', 'Unknown error')}"
|
||||||
|
)
|
||||||
|
result["errors"].append(
|
||||||
|
f"{patient_data.get('PatientName')}: {process_result.get('message')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error processing patient {patient_data.get('PatientName', 'Unknown')}: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
result["errors"].append(error_msg)
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"TEST HIS survey fetch completed: "
|
||||||
|
f"{result['patients_fetched']} patients, "
|
||||||
|
f"{result['surveys_created']} surveys created, "
|
||||||
|
f"{result['surveys_queued']} surveys queued"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Fatal error in test_fetch_his_surveys_from_json: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
result["errors"].append(error_msg)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def fetch_his_surveys():
|
def fetch_his_surveys():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -34,10 +34,14 @@ class SurveyTemplateMappingViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
# If user is not superuser, filter by their hospital
|
# Superusers and PX Admins see all mappings
|
||||||
if not user.is_superuser and user.hospital:
|
if user.is_superuser or user.is_px_admin():
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
# Hospital users filter by their assigned hospital
|
||||||
|
if user.hospital:
|
||||||
queryset = queryset.filter(hospital=user.hospital)
|
queryset = queryset.filter(hospital=user.hospital)
|
||||||
elif not user.is_superuser and not user.hospital:
|
else:
|
||||||
# User without hospital assignment - no access
|
# User without hospital assignment - no access
|
||||||
queryset = queryset.none()
|
queryset = queryset.none()
|
||||||
|
|
||||||
@ -149,19 +153,28 @@ def survey_mapping_settings(request):
|
|||||||
# Get user's accessible hospitals based on role
|
# Get user's accessible hospitals based on role
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
# Superusers can see all hospitals
|
# Superusers can see all hospitals
|
||||||
hospitals = Hospital.objects.all()
|
hospitals = Hospital.objects.filter(status='active')
|
||||||
|
elif user.is_px_admin():
|
||||||
|
# PX Admins see all active hospitals for the dropdown
|
||||||
|
# They use session-based hospital selection (request.tenant_hospital)
|
||||||
|
hospitals = Hospital.objects.filter(status='active')
|
||||||
elif user.hospital:
|
elif user.hospital:
|
||||||
# Regular users can only see their assigned hospital
|
# Regular users can only see their assigned hospital
|
||||||
hospitals = Hospital.objects.filter(id=user.hospital.id)
|
hospitals = Hospital.objects.filter(id=user.hospital.id)
|
||||||
else:
|
else:
|
||||||
# User without hospital assignment - no access
|
# User without hospital assignment - no access
|
||||||
hospitals = []
|
hospitals = Hospital.objects.none()
|
||||||
|
|
||||||
# Get all mappings
|
# Get all mappings based on user role
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
mappings = SurveyTemplateMapping.objects.select_related(
|
mappings = SurveyTemplateMapping.objects.select_related(
|
||||||
'hospital', 'survey_template'
|
'hospital', 'survey_template'
|
||||||
).all()
|
).all()
|
||||||
|
elif user.is_px_admin():
|
||||||
|
# PX Admins see mappings for all hospitals (they manage all)
|
||||||
|
mappings = SurveyTemplateMapping.objects.select_related(
|
||||||
|
'hospital', 'survey_template'
|
||||||
|
).all()
|
||||||
else:
|
else:
|
||||||
mappings = SurveyTemplateMapping.objects.filter(
|
mappings = SurveyTemplateMapping.objects.filter(
|
||||||
hospital__in=hospitals
|
hospital__in=hospitals
|
||||||
@ -170,6 +183,9 @@ def survey_mapping_settings(request):
|
|||||||
# Group mappings by hospital
|
# Group mappings by hospital
|
||||||
mappings_by_hospital = {}
|
mappings_by_hospital = {}
|
||||||
for mapping in mappings:
|
for mapping in mappings:
|
||||||
|
# Skip mappings with missing hospital (orphaned records)
|
||||||
|
if mapping.hospital is None:
|
||||||
|
continue
|
||||||
hospital_name = mapping.hospital.name
|
hospital_name = mapping.hospital.name
|
||||||
if hospital_name not in mappings_by_hospital:
|
if hospital_name not in mappings_by_hospital:
|
||||||
mappings_by_hospital[hospital_name] = []
|
mappings_by_hospital[hospital_name] = []
|
||||||
|
|||||||
@ -237,6 +237,12 @@ class SurveyInstance(UUIDModel, TimeStampedModel, TenantModel):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
sent_at = models.DateTimeField(null=True, blank=True, db_index=True)
|
sent_at = models.DateTimeField(null=True, blank=True, db_index=True)
|
||||||
|
scheduled_send_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
db_index=True,
|
||||||
|
help_text="When this survey should be sent (for delayed sending)"
|
||||||
|
)
|
||||||
opened_at = models.DateTimeField(null=True, blank=True)
|
opened_at = models.DateTimeField(null=True, blank=True)
|
||||||
completed_at = models.DateTimeField(null=True, blank=True)
|
completed_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
|||||||
@ -676,3 +676,86 @@ def send_bulk_surveys(self, job_id):
|
|||||||
raise self.retry(countdown=60 * (self.request.retries + 1))
|
raise self.retry(countdown=60 * (self.request.retries + 1))
|
||||||
|
|
||||||
return {'status': 'error', 'error': str(e)}
|
return {'status': 'error', 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_scheduled_survey(survey_instance_id):
|
||||||
|
"""
|
||||||
|
Send a scheduled survey.
|
||||||
|
|
||||||
|
This task is called after the delay period expires.
|
||||||
|
It sends the survey via the configured delivery channel (SMS/Email).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
survey_instance_id: UUID of the SurveyInstance to send
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with status and details
|
||||||
|
"""
|
||||||
|
from apps.surveys.models import SurveyInstance, SurveyStatus
|
||||||
|
from apps.surveys.services import SurveyDeliveryService
|
||||||
|
|
||||||
|
try:
|
||||||
|
survey = SurveyInstance.objects.get(id=survey_instance_id)
|
||||||
|
|
||||||
|
# Check if already sent
|
||||||
|
if survey.status != SurveyStatus.PENDING:
|
||||||
|
logger.warning(f"Survey {survey.id} already sent/cancelled (status: {survey.status})")
|
||||||
|
return {'status': 'skipped', 'reason': 'already_sent', 'survey_id': survey.id}
|
||||||
|
|
||||||
|
# Check if scheduled time has passed
|
||||||
|
if survey.scheduled_send_at and survey.scheduled_send_at > timezone.now():
|
||||||
|
logger.warning(f"Survey {survey.id} not due yet (scheduled: {survey.scheduled_send_at})")
|
||||||
|
return {'status': 'delayed', 'scheduled_at': survey.scheduled_send_at.isoformat(), 'survey_id': survey.id}
|
||||||
|
|
||||||
|
# Send survey
|
||||||
|
success = SurveyDeliveryService.deliver_survey(survey)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
survey.status = SurveyStatus.SENT
|
||||||
|
survey.sent_at = timezone.now()
|
||||||
|
survey.save()
|
||||||
|
logger.info(f"Scheduled survey {survey.id} sent successfully")
|
||||||
|
return {'status': 'sent', 'survey_id': survey.id}
|
||||||
|
else:
|
||||||
|
survey.status = SurveyStatus.FAILED
|
||||||
|
survey.save()
|
||||||
|
logger.error(f"Scheduled survey {survey.id} delivery failed")
|
||||||
|
return {'status': 'failed', 'survey_id': survey.id, 'reason': 'delivery_failed'}
|
||||||
|
|
||||||
|
except SurveyInstance.DoesNotExist:
|
||||||
|
logger.error(f"Survey {survey_instance_id} not found")
|
||||||
|
return {'status': 'error', 'reason': 'not_found'}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending scheduled survey: {e}", exc_info=True)
|
||||||
|
return {'status': 'error', 'reason': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_pending_scheduled_surveys():
|
||||||
|
"""
|
||||||
|
Periodic task to send any overdue scheduled surveys.
|
||||||
|
|
||||||
|
Runs every 10 minutes as a safety net to catch any surveys
|
||||||
|
that weren't sent due to task failures or delays.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with count of queued surveys
|
||||||
|
"""
|
||||||
|
from apps.surveys.models import SurveyInstance, SurveyStatus
|
||||||
|
|
||||||
|
# Find surveys that should have been sent but weren't
|
||||||
|
overdue_surveys = SurveyInstance.objects.filter(
|
||||||
|
status=SurveyStatus.PENDING,
|
||||||
|
scheduled_send_at__lte=timezone.now()
|
||||||
|
)[:50] # Max 50 at a time
|
||||||
|
|
||||||
|
sent_count = 0
|
||||||
|
for survey in overdue_surveys:
|
||||||
|
send_scheduled_survey.delay(str(survey.id))
|
||||||
|
sent_count += 1
|
||||||
|
|
||||||
|
if sent_count > 0:
|
||||||
|
logger.info(f"Queued {sent_count} overdue scheduled surveys")
|
||||||
|
|
||||||
|
return {'queued': sent_count}
|
||||||
|
|||||||
@ -226,7 +226,13 @@ def survey_template_list(request):
|
|||||||
# Apply RBAC filters
|
# Apply RBAC filters
|
||||||
user = request.user
|
user = request.user
|
||||||
if user.is_px_admin():
|
if user.is_px_admin():
|
||||||
pass
|
# PX Admins see templates for their selected hospital (from session)
|
||||||
|
tenant_hospital = getattr(request, 'tenant_hospital', None)
|
||||||
|
if tenant_hospital:
|
||||||
|
queryset = queryset.filter(hospital=tenant_hospital)
|
||||||
|
else:
|
||||||
|
# If no hospital selected, show none (user needs to select a hospital)
|
||||||
|
queryset = queryset.none()
|
||||||
elif user.hospital:
|
elif user.hospital:
|
||||||
queryset = queryset.filter(hospital=user.hospital)
|
queryset = queryset.filter(hospital=user.hospital)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -35,6 +35,16 @@ app.conf.beat_schedule = {
|
|||||||
'expires': 240, # Task expires after 4 minutes if not picked up
|
'expires': 240, # Task expires after 4 minutes if not picked up
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
# TEST TASK - Fetch from JSON file (uncomment for testing, remove when done)
|
||||||
|
# 'test-fetch-his-surveys-from-json': {
|
||||||
|
# 'task': 'apps.integrations.tasks.test_fetch_his_surveys_from_json',
|
||||||
|
# 'schedule': crontab(minute='*/5'), # Every 5 minutes
|
||||||
|
# },
|
||||||
|
# Send pending scheduled surveys every 10 minutes
|
||||||
|
'send-pending-scheduled-surveys': {
|
||||||
|
'task': 'apps.surveys.tasks.send_pending_scheduled_surveys',
|
||||||
|
'schedule': crontab(minute='*/10'), # Every 10 minutes
|
||||||
|
},
|
||||||
# Check for overdue complaints every 15 minutes
|
# Check for overdue complaints every 15 minutes
|
||||||
'check-overdue-complaints': {
|
'check-overdue-complaints': {
|
||||||
'task': 'apps.complaints.tasks.check_overdue_complaints',
|
'task': 'apps.complaints.tasks.check_overdue_complaints',
|
||||||
|
|||||||
245
docs/SETUP_COMPLETE.md
Normal file
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hospital -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</label>
|
|
||||||
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"
|
|
||||||
name="hospital" id="hospitalFilter" onchange="loadDepartments()">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name_en|default:hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Department -->
|
<!-- Department -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
|
||||||
@ -463,7 +449,7 @@ function handleDateRangeChange() {
|
|||||||
|
|
||||||
function updateFilters() {
|
function updateFilters() {
|
||||||
currentFilters.date_range = document.getElementById('dateRange').value;
|
currentFilters.date_range = document.getElementById('dateRange').value;
|
||||||
currentFilters.hospital = document.getElementById('hospitalFilter').value;
|
currentFilters.hospital = '{{ current_hospital.id|default:"" }}';
|
||||||
currentFilters.department = document.getElementById('departmentFilter').value;
|
currentFilters.department = document.getElementById('departmentFilter').value;
|
||||||
currentFilters.kpi_category = document.getElementById('kpiCategoryFilter').value;
|
currentFilters.kpi_category = document.getElementById('kpiCategoryFilter').value;
|
||||||
currentFilters.custom_start = document.getElementById('customStart').value;
|
currentFilters.custom_start = document.getElementById('customStart').value;
|
||||||
@ -669,7 +655,6 @@ function refreshDashboard() {
|
|||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
document.getElementById('dateRange').value = '30d';
|
document.getElementById('dateRange').value = '30d';
|
||||||
document.getElementById('hospitalFilter').value = '';
|
|
||||||
document.getElementById('departmentFilter').value = '';
|
document.getElementById('departmentFilter').value = '';
|
||||||
document.getElementById('kpiCategoryFilter').value = '';
|
document.getElementById('kpiCategoryFilter').value = '';
|
||||||
document.getElementById('customStart').value = '';
|
document.getElementById('customStart').value = '';
|
||||||
|
|||||||
@ -240,14 +240,6 @@
|
|||||||
<p class="mt-1 opacity-90">{% trans "Comprehensive overview of patient experience metrics" %}</p>
|
<p class="mt-1 opacity-90">{% trans "Comprehensive overview of patient experience metrics" %}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<select class="form-select-px360" onchange="window.location.href='?hospital='+this.value">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if selected_hospital and selected_hospital.id == hospital.id %}selected{% endif %}>
|
|
||||||
{{ hospital.name_en }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button onclick="refreshDashboard()" class="p-2.5 bg-white/20 hover:bg-white/30 rounded-xl transition" title="{% trans 'Refresh' %}">
|
<button onclick="refreshDashboard()" class="p-2.5 bg-white/20 hover:bg-white/30 rounded-xl transition" title="{% trans 'Refresh' %}">
|
||||||
<i data-lucide="refresh-cw" class="w-5 h-5 text-white"></i>
|
<i data-lucide="refresh-cw" class="w-5 h-5 text-white"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -58,19 +58,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hospital -->
|
<input type="hidden" name="hospital" value="{{ current_hospital.id }}">
|
||||||
<div>
|
|
||||||
<label for="hospital" class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">
|
|
||||||
{% trans "Hospital" %} <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<select name="hospital" id="hospital" required
|
|
||||||
class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:bg-white focus:border-navy focus:ring-2 focus:ring-navy/10 outline-none transition">
|
|
||||||
<option value="">{% trans "Select Hospital" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Year and Month -->
|
<!-- Year and Month -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@ -123,20 +123,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if request.user.is_px_admin %}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<label class="text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="px-3 py-1.5 bg-white border rounded-lg text-xs">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:'s' %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="text-xs font-bold text-slate uppercase">{% trans "Year" %}</label>
|
<label class="text-xs font-bold text-slate uppercase">{% trans "Year" %}</label>
|
||||||
<select name="year" class="px-3 py-1.5 bg-white border rounded-lg text-xs">
|
<select name="year" class="px-3 py-1.5 bg-white border rounded-lg text-xs">
|
||||||
|
|||||||
@ -39,15 +39,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="hospital_id" class="form-label">
|
<input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
|
||||||
{% trans "Hospital" %} <span class="text-danger">*</span>
|
<label class="form-label">{% trans "Hospital" %}</label>
|
||||||
</label>
|
<input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
|
||||||
<select class="form-select" id="hospital_id" name="hospital_id" required>
|
|
||||||
<option value="">-- {% trans "Select Hospital" %} --</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -240,31 +234,26 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const hospitalSelect = document.getElementById('hospital_id');
|
|
||||||
const recipientTypeSelect = document.getElementById('recipient_type');
|
const recipientTypeSelect = document.getElementById('recipient_type');
|
||||||
const recipientSelect = document.getElementById('recipient_id');
|
const recipientSelect = document.getElementById('recipient_id');
|
||||||
const departmentSelect = document.getElementById('department_id');
|
const departmentSelect = document.getElementById('department_id');
|
||||||
const recipientHelp = document.getElementById('recipientHelp');
|
const recipientHelp = document.getElementById('recipientHelp');
|
||||||
|
|
||||||
|
const currentHospitalId = '{{ current_hospital.id|default:"" }}';
|
||||||
let recipientData = [];
|
let recipientData = [];
|
||||||
|
|
||||||
// Load recipients when hospital changes
|
// Load recipients and departments on page load
|
||||||
hospitalSelect.addEventListener('change', function() {
|
function loadRecipientsAndDepartments() {
|
||||||
const hospitalId = this.value;
|
const hospitalId = currentHospitalId;
|
||||||
const recipientType = recipientTypeSelect.value;
|
const recipientType = recipientTypeSelect.value;
|
||||||
|
|
||||||
|
if (!hospitalId) return;
|
||||||
|
|
||||||
|
// Load recipients
|
||||||
recipientSelect.disabled = true;
|
recipientSelect.disabled = true;
|
||||||
recipientSelect.innerHTML = '<option value="">Loading...</option>';
|
recipientSelect.innerHTML = '<option value="">Loading...</option>';
|
||||||
recipientHelp.textContent = 'Loading recipients...';
|
recipientHelp.textContent = 'Loading recipients...';
|
||||||
|
|
||||||
if (!hospitalId) {
|
|
||||||
recipientSelect.innerHTML = '<option value="">-- Select Recipient --</option>';
|
|
||||||
recipientSelect.disabled = true;
|
|
||||||
recipientHelp.textContent = 'Select a hospital first';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch recipients
|
|
||||||
const url = recipientType === 'user'
|
const url = recipientType === 'user'
|
||||||
? "{% url 'appreciation:get_users_by_hospital' %}?hospital_id=" + hospitalId
|
? "{% url 'appreciation:get_users_by_hospital' %}?hospital_id=" + hospitalId
|
||||||
: "{% url 'appreciation:get_physicians_by_hospital' %}?hospital_id=" + hospitalId;
|
: "{% url 'appreciation:get_physicians_by_hospital' %}?hospital_id=" + hospitalId;
|
||||||
@ -290,16 +279,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
recipientSelect.innerHTML = '<option value="">Error loading recipients</option>';
|
recipientSelect.innerHTML = '<option value="">Error loading recipients</option>';
|
||||||
recipientHelp.textContent = 'Error loading recipients';
|
recipientHelp.textContent = 'Error loading recipients';
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Load departments when hospital changes
|
|
||||||
hospitalSelect.addEventListener('change', function() {
|
|
||||||
const hospitalId = this.value;
|
|
||||||
|
|
||||||
|
// Load departments
|
||||||
departmentSelect.innerHTML = '<option value="">-- Select Department --</option>';
|
departmentSelect.innerHTML = '<option value="">-- Select Department --</option>';
|
||||||
|
|
||||||
if (!hospitalId) return;
|
|
||||||
|
|
||||||
fetch("{% url 'appreciation:get_departments_by_hospital' %}?hospital_id=" + hospitalId)
|
fetch("{% url 'appreciation:get_departments_by_hospital' %}?hospital_id=" + hospitalId)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@ -313,14 +296,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh recipients when recipient type changes
|
|
||||||
recipientTypeSelect.addEventListener('change', function() {
|
|
||||||
if (hospitalSelect.value) {
|
|
||||||
hospitalSelect.dispatchEvent(new Event('change'));
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Load on page load
|
||||||
|
loadRecipientsAndDepartments();
|
||||||
|
|
||||||
|
// Refresh when recipient type changes
|
||||||
|
recipientTypeSelect.addEventListener('change', loadRecipientsAndDepartments);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -47,17 +47,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="hospital" class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select class="form-select" id="hospital" name="hospital">
|
|
||||||
<option value="">All Hospitals</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label for="department" class="form-label">{% trans "Department" %}</label>
|
<label for="department" class="form-label">{% trans "Department" %}</label>
|
||||||
<select class="form-select" id="department" name="department">
|
<select class="form-select" id="department" name="department">
|
||||||
|
|||||||
@ -116,18 +116,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-48">
|
|
||||||
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy/20 text-sm bg-white">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-40">
|
<div class="w-40">
|
||||||
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "From Date" %}</label>
|
<label class="block text-xs font-bold text-slate uppercase mb-1.5">{% trans "From Date" %}</label>
|
||||||
<input type="date" name="date_from" value="{{ filters.date_from }}"
|
<input type="date" name="date_from" value="{{ filters.date_from }}"
|
||||||
|
|||||||
@ -130,13 +130,9 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label required-field">{% trans "Hospital" %}</label>
|
<label class="form-label">{% trans "Hospital" %}</label>
|
||||||
<select name="hospital_id" class="form-select" id="hospitalSelect" required>
|
<input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
|
||||||
<option value="">{% trans "Select hospital..." %}</option>
|
<input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}">{{ hospital.name_en }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
@ -283,7 +279,6 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const hospitalSelect = document.getElementById('hospitalSelect');
|
|
||||||
const departmentSelect = document.getElementById('departmentSelect');
|
const departmentSelect = document.getElementById('departmentSelect');
|
||||||
const physicianSelect = document.getElementById('physicianSelect');
|
const physicianSelect = document.getElementById('physicianSelect');
|
||||||
const patientSearch = document.getElementById('patientSearch');
|
const patientSearch = document.getElementById('patientSearch');
|
||||||
@ -293,19 +288,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const callerNameInput = document.getElementById('callerName');
|
const callerNameInput = document.getElementById('callerName');
|
||||||
const callerPhoneInput = document.getElementById('callerPhone');
|
const callerPhoneInput = document.getElementById('callerPhone');
|
||||||
|
|
||||||
// Hospital change handler - load departments and physicians
|
const currentHospitalId = '{{ current_hospital.id|default:"" }}';
|
||||||
hospitalSelect.addEventListener('change', function() {
|
|
||||||
const hospitalId = this.value;
|
|
||||||
|
|
||||||
// Clear department and physician
|
// Load departments and physicians on page load
|
||||||
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>';
|
if (currentHospitalId) {
|
||||||
physicianSelect.innerHTML = '<option value="">{% trans "Select physician..." %}</option>';
|
|
||||||
|
|
||||||
if (hospitalId) {
|
|
||||||
// Load departments
|
// Load departments
|
||||||
fetch(`/callcenter/ajax/departments/?hospital_id=${hospitalId}`)
|
fetch(`/callcenter/ajax/departments/?hospital_id=${currentHospitalId}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>';
|
||||||
data.departments.forEach(dept => {
|
data.departments.forEach(dept => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = dept.id;
|
option.value = dept.id;
|
||||||
@ -316,9 +307,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.catch(error => console.error('Error loading departments:', error));
|
.catch(error => console.error('Error loading departments:', error));
|
||||||
|
|
||||||
// Load physicians
|
// Load physicians
|
||||||
fetch(`/callcenter/ajax/physicians/?hospital_id=${hospitalId}`)
|
fetch(`/callcenter/ajax/physicians/?hospital_id=${currentHospitalId}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
physicianSelect.innerHTML = '<option value="">{% trans "Select physician..." %}</option>';
|
||||||
data.physicians.forEach(physician => {
|
data.physicians.forEach(physician => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = physician.id;
|
option.value = physician.id;
|
||||||
@ -328,12 +320,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
})
|
})
|
||||||
.catch(error => console.error('Error loading physicians:', error));
|
.catch(error => console.error('Error loading physicians:', error));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Patient search
|
// Patient search
|
||||||
function searchPatients() {
|
function searchPatients() {
|
||||||
const query = patientSearch.value.trim();
|
const query = patientSearch.value.trim();
|
||||||
const hospitalId = hospitalSelect.value;
|
const hospitalId = currentHospitalId;
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>';
|
patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>';
|
||||||
|
|||||||
@ -110,17 +110,6 @@
|
|||||||
<option value="low" {% if filters.severity == 'low' %}selected{% endif %}>{% trans "Low" %}</option>
|
<option value="low" {% if filters.severity == 'low' %}selected{% endif %}>{% trans "Low" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-bold text-slate uppercase mb-2">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy focus:border-transparent transition">
|
|
||||||
<option value="">{% trans "All" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name_en }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-end">
|
<div class="flex items-end">
|
||||||
<button type="submit" class="w-full bg-navy text-white px-4 py-2.5 rounded-xl text-sm font-bold hover:bg-blue transition flex items-center justify-center gap-2">
|
<button type="submit" class="w-full bg-navy text-white px-4 py-2.5 rounded-xl text-sm font-bold hover:bg-blue transition flex items-center justify-center gap-2">
|
||||||
<i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %}
|
<i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %}
|
||||||
|
|||||||
@ -134,13 +134,9 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label required-field">{% trans "Hospital" %}</label>
|
<label class="form-label">{% trans "Hospital" %}</label>
|
||||||
<select name="hospital_id" class="form-select" id="hospitalSelect" required>
|
<input type="text" class="form-control" value="{{ current_hospital.name }}" readonly>
|
||||||
<option value="">{% trans "Select hospital..." %}</option>
|
<input type="hidden" name="hospital_id" value="{{ current_hospital.id }}">
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}">{{ hospital.name_en }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
@ -242,7 +238,6 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const hospitalSelect = document.getElementById('hospitalSelect');
|
|
||||||
const departmentSelect = document.getElementById('departmentSelect');
|
const departmentSelect = document.getElementById('departmentSelect');
|
||||||
const patientSearch = document.getElementById('patientSearch');
|
const patientSearch = document.getElementById('patientSearch');
|
||||||
const searchBtn = document.getElementById('searchBtn');
|
const searchBtn = document.getElementById('searchBtn');
|
||||||
@ -252,18 +247,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const contactPhoneInput = document.getElementById('contactPhone');
|
const contactPhoneInput = document.getElementById('contactPhone');
|
||||||
const contactEmailInput = document.getElementById('contactEmail');
|
const contactEmailInput = document.getElementById('contactEmail');
|
||||||
|
|
||||||
// Hospital change handler - load departments
|
const currentHospitalId = '{{ current_hospital.id|default:"" }}';
|
||||||
hospitalSelect.addEventListener('change', function() {
|
|
||||||
const hospitalId = this.value;
|
|
||||||
|
|
||||||
// Clear department
|
// Load departments on page load
|
||||||
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>';
|
if (currentHospitalId) {
|
||||||
|
fetch(`/callcenter/ajax/departments/?hospital_id=${currentHospitalId}`)
|
||||||
if (hospitalId) {
|
|
||||||
// Load departments
|
|
||||||
fetch(`/callcenter/ajax/departments/?hospital_id=${hospitalId}`)
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
departmentSelect.innerHTML = '<option value="">{% trans "Select department..." %}</option>';
|
||||||
data.departments.forEach(dept => {
|
data.departments.forEach(dept => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = dept.id;
|
option.value = dept.id;
|
||||||
@ -273,12 +264,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
})
|
})
|
||||||
.catch(error => console.error('Error loading departments:', error));
|
.catch(error => console.error('Error loading departments:', error));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Patient search
|
// Patient search
|
||||||
function searchPatients() {
|
function searchPatients() {
|
||||||
const query = patientSearch.value.trim();
|
const query = patientSearch.value.trim();
|
||||||
const hospitalId = hospitalSelect.value;
|
const hospitalId = currentHospitalId;
|
||||||
|
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>';
|
patientResults.innerHTML = '<div class="alert alert-warning small">{% trans "Please enter at least 2 characters to search" %}</div>';
|
||||||
|
|||||||
@ -107,17 +107,6 @@
|
|||||||
<option value="other" {% if filters.category == 'other' %}selected{% endif %}>{% trans "Other" %}</option>
|
<option value="other" {% if filters.category == 'other' %}selected{% endif %}>{% trans "Other" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-bold text-slate uppercase mb-2">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy focus:border-transparent transition">
|
|
||||||
<option value="">{% trans "All" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name_en }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-5 flex justify-end">
|
<div class="md:col-span-5 flex justify-end">
|
||||||
<button type="submit" class="bg-cyan-500 text-white px-5 py-2.5 rounded-xl text-sm font-bold hover:bg-cyan-600 transition flex items-center gap-2">
|
<button type="submit" class="bg-cyan-500 text-white px-5 py-2.5 rounded-xl text-sm font-bold hover:bg-cyan-600 transition flex items-center gap-2">
|
||||||
<i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %}
|
<i data-lucide="filter" class="w-4 h-4"></i> {% trans "Filter" %}
|
||||||
|
|||||||
@ -33,20 +33,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
{% if request.user.is_px_admin %}
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">{% translate "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% translate "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">{% translate "Threshold Type" %}</label>
|
<label class="form-label">{% translate "Threshold Type" %}</label>
|
||||||
<select name="threshold_type" class="form-select">
|
<select name="threshold_type" class="form-select">
|
||||||
|
|||||||
@ -33,20 +33,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
{% if request.user.is_px_admin %}
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">{% translate "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% translate "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">{% translate "Escalation Level" %}</label>
|
<label class="form-label">{% translate "Escalation Level" %}</label>
|
||||||
<select name="escalation_level" class="form-select">
|
<select name="escalation_level" class="form-select">
|
||||||
|
|||||||
@ -33,20 +33,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
{% if request.user.is_px_admin %}
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">{% translate "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% translate "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">{% translate "Severity" %}</label>
|
<label class="form-label">{% translate "Severity" %}</label>
|
||||||
<select name="severity" class="form-select">
|
<select name="severity" class="form-select">
|
||||||
|
|||||||
@ -102,17 +102,6 @@
|
|||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 animate-in">
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 animate-in">
|
||||||
<form method="get" class="flex flex-wrap gap-4">
|
<form method="get" class="flex flex-wrap gap-4">
|
||||||
<div class="flex-1 min-w-[200px]">
|
|
||||||
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if hospital_filter == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
|
<label class="block text-sm font-semibold text-slate mb-1.5">{% trans "Status" %}</label>
|
||||||
<select name="is_active" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
|
<select name="is_active" class="px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-blue bg-white">
|
||||||
|
|||||||
@ -251,25 +251,10 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hospital Filter -->
|
|
||||||
{% if hospitals.exists %}
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Hospital" %}</label>
|
|
||||||
<select class="form-select-px360 w-full" id="hospitalFilter">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if selected_hospital_id == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Department Filter -->
|
<!-- Department Filter -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Department" %}</label>
|
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Department" %}</label>
|
||||||
<select class="form-select-px360 w-full" id="departmentFilter" {% if not selected_hospital_id and not request.user.hospital %}disabled{% endif %}>
|
<select class="form-select-px360 w-full" id="departmentFilter">
|
||||||
<option value="">{% trans "All Departments" %}</option>
|
<option value="">{% trans "All Departments" %}</option>
|
||||||
{% for department in departments %}
|
{% for department in departments %}
|
||||||
<option value="{{ department.id }}" {% if selected_department_id == department.id|stringformat:"s" %}selected{% endif %}>
|
<option value="{{ department.id }}" {% if selected_department_id == department.id|stringformat:"s" %}selected{% endif %}>
|
||||||
@ -730,7 +715,7 @@ function updateSummaryCards(data) {
|
|||||||
|
|
||||||
function exportReport(format) {
|
function exportReport(format) {
|
||||||
const dateRange = document.getElementById('dateRange').value;
|
const dateRange = document.getElementById('dateRange').value;
|
||||||
const hospital = document.getElementById('hospitalFilter')?.value || '';
|
const hospital = '{{ current_hospital.id|default:"" }}';
|
||||||
const department = document.getElementById('departmentFilter')?.value || '';
|
const department = document.getElementById('departmentFilter')?.value || '';
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|||||||
@ -244,19 +244,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hospital -->
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select class="form-select" name="hospital">
|
|
||||||
<option value="">All Hospitals</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rating Range -->
|
<!-- Rating Range -->
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">{% trans "Min Rating" %}</label>
|
<label class="form-label">{% trans "Min Rating" %}</label>
|
||||||
|
|||||||
@ -1,79 +1,77 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Survey Template Mappings" %} - {{ block.super }}{% endblock %}
|
{% block title %}{% trans "Survey Template Mappings" %} - PX360{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<!-- Page Header -->
|
||||||
<!-- Page Header -->
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-3xl font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<h1 class="text-2xl font-bold text-navy flex items-center gap-2">
|
||||||
<i data-lucide="layers" class="w-8 h-8 text-navy"></i>
|
<i data-lucide="layers" class="w-7 h-7 text-blue"></i>
|
||||||
{% trans "Survey Template Mappings" %}
|
{% trans "Survey Template Mappings" %}
|
||||||
</h2>
|
</h1>
|
||||||
<p class="text-gray-500">
|
<p class="text-slate mt-1">{% trans "Configure which survey templates are sent for each patient type at each hospital" %}</p>
|
||||||
{% trans "Configure which survey templates are sent for each patient type at each hospital." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="addMappingBtn" class="bg-light0 text-white px-6 py-3 rounded-xl font-bold hover:bg-navy transition flex items-center gap-2 shadow-lg shadow-blue-200 hover:shadow-xl hover:-translate-y-0.5">
|
<button id="addMappingBtn" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center gap-2">
|
||||||
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Add Mapping" %}
|
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Add Mapping" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mappings List -->
|
<!-- Mappings List Card -->
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
||||||
{% if mappings %}
|
{% if mappings %}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-slate-50 border-b border-slate-100">
|
||||||
<tr>
|
<tr class="text-xs font-bold text-slate uppercase tracking-wider">
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Hospital" %}</th>
|
<th class="px-6 py-4 text-left">{% trans "Hospital" %}</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Patient Type" %}</th>
|
<th class="px-6 py-4 text-left">{% trans "Patient Type" %}</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Survey Template" %}</th>
|
<th class="px-6 py-4 text-left">{% trans "Survey Template" %}</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Status" %}</th>
|
<th class="px-6 py-4 text-left">{% trans "Status" %}</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
|
<th class="px-6 py-4 text-left">{% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-100">
|
<tbody class="divide-y divide-slate-100">
|
||||||
{% for mapping in mappings %}
|
{% for mapping in mappings %}
|
||||||
<tr class="hover:bg-gray-50 transition">
|
<tr class="hover:bg-light/30 transition">
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="font-semibold text-gray-800">{{ mapping.hospital.name }}</div>
|
<div class="font-semibold text-navy">
|
||||||
|
{% if mapping.hospital %}
|
||||||
|
{{ mapping.hospital.name }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-slate italic">{% trans "All Hospitals" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-blue-100 text-blue-700">
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-blue-100 text-blue-700">
|
||||||
{{ mapping.patient_type_display }}
|
{{ mapping.get_patient_type_display }}
|
||||||
</span>
|
</span>
|
||||||
<div class="text-xs text-gray-400 mt-1">({{ mapping.patient_type }})</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="font-semibold text-gray-800">{{ mapping.survey_template.name }}</div>
|
<div class="font-semibold text-navy">{{ mapping.survey_template.name }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
{% if mapping.is_active %}
|
{% if mapping.is_active %}
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-700">
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-green-100 text-green-700">{% trans "Active" %}</span>
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600">
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 text-slate-600">{% trans "Inactive" %}</span>
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="edit-mapping px-3 py-2 text-navy bg-light rounded-lg hover:bg-light transition font-medium text-sm flex items-center gap-1"
|
<button class="edit-mapping px-3 py-2 text-navy bg-light rounded-lg hover:bg-light/80 transition font-medium text-sm flex items-center gap-1"
|
||||||
data-id="{{ mapping.id }}"
|
data-id="{{ mapping.id }}"
|
||||||
data-hospital="{{ mapping.hospital.id }}"
|
data-hospital="{{ mapping.hospital.id|default:'' }}"
|
||||||
data-patient-type="{{ mapping.patient_type }}"
|
data-patient-type="{{ mapping.patient_type }}"
|
||||||
data-survey-template="{{ mapping.survey_template.id }}"
|
data-survey-template="{{ mapping.survey_template.id }}"
|
||||||
data-active="{{ mapping.is_active }}">
|
data-active="{{ mapping.is_active }}">
|
||||||
<i data-lucide="edit" class="w-4 h-4"></i> {% trans "Edit" %}
|
<i data-lucide="pencil" class="w-4 h-4"></i> {% trans "Edit" %}
|
||||||
</button>
|
</button>
|
||||||
<button class="delete-mapping px-3 py-2 text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition font-medium text-sm flex items-center gap-1"
|
<button class="delete-mapping px-3 py-2 text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition font-medium text-sm flex items-center gap-1"
|
||||||
data-id="{{ mapping.id }}"
|
data-id="{{ mapping.id }}"
|
||||||
data-name="{{ mapping.patient_type_display }} - {{ mapping.survey_template.name }}">
|
data-name="{{ mapping.get_patient_type_display }} - {{ mapping.survey_template.name }}">
|
||||||
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Delete" %}
|
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -84,38 +82,49 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="p-12 text-center">
|
<!-- Empty State -->
|
||||||
<div class="inline-flex items-center justify-center w-20 h-20 bg-blue-100 rounded-full mb-4">
|
<div class="text-center py-16">
|
||||||
<i data-lucide="layers" class="w-10 h-10 text-blue-500"></i>
|
<div class="inline-flex items-center justify-center w-20 h-20 bg-slate-100 rounded-full mb-4">
|
||||||
|
<i data-lucide="layers" class="w-10 h-10 text-slate-400"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-800 mb-2">{% trans "No Mappings Configured" %}</h3>
|
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Mappings Configured" %}</h3>
|
||||||
<p class="text-gray-500 mb-4">{% trans "No survey template mappings configured yet. Click 'Add Mapping' to create your first mapping." %}</p>
|
<p class="text-slate mb-6">{% trans "No survey template mappings configured yet. Click 'Add Mapping' to create your first mapping." %}</p>
|
||||||
|
<button id="addMappingBtnEmpty" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition inline-flex items-center gap-2">
|
||||||
|
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Add Mapping" %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Mapping Modal -->
|
<!-- Add/Edit Mapping Modal -->
|
||||||
<div id="addMappingModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
<div id="addMappingModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
|
||||||
<div class="bg-white rounded-2xl shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
<div class="flex items-center justify-center min-h-screen px-4">
|
||||||
<div class="sticky top-0 bg-white border-b border-gray-200 px-8 py-6 flex justify-between items-center">
|
<div class="fixed inset-0 bg-black/50 modal-backdrop"></div>
|
||||||
<h3 id="mappingModalTitle" class="text-2xl font-bold text-gray-800">{% trans "Add Survey Template Mapping" %}</h3>
|
<div class="relative bg-white rounded-2xl max-w-2xl w-full shadow-xl overflow-hidden">
|
||||||
<button id="closeAddModal" class="text-gray-400 hover:text-gray-600 transition">
|
<!-- Modal Header -->
|
||||||
<i data-lucide="x" class="w-6 h-6"></i>
|
<div class="bg-slate-50 border-b border-slate-100 px-6 py-4 flex justify-between items-center">
|
||||||
|
<h3 id="mappingModalTitle" class="text-xl font-bold text-navy flex items-center gap-2">
|
||||||
|
<i data-lucide="layers" class="w-6 h-6 text-blue"></i>
|
||||||
|
{% trans "Add Survey Template Mapping" %}
|
||||||
|
</h3>
|
||||||
|
<button id="closeAddModal" class="text-slate hover:text-navy transition p-1 hover:bg-light rounded-lg">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-8">
|
|
||||||
{% csrf_token %}
|
<!-- Modal Body -->
|
||||||
<form id="mappingForm" class="space-y-6">
|
<div class="p-6">
|
||||||
|
<form id="mappingForm" class="space-y-5">
|
||||||
<input type="hidden" id="mappingId" name="id">
|
<input type="hidden" id="mappingId" name="id">
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
<!-- Hospital -->
|
||||||
<div>
|
<div>
|
||||||
<label for="hospital" class="block text-sm font-bold text-gray-700 mb-2">
|
<label for="hospital" class="block text-sm font-bold text-slate mb-2">
|
||||||
{% trans "Hospital" %} <span class="text-red-500">*</span>
|
{% trans "Hospital" %} <span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select id="hospital" name="hospital" required
|
<select id="hospital" name="hospital" required
|
||||||
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition">
|
class="form-select w-full px-4 py-2.5 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white">
|
||||||
<option value="">{% trans "Select Hospital" %}</option>
|
<option value="">{% trans "Select Hospital" %}</option>
|
||||||
{% for hospital in hospitals %}
|
{% for hospital in hospitals %}
|
||||||
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
|
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
|
||||||
@ -123,79 +132,100 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Survey Template -->
|
||||||
<div>
|
<div>
|
||||||
<label for="surveyTemplate" class="block text-sm font-bold text-gray-700 mb-2">
|
<label for="surveyTemplate" class="block text-sm font-bold text-slate mb-2">
|
||||||
{% trans "Survey Template" %} <span class="text-red-500">*</span>
|
{% trans "Survey Template" %} <span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select id="surveyTemplate" name="survey_template" required
|
<select id="surveyTemplate" name="survey_template" required
|
||||||
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition">
|
class="form-select w-full px-4 py-2.5 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white">
|
||||||
<option value="">{% trans "Select Survey Template" %}</option>
|
<option value="">{% trans "Select Survey Template" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
<!-- Patient Type -->
|
||||||
<div>
|
<div>
|
||||||
<label for="patientType" class="block text-sm font-bold text-gray-700 mb-2">
|
<label for="patientType" class="block text-sm font-bold text-slate mb-2">
|
||||||
{% trans "Patient Type" %} <span class="text-red-500">*</span>
|
{% trans "Patient Type" %} <span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select id="patientType" name="patient_type" required
|
<select id="patientType" name="patient_type" required
|
||||||
class="w-full px-4 py-3 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition">
|
class="form-select w-full px-4 py-2.5 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white">
|
||||||
<option value="">{% trans "Select Patient Type" %}</option>
|
<option value="">{% trans "Select Patient Type" %}</option>
|
||||||
<option value="1">1 - {% trans "Inpatient (Type 1)" %}</option>
|
<option value="1">{% trans "Inpatient (Type 1)" %}</option>
|
||||||
<option value="2">2 - {% trans "Outpatient (Type 2)" %}</option>
|
<option value="2">{% trans "Outpatient (Type 2)" %}</option>
|
||||||
<option value="3">3 - {% trans "Emergency (Type 3)" %}</option>
|
<option value="3">{% trans "Emergency (Type 3)" %}</option>
|
||||||
<option value="4">4 - {% trans "Day Case (Type 4)" %}</option>
|
<option value="4">{% trans "Day Case (Type 4)" %}</option>
|
||||||
<option value="APPOINTMENT">APPOINTMENT - {% trans "Appointment" %}</option>
|
<option value="APPOINTMENT">{% trans "Appointment" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
<div>
|
<div>
|
||||||
<label for="isActive" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Status" %}</label>
|
<label class="block text-sm font-bold text-slate mb-2">{% trans "Status" %}</label>
|
||||||
<div class="flex items-center gap-3 pt-3">
|
<label class="flex items-center gap-3 p-3 bg-light/50 rounded-xl border border-slate-100 cursor-pointer hover:bg-light transition">
|
||||||
<input type="checkbox" id="isActive" name="is_active" checked
|
<input type="checkbox" id="isActive" name="is_active" checked
|
||||||
class="w-5 h-5 text-navy border-2 border-gray-300 rounded focus:ring-navy focus:ring-offset-2">
|
class="w-5 h-5 text-navy border-2 border-slate-300 rounded focus:ring-navy focus:ring-offset-2">
|
||||||
<label for="isActive" class="text-gray-700 font-medium">{% trans "Active" %}</label>
|
<span class="text-navy font-medium">{% trans "Active" %}</span>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="sticky bottom-0 bg-gray-50 border-t border-gray-200 px-8 py-6 flex justify-end gap-3">
|
|
||||||
<button id="cancelAddModal" class="border-2 border-gray-300 text-gray-700 px-6 py-3 rounded-xl font-bold hover:bg-gray-100 transition flex items-center gap-2">
|
<!-- Modal Footer -->
|
||||||
<i data-lucide="x" class="w-5 h-5"></i> {% trans "Cancel" %}
|
<div class="bg-slate-50 border-t border-slate-100 px-6 py-4 flex justify-end gap-3">
|
||||||
|
<button id="cancelAddModal" class="px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-white hover:text-navy transition">
|
||||||
|
{% trans "Cancel" %}
|
||||||
</button>
|
</button>
|
||||||
<button id="saveMapping" class="bg-light0 text-white px-6 py-3 rounded-xl font-bold hover:bg-navy transition flex items-center gap-2 shadow-lg shadow-blue-200">
|
<button id="saveMapping" class="px-4 py-2 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition flex items-center gap-2">
|
||||||
<i data-lucide="save" class="w-5 h-5"></i> {% trans "Save Mapping" %}
|
<i data-lucide="save" class="w-4 h-4"></i> {% trans "Save Mapping" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
<div id="deleteModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
|
||||||
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full mx-4">
|
<div class="flex items-center justify-center min-h-screen px-4">
|
||||||
<div class="p-8">
|
<div class="fixed inset-0 bg-black/50 modal-backdrop"></div>
|
||||||
<div class="text-center mb-6">
|
<div class="relative bg-white rounded-2xl max-w-md w-full shadow-xl overflow-hidden">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4">
|
<!-- Modal Header -->
|
||||||
<i data-lucide="alert-triangle" class="w-8 h-8 text-red-500"></i>
|
<div class="bg-red-50 border-b border-red-100 px-6 py-4 flex justify-between items-center">
|
||||||
</div>
|
<h3 class="text-xl font-bold text-red-700 flex items-center gap-2">
|
||||||
<h3 class="text-xl font-bold text-gray-800 mb-2">{% trans "Confirm Delete" %}</h3>
|
<i data-lucide="alert-triangle" class="w-6 h-6 text-red-600"></i>
|
||||||
<p class="text-gray-500">{% trans "Are you sure you want to delete this mapping?" %}</p>
|
{% trans "Confirm Delete" %}
|
||||||
<p id="deleteMappingName" class="font-bold text-gray-800 mt-2"></p>
|
</h3>
|
||||||
</div>
|
<button id="cancelDeleteModal" class="text-red-400 hover:text-red-700 transition p-1 hover:bg-red-100 rounded-lg">
|
||||||
<div class="flex justify-center gap-3">
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
<button id="cancelDeleteModal" class="border-2 border-gray-300 text-gray-700 px-6 py-3 rounded-xl font-bold hover:bg-gray-100 transition flex items-center gap-2">
|
|
||||||
<i data-lucide="x" class="w-5 h-5"></i> {% trans "Cancel" %}
|
|
||||||
</button>
|
</button>
|
||||||
<button id="confirmDelete" class="bg-red-600 text-white px-6 py-3 rounded-xl font-bold hover:bg-red-700 transition flex items-center gap-2 shadow-lg shadow-red-200">
|
</div>
|
||||||
<i data-lucide="trash-2" class="w-5 h-5"></i> {% trans "Delete" %}
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-slate mb-4">{% trans "Are you sure you want to delete this mapping?" %}</p>
|
||||||
|
<p id="deleteMappingName" class="font-bold text-navy bg-light/50 p-3 rounded-xl inline-block"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="bg-slate-50 border-t border-slate-100 px-6 py-4 flex justify-end gap-3">
|
||||||
|
<button id="cancelDeleteBtn" class="px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-white hover:text-navy transition">
|
||||||
|
{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
|
<button id="confirmDelete" class="px-4 py-2 bg-red-600 text-white rounded-xl font-semibold hover:bg-red-700 transition flex items-center gap-2">
|
||||||
|
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const API_URL = '/api/integrations/survey-template-mappings/';
|
const API_URL = '/api/integrations/survey-template-mappings/';
|
||||||
@ -205,33 +235,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const addModal = document.getElementById('addMappingModal');
|
const addModal = document.getElementById('addMappingModal');
|
||||||
const deleteModal = document.getElementById('deleteModal');
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
const addMappingBtn = document.getElementById('addMappingBtn');
|
const addMappingBtn = document.getElementById('addMappingBtn');
|
||||||
|
const addMappingBtnEmpty = document.getElementById('addMappingBtnEmpty');
|
||||||
const closeAddModal = document.getElementById('closeAddModal');
|
const closeAddModal = document.getElementById('closeAddModal');
|
||||||
const cancelAddModal = document.getElementById('cancelAddModal');
|
const cancelAddModal = document.getElementById('cancelAddModal');
|
||||||
const closeDeleteModal = document.getElementById('cancelDeleteModal');
|
const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
|
||||||
|
const cancelDeleteModal = document.getElementById('cancelDeleteModal');
|
||||||
|
|
||||||
// Show/Hide modal helper functions
|
// Show/Hide modal helper functions
|
||||||
function showModal(modal) {
|
function showModal(modal) {
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
modal.classList.add('flex');
|
lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideModal(modal) {
|
function hideModal(modal) {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
modal.classList.remove('flex');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listeners for modal controls
|
// Event listeners for modal controls
|
||||||
addMappingBtn.addEventListener('click', () => showModal(addModal));
|
if (addMappingBtn) addMappingBtn.addEventListener('click', () => showModal(addModal));
|
||||||
|
if (addMappingBtnEmpty) addMappingBtnEmpty.addEventListener('click', () => showModal(addModal));
|
||||||
closeAddModal.addEventListener('click', () => hideModal(addModal));
|
closeAddModal.addEventListener('click', () => hideModal(addModal));
|
||||||
cancelAddModal.addEventListener('click', () => hideModal(addModal));
|
cancelAddModal.addEventListener('click', () => hideModal(addModal));
|
||||||
closeDeleteModal.addEventListener('click', () => hideModal(deleteModal));
|
cancelDeleteBtn.addEventListener('click', () => hideModal(deleteModal));
|
||||||
|
cancelDeleteModal.addEventListener('click', () => hideModal(deleteModal));
|
||||||
|
|
||||||
// Close modal when clicking outside
|
// Close modal when clicking outside
|
||||||
addModal.addEventListener('click', (e) => {
|
addModal.addEventListener('click', (e) => {
|
||||||
if (e.target === addModal) hideModal(addModal);
|
if (e.target === addModal.querySelector('.modal-backdrop')) hideModal(addModal);
|
||||||
});
|
});
|
||||||
deleteModal.addEventListener('click', (e) => {
|
deleteModal.addEventListener('click', (e) => {
|
||||||
if (e.target === deleteModal) hideModal(deleteModal);
|
if (e.target === deleteModal.querySelector('.modal-backdrop')) hideModal(deleteModal);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load survey templates when hospital changes
|
// Load survey templates when hospital changes
|
||||||
@ -239,12 +272,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const hospitalId = this.value;
|
const hospitalId = this.value;
|
||||||
const surveyTemplateSelect = document.getElementById('surveyTemplate');
|
const surveyTemplateSelect = document.getElementById('surveyTemplate');
|
||||||
|
|
||||||
|
// Get any pending value BEFORE clearing options
|
||||||
|
const pendingValue = surveyTemplateSelect.dataset.pendingValue;
|
||||||
|
|
||||||
// Clear existing options
|
// Clear existing options
|
||||||
surveyTemplateSelect.innerHTML = '<option value="">{% trans "Select Survey Template" %}</option>';
|
surveyTemplateSelect.innerHTML = '<option value="">{% trans "Select Survey Template" %}</option>';
|
||||||
|
|
||||||
// Store of pending survey template ID to set after loading
|
|
||||||
surveyTemplateSelect.dataset.pendingValue = '';
|
|
||||||
|
|
||||||
if (hospitalId) {
|
if (hospitalId) {
|
||||||
// Fetch survey templates for this hospital
|
// Fetch survey templates for this hospital
|
||||||
fetch(`/surveys/api/templates/?hospital=${hospitalId}`)
|
fetch(`/surveys/api/templates/?hospital=${hospitalId}`)
|
||||||
@ -260,15 +293,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
surveyTemplateSelect.appendChild(option);
|
surveyTemplateSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set of pending value after loading
|
// Set pending value after loading (if it was set before the fetch)
|
||||||
if (surveyTemplateSelect.dataset.pendingValue) {
|
if (pendingValue) {
|
||||||
surveyTemplateSelect.value = surveyTemplateSelect.dataset.pendingValue;
|
surveyTemplateSelect.value = pendingValue;
|
||||||
surveyTemplateSelect.dataset.pendingValue = '';
|
surveyTemplateSelect.dataset.pendingValue = '';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error loading survey templates:', error);
|
console.error('Error loading survey templates:', error);
|
||||||
console.error('Response data:', error.message);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -288,10 +320,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
surveyTemplateSelect.dataset.pendingValue = surveyTemplateId;
|
surveyTemplateSelect.dataset.pendingValue = surveyTemplateId;
|
||||||
|
|
||||||
// Update modal title
|
// Update modal title
|
||||||
document.getElementById('mappingModalTitle').textContent = '{% trans "Edit Survey Template Mapping" %}';
|
document.getElementById('mappingModalTitle').innerHTML = `
|
||||||
|
<i data-lucide="layers" class="w-6 h-6 text-blue"></i> {% trans "Edit Survey Template Mapping" %}
|
||||||
|
`;
|
||||||
|
|
||||||
// Trigger hospital change to load survey templates
|
// Trigger hospital change to load survey templates
|
||||||
// The survey template value will be set after loading completes
|
|
||||||
document.getElementById('hospital').dispatchEvent(new Event('change'));
|
document.getElementById('hospital').dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
showModal(addModal);
|
showModal(addModal);
|
||||||
@ -310,8 +343,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Confirm delete
|
// Confirm delete
|
||||||
document.getElementById('confirmDelete').addEventListener('click', function() {
|
document.getElementById('confirmDelete').addEventListener('click', function() {
|
||||||
if (deleteMappingId) {
|
if (deleteMappingId) {
|
||||||
// Get CSRF token
|
const csrfToken = getCookie('csrftoken');
|
||||||
const csrfToken = getCSRFToken();
|
|
||||||
if (!csrfToken) {
|
if (!csrfToken) {
|
||||||
alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}');
|
alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}');
|
||||||
return;
|
return;
|
||||||
@ -339,12 +371,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Save mapping
|
// Save mapping
|
||||||
document.getElementById('saveMapping').addEventListener('click', function(event) {
|
document.getElementById('saveMapping').addEventListener('click', function(event) {
|
||||||
// Prevent form submission
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const form = document.getElementById('mappingForm');
|
const form = document.getElementById('mappingForm');
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
form.reportValidity();
|
form.reportValidity();
|
||||||
return;
|
return;
|
||||||
@ -359,27 +389,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
is_active: document.getElementById('isActive').checked
|
is_active: document.getElementById('isActive').checked
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Saving mapping:', data);
|
|
||||||
|
|
||||||
const url = mappingId ? `${API_URL}${mappingId}/` : API_URL;
|
const url = mappingId ? `${API_URL}${mappingId}/` : API_URL;
|
||||||
const method = mappingId ? 'PUT' : 'POST';
|
const method = mappingId ? 'PUT' : 'POST';
|
||||||
|
|
||||||
// Disable button to prevent double submission
|
|
||||||
const saveButton = this;
|
const saveButton = this;
|
||||||
|
const originalContent = saveButton.innerHTML;
|
||||||
saveButton.disabled = true;
|
saveButton.disabled = true;
|
||||||
saveButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>{% trans "Saving..." %}';
|
saveButton.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> {% trans "Saving..." %}';
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
// Get CSRF token
|
const csrfToken = getCookie('csrftoken');
|
||||||
const csrfToken = getCSRFToken();
|
|
||||||
if (!csrfToken) {
|
if (!csrfToken) {
|
||||||
alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}');
|
alert('{% trans "Error: Unable to get CSRF token. Please refresh the page and try again." %}');
|
||||||
saveButton.disabled = false;
|
saveButton.disabled = false;
|
||||||
saveButton.innerHTML = '<i data-lucide="save" class="w-5 h-5"></i> {% trans "Save Mapping" %}';
|
saveButton.innerHTML = originalContent;
|
||||||
|
lucide.createIcons();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('CSRF Token:', csrfToken.substring(0, 20) + '...');
|
|
||||||
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
@ -389,7 +416,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log('Response status:', response.status);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return response.json().then(err => {
|
return response.json().then(err => {
|
||||||
throw new Error(JSON.stringify(err));
|
throw new Error(JSON.stringify(err));
|
||||||
@ -398,17 +424,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log('Success:', data);
|
|
||||||
// Hide modal and reload page
|
|
||||||
hideModal(addModal);
|
hideModal(addModal);
|
||||||
location.reload();
|
location.reload();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error saving mapping:', error);
|
console.error('Error saving mapping:', error);
|
||||||
alert('{% trans "Error saving mapping" %}: ' + error.message);
|
alert('{% trans "Error saving mapping" %}: ' + error.message);
|
||||||
// Re-enable button
|
|
||||||
saveButton.disabled = false;
|
saveButton.disabled = false;
|
||||||
saveButton.innerHTML = '<i data-lucide="save" class="w-5 h-5"></i> {% trans "Save Mapping" %}';
|
saveButton.innerHTML = originalContent;
|
||||||
|
lucide.createIcons();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -416,37 +440,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
addModal.addEventListener('hidden', function() {
|
addModal.addEventListener('hidden', function() {
|
||||||
document.getElementById('mappingForm').reset();
|
document.getElementById('mappingForm').reset();
|
||||||
document.getElementById('mappingId').value = '';
|
document.getElementById('mappingId').value = '';
|
||||||
document.getElementById('mappingModalTitle').textContent = '{% trans "Add Survey Template Mapping" %}';
|
document.getElementById('mappingModalTitle').innerHTML = `
|
||||||
|
<i data-lucide="layers" class="w-6 h-6 text-blue"></i> {% trans "Add Survey Template Mapping" %}
|
||||||
|
`;
|
||||||
|
lucide.createIcons();
|
||||||
});
|
});
|
||||||
|
|
||||||
// CSRF token helper - multiple methods for reliability
|
|
||||||
function getCSRFToken() {
|
|
||||||
// Method 1: Try to get from hidden input (most reliable)
|
|
||||||
const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
|
|
||||||
if (csrfInput) {
|
|
||||||
return csrfInput.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Try to get from cookie (case-insensitive)
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i].trim();
|
|
||||||
// Check both 'csrftoken' and 'csrfToken' (case-insensitive)
|
|
||||||
if (cookie.toLowerCase().startsWith('csrftoken=')) {
|
|
||||||
return decodeURIComponent(cookie.substring(10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Check for Django's meta tag
|
|
||||||
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
|
||||||
if (metaTag) {
|
|
||||||
return metaTag.getAttribute('content');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Unable to find CSRF token');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Lucide icons
|
// Initialize Lucide icons
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -150,18 +150,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select class="form-select" name="hospital">
|
|
||||||
<option value="">All Hospitals</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name_en }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">{% trans "Department" %}</label>
|
<label class="form-label">{% trans "Department" %}</label>
|
||||||
<select class="form-select" name="department">
|
<select class="form-select" name="department">
|
||||||
|
|||||||
@ -68,20 +68,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if request.user.is_px_admin %}
|
|
||||||
<div>
|
|
||||||
<label class="field-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:'s' %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="field-label">{% trans "Status" %}</label>
|
<label class="field-label">{% trans "Status" %}</label>
|
||||||
<select name="status" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
<select name="status" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
||||||
|
|||||||
@ -196,17 +196,6 @@
|
|||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if request.GET.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">{% trans "Department" %}</label>
|
<label class="form-label">{% trans "Department" %}</label>
|
||||||
<select name="department" class="form-select">
|
<select name="department" class="form-select">
|
||||||
|
|||||||
@ -50,17 +50,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 d-flex align-items-end">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<i class="bi bi-search me-2"></i>{% trans "Filter" %}
|
<i class="bi bi-search me-2"></i>{% trans "Filter" %}
|
||||||
|
|||||||
@ -64,18 +64,6 @@
|
|||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mb-6">
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 mb-6">
|
||||||
<form method="get" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<form method="get" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<div>
|
|
||||||
<label class="field-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="field-label">{% trans "Doctor ID" %}</label>
|
<label class="field-label">{% trans "Doctor ID" %}</label>
|
||||||
<input type="text" name="doctor_id"
|
<input type="text" name="doctor_id"
|
||||||
|
|||||||
@ -129,18 +129,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="field-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="field-label">{% trans "Department" %}</label>
|
<label class="field-label">{% trans "Department" %}</label>
|
||||||
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
||||||
|
|||||||
@ -105,18 +105,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="field-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:'s' %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="field-label">{% trans "Department" %}</label>
|
<label class="field-label">{% trans "Department" %}</label>
|
||||||
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
||||||
|
|||||||
@ -276,17 +276,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
|
||||||
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
<select name="department" class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none transition">
|
||||||
|
|||||||
@ -57,17 +57,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">{% trans "Department" %}</label>
|
<label class="form-label">{% trans "Department" %}</label>
|
||||||
<select name="department" class="form-select">
|
<select name="department" class="form-select">
|
||||||
|
|||||||
@ -50,17 +50,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="form-select">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 d-flex align-items-end">
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<i class="bi bi-search me-2"></i>{% trans "Filter" %}
|
<i class="bi bi-search me-2"></i>{% trans "Filter" %}
|
||||||
|
|||||||
@ -105,20 +105,7 @@
|
|||||||
<a href="?status=completed" class="filter-btn px-4 py-1.5 rounded-full text-xs font-semibold transition {% if filters.status == 'completed' %}active{% endif %}">
|
<a href="?status=completed" class="filter-btn px-4 py-1.5 rounded-full text-xs font-semibold transition {% if filters.status == 'completed' %}active{% endif %}">
|
||||||
{% trans "Completed" %}
|
{% trans "Completed" %}
|
||||||
</a>
|
</a>
|
||||||
{% if user.is_px_admin %}
|
|
||||||
<div class="h-4 w-[1px] bg-slate-200 mx-2"></div>
|
<div class="h-4 w-[1px] bg-slate-200 mx-2"></div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<label class="text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</label>
|
|
||||||
<select id="hospitalFilter" class="px-3 py-1.5 bg-white border rounded-lg text-xs">
|
|
||||||
<option value="">{% trans "All Hospitals" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:'s' %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="px-6 py-2 text-[10px] font-bold text-slate uppercase bg-slate-50 border-b-2 border-slate-200">
|
<p class="px-6 py-2 text-[10px] font-bold text-slate uppercase bg-slate-50 border-b-2 border-slate-200">
|
||||||
@ -297,18 +284,8 @@ document.getElementById('searchInput')?.addEventListener('keypress', function(e)
|
|||||||
let url = '?';
|
let url = '?';
|
||||||
if (value) url += 'search=' + encodeURIComponent(value);
|
if (value) url += 'search=' + encodeURIComponent(value);
|
||||||
{% if filters.status %}url += '&status={{ filters.status }}';{% endif %}
|
{% if filters.status %}url += '&status={{ filters.status }}';{% endif %}
|
||||||
{% if filters.hospital %}url += '&hospital={{ filters.hospital }}';{% endif %}
|
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hospital filter
|
|
||||||
document.getElementById('hospitalFilter')?.addEventListener('change', function() {
|
|
||||||
let url = '?';
|
|
||||||
if (this.value) url += 'hospital=' + this.value;
|
|
||||||
{% if filters.search %}url += '&search={{ filters.search }}';{% endif %}
|
|
||||||
{% if filters.status %}url += '&status={{ filters.status }}';{% endif %}
|
|
||||||
window.location.href = url;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -76,17 +76,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hospital Filter -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block text-sm font-bold text-navy mb-2">Hospital</label>
|
|
||||||
<select id="hospitalFilter" class="w-full px-4 py-2.5 border-2 border-blue-100 rounded-xl text-navy focus:ring-2 focus:ring-blue focus:border-transparent transition bg-white">
|
|
||||||
<option value="">All Hospitals</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}">{{ hospital.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Department Filter -->
|
<!-- Department Filter -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label class="block text-sm font-bold text-navy mb-2">Department</label>
|
<label class="block text-sm font-bold text-navy mb-2">Department</label>
|
||||||
@ -225,7 +214,6 @@ const CSRF_TOKEN = '{{ csrf_token }}';
|
|||||||
// DOM elements
|
// DOM elements
|
||||||
let dataSourceSelect = null;
|
let dataSourceSelect = null;
|
||||||
let dateRangeSelect = null;
|
let dateRangeSelect = null;
|
||||||
let hospitalFilter = null;
|
|
||||||
let departmentFilter = null;
|
let departmentFilter = null;
|
||||||
let statusFilter = null;
|
let statusFilter = null;
|
||||||
let previewBtn = null;
|
let previewBtn = null;
|
||||||
@ -244,7 +232,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Initialize DOM element references
|
// Initialize DOM element references
|
||||||
dataSourceSelect = document.getElementById('dataSource');
|
dataSourceSelect = document.getElementById('dataSource');
|
||||||
dateRangeSelect = document.getElementById('dateRange');
|
dateRangeSelect = document.getElementById('dateRange');
|
||||||
hospitalFilter = document.getElementById('hospitalFilter');
|
|
||||||
departmentFilter = document.getElementById('departmentFilter');
|
departmentFilter = document.getElementById('departmentFilter');
|
||||||
statusFilter = document.getElementById('statusFilter');
|
statusFilter = document.getElementById('statusFilter');
|
||||||
previewBtn = document.getElementById('previewBtn');
|
previewBtn = document.getElementById('previewBtn');
|
||||||
@ -261,8 +248,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Event listeners
|
// Event listeners
|
||||||
if (dataSourceSelect) dataSourceSelect.addEventListener('change', loadFilterOptions);
|
if (dataSourceSelect) dataSourceSelect.addEventListener('change', loadFilterOptions);
|
||||||
if (dateRangeSelect) dateRangeSelect.addEventListener('change', toggleCustomDateRange);
|
if (dateRangeSelect) dateRangeSelect.addEventListener('change', toggleCustomDateRange);
|
||||||
if (hospitalFilter) hospitalFilter.addEventListener('change', loadDepartments);
|
|
||||||
if (previewBtn) previewBtn.addEventListener('click', generateReport);
|
if (previewBtn) previewBtn.addEventListener('click', generateReport);
|
||||||
|
|
||||||
|
// Load departments for current hospital on page load
|
||||||
|
loadDepartments();
|
||||||
if (saveBtn) saveBtn.addEventListener('click', showSaveModal);
|
if (saveBtn) saveBtn.addEventListener('click', showSaveModal);
|
||||||
if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', hideSaveModal);
|
if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', hideSaveModal);
|
||||||
if (confirmSaveBtn) confirmSaveBtn.addEventListener('click', saveReport);
|
if (confirmSaveBtn) confirmSaveBtn.addEventListener('click', saveReport);
|
||||||
@ -385,7 +374,7 @@ function getSelectedColumns() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDepartments() {
|
async function loadDepartments() {
|
||||||
const hospitalId = hospitalFilter.value;
|
const hospitalId = '{{ current_hospital.id|default:"" }}';
|
||||||
if (!hospitalId) {
|
if (!hospitalId) {
|
||||||
departmentFilter.innerHTML = '<option value="">All Departments</option>';
|
departmentFilter.innerHTML = '<option value="">All Departments</option>';
|
||||||
return;
|
return;
|
||||||
@ -437,7 +426,7 @@ async function generateReport() {
|
|||||||
date_range: dateRangeSelect.value,
|
date_range: dateRangeSelect.value,
|
||||||
date_start: dateRange.start,
|
date_start: dateRange.start,
|
||||||
date_end: dateRange.end,
|
date_end: dateRange.end,
|
||||||
hospital: hospitalFilter.value,
|
hospital: '{{ current_hospital.id|default:"" }}',
|
||||||
department: departmentFilter.value,
|
department: departmentFilter.value,
|
||||||
status: statusFilter.value,
|
status: statusFilter.value,
|
||||||
},
|
},
|
||||||
@ -983,7 +972,7 @@ async function saveReport() {
|
|||||||
date_range: dateRangeSelect.value,
|
date_range: dateRangeSelect.value,
|
||||||
date_start: dateRange.start,
|
date_start: dateRange.start,
|
||||||
date_end: dateRange.end,
|
date_end: dateRange.end,
|
||||||
hospital: hospitalFilter.value,
|
hospital: '{{ current_hospital.id|default:"" }}',
|
||||||
department: departmentFilter.value,
|
department: departmentFilter.value,
|
||||||
status: statusFilter.value,
|
status: statusFilter.value,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -132,20 +132,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hospital -->
|
<!-- Hospital -->
|
||||||
<div>
|
|
||||||
<label for="hospital" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</label>
|
|
||||||
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"
|
|
||||||
id="hospital" name="hospital">
|
|
||||||
<option value="">{% trans "All" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}"
|
|
||||||
{% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Patient Type -->
|
<!-- Patient Type -->
|
||||||
<div>
|
<div>
|
||||||
<label for="patient_type" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Patient Type" %}</label>
|
<label for="patient_type" class="block text-sm font-bold text-gray-700 mb-2">{% trans "Patient Type" %}</label>
|
||||||
|
|||||||
@ -60,18 +60,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<label class="text-xs font-bold text-slate uppercase">{% trans "Hospital" %}</label>
|
|
||||||
<select name="hospital" class="px-3 py-1.5 bg-white border border-slate-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-navy">
|
|
||||||
<option value="">{% trans "All" %}</option>
|
|
||||||
{% for hospital in hospitals %}
|
|
||||||
<option value="{{ hospital.id }}" {% if filters.hospital|add:"0" == hospital.id %}selected{% endif %}>
|
|
||||||
{{ hospital.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button type="submit" class="px-4 py-1.5 bg-navy text-white rounded-lg text-xs font-bold hover:bg-blue transition">{% trans "Apply" %}</button>
|
<button type="submit" class="px-4 py-1.5 bg-navy text-white rounded-lg text-xs font-bold hover:bg-blue transition">{% trans "Apply" %}</button>
|
||||||
<a href="{% url 'surveys:instance_list' %}" class="px-4 py-1.5 border border-slate-200 rounded-lg text-xs font-semibold text-slate hover:bg-white transition">{% trans "Clear" %}</a>
|
<a href="{% url 'surveys:instance_list' %}" class="px-4 py-1.5 border border-slate-200 rounded-lg text-xs font-semibold text-slate hover:bg-white transition">{% trans "Clear" %}</a>
|
||||||
|
|||||||
@ -126,11 +126,16 @@
|
|||||||
<div class="inline-flex items-center justify-center w-20 h-20 bg-slate-100 rounded-full mb-4">
|
<div class="inline-flex items-center justify-center w-20 h-20 bg-slate-100 rounded-full mb-4">
|
||||||
<i data-lucide="clipboard-list" class="w-10 h-10 text-slate-400"></i>
|
<i data-lucide="clipboard-list" class="w-10 h-10 text-slate-400"></i>
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_px_admin and not current_hospital %}
|
||||||
|
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Hospital Selected" %}</h3>
|
||||||
|
<p class="text-slate mb-6">{% trans "Please select a hospital from the dropdown in the header to view survey templates." %}</p>
|
||||||
|
{% else %}
|
||||||
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Templates Found" %}</h3>
|
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Templates Found" %}</h3>
|
||||||
<p class="text-slate mb-6">{% trans "Get started by creating your first survey template." %}</p>
|
<p class="text-slate mb-6">{% trans "Get started by creating your first survey template." %}</p>
|
||||||
<a href="{% url 'surveys:template_create' %}" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition inline-flex items-center gap-2">
|
<a href="{% url 'surveys:template_create' %}" class="px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-blue transition inline-flex items-center gap-2">
|
||||||
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Create Survey Template" %}
|
<i data-lucide="plus" class="w-5 h-5"></i> {% trans "Create Survey Template" %}
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user