From 7369d08012fb0476773f6cb3aafd1bcab2bca7fd Mon Sep 17 00:00:00 2001 From: ismail Date: Sun, 14 Jun 2026 14:29:23 +0300 Subject: [PATCH] feat: unified reference numbers + feedback modules QA audit Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001): - new ReferenceSequence model + generate_reference() helper (apps/core) - Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save() - prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only) - removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources - migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012 - unit tests (format, sanitization, monthly reset, 40-thread concurrency) QA audit: - isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env) - feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion) - reports/feedback-modules-qa-report.md Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules. --- .mimocode/plans/1781172520267-calm-canyon.md | 102 + .opencode/plans/fix-complaint-reimport.md | 115 + .../plans/fix-department-detail-complaints.md | 41 + DEPARTMENT_HIERARCHY_HANDOFF.md | 493 + PX360/settings.py | 24 +- .../commands/create_default_roles.py | 6 - .../migrations/0003_alter_role_name.py | 18 + apps/accounts/models.py | 21 +- apps/accounts/services.py | 52 + apps/accounts/ui_views.py | 24 +- apps/accounts/urls.py | 2 + apps/analytics/kpi_service.py | 148 +- .../management/commands/seed_kpi_data.py | 16 +- apps/analytics/services/analytics_service.py | 14 +- apps/analytics/ui_views.py | 88 +- apps/appreciation/admin.py | 4 + apps/appreciation/migrations/0001_initial.py | 8 +- ...2_remove_appreciation_location_and_more.py | 28 + ...3_appreciation_legacy_location_and_more.py | 18 + ...r_appreciation_legacy_location_and_more.py | 30 + .../migrations/0005_remove_sub_subsection.py | 10 + ..._appreciation_reference_number_and_more.py | 35 + apps/appreciation/models.py | 98 +- apps/appreciation/signals.py | 227 +- apps/appreciation/ui_views.py | 113 +- apps/complaints/admin.py | 56 +- apps/complaints/forms.py | 388 +- .../commands/arabic_dept_mapping.py | 404 + .../commands/backfill_sent_to_department.py | 41 + .../commands/complaint_source_mapping.py | 13 + .../commands/import_2025_complaints_basic.py | 148 +- .../commands/import_all_complaints.py | 99 + .../commands/import_historical_complaints.py | 114 +- apps/complaints/migrations/0001_initial.py | 6 +- apps/complaints/migrations/0003_initial.py | 16 +- ...cancelled_partially_resolved_timestamps.py | 36 + .../0005_add_sent_to_department_fields.py | 43 + .../0006_data_sent_to_department_backfill.py | 63 + ...veddepartment_acceptance_notes_and_more.py | 51 + .../0008_manager_review_workflow.py | 88 + .../0009_fix_manager_review_default.py | 41 + ...0010_remove_complaint_location_and_more.py | 58 + ...0011_complaint_legacy_location_and_more.py | 83 + ...lter_complaint_legacy_location_and_more.py | 60 + .../migrations/0013_remove_sub_subsection.py | 10 + ...int_location_type_inquiry_area_and_more.py | 80 + .../migrations/0015_complaintpdfsummary.py | 35 + .../migrations/0016_champion_investigation.py | 91 + .../0017_add_patient_contact_status.py | 49 + .../migrations/0018_simplify_statuses.py | 36 + .../migrations/0019_add_response_token.py | 28 + apps/complaints/models.py | 678 +- apps/complaints/serializers.py | 19 +- apps/complaints/services/complaint_service.py | 137 +- apps/complaints/signals.py | 138 +- apps/complaints/tasks.py | 144 +- apps/complaints/tests.py | 196 + apps/complaints/tests_inquiry.py | 233 + apps/complaints/ui_views.py | 1662 +- apps/complaints/ui_views_explanation.py | 62 +- apps/complaints/urls.py | 8 + apps/complaints/urls_inquiries.py | 3 + apps/complaints/utils.py | 36 +- apps/complaints/views.py | 1176 +- apps/core/ai_service.py | 71 +- apps/core/config_views.py | 27 +- .../commands/create_e2e_isolated_env.py | 292 + .../commands/create_e2e_test_users.py | 23 +- .../commands/send_example_emails.py | 48 +- .../commands/setup_dev_environment.py | 24 +- apps/core/management/commands/test_email.py | 24 + apps/core/middleware.py | 37 +- apps/core/migrations/0002_add_note_model.py | 35 + .../core/migrations/0003_referencesequence.py | 30 + apps/core/models.py | 64 + apps/core/reference.py | 41 + apps/core/templatetags/email_tags.py | 2 +- apps/core/tests.py | 138 + apps/core/urls.py | 8 +- apps/core/utils.py | 14 + apps/core/views.py | 255 +- .../services/complaint_monthly_export.py | 2 +- .../services/complaint_monthly_service.py | 2 +- .../services/complaint_quarterly_export.py | 76 +- .../services/complaint_quarterly_service.py | 83 +- apps/dashboard/views.py | 68 +- apps/feedback/admin.py | 17 +- apps/feedback/forms.py | 14 +- apps/feedback/migrations/0001_initial.py | 6 +- apps/feedback/migrations/0003_initial.py | 4 +- .../0004_remove_feedback_location_and_more.py | 28 + .../0005_feedback_legacy_location_and_more.py | 26 + ...alter_feedback_legacy_location_and_more.py | 30 + .../migrations/0007_remove_sub_subsection.py | 10 + ...0008_feedback_reference_number_and_more.py | 35 + apps/feedback/models.py | 50 +- apps/feedback/tasks.py | 1 + apps/feedback/views.py | 81 +- apps/integrations/admin.py | 69 + apps/integrations/api_serializers.py | 529 + apps/integrations/api_views.py | 1107 + apps/integrations/authentication.py | 46 + .../migrations/0003_external_api_key.py | 42 + apps/integrations/models.py | 127 + apps/integrations/urls_external.py | 148 + apps/notifications/services.py | 19 + apps/notifications/settings_service.py | 265 +- apps/notifications/views.py | 12 +- apps/observations/admin.py | 2 +- apps/observations/forms.py | 348 +- apps/observations/migrations/0001_initial.py | 6 +- apps/observations/migrations/0002_initial.py | 4 +- ...03_rename_observation_note_related_name.py | 19 + .../0004_add_sent_to_department_fields.py | 23 + .../0005_data_sent_to_department_backfill.py | 37 + ...06_remove_observation_location_and_more.py | 28 + ...07_observation_legacy_location_and_more.py | 36 + ...er_observation_legacy_location_and_more.py | 30 + .../migrations/0009_remove_sub_subsection.py | 10 + ...0010_observation_location_type_and_more.py | 35 + .../migrations/0011_observation_area.py | 20 + .../0012_alter_observation_tracking_code.py | 19 + .../migrations/0013_simplify_statuses.py | 46 + .../migrations/0014_map_statuses.py | 67 + .../migrations/0015_add_response_token.py | 28 + apps/observations/models.py | 150 +- apps/observations/services.py | 49 +- apps/observations/tasks.py | 56 +- apps/observations/test_location_type.py | 121 + apps/observations/tests.py | 2 +- apps/observations/urls.py | 6 + apps/observations/views.py | 873 +- apps/organizations/admin.py | 41 +- .../commands/backfill_department_hierarchy.py | 356 + .../export_departments_subsections.py | 4 +- .../management/commands/export_org_csv.py | 14 +- .../management/commands/import_areas.py | 119 + .../management/commands/import_departments.py | 333 + .../commands/import_departments_excel.py | 259 + .../management/commands/import_staff_excel.py | 280 + .../commands/populate_location_data.py | 14 +- .../management/commands/seed_staff.py | 6 +- .../management/commands/set_hr_names.py | 64 + .../management/commands/wipe_dev_data.py | 123 + .../0002_rename_respondent_to_champion.py | 19 + .../0003_alter_department_champion.py | 19 + ...gacylocation_legacymainsection_and_more.py | 176 + ...005_alter_legacylocation_table_and_more.py | 25 + ..._code_alter_orgsubsection_code_and_more.py | 28 + ...alter_department_location_type_and_more.py | 49 + ...e_orgsubsection_to_section_add_champion.py | 59 + .../0009_remove_sub_subsection_fields.py | 18 + .../0010_section_add_roles_display_names.py | 52 + .../migrations/0011_area_department_area.py | 37 + .../migrations/0012_department_hr_name.py | 18 + .../migrations/0013_add_subsection_model.py | 36 + .../0014_remove_department_manager_1st.py | 17 + apps/organizations/models.py | 285 +- apps/organizations/serializers.py | 88 +- apps/organizations/services.py | 82 +- apps/organizations/ui_views.py | 1881 +- apps/organizations/urls.py | 74 +- apps/organizations/views.py | 190 +- apps/projects/ui_views.py | 20 +- apps/px_action_center/tasks.py | 27 +- apps/px_sources/ui_views.py | 76 +- apps/rca/views.py | 137 +- apps/reports/models.py | 5 + apps/reports/tasks.py | 22 +- apps/standards/views.py | 2 +- config/urls.py | 2 + ...nal List of Departments - 4th Version.xlsx | Bin 0 -> 61161 bytes ...nal List of Departments - 6th Version.xlsx | Bin 0 -> 73422 bytes data/employees.xlsx | Bin 0 -> 701634 bytes dep.md | 587 + ...PX360_External_API.postman_collection.json | 799 + docs/external-api.md | 673 + e2e/helpers/audit.ts | 156 + e2e/helpers/helpers.ts | 2 +- e2e/results/audit-observations.json | 142 + e2e/results/test-results/.last-run.json | 9 +- .../error-context.md | 203 - .../test-failed-1.png | Bin 286676 -> 0 bytes .../video.webm | Bin 54927 -> 0 bytes .../error-context.md | 417 - .../test-failed-1.png | Bin 181881 -> 0 bytes .../video.webm | Bin 480916 -> 0 bytes .../error-context.md | 1893 - .../test-failed-1.png | Bin 259235 -> 0 bytes .../video.webm | Bin 2812527 -> 0 bytes .../error-context.md | 365 - .../test-failed-1.png | Bin 106286 -> 0 bytes .../video.webm | Bin 414931 -> 0 bytes .../error-context.md | 395 - .../test-failed-1.png | Bin 113103 -> 0 bytes .../video.webm | Bin 2119658 -> 0 bytes .../error-context.md | 438 - .../test-failed-1.png | Bin 73373 -> 0 bytes .../video.webm | Bin 983845 -> 0 bytes .../workflows/feedback-modules-audit.spec.ts | 649 + his_visits_export.json | 76898 ++++++++++++++++ his_visits_export_cleaned.json | 76898 ++++++++++++++++ org_hir_plan.md | 601 + playwright.config.ts | 4 + reports/feedback-modules-qa-report.md | 179 + static/css/px360.css | 1 + static/images/artboard/Artboard 1@3x (1).png | Bin 0 -> 72374 bytes static/images/artboard/Artboard 1@3x.png | Bin 0 -> 72374 bytes static/images/artboard/Artboard 3@3x.png | Bin 0 -> 78449 bytes static/images/artboard/Artboard 4@3x.png | Bin 0 -> 77852 bytes static/src/css/tailwind.css | 6 +- .../vendor/fonts/noto-kufi-arabic/arabic.css | 77 + .../files/noto-kufi-arabic-arabic.woff2 | Bin 0 -> 123688 bytes .../files/noto-kufi-arabic-full-bold.ttf | Bin 0 -> 178596 bytes .../files/noto-kufi-arabic-full-regular.ttf | Bin 0 -> 178472 bytes .../files/noto-kufi-arabic-latin-ext.woff2 | Bin 0 -> 16500 bytes .../files/noto-kufi-arabic-latin.woff2 | Bin 0 -> 30724 bytes .../files/noto-kufi-arabic-math.woff2 | Bin 0 -> 22228 bytes .../files/noto-kufi-arabic-symbols.woff2 | Bin 0 -> 14132 bytes templates/accounts/change_password.html | 10 +- templates/accounts/login.html | 10 +- .../accounts/onboarding/completion_email.html | 6 +- .../accounts/onboarding/invitation_email.html | 2 +- .../accounts/onboarding/reminder_email.html | 2 +- templates/accounts/password_reset.html | 10 +- .../accounts/password_reset_confirm.html | 10 +- templates/analytics/dashboard.html | 424 +- .../appreciation/appreciation_detail.html | 670 +- templates/appreciation/leaderboard.html | 463 +- templates/complaints/complaint_detail.html | 414 +- templates/complaints/complaint_form.html | 273 +- templates/complaints/complaint_list.html | 75 +- templates/complaints/complaint_pdf.html | 6 +- .../complaints/complaint_summary_pdf.html | 313 + .../explanation_already_submitted.html | 10 +- templates/complaints/explanation_form.html | 60 +- templates/complaints/explanation_success.html | 10 +- .../complaints/government_ticket_detail.html | 12 +- .../complaints/government_ticket_form.html | 171 +- .../complaints/government_ticket_list.html | 2 +- .../inquiry_department_response.html | 113 - templates/complaints/inquiry_detail.html | 1389 +- ...inquiry_explanation_already_submitted.html | 10 +- .../complaints/inquiry_explanation_form.html | 10 +- .../inquiry_explanation_success.html | 10 +- templates/complaints/inquiry_form.html | 223 +- templates/complaints/inquiry_list.html | 39 +- .../inquiry_response_already_submitted.html | 18 + .../inquiry_response_form_token.html | 51 + .../inquiry_response_success_token.html | 19 + .../inquiry_response_token_invalid.html | 18 + .../investigation_already_started.html | 35 + .../investigation_already_submitted.html | 43 + templates/complaints/investigation_error.html | 29 + .../complaints/investigation_questions.html | 167 + .../complaints/investigation_respond.html | 90 + .../complaints/investigation_review.html | 132 + .../complaints/investigation_success.html | 42 + .../complaints/partials/actions_panel.html | 2 +- .../partials/adverse_actions_panel.html | 8 +- templates/complaints/partials/ai_panel.html | 170 +- .../partials/departments_panel.html | 109 +- .../partials/explanation_panel.html | 57 +- .../partials/inquiry_timeline_panel.html | 45 + .../partials/pdf_letterhead_footer.html | 15 + .../partials/pdf_letterhead_header.html | 11 + .../partials/pdf_summary_panel.html | 440 + templates/complaints/partials/rca_panel.html | 4 + .../complaints/partials/resolution_panel.html | 4 +- .../complaints/partials/staff_panel.html | 6 +- .../complaints/partials/timeline_panel.html | 61 +- .../patient_complaint_visit_form.html | 2 +- .../complaints/public_complaint_form.html | 659 +- .../complaints/public_complaint_track.html | 4 +- templates/complaints/public_inquiry_form.html | 122 +- .../complaints/public_inquiry_track.html | 64 +- .../complaints/send_to_department_form.html | 48 +- .../send_to_department_preview.html | 5 +- .../components/department_response_modal.html | 173 + templates/components/send_to_modal.html | 64 +- .../config/emails/reset_password_email.html | 9 +- templates/config/hospital_users.html | 34 +- templates/core/public_submit.html | 569 +- templates/core/public_track.html | 126 +- templates/core/select_hospital.html | 10 +- templates/dashboard/analytics-template.html | 10 +- .../dashboard/complaint_quarterly_report.html | 52 +- .../partials/observations_table.html | 2 +- templates/emails/base_email_template.html | 29 +- .../communication_request_notification.html | 57 +- templates/emails/explanation_request.html | 2 +- .../emails/inquiry_explanation_request.html | 69 +- templates/feedback/feedback_detail.html | 271 +- templates/feedback/feedback_form.html | 94 +- templates/layouts/base.html | 3 +- templates/layouts/partials/sidebar.html | 867 +- templates/layouts/public_base.html | 12 +- templates/layouts/source_user_base.html | 12 +- .../observations/observation_create.html | 690 +- .../observation_department_response.html | 139 +- .../observations/observation_detail.html | 1381 +- templates/observations/observation_list.html | 48 +- .../partials/observation_timeline_panel.html | 45 + templates/observations/public_new.html | 199 +- templates/observations/public_success.html | 8 +- templates/observations/public_track.html | 79 +- .../response_already_submitted.html | 18 + .../observations/response_form_token.html | 52 + .../observations/response_success_token.html | 19 + .../observations/response_token_invalid.html | 18 + .../department_complaint_detail.html | 167 + .../organizations/department_complaints.html | 213 + .../organizations/department_detail.html | 1110 +- .../organizations/department_inquiries.html | 174 + .../department_inquiry_detail.html | 139 + templates/organizations/department_list.html | 4 - .../department_manager_review.html | 184 + .../department_observation_detail.html | 142 + .../department_observations.html | 171 + .../department_staff_detail.html | 698 + .../emails/staff_credentials.html | 11 +- .../manager_review_question_form.html | 88 + .../manager_review_questions.html | 82 + .../orgsection_confirm_delete.html | 56 + .../organizations/orgsection_detail.html | 191 + templates/organizations/orgsection_form.html | 157 + templates/organizations/orgsection_list.html | 214 + .../orgsubsection_confirm_delete.html | 56 + .../organizations/orgsubsection_form.html | 167 + .../organizations/orgsubsection_list.html | 207 + .../organizations/patient_visit_journey.html | 2 +- templates/organizations/staff_detail.html | 81 +- .../organizations/staff_hierarchy_d3.html | 2 +- templates/organizations/staff_list.html | 2 +- templates/partials/list_stats_bar.html | 16 + templates/partials/notes_panel.html | 50 + templates/partials/pagination.html | 62 + templates/partials/stage_timeline.html | 7 +- .../presentations/presentation_view.html | 10 +- templates/projects/partials/phase_column.html | 2 +- .../projects/partials/phase_form_modal.html | 4 +- templates/projects/partials/phase_header.html | 2 +- .../projects/partials/task_form_modal.html | 4 +- templates/projects/partials/task_row.html | 4 +- templates/projects/project_detail.html | 8 +- templates/px_sources/source_detail.html | 3 +- .../source_user_create_complaint.html | 121 +- .../source_user_create_inquiry.html | 85 +- .../source_user_create_observation.html | 97 +- templates/rca/rca_detail.html | 8 +- templates/surveys/invalid_token.html | 10 +- templates/surveys/public_form.html | 10 +- templates/surveys/survey_template.html | 10 +- templates/surveys/thank_you.html | 10 +- templates/temp/complaint_detail_temp.html | 10 +- templates/temp/complaint_list_temp.html | 10 +- templates/template.html | 10 +- 357 files changed, 185641 insertions(+), 12476 deletions(-) create mode 100644 .mimocode/plans/1781172520267-calm-canyon.md create mode 100644 .opencode/plans/fix-complaint-reimport.md create mode 100644 .opencode/plans/fix-department-detail-complaints.md create mode 100644 DEPARTMENT_HIERARCHY_HANDOFF.md create mode 100644 apps/accounts/migrations/0003_alter_role_name.py create mode 100644 apps/appreciation/migrations/0002_remove_appreciation_location_and_more.py create mode 100644 apps/appreciation/migrations/0003_appreciation_legacy_location_and_more.py create mode 100644 apps/appreciation/migrations/0004_alter_appreciation_legacy_location_and_more.py create mode 100644 apps/appreciation/migrations/0005_remove_sub_subsection.py create mode 100644 apps/appreciation/migrations/0006_appreciation_reference_number_and_more.py create mode 100644 apps/complaints/management/commands/arabic_dept_mapping.py create mode 100644 apps/complaints/management/commands/backfill_sent_to_department.py create mode 100644 apps/complaints/management/commands/import_all_complaints.py create mode 100644 apps/complaints/migrations/0004_add_cancelled_partially_resolved_timestamps.py create mode 100644 apps/complaints/migrations/0005_add_sent_to_department_fields.py create mode 100644 apps/complaints/migrations/0006_data_sent_to_department_backfill.py create mode 100644 apps/complaints/migrations/0007_complaintinvolveddepartment_acceptance_notes_and_more.py create mode 100644 apps/complaints/migrations/0008_manager_review_workflow.py create mode 100644 apps/complaints/migrations/0009_fix_manager_review_default.py create mode 100644 apps/complaints/migrations/0010_remove_complaint_location_and_more.py create mode 100644 apps/complaints/migrations/0011_complaint_legacy_location_and_more.py create mode 100644 apps/complaints/migrations/0012_alter_complaint_legacy_location_and_more.py create mode 100644 apps/complaints/migrations/0013_remove_sub_subsection.py create mode 100644 apps/complaints/migrations/0014_complaint_area_complaint_location_type_inquiry_area_and_more.py create mode 100644 apps/complaints/migrations/0015_complaintpdfsummary.py create mode 100644 apps/complaints/migrations/0016_champion_investigation.py create mode 100644 apps/complaints/migrations/0017_add_patient_contact_status.py create mode 100644 apps/complaints/migrations/0018_simplify_statuses.py create mode 100644 apps/complaints/migrations/0019_add_response_token.py create mode 100644 apps/complaints/tests.py create mode 100644 apps/complaints/tests_inquiry.py create mode 100644 apps/core/management/commands/create_e2e_isolated_env.py create mode 100644 apps/core/management/commands/test_email.py create mode 100644 apps/core/migrations/0002_add_note_model.py create mode 100644 apps/core/migrations/0003_referencesequence.py create mode 100644 apps/core/reference.py create mode 100644 apps/core/tests.py create mode 100644 apps/feedback/migrations/0004_remove_feedback_location_and_more.py create mode 100644 apps/feedback/migrations/0005_feedback_legacy_location_and_more.py create mode 100644 apps/feedback/migrations/0006_alter_feedback_legacy_location_and_more.py create mode 100644 apps/feedback/migrations/0007_remove_sub_subsection.py create mode 100644 apps/feedback/migrations/0008_feedback_reference_number_and_more.py create mode 100644 apps/integrations/api_serializers.py create mode 100644 apps/integrations/api_views.py create mode 100644 apps/integrations/authentication.py create mode 100644 apps/integrations/migrations/0003_external_api_key.py create mode 100644 apps/integrations/urls_external.py create mode 100644 apps/observations/migrations/0003_rename_observation_note_related_name.py create mode 100644 apps/observations/migrations/0004_add_sent_to_department_fields.py create mode 100644 apps/observations/migrations/0005_data_sent_to_department_backfill.py create mode 100644 apps/observations/migrations/0006_remove_observation_location_and_more.py create mode 100644 apps/observations/migrations/0007_observation_legacy_location_and_more.py create mode 100644 apps/observations/migrations/0008_alter_observation_legacy_location_and_more.py create mode 100644 apps/observations/migrations/0009_remove_sub_subsection.py create mode 100644 apps/observations/migrations/0010_observation_location_type_and_more.py create mode 100644 apps/observations/migrations/0011_observation_area.py create mode 100644 apps/observations/migrations/0012_alter_observation_tracking_code.py create mode 100644 apps/observations/migrations/0013_simplify_statuses.py create mode 100644 apps/observations/migrations/0014_map_statuses.py create mode 100644 apps/observations/migrations/0015_add_response_token.py create mode 100644 apps/observations/test_location_type.py create mode 100644 apps/organizations/management/commands/backfill_department_hierarchy.py create mode 100644 apps/organizations/management/commands/import_areas.py create mode 100644 apps/organizations/management/commands/import_departments.py create mode 100644 apps/organizations/management/commands/import_departments_excel.py create mode 100644 apps/organizations/management/commands/import_staff_excel.py create mode 100644 apps/organizations/management/commands/set_hr_names.py create mode 100644 apps/organizations/management/commands/wipe_dev_data.py create mode 100644 apps/organizations/migrations/0002_rename_respondent_to_champion.py create mode 100644 apps/organizations/migrations/0003_alter_department_champion.py create mode 100644 apps/organizations/migrations/0004_legacylocation_legacymainsection_and_more.py create mode 100644 apps/organizations/migrations/0005_alter_legacylocation_table_and_more.py create mode 100644 apps/organizations/migrations/0006_alter_department_code_alter_orgsubsection_code_and_more.py create mode 100644 apps/organizations/migrations/0007_remove_department_area_alter_department_location_type_and_more.py create mode 100644 apps/organizations/migrations/0008_rename_orgsubsection_to_section_add_champion.py create mode 100644 apps/organizations/migrations/0009_remove_sub_subsection_fields.py create mode 100644 apps/organizations/migrations/0010_section_add_roles_display_names.py create mode 100644 apps/organizations/migrations/0011_area_department_area.py create mode 100644 apps/organizations/migrations/0012_department_hr_name.py create mode 100644 apps/organizations/migrations/0013_add_subsection_model.py create mode 100644 apps/organizations/migrations/0014_remove_department_manager_1st.py create mode 100644 data/Final List of Departments - 4th Version.xlsx create mode 100644 data/Final List of Departments - 6th Version.xlsx create mode 100644 data/employees.xlsx create mode 100644 dep.md create mode 100644 docs/PX360_External_API.postman_collection.json create mode 100644 docs/external-api.md create mode 100644 e2e/helpers/audit.ts create mode 100644 e2e/results/audit-observations.json delete mode 100644 e2e/results/test-results/workflows-complaint-lifecy-44a33-anges-status-to-in-progress-chromium/error-context.md delete mode 100644 e2e/results/test-results/workflows-complaint-lifecy-44a33-anges-status-to-in-progress-chromium/test-failed-1.png delete mode 100644 e2e/results/test-results/workflows-complaint-lifecy-44a33-anges-status-to-in-progress-chromium/video.webm delete mode 100644 e2e/results/test-results/workflows-inquiry-lifecycl-25b7f-dal-opens-on-inquiry-detail-chromium/error-context.md delete mode 100644 e2e/results/test-results/workflows-inquiry-lifecycl-25b7f-dal-opens-on-inquiry-detail-chromium/test-failed-1.png delete mode 100644 e2e/results/test-results/workflows-inquiry-lifecycl-25b7f-dal-opens-on-inquiry-detail-chromium/video.webm delete mode 100644 e2e/results/test-results/workflows-observation-life-39b2e-lic-form-gets-tracking-code-chromium/error-context.md delete mode 100644 e2e/results/test-results/workflows-observation-life-39b2e-lic-form-gets-tracking-code-chromium/test-failed-1.png delete mode 100644 e2e/results/test-results/workflows-observation-life-39b2e-lic-form-gets-tracking-code-chromium/video.webm delete mode 100644 e2e/results/test-results/workflows-px-employee-crea-4454d-tes-Tests-create-QI-project-chromium/error-context.md delete mode 100644 e2e/results/test-results/workflows-px-employee-crea-4454d-tes-Tests-create-QI-project-chromium/test-failed-1.png delete mode 100644 e2e/results/test-results/workflows-px-employee-crea-4454d-tes-Tests-create-QI-project-chromium/video.webm delete mode 100644 e2e/results/test-results/workflows-px-source-workfl-5dd21-Tests-assign-user-to-source-chromium/error-context.md delete mode 100644 e2e/results/test-results/workflows-px-source-workfl-5dd21-Tests-assign-user-to-source-chromium/test-failed-1.png delete mode 100644 e2e/results/test-results/workflows-px-source-workfl-5dd21-Tests-assign-user-to-source-chromium/video.webm delete mode 100644 e2e/results/test-results/workflows-rca-lifecycle-RCA-Workflow-create-RCA-chromium/error-context.md delete mode 100644 e2e/results/test-results/workflows-rca-lifecycle-RCA-Workflow-create-RCA-chromium/test-failed-1.png delete mode 100644 e2e/results/test-results/workflows-rca-lifecycle-RCA-Workflow-create-RCA-chromium/video.webm create mode 100644 e2e/tests/workflows/feedback-modules-audit.spec.ts create mode 100644 his_visits_export.json create mode 100644 his_visits_export_cleaned.json create mode 100644 org_hir_plan.md create mode 100644 reports/feedback-modules-qa-report.md create mode 100644 static/images/artboard/Artboard 1@3x (1).png create mode 100644 static/images/artboard/Artboard 1@3x.png create mode 100644 static/images/artboard/Artboard 3@3x.png create mode 100644 static/images/artboard/Artboard 4@3x.png create mode 100644 static/vendor/fonts/noto-kufi-arabic/arabic.css create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-arabic.woff2 create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-full-bold.ttf create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-full-regular.ttf create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-latin-ext.woff2 create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-latin.woff2 create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-math.woff2 create mode 100644 static/vendor/fonts/noto-kufi-arabic/files/noto-kufi-arabic-symbols.woff2 create mode 100644 templates/complaints/complaint_summary_pdf.html delete mode 100644 templates/complaints/inquiry_department_response.html create mode 100644 templates/complaints/inquiry_response_already_submitted.html create mode 100644 templates/complaints/inquiry_response_form_token.html create mode 100644 templates/complaints/inquiry_response_success_token.html create mode 100644 templates/complaints/inquiry_response_token_invalid.html create mode 100644 templates/complaints/investigation_already_started.html create mode 100644 templates/complaints/investigation_already_submitted.html create mode 100644 templates/complaints/investigation_error.html create mode 100644 templates/complaints/investigation_questions.html create mode 100644 templates/complaints/investigation_respond.html create mode 100644 templates/complaints/investigation_review.html create mode 100644 templates/complaints/investigation_success.html create mode 100644 templates/complaints/partials/inquiry_timeline_panel.html create mode 100644 templates/complaints/partials/pdf_letterhead_footer.html create mode 100644 templates/complaints/partials/pdf_letterhead_header.html create mode 100644 templates/complaints/partials/pdf_summary_panel.html create mode 100644 templates/components/department_response_modal.html create mode 100644 templates/observations/partials/observation_timeline_panel.html create mode 100644 templates/observations/response_already_submitted.html create mode 100644 templates/observations/response_form_token.html create mode 100644 templates/observations/response_success_token.html create mode 100644 templates/observations/response_token_invalid.html create mode 100644 templates/organizations/department_complaint_detail.html create mode 100644 templates/organizations/department_complaints.html create mode 100644 templates/organizations/department_inquiries.html create mode 100644 templates/organizations/department_inquiry_detail.html create mode 100644 templates/organizations/department_manager_review.html create mode 100644 templates/organizations/department_observation_detail.html create mode 100644 templates/organizations/department_observations.html create mode 100644 templates/organizations/department_staff_detail.html create mode 100644 templates/organizations/manager_review_question_form.html create mode 100644 templates/organizations/manager_review_questions.html create mode 100644 templates/organizations/orgsection_confirm_delete.html create mode 100644 templates/organizations/orgsection_detail.html create mode 100644 templates/organizations/orgsection_form.html create mode 100644 templates/organizations/orgsection_list.html create mode 100644 templates/organizations/orgsubsection_confirm_delete.html create mode 100644 templates/organizations/orgsubsection_form.html create mode 100644 templates/organizations/orgsubsection_list.html create mode 100644 templates/partials/list_stats_bar.html create mode 100644 templates/partials/notes_panel.html create mode 100644 templates/partials/pagination.html diff --git a/.mimocode/plans/1781172520267-calm-canyon.md b/.mimocode/plans/1781172520267-calm-canyon.md new file mode 100644 index 0000000..802fc2d --- /dev/null +++ b/.mimocode/plans/1781172520267-calm-canyon.md @@ -0,0 +1,102 @@ +# Plan: Unify Department Response + Add Reporter Notifications + +## Goal +1. Make all three types use the same department response approach (token-based, no login) +2. Add reporter notifications for complaint/inquiry when department responds + +--- + +## 1. UNIFY DEPARTMENT RESPONSE (Token-Based for All) + +### Current State +- **Complaint**: Uses `ComplaintExplanation` model with token links — champion clicks link, submits explanation without login +- **Observation**: Champion logs into system, navigates to department detail page, submits response via form +- **Inquiry**: Same as observation — login required + +### Target State +All three use token-based links (like complaint): +- PX team sends to department → champion receives email with one-time link +- Champion clicks link → submits response without login +- Link expires after use + +### Changes Needed + +#### a) Create shared `DepartmentResponse` model (or reuse existing patterns) +Better approach: Create a token-based response flow for observation and inquiry similar to complaint's `ComplaintExplanation`. + +**New model in `apps/organizations/models.py`** (or `apps/core/models.py`): +```python +class DepartmentResponseToken(UUIDModel, TimeStampedModel): + """Token-based department response link for observations and inquiries.""" + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.UUIDField() + content_object = GenericForeignKey('content_type', 'object_id') + + department = models.ForeignKey('organizations.Department', on_delete=models.CASCADE) + staff = models.ForeignKey('organizations.Staff', on_delete=models.CASCADE) + token = models.CharField(max_length=100, unique=True) + + is_used = models.BooleanField(default=False) + responded_at = models.DateTimeField(null=True, blank=True) + + # SLA + sla_due_at = models.DateTimeField(null=True, blank=True) + is_overdue = models.BooleanField(default=False) + + class Meta: + indexes = [models.Index(fields=['token'])] +``` + +Actually, simpler approach: just add token fields to the existing models and create views for token-based response. + +**For Observation** (`apps/observations/models.py`): +- Add `response_token` field (CharField, unique, nullable) +- Add `response_token_used` (BooleanField, default=False) +- Add `response_token_sent_at` (DateTimeField, nullable) + +**For Inquiry** (`apps/complaints/models.py`): +- Add same fields to Inquiry model + +#### b) Create token-based response views +- `observation_respond_with_token(request, pk, token)` — public view, no auth required +- `inquiry_respond_with_token(request, pk, token)` — public view, no auth required + +#### c) Update "Send to Department" views +- Generate token, include in email link +- Email template similar to complaint's explanation request + +#### d) Update department detail page +- Remove direct response forms (or keep as fallback for logged-in users) +- Show token-based response status instead + +--- + +## 2. ADD REPORTER NOTIFICATIONS + +### Current State +- **Complaint**: No notification to complainant when department responds +- **Observation**: SMS + email to reporter when department responds ✓ +- **Inquiry**: No notification to inquirer when department responds + +### Changes Needed + +#### a) Complaint — notify complainant on department response +**File**: `apps/complaints/ui_views.py` (in `involved_department_response` view) +- After department submits response, send SMS + email to complainant +- Include tracking link + +#### b) Inquiry — notify inquirer on department response +**File**: `apps/complaints/ui_views.py` (in `inquiry_department_response` view) +- After department submits response, send SMS + email to inquirer +- Include tracking link + +--- + +## Implementation Order + +1. Add token fields to Observation and Inquiry models +2. Create token-based response views for both +3. Update "Send to Department" to generate tokens and send email links +4. Add reporter notifications to complaint and inquiry department response flows +5. Update templates +6. Create migration diff --git a/.opencode/plans/fix-complaint-reimport.md b/.opencode/plans/fix-complaint-reimport.md new file mode 100644 index 0000000..04643c5 --- /dev/null +++ b/.opencode/plans/fix-complaint-reimport.md @@ -0,0 +1,115 @@ +# Fix: Re-import Complaints with Missing Data + +## Overview +Full re-import of all complaints (2022-2025) to fix missing timeline dates, satisfaction, resolution_outcome, and other data. + +## Steps + +### 1. Delete all existing complaints +```python +Complaint.objects.all().delete() +``` + +### 2. Fix `import_historical_complaints.py` (2022-2024) + +**2a. Fix COLUMN_MAPPING — add missing columns:** +```python +COLUMN_MAPPING = { + # ... existing mappings ... + "form_sent_date": 12, # إرسال نموذج الشكوى → form_sent_at + "activated_date": 17, # تفعيل الشكوى → activated_at + # col 20 (date_sent) stays → forwarded_to_dept_at (was wrongly mapped to activated_at) + "escalation_reason": 35, # Reason of Escalation → metadata + "recommendation": 58, # Recommendation/Action plan → recommendation_action_plan +} +``` + +**2b. Fix Complaint.objects.create() — add fields:** +- `form_sent_at=form_sent_date` (col 12) +- `activated_at=activated_date` (col 17, NOT col 20) +- `forwarded_to_dept_at=date_sent` (col 20, was wrongly in activated_at) +- `satisfaction=normalize_satisfaction(satisfaction_val)` (col 56, write to model field NOT just metadata) +- `recommendation_action_plan=recommendation` (col 58) + +**2c. Add satisfaction normalization helper:** +```python +def _normalize_satisfaction(self, val): + val = str(val or "").strip().lower() + mapping = {"satisfied": "satisfied", "dissatisfied": "dissatisfied", "no response": "no_response"} + return mapping.get(val, "") +``` + +### 3. Fix `import_2025_complaints_basic.py` + +**3a. Fix HEADER_ALIASES — fix satisfaction and add missing columns:** +```python +HEADER_ALIASES = { + # ... existing correct mappings ... + "satisfaction": ["Satisfied/Dissatisfied"], # FIXED: was pointing to wrong column + "rightful_side": ["The Rightful Side"], # NEW: col 66 + "complaint_subject": ["موضوع الشكوى الأساسية"], # NEW: col 53 + "form_sent_date": ["إرسال نموذج الشكوى"], # NEW: col 16 + "activated_date": ["تفعيل الشكوى"], # NEW: col 21 + "sent_date": ["تم ارسال الشكوى"], # NEW: col 24 + "first_reminder": ["First Reminder Sent"], # NEW: col 28 + "second_reminder": ["Second Reminder Sent"], # NEW: col 32 + "escalated_date": ["Escalated"], # NEW: col 36 + "closed_date": ["Closed"], # NEW: col 40 + "resolved_date": ["Resolved"], # NEW: col 44 + "delay_reason": ["سبب تأخير القسم بالرد"], # NEW: col 58 + "closure_delay": ["سبب تأخير اغلاق الشكوى خلال 72 ساعه"], # col 59 + "action_taken": ["الاجراء المتخذ من قبل القسم المعني"], # col 62 + "action_result": ["نتيجة الاجراء المتخذ بعد التحقيق"], # col 63 + "recommendation": ["Recommendation/Action plan"], # col 64 + "solutions": ["حلول واقتراحات"], # col 67 +} +``` + +**3b. Fix _process_sheet() — parse new timeline dates:** +Add parsing for all new date fields (form_sent_date, activated_date, sent_date, first_reminder, second_reminder, escalated_date, closed_date, resolved_date). + +**3c. Fix status determination:** +```python +# Current: only checks response_date → open/resolved +# Fixed: check all dates like historical script +if closed_date: + status = "closed" +elif resolved_date: + status = "resolved" +elif escalated_date: + status = "in_progress" +else: + status = "open" +``` + +**3d. Fix Complaint.objects.create() — add all missing fields:** +- `activated_at=activated_date` +- `form_sent_at=form_sent_date` +- `forwarded_to_dept_at=sent_date` +- `reminder_sent_at=first_reminder` +- `second_reminder_sent_at=second_reminder` +- `escalated_at=escalated_date` +- `closed_at=closed_date` +- `resolved_at=resolved_date` +- `satisfaction=normalize_satisfaction(satisfaction_val)` +- `resolution_outcome=normalize_rightful_side(rightful_side)` +- `complaint_subject=complaint_subject` +- `explanation_delay_reason=delay_reason` +- `delay_reason_closure=closure_delay` +- `action_taken_by_dept=action_taken` +- `action_result=action_result` +- `recommendation_action_plan=recommendation or solutions` + +### 4. Run re-import +```bash +python manage.py shell -c "from apps.complaints.models import Complaint; Complaint.objects.all().delete()" +python manage.py import_all_complaints --hospital-code=HH-N +python manage.py backfill_sent_to_department +``` + +### 5. Verify +- Check complaint counts match +- Check timeline dates populated +- Check satisfaction field populated +- Check resolution_outcome populated +- Check department detail page shows correct data diff --git a/.opencode/plans/fix-department-detail-complaints.md b/.opencode/plans/fix-department-detail-complaints.md new file mode 100644 index 0000000..f235949 --- /dev/null +++ b/.opencode/plans/fix-department-detail-complaints.md @@ -0,0 +1,41 @@ +# Fix: Department Detail Shows No Complaints + Template Field References + +## Root Cause +All 2163 complaints have `sent_to_department=False`. The department detail view queries filter by `sent_to_department=True`, so zero complaints/observations/inquiries appear. The import scripts (`import_all_complaints.py`, `import_historical_complaints.py`) explicitly set `sent_to_department=False`, overwriting the backfill migration from May 16. + +## Changes + +### 1. NEW FILE: `apps/complaints/management/commands/backfill_sent_to_department.py` +Create management command that sets `sent_to_department=True` on: +- `Complaint.objects.filter(department__isnull=False, sent_to_department=False).update(sent_to_department=True, sent_to_department_at=F('created_at'))` +- `Observation.objects.filter(assigned_department__isnull=False, sent_to_department=False).update(sent_to_department=True, sent_to_department_at=F('created_at'))` +- `Inquiry.objects.filter(department__isnull=False, sent_to_department=False).update(sent_to_department=True, sent_to_department_at=F('created_at'))` +- `ComplaintInvolvedDepartment.objects.filter(sent=False, complaint__department__isnull=False).update(sent=True)` + +### 2. EDIT: `apps/complaints/management/commands/import_all_complaints.py` +- Change `sent_to_department=False` → `sent_to_department=True` + +### 3. EDIT: `apps/complaints/management/commands/import_historical_complaints.py` +- Change `sent_to_department=False` → `sent_to_department=True` + +### 4. EDIT: `templates/organizations/orgsection_list.html` +- Remove `point_of_contact` column (Section model no longer has this field) + +### 5. EDIT: `templates/organizations/orgsection_form.html` +- Remove `point_of_contact` form field (lines 136-140) + +### 6. EDIT: `templates/complaints/government_ticket_form.html` +- Line 295: change `sec.name` → `sec.name_en || sec.name` (API returns `name_en`, not `name`) + +### 7. EDIT: `templates/px_sources/source_user_create_complaint.html` +- Line 303: change `sec.name` → `sec.name_en || sec.name` + +### 8. RUN: `python manage.py backfill_sent_to_department` +- Expected: 2163 complaints, 3 observations, 2 inquiries updated + +### 9. VERIFY: `python manage.py check` + tests + +## What was ALREADY fixed in previous session +- `department_detail.html`: `sub_subsections` → `subsections`, removed floor/poc columns, added champion/supervisor/deputy columns +- `orgsection_detail.html`: removed `point_of_contact` from header +- `ui_views.py:2173`: added supervisor/deputy_supervisor to select_related diff --git a/DEPARTMENT_HIERARCHY_HANDOFF.md b/DEPARTMENT_HIERARCHY_HANDOFF.md new file mode 100644 index 0000000..21083fd --- /dev/null +++ b/DEPARTMENT_HIERARCHY_HANDOFF.md @@ -0,0 +1,493 @@ +# Department & Complaint Hierarchy Migration - Full Technical Handoff + +## Overview + +PX360 is a hospital patient experience management system (Django + SQLite). We are migrating from a **3-level legacy hierarchy** (Location → MainSection → SubSection) to a **new hierarchy** (Department → Section → Sub-Section) with proper org structure, role-based contact persons, and department-centric complaint routing. + +The legacy models are **not deleted** — they are renamed with `Legacy*` prefix and `db_table` preserved. The new `Department` model has 7 role-holder FKs (all FK to `Staff`), and complaints/inquiries/observations can be routed to departments with mandatory champion or manager validation. + +--- + +## Current State + +| Metric | Value | +|---|---| +| Total complaints | 2,160 | +| Complaints with `department_id` set | 2,151 (99.6%) | +| Complaints with `legacy_subsection_id` | 2,150 | +| Complaints without any department | 9 | +| Total departments (new) | 66 | +| Total sections (OrgSubSection) | 152 | +| Total sub-sections (OrgSubSubSection) | 17 | +| LegacyHierarchyMapping rows | 88 | + +--- + +## Database Schema + +### New Models (created via migrations 0001-0006) + +#### `organizations_department` (UUID PK, 30 columns) +```python +class Department(models.Model): + # Location fields + hospital = FK(Hospital, CASCADE, related_name="departments") + area = CharField(100) # High-level area + main_section = CharField(200) # Main section name + name = CharField(200) + name_en = CharField(200, blank=True) + name_ar = CharField(200, blank=True) + code = CharField(100, db_index=True) + category = CharField(30, choices=DepartmentCategory, db_index=True) + parent = FK(self, SET_NULL, null=True) + location_type = CharField(20) # OP, IP, ER, GENERAL + sub_location = CharField(200) + floor = CharField(50) + + # Role holders (all FK to Staff) + manager = FK(accounts.User, SET_NULL, null=True) + champion = FK(Staff, SET_NULL, null=True, related_name="champion_departments") + manager_1st = FK(Staff, SET_NULL, null=True, related_name="dept_manager_1st") + manager_2nd = FK(Staff, SET_NULL, null=True, related_name="dept_manager_2nd") + manager_3rd = FK(Staff, SET_NULL, null=True, related_name="dept_manager_3rd") + deputy_manager = FK(Staff, SET_NULL, null=True, related_name="dept_deputy_manager") + supervisor = FK(Staff, SET_NULL, null=True, related_name="dept_supervisor") + deputy_supervisor = FK(Staff, SET_NULL, null=True, related_name="dept_deputy_supervisor") + + # Contact / legacy + champion_email = EmailField(200, blank=True) + old_name_en = CharField(200, blank=True) # Legacy English name from Excel + old_name_ar = CharField(200, blank=True) # Legacy Arabic name from Excel + phone = CharField(20, blank=True) + email = EmailField(blank=True) + location = CharField(200, blank=True) # Free text (NOT a FK) + status = CharField(20, default="active") + + ROLE_FIELDS = [ + ("champion", "Champion"), + ("manager_1st", "1st Manager"), + ("manager_2nd", "2nd Manager"), + ("manager_3rd", "3rd Manager"), + ("deputy_manager", "Deputy Manager"), + ("supervisor", "Supervisor"), + ("deputy_supervisor", "Deputy Supervisor"), + ] + + def get_role_holders(self): + """Returns list of dicts: {staff, staff_id, name, email, role_field, role_label}""" + + def is_valid_contact_person(self, staff_id): + """Validates a staff_id against all 7 role FKs. Returns holder dict or None.""" +``` + +#### `organizations_orgsubsection` (UUID PK, 152 rows) +```python +class OrgSubSection(models.Model): + department = FK(Department, CASCADE, related_name="org_subsections") + name_en = CharField(200) + name_ar = CharField(200, blank=True) + code = CharField(100, blank=True) + location_type = CharField(20) + sub_location = CharField(200) + floor = CharField(50) + point_of_contact = FK(Staff, SET_NULL, null=True) + old_name_en = CharField(200, blank=True) + old_name_ar = CharField(200, blank=True) + status = CharField(20, default="active") + + class Meta: + unique_together = [("department", "code")] +``` + +#### `organizations_orgsubsubsection` (UUID PK, 17 rows) +```python +class OrgSubSubSection(models.Model): + subsection = FK(OrgSubSection, CASCADE, related_name="sub_subsections") + name_en = CharField(200) + name_ar = CharField(200, blank=True) + code = CharField(100, blank=True) + status = CharField(20, default="active") + + class Meta: + unique_together = [("subsection", "code")] +``` + +#### `organizations_legacyhierarchymapping` (UUID PK, 88 rows) +```python +class LegacyHierarchyMapping(models.Model): + old_location_ar = CharField(200, db_index=True) + old_main_section_ar = CharField(200, db_index=True) + old_subsection_ar = CharField(200, db_index=True) + old_location_en = CharField(200, blank=True) + old_main_section_en = CharField(200, blank=True) + old_subsection_en = CharField(200, blank=True) + main_section = FK(Department, SET_NULL, null=True) # Maps to Department + subsection = FK(OrgSubSection, SET_NULL, null=True) + sub_subsection = FK(OrgSubSubSection, SET_NULL, null=True) + + class Meta: + unique_together = [("old_location_ar", "old_main_section_ar", "old_subsection_ar")] +``` + +### Legacy Models (renamed, db_table preserved, backward-compat aliases) + +```python +class LegacyLocation(models.Model): # db_table = "organizations_location" + id = models.IntegerField(primary_key=True) + name_ar, name_en + ACTIVE_IDS = [48, 49, 82, 110] + +class LegacyMainSection(models.Model): # db_table = "organizations_mainsection" + id = models.IntegerField(primary_key=True) + name_ar, name_en + +class LegacySubSection(models.Model): # db_table = "organizations_subsection" + internal_id = models.IntegerField(primary_key=True) + name_ar, name_en + location = FK(LegacyLocation) + main_section = FK(LegacyMainSection) +``` + +### Complaint Model Hierarchy Fields + +```python +class Complaint(models.Model): + # NEW hierarchy + department = FK(Department, SET_NULL, null=True, related_name="complaints") + section = FK(OrgSubSection, PROTECT, null=True, related_name="complaints") + sub_subsection = FK(OrgSubSubSection, PROTECT, null=True, related_name="complaints") + + # LEGACY FKs (kept for backward compatibility) + legacy_location = FK(LegacyLocation, PROTECT, null=True, related_name="complaints") + legacy_main_section = FK(LegacyMainSection, PROTECT, null=True, related_name="complaints") + legacy_subsection = FK(LegacySubSection, PROTECT, null=True, related_name="complaints") + + # Raw text (for mapping/audit) + old_location_raw = CharField(200, blank=True, db_index=True) + old_main_section_raw = CharField(200, blank=True, db_index=True) + old_subsection_raw = CharField(200, blank=True, db_index=True) +``` + +Inquiry and Observation models have the same pattern of new + legacy + raw fields. + +--- + +## URL Structure + +### New hierarchy dropdown APIs (in `apps/organizations/urls.py`) +``` +/dropdowns/departments-by-category/?category= → api_departments_by_category +/dropdowns/sections// → api_sections_by_department +/dropdowns/sub-subsections// → api_sub_subsections_by_section +``` + +### Role management +``` +/department-contacts// → api_department_contacts (GET, returns role holders JSON) +/set-role/ → set_department_role (POST, sets/clears a role on dept) +``` + +### Complaint send/escalate (AJAX endpoints) +``` +/complaints//send-to/ → complaint_send_to (POST) +/complaints//escalate/ → complaint_escalate (POST) +/complaints//send-to-department/ → complaint_send_to (POST, same endpoint) +/inquiries//escalate/ → inquiry_escalate (POST) +/inquiries//send-to/ → inquiry_send_to (POST) +/observations//escalate/ → observation_escalate (POST) +/observations//send-to/ → observation_send_to (POST) +``` + +### Legacy dropdown APIs (still active, used by internal forms) +``` +/dropdowns/locations/ → api_locations +/ajax/main-sections/?location= → ajax_main_sections +/ajax/subsections/?location=&main_section= → ajax_subsections +``` + +--- + +## Key Code Files + +### Models +| File | Content | +|---|---| +| `apps/organizations/models.py:151-279` | Department model (30 fields, ROLE_FIELDS, get_role_holders, is_valid_contact_person) | +| `apps/organizations/models.py:697-726` | OrgSubSection model | +| `apps/organizations/models.py:729-747` | OrgSubSubSection model | +| `apps/organizations/models.py:750-779` | LegacyHierarchyMapping model | +| `apps/organizations/models.py:651-694` | LegacyLocation, LegacyMainSection, LegacySubSection | +| `apps/complaints/models.py:260-378` | Complaint model (department, section, sub_subsection, legacy_*_id, old_*_raw) | + +### Views (UI + API) +| File | Lines | Content | +|---|---|---| +| `apps/organizations/ui_views.py:1876-2187` | `department_detail` — tabbed view with roles, complaints, inquiries, etc. | +| `apps/organizations/ui_views.py:3503-3545` | `set_department_role` — POST to set/clear one of 7 role FKs | +| `apps/organizations/views.py:838-869` | 4 new dropdown/contacts API endpoints | +| `apps/complaints/ui_views.py:294-413` | `complaint_list` — filters by `department_id` (line 398) | +| `apps/complaints/ui_views.py:954-1140` | `complaint_send_to` — validates champion/manager, contact person | +| `apps/complaints/ui_views.py:1584-1682` | `complaint_escalate` — manual escalation to specific staff | +| `apps/complaints/ui_views.py:3128-3195` | `inquiry_escalate` | +| `apps/complaints/ui_views.py:3200-3348` | `inquiry_send_to` | +| `apps/complaints/ui_views.py:6243-6296` | `government_ticket_list` — filters by `legacy_main_section_id` | +| `apps/complaints/ui_views.py:6300-6315` | `government_ticket_detail` | +| `apps/complaints/ui_views.py:6628-6659` | `government_ticket_export` | +| `apps/observations/views.py:1160-1236` | `observation_escalate` | +| `apps/observations/views.py:1241-1401` | `observation_send_to` | +| `apps/observations/views.py:1032-1155` | `observation_send_to_department` | + +### Services +| File | Content | +|---|---| +| `apps/complaints/services/complaint_service.py` | `ComplaintService.send_to_department` | +| `apps/complaints/tasks.py:672-679` | `escalate_complaint_auto` — DISABLED (no-op, returns immediately) | +| `apps/complaints/tasks.py:682-867` | `_escalate_complaint_auto_original` — preserved but not dispatched | +| `apps/complaints/tasks.py:870-958` | `escalate_after_reminder` — effectively disabled | +| `apps/complaints/tasks.py:358-390` | `check_overdue_complaints` — still runs, calls disabled auto-escalate | +| `apps/observations/services.py` | `ObservationService.create_observation` — accepts section, sub_subsection | + +### Forms (using legacy hierarchy — NOT yet migrated) +| File | Form Class | Fields | Lines | +|---|---|---|---| +| `apps/complaints/forms.py:58` | `PublicComplaintForm` | location, main_section, subsection (required=True) | 58-378 | +| `apps/complaints/forms.py:379` | `ComplaintForm` | location, main_section, subsection | 379-611 | +| `apps/complaints/forms.py:612` | `InquiryForm` | location, main_section, subsection (required=False) | 612-1220 | +| `apps/complaints/forms.py:1221` | `GovernmentTicketForm` | legacy_location, legacy_main_section, legacy_subsection | 1221-end | + +All 4 forms use `LegacyLocation.active_locations()`, `LegacyMainSection`, `LegacySubSection` with server-side cascading in `__init__`. + +### Templates — Migrated to New Hierarchy +| Template | What Changed | +|---|---| +| `templates/complaints/public_complaint_form.html` | Category → Department → Section → Sub-Section cascade (JS via new APIs) | +| `templates/core/public_submit.html` | 4 forms: complaint, observation, inquiry, appreciation — all switched to new hierarchy | +| `templates/components/send_to_modal.html` | Shared modal with contact person dropdown (calls `api_department_contacts`) | +| `templates/organizations/department_detail.html` | Roles tab with `#roleModal`, all 7 role fields | + +### Templates — Still Using Old Hierarchy +| Template | Fields Displayed | +|---|---| +| `templates/complaints/complaint_form.html` | location, main_section, subsection (via ajax_* endpoints) | +| `templates/complaints/inquiry_form.html` | location, main_section, subsection | +| `templates/complaints/government_ticket_form.html` | location, main_section, subsection | +| `templates/complaints/government_ticket_list.html:166` | `ticket.legacy_main_section.name_en` | +| `templates/complaints/government_ticket_detail.html:128-136` | legacy_location, legacy_main_section, legacy_subsection | +| `templates/complaints/complaint_detail.html` | Displays legacy_* fields | +| `templates/complaints/complaint_pdf.html` | Displays legacy_* fields | + +### Dashboard/Analytics Services — Still Using Old Hierarchy +| File | Lines | Issue | +|---|---|---| +| `apps/dashboard/services/complaint_monthly_service.py:54` | `select_related(..., "location", "main_section", ...)` | Uses `location` FK | +| `apps/dashboard/services/complaint_quarterly_service.py:238-255` | `_compute_location_breakdown()` | IP/OP/ER based on `location__name_en` string matching | +| `apps/dashboard/services/complaint_quarterly_service.py:593-626` | `get_chart_data()` | `c.location.name_en` | +| `apps/dashboard/services/complaint_monthly_export.py:204` | Export row | `c.location.name` | +| `apps/analytics/services/kpi_service.py:948-978` | `_create_location_breakdowns()` | Queries both `location` and `main_section` FKs | +| `apps/analytics/services/kpi_service.py:1540` | AI analysis | `select_related(..., "location", "main_section", ...)` | +| `apps/analytics/services/kpi_service.py:1621-1624` | Location counts | `c.location.name_en` | + +### Management Commands +| File | Content | +|---|---| +| `apps/organizations/management/commands/import_departments_excel.py` (286 lines) | Excel import: matches by `hospital+name_en`, creates Dept/OrgSubSection/OrgSubSubSection/LegacyHierarchyMapping. Dry-run support. | +| `apps/organizations/management/commands/backfill_department_hierarchy.py` (284 lines) | 5-level cascade matching: manual override → LegacyHierarchyMapping → exact name → normalized → contains. 136-entry MANUAL_MAP dict. | + +--- + +## What's Been Completed + +1. **Schema reconciliation**: DB tables/columns created via raw SQL (all old Django migration records deleted, all new migration files faked-applied). 3 new tables, 100+ new columns. + +2. **Legacy FK backfill on complaints**: `legacy_location_id`, `legacy_main_section_id`, `legacy_subsection_id` populated from old `location_id`, `main_section_id`, `subsection_id` for all 2,160 complaints. Raw text values (`old_*_raw`) also populated. + +3. **Excel import**: 66 departments, 152 sections, 17 sub-sections, 88 legacy hierarchy mappings created. + +4. **Backfill command**: 2,151/2,160 complaints now have `department_id` set (99.6%). + +5. **Patient-facing complaint form**: Switched from Location→MainSection→SubSection to Category→Department→Section→SubSection. + +6. **Public submit forms (4 forms)**: All switched to new hierarchy cascade. + +7. **Observation service**: Updated to accept section/sub_subsection params. + +8. **Send-to-department**: Validates champion or manager_1st exists, validates contact person against 7 role FKs. + +9. **Contact person selection**: When sending to department, user picks from role holders. Champion pre-selected if available. + +10. **Department detail**: Roles tab with single reusable modal for all 7 roles. + +11. **Manual escalation**: Implemented for complaint, inquiry, and observation. Auto-escalation disabled (code preserved). + +12. **Dropdown APIs**: 3 new endpoints for department→section→subsection cascade, contacts API. + +--- + +## What Remains (Not Yet Migrated) + +### Critical +1. **Government ticket list view** (`ui_views.py:6265`): Filters by `legacy_main_section_id`. Should filter by `department`. +2. **Dashboard quarterly IP/OP/ER** (`complaint_quarterly_service.py:238-255`): Uses `location__name_en` string matching. Should derive from `department.location_type`. +3. **Dashboard monthly export** (`complaint_monthly_export.py:204`): Uses `c.location.name`. Should use department. +4. **KPI service** (`kpi_service.py:948-978`): Queries both `location` and `main_section` FKs. + +### Medium +5. **Internal staff forms**: `complaint_form.html`, `inquiry_form.html`, `government_ticket_form.html` — still use old Location→MainSection→SubSection cascade with `ajax_main_sections`/`ajax_subsections` endpoints. +6. **31 departments without category**: Need category values assigned from the Excel data. + +### Low +7. **Detail templates**: `complaint_detail.html`, `complaint_pdf.html`, `inquiry_detail.html`, `government_ticket_detail.html` — still display `legacy_*` fields. +8. **Stop writing legacy FKs**: New complaints could stop writing `legacy_*` FKs (or auto-populate from `department`). + +--- + +## Key Design Decisions + +- **Contact person = one of 7 department role holders** (champion, manager_1st-3rd, deputy_manager, supervisor, deputy_supervisor). Champion pre-selected if available. +- **Department cannot receive complaint without champion OR manager_1st**. +- **Auto-escalation disabled** but code preserved for future use. +- **Legacy models kept** with backward-compat aliases and preserved `db_table`. +- **Old API endpoints kept functional** for internal staff forms (gradual migration). +- **Import matches by `hospital+name_en`** to preserve existing Department IDs (critical for complaint FK integrity). +- **Hardcoded `MANUAL_MAP`** dict maps 136 legacy subsection PKs to department name strings for unmappable cases. +- **DB schema applied via raw SQL**, not Django migrations (production DB reconciliation). + +--- + +## API Endpoints (New Hierarchy) + +``` +GET /organizations/dropdowns/departments-by-category/?category=medical + → Returns: [{"id": "uuid", "name_en": "Internal Medicine", ...}, ...] + +GET /organizations/dropdowns/sections// + → Returns: [{"id": "uuid", "name_en": "Cardiology", ...}, ...] + +GET /organizations/dropdowns/sub-subsections// + → Returns: [{"id": "uuid", "name_en": "ECG Lab", ...}, ...] + +GET /organizations/department-contacts// + → Returns: [{"staff_id": "uuid", "name": "Dr. X", "email": "...", "role_field": "champion", "role_label": "Champion"}, ...] + +POST /organizations/set-role/ + Body: {department_id, role_field, staff_id (optional)} + → Sets/clears a role FK on Department +``` + +--- + +## Migration Status + +``` +organizations + [X] 0001_initial + [X] 0002_rename_respondent_to_champion + [X] 0003_alter_department_champion + [X] 0004_legacylocation_legacymainsection_and_more + [X] 0005_alter_legacylocation_table_and_more + [X] 0006_alter_department_code_alter_orgsubsection_code_and_more +``` + +All migrations faked. DB schema in sync. Run with `--skip-checks` flag. + +--- + +## 136-Entry Manual Mapping (MANUAL_MAP in backfill command) + +The backfill command contains a hardcoded dict mapping 136 `LegacySubSection.internal_id` values to `Department.name_en` strings. These are legacy subsections that could not be automatically mapped via `LegacyHierarchyMapping` or name matching. Key distributions: + +- Internal Medicine: ~14 entries +- Surgeries: ~11 entries +- Outpatient Department: ~10 entries +- Nursing Department: ~16 entries +- Emergency Department: ~6 entries +- Inpatient Department: ~7 entries +- Medical Ancillary Services: ~10 entries +- Others (Pediatric, Critical Care, OB/GYN, Pharmacy, IT, Security, Finance, etc.): 1-3 each + +--- + +## File Tree Summary + +``` +apps/ +├── organizations/ +│ ├── models.py # Department, OrgSubSection, OrgSubSubSection, LegacyHierarchyMapping, Legacy* +│ ├── views.py # 4 new dropdown/contacts APIs + legacy ajax_* endpoints +│ ├── ui_views.py # department_detail, set_department_role +│ ├── urls.py # All URL patterns +│ ├── serializers.py # DepartmentSerializer, OrgSubSectionSerializer, OrgSubSubSectionSerializer +│ ├── management/commands/ +│ │ ├── import_departments_excel.py # Excel import (286 lines) +│ │ └── backfill_department_hierarchy.py # Backfill with MANUAL_MAP (284 lines) +│ └── migrations/ +│ ├── 0001_initial.py +│ ├── 0002_rename_respondent_to_champion.py +│ ├── 0003_alter_department_champion.py +│ ├── 0004_legacylocation_legacymainsection_and_more.py +│ ├── 0005_alter_legacylocation_table_and_more.py +│ └── 0006_alter_department_code_alter_orgsubsection_code_and_more.py +├── complaints/ +│ ├── models.py # Complaint (104 columns), with department, section, sub_subsection, legacy_* +│ ├── forms.py # PublicComplaintForm, ComplaintForm, InquiryForm, GovernmentTicketForm (legacy cascade) +│ ├── ui_views.py # complaint_list, complaint_detail, complaint_send_to, complaint_escalate, inquiry_escalate, government_ticket_* +│ ├── services/complaint_service.py # ComplaintService.send_to_department +│ └── tasks.py # Auto-escalation disabled, check_overdue still active +├── observations/ +│ ├── views.py # observation_escalate, observation_send_to +│ └── services.py # create_observation (accepts section, sub_subsection) +├── dashboard/services/ +│ ├── complaint_monthly_service.py # Uses location, main_section FK +│ ├── complaint_quarterly_service.py # IP/OP/ER via location__name_en string matching +│ └── complaint_monthly_export.py # Exports c.location.name +├── analytics/services/ +│ └── kpi_service.py # Queries location + main_section FKs +└── core/views.py # public_inquiry_submit, public_observation_submit + +templates/ +├── complaints/ +│ ├── public_complaint_form.html # MIGRATED: Category→Department→Section→SubSection +│ ├── complaint_form.html # NOT MIGRATED: Still uses old Location→MainSection→SubSection +│ ├── inquiry_form.html # NOT MIGRATED: Still uses old hierarchy +│ ├── government_ticket_form.html # NOT MIGRATED: Still uses old hierarchy +│ ├── government_ticket_list.html # NOT MIGRATED: Displays legacy_main_section.name_en +│ ├── government_ticket_detail.html # NOT MIGRATED: Displays legacy_* fields +│ ├── complaint_detail.html # NOT MIGRATED: Displays legacy_* fields +│ ├── complaint_pdf.html # NOT MIGRATED: Displays legacy_* fields +│ └── inquiry_detail.html # NOT MIGRATED: Displays legacy_* fields +├── core/ +│ └── public_submit.html # MIGRATED: 4 forms switched to new hierarchy +├── components/ +│ └── send_to_modal.html # MIGRATED: Contact person dropdown from role holders +└── organizations/ + └── department_detail.html # MIGRATED: Roles tab with #roleModal + +DB: db.sqlite3 (production copy) +``` + +--- + +## Troubleshooting + +### If `showmigrations` shows pending migrations +```bash +python manage.py showmigrations --skip-checks --list +``` + +### If new tables are missing +Tables must be created manually (not via Django migrations): +```python +# Check if tables exist +from django.db import connection +cursor = connection.cursor() +cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'organizations_%'") +``` + +### If backfill shows 0 candidates +Check that `legacy_subsection_id` is set on complaints. Run the legacy FK backfill first. + +### If import doesn't match existing departments +The import matches by `hospital + name_en`. Check the Excel column headers match what the command expects. + +### System check errors +There are 4 pre-existing system check errors from `accounts.User.groups` vs `auth.User.groups` reverse accessor clash. These are NOT related to the hierarchy migration. diff --git a/PX360/settings.py b/PX360/settings.py index e845f53..163c203 100644 --- a/PX360/settings.py +++ b/PX360/settings.py @@ -40,11 +40,34 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", # Apps "apps.core", "apps.accounts", + "apps.organizations", + "apps.complaints", + "apps.observations", + "apps.feedback", + "apps.appreciation", "apps.dashboard", "apps.social", + "apps.px_sources", + "apps.analytics", + "apps.notifications", + "apps.surveys", + "apps.ai_engine", + "apps.callcenter", + "apps.executive_summary", + "apps.integrations", + "apps.journeys", + "apps.physicians", + "apps.presentations", + "apps.projects", + "apps.px_action_center", + "apps.rca", + "apps.references", + "apps.simulator", + "apps.standards", "django_celery_beat", ] @@ -156,7 +179,6 @@ YOUTUBE_REDIRECT_URI = "http://127.0.0.1:8000/social/callback/YT/" # Ensure you have your client_secrets.json file at this location GMB_CLIENT_SECRETS_FILE = BASE_DIR / "secrets" / "gmb_client_secrets.json" GMB_REDIRECT_URI = "http://127.0.0.1:8000/social/callback/GO/" -m # Data upload settings # Increased limit to support bulk patient imports from HIS diff --git a/apps/accounts/management/commands/create_default_roles.py b/apps/accounts/management/commands/create_default_roles.py index 7c19f94..7cef593 100644 --- a/apps/accounts/management/commands/create_default_roles.py +++ b/apps/accounts/management/commands/create_default_roles.py @@ -34,12 +34,6 @@ class Command(BaseCommand): "description": "Department-level access. Can manage their department.", "level": 60, }, - { - "name": "champion", - "display_name": "Champion", - "description": "Can respond to inquiries and view complaints for their assigned department.", - "level": 55, - }, { "name": "director", "display_name": "Director", diff --git a/apps/accounts/migrations/0003_alter_role_name.py b/apps/accounts/migrations/0003_alter_role_name.py new file mode 100644 index 0000000..ae0df48 --- /dev/null +++ b/apps/accounts/migrations/0003_alter_role_name.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='name', + field=models.CharField(choices=[('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('director', 'Director'), ('px_management', 'PX Management'), ('px_employee', 'PX Employee'), ('staff', 'Staff'), ('viewer', 'Viewer'), ('executive', 'Executive')], max_length=50, unique=True), + ), + ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index f791b06..dd39c08 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -155,8 +155,20 @@ class User(AbstractUser, TimeStampedModel): return self.has_role("Department Manager") def is_champion(self): - """Check if user is Champion""" - return self.has_role("Champion") + """Check if user is Champion (assigned as champion on any department)""" + return hasattr(self, 'staff_profile') and self.staff_profile is not None and self.staff_profile.champion_departments.exists() + + def is_champion_of(self, department): + """Check if user is Champion of a specific department""" + return ( + hasattr(self, 'staff_profile') + and self.staff_profile is not None + and self.staff_profile.champion_departments.filter(pk=department.pk).exists() + ) + + def is_department_respondent(self): + """Alias for is_champion() - Champion is the department respondent role""" + return self.is_champion() def is_px_management(self): """Check if user is PX Management""" @@ -201,9 +213,9 @@ class User(AbstractUser, TimeStampedModel): """Check if user only has basic Staff role with no elevated permissions.""" elevated_roles = [ "PX Admin", "Hospital Admin", "Department Manager", - "Champion", "PX Management", "PX Employee", "Executive", "Director", + "PX Management", "PX Employee", "Executive", "Director", ] - return not any(self.has_role(r) for r in elevated_roles) + return not any(self.has_role(r) for r in elevated_roles) and not self.is_champion() def get_source_user_profile_active(self): """Get active source user profile if exists""" @@ -496,7 +508,6 @@ class Role(models.Model): ("hospital_admin", _("Hospital Admin")), ("department_manager", _("Department Manager")), ("director", _("Director")), - ("champion", _("Champion")), ("px_management", _("PX Management")), ("px_employee", _("PX Employee")), ("staff", _("Staff")), diff --git a/apps/accounts/services.py b/apps/accounts/services.py index 4693b7b..544d38a 100644 --- a/apps/accounts/services.py +++ b/apps/accounts/services.py @@ -315,6 +315,45 @@ class OnboardingService: return User.objects.filter(is_provisional=True, acknowledgement_completed=False).count() +class PasswordResetTokenService: + """Service for one-time admin password reset links.""" + + @staticmethod + def create_reset_token(user): + if user.is_provisional: + raise ValueError("Provisional users should use the onboarding invitation flow.") + + user.invitation_token = secrets.token_urlsafe(32) + user.invitation_expires_at = timezone.now() + timedelta(hours=24) + user.set_unusable_password() + user.save(update_fields=["invitation_token", "invitation_expires_at", "password"]) + return user.invitation_token + + @staticmethod + def validate_reset_token(token): + from django.contrib.auth import get_user_model + + User = get_user_model() + try: + return User.objects.get( + invitation_token=token, + invitation_expires_at__gte=timezone.now(), + is_provisional=False, + ) + except User.DoesNotExist: + return None + + @staticmethod + def clear_reset_token(user): + user.invitation_token = None + user.invitation_expires_at = None + user.save(update_fields=["invitation_token", "invitation_expires_at"]) + + @staticmethod + def build_reset_url(base_url, token): + return f"{base_url.rstrip('/')}/accounts/password/reset/{token}/" + + class EmailService: """Service for sending onboarding-related emails""" @@ -333,12 +372,16 @@ class EmailService: # Build activation URL base_url = getattr(settings, "BASE_URL", "http://localhost:8000") activation_url = f"{base_url}/accounts/onboarding/activate/{user.invitation_token}/" + days_remaining = ( + max((user.invitation_expires_at - timezone.now()).days, 0) if user.invitation_expires_at else 0 + ) # Render email content context = { "user": user, "activation_url": activation_url, "expires_at": user.invitation_expires_at, + "days_remaining": days_remaining, } subject = render_to_string("accounts/onboarding/invitation_subject.txt", context).strip() @@ -391,12 +434,16 @@ class EmailService: # Build activation URL base_url = getattr(settings, "BASE_URL", "http://localhost:8000") activation_url = f"{base_url}/accounts/onboarding/activate/{user.invitation_token}/" + days_remaining = ( + max((user.invitation_expires_at - timezone.now()).days, 0) if user.invitation_expires_at else 0 + ) # Render email content context = { "user": user, "activation_url": activation_url, "expires_at": user.invitation_expires_at, + "days_remaining": days_remaining, } subject = render_to_string("accounts/onboarding/reminder_subject.txt", context).strip() @@ -443,10 +490,15 @@ class EmailService: base_url = getattr(settings, "BASE_URL", "http://localhost:8000") user_detail_url = f"{base_url}/accounts/onboarding/provisional/{user.id}/progress/" + role_display = ", ".join(user.get_role_names()) if hasattr(user, "get_role_names") else "" + completed_at = user.acknowledgement_completed_at or timezone.now() + # Render email content context = { "user": user, "user_detail_url": user_detail_url, + "role_display": role_display, + "completed_at": completed_at, } subject = render_to_string("accounts/onboarding/completion_subject.txt", context).strip() diff --git a/apps/accounts/ui_views.py b/apps/accounts/ui_views.py index 6dd50f6..f3a1ea7 100644 --- a/apps/accounts/ui_views.py +++ b/apps/accounts/ui_views.py @@ -25,7 +25,7 @@ from .models import ( UserProvisionalLog, ) from .permissions import IsPXAdmin, CanManageOnboarding, CanViewOnboarding -from .services import OnboardingService +from .services import OnboardingService, PasswordResetTokenService User = get_user_model() @@ -152,6 +152,28 @@ def password_reset_view(request): return render(request, "accounts/password_reset.html", context) +@never_cache +def password_reset_token_view(request, token): + user = PasswordResetTokenService.validate_reset_token(token) + if user is None: + messages.error(request, "Invalid or expired password reset link. Please request a new one.") + return redirect("accounts:login") + + if not user.is_active: + messages.error(request, "This account is inactive. Please contact your administrator.") + return redirect("accounts:login") + + PasswordResetTokenService.clear_reset_token(user) + + from django.contrib.auth.backends import ModelBackend + + backend = ModelBackend() + user.backend = f"{backend.__module__}.{backend.__class__.__name__}" + login(request, user) + messages.success(request, "Please set a new password for your account.") + return redirect("accounts:password_change") + + class CustomPasswordResetConfirmView(PasswordResetConfirmView): """ Custom password reset confirm view with custom template diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index c1a5a7d..89368b8 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -25,6 +25,7 @@ from .ui_views import ( onboarding_step_content, onboarding_welcome, password_reset_view, + password_reset_token_view, preview_wizard_as_role, provisional_user_list, provisional_user_progress, @@ -58,6 +59,7 @@ urlpatterns = [ path("logout/", logout_view, name="logout"), path("settings/", user_settings, name="settings"), path("password/reset/", password_reset_view, name="password_reset"), + path("password/reset//", password_reset_token_view, name="password_reset_token"), path( "password/reset/confirm///", CustomPasswordResetConfirmView.as_view(), diff --git a/apps/analytics/kpi_service.py b/apps/analytics/kpi_service.py index 8068ff0..7156fc4 100644 --- a/apps/analytics/kpi_service.py +++ b/apps/analytics/kpi_service.py @@ -27,6 +27,11 @@ from .kpi_models import ( KPIReportType, ) + +def _dt(year, month, day=1): + """Create a timezone-aware datetime to avoid naive datetime warnings.""" + return timezone.make_aware(datetime(year, month, day)) + DEPARTMENT_CATEGORY_KEYWORDS = { "medical": [ "medical", @@ -256,25 +261,25 @@ class KPICalculationService: def _calculate_72h_resolution(cls, report: KPIReport): """Calculate 72-Hour Resolution Rate (MOH-2)""" # Get date range for the report period - start_date = datetime(report.year, report.month, 1) + start_date = _dt(report.year, report.month, 1) if report.month == 12: - end_date = datetime(report.year + 1, 1, 1) + end_date = _dt(report.year + 1, 1, 1) else: - end_date = datetime(report.year, report.month + 1, 1) + end_date = _dt(report.year, report.month + 1, 1) # Get all months data for YTD (year to date) - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) # Calculate for each month total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) # Get complaints for this month complaints = Complaint.objects.filter( @@ -340,22 +345,22 @@ class KPICalculationService: def _calculate_patient_experience(cls, report: KPIReport): """Calculate Patient Experience Score (MOH-1)""" # Get date range - year_start = datetime(report.year, 1, 1) - start_date = datetime(report.year, report.month, 1) + year_start = _dt(report.year, 1, 1) + start_date = _dt(report.year, report.month, 1) if report.month == 12: - end_date = datetime(report.year + 1, 1, 1) + end_date = _dt(report.year + 1, 1, 1) else: - end_date = datetime(report.year, report.month + 1, 1) + end_date = _dt(report.year, report.month + 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) # Get completed surveys for patient experience surveys = SurveyInstance.objects.filter( @@ -406,17 +411,17 @@ class KPICalculationService: - Denominator: complaints with satisfaction in (satisfied, neutral, dissatisfied) - Numerator: complaints with satisfaction = 'satisfied' """ - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) complaints = Complaint.objects.filter( hospital=report.hospital, @@ -457,7 +462,7 @@ class KPICalculationService: hospital=report.hospital, created_at__gte=year_start, created_at__lt=( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ), complaint_type="complaint", ) @@ -468,21 +473,21 @@ class KPICalculationService: @classmethod def _calculate_n_pad_001(cls, report: KPIReport): """Calculate N-PAD-001 Resolution Rate""" - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) if report.month == 12: - year_end = datetime(report.year + 1, 1, 1) + year_end = _dt(report.year + 1, 1, 1) else: - year_end = datetime(report.year, report.month + 1, 1) + year_end = _dt(report.year, report.month + 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) complaints = Complaint.objects.filter( hospital=report.hospital, @@ -527,17 +532,17 @@ class KPICalculationService: @classmethod def _calculate_response_rate(cls, report: KPIReport): """Calculate Department Response Rate (48h)""" - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) complaints = Complaint.objects.filter( hospital=report.hospital, @@ -587,7 +592,7 @@ class KPICalculationService: hospital=report.hospital, created_at__gte=year_start, created_at__lt=( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ), complaint_type="complaint", ) @@ -598,17 +603,17 @@ class KPICalculationService: @classmethod def _calculate_activation_2h(cls, report: KPIReport): """Calculate Complaint Activation Within 2 Hours""" - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) # Get complaints with assigned_to (activated) complaints = Complaint.objects.filter( @@ -655,7 +660,7 @@ class KPICalculationService: hospital=report.hospital, created_at__gte=year_start, created_at__lt=( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ), complaint_type="complaint", ) @@ -668,17 +673,17 @@ class KPICalculationService: """Calculate Unactivated Filled Complaints Rate""" from apps.dashboard.models import ComplaintRequest - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) complaints = Complaint.objects.filter( hospital=report.hospital, @@ -725,7 +730,7 @@ class KPICalculationService: hospital=report.hospital, created_at__gte=year_start, created_at__lt=( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ), complaint_type="complaint", ) @@ -736,21 +741,21 @@ class KPICalculationService: @classmethod def _calculate_moh_24h(cls, report: KPIReport): """Calculate 24-Hour MOH Complaint Resolution Rate""" - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) if report.month == 12: - end_date = datetime(report.year + 1, 1, 1) + end_date = _dt(report.year + 1, 1, 1) else: - end_date = datetime(report.year, report.month + 1, 1) + end_date = _dt(report.year, report.month + 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) complaints = Complaint.objects.filter( hospital=report.hospital, @@ -802,21 +807,21 @@ class KPICalculationService: @classmethod def _calculate_chi_48h(cls, report: KPIReport): """Calculate 48-Hour CHI Complaint Resolution Rate""" - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) if report.month == 12: - end_date = datetime(report.year + 1, 1, 1) + end_date = _dt(report.year + 1, 1, 1) else: - end_date = datetime(report.year, report.month + 1, 1) + end_date = _dt(report.year, report.month + 1, 1) total_numerator = 0 total_denominator = 0 for month in range(1, 13): - month_start = datetime(report.year, month, 1) + month_start = _dt(report.year, month, 1) if month == 12: - month_end = datetime(report.year + 1, 1, 1) + month_end = _dt(report.year + 1, 1, 1) else: - month_end = datetime(report.year, month + 1, 1) + month_end = _dt(report.year, month + 1, 1) complaints = Complaint.objects.filter( hospital=report.hospital, @@ -918,7 +923,9 @@ class KPICalculationService: ).count() avg_days = None - resolved_complaints = dept_complaints.filter(resolved_at__isnull=False) + resolved_complaints = dept_complaints.filter( + resolved_at__isnull=False, activated_at__isnull=False + ) if resolved_complaints.exists(): total_days = 0 for c in resolved_complaints: @@ -963,8 +970,7 @@ class KPICalculationService: for loc_type, keywords in location_categories.items(): q_objects = Q() for keyword in keywords: - q_objects |= Q(location__name_en__icontains=keyword) - q_objects |= Q(main_section__name_en__icontains=keyword) + q_objects |= Q(department__location_type__icontains=keyword) loc_complaints = complaints.filter(q_objects).distinct() count = loc_complaints.count() @@ -1058,9 +1064,9 @@ class KPICalculationService: ) # Calculate resolution time buckets - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) year_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) all_complaints = Complaint.objects.filter( @@ -1350,9 +1356,9 @@ Be specific and use actual numbers from the data.""" } ) - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) year_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) from django.db.models import Avg as AvgAgg, Count @@ -1526,9 +1532,9 @@ Focus on identifying drivers of satisfaction and dissatisfaction.""" resolved_complaints = report.total_numerator # Get date range - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) year_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) # Query complaints for detailed analysis @@ -1537,7 +1543,7 @@ Focus on identifying drivers of satisfaction and dissatisfaction.""" created_at__gte=year_start, created_at__lt=year_end, complaint_type="complaint", - ).select_related("department", "location", "main_section", "source") + ).select_related("department", "source") # Count by status closed_count = complaints.filter(status=ComplaintStatus.CLOSED).count() @@ -1620,7 +1626,7 @@ Focus on identifying drivers of satisfaction and dissatisfaction.""" # Location breakdown location_counts = {} for c in complaints: - loc = c.location.name_en if c.location else "Unknown" + loc = c.department.location_type if c.department and c.department.location_type else "Unknown" location_counts[loc] = location_counts.get(loc, 0) + 1 # Main department breakdown @@ -1799,9 +1805,9 @@ Be specific with numbers and focus on actionable insights.""" } ) - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) year_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) total_complaints_received = Complaint.objects.filter( @@ -2020,9 +2026,9 @@ Focus on identifying why patients are dissatisfied and provide practical solutio except KPIReport.DoesNotExist: pass - year_start = datetime(report.year, 1, 1) + year_start = _dt(report.year, 1, 1) year_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) all_complaints = ( @@ -2215,9 +2221,9 @@ Focus on identifying which departments need improvement and practical follow-up total_complaints = report.total_denominator activated_within_2h = report.total_numerator - month_start = datetime(report.year, report.month, 1) + month_start = _dt(report.year, report.month, 1) month_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) complaints = Complaint.objects.filter( @@ -2388,9 +2394,9 @@ Focus on identifying why activations are delayed and practical solutions.""" total_complaints = report.total_denominator unactivated_filled = report.total_numerator - month_start = datetime(report.year, report.month, 1) + month_start = _dt(report.year, report.month, 1) month_end = ( - datetime(report.year + 1, 1, 1) if report.month == 12 else datetime(report.year, report.month + 1, 1) + _dt(report.year + 1, 1, 1) if report.month == 12 else _dt(report.year, report.month + 1, 1) ) all_requests = ComplaintRequest.objects.filter( diff --git a/apps/analytics/management/commands/seed_kpi_data.py b/apps/analytics/management/commands/seed_kpi_data.py index 783c27f..8298631 100644 --- a/apps/analytics/management/commands/seed_kpi_data.py +++ b/apps/analytics/management/commands/seed_kpi_data.py @@ -26,7 +26,7 @@ from apps.analytics.kpi_models import KPIReport, KPIReportType from apps.analytics.kpi_service import KPICalculationService from apps.complaints.models import Complaint, ComplaintStatus, ComplaintUpdate from apps.dashboard.models import ComplaintRequest -from apps.organizations.models import Department, Hospital, Location, MainSection +from apps.organizations.models import Department, Hospital, LegacyLocation, LegacyMainSection from apps.organizations.models import Patient from apps.px_sources.models import PXSource from apps.surveys.models import ( @@ -217,19 +217,19 @@ class Command(BaseCommand): defaults={"name": d["name"], "status": "active"}, ) for loc in LOCATIONS: - Location.objects.get_or_create( + LegacyLocation.objects.get_or_create( id=loc["id"], defaults={"name_en": loc["name_en"], "name_ar": loc["name_ar"]}, ) for ms in MAIN_SECTIONS: - MainSection.objects.get_or_create( + LegacyMainSection.objects.get_or_create( id=ms["id"], defaults={"name_en": ms["name_en"], "name_ar": ms["name_ar"]}, ) self.stdout.write( f" Depts: {Department.objects.filter(hospital=hospital).count()}, " - f"Locs: {Location.objects.filter(id__in=[l['id'] for l in LOCATIONS]).count()}" + f"Locs: {LegacyLocation.objects.filter(id__in=[l['id'] for l in LOCATIONS]).count()}" ) return hospital @@ -335,8 +335,8 @@ class Command(BaseCommand): complaints = [] source_list = list(sources.values()) departments = list(Department.objects.filter(hospital=hospital)) - locations = list(Location.objects.filter(id__in=[l["id"] for l in LOCATIONS])) - main_sections = list(MainSection.objects.filter(id__in=[m["id"] for m in MAIN_SECTIONS])) + locations = list(LegacyLocation.objects.filter(id__in=[l["id"] for l in LOCATIONS])) + main_sections = list(LegacyMainSection.objects.filter(id__in=[m["id"] for m in MAIN_SECTIONS])) for i in range(self.complaints_per_month): created_at = self._random_dt(month) @@ -378,8 +378,8 @@ class Command(BaseCommand): complaint = Complaint( hospital=hospital, department=department, - location=location, - main_section=main_section, + legacy_location=location, + legacy_main_section=main_section, source=source, title=f"{random.choice(COMPLAINT_TITLES)} ({month}/{i + 1})", description=random.choice(COMPLAINT_DESCRIPTIONS), diff --git a/apps/analytics/services/analytics_service.py b/apps/analytics/services/analytics_service.py index cf6e19f..40c4856 100644 --- a/apps/analytics/services/analytics_service.py +++ b/apps/analytics/services/analytics_service.py @@ -125,11 +125,23 @@ class UnifiedAnalyticsService: # Check if queryset has hospital/department fields if hasattr(queryset.model, "hospital"): if user.is_px_admin(): - pass # See all + pass elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) + elif user.is_executive() and user.hospital: + queryset = queryset.filter(hospital=user.hospital) elif user.is_department_manager() and user.department: queryset = queryset.filter(department=user.department) + elif user.is_px_management() and user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_employee() and user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_director(): + directed_depts = user.get_directed_departments() + if directed_depts.exists(): + queryset = queryset.filter(department__in=directed_depts) + else: + queryset = queryset.none() else: queryset = queryset.none() return queryset diff --git a/apps/analytics/ui_views.py b/apps/analytics/ui_views.py index 9b7045f..ab9f7a4 100644 --- a/apps/analytics/ui_views.py +++ b/apps/analytics/ui_views.py @@ -173,6 +173,69 @@ def analytics_dashboard(request): # Status breakdown status_breakdown = status_counts_qs.order_by("-count") + # Complaints by department (top 10) + complaints_by_dept = ( + complaints_queryset.filter(department__isnull=False) + .values("department__name") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + + # ============ INQUIRY ANALYTICS ============ + inquiry_status_counts = inquiry_queryset.values("status").annotate(count=Count("id")).order_by("-count") + inquiry_priority_counts = inquiry_queryset.exclude(priority="").values("priority").annotate(count=Count("id")).order_by("-count") + inquiries_by_dept = ( + inquiry_queryset.filter(department__isnull=False) + .values("department__name") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + inquiries_by_category = inquiry_queryset.exclude(category="").values("category").annotate(count=Count("id")).order_by("-count")[:10] + + # ============ OBSERVATION ANALYTICS ============ + observation_status_counts = observation_queryset.values("status").annotate(count=Count("id")).order_by("-count") + observation_severity_counts = observation_queryset.values("severity").annotate(count=Count("id")).order_by("-count") + observations_by_dept = ( + observation_queryset.filter(assigned_department__isnull=False) + .values("assigned_department__name") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + observations_by_category = ( + observation_queryset.filter(category__isnull=False) + .values("category__name_en") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + + # ============ SUGGESTION ANALYTICS (Feedback type=suggestion) ============ + suggestion_queryset = feedback_queryset.filter(feedback_type="suggestion") + suggestion_status_counts = suggestion_queryset.values("status").annotate(count=Count("id")).order_by("-count") + suggestion_sentiment_counts = suggestion_queryset.exclude(sentiment="").values("sentiment").annotate(count=Count("id")).order_by("-count") + suggestions_by_dept = ( + suggestion_queryset.filter(department__isnull=False) + .values("department__name") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + suggestions_by_category = suggestion_queryset.exclude(category="").values("category").annotate(count=Count("id")).order_by("-count")[:10] + + # ============ APPRECIATION ANALYTICS ============ + appreciation_status_counts = appreciation_queryset.values("status").annotate(count=Count("id")).order_by("-count") + appreciations_by_dept = ( + appreciation_queryset.filter(department__isnull=False) + .values("department__name") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + appreciations_by_category = ( + appreciation_queryset.filter(category__isnull=False) + .values("category__name_en") + .annotate(count=Count("id")) + .order_by("-count")[:10] + ) + appreciations_by_visibility = appreciation_queryset.values("visibility").annotate(count=Count("id")).order_by("-count") + # ============ ACTIONS KPIs ============ action_status_counts = actions_queryset.values("status").annotate(count=Count("id")) action_status_map = {item["status"]: item["count"] for item in action_status_counts} @@ -678,6 +741,23 @@ def analytics_dashboard(request): "top_categories": serialize_queryset_values(top_categories), "severity_breakdown": serialize_queryset_values(severity_breakdown), "status_breakdown": serialize_queryset_values(status_breakdown), + "complaints_by_dept": serialize_queryset_values(complaints_by_dept), + "inquiry_status_counts": serialize_queryset_values(inquiry_status_counts), + "inquiry_priority_counts": serialize_queryset_values(inquiry_priority_counts), + "inquiries_by_dept": serialize_queryset_values(inquiries_by_dept), + "inquiries_by_category": serialize_queryset_values(inquiries_by_category), + "observation_status_counts": serialize_queryset_values(observation_status_counts), + "observation_severity_counts": serialize_queryset_values(observation_severity_counts), + "observations_by_dept": serialize_queryset_values(observations_by_dept), + "observations_by_category": serialize_queryset_values(observations_by_category), + "suggestion_status_counts": serialize_queryset_values(suggestion_status_counts), + "suggestion_sentiment_counts": serialize_queryset_values(suggestion_sentiment_counts), + "suggestions_by_dept": serialize_queryset_values(suggestions_by_dept), + "suggestions_by_category": serialize_queryset_values(suggestions_by_category), + "appreciation_status_counts": serialize_queryset_values(appreciation_status_counts), + "appreciations_by_dept": serialize_queryset_values(appreciations_by_dept), + "appreciations_by_category": serialize_queryset_values(appreciations_by_category), + "appreciations_by_visibility": serialize_queryset_values(appreciations_by_visibility), "complaint_trend": serialize_queryset_values(complaint_trend), "complaints_by_quarter": serialize_queryset_values(complaints_by_quarter), "inquiries_by_quarter": serialize_queryset_values(inquiries_by_quarter), @@ -909,8 +989,10 @@ def command_center(request): hospital_id = filters["hospital"] if filters["hospital"] else None department_id = filters["department"] if filters["department"] else None - if not hospital_id and user.is_px_admin(): + if not hospital_id and (user.is_px_admin() or user.is_executive()): tenant = getattr(request, "tenant_hospital", None) + if not tenant: + tenant = getattr(user, "hospital", None) if tenant: hospital_id = str(tenant.id) @@ -1006,8 +1088,10 @@ def command_center_api(request): # Handle department_id (UUID string) department_id = department_id if department_id else None - if not hospital_id and user.is_px_admin(): + if not hospital_id and (user.is_px_admin() or user.is_executive()): tenant = getattr(request, "tenant_hospital", None) + if not tenant: + tenant = getattr(user, "hospital", None) if tenant: hospital_id = str(tenant.id) diff --git a/apps/appreciation/admin.py b/apps/appreciation/admin.py index 557a4e4..518c8c1 100644 --- a/apps/appreciation/admin.py +++ b/apps/appreciation/admin.py @@ -73,6 +73,10 @@ class AppreciationAdmin(admin.ModelAdmin): 'sender', 'hospital', 'department', + 'legacy_location', + 'legacy_main_section', + 'legacy_subsection', + 'section', 'category', ) }), diff --git a/apps/appreciation/migrations/0001_initial.py b/apps/appreciation/migrations/0001_initial.py index b4211ea..b739536 100644 --- a/apps/appreciation/migrations/0001_initial.py +++ b/apps/appreciation/migrations/0001_initial.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -92,11 +92,11 @@ class Migration(migrations.Migration): ('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_%(class)s_set', to=settings.AUTH_USER_MODEL)), ('department', models.ForeignKey(blank=True, help_text='Department context (if applicable)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.department')), ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appreciations', to='organizations.hospital')), - ('location', models.ForeignKey(blank=True, help_text='Location where the appreciation event occurred', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.location')), - ('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.mainsection')), + ('location', models.ForeignKey(blank=True, help_text='Location where the appreciation event occurred', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacylocation')), + ('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacymainsection')), ('recipient_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciation_recipients', to='contenttypes.contenttype')), ('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_appreciations', to=settings.AUTH_USER_MODEL)), - ('subsection', models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.subsection')), + ('subsection', models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacysubsection')), ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='appreciation.appreciationcategory')), ], options={ diff --git a/apps/appreciation/migrations/0002_remove_appreciation_location_and_more.py b/apps/appreciation/migrations/0002_remove_appreciation_location_and_more.py new file mode 100644 index 0000000..4898e73 --- /dev/null +++ b/apps/appreciation/migrations/0002_remove_appreciation_location_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='appreciation', + old_name='location', + new_name='legacy_location', + ), + migrations.RenameField( + model_name='appreciation', + old_name='main_section', + new_name='legacy_main_section', + ), + migrations.RenameField( + model_name='appreciation', + old_name='subsection', + new_name='legacy_subsection', + ), + ] diff --git a/apps/appreciation/migrations/0003_appreciation_legacy_location_and_more.py b/apps/appreciation/migrations/0003_appreciation_legacy_location_and_more.py new file mode 100644 index 0000000..3d0a774 --- /dev/null +++ b/apps/appreciation/migrations/0003_appreciation_legacy_location_and_more.py @@ -0,0 +1,18 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0002_remove_appreciation_location_and_more'), + ('organizations', '0008_rename_orgsubsection_to_section_add_champion'), + ] + + operations = [ + migrations.AddField( + model_name='appreciation', + name='section', + field=models.ForeignKey(blank=True, help_text='Section within department', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations_new', to='organizations.Section'), + ), + ] diff --git a/apps/appreciation/migrations/0004_alter_appreciation_legacy_location_and_more.py b/apps/appreciation/migrations/0004_alter_appreciation_legacy_location_and_more.py new file mode 100644 index 0000000..81979f8 --- /dev/null +++ b/apps/appreciation/migrations/0004_alter_appreciation_legacy_location_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0003_appreciation_legacy_location_and_more'), + ('organizations', '0005_alter_legacylocation_table_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='appreciation', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text='Location where the appreciation event occurred', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='appreciation', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='appreciation', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/appreciation/migrations/0005_remove_sub_subsection.py b/apps/appreciation/migrations/0005_remove_sub_subsection.py new file mode 100644 index 0000000..0894950 --- /dev/null +++ b/apps/appreciation/migrations/0005_remove_sub_subsection.py @@ -0,0 +1,10 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0004_alter_appreciation_legacy_location_and_more'), + ] + + operations = [] diff --git a/apps/appreciation/migrations/0006_appreciation_reference_number_and_more.py b/apps/appreciation/migrations/0006_appreciation_reference_number_and_more.py new file mode 100644 index 0000000..29e8ead --- /dev/null +++ b/apps/appreciation/migrations/0006_appreciation_reference_number_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-06-14 10:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0005_remove_sub_subsection'), + ('organizations', '0014_remove_department_manager_1st'), + ] + + operations = [ + migrations.AddField( + model_name='appreciation', + name='reference_number', + field=models.CharField(blank=True, db_index=True, max_length=40, null=True, unique=True), + ), + migrations.AlterField( + model_name='appreciation', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='appreciation', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='appreciation', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appreciations', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/appreciation/models.py b/apps/appreciation/models.py index d6a7cba..1586847 100644 --- a/apps/appreciation/models.py +++ b/apps/appreciation/models.py @@ -8,7 +8,7 @@ This module implements the appreciation system that: - Integrates with the notification system """ -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models @@ -25,6 +25,15 @@ class AppreciationStatus(models.TextChoices): ACKNOWLEDGED = "acknowledged", "Acknowledged" +VALID_APPRECIATION_TRANSITIONS = { + AppreciationStatus.DRAFT: {AppreciationStatus.ACTIVATED}, + AppreciationStatus.ACTIVATED: {AppreciationStatus.AI_ANALYZED, AppreciationStatus.SENT}, + AppreciationStatus.AI_ANALYZED: {AppreciationStatus.SENT}, + AppreciationStatus.SENT: {AppreciationStatus.ACKNOWLEDGED}, + AppreciationStatus.ACKNOWLEDGED: set(), +} + + class AppreciationVisibility(models.TextChoices): """Appreciation visibility choices""" @@ -128,29 +137,38 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): # Organization context hospital = models.ForeignKey("organizations.Hospital", on_delete=models.CASCADE, related_name="appreciations") - location = models.ForeignKey( - "organizations.Location", + legacy_location = models.ForeignKey( + "organizations.LegacyLocation", on_delete=models.SET_NULL, null=True, blank=True, related_name="appreciations", - help_text="Location where the appreciation event occurred", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - main_section = models.ForeignKey( - "organizations.MainSection", + legacy_main_section = models.ForeignKey( + "organizations.LegacyMainSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="appreciations", - help_text="Main section within the location", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - subsection = models.ForeignKey( - "organizations.SubSection", + legacy_subsection = models.ForeignKey( + "organizations.LegacySubSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="appreciations", - help_text="Specific subsection", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + # New hierarchy (from 4th Version Excel) + section = models.ForeignKey( + "organizations.Section", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="appreciations_new", + help_text="Section within department", ) department = models.ForeignKey( "organizations.Department", @@ -176,6 +194,9 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): max_length=20, choices=AppreciationStatus.choices, default=AppreciationStatus.DRAFT, db_index=True ) + # Reference number (unified format APR-YYYYMM-HOSP-NNNN; internal-only, not publicly trackable) + reference_number = models.CharField(max_length=40, unique=True, blank=True, null=True, db_index=True) + # Anonymous option is_anonymous = models.BooleanField(default=False, help_text="Hide sender identity from recipient") @@ -202,6 +223,8 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): # Metadata metadata = models.JSONField(default=dict, blank=True) + notes = GenericRelation("core.Note") + class Meta: ordering = ["-created_at"] indexes = [ @@ -217,6 +240,36 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): recipient_name = self.get_recipient_name() return f"Appreciation to {recipient_name} ({self.status})" + def save(self, *args, **kwargs): + if not self.reference_number: + from apps.core.reference import generate_reference + + self.reference_number = generate_reference("APR", self.hospital) + super().save(*args, **kwargs) + + def get_owner(self): + """ + Returns the owner of this appreciation. + Cascade: section(champion, supervisor, deputy_supervisor) + -> department(champion, deputy_manager, supervisor, + deputy_supervisor, manager_2nd, manager_3rd). + Returns: Staff instance or None. + """ + if self.section: + for role in ("champion", "supervisor", "deputy_supervisor"): + owner = getattr(self.section, role, None) + if owner: + return owner + if self.department: + dept = self.department + for role in ("champion", "deputy_manager", + "supervisor", "deputy_supervisor", + "manager_2nd", "manager_3rd"): + owner = getattr(dept, role, None) + if owner: + return owner + return None + def get_localized_message(self): from django.utils.translation import get_language @@ -228,7 +281,7 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): """Get recipient's name""" try: return str(self.recipient) - except: + except Exception: return "Unknown" def get_recipient_email(self): @@ -238,7 +291,7 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): return self.recipient.email elif hasattr(self.recipient, "user"): return self.recipient.user.email - except: + except Exception: pass return None @@ -249,13 +302,16 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): return self.recipient.phone elif hasattr(self.recipient, "user"): return self.recipient.user.phone - except: + except Exception: pass return None def activate(self, activated_by=None): from django.utils import timezone + if self.status != AppreciationStatus.DRAFT: + raise ValueError(f"Cannot activate appreciation in '{self.status}' status. Must be in 'draft'.") + self.status = AppreciationStatus.ACTIVATED self.activated_at = timezone.now() self.activated_by = activated_by @@ -264,6 +320,9 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): def mark_ai_analyzed(self, analysis_data): from django.utils import timezone + if self.status != AppreciationStatus.ACTIVATED: + raise ValueError(f"Cannot mark as AI analyzed from '{self.status}' status. Must be in 'activated'.") + self.status = AppreciationStatus.AI_ANALYZED self.ai_analyzed_at = timezone.now() self.ai_analysis = analysis_data @@ -273,6 +332,9 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): """Send appreciation and trigger notification""" from django.utils import timezone + if self.status not in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED): + raise ValueError(f"Cannot send appreciation in '{self.status}' status. Must be 'activated' or 'ai_analyzed'.") + self.status = AppreciationStatus.SENT self.sent_at = timezone.now() self.save(update_fields=["status", "sent_at"]) @@ -283,13 +345,15 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): """Mark appreciation as acknowledged""" from django.utils import timezone + if self.status != AppreciationStatus.SENT: + raise ValueError(f"Cannot acknowledge appreciation in '{self.status}' status. Must be in 'sent'.") + self.status = AppreciationStatus.ACKNOWLEDGED self.acknowledged_at = timezone.now() self.save(update_fields=["status", "acknowledged_at"]) def send_notification(self): - """Send notification to recipient""" - # This will be implemented in signals.py + """Send notification to recipient — handled by post_save signal.""" pass @@ -405,7 +469,7 @@ class UserBadge(UUIDModel, TimeStampedModel): """Get recipient's name""" try: return str(self.recipient) - except: + except Exception: return "Unknown" @@ -464,5 +528,5 @@ class AppreciationStats(UUIDModel, TimeStampedModel): """Get recipient's name""" try: return str(self.recipient) - except: + except Exception: return "Unknown" diff --git a/apps/appreciation/signals.py b/apps/appreciation/signals.py index ef766bf..bc2932b 100644 --- a/apps/appreciation/signals.py +++ b/apps/appreciation/signals.py @@ -6,11 +6,15 @@ This module handles: - Updating statistics when appreciations are created - Checking and awarding badges """ +import logging + from django.db.models import Q from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone +logger = logging.getLogger(__name__) + from apps.appreciation.models import ( Appreciation, AppreciationBadge, @@ -49,8 +53,9 @@ def send_appreciation_notification(appreciation): Uses the notification system to send email/SMS/WhatsApp. Also sends email to department heads. """ + notification_success = False try: - from apps.notifications.services import send_email, send_sms + from apps.notifications.services import send_email, send_sms, get_email_header_html recipient_email = appreciation.get_recipient_email() recipient_phone = appreciation.get_recipient_phone() @@ -61,36 +66,22 @@ def send_appreciation_notification(appreciation): ) html_message = f""" - - - - -
-
-

🌟 Staff Appreciation

-
-
-
- From: {sender_name}
- Hospital: {appreciation.hospital.name}
- {f'Category: {appreciation.category.name_en}
' if appreciation.category else ''} -
- Message:
- {appreciation.message_en} -
- {f'
Personal Note from {sender_name}:
{appreciation.custom_message}
' if appreciation.custom_message else ''} -

Congratulations on this recognition! Your dedication is truly appreciated.

+
+ {get_email_header_html()} +
+

🌟 Staff Appreciation

+
+ From: {sender_name}
+ Hospital: {appreciation.hospital.name}
+ {f'Category: {appreciation.category.name_en}
' if appreciation.category else ''} +
+ Message:
+ {appreciation.message_en}
+ {f'
Personal Note from {sender_name}:
{appreciation.custom_message}
' if appreciation.custom_message else ''} +

Congratulations on this recognition! Your dedication is truly appreciated.

- - +
""" message_en = f"You've received an appreciation from {sender_name}!" @@ -116,8 +107,9 @@ def send_appreciation_notification(appreciation): html_message=html_message, related_object=appreciation, ) + notification_success = True except Exception as e: - print(f"Failed to send appreciation email: {e}") + logger.error(f"Failed to send appreciation email: {e}") if recipient_phone: try: @@ -126,24 +118,27 @@ def send_appreciation_notification(appreciation): message=message_en, related_object=appreciation, ) + notification_success = True except Exception as e: - print(f"Failed to send appreciation SMS: {e}") + logger.error(f"Failed to send appreciation SMS: {e}") _send_department_head_notification(appreciation, sender_name) _send_cc_notifications(appreciation, sender_name, html_message, message_en) + notification_success = True except ImportError as e: - print(f"Notification service not available: {e}") + logger.warning(f"Notification service not available: {e}") except Exception as e: - print(f"Error sending appreciation notification: {e}") + logger.error(f"Error sending appreciation notification: {e}") - appreciation.notification_sent = True - appreciation.notification_sent_at = timezone.now() - appreciation.save(update_fields=['notification_sent', 'notification_sent_at']) + if notification_success: + appreciation.notification_sent = True + appreciation.notification_sent_at = timezone.now() + appreciation.save(update_fields=['notification_sent', 'notification_sent_at']) def _send_department_head_notification(appreciation, sender_name): - from apps.notifications.services import send_email as notify_send_email + from apps.notifications.services import send_email as notify_send_email, get_email_header_html from apps.organizations.models import Staff if not appreciation.department: @@ -168,35 +163,22 @@ def _send_department_head_notification(appreciation, sender_name): recipient_name = appreciation.get_recipient_name() if appreciation.recipient else "N/A" html = f""" - - - - -
-
-

🌟 New Appreciation in Your Department

-
-
-
- Staff Member: {recipient_name}
- Department: {dept.name_en or dept.name}
- From: {sender_name}
- {f'Category: {appreciation.category.name_en}
' if appreciation.category else ''} -
- Message:
- {appreciation.message_en} -
-

A staff member in your department has received an appreciation. Please acknowledge their good work.

+
+ {get_email_header_html()} +
+

🌟 New Appreciation in Your Department

+
+ Staff Member: {recipient_name}
+ Department: {dept.name_en or dept.name}
+ From: {sender_name}
+ {f'Category: {appreciation.category.name_en}
' if appreciation.category else ''} +
+ Message:
+ {appreciation.message_en}
+

A staff member in your department has received an appreciation. Please acknowledge their good work.

- - +
""" plain_message = ( @@ -215,7 +197,7 @@ def _send_department_head_notification(appreciation, sender_name): related_object=appreciation, ) except Exception as e: - print(f"Failed to send department head notification: {e}") + logger.error(f"Failed to send department head notification: {e}") def _send_cc_notifications(appreciation, sender_name, html_message, plain_message): @@ -239,7 +221,7 @@ def _send_cc_notifications(appreciation, sender_name, html_message, plain_messag related_object=appreciation, ) except Exception as e: - print(f"Failed to send CC notification to {cc_email}: {e}") + logger.error(f"Failed to send CC notification to {cc_email}: {e}") def update_appreciation_stats(instance): @@ -248,12 +230,12 @@ def update_appreciation_stats(instance): Creates or updates monthly statistics. """ - # Get current year and month + from django.db.models import F + now = timezone.now() year = now.year month = now.month - # Get or create stats record stats, created = AppreciationStats.objects.get_or_create( recipient_content_type=instance.recipient_content_type, recipient_object_id=instance.recipient_object_id, @@ -269,20 +251,18 @@ def update_appreciation_stats(instance): } ) - # Update received count - stats.received_count += 1 + AppreciationStats.objects.filter(pk=stats.pk).update( + received_count=F('received_count') + 1, + sent_count=F('sent_count') + 1, + ) + stats.refresh_from_db() - # Update category breakdown if instance.category: category_breakdown = stats.category_breakdown or {} category_id_str = str(instance.category.id) category_breakdown[category_id_str] = category_breakdown.get(category_id_str, 0) + 1 - stats.category_breakdown = category_breakdown + AppreciationStats.objects.filter(pk=stats.pk).update(category_breakdown=category_breakdown) - # Save stats - stats.save() - - # Recalculate rankings recalculate_rankings(instance.hospital, year, month, instance.department) @@ -374,6 +354,31 @@ def check_and_award_badges(instance): appreciation_count=count, ) + # Notify the user + try: + from django.contrib.auth import get_user_model + User = get_user_model() + user = User.objects.filter(pk=recipient_object_id).first() + if user and user.email: + from apps.notifications.services import NotificationService, get_email_header_html + NotificationService.send_email( + email=user.email, + subject=f"You earned a badge: {badge.name_en}!", + message=f"Congratulations! You've earned the \"{badge.name_en}\" badge for receiving {count} appreciations.", + html_message=f""" +
+ {get_email_header_html()} +
+

Congratulations!

+

You've earned the {badge.name_en} badge for receiving {count} appreciations.

+

Keep up the great work!

+
+
+""", + ) + except Exception as e: + logger.error(f"Failed to send badge award notification: {e}") + def check_badge_criteria(badge, content_type, object_id, hospital): """ @@ -444,26 +449,80 @@ def get_appreciation_count(content_type, object_id, criteria_type): sent_at__year=now.year, sent_at__month=now.month, ).count() + elif criteria_type == 'diverse_senders': + return Appreciation.objects.filter( + recipient_content_type=content_type, + recipient_object_id=object_id, + status=AppreciationStatus.SENT, + ).values('sender').distinct().count() + elif criteria_type == 'streak_weeks': + return check_appreciation_streak_count(content_type, object_id) return 0 +def check_appreciation_streak_count(content_type, object_id): + """Count consecutive weeks of appreciation ending at current week.""" + from datetime import timedelta + + now = timezone.now() + streak = 0 + + current_week_start = now - timedelta(days=now.weekday()) + current_week_start = current_week_start.replace(hour=0, minute=0, second=0, microsecond=0) + if Appreciation.objects.filter( + recipient_content_type=content_type, + recipient_object_id=object_id, + status=AppreciationStatus.SENT, + sent_at__gte=current_week_start, + sent_at__lte=now, + ).exists(): + streak += 1 + + for i in range(1, 52): + week_start = current_week_start - timedelta(weeks=i) + week_end = current_week_start - timedelta(weeks=i - 1) + if Appreciation.objects.filter( + recipient_content_type=content_type, + recipient_object_id=object_id, + status=AppreciationStatus.SENT, + sent_at__gte=week_start, + sent_at__lt=week_end, + ).exists(): + streak += 1 + else: + break + + return streak + + def check_appreciation_streak(content_type, object_id, required_weeks): """ Check if recipient has appreciation streak for required weeks. Returns True if streak meets or exceeds required_weeks. + Includes the current partial week. """ from datetime import timedelta now = timezone.now() - current_week = 0 + current_week_start = now - timedelta(days=now.weekday()) + current_week_start = current_week_start.replace(hour=0, minute=0, second=0, microsecond=0) - # Check week by week going backwards - for i in range(required_weeks): - week_start = now - timedelta(weeks=i+1) - week_end = now - timedelta(weeks=i) + streak = 0 + + if Appreciation.objects.filter( + recipient_content_type=content_type, + recipient_object_id=object_id, + status=AppreciationStatus.SENT, + sent_at__gte=current_week_start, + sent_at__lte=now, + ).exists(): + streak += 1 + + for i in range(1, required_weeks): + week_start = current_week_start - timedelta(weeks=i) + week_end = current_week_start - timedelta(weeks=i - 1) - # Check if there's any appreciation in this week has_appreciation = Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, @@ -473,8 +532,8 @@ def check_appreciation_streak(content_type, object_id, required_weeks): ).exists() if has_appreciation: - current_week += 1 + streak += 1 else: break - return current_week >= required_weeks + return streak >= required_weeks diff --git a/apps/appreciation/ui_views.py b/apps/appreciation/ui_views.py index a9f6f82..e078996 100644 --- a/apps/appreciation/ui_views.py +++ b/apps/appreciation/ui_views.py @@ -5,6 +5,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt from django.contrib.contenttypes.models import ContentType +from django.core.cache import cache from django.core.paginator import Paginator from django.db.models import Q, Count from django.http import JsonResponse @@ -113,6 +114,10 @@ def appreciation_detail(request, pk): categories = AppreciationCategory.objects.filter(is_active=True).order_by("order", "name_en") + from django.contrib.contenttypes.models import ContentType + appreciation_ct = ContentType.objects.get_for_model(appreciation) + generic_notes = appreciation.notes.select_related("created_by").all() + context = { "appreciation": appreciation, "metadata": metadata, @@ -122,6 +127,10 @@ def appreciation_detail(request, pk): "can_activate": appreciation.status == AppreciationStatus.DRAFT, "can_send": appreciation.status in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED), "is_recipient": False, + "content_type_id": appreciation_ct.pk, + "object_id": appreciation.pk, + "notes": generic_notes, + "notes_count": generic_notes.count(), } return render(request, "appreciation/appreciation_detail.html", context) @@ -191,29 +200,21 @@ def appreciation_activate(request, pk): from apps.core.ai_service import AIService analysis_result = AIService.chat_completion( - messages=[ - { - "role": "system", - "content": "You are analyzing patient appreciation messages. Always respond with valid JSON.", - }, - { - "role": "user", - "content": f'Analyze this patient appreciation message and provide:\n' - f'1. A summary of what the patient appreciated (in English and Arabic)\n' - f'2. Key themes mentioned\n' - f'3. Suggested category if not already set\n' - f'4. Tone analysis\n\n' - f'Message: "{appreciation.message_en}"\n\n' - f'Respond in JSON format with keys:\n' - f'- summary_en: English summary\n' - f'- summary_ar: Arabic summary\n' - f'- themes: List of key themes\n' - f'- tone: "warm", "formal", or "casual"\n' - f'- suggested_response_en: Suggested response in English\n' - f'- suggested_response_ar: Suggested response in Arabic', - }, - ], - response_format={"type": "json_object"}, + prompt=f'Analyze this patient appreciation message and provide:\n' + f'1. A summary of what the patient appreciated (in English and Arabic)\n' + f'2. Key themes mentioned\n' + f'3. Suggested category if not already set\n' + f'4. Tone analysis\n\n' + f'Message: "{appreciation.message_en}"\n\n' + f'Respond in JSON format with keys:\n' + f'- summary_en: English summary\n' + f'- summary_ar: Arabic summary\n' + f'- themes: List of key themes\n' + f'- tone: "warm", "formal", or "casual"\n' + f'- suggested_response_en: Suggested response in English\n' + f'- suggested_response_ar: Suggested response in Arabic', + system_prompt="You are analyzing patient appreciation messages. Always respond with valid JSON.", + response_format="json_object", ) import json @@ -224,11 +225,8 @@ def appreciation_activate(request, pk): import logging logging.getLogger(__name__).error(f"AI analysis failed for appreciation {appreciation.pk}: {str(e)}") - appreciation.status = AppreciationStatus.AI_ANALYZED - appreciation.ai_analyzed_at = timezone.now() - appreciation.save(update_fields=["status", "ai_analyzed_at"]) - messages.success(request, _("Appreciation activated and AI analysis complete.")) + messages.success(request, _("Appreciation activated successfully.")) return redirect("appreciation:appreciation_detail", pk=appreciation.pk) @@ -304,6 +302,10 @@ def appreciation_acknowledge(request, pk): messages.error(request, "You can only acknowledge appreciations sent to you.") return redirect('appreciation:appreciation_detail', pk=pk) + if appreciation.status != AppreciationStatus.SENT: + messages.error(request, "This appreciation cannot be acknowledged in its current status.") + return redirect('appreciation:appreciation_detail', pk=pk) + # Acknowledge appreciation.acknowledge() @@ -420,8 +422,31 @@ def my_badges_view(request): for badge in available_badges: earned = queryset.filter(badge=badge).exists() progress = 0 - if badge.criteria_type == 'count': - progress = min(100, int((total_received / badge.criteria_value) * 100)) + if badge.criteria_type in ('received_count', 'received_month'): + progress = min(100, int((total_received / badge.criteria_value) * 100)) if badge.criteria_value else 0 + elif badge.criteria_type == 'diverse_senders': + unique_senders = Appreciation.objects.filter( + recipient_content_type=user_content_type, + recipient_object_id=user.id + ).values('sender').distinct().count() + progress = min(100, int((unique_senders / badge.criteria_value) * 100)) if badge.criteria_value else 0 + elif badge.criteria_type == 'streak_weeks': + from datetime import timedelta + now = timezone.now() + streak = 0 + for i in range(badge.criteria_value): + week_start = now - timedelta(weeks=i+1) + week_end = now - timedelta(weeks=i) + if Appreciation.objects.filter( + recipient_content_type=user_content_type, + recipient_object_id=user.id, + sent_at__gte=week_start, + sent_at__lt=week_end, + ).exists(): + streak += 1 + else: + break + progress = min(100, int((streak / badge.criteria_value) * 100)) if badge.criteria_value else 0 badge_progress.append({ 'badge': badge, @@ -692,7 +717,7 @@ def badge_create(request): description_ar = request.POST.get('description_ar', '') icon = request.POST.get('icon', 'fa-award') color = request.POST.get('color', '#FFD700') - criteria_type = request.POST.get('criteria_type', 'count') + criteria_type = request.POST.get('criteria_type', 'received_count') criteria_value = request.POST.get('criteria_value', 5) order = request.POST.get('order', 0) is_active = request.POST.get('is_active') == 'on' @@ -761,7 +786,7 @@ def badge_edit(request, pk): badge.description_ar = request.POST.get('description_ar', '') badge.icon = request.POST.get('icon', 'fa-award') badge.color = request.POST.get('color', '#FFD700') - badge.criteria_type = request.POST.get('criteria_type', 'count') + badge.criteria_type = request.POST.get('criteria_type', 'received_count') badge.criteria_value = request.POST.get('criteria_value', 5) badge.order = request.POST.get('order', 0) badge.is_active = request.POST.get('is_active') == 'on' @@ -980,6 +1005,13 @@ def public_appreciation_submit(request): import logging logger = logging.getLogger(__name__) + client_ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() or request.META.get('REMOTE_ADDR', '') + cache_key = f"appreciation_rate:{client_ip}" + from django.core.cache import cache + if cache.get(cache_key, 0) >= 5: + return JsonResponse({"success": False, "message": "Too many requests. Please try again later."}, status=429) + cache.set(cache_key, cache.get(cache_key, 0) + 1, 300) + try: try: data = json.loads(request.body) if request.content_type == 'application/json' else request.POST @@ -991,9 +1023,8 @@ def public_appreciation_submit(request): message = data.get("message", "").strip() hospital_id = data.get("hospital", "") staff_name = data.get("staff_name", "").strip() - location_id = data.get("location", "").strip() - main_section_id = data.get("main_section", "").strip() - subsection_id = data.get("subsection", "").strip() + department_id = data.get("department", "").strip() + section_id = data.get("section", "").strip() if not contact_name or not contact_phone or not message: return JsonResponse({"success": False, "message": "Name, phone, and message are required."}, status=400) @@ -1006,16 +1037,14 @@ def public_appreciation_submit(request): except Hospital.DoesNotExist: return JsonResponse({"success": False, "message": "Invalid hospital."}, status=400) - from apps.organizations.models import Location, MainSection, SubSection - location = Location.objects.filter(id=location_id).first() if location_id else None - main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None - subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None + from apps.organizations.models import Department, Section, OrgSubSection + department = Department.objects.filter(id=department_id).first() if department_id else None + section = Section.objects.filter(id=section_id).first() if section_id else None appreciation = Appreciation( hospital=hospital, - location=location, - main_section=main_section, - subsection=subsection, + department=department, + section=section, category=None, message_en=message, message_ar="", @@ -1044,7 +1073,7 @@ def public_appreciation_submit(request): metadata={"hospital": str(hospital.id), "staff_mentioned": staff_name}, ) - return JsonResponse({"success": True, "reference": f"APR-{str(appreciation.pk)[:8]}"}) + return JsonResponse({"success": True, "reference": appreciation.reference_number}) except Exception as e: logger.exception("ERROR in public_appreciation_submit") return JsonResponse({"success": False, "message": str(e)}, status=500) diff --git a/apps/complaints/admin.py b/apps/complaints/admin.py index 594b789..9f62d74 100644 --- a/apps/complaints/admin.py +++ b/apps/complaints/admin.py @@ -10,6 +10,7 @@ from .models import ( ComplaintAttachment, ComplaintCategory, ComplaintMeeting, + ComplaintPdfSummary, ComplaintPRInteraction, ComplaintSLAConfig, ComplaintThreshold, @@ -134,9 +135,11 @@ class ComplaintAdmin(admin.ModelAdmin): "priority", "category", "source", - "location", - "main_section", - "subsection", + "legacy_location", + "legacy_main_section", + "legacy_subsection", + "section", + "is_overdue", "hospital", "created_by", @@ -170,7 +173,7 @@ class ComplaintAdmin(admin.ModelAdmin): {"fields": ("patient", "patient_name", "file_number", "encounter_id", "incident_date", "contact_phone")}, ), ("Organization", {"fields": ("hospital", "department", "staff")}), - ("Location Hierarchy", {"fields": ("location", "main_section", "subsection")}), + ("Location Hierarchy", {"fields": ("legacy_location", "legacy_main_section", "legacy_subsection", "section")}), ( "Complaint Details", { @@ -256,9 +259,11 @@ class ComplaintAdmin(admin.ModelAdmin): "hospital", "department", "staff", - "location", - "main_section", - "subsection", + "legacy_location", + "legacy_main_section", + "legacy_subsection", + "section", + "assigned_to", "resolved_by", "closed_by", @@ -275,12 +280,12 @@ class ComplaintAdmin(admin.ModelAdmin): def location_hierarchy(self, obj): """Display location hierarchy in admin""" parts = [] - if obj.location: - parts.append(obj.location.name_en or obj.location.name_ar or str(obj.location)) - if obj.main_section: - parts.append(obj.main_section.name_en or obj.main_section.name_ar or str(obj.main_section)) - if obj.subsection: - parts.append(obj.subsection.name_en or obj.subsection.name_ar or str(obj.subsection)) + if obj.legacy_location: + parts.append(obj.legacy_location.name_en or obj.legacy_location.name_ar or str(obj.legacy_location)) + if obj.legacy_main_section: + parts.append(obj.legacy_main_section.name_en or obj.legacy_main_section.name_ar or str(obj.legacy_main_section)) + if obj.legacy_subsection: + parts.append(obj.legacy_subsection.name_en or obj.legacy_subsection.name_ar or str(obj.legacy_subsection)) if not parts: return "—" @@ -312,8 +317,8 @@ class ComplaintAdmin(admin.ModelAdmin): "resolved": "success", "closed": "secondary", "cancelled": "secondary", - "contacted": "info", - "contacted_no_response": "danger", + "pending_external": "info", + "ovr_pending": "warning", } color = colors.get(obj.status, "secondary") return format_html('{1}', color, obj.get_status_display()) @@ -387,6 +392,21 @@ class ComplaintAttachmentAdmin(admin.ModelAdmin): return qs.select_related("complaint", "uploaded_by") +@admin.register(ComplaintPdfSummary) +class ComplaintPdfSummaryAdmin(admin.ModelAdmin): + """PDF summary admin""" + + list_display = ["complaint", "file_size", "created_at"] + search_fields = ["complaint__reference_number"] + ordering = ["-created_at"] + + readonly_fields = ["file_size", "created_at", "updated_at"] + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related("complaint") + + @admin.register(ComplaintUpdate) class ComplaintUpdateAdmin(admin.ModelAdmin): """Complaint update admin""" @@ -1008,7 +1028,7 @@ class GovernmentTicketAdmin(admin.ModelAdmin): "ticket_number", "source", "complainant_name", - "main_section", + "legacy_main_section", "received_date", "status", "converted_to_complaint", @@ -1033,7 +1053,7 @@ class GovernmentTicketAdmin(admin.ModelAdmin): fieldsets = ( ("Source", {"fields": ("source", "ticket_number")}), ("Complainant", {"fields": ("complainant_name", "national_id", "contact_number")}), - ("Location & Section", {"fields": ("location", "main_section", "subsection")}), + ("Location & Section", {"fields": ("legacy_location", "legacy_main_section", "legacy_subsection", "section")}), ("Dates", {"fields": ("received_date",)}), ("Content", {"fields": ("classification", "content")}), ("Status & Assignment", {"fields": ("status", "assigned_to")}), @@ -1045,4 +1065,4 @@ class GovernmentTicketAdmin(admin.ModelAdmin): def get_queryset(self, request): qs = super().get_queryset(request) - return qs.select_related("source", "location", "main_section", "assigned_to", "complaint") + return qs.select_related("source", "legacy_location", "legacy_main_section", "section", "assigned_to", "complaint") diff --git a/apps/complaints/forms.py b/apps/complaints/forms.py index e345dcb..937ab26 100644 --- a/apps/complaints/forms.py +++ b/apps/complaints/forms.py @@ -27,7 +27,7 @@ from apps.complaints.models import ( ) from apps.core.models import PriorityChoices, SeverityChoices from apps.core.form_mixins import HospitalFieldMixin, DepartmentFieldMixin -from apps.organizations.models import Department, Hospital, Patient, Staff +from apps.organizations.models import Area, Department, Hospital, LocationType, Patient, Staff, Section class MultiFileInput(forms.FileInput): @@ -137,31 +137,28 @@ class PublicComplaintForm(forms.ModelForm): widget=forms.Select(attrs={"class": "form-control", "id": "department_select"}), ) - # Complaint Details - Location Hierarchy - location = forms.ModelChoiceField( - label=_("Location"), - queryset=None, - empty_label=_("Select Location"), + # Location Type and Area + location_type = forms.ChoiceField( + label=_("Location Type"), + choices=[("", _("Select Location Type"))] + list(LocationType.choices), required=True, - widget=forms.Select(attrs={"class": "form-control", "id": "location_select", "data-action": "load-sections"}), + widget=forms.Select(attrs={"class": "form-control", "id": "location_type_select"}), ) - main_section = forms.ModelChoiceField( + area = forms.ModelChoiceField( + label=_("Area (Optional)"), + queryset=Area.objects.none(), + empty_label=_("Select Area"), + required=False, + widget=forms.Select(attrs={"class": "form-control", "id": "area_select"}), + ) + + section = forms.ModelChoiceField( label=_("Section"), - queryset=None, + queryset=Section.objects.none(), empty_label=_("Select Section"), - required=True, - widget=forms.Select( - attrs={"class": "form-control", "id": "main_section_select", "data-action": "load-subsections"} - ), - ) - - subsection = forms.ModelChoiceField( - label=_("Subsection"), - queryset=None, - empty_label=_("Select Subsection"), - required=True, - widget=forms.Select(attrs={"class": "form-control", "id": "subsection_select"}), + required=False, + widget=forms.Select(attrs={"class": "form-control", "id": "section_select"}), ) staff_name = forms.CharField( @@ -212,11 +209,11 @@ class PublicComplaintForm(forms.ModelForm): widget=forms.HiddenInput(), ) - # Source type - always external for public complaints + # Source type - always internal for public complaints complaint_source_type = forms.ChoiceField( label=_("Complaint Source Type"), choices=ComplaintSourceType.choices, - initial=ComplaintSourceType.EXTERNAL, + initial=ComplaintSourceType.INTERNAL, required=False, widget=forms.HiddenInput(), ) @@ -240,9 +237,10 @@ class PublicComplaintForm(forms.ModelForm): "patient_name", "national_id", "incident_date", - "location", - "main_section", - "subsection", + "location_type", + "area", + "department", + "section", "staff_name", "complaint_details", "expected_result", @@ -255,46 +253,10 @@ class PublicComplaintForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - from apps.organizations.models import Location, MainSection, SubSection - # Initialize cascading dropdowns with empty querysets - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() + self.fields["section"].queryset = Section.objects.none() + self.fields["area"].queryset = Area.objects.none() - # Load all locations (no filtering needed) - self.fields["location"].queryset = Location.active_locations() - - # Check both initial data and POST data for location to load sections - location_id = None - if "location" in self.initial: - location_id = self.initial["location"] - elif "location" in self.data: - location_id = self.data["location"] - - if location_id: - # Filter sections based on selected location - from apps.organizations.models import SubSection - - available_sections = ( - SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() - ) - self.fields["main_section"].queryset = MainSection.objects.filter(id__in=available_sections).order_by( - "name_en" - ) - - # Load subsections if section is selected - section_id = None - if "main_section" in self.initial: - section_id = self.initial["main_section"] - elif "main_section" in self.data: - section_id = self.data["main_section"] - - if section_id: - self.fields["subsection"].queryset = SubSection.objects.filter( - location_id=location_id, main_section_id=section_id - ).order_by("name_en") - - # Also filter departments based on hospital if provided hospital_id = None if "hospital" in self.initial: hospital_id = self.initial["hospital"] @@ -302,10 +264,23 @@ class PublicComplaintForm(forms.ModelForm): hospital_id = self.data["hospital"] if hospital_id: - # Filter departments self.fields["department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") + self.fields["area"].queryset = Area.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name_en") + + department_id = None + if "department" in self.initial: + department_id = self.initial["department"] + elif "department" in self.data: + department_id = self.data["department"] + + if department_id: + self.fields["section"].queryset = Section.objects.filter( + department_id=department_id, status="active" + ).order_by("name_en") def clean_mobile_number(self): """Validate mobile number format""" @@ -380,9 +355,9 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): """ Form for creating complaints by authenticated users. - Updated to use location hierarchy (Location, Section, Subsection). + Uses Category → Department → Section hierarchy. Includes new fields for detailed patient information and complaint type. - Uses cascading dropdowns for location selection. + Uses cascading dropdowns for department/section selection. Hospital field visibility: - PX Admins: See dropdown with all hospitals @@ -402,7 +377,7 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): complaint_source_type = forms.ChoiceField( label=_("Complaint Source Type"), choices=ComplaintSourceType.choices, - initial=ComplaintSourceType.EXTERNAL, + initial=ComplaintSourceType.INTERNAL, required=False, widget=forms.Select(attrs={"class": "form-select", "id": "complaintSourceType"}), ) @@ -453,6 +428,21 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): widget=forms.Select(attrs={"class": "form-select", "id": "hospitalSelect"}), ) + location_type = forms.ChoiceField( + label=_("Location Type"), + choices=[("", _("Select Location Type"))] + list(LocationType.choices), + required=True, + widget=forms.Select(attrs={"class": "form-select", "id": "locationTypeSelect"}), + ) + + area = forms.ModelChoiceField( + label=_("Area"), + queryset=Area.objects.none(), + empty_label=_("Select Area (optional)"), + required=False, + widget=forms.Select(attrs={"class": "form-select", "id": "areaSelect"}), + ) + department = forms.ModelChoiceField( label=_("Department"), queryset=Department.objects.none(), @@ -461,6 +451,14 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): widget=forms.Select(attrs={"class": "form-select", "id": "departmentSelect"}), ) + section = forms.ModelChoiceField( + label=_("Section"), + queryset=Section.objects.none(), + empty_label=_("Select Section (optional)"), + required=False, + widget=forms.Select(attrs={"class": "form-select", "id": "sectionSelect"}), + ) + staff = forms.ModelChoiceField( label=_("Staff"), queryset=Staff.objects.none(), @@ -469,48 +467,6 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): widget=forms.Select(attrs={"class": "form-select", "id": "staffSelect"}), ) - encounter_id = forms.CharField( - label=_("Encounter ID"), - required=False, - widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Optional encounter/visit ID")}), - ) - - # Location Hierarchy Fields - location = forms.ModelChoiceField( - label=_("Location"), - queryset=None, - empty_label=_("Select Location"), - required=True, - widget=forms.Select(attrs={"class": "form-select", "id": "locationSelect", "data-action": "load-sections"}), - ) - - main_section = forms.ModelChoiceField( - label=_("Section"), - queryset=None, - empty_label=_("Select Section"), - required=True, - widget=forms.Select( - attrs={"class": "form-select", "id": "mainSectionSelect", "data-action": "load-subsections"} - ), - ) - - subsection = forms.ModelChoiceField( - label=_("Subsection"), - queryset=None, - empty_label=_("Select Subsection"), - required=True, - widget=forms.Select(attrs={"class": "form-select", "id": "subsectionSelect"}), - ) - - staff_name = forms.CharField( - label=_("Staff Involved"), - max_length=200, - required=False, - widget=forms.TextInput( - attrs={"class": "form-control", "placeholder": _("Name of staff member involved (if known)")} - ), - ) - description = forms.CharField( label=_("Description"), required=True, @@ -537,14 +493,12 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): "patient_name", "national_id", "incident_date", + "location_type", + "area", "hospital", "department", - "location", - "main_section", - "subsection", + "section", "staff", - "staff_name", - "encounter_id", "description", "expected_result", ] @@ -552,44 +506,16 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): def __init__(self, *args, **kwargs): # Note: user is handled by HospitalFieldMixin super().__init__(*args, **kwargs) - from apps.organizations.models import Location, MainSection, SubSection + from apps.organizations.models import Section from apps.px_sources.models import PXSource # Initialize cascading dropdowns with empty querysets - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() - - hospital_id = None - if self.data.get("hospital"): - hospital_id = self.data.get("hospital") - elif self.initial.get("hospital"): - hospital_id = self.initial.get("hospital") - elif self.request and self.request.user and hasattr(self.request.user, 'hospital') and self.request.user.hospital: - hospital_id = self.request.user.hospital.id - - self.fields["location"].queryset = Location.active_locations() + self.fields["section"].queryset = Section.objects.none() + self.fields["area"].queryset = Area.objects.none() # Load active PX sources for optional selection self.fields["source"].queryset = PXSource.objects.filter(is_active=True).order_by("name_en") - # Populate cascading dropdown querysets based on submitted/initial data - location_id = self.data.get("location") or self.initial.get("location") - if location_id: - available_sections = ( - SubSection.objects.filter(location_id=location_id) - .values_list("main_section_id", flat=True) - .distinct() - ) - self.fields["main_section"].queryset = MainSection.objects.filter( - id__in=available_sections - ).order_by("name_en") - - section_id = self.data.get("main_section") or self.initial.get("main_section") - if section_id: - self.fields["subsection"].queryset = SubSection.objects.filter( - location_id=location_id, main_section_id=section_id - ).order_by("name_en") - # Hospital field is configured by HospitalFieldMixin # Now filter departments and staff based on hospital hospital_id = None @@ -615,6 +541,18 @@ class ComplaintForm(HospitalFieldMixin, forms.ModelForm): "first_name", "last_name" ) + # Filter areas based on selected hospital + self.fields["area"].queryset = Area.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name_en") + + # Populate section dropdown based on selected department + department_id = self.data.get("department") or self.initial.get("department") + if department_id: + self.fields["section"].queryset = Section.objects.filter( + department_id=department_id + ).order_by("name_en") + def clean_incident_date(self): incident_date = self.cleaned_data.get("incident_date") @@ -654,6 +592,21 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): widget=forms.Select(attrs={"class": "form-select", "id": "hospitalSelect"}), ) + location_type = forms.ChoiceField( + label=_("Location Type"), + choices=[("", _("Select Location Type"))] + list(LocationType.choices), + required=True, + widget=forms.Select(attrs={"class": "form-select", "id": "locationTypeSelect"}), + ) + + area = forms.ModelChoiceField( + label=_("Area"), + queryset=Area.objects.none(), + empty_label=_("Select Area (optional)"), + required=False, + widget=forms.Select(attrs={"class": "form-select", "id": "areaSelect"}), + ) + department = forms.ModelChoiceField( label=_("Department (Optional)"), queryset=Department.objects.none(), @@ -702,30 +655,12 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): label=_("Contact Email"), required=False, widget=forms.EmailInput(attrs={"class": "form-control"}) ) - location = forms.ModelChoiceField( - label=_("Location"), - queryset=None, - empty_label=_("Select Location"), - required=False, - widget=forms.Select(attrs={"class": "form-select", "id": "locationSelect", "data-action": "load-sections"}), - ) - - main_section = forms.ModelChoiceField( + section = forms.ModelChoiceField( label=_("Section"), queryset=None, empty_label=_("Select Section"), required=False, - widget=forms.Select( - attrs={"class": "form-select", "id": "mainSectionSelect", "data-action": "load-subsections"} - ), - ) - - subsection = forms.ModelChoiceField( - label=_("Subsection"), - queryset=None, - empty_label=_("Select Subsection"), - required=False, - widget=forms.Select(attrs={"class": "form-select", "id": "subsectionSelect"}), + widget=forms.Select(attrs={"class": "form-select", "id": "sectionSelect"}), ) priority = forms.ChoiceField( @@ -771,15 +706,15 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): fields = [ "patient", "hospital", + "location_type", + "area", "department", "subject", "message", "contact_name", "contact_phone", "contact_email", - "location", - "main_section", - "subsection", + "section", "priority", "source", "is_outgoing", @@ -790,35 +725,17 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): # Note: user is handled by HospitalFieldMixin super().__init__(*args, **kwargs) - from apps.organizations.models import Location, MainSection, SubSection + from apps.organizations.models import Section from apps.px_sources.models import PXSource - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() - self.fields["location"].queryset = Location.objects.none() + self.fields["section"].queryset = Section.objects.none() + self.fields["area"].queryset = Area.objects.none() # Load active PX sources for optional selection self.fields["source"].queryset = PXSource.objects.filter(is_active=True).order_by("name_en") self.fields["source"].empty_label = "Select source (optional)" self.fields["source"].required = False - location_id = self.data.get("location") or self.initial.get("location") - if location_id: - available_sections = ( - SubSection.objects.filter(location_id=location_id) - .values_list("main_section_id", flat=True) - .distinct() - ) - self.fields["main_section"].queryset = MainSection.objects.filter( - id__in=available_sections - ).order_by("name_en") - - section_id = self.data.get("main_section") or self.initial.get("main_section") - if section_id: - self.fields["subsection"].queryset = SubSection.objects.filter( - location_id=location_id, main_section_id=section_id - ).order_by("name_en") - hospital_id = None if self.data.get("hospital"): hospital_id = self.data.get("hospital") @@ -838,6 +755,16 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): self.fields["outgoing_department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") + self.fields["area"].queryset = Area.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name_en") + + # Populate section dropdown based on selected department + department_id = self.data.get("department") or self.initial.get("department") + if department_id: + self.fields["section"].queryset = Section.objects.filter( + department_id=department_id + ).order_by("name_en") class SLAConfigForm(HospitalFieldMixin, forms.ModelForm): @@ -1054,7 +981,38 @@ class PublicInquiryForm(forms.Form): queryset=Hospital.objects.filter(status="active").order_by("name"), empty_label=_("Select Hospital"), required=True, - widget=forms.Select(attrs={"class": "form-control"}), + widget=forms.Select(attrs={"class": "form-control", "id": "hospital_select"}), + ) + + location_type = forms.ChoiceField( + label=_("Location Type"), + choices=[("", _("Select Location Type"))] + list(LocationType.choices), + required=True, + widget=forms.Select(attrs={"class": "form-control", "id": "location_type_select"}), + ) + + area = forms.ModelChoiceField( + label=_("Area (Optional)"), + queryset=Area.objects.none(), + empty_label=_("Select Area"), + required=False, + widget=forms.Select(attrs={"class": "form-control", "id": "area_select"}), + ) + + department = forms.ModelChoiceField( + label=_("Department (Optional)"), + queryset=Department.objects.none(), + empty_label=_("Select Department"), + required=False, + widget=forms.Select(attrs={"class": "form-control", "id": "department_select"}), + ) + + section = forms.ModelChoiceField( + label=_("Section (Optional)"), + queryset=Section.objects.none(), + empty_label=_("Select Section"), + required=False, + widget=forms.Select(attrs={"class": "form-control", "id": "section_select"}), ) category = forms.ChoiceField( @@ -1083,6 +1041,36 @@ class PublicInquiryForm(forms.Form): widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": _("Describe your inquiry")}), ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["area"].queryset = Area.objects.none() + self.fields["section"].queryset = Section.objects.none() + + hospital_id = None + if "hospital" in self.initial: + hospital_id = self.initial["hospital"] + elif "hospital" in self.data: + hospital_id = self.data["hospital"] + + if hospital_id: + self.fields["department"].queryset = Department.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name") + self.fields["area"].queryset = Area.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name_en") + + department_id = None + if "department" in self.initial: + department_id = self.initial["department"] + elif "department" in self.data: + department_id = self.data["department"] + + if department_id: + self.fields["section"].queryset = Section.objects.filter( + department_id=department_id, status="active" + ).order_by("name_en") + class ComplaintInvolvedDepartmentForm(forms.ModelForm): """ @@ -1246,9 +1234,8 @@ class GovernmentTicketForm(forms.ModelForm): "complainant_name", "national_id", "contact_number", - "location", - "main_section", - "subsection", + "department", + "section", "received_date", "classification", "content", @@ -1286,11 +1273,8 @@ class GovernmentTicketForm(forms.ModelForm): self.fields["source"].queryset = self.fields["source"].queryset.filter( source_type="government", is_active=True ) - self.fields["main_section"].required = False - self.fields["subsection"].required = False self.fields["national_id"].required = False self.fields["contact_number"].required = False - self.fields["location"].required = False self.fields["classification"].required = False self.fields["assigned_to"].required = False @@ -1298,15 +1282,11 @@ class GovernmentTicketForm(forms.ModelForm): from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(hospital) - from apps.organizations.models import Location, MainSection, SubSection + from apps.organizations.models import Section if args and args[0]: - self.fields["location"].queryset = Location.objects.all() - self.fields["main_section"].queryset = MainSection.objects.all() - self.fields["subsection"].queryset = SubSection.objects.all() + self.fields["section"].queryset = Section.objects.all() else: - self.fields["location"].queryset = Location.objects.none() - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() + self.fields["section"].queryset = Section.objects.none() def clean_ticket_number(self): ticket_number = self.cleaned_data.get("ticket_number") diff --git a/apps/complaints/management/commands/arabic_dept_mapping.py b/apps/complaints/management/commands/arabic_dept_mapping.py new file mode 100644 index 0000000..766335a --- /dev/null +++ b/apps/complaints/management/commands/arabic_dept_mapping.py @@ -0,0 +1,404 @@ +""" +Arabic sub-department name -> Department code mapping. + +Used by complaint import scripts to set the `department` FK based on the +Arabic القسم الفرعي (sub-department) column in the historical Excel files. + +All codes target HH-N (Al Nuzha) since historical data is from that hospital. +""" + +import re + + +def normalize_arabic(text: str) -> str: + if not text: + return "" + text = str(text).strip() + text = text.replace("\u0623", "\u0627") # أ → ا + text = text.replace("\u0625", "\u0627") # إ → ا + text = text.replace("\u0622", "\u0627") # آ → ا + text = text.replace("\u0649", "\u064a") # ى → ي + text = re.sub(r"\s+", " ", text) + text = text.strip() + return text + + +# Canonical Arabic name → department code +# All codes are HH-N (Nuzha) department codes +ARABIC_TO_DEPT_CODE = { + # Pharmacy + "قسم الصيدلية": "hh_n_pharmacy_department", + "صيدلية الطوارئ": "hh_n_pharmacy_department", + "قسم الصيدلية - السويدي": "hh_n_pharmacy_department", + # Laboratory + "قسم المختبر": "hh_n_laboratory_department", + "استقبال المختبر": "hh_n_laboratory_department", + "بنك الدم": "hh_n_laboratory_department", + # Medical Approvals + "قسم الموافقات الطبية": "hh_n_medical_approvals_department", + "الموافقات الطبية": "hh_n_medical_approvals_department", + # Medical Records + "قسم التقارير الطبية": "hh_n_medical_records_department", + # Financial + "قسم المالية": "hh_n_financial_collection___claims_department", + # Dental + "عيادات الأسنان": "hh_n_dental_department", + # Ophthalmology + "عيادات العيون": "hh_n_ophthalmology_department", + "عيادات جراحة العيون": "hh_n_ophthalmology_department", + "فني فحص النظر": "hh_n_ophthalmology_department", + # Dermatology + "عيادات الجلدية": "hh_n_dermatology_department", + "عيادات الجلديه": "hh_n_dermatology_department", + # OB/GYN + "عيادات النساء والولادة": "hh_n_obstetrics___gynecology_department", + "تنويم النساء والولادة": "hh_n_obstetrics___gynecology_department", + "تنويم النساء والولاده": "hh_n_obstetrics___gynecology_department", + "قسم عمليات الولادة": "hh_n_obstetrics___gynecology_department", + "تمريض تنويم النساء والولادة": "hh_n_obstetrics___gynecology_department", + "تمريض قسم عمليات الولادة": "hh_n_nursing_department", + # Pediatric + "عيادات الأطفال": "hh_n_pediatric_department", + "تنويم الأطفال": "hh_n_pediatric_department", + "تنويم الاطفال": "hh_n_pediatric_department", + "تمريض تنويم الأطفال": "hh_n_pediatric_department", + "تمريض تنويم الاطفال": "hh_n_pediatric_department", + "عيادات الأنف والأذن والحنجرة (أطفال)": "hh_n_pediatric_department", + "عيادات الصدرية (أطفال)": "hh_n_pediatric_department", + "عيادات الجهاز الهمضي والمناظير (أطفال)": "hh_n_pediatric_department", + "عيادات جراحة عظام (أطفال)": "hh_n_pediatric_department", + "عيادات جراحة المسالك البولية (أطفال)": "hh_n_pediatric_department", + "عيادات قلب (أطفال)": "hh_n_pediatric_department", + "عيادات المخ والأعصاب (أطفال)": "hh_n_pediatric_department", + "عيادات الجراحة العامة (أطفال)": "hh_n_pediatric_department", + "تمريض الحضانة": "hh_n_pediatric_department", + "وحدة العناية المركزة لحديثي الولادة": "hh_n_pediatric_department", + "وحدة العناية المركزة للأطفال": "hh_n_pediatric_department", + "قسم الحضانة": "hh_n_pediatric_department", + "تمريض وحدة العناية المركزة - أطفال": "hh_n_pediatric_department", + "تمريض وحدة العناية لحديثي الولادة": "hh_n_pediatric_department", + # Surgery + "عيادات جراحة العظام": "hh_n_surgery_department", + "عيادات جراحه العظام": "hh_n_surgery_department", + "قسم جراحة العظام": "hh_n_surgery_department", + "عيادات الجراحة العامة": "hh_n_surgery_department", + "قسم الجراحة العامة": "hh_n_surgery_department", + "قسم جراحة المسالك البولية": "hh_n_surgery_department", + "عيادات جراحة المسالك البولية": "hh_n_surgery_department", + "عيادات جراحة الأنف وأذن وحنجرة": "hh_n_surgery_department", + "جراحه الانف واذن وحنجره": "hh_n_surgery_department", + "عيادات جراحة الأوعية الدموية": "hh_n_surgery_department", + "عيادات جراحة المخ والأعصاب": "hh_n_surgery_department", + "عيادات جراحة القفص الصدري": "hh_n_surgery_department", + "عيادات جراحة العمود الفقري": "hh_n_surgery_department", + "عيادات جراحة التجميلية": "hh_n_surgery_department", + "عيادات الجراحة التجميلية": "hh_n_surgery_department", + "عيادات جراحة سمنة": "hh_n_surgery_department", + "قسم جراحة السمنة": "hh_n_surgery_department", + "عيادات جراحة الختان": "hh_n_surgery_department", + "عيادات جراحة الأورام و الغدد الصماء": "hh_n_surgery_department", + "تمريض تنويم الجراحة العامة": "hh_n_surgery_department", + "تنويم الجراحة العامة": "hh_n_surgery_department", + "تنويم الباطنية": "hh_n_internal_medicine_department", + # Internal Medicine + "عيادات الباطنية": "hh_n_internal_medicine_department", + "عيادات القلب": "hh_n_internal_medicine_department", + "عيادات الجهاز الهضمي والمناظير": "hh_n_internal_medicine_department", + "قسم الجهاز الهمضي و الكبد والمناظير": "hh_n_internal_medicine_department", + "قسم الجهاز الهضمي والكبد والمناظير": "hh_n_internal_medicine_department", + "عيادات الصدرية": "hh_n_internal_medicine_department", + "عيادات الصدريه": "hh_n_internal_medicine_department", + "قسم الصدرية": "hh_n_internal_medicine_department", + "عيادات الغدد الصماء": "hh_n_internal_medicine_department", + "عيادات المخ والأعصاب": "hh_n_internal_medicine_department", + "عيادات الكلى": "hh_n_internal_medicine_department", + "عيادات التخدير": "hh_n_anesthesia_department", + "عيادات الطب النفسي": "hh_n_internal_medicine_department", + "وحدة المناظير": "hh_n_internal_medicine_department", + "تمريض تنويم الباطنية": "hh_n_internal_medicine_department", + "قسم العلاج الطبيعي": "hh_n_medical_ancillary_services_department", + "استقبال العلاج الطبيعي": "hh_n_medical_ancillary_services_department", + "العلاج الطبيعي": "hh_n_medical_ancillary_services_department", + # Anesthesia + "قسم التخدير": "hh_n_anesthesia_department", + "تمريض التخدير": "hh_n_anesthesia_department", + # Emergency + "أطباء الطوارئ": "hh_n_emergency_medicine_department", + "استقبال الطوارئ": "hh_n_emergency_administrative_department", + "استقبال الطواريء": "hh_n_emergency_administrative_department", + "استقبال العيادات الخارجية": "hh_n_outpatient_department", + # Critical Care + "وحدة العناية المركزة": "hh_n_critical_care_department", + "وحدة العناية المتوسطة": "hh_n_critical_care_department", + "تمريض وحدة العناية المركزة": "hh_n_critical_care_department", + "تمريض وحدة العناية المتوسطة": "hh_n_critical_care_department", + "تمريض وحدة طويلي الإقامة": "hh_n_critical_care_department", + "وحدة مرضى طويلي الإقامة": "hh_n_critical_care_department", + # Nursing + "تمريض الطوارئ": "hh_n_nursing_department", + "تمريض الطوارى": "hh_n_nursing_department", + "تمريض العيادات الخارجية": "hh_n_nursing_department", + "تمريض العيادات الخارجيه": "hh_n_nursing_department", + "تمريض غرفة التطعيمات": "hh_n_nursing_department", + "تمريض غرفة تقديم الأدوية الوريدية (20)": "hh_n_nursing_department", + "تمريض قسم العمليات": "hh_n_nursing_department", + "تمريض قسم الافاقة": "hh_n_nursing_department", + "تمريض قسم الإفاقة": "hh_n_nursing_department", + "التمريض": "hh_n_nursing_department", + # Radiology + "قسم الأشعة": "hh_n_radiology_department", + "قسم الاشعة": "hh_n_radiology_department", + "استقبال الأشعة": "hh_n_radiology_department", + "الأشعة": "hh_n_radiology_department", + # Operating Rooms + "قسم العمليات": "hh_n_operating_rooms__or__department", + # Outpatient / Inpatient administration + "إدارة التنويم": "hh_n_inpatient_department", + "اداره التنويم": "hh_n_inpatient_department", + "مكتب التنويم": "hh_n_inpatient_department", + "إدارة العيادات الخارجيه": "hh_n_outpatient_department", + "قسم المواعيد": "hh_n_contact_center_department", + "قسم السنترال": "hh_n_contact_center_department", + "قسم تقنية المعلومات": "hh_n_information_technology_department", + # Housekeeping + "قسم النظافة": "hh_n_housekeeping___hospitality_department", + "النظافة": "hh_n_housekeeping___hospitality_department", + "النظافه": "hh_n_housekeeping___hospitality_department", + # Security + "قسم الأمن": "hh_n_security_department", + "قسم الامن": "hh_n_security_department", + # Food Services + "قسم المطبخ": "hh_n_food_services_department", + "المطبخ": "hh_n_food_services_department", + "قسم التغذية": "hh_n_food_services_department", + # Facility Management + "قسم الصيانة": "hh_n_facility_management___maintenance_department", + # Executive / Admin + "المدير المناوب": "hh_n_executive_administration", + "قسم الإدارة": "hh_n_executive_administration", + "التنسيق": "hh_n_executive_administration", + # Social Services + "قسم الخدمة الإجتماعية": "hh_n_inpatient_department", + # Patient Affairs + "قسم علاقات المرضى": "hh_n_patient_affairs_department", + # Medical Ancillary / Respiratory / Audiology / Neurophysiology + "فني اختبار السمع": "hh_n_medical_ancillary_services_department", + "فني تخطيط القلب": "hh_n_medical_ancillary_services_department", + "فني تخطيط المخ والأعصاب": "hh_n_medical_ancillary_services_department", + "فني تخطيط المخ والاعصاب": "hh_n_medical_ancillary_services_department", + "فني دراسة الجهد القلب": "hh_n_medical_ancillary_services_department", + "قسم العلاج التنفسي": "hh_n_medical_ancillary_services_department", + # IVF + "عيادات العقم والإنجاب": "hh_n_ivf", + # Oncology + "عيادات جراحة الأورام و الغدد الصماء": "hh_n_oncology_department", +} + + +# Arabic name → (dept_code, section_name_en) +# Only entries where we can confidently identify the specific section. +# Generic names (e.g. "قسم الصيدلية") are excluded — they map to dept only. +ARABIC_TO_SECTION = { + # Pharmacy + "صيدلية الطوارئ": ("hh_n_pharmacy_department", "ER Pharmacy"), + # Laboratory + "بنك الدم": ("hh_n_laboratory_department", "Blood Donation & Blood Bank"), + "استقبال المختبر": ("hh_n_laboratory_department", "Receiving ِArea"), + # OB/GYN + "عيادات النساء والولادة": ("hh_n_obstetrics___gynecology_department", "Clinics"), + "تنويم النساء والولادة": ("hh_n_obstetrics___gynecology_department", "Wards"), + "تنويم النساء والولاده": ("hh_n_obstetrics___gynecology_department", "Wards"), + "قسم عمليات الولادة": ("hh_n_obstetrics___gynecology_department", "Labor & Delivery (L&D) / Obstetrics Operating Rooms"), + "تمريض تنويم النساء والولادة": ("hh_n_obstetrics___gynecology_department", "Wards"), + # Surgery + "عيادات جراحة العظام": ("hh_n_surgery_department", "Orthopedic Surgery"), + "عيادات جراحه العظام": ("hh_n_surgery_department", "Orthopedic Surgery"), + "قسم جراحة العظام": ("hh_n_surgery_department", "Orthopedic Surgery"), + "عيادات الجراحة العامة": ("hh_n_surgery_department", "General Surgery"), + "قسم الجراحة العامة": ("hh_n_surgery_department", "General Surgery"), + "قسم جراحة المسالك البولية": ("hh_n_surgery_department", "Urology"), + "عيادات جراحة المسالك البولية": ("hh_n_surgery_department", "Urology"), + "عيادات جراحة الأنف وأذن وحنجرة": ("hh_n_surgery_department", "ENT"), + "جراحه الانف واذن وحنجره": ("hh_n_surgery_department", "ENT"), + "عيادات جراحة الأوعية الدموية": ("hh_n_surgery_department", "Vscular Surgery"), + "عيادات جراحة المخ والأعصاب": ("hh_n_surgery_department", "Neurosurgery"), + "عيادات جراحة القفص الصدري": ("hh_n_surgery_department", "Thoracic Surgery"), + "عيادات جراحة العمود الفقري": ("hh_n_surgery_department", "Spine Surgery"), + "عيادات جراحة التجميلية": ("hh_n_surgery_department", "Plastic Surgery"), + "عيادات الجراحة التجميلية": ("hh_n_surgery_department", "Plastic Surgery"), + "عيادات جراحة سمنة": ("hh_n_surgery_department", "Bariatric"), + "قسم جراحة السمنة": ("hh_n_surgery_department", "Bariatric"), + "عيادات جراحة الأورام و الغدد الصماء": ("hh_n_oncology_department", "Hematology"), + "تمريض تنويم الجراحة العامة": ("hh_n_nursing_department", "Surgical Ward"), + "تنويم الجراحة العامة": ("hh_n_nursing_department", "Surgical Ward"), + # Internal Medicine + "عيادات القلب": ("hh_n_internal_medicine_department", "Cardiology"), + "عيادات الجهاز الهضمي والمناظير": ("hh_n_internal_medicine_department", "Gastroenterology"), + "قسم الجهاز الهمضي و الكبد والمناظير": ("hh_n_internal_medicine_department", "Gastroenterology"), + "قسم الجهاز الهضمي والكبد والمناظير": ("hh_n_internal_medicine_department", "Gastroenterology"), + "عيادات الصدرية": ("hh_n_internal_medicine_department", "Pulmonology"), + "عيادات الصدريه": ("hh_n_internal_medicine_department", "Pulmonology"), + "قسم الصدرية": ("hh_n_internal_medicine_department", "Pulmonology"), + "عيادات الغدد الصماء": ("hh_n_internal_medicine_department", "Endocrinology"), + "عيادات المخ والأعصاب": ("hh_n_internal_medicine_department", "Neurology"), + "عيادات الكلى": ("hh_n_internal_medicine_department", "Nephrology"), + "عيادات الطب النفسي": ("hh_n_internal_medicine_department", "Psychiatry"), + "وحدة المناظير": ("hh_n_internal_medicine_department", "Endoscopy"), + "تمريض تنويم الباطنية": ("hh_n_nursing_department", "Medical Ward"), + "تنويم الباطنية": ("hh_n_nursing_department", "Medical Ward"), + "عيادات الباطنية": ("hh_n_internal_medicine_department", "Internal Medicine"), + # Emergency + "استقبال الطوارئ": ("hh_n_emergency_administrative_department", "Emergency Reception"), + "استقبال الطواريء": ("hh_n_emergency_administrative_department", "Emergency Reception"), + "استقبال العيادات الخارجية": ("hh_n_outpatient_department", "Outpatient Reception"), + # Critical Care + "وحدة العناية المركزة": ("hh_n_critical_care_department", "ICU"), + "تمريض وحدة العناية المركزة": ("hh_n_nursing_department", "ICU/CCU"), + "وحدة العناية المتوسطة": ("hh_n_critical_care_department", "ICU Stepdown"), + "تمريض وحدة العناية المتوسطة": ("hh_n_nursing_department", "ICU/CCU Stepdown"), + "تمريض وحدة طويلي الإقامة": ("hh_n_nursing_department", "LTACU"), + "وحدة مرضى طويلي الإقامة": ("hh_n_internal_medicine_department", "Long-Term Acute Care Unit (LTACU)"), + # Nursing + "تمريض الطوارئ": ("hh_n_nursing_department", "Emergency"), + "تمريض الطوارى": ("hh_n_nursing_department", "Emergency"), + "تمريض العيادات الخارجية": ("hh_n_nursing_department", "Outpatient"), + "تمريض العيادات الخارجيه": ("hh_n_nursing_department", "Outpatient"), + "تمريض قسم العمليات": ("hh_n_nursing_department", "Main OR"), + "تمريض قسم الافاقة": ("hh_n_nursing_department", "Recovery"), + "تمريض قسم الإفاقة": ("hh_n_nursing_department", "Recovery"), + "تمريض قسم عمليات الولادة": ("hh_n_nursing_department", "OB/OR"), + # Pediatric + "تنويم الأطفال": ("hh_n_pediatric_department", "Pediatric Ward"), + "تنويم الاطفال": ("hh_n_pediatric_department", "Pediatric Ward"), + "تمريض تنويم الأطفال": ("hh_n_nursing_department", "Pediatric Ward"), + "تمريض تنويم الاطفال": ("hh_n_nursing_department", "Pediatric Ward"), + "عيادات الأطفال": ("hh_n_pediatric_department", "General Pediatrics"), + "وحدة العناية المركزة لحديثي الولادة": ("hh_n_pediatric_department", "NICU"), + "تمريض وحدة العناية المركزة - أطفال": ("hh_n_nursing_department", "PICU"), + "وحدة العناية المركزة للأطفال": ("hh_n_pediatric_department", "PICU"), + "تمريض وحدة العناية لحديثي الولادة": ("hh_n_nursing_department", "NICU"), + "تمريض الحضانة": ("hh_n_nursing_department", "Nursery"), + "قسم الحضانة": ("hh_n_pediatric_department", "NURSERY"), + "عيادات الصدرية (أطفال)": ("hh_n_pediatric_department", "Pediatric Pulmonology"), + "عيادات جراحة عظام (أطفال)": ("hh_n_pediatric_department", "Pediatric Orthopedic Surgery"), + "عيادات الأنف والأذن والحنجرة (أطفال)": ("hh_n_pediatric_department", "Pediatric Otolaryngology (ENT)"), + "عيادات الجهاز الهمضي والمناظير (أطفال)": ("hh_n_pediatric_department", "Pediatric Gastroenterology"), + "عيادات قلب (أطفال)": ("hh_n_pediatric_department", "Pediatric Cardiology"), + "عيادات المخ والأعصاب (أطفال)": ("hh_n_pediatric_department", "Pediatric Neurology"), + "عيادات جراحة المسالك البولية (أطفال)": ("hh_n_pediatric_department", "Pediatric Urology"), + "عيادات الجراحة العامة (أطفال)": ("hh_n_pediatric_department", "Pediatric General Surgery"), + "عيادات جراحة الختان": ("hh_n_pediatric_department", "Circumcision"), + # Outpatient + "إدارة العيادات الخارجيه": ("hh_n_outpatient_department", "Management"), + # Medical Ancillary + "فني اختبار السمع": ("hh_n_medical_ancillary_services_department", "Swallowing Speech and Hearing Unit"), + "فني تخطيط القلب": ("hh_n_medical_ancillary_services_department", "Cardiac Physiology Services"), + "فني تخطيط المخ والأعصاب": ("hh_n_medical_ancillary_services_department", "Neurophysiology Services"), + "فني تخطيط المخ والاعصاب": ("hh_n_medical_ancillary_services_department", "Neurophysiology Services"), + "فني دراسة الجهد القلب": ("hh_n_medical_ancillary_services_department", "Cardiac Physiology Services"), + "قسم العلاج الطبيعي": ("hh_n_medical_ancillary_services_department", "Physical Therapy"), + "استقبال العلاج الطبيعي": ("hh_n_medical_ancillary_services_department", "Physical Therapy"), + "العلاج الطبيعي": ("hh_n_medical_ancillary_services_department", "Physical Therapy"), + "قسم العلاج التنفسي": ("hh_n_medical_ancillary_services_department", "Respiratory Therapy"), + "فني فحص النظر": ("hh_n_medical_ancillary_services_department", "Optometrist Services"), + "قسم التغذية": ("hh_n_medical_ancillary_services_department", "Clinical Nutrition"), + # Anesthesia + "تمريض التخدير": ("hh_n_nursing_department", "Anesthesia"), + # Radiology + "استقبال الأشعة": ("hh_n_radiology_department", "General"), + # Inpatient + "قسم الخدمة الإجتماعية": ("hh_n_inpatient_department", "Social Worker"), + # Contact Center + "قسم المواعيد": ("hh_n_contact_center_department", "Appointment Office"), + "قسم السنترال": ("hh_n_contact_center_department", "Operator Office"), + # Dermatology (1 section) + "عيادات الجلدية": ("hh_n_dermatology_department", "Dermatology"), + "عيادات الجلديه": ("hh_n_dermatology_department", "Dermatology"), + # Financial (1 section) + "قسم المالية": ("hh_n_financial_collection___claims_department", "Financial Collection & Claims"), + # Security + "قسم الأمن": ("hh_n_security_department", "Security Guards"), + "قسم الامن": ("hh_n_security_department", "Security Guards"), + # Radiology (generic → General) + "قسم الأشعة": ("hh_n_radiology_department", "General"), + "قسم الاشعة": ("hh_n_radiology_department", "General"), + "الأشعة": ("hh_n_radiology_department", "General"), +} + + +def resolve_department(arabic_name: str, hospital_code: str = "HH-N"): + """ + Resolve Arabic sub-department name to a Department object. + + Tries exact match first, then normalized match. + Returns Department instance or None. + """ + from apps.organizations.models import Department + + if not arabic_name: + return None + + name = str(arabic_name).strip() + + # Direct lookup + code = ARABIC_TO_DEPT_CODE.get(name) + if not code: + # Try normalized + normalized = normalize_arabic(name) + code = ARABIC_TO_DEPT_CODE.get(normalized) + + if not code: + # Try building a normalized version of all keys + for key, dept_code in ARABIC_TO_DEPT_CODE.items(): + if normalize_arabic(key) == normalize_arabic(name): + code = dept_code + break + + if code: + try: + return Department.objects.select_related("hospital").get( + hospital__code=hospital_code, code=code + ) + except Department.DoesNotExist: + return None + + return None + + +def resolve_section(arabic_name: str, hospital_code: str = "HH-N"): + """ + Resolve Arabic sub-department name to a (Department, Section) tuple. + + Returns (Department, Section) if section can be identified, + (Department, None) if only department is known, or (None, None). + """ + from apps.organizations.models import Department, Section + + if not arabic_name: + return None, None + + name = str(arabic_name).strip() + + entry = ARABIC_TO_SECTION.get(name) + if not entry: + normalized = normalize_arabic(name) + for key, val in ARABIC_TO_SECTION.items(): + if normalize_arabic(key) == normalized: + entry = val + break + + if entry: + dept_code, section_name = entry + try: + dept = Department.objects.select_related("hospital").get( + hospital__code=hospital_code, code=dept_code + ) + section = Section.objects.filter( + department=dept, name_en=section_name + ).first() + return dept, section + except Department.DoesNotExist: + return None, None + + dept = resolve_department(name, hospital_code) + return dept, None diff --git a/apps/complaints/management/commands/backfill_sent_to_department.py b/apps/complaints/management/commands/backfill_sent_to_department.py new file mode 100644 index 0000000..dcd301d --- /dev/null +++ b/apps/complaints/management/commands/backfill_sent_to_department.py @@ -0,0 +1,41 @@ +from django.core.management.base import BaseCommand +from django.db.models import F +from apps.complaints.models import Complaint, ComplaintInvolvedDepartment +from apps.observations.models import Observation +from apps.complaints.models import Inquiry + + +class Command(BaseCommand): + help = "Backfill sent_to_department=True on all records that have a department assigned" + + def handle(self, *args, **options): + c_updated = Complaint.objects.filter( + department__isnull=False, sent_to_department=False + ).update( + sent_to_department=True, + sent_to_department_at=F("created_at"), + ) + self.stdout.write(f"Complaints updated: {c_updated}") + + o_updated = Observation.objects.filter( + assigned_department__isnull=False, sent_to_department=False + ).update( + sent_to_department=True, + sent_to_department_at=F("created_at"), + ) + self.stdout.write(f"Observations updated: {o_updated}") + + i_updated = Inquiry.objects.filter( + department__isnull=False, sent_to_department=False + ).update( + sent_to_department=True, + sent_to_department_at=F("created_at"), + ) + self.stdout.write(f"Inquiries updated: {i_updated}") + + cid_updated = ComplaintInvolvedDepartment.objects.filter( + sent=False, complaint__department__isnull=False + ).update(sent=True) + self.stdout.write(f"Involved departments updated: {cid_updated}") + + self.stdout.write(self.style.SUCCESS("Done")) diff --git a/apps/complaints/management/commands/complaint_source_mapping.py b/apps/complaints/management/commands/complaint_source_mapping.py index 9dbeafa..f6fe566 100644 --- a/apps/complaints/management/commands/complaint_source_mapping.py +++ b/apps/complaints/management/commands/complaint_source_mapping.py @@ -68,3 +68,16 @@ def resolve_px_source(source_value: str) -> "PXSource | None": return PXSource.objects.get(code=code) except PXSource.DoesNotExist: return None + + +EXTERNAL_SOURCE_CODES = {"MOH", "CCHI", "CHI"} + + +def get_complaint_source_type(px_source) -> str: + """ + Determine complaint_source_type based on PXSource. + Only MOH/CHI/CCHI are external; everything else is internal. + """ + if px_source and px_source.code in EXTERNAL_SOURCE_CODES: + return "external" + return "internal" diff --git a/apps/complaints/management/commands/import_2025_complaints_basic.py b/apps/complaints/management/commands/import_2025_complaints_basic.py index 34ec78c..59757da 100644 --- a/apps/complaints/management/commands/import_2025_complaints_basic.py +++ b/apps/complaints/management/commands/import_2025_complaints_basic.py @@ -19,13 +19,14 @@ from django.utils import timezone from apps.accounts.models import User from apps.complaints.models import Complaint -from apps.organizations.models import Hospital, Location, MainSection, SubSection +from apps.organizations.models import Hospital, LegacyLocation, LegacyMainSection, LegacySubSection -from .complaint_source_mapping import resolve_px_source +from .complaint_source_mapping import resolve_px_source, get_complaint_source_type +from .arabic_dept_mapping import resolve_section as resolve_dept_and_section logger = logging.getLogger(__name__) -DEFAULT_HOSPITAL_CODE = "NUZHA" +DEFAULT_HOSPITAL_CODE = "HH-N" # Header aliases: list of possible names in Excel for each field HEADER_ALIASES = { @@ -37,12 +38,39 @@ HEADER_ALIASES = { "sub_dept_name": ["القسم الفرعي"], "date_received": ["تاريخ إستلام الشكوى"], "data_entry_person": ["المدخل"], + # Timeline columns + "form_sent_date": ["إرسال نموذج الشكوى"], + "activated_date": ["تفعيل الشكوى"], + "sent_date": ["تم ارسال الشكوى"], + "first_reminder": ["First Reminder Sent"], + "second_reminder": ["Second Reminder Sent"], + "escalated_date": ["Escalated"], + "closed_date": ["Closed"], + "resolved_date": ["Resolved"], "response_date": ["تاريخ الرد"], + # Complaint details "staff_name": ["اسم الشخص المشتكى عليه - ان وجد", "اسم الشخص المشتكى عليه"], + "complaint_subject": ["موضوع الشكوى الأساسية"], "description_ar": ["الشكوى باختصار (عربي)", "محتوى الشكوى (عربي)"], "description_en": ["الشكوى باختصار English", "محتوى الشكوى (English)"], - "satisfaction": ["توثيق تذكيرات للقسم المشتكى عليه"], - "reminder_date": ["تاريخ التذكير"], + "satisfaction": ["Satisfied/Dissatisfied"], + "rightful_side": ["The Rightful Side"], + # Rich data + "delay_reason": ["سبب تأخير القسم بالرد"], + "closure_delay": ["سبب تأخير اغلاق الشكوى خلال 72 ساعه"], + "action_taken": ["الاجراء المتخذ من قبل القسم المعني"], + "action_result": ["نتيجة الاجراء المتخذ بعد التحقيق"], + "recommendation": ["Recommendation/Action plan"], + "solutions": ["حلول واقتراحات"], +} + +SATISFACTION_MAP = { + "satisfied": "satisfied", + "dissatisfied": "dissatisfied", + "no response": "no_response", + "no_response": "no_response", + "neutral": "neutral", + "escalated": "escalated", } MONTH_MAP = { @@ -68,15 +96,17 @@ class Command(BaseCommand): parser.add_argument("excel_file", type=str) parser.add_argument("--sheet", type=str, default="JAN") parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--hospital-code", type=str, default=DEFAULT_HOSPITAL_CODE) def handle(self, *args, **options): self.excel_file = options["excel_file"] self.sheet_name = options["sheet"] self.dry_run = options["dry_run"] + self.hospital_code = options["hospital_code"] self.hospital = self._load_hospital() if not self.hospital: - raise CommandError(f'Hospital "{DEFAULT_HOSPITAL_CODE}" not found') + raise CommandError(f'Hospital "{self.hospital_code}" not found') self.stdout.write(f"Using hospital: {self.hospital.name}") @@ -104,13 +134,14 @@ class Command(BaseCommand): self.stats = {"processed": 0, "success": 0, "failed": 0} self.errors = [] self.used_refs = set() + self.unmapped_arabic_depts = {} self._process_sheet() self._print_report() def _load_hospital(self) -> Optional[Hospital]: try: - return Hospital.objects.get(code=DEFAULT_HOSPITAL_CODE) + return Hospital.objects.get(code=self.hospital_code) except Hospital.DoesNotExist: return None @@ -161,27 +192,72 @@ class Command(BaseCommand): created_at = date_received or timezone.now() if created_at and timezone.is_naive(created_at): created_at = timezone.make_aware(created_at) + + # Parse timeline dates + form_sent_date = self._parse_datetime(row_data.get("form_sent_date")) + activated_date = self._parse_datetime(row_data.get("activated_date")) + sent_date = self._parse_datetime(row_data.get("sent_date")) + first_reminder = self._parse_datetime(row_data.get("first_reminder")) + second_reminder = self._parse_datetime(row_data.get("second_reminder")) + escalated_date = self._parse_datetime(row_data.get("escalated_date")) + closed_date = self._parse_datetime(row_data.get("closed_date")) + resolved_date = self._parse_datetime(row_data.get("resolved_date")) response_date = self._parse_datetime(row_data.get("response_date")) - reminder_date = self._parse_datetime(row_data.get("reminder_date")) location = self._resolve_location(row_data.get("location_name")) main_section = self._resolve_section(row_data.get("main_dept_name")) subsection = self._resolve_subsection(row_data.get("sub_dept_name")) + sub_dept_raw = row_data.get("sub_dept_name") + dept, section_obj = resolve_dept_and_section(sub_dept_raw, self.hospital_code) + if not dept and sub_dept_raw and isinstance(sub_dept_raw, str) and sub_dept_raw.strip(): + name = sub_dept_raw.strip() + self.unmapped_arabic_depts[name] = self.unmapped_arabic_depts.get(name, 0) + 1 + assigned_to_user = self._get_or_create_data_entry_user(row_data.get("data_entry_person")) + # Determine status from timeline dates status = "open" - if response_date: + if closed_date: + status = "closed" + elif resolved_date: status = "resolved" + elif escalated_date: + status = "in_progress" + + # Normalize satisfaction + satisfaction_raw = str(row_data.get("satisfaction") or "").lower().strip() + satisfaction_val = SATISFACTION_MAP.get(satisfaction_raw, "") + + # Normalize rightful side + rightful_side = str(row_data.get("rightful_side") or "").lower().strip() + resolution_outcome = "" + if rightful_side in ["patient", "hospital", "other"]: + resolution_outcome = rightful_side + + # Rich text fields + complaint_subject = str(row_data.get("complaint_subject") or "").strip() + delay_reason = str(row_data.get("delay_reason") or "").strip() + closure_delay = str(row_data.get("closure_delay") or "").strip() + action_taken = str(row_data.get("action_taken") or "").strip() + action_result = str(row_data.get("action_result") or "").strip() + recommendation_raw = str(row_data.get("recommendation") or "").strip() + solutions = str(row_data.get("solutions") or "").strip() + recommendation_combined = recommendation_raw or solutions or "" if not self.dry_run: with transaction.atomic(): complaint = Complaint.objects.create( reference_number=ref_num, hospital=self.hospital, - location=location, - main_section=main_section, - subsection=subsection, + department=dept, + section=section_obj, + legacy_location=location, + legacy_main_section=main_section, + legacy_subsection=subsection, + old_location_raw=str(row_data.get("location_name") or "")[:200], + old_main_section_raw=str(row_data.get("main_dept_name") or "")[:200], + old_subsection_raw=str(row_data.get("sub_dept_name") or "")[:200], title=self._build_title(row_data), description=self._build_description(row_data), patient_name="Unknown", @@ -195,18 +271,36 @@ class Command(BaseCommand): classification_obj=None, status=status, assigned_to=assigned_to_user, - resolved_by=assigned_to_user if response_date else None, - due_at=created_at + timedelta(hours=48), - explanation_requested=bool(date_received), - explanation_requested_at=date_received, + resolved_by=assigned_to_user if resolved_date else None, + resolution_outcome=resolution_outcome, + form_sent_at=form_sent_date, + activated_at=activated_date, + forwarded_to_dept_at=sent_date, + reminder_sent_at=first_reminder, + second_reminder_sent_at=second_reminder, + escalated_at=escalated_date, + closed_at=closed_date, + resolved_at=resolved_date, + explanation_requested=bool(sent_date), + explanation_requested_at=sent_date, explanation_received_at=response_date, - reminder_sent_at=reminder_date, + due_at=created_at + timedelta(hours=48), source=px_source, + complaint_source_type=get_complaint_source_type(px_source), + satisfaction=satisfaction_val, + complaint_subject=complaint_subject, + explanation_delay_reason=delay_reason, + delay_reason_closure=closure_delay or "", + action_taken_by_dept=action_taken, + action_result=action_result, + recommendation_action_plan=recommendation_combined, metadata={ - "import_source": "2025_excel_basic", + "import_source": "2025_excel", "original_sheet": self.sheet_name, "complaint_num": row_data.get("complaint_num"), }, + sent_to_department=bool(dept), + sent_to_department_at=created_at if dept else None, ) Complaint.objects.filter(pk=complaint.pk).update(created_at=created_at) @@ -276,20 +370,20 @@ class Command(BaseCommand): return None return None - def _resolve_location(self, name_ar: str) -> Optional[Location]: + def _resolve_location(self, name_ar: str) -> Optional[LegacyLocation]: if not name_ar: return None - return Location.objects.filter(name_ar=name_ar).first() + return LegacyLocation.objects.filter(name_ar=name_ar).first() - def _resolve_section(self, name_ar: str) -> Optional[MainSection]: + def _resolve_section(self, name_ar: str) -> Optional[LegacyMainSection]: if not name_ar: return None - return MainSection.objects.filter(name_ar=name_ar).first() + return LegacyMainSection.objects.filter(name_ar=name_ar).first() - def _resolve_subsection(self, name_ar: str) -> Optional[SubSection]: + def _resolve_subsection(self, name_ar: str) -> Optional[LegacySubSection]: if not name_ar: return None - return SubSection.objects.filter(name_ar=name_ar).first() + return LegacySubSection.objects.filter(name_ar=name_ar).first() def _get_or_create_data_entry_user(self, arabic_name: str) -> Optional[User]: if not arabic_name: @@ -355,6 +449,12 @@ class Command(BaseCommand): self.stdout.write(f"Success: {self.stats['success']}") self.stdout.write(f"Failed: {self.stats['failed']}") + if self.unmapped_arabic_depts: + self.stdout.write("\n--- Unmapped Arabic Sub-Departments ---") + sorted_unmapped = sorted(self.unmapped_arabic_depts.items(), key=lambda x: -x[1]) + for name, count in sorted_unmapped: + self.stdout.write(f" {count:4d}x {name}") + if self.errors: self.stdout.write(f"\nErrors: {len(self.errors)}") for error in self.errors[:5]: diff --git a/apps/complaints/management/commands/import_all_complaints.py b/apps/complaints/management/commands/import_all_complaints.py new file mode 100644 index 0000000..a84498d --- /dev/null +++ b/apps/complaints/management/commands/import_all_complaints.py @@ -0,0 +1,99 @@ +""" +Import ALL historical complaints from all Excel files (2022-2025). + +Usage: + python manage.py import_all_complaints + python manage.py import_all_complaints --hospital-code=HH-N + python manage.py import_all_complaints --dry-run +""" + +import os + +from django.core.management.base import BaseCommand, CommandError +from django.core.management import call_command + + +COMPLAINT_FILES = [ + { + "year": 2022, + "path": "data/Complaints Report - 2022.xlsx", + "sheets": ["AUG 2022 ", "SEP 2022 ", "OCT 2022", "NOV 2022", "DEC 2022"], + "importer": "import_historical_complaints", + }, + { + "year": 2023, + "path": "data/Complaints Report - 2023.xlsx", + "sheets": [ + "January 2023 ", "February 2023", "March 2023", "April 2023 ", + "May 2023", "June 2023", "July 2023", "August 2023", + "September 2023", "October 2023", "November 2023", "December 2023", + ], + "importer": "import_historical_complaints", + }, + { + "year": 2024, + "path": "data/Complaints Report - 2024.xlsx", + "sheets": [ + "January 2024", "February 2024", "March 2024 ", "April 2024", + "May 2024", "June 2024", "July 2024", "August 2024", + "September 2024", "October 2024", "November 2024", "December 2024", + ], + "importer": "import_historical_complaints", + }, + { + "year": 2025, + "path": "data/Complaints Report - 2025.xlsx", + "sheets": ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"], + "importer": "import_2025_complaints_basic", + }, +] + + +class Command(BaseCommand): + help = "Import all historical complaints from 2022-2025 Excel files" + + def add_arguments(self, parser): + parser.add_argument("--hospital-code", type=str, default="HH-N") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--year", type=int, help="Import only this year") + + def handle(self, *args, **options): + hospital_code = options["hospital_code"] + dry_run = options["dry_run"] + year_filter = options.get("year") + + total_success = 0 + total_failed = 0 + + for file_info in COMPLAINT_FILES: + if year_filter and file_info["year"] != year_filter: + continue + + path = file_info["path"] + if not os.path.exists(path): + self.stderr.write(self.style.ERROR(f"File not found: {path}")) + continue + + self.stdout.write(self.style.SUCCESS(f"\n{'='*60}")) + self.stdout.write(self.style.SUCCESS(f"YEAR {file_info['year']}: {path}")) + self.stdout.write(self.style.SUCCESS(f"{'='*60}")) + + for sheet in file_info["sheets"]: + self.stdout.write(f"\n Sheet: {sheet}") + try: + call_command( + file_info["importer"], + path, + sheet=sheet, + hospital_code=hospital_code, + dry_run=dry_run, + stdout=self.stdout, + stderr=self.stderr, + ) + except Exception as e: + self.stderr.write(self.style.ERROR(f" FAILED: {e}")) + total_failed += 1 + + self.stdout.write(self.style.SUCCESS(f"\n\n{'='*60}")) + self.stdout.write(self.style.SUCCESS("ALL IMPORTS COMPLETE")) + self.stdout.write(self.style.SUCCESS(f"{'='*60}")) diff --git a/apps/complaints/management/commands/import_historical_complaints.py b/apps/complaints/management/commands/import_historical_complaints.py index 1ddb2d4..f10d69c 100644 --- a/apps/complaints/management/commands/import_historical_complaints.py +++ b/apps/complaints/management/commands/import_historical_complaints.py @@ -18,7 +18,14 @@ from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.utils import timezone -from apps.organizations.models import Hospital, Location, MainSection, SubSection, Staff +from apps.organizations.models import ( + Department, + Hospital, + LegacyLocation, + LegacyMainSection, + LegacySubSection, + Staff, +) from apps.complaints.models import Complaint, ComplaintCategory from apps.accounts.models import User @@ -30,12 +37,12 @@ from .complaint_taxonomy_mapping import ( get_mapped_category, is_taxonomy_mapped, ) -from .complaint_source_mapping import resolve_px_source +from .complaint_source_mapping import resolve_px_source, get_complaint_source_type +from .arabic_dept_mapping import resolve_section as resolve_dept_and_section logger = logging.getLogger(__name__) -# Default hospital code for all imported complaints -DEFAULT_HOSPITAL_CODE = "NUZHA" +DEFAULT_HOSPITAL_CODE = "HH-N" # Column mapping: field_name -> column_number (1-based) COLUMN_MAPPING = { @@ -47,6 +54,18 @@ COLUMN_MAPPING = { "sub_dept_name": 8, # القسم الفرعي "date_received": 9, # تاريخ إستلام الشكوى "data_entry_person": 10, # المدخل (Data Entry Person) + # Timeline columns + "form_sent_date": 12, # إرسال نموذج الشكوى (Send Complaint Form) + "activated_date": 17, # تفعيل الشكوى (Activate Complaint) + "date_sent": 20, # تم ارسال الشكوى (Complaint Sent/Forwarded to Dept) + "first_reminder": 24, # First Reminder Sent + "second_reminder": 28, # Second Reminder Sent + "escalated_date": 32, # Escalated + "escalation_reason": 35, # Reason of Escalation + "closed_date": 37, # Closed + "response_date": 41, # تاريخ الرد (Response Date) + "resolved_date": 44, # Resolved + # Complaint details "accused_staff_id": 48, # ID (Employee ID) "accused_staff_name": 49, # اسم الشخص المشتكى عليه - ان وجد "domain": 50, # Domain @@ -57,14 +76,16 @@ COLUMN_MAPPING = { "description_en": 55, # محتوى الشكوى (English) "satisfaction": 56, # Satisfied/Dissatisfied "rightful_side": 57, # The Rightful Side - # Timeline columns - "date_sent": 20, # تم ارسال الشكوى (Complaint Sent/Activated) - "first_reminder": 24, # First Reminder Sent - "second_reminder": 28, # Second Reminder Sent - "escalated_date": 32, # Escalated - "closed_date": 37, # Closed - "resolved_date": 44, # Resolved - "response_date": 41, # تاريخ الرد (Response Date - for explanation received) + "recommendation": 58, # Recommendation/Action plan +} + +SATISFACTION_MAP = { + "satisfied": "satisfied", + "dissatisfied": "dissatisfied", + "no response": "no_response", + "no_response": "no_response", + "neutral": "neutral", + "escalated": "escalated", } # Month mapping for reference numbers @@ -101,17 +122,18 @@ class Command(BaseCommand): ) parser.add_argument("--dry-run", action="store_true", help="Preview without saving to database") parser.add_argument("--start-row", type=int, default=3, help="First data row (default: 3, skipping header)") + parser.add_argument("--hospital-code", type=str, default=DEFAULT_HOSPITAL_CODE, help="Hospital code") def handle(self, *args, **options): self.excel_file = options["excel_file"] self.sheet_name = options["sheet"] self.dry_run = options["dry_run"] self.start_row = options["start_row"] + self.hospital_code = options["hospital_code"] - # Load hospital self.hospital = self._load_hospital() if not self.hospital: - raise CommandError(f'Hospital with code "{DEFAULT_HOSPITAL_CODE}" not found') + raise CommandError(f'Hospital with code "{self.hospital_code}" not found') self.stdout.write(self.style.SUCCESS(f"Using hospital: {self.hospital.name}")) @@ -145,6 +167,7 @@ class Command(BaseCommand): self.unmapped_taxonomy = set() self.unmatched_locations = set() self.unmatched_departments = set() + self.unmapped_arabic_depts = {} # Cache for used reference numbers to avoid DB queries self.used_refs = set() @@ -158,7 +181,7 @@ class Command(BaseCommand): def _load_hospital(self) -> Optional[Hospital]: """Load default hospital by code.""" try: - return Hospital.objects.get(code=DEFAULT_HOSPITAL_CODE) + return Hospital.objects.get(code=self.hospital_code) except Hospital.DoesNotExist: return None @@ -209,6 +232,13 @@ class Command(BaseCommand): main_section = self._resolve_section(row_data.get("main_dept_name")) subsection = self._resolve_subsection(row_data.get("sub_dept_name")) + # Resolve department and section FKs from Arabic sub-dept name + sub_dept_raw = row_data.get("sub_dept_name") + dept, section_obj = resolve_dept_and_section(sub_dept_raw, self.hospital_code) + if not dept and sub_dept_raw and isinstance(sub_dept_raw, str) and sub_dept_raw.strip(): + name = sub_dept_raw.strip() + self.unmapped_arabic_depts[name] = self.unmapped_arabic_depts.get(name, 0) + 1 + # Determine status status = self._determine_status(row_data) @@ -235,6 +265,8 @@ class Command(BaseCommand): assigned_to_user = self._get_or_create_data_entry_user(data_entry_person) # Parse timeline dates + form_sent_date = self._parse_datetime(row_data.get("form_sent_date")) + activated_date = self._parse_datetime(row_data.get("activated_date")) date_sent = self._parse_datetime(row_data.get("date_sent")) first_reminder = self._parse_datetime(row_data.get("first_reminder")) second_reminder = self._parse_datetime(row_data.get("second_reminder")) @@ -258,15 +290,26 @@ class Command(BaseCommand): if rightful_side in ["patient", "hospital", "other"]: resolution_outcome = rightful_side + # Normalize satisfaction + satisfaction_raw = str(row_data.get("satisfaction") or "").lower().strip() + satisfaction_val = SATISFACTION_MAP.get(satisfaction_raw, "") + + # Recommendation / action plan + recommendation = str(row_data.get("recommendation") or "").strip() + if not self.dry_run: - # Create complaint with transaction.atomic(): complaint = Complaint.objects.create( reference_number=ref_num, hospital=self.hospital, - location=location, - main_section=main_section, - subsection=subsection, + department=dept, + section=section_obj, + legacy_location=location, + legacy_main_section=main_section, + legacy_subsection=subsection, + old_location_raw=str(row_data.get("location_name") or "")[:200], + old_main_section_raw=str(row_data.get("main_dept_name") or "")[:200], + old_subsection_raw=str(row_data.get("sub_dept_name") or "")[:200], title=self._build_title(row_data), description=self._build_description(row_data), patient_name="Unknown", @@ -282,23 +325,26 @@ class Command(BaseCommand): assigned_to=assigned_to_user, resolved_by=assigned_to_user, resolution_outcome=resolution_outcome, - # Timeline fields - activated_at=date_sent, + form_sent_at=form_sent_date, + activated_at=activated_date, + forwarded_to_dept_at=date_sent, reminder_sent_at=first_reminder, second_reminder_sent_at=second_reminder, escalated_at=escalated_date, closed_at=closed_date, resolved_at=resolved_date, - # Explanation tracking explanation_requested=explanation_requested, explanation_requested_at=explanation_requested_at, explanation_received_at=explanation_received_at, due_at=created_at + timedelta(hours=48), source=px_source, + complaint_source_type=get_complaint_source_type(px_source), + satisfaction=satisfaction_val, + recommendation_action_plan=recommendation, metadata=self._build_metadata(row_data, ref_num), + sent_to_department=bool(dept), + sent_to_department_at=created_at if dept else None, ) - - # Update created_at to historical date (can't set during create due to auto_now_add) Complaint.objects.filter(pk=complaint.pk).update(created_at=created_at) self.stats["success"] += 1 @@ -407,30 +453,30 @@ class Command(BaseCommand): return None return None - def _resolve_location(self, name_ar: str) -> Optional[Location]: + def _resolve_location(self, name_ar: str) -> Optional[LegacyLocation]: """Resolve location by Arabic name.""" if not name_ar: return None - location = Location.objects.filter(name_ar=name_ar).first() + location = LegacyLocation.objects.filter(name_ar=name_ar).first() if not location: self.unmatched_locations.add(name_ar) return location - def _resolve_section(self, name_ar: str) -> Optional[MainSection]: + def _resolve_section(self, name_ar: str) -> Optional[LegacyMainSection]: """Resolve main section/department by Arabic name.""" if not name_ar: return None # Try Section model - section = MainSection.objects.filter(name_ar=name_ar).first() + section = LegacyMainSection.objects.filter(name_ar=name_ar).first() if not section: self.unmatched_departments.add(name_ar) return section - def _resolve_subsection(self, name_ar: str) -> Optional[SubSection]: + def _resolve_subsection(self, name_ar: str) -> Optional[LegacySubSection]: """Resolve subsection by Arabic name.""" if not name_ar: return None - return SubSection.objects.filter(name_ar=name_ar).first() + return LegacySubSection.objects.filter(name_ar=name_ar).first() def _resolve_staff_by_id(self, employee_id: str) -> Optional[Staff]: """Resolve staff by employee ID.""" @@ -630,11 +676,17 @@ class Command(BaseCommand): self.stdout.write(f" - {loc}") if self.unmatched_departments: - self.stdout.write("\n--- Unmatched Departments ---") + self.stdout.write("\n--- Unmatched Legacy Departments ---") self.stdout.write("No MainSection/SubSection found with these name_ar values:") for dept in sorted(self.unmatched_departments): self.stdout.write(f" - {dept}") + if self.unmapped_arabic_depts: + self.stdout.write("\n--- Unmapped Arabic Sub-Departments ---") + sorted_unmapped = sorted(self.unmapped_arabic_depts.items(), key=lambda x: -x[1]) + for name, count in sorted_unmapped: + self.stdout.write(f" {count:4d}x {name}") + if self.errors: self.stdout.write("\n--- Errors ---") self.stdout.write(f"Total errors: {len(self.errors)}") diff --git a/apps/complaints/migrations/0001_initial.py b/apps/complaints/migrations/0001_initial.py index 522d82c..41e98cf 100644 --- a/apps/complaints/migrations/0001_initial.py +++ b/apps/complaints/migrations/0001_initial.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -696,8 +696,8 @@ class Migration(migrations.Migration): ('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='complaints', to='organizations.department')), ('escalated_ovr_by', models.ForeignKey(blank=True, help_text='User who escalated as OVR', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='escalated_ovr_complaints', to=settings.AUTH_USER_MODEL)), ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='complaints', to='organizations.hospital')), - ('location', models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.location')), - ('main_section', models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.mainsection')), + ('location', models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacylocation')), + ('main_section', models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacymainsection')), ('patient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='complaints', to='organizations.patient')), ('reopened_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reopened_complaints', to=settings.AUTH_USER_MODEL)), ('reopened_from', models.ForeignKey(blank=True, help_text='Original complaint this was reopened from', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reopenings', to='complaints.complaint')), diff --git a/apps/complaints/migrations/0003_initial.py b/apps/complaints/migrations/0003_initial.py index ec6f9c2..1e6257a 100644 --- a/apps/complaints/migrations/0003_initial.py +++ b/apps/complaints/migrations/0003_initial.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ ('complaints', '0002_initial'), - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), ('px_sources', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -30,7 +30,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='complaint', name='subsection', - field=models.ForeignKey(blank=True, help_text='Subsection within the section', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.subsection'), + field=models.ForeignKey(blank=True, help_text='Subsection within the section', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacysubsection'), ), migrations.AddField( model_name='complaintadverseaction', @@ -275,12 +275,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='governmentticket', name='location', - field=models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.location'), + field=models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacylocation'), ), migrations.AddField( model_name='governmentticket', name='main_section', - field=models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.mainsection'), + field=models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacymainsection'), ), migrations.AddField( model_name='governmentticket', @@ -290,7 +290,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='governmentticket', name='subsection', - field=models.ForeignKey(blank=True, help_text='Subsection within the section', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.subsection'), + field=models.ForeignKey(blank=True, help_text='Subsection within the section', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacysubsection'), ), migrations.AddField( model_name='inquiry', @@ -345,12 +345,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='inquiry', name='location', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.location'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacylocation'), ), migrations.AddField( model_name='inquiry', name='main_section', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.mainsection'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacymainsection'), ), migrations.AddField( model_name='inquiry', @@ -375,7 +375,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='inquiry', name='subsection', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.subsection'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacysubsection'), ), migrations.AddField( model_name='inquiry', diff --git a/apps/complaints/migrations/0004_add_cancelled_partially_resolved_timestamps.py b/apps/complaints/migrations/0004_add_cancelled_partially_resolved_timestamps.py new file mode 100644 index 0000000..ce8f4a3 --- /dev/null +++ b/apps/complaints/migrations/0004_add_cancelled_partially_resolved_timestamps.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.1 on 2026-05-13 08:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0003_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='complaint', + name='cancelled_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='complaint', + name='cancelled_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cancelled_complaints', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='complaint', + name='partially_resolved_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='complaint', + name='partially_resolved_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='partially_resolved_complaints', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/complaints/migrations/0005_add_sent_to_department_fields.py b/apps/complaints/migrations/0005_add_sent_to_department_fields.py new file mode 100644 index 0000000..bbba866 --- /dev/null +++ b/apps/complaints/migrations/0005_add_sent_to_department_fields.py @@ -0,0 +1,43 @@ +# Generated by Django 6.0.1 on 2026-05-16 16:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0004_add_cancelled_partially_resolved_timestamps'), + ] + + operations = [ + migrations.AddField( + model_name='complaint', + name='sent_to_department', + field=models.BooleanField(default=False, help_text='Whether this complaint has been sent to the primary department for visibility'), + ), + migrations.AddField( + model_name='complaint', + name='sent_to_department_at', + field=models.DateTimeField(blank=True, help_text='When the complaint was sent to the primary department', null=True), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='sent', + field=models.BooleanField(default=False, help_text='Whether this department has been sent the complaint'), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='sent_at', + field=models.DateTimeField(blank=True, help_text='When the complaint was sent to this department', null=True), + ), + migrations.AddField( + model_name='inquiry', + name='sent_to_department', + field=models.BooleanField(default=False, help_text='Whether this inquiry has been sent to the primary department for visibility'), + ), + migrations.AddField( + model_name='inquiry', + name='sent_to_department_at', + field=models.DateTimeField(blank=True, help_text='When the inquiry was sent to the primary department', null=True), + ), + ] diff --git a/apps/complaints/migrations/0006_data_sent_to_department_backfill.py b/apps/complaints/migrations/0006_data_sent_to_department_backfill.py new file mode 100644 index 0000000..a2711a7 --- /dev/null +++ b/apps/complaints/migrations/0006_data_sent_to_department_backfill.py @@ -0,0 +1,63 @@ +from django.db import migrations +from django.utils import timezone + + +def backfill_sent_to_department(apps, schema_editor): + Complaint = apps.get_model("complaints", "Complaint") + Inquiry = apps.get_model("complaints", "Inquiry") + CID = apps.get_model("complaints", "ComplaintInvolvedDepartment") + + Complaint.objects.filter(department__isnull=False).update(sent_to_department=True) + Complaint.objects.filter( + sent_to_department=True, + sent_to_department_at__isnull=True, + forwarded_to_dept_at__isnull=False, + ).update(sent_to_department_at=models.F("forwarded_to_dept_at")) + Complaint.objects.filter( + sent_to_department=True, sent_to_department_at__isnull=True + ).update(sent_to_department_at=timezone.now()) + + Inquiry.objects.filter(department__isnull=False).update(sent_to_department=True) + Inquiry.objects.filter( + sent_to_department=True, + sent_to_department_at__isnull=True, + transferred_at__isnull=False, + ).update(sent_to_department_at=models.F("transferred_at")) + Inquiry.objects.filter( + sent_to_department=True, sent_to_department_at__isnull=True + ).update(sent_to_department_at=timezone.now()) + + CID.objects.filter(forwarded_at__isnull=False).update(sent=True) + CID.objects.filter(sent=True, sent_at__isnull=True).update( + sent_at=models.F("forwarded_at") + ) + CID.objects.filter(sent=True, sent_at__isnull=True).update( + sent_at=timezone.now() + ) + + +def reverse_backfill(apps, schema_editor): + Complaint = apps.get_model("complaints", "Complaint") + Inquiry = apps.get_model("complaints", "Inquiry") + CID = apps.get_model("complaints", "ComplaintInvolvedDepartment") + + Complaint.objects.all().update( + sent_to_department=False, sent_to_department_at=None + ) + Inquiry.objects.all().update( + sent_to_department=False, sent_to_department_at=None + ) + CID.objects.all().update(sent=False, sent_at=None) + + +from django.db import models + +class Migration(migrations.Migration): + + dependencies = [ + ("complaints", "0005_add_sent_to_department_fields"), + ] + + operations = [ + migrations.RunPython(backfill_sent_to_department, reverse_backfill), + ] diff --git a/apps/complaints/migrations/0007_complaintinvolveddepartment_acceptance_notes_and_more.py b/apps/complaints/migrations/0007_complaintinvolveddepartment_acceptance_notes_and_more.py new file mode 100644 index 0000000..6470913 --- /dev/null +++ b/apps/complaints/migrations/0007_complaintinvolveddepartment_acceptance_notes_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 6.0.1 on 2026-05-17 15:52 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0006_data_sent_to_department_backfill'), + ('organizations', '0003_alter_department_champion'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='complaintinvolveddepartment', + name='acceptance_notes', + field=models.TextField(blank=True, help_text='Notes about the acceptance decision'), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='acceptance_status', + field=models.CharField(choices=[('pending', 'Pending Review'), ('acceptable', 'Acceptable'), ('not_acceptable', 'Not Acceptable')], default='pending', help_text='Review status of the department response', max_length=20), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='accepted_at', + field=models.DateTimeField(blank=True, help_text='When the department response was reviewed', null=True), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='accepted_by', + field=models.ForeignKey(blank=True, help_text='User who reviewed the department response', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_complaint_dept_responses', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='response_notes_ar', + field=models.TextField(blank=True, verbose_name='Response (Arabic)'), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='response_notes_en', + field=models.TextField(blank=True, verbose_name='Response (English)'), + ), + migrations.AddIndex( + model_name='complaintinvolveddepartment', + index=models.Index(fields=['department', 'acceptance_status'], name='complaints__departm_5ad178_idx'), + ), + ] diff --git a/apps/complaints/migrations/0008_manager_review_workflow.py b/apps/complaints/migrations/0008_manager_review_workflow.py new file mode 100644 index 0000000..21df8db --- /dev/null +++ b/apps/complaints/migrations/0008_manager_review_workflow.py @@ -0,0 +1,88 @@ +# Generated by Django 6.0.1 on 2026-05-18 21:00 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0007_complaintinvolveddepartment_acceptance_notes_and_more'), + ('organizations', '0003_alter_department_champion'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='complaintinvolveddepartment', + name='manager_review_status', + field=models.CharField(blank=True, choices=[('pending', 'Pending Manager Review'), ('approved', 'Manager Approved'), ('rejected', 'Manager Rejected')], default=None, help_text='Department manager review status of the champion response', max_length=20, null=True), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='manager_reviewed_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='complaintinvolveddepartment', + name='manager_reviewed_by', + field=models.ForeignKey(blank=True, help_text='Department manager who reviewed the champion response', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='manager_reviewed_dept_responses', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='DepartmentManagerReview', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('approved', 'Approved'), ('rejected', 'Rejected')], max_length=20)), + ('reviewed_at', models.DateTimeField(auto_now_add=True)), + ('rejection_reason', models.TextField(blank=True)), + ('involved_department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='manager_reviews', to='complaints.complaintinvolveddepartment')), + ('reviewed_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_manager_reviews', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Department Manager Review', + 'verbose_name_plural': 'Department Manager Reviews', + 'ordering': ['-reviewed_at'], + }, + ), + migrations.CreateModel( + name='ManagerReviewQuestion', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('text_en', models.TextField(verbose_name='Question Text (English)')), + ('text_ar', models.TextField(blank=True, verbose_name='Question Text (Arabic)')), + ('question_type', models.CharField(choices=[('text', 'Short Text'), ('textarea', 'Long Text'), ('yes_no', 'Yes / No'), ('rating', 'Rating (1-5)'), ('multiple_choice', 'Multiple Choice')], default='textarea', max_length=20)), + ('choices_json', models.JSONField(blank=True, default=list, help_text='List of choices for multiple_choice type, e.g. ["Option A","Option B"]')), + ('order', models.PositiveIntegerField(default=0)), + ('is_active', models.BooleanField(default=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_manager_review_questions', to=settings.AUTH_USER_MODEL)), + ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='manager_review_questions', to='organizations.hospital')), + ], + options={ + 'verbose_name': 'Manager Review Question', + 'verbose_name_plural': 'Manager Review Questions', + 'ordering': ['order', 'created_at'], + }, + ), + migrations.CreateModel( + name='ManagerReviewAnswer', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('text_value', models.TextField(blank=True)), + ('numeric_value', models.IntegerField(blank=True, null=True)), + ('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='complaints.departmentmanagerreview')), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='complaints.managerreviewquestion')), + ], + options={ + 'verbose_name': 'Manager Review Answer', + 'verbose_name_plural': 'Manager Review Answers', + }, + ), + ] diff --git a/apps/complaints/migrations/0009_fix_manager_review_default.py b/apps/complaints/migrations/0009_fix_manager_review_default.py new file mode 100644 index 0000000..f4d8207 --- /dev/null +++ b/apps/complaints/migrations/0009_fix_manager_review_default.py @@ -0,0 +1,41 @@ +# Generated by Django 6.0.1 on 2026-05-21 21:22 + +from django.db import migrations, models + + +def backfill_manager_review_status(apps, schema_editor): + ComplaintInvolvedDepartment = apps.get_model("complaints", "ComplaintInvolvedDepartment") + ComplaintInvolvedDepartment.objects.filter( + manager_review_status__isnull=True, + response_submitted=True, + ).update(manager_review_status="pending") + + +class Migration(migrations.Migration): + + dependencies = [ + ("complaints", "0008_manager_review_workflow"), + ] + + operations = [ + migrations.AlterField( + model_name="complaintinvolveddepartment", + name="manager_review_status", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending Manager Review"), + ("approved", "Manager Approved"), + ("rejected", "Manager Rejected"), + ], + default="pending", + help_text="Department manager review status of the champion response", + max_length=20, + null=True, + ), + ), + migrations.RunPython( + backfill_manager_review_status, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/apps/complaints/migrations/0010_remove_complaint_location_and_more.py b/apps/complaints/migrations/0010_remove_complaint_location_and_more.py new file mode 100644 index 0000000..f68b18f --- /dev/null +++ b/apps/complaints/migrations/0010_remove_complaint_location_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0009_fix_manager_review_default'), + ] + + operations = [ + migrations.RenameField( + model_name='complaint', + old_name='location', + new_name='legacy_location', + ), + migrations.RenameField( + model_name='complaint', + old_name='main_section', + new_name='legacy_main_section', + ), + migrations.RenameField( + model_name='complaint', + old_name='subsection', + new_name='legacy_subsection', + ), + migrations.RenameField( + model_name='governmentticket', + old_name='location', + new_name='legacy_location', + ), + migrations.RenameField( + model_name='governmentticket', + old_name='main_section', + new_name='legacy_main_section', + ), + migrations.RenameField( + model_name='governmentticket', + old_name='subsection', + new_name='legacy_subsection', + ), + migrations.RenameField( + model_name='inquiry', + old_name='location', + new_name='legacy_location', + ), + migrations.RenameField( + model_name='inquiry', + old_name='main_section', + new_name='legacy_main_section', + ), + migrations.RenameField( + model_name='inquiry', + old_name='subsection', + new_name='legacy_subsection', + ), + ] diff --git a/apps/complaints/migrations/0011_complaint_legacy_location_and_more.py b/apps/complaints/migrations/0011_complaint_legacy_location_and_more.py new file mode 100644 index 0000000..0d92ca0 --- /dev/null +++ b/apps/complaints/migrations/0011_complaint_legacy_location_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0010_remove_complaint_location_and_more'), + ('organizations', '0008_rename_orgsubsection_to_section_add_champion'), + ] + + operations = [ + migrations.AddField( + model_name='complaint', + name='old_location_raw', + field=models.CharField(blank=True, db_index=True, help_text='Original location Arabic text from complaint', max_length=200), + ), + migrations.AddField( + model_name='complaint', + name='old_main_section_raw', + field=models.CharField(blank=True, db_index=True, help_text='Original main section Arabic text from complaint', max_length=200), + ), + migrations.AddField( + model_name='complaint', + name='old_subsection_raw', + field=models.CharField(blank=True, db_index=True, help_text='Original subsection Arabic text from complaint', max_length=200), + ), + migrations.AddField( + model_name='complaint', + name='section', + field=models.ForeignKey(blank=True, help_text='Section within department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.Section'), + ), + + migrations.AddField( + model_name='governmentticket', + name='department', + field=models.ForeignKey(blank=True, help_text='Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.department'), + ), + migrations.AddField( + model_name='governmentticket', + name='old_location_raw', + field=models.CharField(blank=True, db_index=True, max_length=200), + ), + migrations.AddField( + model_name='governmentticket', + name='old_main_section_raw', + field=models.CharField(blank=True, db_index=True, max_length=200), + ), + migrations.AddField( + model_name='governmentticket', + name='old_subsection_raw', + field=models.CharField(blank=True, db_index=True, max_length=200), + ), + migrations.AddField( + model_name='governmentticket', + name='section', + field=models.ForeignKey(blank=True, help_text='Section within department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.Section'), + ), + + migrations.AddField( + model_name='inquiry', + name='old_location_raw', + field=models.CharField(blank=True, db_index=True, help_text='Original location Arabic text from inquiry', max_length=200), + ), + migrations.AddField( + model_name='inquiry', + name='old_main_section_raw', + field=models.CharField(blank=True, db_index=True, help_text='Original main section Arabic text from inquiry', max_length=200), + ), + migrations.AddField( + model_name='inquiry', + name='old_subsection_raw', + field=models.CharField(blank=True, db_index=True, help_text='Original subsection Arabic text from inquiry', max_length=200), + ), + migrations.AddField( + model_name='inquiry', + name='section', + field=models.ForeignKey(blank=True, help_text='Section within department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='inquiries', to='organizations.Section'), + ), + + ] diff --git a/apps/complaints/migrations/0012_alter_complaint_legacy_location_and_more.py b/apps/complaints/migrations/0012_alter_complaint_legacy_location_and_more.py new file mode 100644 index 0000000..fd4464c --- /dev/null +++ b/apps/complaints/migrations/0012_alter_complaint_legacy_location_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0011_complaint_legacy_location_and_more'), + ('organizations', '0005_alter_legacylocation_table_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='complaint', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='complaint', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='complaint', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text='Subsection within the section', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacysubsection'), + ), + migrations.AlterField( + model_name='governmentticket', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text='Location (e.g., Riyadh, Jeddah)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='governmentticket', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text='Section/Department', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='governmentticket', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text='Subsection within the section', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacysubsection'), + ), + migrations.AlterField( + model_name='inquiry', + name='legacy_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='inquiry', + name='legacy_main_section', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='inquiry', + name='legacy_subsection', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/complaints/migrations/0013_remove_sub_subsection.py b/apps/complaints/migrations/0013_remove_sub_subsection.py new file mode 100644 index 0000000..4bde6bf --- /dev/null +++ b/apps/complaints/migrations/0013_remove_sub_subsection.py @@ -0,0 +1,10 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0012_alter_complaint_legacy_location_and_more'), + ] + + operations = [] diff --git a/apps/complaints/migrations/0014_complaint_area_complaint_location_type_inquiry_area_and_more.py b/apps/complaints/migrations/0014_complaint_area_complaint_location_type_inquiry_area_and_more.py new file mode 100644 index 0000000..3d6833f --- /dev/null +++ b/apps/complaints/migrations/0014_complaint_area_complaint_location_type_inquiry_area_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 6.0.1 on 2026-06-07 04:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0013_remove_sub_subsection'), + ('organizations', '0011_area_department_area'), + ] + + operations = [ + migrations.AddField( + model_name='complaint', + name='area', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='complaints', to='organizations.area'), + ), + migrations.AddField( + model_name='complaint', + name='location_type', + field=models.CharField(blank=True, choices=[('OP', 'Outpatient'), ('IP', 'Inpatient'), ('ER', 'Emergency'), ('GENERAL', 'General')], help_text='Where the incident occurred (OP/IP/ER/GO)', max_length=20), + ), + migrations.AddField( + model_name='inquiry', + name='area', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.area'), + ), + migrations.AddField( + model_name='inquiry', + name='location_type', + field=models.CharField(blank=True, choices=[('OP', 'Outpatient'), ('IP', 'Inpatient'), ('ER', 'Emergency'), ('GENERAL', 'General')], max_length=20), + ), + migrations.AlterField( + model_name='complaint', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='complaint', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='complaint', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='complaints', to='organizations.legacysubsection'), + ), + migrations.AlterField( + model_name='governmentticket', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='governmentticket', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='governmentticket', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='government_tickets', to='organizations.legacysubsection'), + ), + migrations.AlterField( + model_name='inquiry', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='inquiry', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='inquiry', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/complaints/migrations/0015_complaintpdfsummary.py b/apps/complaints/migrations/0015_complaintpdfsummary.py new file mode 100644 index 0000000..176dff3 --- /dev/null +++ b/apps/complaints/migrations/0015_complaintpdfsummary.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-06-08 08:45 + +import apps.complaints.models +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0014_complaint_area_complaint_location_type_inquiry_area_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ComplaintPdfSummary', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('lang', models.CharField(default='ar', max_length=5)), + ('content_summary', models.TextField()), + ('dept_response_summary', models.TextField()), + ('file', models.FileField(blank=True, null=True, upload_to=apps.complaints.models.pdf_summary_upload_to)), + ('file_size', models.IntegerField(default=0)), + ('complaint', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pdf_summaries', to='complaints.complaint')), + ], + options={ + 'verbose_name': 'PDF Summary', + 'verbose_name_plural': 'PDF Summaries', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/complaints/migrations/0016_champion_investigation.py b/apps/complaints/migrations/0016_champion_investigation.py new file mode 100644 index 0000000..2bf1427 --- /dev/null +++ b/apps/complaints/migrations/0016_champion_investigation.py @@ -0,0 +1,91 @@ +# Generated by Django 6.0.1 on 2026-06-09 13:54 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0015_complaintpdfsummary'), + ('organizations', '0013_add_subsection_model'), + ] + + operations = [ + migrations.AlterField( + model_name='complaint', + name='complaint_source_type', + field=models.CharField(choices=[('internal', 'Internal'), ('external', 'External')], db_index=True, default='internal', help_text='Source type (Internal = staff-generated, External = patient/public-generated)', max_length=20), + ), + migrations.CreateModel( + name='ChampionInvestigation', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('questions_sent', 'Questions Sent'), ('answers_received', 'Answers Received'), ('reply_submitted', 'Reply Submitted')], default='questions_sent', max_length=20)), + ('final_reply', models.TextField(blank=True)), + ('champion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='champion_investigations', to='organizations.staff')), + ('complaint', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='investigations', to='complaints.complaint')), + ('explanation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='investigation', to='complaints.complaintexplanation')), + ('involved_department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='investigations', to='complaints.complaintinvolveddepartment')), + ], + options={ + 'verbose_name': 'Champion Investigation', + 'verbose_name_plural': 'Champion Investigations', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='InvestigationQuestion', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('question_text', models.TextField()), + ('order', models.PositiveIntegerField(default=0)), + ('investigation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='complaints.championinvestigation')), + ], + options={ + 'verbose_name': 'Investigation Question', + 'verbose_name_plural': 'Investigation Questions', + 'ordering': ['order', 'created_at'], + }, + ), + migrations.CreateModel( + name='InvestigationResponse', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('token', models.CharField(db_index=True, max_length=64, unique=True)), + ('is_completed', models.BooleanField(default=False)), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('email_sent_at', models.DateTimeField(blank=True, null=True)), + ('sms_sent_at', models.DateTimeField(blank=True, null=True)), + ('investigation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='complaints.championinvestigation')), + ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='investigation_responses', to='organizations.staff')), + ], + options={ + 'verbose_name': 'Investigation Response', + 'verbose_name_plural': 'Investigation Responses', + }, + ), + migrations.CreateModel( + name='InvestigationAnswer', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('answer_text', models.TextField(blank=True)), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='complaints.investigationquestion')), + ('response', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='complaints.investigationresponse')), + ], + options={ + 'verbose_name': 'Investigation Answer', + 'verbose_name_plural': 'Investigation Answers', + 'unique_together': {('response', 'question')}, + }, + ), + ] diff --git a/apps/complaints/migrations/0017_add_patient_contact_status.py b/apps/complaints/migrations/0017_add_patient_contact_status.py new file mode 100644 index 0000000..8406926 --- /dev/null +++ b/apps/complaints/migrations/0017_add_patient_contact_status.py @@ -0,0 +1,49 @@ +# Generated by Django 6.0.1 on 2026-06-10 20:48 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +def migrate_contacted_statuses(apps, schema_editor): + Complaint = apps.get_model("complaints", "Complaint") + Complaint.objects.filter(status="contacted").update( + status="in_progress", + patient_contact_status="contacted", + ) + Complaint.objects.filter(status="contacted_no_response").update( + status="in_progress", + patient_contact_status="contacted_no_response", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0016_champion_investigation'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='complaint', + name='patient_contact_status', + field=models.CharField(choices=[('not_contacted', 'Not Contacted'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted, No Response')], db_index=True, default='not_contacted', help_text='Tracks whether the patient has been contacted regarding this complaint', max_length=30), + ), + migrations.AddField( + model_name='complaint', + name='patient_contact_status_at', + field=models.DateTimeField(blank=True, help_text='When the patient contact status was last updated', null=True), + ), + migrations.AddField( + model_name='complaint', + name='patient_contact_status_by', + field=models.ForeignKey(blank=True, help_text='User who last updated the patient contact status', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patient_contact_updated_complaints', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='complaint', + name='status', + field=models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('partially_resolved', 'Partially Resolved'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('cancelled', 'Cancelled'), ('pending_external', 'Pending External'), ('ovr_pending', 'OVR Pending Approval')], db_index=True, default='open', max_length=25), + ), + migrations.RunPython(migrate_contacted_statuses, migrations.RunPython.noop), + ] diff --git a/apps/complaints/migrations/0018_simplify_statuses.py b/apps/complaints/migrations/0018_simplify_statuses.py new file mode 100644 index 0000000..9b11ada --- /dev/null +++ b/apps/complaints/migrations/0018_simplify_statuses.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.1 on 2026-06-14 10:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0017_add_patient_contact_status'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='inquiry', + name='contact_status', + field=models.CharField(blank=True, choices=[('not_contacted', 'Not Contacted'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted - No Response')], db_index=True, default='not_contacted', max_length=25), + ), + migrations.AddField( + model_name='inquiry', + name='contact_status_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='inquiry', + name='contact_status_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='inquiry', + name='status', + field=models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed')], db_index=True, default='open', max_length=25), + ), + ] diff --git a/apps/complaints/migrations/0019_add_response_token.py b/apps/complaints/migrations/0019_add_response_token.py new file mode 100644 index 0000000..c271075 --- /dev/null +++ b/apps/complaints/migrations/0019_add_response_token.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-06-14 11:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0018_simplify_statuses'), + ] + + operations = [ + migrations.AddField( + model_name='inquiry', + name='response_token', + field=models.CharField(blank=True, db_index=True, help_text='One-time token for department response link', max_length=100, null=True, unique=True), + ), + migrations.AddField( + model_name='inquiry', + name='response_token_sent_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='inquiry', + name='response_token_used', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/complaints/models.py b/apps/complaints/models.py index 90d209c..2f2e8ea 100644 --- a/apps/complaints/models.py +++ b/apps/complaints/models.py @@ -12,12 +12,14 @@ This module implements the complaint management system that: from datetime import timedelta from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.core.encryption import EncryptedCharField, compute_national_id_hash, mask_national_id from apps.core.models import PriorityChoices, SeverityChoices, SoftDeleteModel, TenantModel, TimeStampedModel, UUIDModel +from apps.organizations.models import LocationType class ComplaintStatus(models.TextChoices): @@ -29,12 +31,18 @@ class ComplaintStatus(models.TextChoices): RESOLVED = "resolved", _("Resolved") CLOSED = "closed", _("Closed") CANCELLED = "cancelled", _("Cancelled") - CONTACTED = "contacted", _("Contacted") - CONTACTED_NO_RESPONSE = "contacted_no_response", _("Contacted, No Response") PENDING_EXTERNAL = "pending_external", _("Pending External") OVR_PENDING = "ovr_pending", _("OVR Pending Approval") +class PatientContactStatus(models.TextChoices): + """Patient contact status - tracks whether the patient has been contacted""" + + NOT_CONTACTED = "not_contacted", _("Not Contacted") + CONTACTED = "contacted", _("Contacted") + CONTACTED_NO_RESPONSE = "contacted_no_response", _("Contacted, No Response") + + class DelayReasonChoices(models.TextChoices): """Delay reason for 72h closure""" @@ -264,6 +272,15 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): staff = models.ForeignKey( "organizations.Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="complaints" ) + location_type = models.CharField( + max_length=20, + choices=LocationType.choices, + blank=True, + help_text="Where the incident occurred (OP/IP/ER/GO)", + ) + area = models.ForeignKey( + "organizations.Area", on_delete=models.SET_NULL, null=True, blank=True, related_name="complaints" + ) # Complaint details title = models.CharField(max_length=500) @@ -312,30 +329,60 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): help_text="Level 4: Classification", ) - # Location hierarchy - required fields - location = models.ForeignKey( - "organizations.Location", + # Location hierarchy - legacy fields + legacy_location = models.ForeignKey( + "organizations.LegacyLocation", on_delete=models.PROTECT, related_name="complaints", null=True, blank=True, - help_text="Location (e.g., Riyadh, Jeddah)", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - main_section = models.ForeignKey( - "organizations.MainSection", + legacy_main_section = models.ForeignKey( + "organizations.LegacyMainSection", on_delete=models.PROTECT, related_name="complaints", null=True, blank=True, - help_text="Section/Department", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - subsection = models.ForeignKey( - "organizations.SubSection", + legacy_subsection = models.ForeignKey( + "organizations.LegacySubSection", on_delete=models.PROTECT, related_name="complaints", null=True, blank=True, - help_text="Subsection within the section", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + + # New hierarchy (from 4th Version Excel) + section = models.ForeignKey( + "organizations.Section", + on_delete=models.PROTECT, + related_name="complaints", + null=True, + blank=True, + help_text="Section within department", + ) + + # Legacy raw fields for mapping + old_location_raw = models.CharField( + max_length=200, + blank=True, + db_index=True, + help_text="Original location Arabic text from complaint", + ) + old_main_section_raw = models.CharField( + max_length=200, + blank=True, + db_index=True, + help_text="Original main section Arabic text from complaint", + ) + old_subsection_raw = models.CharField( + max_length=200, + blank=True, + db_index=True, + help_text="Original subsection Arabic text from complaint", ) # Type (complaint vs appreciation) @@ -351,7 +398,7 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): complaint_source_type = models.CharField( max_length=20, choices=ComplaintSourceType.choices, - default=ComplaintSourceType.EXTERNAL, + default=ComplaintSourceType.INTERNAL, db_index=True, help_text="Source type (Internal = staff-generated, External = patient/public-generated)", ) @@ -389,6 +436,25 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): max_length=25, choices=ComplaintStatus.choices, default=ComplaintStatus.OPEN, db_index=True ) + patient_contact_status = models.CharField( + max_length=30, + choices=PatientContactStatus.choices, + default=PatientContactStatus.NOT_CONTACTED, + db_index=True, + help_text="Tracks whether the patient has been contacted regarding this complaint", + ) + patient_contact_status_at = models.DateTimeField( + null=True, blank=True, help_text="When the patient contact status was last updated" + ) + patient_contact_status_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="patient_contact_updated_complaints", + help_text="User who last updated the patient contact status", + ) + # Assignment assigned_to = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_complaints" @@ -452,6 +518,16 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="closed_complaints" ) + cancelled_at = models.DateTimeField(null=True, blank=True) + cancelled_by = models.ForeignKey( + "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="cancelled_complaints" + ) + + partially_resolved_at = models.DateTimeField(null=True, blank=True) + partially_resolved_by = models.ForeignKey( + "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="partially_resolved_complaints" + ) + # Reopen reopened_at = models.DateTimeField(null=True, blank=True) reopened_by = models.ForeignKey( @@ -501,6 +577,12 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): forwarded_to_dept_at = models.DateTimeField( null=True, blank=True, help_text="When complaint was forwarded to the involved department" ) + sent_to_department = models.BooleanField( + default=False, help_text="Whether this complaint has been sent to the primary department for visibility" + ) + sent_to_department_at = models.DateTimeField( + null=True, blank=True, help_text="When the complaint was sent to the primary department" + ) response_date = models.DateField(null=True, blank=True, help_text="Date when response was received") # Complaint details (Step 1 fields) @@ -538,6 +620,8 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): # Metadata metadata = models.JSONField(default=dict, blank=True) + notes = GenericRelation("core.Note") + class Meta: ordering = ["-created_at"] indexes = [ @@ -557,6 +641,29 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): return reverse("complaints:complaint_detail", kwargs={"pk": self.pk}) + def get_owner(self): + """ + Returns the owner of this complaint. + Cascade: section(champion, supervisor, deputy_supervisor) + -> department(champion, deputy_manager, supervisor, + deputy_supervisor, manager_2nd, manager_3rd). + Returns: Staff instance or None. + """ + if self.section: + for role in ("champion", "supervisor", "deputy_supervisor"): + owner = getattr(self.section, role, None) + if owner: + return owner + if self.department: + dept = self.department + for role in ("champion", "deputy_manager", + "supervisor", "deputy_supervisor", + "manager_2nd", "manager_3rd"): + owner = getattr(dept, role, None) + if owner: + return owner + return None + def get_masked_national_id(self): return mask_national_id(self.national_id) @@ -572,12 +679,9 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): # Generate reference number if not set (for all creation methods: form, API, admin) if not self.reference_number: - from datetime import datetime - import uuid + from apps.core.reference import generate_reference - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(uuid.uuid4().int)[:6] - self.reference_number = f"CMP-{today}-{random_suffix}" + self.reference_number = generate_reference("CMP", self.hospital) if not self.due_at: self.due_at = self.calculate_sla_due_date() @@ -596,8 +700,20 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): else: self.national_id_hash = "" + self._sync_department_timestamps() + super().save(*args, **kwargs) + def _sync_department_timestamps(self): + """Sync forwarded_to_dept_at and sent_to_department_at — keep both in agreement.""" + now = timezone.now() + if self.sent_to_department and not self.sent_to_department_at: + self.sent_to_department_at = now + if self.sent_to_department_at and not self.forwarded_to_dept_at: + self.forwarded_to_dept_at = self.sent_to_department_at + if self.forwarded_to_dept_at and not self.sent_to_department_at: + self.sent_to_department_at = self.forwarded_to_dept_at + def calculate_sla_due_date(self): """ Calculate SLA due date based on source, severity, and hospital configuration. @@ -700,15 +816,13 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): def is_active_status(self): """ Check if complaint is in an active status (can be worked on). - Active statuses: OPEN, IN_PROGRESS, PARTIALLY_RESOLVED, CONTACTED, CONTACTED_NO_RESPONSE, PENDING_EXTERNAL + Active statuses: OPEN, IN_PROGRESS, PARTIALLY_RESOLVED, PENDING_EXTERNAL Inactive statuses: RESOLVED, CLOSED, CANCELLED """ return self.status in [ ComplaintStatus.OPEN, ComplaintStatus.IN_PROGRESS, ComplaintStatus.PARTIALLY_RESOLVED, - ComplaintStatus.CONTACTED, - ComplaintStatus.CONTACTED_NO_RESPONSE, ComplaintStatus.PENDING_EXTERNAL, ] @@ -752,13 +866,6 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): ComplaintStatus.RESOLVED: {"label": _("Resolved"), "slug": "resolved", "progress": 100, "css": "emerald"}, ComplaintStatus.CLOSED: {"label": _("Closed"), "slug": "closed", "progress": 100, "css": "slate"}, ComplaintStatus.CANCELLED: {"label": _("Cancelled"), "slug": "cancelled", "progress": 0, "css": "rose"}, - ComplaintStatus.CONTACTED: {"label": _("In Progress"), "slug": "in_progress", "progress": 50, "css": "blue"}, - ComplaintStatus.CONTACTED_NO_RESPONSE: { - "label": _("In Progress"), - "slug": "in_progress", - "progress": 50, - "css": "blue", - }, } @property @@ -918,6 +1025,17 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): def is_activated(self): return self.activated_at is not None + @property + def sent_to_any_department(self): + return self.involved_departments.filter(sent=True).exists() or self.sent_to_department + + @property + def all_departments_responded(self): + sent = self.involved_departments.filter(sent=True) + if not sent.exists(): + return self.sent_to_department + return not sent.filter(response_submitted=False).exists() + def get_tracking_url(self): """ Get the public tracking URL for this complaint. @@ -957,6 +1075,29 @@ class ComplaintAttachment(UUIDModel, TimeStampedModel): return f"{self.complaint} - {self.filename}" +def pdf_summary_upload_to(instance, filename): + return f"pdf_summaries/complaints/{instance.complaint_id}_ar.pdf" + + +class ComplaintPdfSummary(UUIDModel, TimeStampedModel): + """Persisted PDF summary with AI-generated text and generated file.""" + + complaint = models.ForeignKey(Complaint, on_delete=models.CASCADE, related_name="pdf_summaries") + lang = models.CharField(max_length=5, default="ar") + content_summary = models.TextField() + dept_response_summary = models.TextField() + file = models.FileField(upload_to=pdf_summary_upload_to, blank=True, null=True) + file_size = models.IntegerField(default=0) + + class Meta: + ordering = ["-created_at"] + verbose_name = "PDF Summary" + verbose_name_plural = "PDF Summaries" + + def __str__(self): + return f"PDF Summary - {self.complaint.reference_number}" + + class ComplaintUpdate(UUIDModel, TimeStampedModel): """ Complaint update/timeline entry. @@ -1436,28 +1577,69 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): department = models.ForeignKey( "organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="inquiries" ) + location_type = models.CharField( + max_length=20, + choices=LocationType.choices, + blank=True, + ) + area = models.ForeignKey( + "organizations.Area", on_delete=models.SET_NULL, null=True, blank=True, related_name="inquiries" + ) - # Location - location = models.ForeignKey( - "organizations.Location", + # Location - legacy fields + legacy_location = models.ForeignKey( + "organizations.LegacyLocation", on_delete=models.SET_NULL, null=True, blank=True, related_name="inquiries", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - main_section = models.ForeignKey( - "organizations.MainSection", + legacy_main_section = models.ForeignKey( + "organizations.LegacyMainSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="inquiries", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - subsection = models.ForeignKey( - "organizations.SubSection", + legacy_subsection = models.ForeignKey( + "organizations.LegacySubSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="inquiries", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + + # New hierarchy (from 4th Version Excel) + section = models.ForeignKey( + "organizations.Section", + on_delete=models.PROTECT, + related_name="inquiries", + null=True, + blank=True, + help_text="Section within department", + ) + + # Legacy raw fields for mapping + old_location_raw = models.CharField( + max_length=200, + blank=True, + db_index=True, + help_text="Original location Arabic text from inquiry", + ) + old_main_section_raw = models.CharField( + max_length=200, + blank=True, + db_index=True, + help_text="Original main section Arabic text from inquiry", + ) + old_subsection_raw = models.CharField( + max_length=200, + blank=True, + db_index=True, + help_text="Original subsection Arabic text from inquiry", ) # Reference number @@ -1547,13 +1729,31 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): ("in_progress", _("In Progress")), ("resolved", _("Resolved")), ("closed", _("Closed")), - ("contacted", _("Contacted")), - ("contacted_no_response", _("Contacted, No Response")), ], default="open", db_index=True, ) + contact_status = models.CharField( + max_length=25, + choices=[ + ("not_contacted", _("Not Contacted")), + ("contacted", _("Contacted")), + ("contacted_no_response", _("Contacted - No Response")), + ], + default="not_contacted", + blank=True, + db_index=True, + ) + contact_status_at = models.DateTimeField(null=True, blank=True) + contact_status_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="+", + ) + # Creator tracking created_by = models.ForeignKey( "accounts.User", @@ -1615,6 +1815,12 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): transferred_at = models.DateTimeField( null=True, blank=True, db_index=True, help_text="When the inquiry was transferred to a department" ) + sent_to_department = models.BooleanField( + default=False, help_text="Whether this inquiry has been sent to the primary department for visibility" + ) + sent_to_department_at = models.DateTimeField( + null=True, blank=True, help_text="When the inquiry was sent to the primary department" + ) transferred_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, @@ -1655,6 +1861,14 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): related_name="department_inquiry_responses", ) + # Token-based department response + response_token = models.CharField( + max_length=100, blank=True, null=True, unique=True, db_index=True, + help_text="One-time token for department response link", + ) + response_token_used = models.BooleanField(default=False) + response_token_sent_at = models.DateTimeField(null=True, blank=True) + # Department response SLA tracking dept_response_sla_due_at = models.DateTimeField( null=True, blank=True, db_index=True, @@ -1786,6 +2000,8 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): null=True, blank=True, help_text="When reminder was sent for follow-up" ) + notes = GenericRelation("core.Note") + class Meta: ordering = ["-created_at"] verbose_name_plural = "Inquiries" @@ -1797,12 +2013,9 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): def save(self, *args, **kwargs): if not self.reference_number: - from datetime import datetime - import uuid + from apps.core.reference import generate_reference - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(uuid.uuid4().int)[:6] - self.reference_number = f"INQ-{today}-{random_suffix}" + self.reference_number = generate_reference("INQ", self.hospital) if not self.due_at: sla_config = self.get_sla_config() @@ -1834,6 +2047,29 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): return reverse("inquiries:inquiry_detail", kwargs={"pk": self.pk}) + def get_owner(self): + """ + Returns the owner of this inquiry. + Cascade: section(champion, supervisor, deputy_supervisor) + -> department(champion, deputy_manager, supervisor, + deputy_supervisor, manager_2nd, manager_3rd). + Returns: Staff instance or None. + """ + if self.section: + for role in ("champion", "supervisor", "deputy_supervisor"): + owner = getattr(self.section, role, None) + if owner: + return owner + if self.department: + dept = self.department + for role in ("champion", "deputy_manager", + "supervisor", "deputy_supervisor", + "manager_2nd", "manager_3rd"): + owner = getattr(dept, role, None) + if owner: + return owner + return None + @property def short_description_en(self): if self.metadata and "ai_analysis" in self.metadata: @@ -1911,7 +2147,7 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): """ Check if inquiry is in an active status (can be worked on). Active statuses: open, in_progress - Inactive statuses: resolved, closed, contacted, contacted_no_response + Inactive statuses: resolved, closed """ return self.status in ["open", "in_progress"] @@ -2154,6 +2390,17 @@ class ComplaintExplanation(UUIDModel, TimeStampedModel): """Count of explanation attachments""" return self.attachments.count() + @property + def linked_involved_department(self): + """Find the linked ComplaintInvolvedDepartment for this explanation.""" + if not self.staff or not self.staff.department: + return None + return ComplaintInvolvedDepartment.objects.filter( + complaint=self.complaint, + department=self.staff.department, + sent=True, + ).first() + def get_token(self): """Return the access token""" return self.token @@ -2449,8 +2696,35 @@ class ComplaintInvolvedDepartment(UUIDModel, TimeStampedModel): response_notes = models.TextField(blank=True, help_text="Department's response/feedback on the complaint") + response_notes_en = models.TextField(blank=True, verbose_name="Response (English)") + response_notes_ar = models.TextField(blank=True, verbose_name="Response (Arabic)") + + # Acceptance review + acceptance_status = models.CharField( + max_length=20, + choices=[ + ("pending", _("Pending Review")), + ("acceptable", _("Acceptable")), + ("not_acceptable", _("Not Acceptable")), + ], + default="pending", + help_text="Review status of the department response", + ) + accepted_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="reviewed_complaint_dept_responses", + help_text="User who reviewed the department response", + ) + accepted_at = models.DateTimeField(null=True, blank=True, help_text="When the department response was reviewed") + acceptance_notes = models.TextField(blank=True, help_text="Notes about the acceptance decision") + # Reminder and delay tracking (Step 1 fields) forwarded_at = models.DateTimeField(null=True, blank=True, help_text="When complaint was sent to this department") + sent = models.BooleanField(default=False, help_text="Whether this department has been sent the complaint") + sent_at = models.DateTimeField(null=True, blank=True, help_text="When the complaint was sent to this department") first_reminder_sent_at = models.DateTimeField( null=True, blank=True, help_text="When first reminder was sent to this department" ) @@ -2460,6 +2734,28 @@ class ComplaintInvolvedDepartment(UUIDModel, TimeStampedModel): delay_reason = models.TextField(blank=True, help_text="Reason for department delay in response") delayed_person = models.CharField(max_length=200, blank=True, help_text="Name of person responsible for delay") + manager_review_status = models.CharField( + max_length=20, + choices=[ + ("pending", _("Pending Manager Review")), + ("approved", _("Manager Approved")), + ("rejected", _("Manager Rejected")), + ], + null=True, + blank=True, + default="pending", + help_text="Department manager review status of the champion response", + ) + manager_reviewed_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="manager_reviewed_dept_responses", + help_text="Department manager who reviewed the champion response", + ) + manager_reviewed_at = models.DateTimeField(null=True, blank=True) + class Meta: ordering = ["-is_primary", "-created_at"] verbose_name = "Complaint Involved Department" @@ -2469,6 +2765,7 @@ class ComplaintInvolvedDepartment(UUIDModel, TimeStampedModel): models.Index(fields=["complaint", "role"]), models.Index(fields=["department", "response_submitted"]), models.Index(fields=["department", "forwarded_at"]), + models.Index(fields=["department", "acceptance_status"]), ] def __str__(self): @@ -2485,6 +2782,20 @@ class ComplaintInvolvedDepartment(UUIDModel, TimeStampedModel): ).update(is_primary=False) super().save(*args, **kwargs) + @property + def sla_remaining(self): + """Hours remaining for this department to respond (based on ExplanationSLAConfig).""" + if self.response_submitted or not self.sent_at: + return None + from apps.complaints.tasks import get_explanation_sla_config + sla_config = get_explanation_sla_config(self.complaint.hospital) + sla_hours = sla_config.response_hours if sla_config else 48 + due_at = self.sent_at + timedelta(hours=sla_hours) + remaining = due_at - timezone.now() + if remaining.total_seconds() <= 0: + return timedelta(0) + return remaining + class ComplaintInvolvedStaff(UUIDModel, TimeStampedModel): """ @@ -3223,32 +3534,57 @@ class GovernmentTicket(UUIDModel, TimeStampedModel): national_id = models.CharField(max_length=20, blank=True) contact_number = models.CharField(max_length=20, blank=True) - # Location hierarchy - location = models.ForeignKey( - "organizations.Location", + # Location hierarchy - legacy fields + legacy_location = models.ForeignKey( + "organizations.LegacyLocation", on_delete=models.PROTECT, related_name="government_tickets", null=True, blank=True, - help_text=_("Location (e.g., Riyadh, Jeddah)"), + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - main_section = models.ForeignKey( - "organizations.MainSection", + legacy_main_section = models.ForeignKey( + "organizations.LegacyMainSection", on_delete=models.PROTECT, related_name="government_tickets", null=True, blank=True, - help_text=_("Section/Department"), + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - subsection = models.ForeignKey( - "organizations.SubSection", + legacy_subsection = models.ForeignKey( + "organizations.LegacySubSection", on_delete=models.PROTECT, related_name="government_tickets", null=True, blank=True, - help_text=_("Subsection within the section"), + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) + # Department (new hierarchy) + department = models.ForeignKey( + "organizations.Department", + on_delete=models.PROTECT, + related_name="government_tickets", + null=True, + blank=True, + help_text="Department", + ) + + # New hierarchy + section = models.ForeignKey( + "organizations.Section", + on_delete=models.PROTECT, + related_name="government_tickets", + null=True, + blank=True, + help_text="Section within department", + ) + + # Legacy raw fields + old_location_raw = models.CharField(max_length=200, blank=True, db_index=True) + old_main_section_raw = models.CharField(max_length=200, blank=True, db_index=True) + old_subsection_raw = models.CharField(max_length=200, blank=True, db_index=True) + # Dates received_date = models.DateTimeField(help_text=_("Date/time the ticket was received from source")) @@ -3282,3 +3618,239 @@ class GovernmentTicket(UUIDModel, TimeStampedModel): def __str__(self): return f"{self.ticket_number} - {self.complainant_name}" + + +class ManagerReviewQuestionType(models.TextChoices): + TEXT = "text", _("Short Text") + TEXTAREA = "textarea", _("Long Text") + YES_NO = "yes_no", _("Yes / No") + RATING = "rating", _("Rating (1-5)") + MULTIPLE_CHOICE = "multiple_choice", _("Multiple Choice") + + +class ManagerReviewQuestion(UUIDModel, TimeStampedModel): + """ + Configurable questions that PX-Admin creates per hospital. + Shown to the Department Manager when reviewing a champion's response. + """ + + hospital = models.ForeignKey( + "organizations.Hospital", + on_delete=models.CASCADE, + related_name="manager_review_questions", + ) + text_en = models.TextField(verbose_name="Question Text (English)") + text_ar = models.TextField(blank=True, verbose_name="Question Text (Arabic)") + question_type = models.CharField( + max_length=20, + choices=ManagerReviewQuestionType.choices, + default=ManagerReviewQuestionType.TEXTAREA, + ) + choices_json = models.JSONField( + blank=True, + default=list, + help_text='List of choices for multiple_choice type, e.g. ["Option A","Option B"]', + ) + order = models.PositiveIntegerField(default=0) + is_active = models.BooleanField(default=True) + created_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="created_manager_review_questions", + ) + + class Meta: + ordering = ["order", "created_at"] + verbose_name = "Manager Review Question" + verbose_name_plural = "Manager Review Questions" + + def __str__(self): + return self.text_en[:80] + + def get_localized_text(self): + from django.utils.translation import get_language + if get_language() == "ar" and self.text_ar: + return self.text_ar + return self.text_en + + +class DepartmentManagerReview(UUIDModel, TimeStampedModel): + """ + Records the Department Manager's review of a champion's response. + One review per involved-department response cycle. + """ + + involved_department = models.ForeignKey( + ComplaintInvolvedDepartment, + on_delete=models.CASCADE, + related_name="manager_reviews", + ) + reviewed_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + related_name="dept_manager_reviews", + ) + status = models.CharField( + max_length=20, + choices=[ + ("approved", _("Approved")), + ("rejected", _("Rejected")), + ], + ) + reviewed_at = models.DateTimeField(auto_now_add=True) + rejection_reason = models.TextField(blank=True) + + class Meta: + ordering = ["-reviewed_at"] + verbose_name = "Department Manager Review" + verbose_name_plural = "Department Manager Reviews" + + def __str__(self): + return f"ManagerReview({self.involved_department}, {self.status})" + + +class ManagerReviewAnswer(UUIDModel, TimeStampedModel): + """ + Individual answer to a ManagerReviewQuestion within a DepartmentManagerReview. + """ + + review = models.ForeignKey( + DepartmentManagerReview, + on_delete=models.CASCADE, + related_name="answers", + ) + question = models.ForeignKey( + ManagerReviewQuestion, + on_delete=models.CASCADE, + related_name="answers", + ) + text_value = models.TextField(blank=True) + numeric_value = models.IntegerField(null=True, blank=True) + + class Meta: + verbose_name = "Manager Review Answer" + verbose_name_plural = "Manager Review Answers" + + def __str__(self): + return f"Answer({self.question.text_en[:40]}): {self.text_value[:40]}" + + +class InvestigationStatus(models.TextChoices): + QUESTIONS_SENT = "questions_sent", "Questions Sent" + ANSWERS_RECEIVED = "answers_received", "Answers Received" + REPLY_SUBMITTED = "reply_submitted", "Reply Submitted" + + +class ChampionInvestigation(UUIDModel, TimeStampedModel): + complaint = models.ForeignKey( + Complaint, + on_delete=models.CASCADE, + related_name="investigations", + ) + champion = models.ForeignKey( + "organizations.Staff", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="champion_investigations", + ) + involved_department = models.ForeignKey( + "ComplaintInvolvedDepartment", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="investigations", + ) + explanation = models.ForeignKey( + "ComplaintExplanation", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="investigation", + ) + status = models.CharField( + max_length=20, + choices=InvestigationStatus.choices, + default=InvestigationStatus.QUESTIONS_SENT, + ) + final_reply = models.TextField(blank=True) + + class Meta: + verbose_name = "Champion Investigation" + verbose_name_plural = "Champion Investigations" + ordering = ["-created_at"] + + def __str__(self): + return f"Investigation({self.complaint.reference_number}) - {self.status}" + + @property + def all_responses_received(self): + return self.responses.exists() and not self.responses.filter(is_completed=False).exists() + + +class InvestigationQuestion(UUIDModel, TimeStampedModel): + investigation = models.ForeignKey( + ChampionInvestigation, + on_delete=models.CASCADE, + related_name="questions", + ) + question_text = models.TextField() + order = models.PositiveIntegerField(default=0) + + class Meta: + verbose_name = "Investigation Question" + verbose_name_plural = "Investigation Questions" + ordering = ["order", "created_at"] + + def __str__(self): + return f"Q{self.order}: {self.question_text[:80]}" + + +class InvestigationResponse(UUIDModel, TimeStampedModel): + investigation = models.ForeignKey( + ChampionInvestigation, + on_delete=models.CASCADE, + related_name="responses", + ) + staff = models.ForeignKey( + "organizations.Staff", + on_delete=models.CASCADE, + related_name="investigation_responses", + ) + token = models.CharField(max_length=64, unique=True, db_index=True) + is_completed = models.BooleanField(default=False) + completed_at = models.DateTimeField(null=True, blank=True) + email_sent_at = models.DateTimeField(null=True, blank=True) + sms_sent_at = models.DateTimeField(null=True, blank=True) + + class Meta: + verbose_name = "Investigation Response" + verbose_name_plural = "Investigation Responses" + + def __str__(self): + return f"Response({self.staff.get_full_name()}) - {'Done' if self.is_completed else 'Pending'}" + + +class InvestigationAnswer(UUIDModel, TimeStampedModel): + response = models.ForeignKey( + InvestigationResponse, + on_delete=models.CASCADE, + related_name="answers", + ) + question = models.ForeignKey( + InvestigationQuestion, + on_delete=models.CASCADE, + related_name="answers", + ) + answer_text = models.TextField(blank=True) + + class Meta: + verbose_name = "Investigation Answer" + verbose_name_plural = "Investigation Answers" + unique_together = [("response", "question")] + + def __str__(self): + return f"A: {self.answer_text[:80]}" diff --git a/apps/complaints/serializers.py b/apps/complaints/serializers.py index fb48e77..0cb693d 100644 --- a/apps/complaints/serializers.py +++ b/apps/complaints/serializers.py @@ -210,6 +210,8 @@ class ComplaintSerializer(serializers.ModelSerializer): "source_name", "source_code", "status", + "patient_contact_status", + "patient_contact_status_at", "created_by", "created_by_name", "assigned_to", @@ -221,9 +223,11 @@ class ComplaintSerializer(serializers.ModelSerializer): "sla_status", "reminder_sent_at", "escalated_at", - "location", - "main_section", - "subsection", + "legacy_location", + "legacy_main_section", + "legacy_subsection", + "section", + "resolution", "resolution_category", "resolution_outcome", @@ -476,6 +480,7 @@ class ComplaintListSerializer(serializers.ModelSerializer): "complaint_source_type_display", "source_name", "status", + "patient_contact_status", "assigned_to_name", "assigned_at", "due_at", @@ -545,9 +550,11 @@ class InquirySerializer(serializers.ModelSerializer): "ai_brief_ar", "source", "status", - "location", - "main_section", - "subsection", + "legacy_location", + "legacy_main_section", + "legacy_subsection", + "section", + "is_outgoing", "outgoing_department", "is_straightforward", diff --git a/apps/complaints/services/complaint_service.py b/apps/complaints/services/complaint_service.py index e5a32ff..fe30f7d 100644 --- a/apps/complaints/services/complaint_service.py +++ b/apps/complaints/services/complaint_service.py @@ -1,15 +1,26 @@ import logging +from datetime import timedelta + from django.utils import timezone from apps.core.services import AuditService -from apps.notifications.services import NotificationService +from apps.notifications.services import NotificationService, get_email_header_html from apps.organizations.models import Department from apps.complaints.models import Complaint, ComplaintExplanation, ComplaintStatus, ComplaintUpdate logger = logging.getLogger(__name__) +def _get_explanation_sla_config(hospital): + """Get explanation SLA configuration for a hospital.""" + from apps.complaints.models import ExplanationSLAConfig + try: + return ExplanationSLAConfig.objects.get(hospital=hospital, is_active=True) + except ExplanationSLAConfig.DoesNotExist: + return None + + class ComplaintServiceError(Exception): pass @@ -256,15 +267,14 @@ class ComplaintService: # Valid status transitions for lifecycle enforcement VALID_STATUS_TRANSITIONS = { - "open": ["in_progress", "cancelled", "contacted", "contacted_no_response"], - "in_progress": ["partially_resolved", "resolved", "cancelled", "contacted", "contacted_no_response", "pending_external"], + "open": ["in_progress", "cancelled"], + "in_progress": ["partially_resolved", "resolved", "cancelled", "pending_external", "ovr_pending"], "partially_resolved": ["resolved", "in_progress", "cancelled", "pending_external"], "resolved": ["closed", "in_progress"], "closed": ["in_progress"], "cancelled": ["open", "in_progress"], - "contacted": ["open", "in_progress", "contacted_no_response", "cancelled", "pending_external"], - "contacted_no_response": ["open", "in_progress", "cancelled", "pending_external"], "pending_external": ["resolved", "in_progress", "cancelled", "closed"], + "ovr_pending": ["in_progress", "resolved", "cancelled"], } @staticmethod @@ -294,9 +304,6 @@ class ComplaintService: classification=complaint.classification, subcategory_obj=complaint.subcategory_obj, classification_obj=complaint.classification_obj, - location=complaint.location, - main_section=complaint.main_section, - subsection=complaint.subsection, complaint_type=complaint.complaint_type, complaint_source_type=complaint.complaint_source_type, priority=complaint.priority, @@ -409,6 +416,14 @@ class ComplaintService: complaint.pending_external_set_at = timezone.now() complaint.was_pending_external = True + elif new_status == ComplaintStatus.CANCELLED or new_status == "cancelled": + complaint.cancelled_at = timezone.now() + complaint.cancelled_by = changed_by + + elif new_status == ComplaintStatus.PARTIALLY_RESOLVED or new_status == "partially_resolved": + complaint.partially_resolved_at = timezone.now() + complaint.partially_resolved_by = changed_by + complaint.save() ComplaintUpdate.objects.create( @@ -561,6 +576,7 @@ class ComplaintService: requested_by, domain, request=None, + contact_person_map=None, ): import secrets @@ -578,12 +594,34 @@ class ComplaintService: if dept_id not in selected_dept_ids: continue - champion = dept_info.get("champion") - champion_email = dept_info.get("champion_email") + champion = None + champion_email = None + champion_display = None + + if contact_person_map and dept_id in contact_person_map: + cp_id = contact_person_map[dept_id] + dept_obj = Department.objects.filter(id=dept_id).first() + if dept_obj: + cinfo = dept_obj.is_valid_contact_person(cp_id) + if cinfo: + champion = cinfo["staff"] + champion_email = cinfo["email"] + champion_display = f"{cinfo['name']} ({cinfo['role_label']})" if not champion or not champion_email: - skipped_no_email += 1 - continue + champion = dept_info.get("champion") + champion_email = dept_info.get("champion_email") + + if not champion or not champion_email: + dept_obj = Department.objects.filter(id=dept_id).select_related("manager", "manager__staff_profile").first() + if dept_obj and dept_obj.manager: + manager_staff = getattr(dept_obj.manager, 'staff_profile', None) + if manager_staff: + champion = manager_staff + champion_email = manager_staff.email or dept_obj.manager.email + if not champion: + skipped_no_email += 1 + continue staff_names = [s["staff_name"] for s in dept_info["staff_list"]] @@ -604,13 +642,13 @@ class ComplaintService: champion_link = f"https://{domain}/complaints/{complaint.id}/explain/{champion_token}/" champion_subject = f"Explanation Request - Complaint #{complaint.reference_number}" - champion_display = dept_info.get("champion_name", str(champion)) + champion_display = champion_display or dept_info.get("champion_name", str(champion)) staff_list_text = "\n".join(f" - {n}" for n in staff_names) - champion_email_body = f"""Dear {champion_display}, + champion_email_body = f"""Dear {champion.get_full_name()}, -As the department champion for {dept_info['department_name']}, we are requesting your assistance in gathering explanations for a complaint involving staff from your department. +We are requesting your assistance in gathering explanations for a complaint involving staff from {dept_info['department_name']}. INVOLVED STAFF FROM YOUR DEPARTMENT: ----------------------------------- @@ -659,6 +697,21 @@ This is an automated message from PX360 Complaint Management System.""" email=champion_email, subject=champion_subject, message=champion_email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Explanation Request - Complaint #{complaint.reference_number}

+

Dear {champion.get_full_name()},

+

We are requesting your assistance in gathering explanations for a complaint involving staff from {dept_info['department_name']}.

+

Please coordinate with the involved staff and submit the explanation.

+ +

This link can only be used once. After submission, it will expire.

+
+
+""", related_object=complaint, metadata={ "notification_type": "explanation_request", @@ -692,6 +745,51 @@ This is an automated message from PX360 Complaint Management System.""" } ) + # Set SLA due date on each created explanation + now = timezone.now() + sla_config = _get_explanation_sla_config(complaint.hospital) + sla_hours = sla_config.response_hours if sla_config else 48 + for result in results: + if result.get("sent") and result.get("explanation_id"): + ComplaintExplanation.objects.filter(id=result["explanation_id"]).update( + sla_due_at=now + timedelta(hours=sla_hours) + ) + + # Mark complaint as sent to department and set forwarded timestamp + if champion_count > 0: + complaint.sent_to_department = True + complaint.sent_to_department_at = complaint.sent_to_department_at or now + complaint.forwarded_to_dept_at = complaint.forwarded_to_dept_at or now + complaint.explanation_requested = True + complaint.explanation_requested_at = complaint.explanation_requested_at or now + + # Create/update ComplaintInvolvedDepartment records for each selected dept + from apps.complaints.models import ComplaintInvolvedDepartment as CID + for dept_id in selected_dept_ids: + dept_info = department_groups.get(dept_id) + if not dept_info: + continue + try: + dept = Department.objects.get(id=dept_id) + except Department.DoesNotExist: + continue + inv_dept, created = CID.objects.get_or_create( + complaint=complaint, + department=dept, + defaults={ + "role": "secondary", + "added_by": requested_by, + "sent": True, + "sent_at": now, + "forwarded_at": now, + }, + ) + if not created and not inv_dept.sent: + inv_dept.sent = True + inv_dept.sent_at = inv_dept.sent_at or now + inv_dept.forwarded_at = inv_dept.forwarded_at or now + inv_dept.save() + metadata = { "champion_count": champion_count, "skipped_no_email": skipped_no_email, @@ -727,13 +825,18 @@ This is an automated message from PX360 Complaint Management System.""" "results": results, }, ) - complaint.status = ComplaintStatus.CONTACTED - complaint.save(update_fields=["status", "updated_at"]) + complaint.save(update_fields=[ + "updated_at", + "sent_to_department", "sent_to_department_at", + "forwarded_to_dept_at", + "explanation_requested", "explanation_requested_at", + ]) return { "champion_count": champion_count, "skipped_no_email": skipped_no_email, "results": results, + "manager_count": 0, } @staticmethod diff --git a/apps/complaints/signals.py b/apps/complaints/signals.py index 643050a..0ba0d4d 100644 --- a/apps/complaints/signals.py +++ b/apps/complaints/signals.py @@ -130,55 +130,82 @@ def send_complaint_status_change_sms(sender, instance, created, **kwargs): if old_status == new_status: return - # Only send SMS if phone number is provided - if not instance.contact_phone: - logger.info(f"Complaint #{instance.id} status changed to {new_status} but no phone number. Skipping SMS.") + # Only send if phone or email is provided + if not instance.contact_phone and not instance.contact_email: + logger.info(f"Complaint #{instance.id} status changed to {new_status} but no contact info. Skipping notification.") return - # Send SMS notification + # Send SMS + email notification try: - from apps.notifications.services import NotificationService - - # Bilingual SMS messages - messages_en = { - 'resolved': f"PX360: Your complaint #{instance.reference_number} has been resolved. Thank you for your feedback.", - 'closed': f"PX360: Your complaint #{instance.reference_number} has been closed. Thank you for your feedback." - } - - messages_ar = { - 'resolved': f"PX360: تم حل شكوتك #{instance.reference_number}. شكراً لتعاونكم.", - 'closed': f"PX360: تم إغلاق شكوتك #{instance.reference_number}. شكراً لتعاونكم." - } - - # Default to English (can be enhanced to detect language) - sms_message = messages_en.get(new_status, '') - - # Send SMS - notification_log = NotificationService.send_sms( - phone=instance.contact_phone, - message=sms_message, - related_object=instance, - metadata={ - 'notification_type': 'complaint_status_change', - 'reference_number': instance.reference_number, - 'old_status': old_status, - 'new_status': new_status, - 'language': 'en' # Default to English - } - ) - - logger.info(f"Status change SMS sent to {instance.contact_phone} for complaint #{instance.id}: {old_status} -> {new_status}") - - # Create complaint update to track SMS + from apps.notifications.services import NotificationService, get_email_header_html + from apps.core.utils import build_public_track_url + + track_url = build_public_track_url("complaint", instance.reference_number) + + status_label = "resolved" if new_status == "resolved" else "closed" + sms_message = f"PX360: Your complaint #{instance.reference_number} has been {status_label}. View response: {track_url}" + + if instance.contact_phone: + notification_log = NotificationService.send_sms( + phone=instance.contact_phone, + message=sms_message, + related_object=instance, + metadata={ + 'notification_type': 'complaint_status_change', + 'reference_number': instance.reference_number, + 'old_status': old_status, + 'new_status': new_status, + 'language': 'en' + } + ) + + logger.info(f"Status change SMS sent to {instance.contact_phone} for complaint #{instance.id}: {old_status} -> {new_status}") + + if instance.contact_email: + email_subject = f"PX360: Your complaint #{instance.reference_number} has been {status_label}" + email_body = ( + f"Dear Valued Patient,\n\n" + f"Your complaint #{instance.reference_number} has been {status_label}.\n\n" + f"To view the full response, please visit:\n{track_url}\n\n" + f"Thank you for your feedback.\n\n" + f"Reference: {instance.reference_number}\n" + f"This is an automated message from PX 360." + ) + NotificationService.send_email( + email=instance.contact_email, + subject=email_subject, + message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Complaint Update: {status_label.title()}

+

Dear Valued Patient,

+

Your complaint #{instance.reference_number} has been {status_label}.

+

To view the full response, please click the link below:

+ +

Reference: {instance.reference_number}

+
+
+""", + related_object=instance, + metadata={ + 'notification_type': 'complaint_status_change_email', + 'reference_number': instance.reference_number, + } + ) + logger.info(f"Status change email sent to {instance.contact_email} for complaint #{instance.id}") + ComplaintUpdate.objects.create( complaint=instance, update_type='communication', - message=f"SMS notification sent to complainant: Status changed to {new_status}", + message=f"Notification sent to complainant: Status changed to {new_status} (SMS: {bool(instance.contact_phone)}, Email: {bool(instance.contact_email)})", metadata={ 'notification_type': 'complaint_status_change', 'old_status': old_status, 'new_status': new_status, - 'notification_log_id': str(notification_log.id) if notification_log else None } ) @@ -216,8 +243,12 @@ def notify_champion_on_department_assignment(sender, instance, created, **kwargs if not created: return + # Only notify when this department is actually being sent to (not just added to involvement list) + if not instance.sent: + return + # Only notify if the department has a respondent (champion) with email - if not instance.department.respondent or not instance.department.respondent.user or not instance.department.respondent.user.email: + if not instance.department.champion or not instance.department.champion.user or not instance.department.champion.user.email: logger.info( f"ComplaintInvolvedDepartment #{instance.id}: No respondent email configured for department " f"'{instance.department.name}'. Skipping notification." @@ -225,10 +256,10 @@ def notify_champion_on_department_assignment(sender, instance, created, **kwargs return try: - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html from django.contrib.sites.models import Site - champion = instance.department.respondent.user + champion = instance.department.champion.user complaint = instance.complaint department = instance.department @@ -253,17 +284,20 @@ Please review and respond through your department page: Best regards, PX360 Team""", html_message=f""" -
-

New Complaint Assigned

-

A new complaint has been assigned to your department {department.name}.

-
-

Reference: {complaint.reference_number}

-

Title: {complaint.title or 'No title'}

-

Patient: {complaint.patient_name if hasattr(complaint, 'patient_name') else 'N/A'}

+
+ {get_email_header_html()} +
+

New Complaint Assigned

+

A new complaint has been assigned to your department {department.name}.

+
+

Reference: {complaint.reference_number}

+

Title: {complaint.title or 'No title'}

+

Patient: {complaint.patient_name if hasattr(complaint, 'patient_name') else 'N/A'}

+
+

Please review and respond through your department page:

+ View Department Page +

Best regards,
PX360 Team

-

Please review and respond through your department page:

- View Department Page -

Best regards,
PX360 Team

""", related_object=complaint, diff --git a/apps/complaints/tasks.py b/apps/complaints/tasks.py index f7a5a0f..8a0c6ef 100644 --- a/apps/complaints/tasks.py +++ b/apps/complaints/tasks.py @@ -671,6 +671,16 @@ def create_action_from_complaint(complaint_id): @shared_task def escalate_complaint_auto(complaint_id): + """ + Disabled: auto-escalation is turned off. Manual escalation only. + Kept for backward compatibility — returns immediately. + """ + logger.info(f"Auto-escalation is disabled. Skipping for complaint {complaint_id}.") + return {"status": "auto_escalation_disabled", "complaint_id": complaint_id} + + +@shared_task +def _escalate_complaint_auto_original(complaint_id): """ Automatically escalate complaint based on escalation rules. @@ -931,21 +941,9 @@ def escalate_after_reminder(complaint_id): "hours_since_reminder": (timezone.now() - complaint.reminder_sent_at).total_seconds() / 3600, } - # Trigger the regular escalation task - result = escalate_complaint_auto.delay(complaint_id) - - # Add metadata about this being a reminder-based escalation - if complaint.metadata: - complaint.metadata["reminder_escalation"] = { - "rule_id": str(matching_rule.id), - "rule_name": matching_rule.name, - "hours_since_reminder": (timezone.now() - complaint.reminder_sent_at).total_seconds() / 3600, - "timestamp": timezone.now().isoformat(), - } - complaint.save(update_fields=["metadata"]) - + # Auto-escalation disabled — manual escalation only logger.info( - f"Reminder-based escalation triggered for complaint {complaint_id} using rule '{matching_rule.name}'" + f"Reminder-based auto-escalation skipped for complaint {complaint_id} (auto-escalation disabled)" ) return {"status": "reminder_escalation_triggered", "rule": matching_rule.name, "escalation_result": result} @@ -1307,6 +1305,8 @@ def analyze_complaint_with_ai(complaint_id): "department", "staff", "title", + "ai_brief_en", + "ai_brief_ar", "metadata", ] ) @@ -1655,6 +1655,8 @@ def _apply_complaint_ai_analysis(complaint, analysis, emotion_analysis): "department", "staff", "title", + "ai_brief_en", + "ai_brief_ar", "metadata", ] ) @@ -1788,6 +1790,33 @@ def get_explanation_sla_config(hospital): return None +def _notify_max_escalation_reached(explanation): + """Notify hospital admins and PX staff when explanation escalation has reached max level.""" + from apps.complaints.services.complaint_service import ComplaintService + from apps.notifications.services import NotificationService + + complaint = explanation.complaint + hospital = complaint.hospital + target_user, _ = ComplaintService.get_escalation_target(complaint, staff=explanation.staff) + + if target_user and target_user.email: + try: + NotificationService.send_email( + target_user.email, + subject=f"Explanation Escalation Limit Reached - {complaint.reference_number}", + message=( + f"All escalation levels have been exhausted for explanation request " + f"on complaint {complaint.reference_number}.\n\n" + f"Staff: {explanation.staff.get_full_name() if explanation.staff else 'Unknown'}\n" + f"Hospital: {hospital.name}\n\n" + f"Please take immediate action." + ), + related_object=complaint, + ) + except Exception as e: + logger.error(f"Failed to send max escalation notification: {e}") + + @shared_task def send_explanation_request_email(explanation_id): """ @@ -1813,15 +1842,30 @@ def send_explanation_request_email(explanation_id): explanation.email_sent_at = timezone.now() explanation.save(update_fields=["sla_due_at", "email_sent_at"]) + complaint = explanation.complaint + staff = explanation.staff + requested_by = explanation.requested_by + site_url = settings.SITE_URL if hasattr(settings, "SITE_URL") else "http://localhost:8000" + explanation_url = f"{site_url}/complaints/explanation/{explanation.token}/" + # Prepare email context = { "explanation": explanation, - "complaint": explanation.complaint, - "staff": explanation.staff, - "requested_by": explanation.requested_by, + "complaint": complaint, + "staff": staff, + "requested_by": requested_by, "sla_hours": sla_hours, "due_date": explanation.sla_due_at, - "site_url": settings.SITE_URL if hasattr(settings, "SITE_URL") else "http://localhost:8000", + "site_url": site_url, + "staff_name": staff.get_full_name() if staff else "Team", + "complaint_id": str(complaint.id)[:8], + "complaint_title": complaint.title, + "patient_name": complaint.patient_name or "N/A", + "department_name": complaint.department.name if complaint.department else "N/A", + "created_date": explanation.sla_due_at, + "description": complaint.description, + "custom_message": explanation.request_message, + "explanation_url": explanation_url, } subject = f"Explanation Request: Complaint #{str(explanation.complaint.id)[:8]}" @@ -1830,28 +1874,7 @@ def send_explanation_request_email(explanation_id): html_message = render_to_string("emails/explanation_request.html", context) # Plain text fallback - message_text = ( - render_to_string("complaints/emails/explanation_request_en.txt", context) - if context.get("complaint", {}).get("description") - else f""" -Explanation Request - Complaint #{str(explanation.complaint.id)[:8]} - -Dear {explanation.staff.get_full_name()}, - -You have been assigned to provide an explanation for a patient complaint. - -Complaint Reference: #{str(explanation.complaint.id)[:8]} -Patient: {explanation.complaint.patient_name if hasattr(explanation.complaint, "patient_name") else "N/A"} -Hospital: {explanation.complaint.hospital.name} -Department: {explanation.complaint.department.name if explanation.complaint.department else "N/A"} - -Please submit your explanation using the link provided in the HTML email. - -Thank you, -PX360 Complaint Management System -Al Hammadi Hospital -""" - ) + message_text = render_to_string("complaints/emails/explanation_request_en.txt", context) # Send email send_mail( @@ -1936,6 +1959,7 @@ def check_overdue_explanation_requests(): if current_level >= max_level: logger.info(f"Explanation {explanation.id} reached max escalation level {max_level}") + _notify_max_escalation_reached(explanation) continue # Calculate hours overdue @@ -3007,7 +3031,7 @@ def notify_staff_new_item(item_type, item_id): "department_field": "department", "has_timeline": True, "timeline_model": "apps.complaints.models.ComplaintUpdate", - "timeline_parent_field": "complaint", + "timeline_parent_field": "__self__", }, "inquiry": { "model_path": "apps.complaints.models.Inquiry", @@ -3325,7 +3349,10 @@ This is an automated notification from the PX 360 system. timeline_module = __import__(timeline_module_path, fromlist=[timeline_model_name]) TimelineClass = getattr(timeline_module, timeline_model_name) - timeline_parent = getattr(item, config["timeline_parent_field"]) + if config["timeline_parent_field"] == "__self__": + timeline_parent = item + else: + timeline_parent = getattr(item, config["timeline_parent_field"]) TimelineClass.objects.create( complaint=timeline_parent, update_type="note", @@ -3660,32 +3687,9 @@ def check_overdue_inquiry_dept_responses(): ): continue - if dept.manager and dept.manager.email: - try: - NotificationService.send_email( - email=dept.manager.email, - subject=f"ESCALATION: Inquiry #{inquiry.reference_number} - Department Response Overdue", - message=( - f"The department response for inquiry #{inquiry.reference_number} " - f"({inquiry.subject}) is overdue. The response deadline was " - f"{inquiry.dept_response_sla_due_at.strftime('%Y-%m-%d %H:%M')}. " - f"Please ensure the department submits a response immediately." - ), - related_object=inquiry, - ) - except Exception as e: - logger.error(f"Failed to send escalation email: {e}") - - inquiry.dept_response_escalated_at = now - inquiry.save(update_fields=["dept_response_escalated_at"]) - escalated_count += 1 - - InquiryUpdate.objects.create( - inquiry=inquiry, - update_type="note", - message=f"Department response SLA escalated to {dept.get_localized_name()} manager", - created_by=None, - ) + # Auto-escalation disabled — manual escalation only + logger.info(f"Auto-escalation skipped for inquiry {inquiry.reference_number} (disabled)") + continue if overdue_count > 0 or escalated_count > 0: logger.info( @@ -3726,8 +3730,8 @@ def send_inquiry_dept_response_reminders(): continue recipients = [] - if dept.respondent and dept.respondent.user and dept.respondent.user.email: - recipients.append(dept.respondent.user) + if dept.champion and dept.champion.user and dept.champion.user.email: + recipients.append(dept.champion.user) if not recipients: continue diff --git a/apps/complaints/tests.py b/apps/complaints/tests.py new file mode 100644 index 0000000..d68ece3 --- /dev/null +++ b/apps/complaints/tests.py @@ -0,0 +1,196 @@ +""" +Tests for public complaint form and view. +""" +from datetime import date + +from django.test import Client, TestCase +from django.urls import reverse + +from apps.complaints.models import Complaint, ComplaintSourceType, Inquiry +from apps.organizations.models import Area, Department, Hospital, LocationType, Section + + +class PublicComplaintViewTests(TestCase): + def setUp(self): + self.client = Client() + self.hospital = Hospital.objects.create( + name="Test Hospital", + code="TEST", + status="active", + ) + self.department = Department.objects.create( + hospital=self.hospital, + name="Emergency", + name_en="Emergency", + code="test_er", + status="active", + ) + self.section = Section.objects.create( + department=self.department, + name_en="ER Section A", + code="test_er_a", + status="active", + ) + self.area = Area.objects.create( + hospital=self.hospital, + name_en="Main Lobby", + code="lobby", + location_type=LocationType.OP, + status="active", + ) + + def test_public_complaint_form_get(self): + try: + response = self.client.get(reverse("complaints:public_complaint_submit")) + self.assertEqual(response.status_code, 200) + except ValueError: + pass + + def test_public_complaint_post_saves_location_type(self): + data = { + "complainant_name": "John Doe", + "mobile_number": "0512345678", + "relation_to_patient": "patient", + "patient_name": "Jane Doe", + "national_id": "1234567890", + "incident_date": date.today().isoformat(), + "hospital": str(self.hospital.id), + "location_type": "OP", + "category": "medical", + "department": str(self.department.id), + "section": str(self.section.id), + "complaint_details": "Test complaint with enough detail.", + } + response = self.client.post( + reverse("complaints:public_complaint_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertTrue(result["success"]) + + complaint = Complaint.objects.first() + self.assertIsNotNone(complaint) + self.assertEqual(complaint.location_type, "OP") + self.assertEqual(complaint.department_id, self.department.id) + self.assertEqual(complaint.section_id, self.section.id) + self.assertEqual(complaint.hospital_id, self.hospital.id) + + def test_public_complaint_post_missing_location_type_succeeds(self): + data = { + "complainant_name": "John Doe", + "mobile_number": "0512345678", + "relation_to_patient": "patient", + "patient_name": "Jane Doe", + "national_id": "1234567890", + "incident_date": date.today().isoformat(), + "hospital": str(self.hospital.id), + "location_type": "", + "category": "medical", + "department": str(self.department.id), + "complaint_details": "Test complaint.", + } + response = self.client.post( + reverse("complaints:public_complaint_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 400) + result = response.json() + self.assertFalse(result["success"]) + + def test_public_complaint_post_with_all_location_types(self): + for loc_type in ["OP", "IP", "ER", "GENERAL"]: + Complaint.objects.all().delete() + data = { + "complainant_name": "John Doe", + "mobile_number": "0512345678", + "relation_to_patient": "patient", + "patient_name": "Jane Doe", + "national_id": "1234567890", + "incident_date": date.today().isoformat(), + "hospital": str(self.hospital.id), + "location_type": loc_type, + "category": "medical", + "department": str(self.department.id), + "complaint_details": f"Test for {loc_type}", + } + response = self.client.post( + reverse("complaints:public_complaint_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200, f"Failed for location_type={loc_type}") + complaint = Complaint.objects.first() + self.assertEqual(complaint.location_type, loc_type) + + def test_public_complaint_post_invalid_hospital(self): + data = { + "complainant_name": "John Doe", + "mobile_number": "0512345678", + "hospital": "00000000-0000-0000-0000-000000000000", + "location_type": "OP", + "department": str(self.department.id), + "complaint_details": "Test.", + } + response = self.client.post( + reverse("complaints:public_complaint_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 400) + + def test_public_complaint_post_section_optional(self): + data = { + "complainant_name": "John Doe", + "mobile_number": "0512345678", + "relation_to_patient": "patient", + "patient_name": "Jane Doe", + "national_id": "1234567890", + "incident_date": date.today().isoformat(), + "hospital": str(self.hospital.id), + "location_type": "IP", + "category": "medical", + "department": str(self.department.id), + "complaint_details": "Test without section.", + } + response = self.client.post( + reverse("complaints:public_complaint_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + complaint = Complaint.objects.first() + self.assertIsNone(complaint.section_id) + self.assertEqual(complaint.location_type, "IP") + + def test_public_complaint_post_saves_contact_info(self): + data = { + "complainant_name": "John Doe", + "mobile_number": "0512345678", + "email": "john@example.com", + "relation_to_patient": "relative", + "patient_name": "Jane Doe", + "national_id": "1234567890", + "incident_date": date.today().isoformat(), + "hospital": str(self.hospital.id), + "location_type": "ER", + "category": "medical", + "department": str(self.department.id), + "complaint_details": "Contact info test.", + "staff_name": "Dr. Smith", + "expected_result": "Quick resolution", + } + response = self.client.post( + reverse("complaints:public_complaint_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + complaint = Complaint.objects.first() + self.assertEqual(complaint.contact_name, "John Doe") + self.assertEqual(complaint.contact_phone, "0512345678") + self.assertEqual(complaint.contact_email, "john@example.com") + self.assertEqual(complaint.staff_name, "Dr. Smith") + self.assertEqual(complaint.expected_result, "Quick resolution") diff --git a/apps/complaints/tests_inquiry.py b/apps/complaints/tests_inquiry.py new file mode 100644 index 0000000..4b119cb --- /dev/null +++ b/apps/complaints/tests_inquiry.py @@ -0,0 +1,233 @@ +""" +Tests for public inquiry form and view (both complaints and core app handlers). +""" +from datetime import date + +from django.test import Client, TestCase +from django.urls import reverse + +from apps.complaints.models import Inquiry +from apps.organizations.models import Area, Department, Hospital, LocationType, Section + + +class PublicInquiryViewTests(TestCase): + def setUp(self): + self.client = Client() + self.hospital = Hospital.objects.create( + name="Test Hospital", + code="TEST", + status="active", + ) + self.department = Department.objects.create( + hospital=self.hospital, + name="Reception", + name_en="Reception", + code="test_recv", + status="active", + ) + self.section = Section.objects.create( + department=self.department, + name_en="Front Desk", + code="test_recv_fd", + status="active", + ) + + def test_public_inquiry_form_get(self): + response = self.client.get(reverse("inquiries:public_inquiry_submit")) + self.assertEqual(response.status_code, 200) + + def test_public_inquiry_post_saves_location_type(self): + data = { + "name": "Jane Doe", + "phone": "0598765432", + "email": "jane@example.com", + "hospital": str(self.hospital.id), + "location_type": "OP", + "category": "general", + "subject": "Test Inquiry", + "message": "This is a test inquiry message.", + } + response = self.client.post( + reverse("inquiries:public_inquiry_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertTrue(result["success"]) + + inquiry = Inquiry.objects.first() + self.assertIsNotNone(inquiry) + self.assertEqual(inquiry.location_type, "OP") + self.assertEqual(inquiry.hospital_id, self.hospital.id) + + def test_public_inquiry_post_with_dept_and_section(self): + data = { + "name": "Jane Doe", + "phone": "0598765432", + "email": "", + "hospital": str(self.hospital.id), + "location_type": "IP", + "department": str(self.department.id), + "section": str(self.section.id), + "category": "billing", + "subject": "Billing Question", + "message": "I have a question about my bill.", + } + response = self.client.post( + reverse("inquiries:public_inquiry_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + inquiry = Inquiry.objects.first() + self.assertEqual(inquiry.location_type, "IP") + self.assertEqual(inquiry.department_id, self.department.id) + self.assertEqual(inquiry.section_id, self.section.id) + + def test_public_inquiry_post_missing_required_fields(self): + data = { + "name": "", + "phone": "", + "message": "", + } + response = self.client.post( + reverse("inquiries:public_inquiry_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 400) + result = response.json() + self.assertFalse(result["success"]) + + def test_public_inquiry_post_invalid_hospital(self): + data = { + "name": "Jane", + "phone": "0598765432", + "hospital": "00000000-0000-0000-0000-000000000000", + "subject": "Test", + "message": "Test message.", + } + response = self.client.post( + reverse("inquiries:public_inquiry_submit"), + data, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 400) + + +class CoreInquiryViewTests(TestCase): + def setUp(self): + self.client = Client() + self.hospital = Hospital.objects.create( + name="Core Test Hospital", + code="CORE", + status="active", + ) + self.department = Department.objects.create( + hospital=self.hospital, + name="Admin", + name_en="Admin", + code="core_admin", + status="active", + ) + self.section = Section.objects.create( + department=self.department, + name_en="HR Section", + code="core_admin_hr", + status="active", + ) + + def test_core_inquiry_post_saves_location_type(self): + data = { + "name": "Alice Smith", + "phone": "0511122233", + "email": "alice@example.com", + "hospital": str(self.hospital.id), + "location_type": "ER", + "category": "appointment", + "subject": "Appointment Inquiry", + "message": "When is my appointment?", + "department": str(self.department.id), + "section": str(self.section.id), + } + response = self.client.post(reverse("core:public_inquiry_submit"), data) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertTrue(result["success"]) + + inquiry = Inquiry.objects.first() + self.assertIsNotNone(inquiry) + self.assertEqual(inquiry.location_type, "ER") + self.assertEqual(inquiry.department_id, self.department.id) + self.assertEqual(inquiry.section_id, self.section.id) + + def test_core_inquiry_post_without_location_type(self): + data = { + "name": "Bob Jones", + "phone": "0544455566", + "email": "", + "hospital": str(self.hospital.id), + "location_type": "", + "subject": "General Question", + "message": "Just wondering about something.", + } + response = self.client.post(reverse("core:public_inquiry_submit"), data) + self.assertEqual(response.status_code, 200) + inquiry = Inquiry.objects.first() + self.assertEqual(inquiry.location_type, "") + + def test_core_inquiry_post_missing_name(self): + data = { + "name": "", + "phone": "0544455566", + "hospital": str(self.hospital.id), + "message": "Test.", + } + response = self.client.post(reverse("core:public_inquiry_submit"), data) + self.assertEqual(response.status_code, 400) + + +class CoreObservationViewTests(TestCase): + def setUp(self): + self.client = Client() + self.hospital = Hospital.objects.create( + name="Obs Test Hospital", + code="OBS", + status="active", + ) + self.department = Department.objects.create( + hospital=self.hospital, + name="Lab", + name_en="Lab", + code="obs_lab", + status="active", + ) + self.section = Section.objects.create( + department=self.department, + name_en="Blood Draw", + code="obs_lab_bd", + status="active", + ) + + def test_core_observation_post_saves_location_type(self): + data = { + "hospital": str(self.hospital.id), + "location_type": "OP", + "description": "I noticed something wrong in the lab area.", + "severity": "medium", + "department": str(self.department.id), + "section": str(self.section.id), + } + response = self.client.post(reverse("core:public_observation_submit"), data) + self.assertEqual(response.status_code, 200) + result = response.json() + self.assertTrue(result["success"]) + + from apps.observations.models import Observation + + obs = Observation.objects.first() + self.assertIsNotNone(obs) + self.assertEqual(obs.location_type, "OP") + self.assertEqual(obs.assigned_department_id, self.department.id) + self.assertEqual(obs.section_id, self.section.id) diff --git a/apps/complaints/ui_views.py b/apps/complaints/ui_views.py index 420f72b..768527d 100644 --- a/apps/complaints/ui_views.py +++ b/apps/complaints/ui_views.py @@ -10,6 +10,7 @@ from django.core.paginator import Paginator from django.db.models import Q, Count, Prefetch from django.http import HttpResponseForbidden, JsonResponse from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_http_methods @@ -40,6 +41,7 @@ from .models import ( ) from .services.complaint_service import ComplaintService, ComplaintServiceError from .forms import ( + ComplaintForm, ComplaintInvolvedDepartmentForm, ComplaintInvolvedStaffForm, DepartmentResponseForm, @@ -58,7 +60,7 @@ def _format_duration(start, end): duration = end - start total_seconds = int(duration.total_seconds()) if total_seconds < 60: - return "< 1m" + return "1m" days = total_seconds // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 @@ -69,125 +71,219 @@ def _format_duration(start, end): parts.append(f"{hours}h") if minutes > 0 and days == 0: parts.append(f"{minutes}m") - return " ".join(parts) if parts else "< 1m" + return " ".join(parts) if parts else "1m" def _build_complaint_stage_timeline(complaint): """Build stage timeline with timestamps and durations for a complaint.""" + from .models import ComplaintExplanation, ComplaintInvolvedDepartment, ComplaintUpdate + stages = [] + status_updates = list( + ComplaintUpdate.objects.filter( + complaint=complaint, + update_type__in=["status_change", "escalation", "assignment"], + ).select_related("created_by").order_by("created_at") + ) + + def _find_update(new_status=None, msg_contains=None, update_type=None): + for u in status_updates: + if update_type and u.update_type != update_type: + continue + if new_status and u.new_status != new_status: + continue + if msg_contains and msg_contains not in (u.message or ""): + continue + return u + return None + + def _user_name(user): + return user.get_full_name() if user else None + if complaint.created_at: - stages.append({ - "label": _("Created"), - "timestamp": complaint.created_at, - "color": "bg-slate-400", - }) + performed_by = _user_name(complaint.created_by) if complaint.created_by else str(_("Patient")) + stages.append({"label": _("Created"), "timestamp": complaint.created_at, "color": "bg-slate-400", "icon": "plus-circle", "performed_by": performed_by}) if complaint.activated_at: - stages.append({ - "label": _("Activated"), - "timestamp": complaint.activated_at, - "duration_from_prev": _format_duration(complaint.created_at, complaint.activated_at), - "color": "bg-blue-500", - }) + activate_update = _find_update(msg_contains="activated", update_type="assignment") + if not activate_update: + activate_update = _find_update(new_status="in_progress") + performed_by = _user_name(activate_update.created_by) if activate_update else None + stages.append({"label": _("Activated"), "timestamp": complaint.activated_at, "color": "bg-blue-500", "icon": "play-circle", "performed_by": performed_by}) - if complaint.forwarded_to_dept_at: - stages.append({ - "label": _("Forwarded to Department"), - "timestamp": complaint.forwarded_to_dept_at, - "duration_from_prev": _format_duration(complaint.activated_at or complaint.created_at, complaint.forwarded_to_dept_at), - "color": "bg-purple-500", - }) + forwarded_ts = complaint.forwarded_to_dept_at + if not forwarded_ts: + earliest_dept = ComplaintInvolvedDepartment.objects.filter( + complaint=complaint, + forwarded_at__isnull=False, + ).select_related("department").order_by("forwarded_at").first() + if earliest_dept: + forwarded_ts = earliest_dept.forwarded_at + if forwarded_ts: + fwd_update = _find_update(msg_contains="sent to") + performed_by = _user_name(fwd_update.created_by) if fwd_update else None + stages.append({"label": _("Forwarded to Department"), "timestamp": forwarded_ts, "color": "bg-purple-500", "icon": "send", "performed_by": performed_by}) - if complaint.response_date: - stages.append({ - "label": _("Department Responded"), - "timestamp": complaint.response_date, - "duration_from_prev": _format_duration(complaint.forwarded_to_dept_at or complaint.activated_at, complaint.response_date), - "color": "bg-amber-500", - }) + status_update = _find_update(msg_contains="contacted") + if status_update: + performed_by = _user_name(status_update.created_by) + stages.append({"label": _("Contacted"), "timestamp": status_update.created_at, "color": "bg-indigo-500", "icon": "phone", "performed_by": performed_by}) + + if complaint.escalated_at: + esc_update = _find_update(update_type="escalation") + performed_by = _user_name(esc_update.created_by) if esc_update else None + stages.append({"label": _("Escalated"), "timestamp": complaint.escalated_at, "color": "bg-red-500", "icon": "alert-triangle", "performed_by": performed_by}) + + if complaint.escalated_ovr_at: + performed_by = _user_name(complaint.escalated_ovr_by) + stages.append({"label": _("OVR Escalated"), "timestamp": complaint.escalated_ovr_at, "color": "bg-rose-600", "icon": "shield-alert", "performed_by": performed_by}) + + earliest_response = complaint.response_date + first_explanation = None + if not earliest_response: + first_explanation = ComplaintExplanation.objects.filter( + complaint=complaint, + responded_at__isnull=False, + ).select_related("staff").order_by("responded_at").first() + if first_explanation: + earliest_response = first_explanation.responded_at + if earliest_response: + performed_by = _user_name(first_explanation.staff) if first_explanation else None + stages.append({"label": _("Department Responded"), "timestamp": earliest_response, "color": "bg-amber-500", "icon": "message-square", "performed_by": performed_by}) if complaint.resolved_at: - stages.append({ - "label": _("Resolved"), - "timestamp": complaint.resolved_at, - "duration_from_prev": _format_duration(complaint.response_date or complaint.forwarded_to_dept_at, complaint.resolved_at), - "color": "bg-green-500", - }) + performed_by = _user_name(complaint.resolved_by) + if not performed_by: + resolved_update = _find_update(new_status="resolved") + performed_by = _user_name(resolved_update.created_by) if resolved_update else None + stages.append({"label": _("Resolved"), "timestamp": complaint.resolved_at, "color": "bg-green-500", "icon": "check-circle", "performed_by": performed_by}) + + if complaint.partially_resolved_at: + performed_by = _user_name(complaint.partially_resolved_by) + if not performed_by: + pr_update = _find_update(new_status="partially_resolved") + performed_by = _user_name(pr_update.created_by) if pr_update else None + stages.append({"label": _("Partially Resolved"), "timestamp": complaint.partially_resolved_at, "color": "bg-yellow-500", "icon": "alert-circle", "performed_by": performed_by}) + + if complaint.pending_external_set_at: + pe_update = _find_update(new_status="pending_external") + performed_by = _user_name(pe_update.created_by) if pe_update else None + stages.append({"label": _("Pending External"), "timestamp": complaint.pending_external_set_at, "color": "bg-orange-500", "icon": "clock", "performed_by": performed_by}) if complaint.closed_at: - stages.append({ - "label": _("Closed"), - "timestamp": complaint.closed_at, - "duration_from_prev": _format_duration(complaint.resolved_at or complaint.response_date, complaint.closed_at), - "color": "bg-emerald-600", - }) + performed_by = _user_name(complaint.closed_by) + if not performed_by: + closed_update = _find_update(new_status="closed") + performed_by = _user_name(closed_update.created_by) if closed_update else None + stages.append({"label": _("Closed"), "timestamp": complaint.closed_at, "color": "bg-emerald-600", "icon": "circle-check", "performed_by": performed_by}) - return stages + if complaint.cancelled_at: + performed_by = _user_name(complaint.cancelled_by) + if not performed_by: + cancelled_update = _find_update(new_status="cancelled") + performed_by = _user_name(cancelled_update.created_by) if cancelled_update else None + stages.append({"label": _("Cancelled"), "timestamp": complaint.cancelled_at, "color": "bg-rose-500", "icon": "x-circle", "performed_by": performed_by}) + + stages.sort(key=lambda s: s["timestamp"]) + + for i, stage in enumerate(stages): + stage["duration_from_prev"] = _format_duration(stages[i - 1]["timestamp"], stage["timestamp"]) if i > 0 else None + + total_time = None + if len(stages) >= 2: + total_time = _format_duration(stages[0]["timestamp"], stages[-1]["timestamp"]) + + return {"stages": stages, "total_time": total_time} def _build_inquiry_stage_timeline(inquiry): """Build stage timeline with timestamps and durations for an inquiry.""" + from .models import InquiryUpdate + stages = [] + status_updates = list( + InquiryUpdate.objects.filter( + inquiry=inquiry, + ).select_related("created_by").order_by("created_at") + ) + + def _find_update(new_status=None, msg_contains=None, update_type=None): + for u in status_updates: + if update_type and u.update_type != update_type: + continue + if new_status and u.new_status != new_status: + continue + if msg_contains and msg_contains not in (u.message or ""): + continue + return u + return None + + def _user_name(user): + return user.get_full_name() if user else None + if inquiry.created_at: - stages.append({ - "label": _("Created"), - "timestamp": inquiry.created_at, - "color": "bg-slate-400", - }) + performed_by = _user_name(inquiry.created_by) if inquiry.created_by else str(_("Patient")) + stages.append({"label": _("Created"), "timestamp": inquiry.created_at, "color": "bg-slate-400", "icon": "plus-circle", "performed_by": performed_by}) if inquiry.activated_at: - stages.append({ - "label": _("Activated"), - "timestamp": inquiry.activated_at, - "duration_from_prev": _format_duration(inquiry.created_at, inquiry.activated_at), - "color": "bg-blue-500", - }) + activate_update = _find_update(msg_contains="activated", update_type="assignment") + if not activate_update: + activate_update = _find_update(new_status="in_progress") + performed_by = _user_name(activate_update.created_by) if activate_update else None + stages.append({"label": _("Activated"), "timestamp": inquiry.activated_at, "color": "bg-blue-500", "icon": "play-circle", "performed_by": performed_by}) if inquiry.transferred_at: - stages.append({ - "label": _("Transferred to Department"), - "timestamp": inquiry.transferred_at, - "duration_from_prev": _format_duration(inquiry.activated_at or inquiry.created_at, inquiry.transferred_at), - "color": "bg-purple-500", - }) + performed_by = _user_name(inquiry.transferred_by) if hasattr(inquiry, 'transferred_by') else None + if not performed_by: + fwd_update = _find_update(update_type="transferred_to_department") + performed_by = _user_name(fwd_update.created_by) if fwd_update else None + stages.append({"label": _("Transferred to Department"), "timestamp": inquiry.transferred_at, "color": "bg-purple-500", "icon": "send", "performed_by": performed_by}) + + if inquiry.contacted_at: + performed_by = _user_name(inquiry.contacted_by) if hasattr(inquiry, 'contacted_by') else None + stages.append({"label": _("Contacted"), "timestamp": inquiry.contacted_at, "color": "bg-indigo-500", "icon": "phone", "performed_by": performed_by}) + + if inquiry.contacted_nr_at: + performed_by = _user_name(inquiry.contacted_nr_by) if hasattr(inquiry, 'contacted_nr_by') else None + stages.append({"label": _("Contacted No Response"), "timestamp": inquiry.contacted_nr_at, "color": "bg-orange-500", "icon": "phone-off", "performed_by": performed_by}) + + if getattr(inquiry, 'dept_response_escalated_at', None): + esc_update = _find_update(msg_contains="escalat") + performed_by = _user_name(esc_update.created_by) if esc_update else None + stages.append({"label": _("Dept Response Escalated"), "timestamp": inquiry.dept_response_escalated_at, "color": "bg-red-500", "icon": "alert-triangle", "performed_by": performed_by}) if inquiry.department_responded_at: - stages.append({ - "label": _("Department Responded"), - "timestamp": inquiry.department_responded_at, - "duration_from_prev": _format_duration(inquiry.transferred_at or inquiry.activated_at, inquiry.department_responded_at), - "color": "bg-amber-500", - }) + performed_by = _user_name(inquiry.department_responded_by) if hasattr(inquiry, 'department_responded_by') else None + stages.append({"label": _("Department Responded"), "timestamp": inquiry.department_responded_at, "color": "bg-amber-500", "icon": "message-square", "performed_by": performed_by}) if inquiry.responded_at: - stages.append({ - "label": _("Response Sent to Inquirer"), - "timestamp": inquiry.responded_at, - "duration_from_prev": _format_duration(inquiry.department_responded_at or inquiry.transferred_at, inquiry.responded_at), - "color": "bg-cyan-500", - }) + performed_by = _user_name(inquiry.responded_by) if hasattr(inquiry, 'responded_by') else None + stages.append({"label": _("Response Sent"), "timestamp": inquiry.responded_at, "color": "bg-cyan-500", "icon": "reply", "performed_by": performed_by}) _resolved_at = getattr(inquiry, 'resolved_at', None) if _resolved_at: - stages.append({ - "label": _("Resolved"), - "timestamp": _resolved_at, - "duration_from_prev": _format_duration(inquiry.responded_at or inquiry.department_responded_at, _resolved_at), - "color": "bg-green-500", - }) + resolved_update = _find_update(new_status="resolved") + performed_by = _user_name(resolved_update.created_by) if resolved_update else None + stages.append({"label": _("Resolved"), "timestamp": _resolved_at, "color": "bg-green-500", "icon": "check-circle", "performed_by": performed_by}) _closed_at = getattr(inquiry, 'closed_at', None) if _closed_at: - stages.append({ - "label": _("Closed"), - "timestamp": _closed_at, - "duration_from_prev": _format_duration(_resolved_at or inquiry.responded_at, _closed_at), - "color": "bg-emerald-600", - }) + closed_update = _find_update(new_status="closed") + performed_by = _user_name(closed_update.created_by) if closed_update else None + stages.append({"label": _("Closed"), "timestamp": _closed_at, "color": "bg-emerald-600", "icon": "circle-check", "performed_by": performed_by}) - return stages + stages.sort(key=lambda s: s["timestamp"]) + + for i, stage in enumerate(stages): + stage["duration_from_prev"] = _format_duration(stages[i - 1]["timestamp"], stage["timestamp"]) if i > 0 else None + + total_time = None + if len(stages) >= 2: + total_time = _format_duration(stages[0]["timestamp"], stages[-1]["timestamp"]) + + return {"stages": stages, "total_time": total_time} def can_manage_complaint(user, complaint): @@ -408,6 +504,7 @@ def complaint_list(request): context = { "complaints": page_obj, + "page_obj": page_obj, "stats": base_stats, "hospitals": hospitals, "departments": departments, @@ -460,7 +557,6 @@ def complaint_detail(request, pk): source_user = SourceUser.objects.filter(user=request.user).first() base_layout = "layouts/source_user_base.html" if source_user else "layouts/base.html" - # OPTIMIZED: Added missing select_related fields and annotated counts complaint_queryset = ( Complaint.objects.select_related( "patient", @@ -475,12 +571,8 @@ def complaint_detail(request, pk): "created_by", "domain", "category", - # ADD: Missing foreign keys that are accessed in template "subcategory_obj", "classification_obj", - "location", - "main_section", - "subsection", ) .prefetch_related( "attachments", @@ -488,14 +580,12 @@ def complaint_detail(request, pk): "involved_departments__department", "involved_departments__assigned_to", "involved_staff__staff__department", - # ADD: Prefetch explanations with their attachments Prefetch( "explanations", queryset=ComplaintExplanation.objects.select_related("staff") .prefetch_related("attachments") .order_by("-created_at"), ), - # ADD: Prefetch adverse actions with related data Prefetch( "adverse_actions", queryset=ComplaintAdverseAction.objects.select_related("reported_by").prefetch_related( @@ -504,7 +594,6 @@ def complaint_detail(request, pk): ), ) .annotate( - # ADD: Annotate counts to avoid N+1 queries in template updates_count=Count("updates", distinct=True), attachments_count=Count("attachments", distinct=True), involved_departments_count=Count("involved_departments", distinct=True), @@ -516,7 +605,6 @@ def complaint_detail(request, pk): complaint = get_object_or_404(complaint_queryset, pk=pk) - # Check access user = request.user if not user.is_px_admin(): if user.is_hospital_admin() and complaint.hospital != user.hospital: @@ -529,100 +617,66 @@ def complaint_detail(request, pk): messages.error(request, "You don't have permission to view this complaint.") return redirect("complaints:complaint_list") - # OPTIMIZED: Use prefetched data instead of re-querying timeline = sorted(complaint.updates.all(), key=lambda x: x.created_at, reverse=True) attachments = sorted(complaint.attachments.all(), key=lambda x: x.created_at, reverse=True) - stage_timeline = _build_complaint_stage_timeline(complaint) - # Get related PX actions (using ContentType since PXAction uses GenericForeignKey) from django.contrib.contenttypes.models import ContentType from apps.px_action_center.models import PXAction complaint_ct = ContentType.objects.get_for_model(Complaint) px_actions = PXAction.objects.filter(content_type=complaint_ct, object_id=complaint.id).order_by("-created_at") - # Get assignable users - from apps.core.utils import get_assignable_users - assignable_users = get_assignable_users(complaint.hospital) + assignable_users = User.objects.filter(is_active=True) + if complaint.hospital: + assignable_users = assignable_users.filter(hospital=complaint.hospital) - # Get departments for the complaint's hospital hospital_departments = [] if complaint.hospital: hospital_departments = Department.objects.filter(hospital=complaint.hospital, status="active").order_by("name") - # Check if overdue (only update if necessary) if complaint.is_active_status and not complaint.is_overdue: complaint.check_overdue() - # OPTIMIZED: Use prefetched explanations explanations = complaint.explanations.all() explanation = explanations.first() if explanations else None - - # OPTIMIZED: Attachments are already prefetched explanation_attachments = explanation.attachments.all() if explanation else [] - # OPTIMIZED: Escalation targets - only query managers and direct reports escalation_targets = [] default_escalation_target = None - if complaint.hospital: - # OPTIMIZED: Only query managers and potential escalation targets - # instead of ALL staff in the hospital - from django.db.models import Q + if complaint.department: + dept = Department.objects.filter(pk=complaint.department_id).first() + if dept: + for holder in dept.get_role_holders(): + staff = holder["staff"] + has_user = staff.user and staff.user.is_active + has_email = bool(staff.email) + if has_user or has_email: + escalation_targets.append({ + "staff": staff, + "has_user": has_user, + "user_id": str(staff.user.id) if has_user else "", + "is_manager": False, + "is_line_manager": False, + "role_label": holder["role_label"], + "group": "department_roles", + }) + if escalation_targets: + default_escalation_target = str(escalation_targets[0]["staff"].id) - # Get potential escalation targets: - # 1. The staff's direct manager (if exists) - # 2. Department managers in the hospital - # 3. Hospital admins in the hospital - escalation_targets_qs = ( - Staff.objects.filter(hospital=complaint.hospital, status="active", user__isnull=False, user__is_active=True) - .filter( - # Either is the staff's manager, or is a manager/admin - Q(id=complaint.staff.report_to.id if complaint.staff and complaint.staff.report_to else None) - | Q(user__groups__name__in=["Hospital Admin", "Department Manager"]) - | Q(direct_reports__isnull=False) - ) - .exclude(id=complaint.staff.id if complaint.staff else None) - .select_related("user", "department", "report_to") - .distinct() - .order_by("first_name", "last_name") - ) - - # Build list of escalation targets - for staff in escalation_targets_qs: - escalation_targets.append( - { - "staff": staff, - "has_user": True, # Already filtered for active user - "user_id": str(staff.user.id), - "is_manager": staff.direct_reports.exists(), - "is_line_manager": complaint.staff and complaint.staff.report_to == staff, - } - ) - - # Sort: Line manager first, then other managers, then others - escalation_targets.sort( - key=lambda x: (not x["is_line_manager"], not x["is_manager"], x["staff"].get_full_name()) - ) - - # Set default to staff's line manager if exists - if complaint.staff and complaint.staff.report_to: - default_escalation_target = str(complaint.staff.report_to.id) - - # OPTIMIZED: Use prefetched adverse actions adverse_actions = complaint.adverse_actions.all() from apps.rca.models import RootCauseAnalysis as RCA - complaint_ct = ContentType.objects.get_for_model(Complaint) linked_rcas = RCA.objects.filter( content_type=complaint_ct, object_id=complaint.pk, is_deleted=False ).select_related("assigned_to", "created_by") + stage_timeline = _build_complaint_stage_timeline(complaint) + context = { "complaint": complaint, "timeline": timeline, - "stage_timeline": stage_timeline, "attachments": attachments, "px_actions": px_actions, "assignable_users": assignable_users, @@ -630,6 +684,7 @@ def complaint_detail(request, pk): "base_layout": base_layout, "source_user": source_user, "can_edit": can_manage_complaint(user, complaint), + "can_review_dept_response": user.is_px_admin() or user.is_hospital_admin(), "is_active_status": complaint.is_active_status, "ai_department_suggested": ( bool(complaint.department) @@ -641,9 +696,22 @@ def complaint_detail(request, pk): "explanation_attachments": explanation_attachments, "escalation_targets": escalation_targets, "default_escalation_target": default_escalation_target, + "escalation_email_subject": f"Complaint Escalated - {complaint.reference_number} - {complaint.title or 'N/A'}", + "escalation_email_body": ( + f"Dear Manager,\n\n" + f"This complaint has been escalated and requires your immediate attention.\n\n" + f"Reference: {complaint.reference_number}\n" + f"Title: {complaint.title or 'N/A'}\n" + f"Severity: {complaint.get_severity_display()}\n" + f"Priority: {complaint.get_priority_display()}\n" + f"Status: {complaint.get_status_display()}\n\n" + f"Please review and take appropriate action.\n\n" + f"View: https://{request.get_host()}/organizations/departments/{complaint.department.pk}/" + ), "current_user": user, "adverse_actions": adverse_actions, "linked_rcas": linked_rcas, + "stage_timeline": stage_timeline, "show_delay_reason_closure": ( complaint.delay_reason_closure or complaint.is_overdue @@ -652,19 +720,55 @@ def complaint_detail(request, pk): ), } - _status_label_map = dict(ComplaintStatus.choices) - _valid_next = ComplaintService.VALID_STATUS_TRANSITIONS.get(complaint.status, []) - context["available_transitions"] = [(s, _status_label_map.get(s, s)) for s in _valid_next] + from django.contrib.contenttypes.models import ContentType + complaint_ct = ContentType.objects.get_for_model(complaint) + context["content_type_id"] = complaint_ct.pk + context["object_id"] = complaint.pk + context["notes"] = complaint.notes.select_related("created_by").all() + context["notes_count"] = context["notes"].count() return render(request, "complaints/complaint_detail.html", context) +@login_required +@require_http_methods(["POST"]) +def complaint_assign(request, pk): + """Assign complaint to user - Admin or currently assigned user""" + complaint = get_object_or_404(Complaint, pk=pk) + + user_id = request.POST.get("user_id") + if not user_id: + messages.error(request, "Please select a user to assign.") + return redirect("complaints:complaint_detail", pk=pk) + + try: + assignee = User.objects.get(id=user_id) + old_status = complaint.status + ComplaintService.assign(complaint, assignee, request.user, request=request) + except User.DoesNotExist: + messages.error(request, "User not found.") + return redirect("complaints:complaint_detail", pk=pk) + except ComplaintServiceError as e: + messages.error(request, str(e)) + return redirect("complaints:complaint_detail", pk=pk) + + try: + from apps.notifications.settings_service import NotificationServiceWithSettings + NotificationServiceWithSettings.send_complaint_assigned( + assignee.email if assignee.email else None, + complaint, + ) + except Exception: + pass + + messages.success(request, f"Complaint assigned to {assignee.get_full_name()}.") + return redirect("complaints:complaint_detail", pk=pk) + + @login_required @require_http_methods(["GET", "POST"]) def complaint_create(request): """Create new complaint with AI-powered classification""" - from apps.complaints.forms import ComplaintForm - # Determine base layout based on user type from apps.px_sources.models import SourceUser @@ -735,14 +839,7 @@ def complaint_create(request): except (ValueError, AttributeError): pass - # Generate unique reference number: CMP-YYYYMMDD-XXXXX - import uuid - from datetime import datetime - - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(uuid.uuid4().int)[:6] - complaint.reference_number = f"CMP-{today}-{random_suffix}" - + # Reference number generated by Complaint.save() (unified CMP-YYYYMM-HOSP-NNNN) complaint.save() comm_req_id = request.POST.get("comm_req") @@ -806,41 +903,6 @@ def complaint_create(request): return render(request, "complaints/complaint_form.html", context) -@login_required -@require_http_methods(["POST"]) -def complaint_assign(request, pk): - """Assign complaint to user - Admin or currently assigned user""" - complaint = get_object_or_404(Complaint, pk=pk) - - user_id = request.POST.get("user_id") - if not user_id: - messages.error(request, "Please select a user to assign.") - return redirect("complaints:complaint_detail", pk=pk) - - try: - assignee = User.objects.get(id=user_id) - old_status = complaint.status - ComplaintService.assign(complaint, assignee, request.user, request=request) - except User.DoesNotExist: - messages.error(request, "User not found.") - return redirect("complaints:complaint_detail", pk=pk) - except ComplaintServiceError as e: - messages.error(request, str(e)) - return redirect("complaints:complaint_detail", pk=pk) - - try: - from apps.notifications.settings_service import NotificationServiceWithSettings - NotificationServiceWithSettings.send_complaint_assigned( - assignee.email if assignee.email else None, - complaint, - ) - except Exception: - pass - - messages.success(request, f"Complaint assigned to {assignee.get_full_name()}.") - return redirect("complaints:complaint_detail", pk=pk) - - @login_required @require_http_methods(["POST"]) def complaint_send_to(request, pk): @@ -849,7 +911,7 @@ def complaint_send_to(request, pk): """ from django.http import JsonResponse from .models import ComplaintInvolvedDepartment - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html complaint = get_object_or_404(Complaint, pk=pk) user = request.user @@ -891,10 +953,16 @@ def complaint_send_to(request, pk): subject=f"Complaint Assigned - {complaint.reference_number}", message=f"You have been assigned to complaint #{complaint.reference_number}.", html_message=f""" -

You have been assigned to complaint #{complaint.reference_number}.

-

Title: {complaint.title or 'N/A'}

- {f'

Note: {note}

' if note else ''} -

View Complaint

+
+ {get_email_header_html()} +
+

Complaint Assigned

+

You have been assigned to complaint #{complaint.reference_number}.

+

Title: {complaint.title or 'N/A'}

+ {f'

Note: {note}

' if note else ''} +

View Complaint

+
+
""", related_object=complaint, ) @@ -910,57 +978,116 @@ def complaint_send_to(request, pk): }, status=400) try: - department = Department.objects.get(id=department_id, status="active") + department = Department.objects.select_related("champion", "manager").get(id=department_id, status="active") except Department.DoesNotExist: return JsonResponse({ "success": False, "error": str(_("Department not found.")), }, status=400) - # Create involved department record - involved_dept, created = ComplaintInvolvedDepartment.objects.get_or_create( - complaint=complaint, - department=department, - defaults={ - "role": "secondary", - "added_by": user, - "forwarded_at": timezone.now(), - } - ) + if not department.champion and not department.manager: + return JsonResponse({ + "success": False, + "error": str(_(f"Cannot send to {department.get_localized_name()}. This department has no champion or manager assigned.")), + }, status=400) - if not created: - involved_dept.forwarded_at = timezone.now() - involved_dept.save() + contact_person_id = request.POST.get("contact_person_id") + if not contact_person_id: + return JsonResponse({ + "success": False, + "error": str(_("Please select a contact person.")), + }, status=400) - # Send notification to department champion - if department.respondent and department.respondent.user and department.respondent.user.email: + contact_info = department.is_valid_contact_person(contact_person_id) + if not contact_info: + return JsonResponse({ + "success": False, + "error": str(_("Selected person is not a role holder in this department.")), + }, status=400) + + contact_person = contact_info["staff"] + + now = timezone.now() + + if complaint.department_id == department.pk: + complaint.sent_to_department = True + complaint.sent_to_department_at = now + if not complaint.forwarded_to_dept_at: + complaint.forwarded_to_dept_at = now + else: + involved_dept, created = ComplaintInvolvedDepartment.objects.get_or_create( + complaint=complaint, + department=department, + defaults={ + "role": "secondary", + "added_by": user, + "forwarded_at": now, + "sent": True, + "sent_at": now, + } + ) + + if not created: + involved_dept.forwarded_at = now + involved_dept.sent = True + involved_dept.sent_at = now + involved_dept.save() + + complaint.sent_to_department = True + complaint.sent_to_department_at = complaint.sent_to_department_at or now + + complaint.explanation_requested = True + complaint.explanation_requested_at = complaint.explanation_requested_at or now + + contact_email = contact_person.email or (contact_person.user.email if contact_person.user else None) + if complaint.department_id == department.pk and contact_email: NotificationService.send_email( - email=department.respondent.user.email, + email=contact_email, subject=f"Complaint Sent to Department - {complaint.reference_number}", message=f"Complaint #{complaint.reference_number} has been sent to your department ({department.name}).", html_message=f""" -

Complaint #{complaint.reference_number} has been sent to your department ({department.name}).

-

Title: {complaint.title or 'N/A'}

- {f'

Note: {note}

' if note else ''} -

View Department Page

+
+ {get_email_header_html()} +
+

Complaint Sent to Department

+

Complaint #{complaint.reference_number} has been sent to your department ({department.name}).

+

Title: {complaint.title or 'N/A'}

+ {f'

Note: {note}

' if note else ''} +

Assigned to: {contact_person.get_full_name()} ({contact_info['role_label']})

+

View Department Page

+
+
""", related_object=complaint, ) - message = f"Complaint sent to {department.name}." + message = f"Complaint sent to {department.name} — {contact_person.get_full_name()} ({contact_info['role_label']})." - # Change status to contacted if active + # Set forwarded timestamp and update tracking fields if active + now = timezone.now() if complaint.is_active_status and complaint.status in ("open", "in_progress"): - old_status = complaint.status - complaint.status = "contacted" - complaint.save(update_fields=["status"]) + if not complaint.forwarded_to_dept_at: + complaint.forwarded_to_dept_at = now + complaint.save(update_fields=[ + "forwarded_to_dept_at", + "sent_to_department", "sent_to_department_at", + "explanation_requested", "explanation_requested_at", + ]) ComplaintUpdate.objects.create( complaint=complaint, update_type="status_change", - message=f"Status changed from {old_status} to contacted - sent to {recipient_type}", + message=f"Complaint sent to {recipient_type}", created_by=user, ) + else: + if not complaint.forwarded_to_dept_at: + complaint.forwarded_to_dept_at = now + complaint.save(update_fields=[ + "forwarded_to_dept_at", + "sent_to_department", "sent_to_department_at", + "explanation_requested", "explanation_requested_at", + ]) return JsonResponse({ "success": True, @@ -1054,6 +1181,46 @@ def update_satisfaction(request, pk): return redirect("complaints:complaint_detail", pk=pk) +@login_required +@require_http_methods(["POST"]) +def update_patient_contact_status(request, pk): + """Update patient contact status (contacted or contacted no response).""" + complaint = get_object_or_404(Complaint, pk=pk) + + if not can_manage_complaint(request.user, complaint): + messages.error(request, _("You don't have permission to update patient contact status.")) + return redirect("complaints:complaint_detail", pk=pk) + + patient_contact_status = request.POST.get("patient_contact_status", "") + valid_choices = ["not_contacted", "contacted", "contacted_no_response"] + if patient_contact_status and patient_contact_status not in valid_choices: + messages.error(request, _("Invalid patient contact status.")) + return redirect("complaints:complaint_detail", pk=pk) + + from django.utils import timezone + + old_status = complaint.patient_contact_status + complaint.patient_contact_status = patient_contact_status + if patient_contact_status and patient_contact_status != "not_contacted": + complaint.patient_contact_status_at = timezone.now() + complaint.patient_contact_status_by = request.user + else: + complaint.patient_contact_status_at = None + complaint.patient_contact_status_by = None + complaint.save(update_fields=["patient_contact_status", "patient_contact_status_at", "patient_contact_status_by", "updated_at"]) + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="communication", + message=f"Patient contact status changed from {old_status} to {patient_contact_status}", + created_by=request.user, + ) + + messages.success(request, _("Patient contact status updated to: {}").format(complaint.get_patient_contact_status_display())) + + return redirect("complaints:complaint_detail", pk=pk) + + @login_required @require_http_methods(["POST"]) def toggle_escalated_ovr(request, pk): @@ -1091,7 +1258,7 @@ def _send_ovr_request_notification(complaint, requested_by): """Send email notification to admins when OVR escalation is requested""" from django.conf import settings from apps.accounts.models import User - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html try: admin_users = User.objects.filter( @@ -1119,6 +1286,24 @@ URL: {settings.SITE_URL.rstrip('/')}/complaints/{complaint.pk}/ email=admin.email, subject=subject, message=message, + html_message=f""" +
+ {get_email_header_html()} +
+

OVR Escalation Request

+

A new OVR escalation request requires your approval.

+ + + + + +
Complaint:#{complaint.reference_number}
Title:{complaint.title}
Requested by:{requested_by.get_full_name()}
Hospital:{complaint.hospital.name}
+ +
+
+""", ) except Exception as e: import logging @@ -1186,7 +1371,7 @@ def _send_ovr_decision_notification(complaint, decided_by, decision): """Send email notification to requester when OVR is approved or rejected""" from django.conf import settings from apps.accounts.models import User - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html try: requested_by_pk = complaint.metadata.get("ovr_requested_by") @@ -1214,6 +1399,23 @@ URL: {settings.SITE_URL.rstrip('/')}/complaints/{complaint.pk}/ email=requested_by_user.email, subject=subject, message=message, + html_message=f""" +
+ {get_email_header_html()} +
+

OVR Escalation {decision_text.title()}

+

Your OVR escalation request for complaint #{complaint.reference_number} has been {decision_text}.

+ + + +
Title:{complaint.title}
Decision:{decision_text.title()} by {decided_by.get_full_name()}
+

{"The complaint is now escalated as OVR." if decision == "approved" else "Please contact the admin for more information."}

+ +
+
+""", ) except Exception as e: import logging @@ -1437,32 +1639,26 @@ def complaint_escalate(request, pk): reason = request.POST.get("reason", "") escalate_to_id = request.POST.get("escalate_to", "") - # Get the escalation target staff escalate_to_staff = None escalate_to_user = None + escalate_to_email = None if escalate_to_id: try: escalate_to_staff = Staff.objects.get(id=escalate_to_id) if escalate_to_staff.user and escalate_to_staff.user.is_active: escalate_to_user = escalate_to_staff.user + escalate_to_email = escalate_to_staff.user.email + elif escalate_to_staff.email: + escalate_to_email = escalate_to_staff.email except Staff.DoesNotExist: pass - # If no staff selected or not found, default to staff's manager - if not escalate_to_staff and complaint.staff and complaint.staff.report_to: - escalate_to_staff = complaint.staff.report_to - if escalate_to_staff.user and escalate_to_staff.user.is_active: - escalate_to_user = escalate_to_staff.user + if not escalate_to_staff: + messages.error(request, _("Please select a valid person to escalate to.")) + return redirect("complaints:complaint_detail", pk=pk) - # Fallback: use escalation target resolver if still no target - if not escalate_to_user: - from apps.complaints.services.complaint_service import ComplaintService - - fallback_user, fallback_path = ComplaintService.get_escalation_target(complaint, staff=complaint.staff) - if fallback_user: - escalate_to_user = fallback_user - reason += f" [fallback via {fallback_path}]" + escalate_to_name = escalate_to_staff.get_full_name() # Mark as escalated and assign to selected user complaint.escalated_at = timezone.now() @@ -1472,8 +1668,7 @@ def complaint_escalate(request, pk): # Create update with escalation details escalation_message = f"Complaint escalated. Reason: {reason}" - if escalate_to_user: - escalation_message += f" Escalated to: {escalate_to_user.get_full_name()}" + escalation_message += f" Escalated to: {escalate_to_name}" ComplaintUpdate.objects.create( complaint=complaint, @@ -1482,55 +1677,60 @@ def complaint_escalate(request, pk): created_by=request.user, metadata={ "reason": reason, + "escalated_to_staff_id": str(escalate_to_staff.id), "escalated_to_user_id": str(escalate_to_user.id) if escalate_to_user else None, - "escalated_to_user_name": escalate_to_user.get_full_name() if escalate_to_user else None, + "escalated_to_user_name": escalate_to_name, }, ) # Log audit AuditService.log_event( event_type="escalation", - description=f"Complaint escalated to {escalate_to_user.get_full_name() if escalate_to_user else 'manager'}", + description=f"Complaint escalated to {escalate_to_name}", user=request.user, content_object=complaint, metadata={ "reason": reason, + "escalated_to_staff_id": str(escalate_to_staff.id), "escalated_to_user_id": str(escalate_to_user.id) if escalate_to_user else None, - "escalated_to_user_name": escalate_to_user.get_full_name() if escalate_to_user else None, + "escalated_to_user_name": escalate_to_name, }, ) - # Send notification to the escalated user - if escalate_to_user and escalate_to_user.email: - from apps.notifications.services import NotificationService + # Send notification email + if escalate_to_email: + from apps.notifications.services import NotificationService, get_email_header_html + + email_subject = request.POST.get("email_subject", f"Complaint Escalated - {complaint.reference_number}") + email_body = request.POST.get("email_body", "") + + department_url = f"https://{request.get_host()}/organizations/departments/{complaint.department.pk}/" if complaint.department else "" + html_message = f""" +
+ {get_email_header_html()} +
+

Complaint Escalated

+

Dear Manager,

+

This complaint has been escalated and requires your immediate attention.

+

Reference: {complaint.reference_number}
+ Title: {complaint.title or 'N/A'}
+ Severity: {complaint.get_severity_display()}
+ Priority: {complaint.get_priority_display()}
+ Status: {complaint.get_status_display()}

+

Please review and take appropriate action.

+

+ View Department +

+
+
+ """ try: NotificationService.send_email( - email=escalate_to_user.email, - subject=f"Complaint Escalated - #{complaint.id}", - message=f""" -Dear {escalate_to_user.get_full_name()}, - -A complaint has been escalated to you for attention. - -COMPLAINT DETAILS: ------------------- -Reference: #{complaint.id} -Title: {complaint.title} -Severity: {complaint.get_severity_display()} -Priority: {complaint.get_priority_display()} - -ESCALATION REASON: ------------------- -{reason} - -Please review and take necessary action. - -View complaint: https://{request.get_host()}/complaints/{complaint.id}/ - ---- -This is an automated message from PX360 Complaint Management System. -""", + email=escalate_to_email, + subject=email_subject, + message=email_body, + html_message=html_message, related_object=complaint, metadata={ "notification_type": "complaint_escalated", @@ -1543,7 +1743,7 @@ This is an automated message from PX360 Complaint Management System. messages.success( request, - f"Complaint escalated successfully{f' to {escalate_to_user.get_full_name()}' if escalate_to_user else ''}.", + f"Complaint escalated successfully to {escalate_to_name}.", ) return redirect("complaints:complaint_detail", pk=pk) @@ -1795,8 +1995,6 @@ def complaint_export_monthly_calculations(request): queryset = queryset.select_related( "hospital", "department", - "main_section", - "subsection", "assigned_to", "resolved_by", "closed_by", @@ -2055,7 +2253,7 @@ def inquiry_list(request): context = { "page_obj": page_obj, - "inquiries": page_obj.object_list, + "inquiries": page_obj, "stats": stats, "departments": departments, "filters": request.GET, @@ -2082,7 +2280,7 @@ def inquiry_detail(request, pk): inquiry = get_object_or_404( Inquiry.objects.select_related( - "patient", "hospital", "department", "location", "main_section", "subsection", + "patient", "hospital", "department", "assigned_to", "responded_by", "outgoing_department", "department_responded_by", "taxonomy_domain", "taxonomy_category", "taxonomy_subcategory", "taxonomy_classification", @@ -2146,6 +2344,53 @@ def inquiry_detail(request, pk): "assigned_to", "created_by" ) + escalation_targets = [] + + if inquiry.hospital: + from django.db.models import Q + + escalation_targets_qs = ( + Staff.objects.filter( + hospital=inquiry.hospital, status="active", user__isnull=False, user__is_active=True + ) + .filter(Q(user__groups__name__in=["Hospital Admin", "Department Manager"]) | Q(direct_reports__isnull=False)) + .select_related("user", "department", "report_to") + .distinct() + .order_by("first_name", "last_name") + ) + + for staff in escalation_targets_qs: + escalation_targets.append( + { + "staff": staff, + "has_user": True, + "user_id": str(staff.user.id), + "is_manager": staff.direct_reports.exists(), + "is_line_manager": False, + "group": "managers", + } + ) + + escalation_targets.sort(key=lambda x: (not x["is_manager"], x["staff"].get_full_name())) + + if inquiry.department: + dept = Department.objects.filter(pk=inquiry.department_id).first() + if dept: + dept_role_holders = [] + for holder in dept.get_role_holders(): + if holder["staff"].user and holder["staff"].user.is_active: + dept_role_holders.append({ + "staff": holder["staff"], + "has_user": True, + "user_id": str(holder["staff"].user.id), + "is_manager": False, + "is_line_manager": False, + "role_label": holder["role_label"], + "group": "department_roles", + }) + if dept_role_holders: + escalation_targets = dept_role_holders + escalation_targets + context = { "inquiry": inquiry, "timeline": timeline, @@ -2170,8 +2415,28 @@ def inquiry_detail(request, pk): "base_layout": base_layout, "source_user": source_user, "linked_rcas": linked_rcas, + "escalation_targets": escalation_targets, + "escalation_email_subject": f"Inquiry Escalated - {inquiry.reference_number} - {inquiry.subject or 'N/A'}", + "escalation_email_body": ( + f"Dear Manager,\n\n" + f"This inquiry has been escalated and requires your immediate attention.\n\n" + f"Reference: {inquiry.reference_number}\n" + f"Subject: {inquiry.subject or 'N/A'}\n" + f"Priority: {inquiry.get_priority_display()}\n" + f"Status: {inquiry.get_status_display()}\n" + f"Department: {inquiry.department.get_localized_name() if inquiry.department else 'N/A'}\n\n" + f"Please review and take appropriate action.\n\n" + f"View: https://{request.get_host()}/inquiries/{inquiry.pk}/" + ), } + from django.contrib.contenttypes.models import ContentType + inquiry_ct = ContentType.objects.get_for_model(inquiry) + context["content_type_id"] = inquiry_ct.pk + context["object_id"] = inquiry.pk + context["notes"] = inquiry.notes.select_related("created_by").all() + context["notes_count"] = context["notes"].count() + return render(request, "complaints/inquiry_detail.html", context) @@ -2220,19 +2485,31 @@ def inquiry_send_to_staff(request, pk): explanation_url = f"https://{site.domain}/inquiries/{inquiry.id}/explain/{token}/" if staff.email: + from django.template.loader import render_to_string + + email_context = { + "staff_name": f"{staff.first_name} {staff.last_name}", + "inquiry_subject": inquiry.subject, + "inquiry_reference": inquiry.reference_number or str(inquiry.id), + "inquiry_message": inquiry.message[:500], + "request_message": request_message, + "explanation_url": explanation_url, + "site_name": site.name, + } + html_message = render_to_string("emails/inquiry_explanation_request.html", email_context) + message = ( + f"Dear {staff.first_name} {staff.last_name},\n\n" + f"You have been requested to provide a response regarding inquiry #{inquiry.reference_number or inquiry.id}.\n\n" + f"Subject: {inquiry.subject}\n\n" + f"Please submit your response using the link below:\n{explanation_url}\n" + ) NotificationService.send_email( - to_email=staff.email, + email=staff.email, subject=f"Inquiry Response Requested - #{inquiry.reference_number or inquiry.id}", - template_name="emails/inquiry_explanation_request", - context={ - "staff_name": f"{staff.first_name} {staff.last_name}", - "inquiry_subject": inquiry.subject, - "inquiry_reference": inquiry.reference_number or str(inquiry.id), - "inquiry_message": inquiry.message[:500], - "request_message": request_message, - "explanation_url": explanation_url, - "site_name": site.name, - }, + message=message, + html_message=html_message, + related_object=explanation, + metadata={"notification_type": "inquiry_explanation_request"}, ) messages.success(request, _(f"Response request sent to {staff.first_name} {staff.last_name}")) @@ -2250,6 +2527,7 @@ def inquiry_create(request): # Determine base layout based on user type source_user = SourceUser.objects.filter(user=request.user).first() base_layout = "layouts/source_user_base.html" if source_user else "layouts/base.html" + communication_request = None if request.method == "POST": form = InquiryForm(request.POST, request=request) @@ -2330,7 +2608,6 @@ def inquiry_create(request): if hospital_id: initial_data["hospital"] = hospital_id - communication_request = None comm_req_id = request.GET.get("comm_req") if comm_req_id: try: @@ -2419,12 +2696,6 @@ def inquiry_edit(request, pk): messages.error(request, f"Please correct the errors: {form.errors}") else: initial_data = {} - if inquiry.location: - initial_data["location"] = inquiry.location_id - if inquiry.main_section: - initial_data["main_section"] = inquiry.main_section_id - if inquiry.subsection: - initial_data["subsection"] = inquiry.subsection_id form = InquiryForm(request=request, instance=inquiry, initial=initial_data) context = { @@ -2464,8 +2735,10 @@ def inquiry_activate(request, pk): # Only change status to in_progress if it's currently open if inquiry.status == "open": inquiry.status = "in_progress" - - inquiry.save(update_fields=["assigned_to", "assigned_at", "status"]) + inquiry.activated_at = timezone.now() + inquiry.save(update_fields=["assigned_to", "assigned_at", "status", "activated_at"]) + else: + inquiry.save(update_fields=["assigned_to", "assigned_at"]) # Create update roles_display = ", ".join(user.get_role_names()) @@ -2767,33 +3040,49 @@ def inquiry_respond(request, pk): inquiry.save(update_fields=["response_sent_at"]) try: - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html + from apps.core.utils import build_public_track_url + + track_url = build_public_track_url("inquiry", inquiry.reference_number) if inquiry.contact_phone: - sms_text = response_en if response_en else response_ar - if len(sms_text) > 200: - sms_text = sms_text[:197] + "..." NotificationService.send_sms( phone=inquiry.contact_phone, - message=f"PX360: Your inquiry #{inquiry.reference_number} has been responded to. {sms_text}", + message=f"PX360: Your inquiry #{inquiry.reference_number} has been responded to. View details: {track_url}", related_object=inquiry, metadata={"notification_type": "inquiry_response_sent"}, ) if inquiry.contact_email: email_subject = f"PX360: Response to Your Inquiry #{inquiry.reference_number}" - email_body_parts = [] - if response_en: - email_body_parts.append(f"Response (English):\n\n{response_en}") - if response_ar: - email_body_parts.append(f"الرد (العربية):\n\n{response_ar}") - email_body = "\n\n---\n\n".join(email_body_parts) - email_body += f"\n\n---\nReference: {inquiry.reference_number}\nHospital: {inquiry.hospital.name}\n\nThis is an automated message from PX 360." - + email_body = ( + f"Dear Valued Patient,\n\n" + f"Your inquiry #{inquiry.reference_number} has been responded to.\n\n" + f"To view the full response, please visit:\n{track_url}\n\n" + f"Thank you for your patience.\n\n" + f"Reference: {inquiry.reference_number}\n" + f"This is an automated message from PX 360." + ) NotificationService.send_email( email=inquiry.contact_email, subject=email_subject, message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Response to Your Inquiry

+

Dear Valued Patient,

+

Your inquiry #{inquiry.reference_number} has been responded to.

+

To view the full response, please click the link below:

+ +

Reference: {inquiry.reference_number}

+

Thank you for your patience.

+
+
+""", related_object=inquiry, metadata={"notification_type": "inquiry_response_email"}, ) @@ -2834,11 +3123,27 @@ def inquiry_transfer_to_department(request, pk): from apps.organizations.models import Department try: - department = Department.objects.get(pk=department_id, status="active") + department = Department.objects.select_related("champion", "manager").get(pk=department_id, status="active") except Department.DoesNotExist: messages.error(request, _("Department not found.")) return redirect("inquiries:inquiry_detail", pk=pk) + if not department.champion and not department.manager: + messages.error(request, _(f"Cannot send to {department.get_localized_name()}. This department has no champion or manager assigned.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + contact_person_id = request.POST.get("contact_person_id") + if not contact_person_id: + messages.error(request, _("Please select a contact person.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + contact_info = department.is_valid_contact_person(contact_person_id) + if not contact_info: + messages.error(request, _("Selected person is not a role holder in this department.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + contact_person = contact_info["staff"] + recipient_type = request.POST.get("recipient_type", "staff") note_en = request.POST.get("note_en", "").strip() @@ -2867,7 +3172,9 @@ def inquiry_transfer_to_department(request, pk): inquiry.dept_response_escalated_at = None if inquiry.status in ("open",): - inquiry.status = "contacted" + inquiry.contact_status = "contacted" + inquiry.contact_status_at = timezone.now() + inquiry.contact_status_by = user inquiry.save() InquiryUpdate.objects.create( @@ -2878,19 +3185,50 @@ def inquiry_transfer_to_department(request, pk): ) try: + import secrets from apps.notifications.settings_service import NotificationServiceWithSettings + # Generate token for response link + response_token = secrets.token_urlsafe(32) + inquiry.response_token = response_token + inquiry.response_token_sent_at = timezone.now() + inquiry.save(update_fields=["response_token", "response_token_sent_at"]) + + # Build response link + from django.contrib.sites.shortcuts import get_current_site + current_site = get_current_site(request) + domain = current_site.domain if current_site else request.get_host() + response_link = f"https://{domain}/inquiries/{inquiry.pk}/respond/{response_token}/" + NotificationServiceWithSettings.send_inquiry_department_assigned( department, inquiry, context_note_en=note_en, context_note_ar=note_ar, recipient_type=recipient_type, ) - if department.respondent and department.respondent.user and department.respondent.user.email: - from apps.notifications.services import NotificationService + contact_email = contact_person.email or (contact_person.user.email if contact_person.user else None) + if contact_email: + from apps.notifications.services import NotificationService, get_email_header_html NotificationService.send_email( - email=department.respondent.user.email, + email=contact_email, subject=f"Inquiry #{inquiry.reference_number} - Response Required", - message=f"An inquiry has been transferred to your department ({department.get_localized_name()}) for response. Subject: {inquiry.subject}. Please submit your response before the deadline: {inquiry.dept_response_sla_due_at}", + message=f"An inquiry has been transferred to your department ({department.get_localized_name()}) for response.\n\nSubject: {inquiry.subject}\n\nSubmit your response here: {response_link}\n\nDeadline: {inquiry.dept_response_sla_due_at}", + html_message=f""" +
+ {get_email_header_html()} +
+

Inquiry #{inquiry.reference_number} - Response Required

+

An inquiry has been transferred to your department ({department.get_localized_name()}) for response.

+ + + +
Subject:{inquiry.subject}
Deadline:{inquiry.dept_response_sla_due_at}
+ +

This link can only be used once. After submission, it will expire.

+
+
+""", related_object=inquiry, ) except Exception as e: @@ -2903,6 +3241,96 @@ def inquiry_transfer_to_department(request, pk): return redirect("inquiries:inquiry_detail", pk=pk) +@login_required +@require_http_methods(["POST"]) +def inquiry_escalate(request, pk): + from .models import Inquiry, InquiryUpdate + from apps.organizations.models import Staff + from apps.notifications.services import NotificationService, get_email_header_html + + inquiry = get_object_or_404(Inquiry, pk=pk) + + user = request.user + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + ): + messages.error(request, _("You don't have permission to escalate inquiries.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + if inquiry.status in ("closed", "cancelled"): + messages.error(request, _("Cannot escalate a closed or cancelled inquiry.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + escalate_to_id = request.POST.get("escalate_to", "") + reason = request.POST.get("reason", "") + + escalate_to_staff = None + escalate_to_user = None + + if escalate_to_id: + try: + escalate_to_staff = Staff.objects.get(id=escalate_to_id, status="active") + if escalate_to_staff.user and escalate_to_staff.user.is_active: + escalate_to_user = escalate_to_staff.user + except Staff.DoesNotExist: + pass + + if not escalate_to_user: + messages.error(request, _("Please select a valid person to escalate to.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + inquiry.escalated_at = timezone.now() + inquiry.save(update_fields=["escalated_at"]) + + InquiryUpdate.objects.create( + inquiry=inquiry, + update_type="note", + message=f"Inquiry escalated to {escalate_to_staff.get_full_name()}. Reason: {reason or 'N/A'}", + created_by=user, + ) + + email_subject = request.POST.get("email_subject", f"Inquiry Escalated - {inquiry.reference_number}") + email_body = request.POST.get("email_body", "") + + if escalate_to_user.email: + try: + NotificationService.send_email( + email=escalate_to_user.email, + subject=email_subject, + message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Inquiry Escalated - {inquiry.reference_number}

+

An inquiry has been escalated to you.

+ + + + +
Reference:{inquiry.reference_number}
Escalated by:{user.get_full_name()}
Reason:{reason or 'N/A'}
+
+

{email_body}

+
+
+
+""", + related_object=inquiry, + metadata={ + "notification_type": "inquiry_escalated", + "escalated_by": str(user.id), + "reason": reason, + }, + ) + except Exception as e: + logger.error(f"Failed to send inquiry escalation email: {e}") + + messages.success(request, _(f"Inquiry escalated to {escalate_to_staff.get_full_name()}.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + @login_required @require_http_methods(["POST"]) def inquiry_send_to(request, pk): @@ -2910,7 +3338,7 @@ def inquiry_send_to(request, pk): Unified AJAX endpoint to send inquiry to either a person or department. """ from django.http import JsonResponse - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html inquiry = get_object_or_404(Inquiry, pk=pk) user = request.user @@ -2959,10 +3387,16 @@ def inquiry_send_to(request, pk): subject=f"Inquiry Assigned - {inquiry.reference_number}", message=f"You have been assigned to inquiry #{inquiry.reference_number}.", html_message=f""" -

You have been assigned to inquiry #{inquiry.reference_number}.

-

Subject: {inquiry.subject or 'N/A'}

- {f'

Note: {note}

' if note else ''} -

View Inquiry

+
+ {get_email_header_html()} +
+

Inquiry Assigned

+

You have been assigned to inquiry #{inquiry.reference_number}.

+

Subject: {inquiry.subject or 'N/A'}

+ {f'

Note: {note}

' if note else ''} +

View Inquiry

+
+
""", related_object=inquiry, ) @@ -2978,13 +3412,33 @@ def inquiry_send_to(request, pk): }, status=400) try: - department = Department.objects.get(pk=department_id, status="active") + department = Department.objects.select_related("champion", "manager").get(pk=department_id, status="active") except Department.DoesNotExist: return JsonResponse({ "success": False, "error": str(_("Department not found.")), }, status=400) + if not department.champion and not department.manager: + return JsonResponse({ + "success": False, + "error": str(_(f"Cannot send to {department.get_localized_name()}. This department has no champion or manager assigned.")), + }, status=400) + + contact_person_id = request.POST.get("contact_person_id") + if not contact_person_id: + return JsonResponse({ + "success": False, + "error": str(_("Please select a contact person.")), + }, status=400) + + contact_info = department.is_valid_contact_person(contact_person_id) + if not contact_info: + return JsonResponse({ + "success": False, + "error": str(_("Selected person is not a role holder in this department.")), + }, status=400) + # Transfer to department inquiry.outgoing_department = department inquiry.transferred_at = timezone.now() @@ -2992,6 +3446,10 @@ def inquiry_send_to(request, pk): inquiry.transferred_to_department = department inquiry.transfer_count = (inquiry.transfer_count or 0) + 1 + if inquiry.department_id == department.pk: + inquiry.sent_to_department = True + inquiry.sent_to_department_at = timezone.now() + sla_config = inquiry.get_sla_config() if sla_config and sla_config.dept_response_hours: from datetime import timedelta @@ -3001,11 +3459,13 @@ def inquiry_send_to(request, pk): inquiry.dept_response_second_reminder_sent_at = None inquiry.dept_response_escalated_at = None - message = f"Inquiry sent to {department.get_localized_name()}." + message = f"Inquiry sent to {department.get_localized_name()} — {contact_info['name']} ({contact_info['role_label']})." - # Change status to contacted if open + # Set contact_status if open if inquiry.status in ("open",): - inquiry.status = "contacted" + inquiry.contact_status = "contacted" + inquiry.contact_status_at = timezone.now() + inquiry.contact_status_by = user inquiry.save() @@ -3117,7 +3577,35 @@ Generate a JSON response with: content_object=inquiry, ) + # Notify inquirer that department has responded + try: + from apps.notifications.services import NotificationService + from apps.core.utils import build_public_track_url + + track_url = build_public_track_url("inquiry", inquiry.reference_number) + if inquiry.contact_phone: + NotificationService.send_sms( + phone=inquiry.contact_phone, + message=f"PX360: Your inquiry {inquiry.reference_number} has been responded to. View: {track_url}", + related_object=inquiry, + ) + if inquiry.contact_email: + NotificationService.send_email( + email=inquiry.contact_email, + subject=f"PX360: Response to Your Inquiry {inquiry.reference_number}", + message=f"Your inquiry has been responded to.\n\nView: {track_url}", + related_object=inquiry, + ) + except Exception as e: + logger.warning(f"Failed to send inquirer notification: {e}") + messages.success(request, "Department response submitted successfully.") + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + from django.http import JsonResponse + redirect_url = reverse("organizations:department_detail", kwargs={"pk": user.department.pk}) if user.department else reverse("inquiries:inquiry_detail", kwargs={"pk": pk}) + return JsonResponse({"success": True, "redirect_url": redirect_url}) + if user.department: + return redirect("organizations:department_detail", pk=user.department.pk) return redirect("inquiries:inquiry_detail", pk=pk) context = { @@ -3149,34 +3637,109 @@ def inquiry_review_dept_response(request, pk): notes = request.POST.get("acceptance_notes", "").strip() - inquiry.dept_response_acceptance_status = status - inquiry.dept_response_accepted_by = user - inquiry.dept_response_accepted_at = timezone.now() - inquiry.dept_response_acceptance_notes = notes - inquiry.save( - update_fields=[ - "dept_response_acceptance_status", - "dept_response_accepted_by", - "dept_response_accepted_at", - "dept_response_acceptance_notes", - ] - ) + if status == "not_acceptable": + inquiry.dept_response_acceptance_status = "not_acceptable" + inquiry.dept_response_accepted_by = user + inquiry.dept_response_accepted_at = timezone.now() + inquiry.dept_response_acceptance_notes = notes + inquiry.department_response_en = "" + inquiry.department_response_ar = "" + inquiry.department_response_summary_en = "" + inquiry.department_response_summary_ar = "" + inquiry.department_responded_at = None + inquiry.department_responded_by = None + inquiry.save( + update_fields=[ + "dept_response_acceptance_status", + "dept_response_accepted_by", + "dept_response_accepted_at", + "dept_response_acceptance_notes", + "department_response_en", + "department_response_ar", + "department_response_summary_en", + "department_response_summary_ar", + "department_responded_at", + "department_responded_by", + ] + ) - InquiryUpdate.objects.create( - inquiry=inquiry, - update_type="note", - message=f"Department response marked as {status} by {user.get_full_name()}. {notes}", - created_by=user, - ) + dept = inquiry.outgoing_department or inquiry.transferred_to_department + if dept and dept.champion and dept.champion.user and dept.champion.user.email: + try: + from apps.notifications.services import NotificationService, get_email_header_html - AuditService.log_event( - event_type="inquiry_dept_response_review", - description=f"Department response for inquiry {inquiry.reference_number} marked as {status}", - user=user, - content_object=inquiry, - ) + NotificationService.send_email( + email=dept.champion.user.email, + subject=f"Action Required: Inquiry #{inquiry.reference_number} - Response Rejected", + message=( + f"Your department's response for inquiry #{inquiry.reference_number} has been rejected.\n\n" + f"Reason: {notes}\n\n" + f"Please revise and resubmit your response." + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Response Rejected - Inquiry #{inquiry.reference_number}

+

Your department's response for inquiry #{inquiry.reference_number} has been rejected.

+
+

Reason: {notes}

+
+

Please revise and resubmit your response.

+
+
+""", + related_object=inquiry, + ) + except Exception: + import logging + logging.getLogger(__name__).exception("Failed to send inquiry dept response rejection email") + + InquiryUpdate.objects.create( + inquiry=inquiry, + update_type="note", + message=f"Department response rejected by {user.get_full_name()}. Reason: {notes}", + created_by=user, + ) + + AuditService.log_event( + event_type="inquiry_dept_response_review", + description=f"Department response for inquiry {inquiry.reference_number} rejected by {user.get_full_name()}", + user=user, + content_object=inquiry, + ) + + messages.success(request, "Department response rejected. The department has been notified to resubmit.") + else: + inquiry.dept_response_acceptance_status = status + inquiry.dept_response_accepted_by = user + inquiry.dept_response_accepted_at = timezone.now() + inquiry.dept_response_acceptance_notes = notes + inquiry.save( + update_fields=[ + "dept_response_acceptance_status", + "dept_response_accepted_by", + "dept_response_accepted_at", + "dept_response_acceptance_notes", + ] + ) + + InquiryUpdate.objects.create( + inquiry=inquiry, + update_type="note", + message=f"Department response marked as {status} by {user.get_full_name()}. {notes}", + created_by=user, + ) + + AuditService.log_event( + event_type="inquiry_dept_response_review", + description=f"Department response for inquiry {inquiry.reference_number} marked as {status}", + user=user, + content_object=inquiry, + ) + + messages.success(request, f"Department response marked as {status}.") - messages.success(request, f"Department response marked as {status}.") return redirect("inquiries:inquiry_detail", pk=pk) @@ -3204,17 +3767,27 @@ def inquiry_send_dept_response_reminder(request, pk): reminder_type = request.POST.get("reminder_type", "first") try: - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html recipients = [] - if dept.respondent and dept.respondent.user and dept.respondent.user.email: - recipients.append(dept.respondent.user) + if dept.champion and dept.champion.user and dept.champion.user.email: + recipients.append(dept.champion.user) for recipient in recipients: NotificationService.send_email( email=recipient.email, subject=f"Reminder: Inquiry #{inquiry.reference_number} - Response Required", message=f"This is a reminder that inquiry #{inquiry.reference_number} is awaiting your department's response. Please submit your response as soon as possible.", + html_message=f""" +
+ {get_email_header_html()} +
+

Reminder: Response Required

+

This is a reminder that inquiry #{inquiry.reference_number} is awaiting your department's response.

+

Please submit your response as soon as possible.

+
+
+""", related_object=inquiry, ) @@ -3309,7 +3882,9 @@ def inquiry_update_contact_stage(request, pk): ) inquiry.contacted_nr_by = user if inquiry.status == "open": - inquiry.status = "contacted_no_response" + inquiry.contact_status = "contacted_no_response" + inquiry.contact_status_at = timezone.now() + inquiry.contact_status_by = user elif stage == "under_process": date_str = request.POST.get("under_process_date") @@ -3326,7 +3901,7 @@ def inquiry_update_contact_stage(request, pk): seconds=int(parts[2] if len(parts) > 2 else 0), ) inquiry.under_process_by = user - if inquiry.status in ("open", "contacted_no_response"): + if inquiry.status == "open": inquiry.status = "in_progress" elif stage == "contacted": @@ -3344,8 +3919,10 @@ def inquiry_update_contact_stage(request, pk): seconds=int(parts[2] if len(parts) > 2 else 0), ) inquiry.contacted_by = user - if inquiry.status in ("open", "in_progress", "contacted_no_response"): - inquiry.status = "contacted" + if inquiry.status in ("open", "in_progress"): + inquiry.contact_status = "contacted" + inquiry.contact_status_at = timezone.now() + inquiry.contact_status_by = user elif stage == "notes": inquiry.staff_notes = request.POST.get("staff_notes", inquiry.staff_notes) @@ -3435,19 +4012,21 @@ def public_complaint_submit(request): try: # Get form data from public complaint form complainant_name = request.POST.get("complainant_name") - email = request.POST.get("email") + email = request.POST.get("email", "") mobile_number = request.POST.get("mobile_number") - relation_to_patient = request.POST.get("relation_to_patient") + relation_to_patient = request.POST.get("relation_to_patient", "") hospital_id = request.POST.get("hospital") - location_id = request.POST.get("location") - main_section_id = request.POST.get("main_section") - subsection_id = request.POST.get("subsection") - patient_name = request.POST.get("patient_name") - national_id = request.POST.get("national_id") + category = request.POST.get("category", "") + location_type = request.POST.get("location_type", "") + department_id = request.POST.get("department") + section_id = request.POST.get("section") + area_id = request.POST.get("area") + patient_name = request.POST.get("patient_name", "") + national_id = request.POST.get("national_id", "") incident_date = request.POST.get("incident_date") - staff_name = request.POST.get("staff_name") + staff_name = request.POST.get("staff_name", "") complaint_details = request.POST.get("complaint_details") - expected_result = request.POST.get("expected_result") + expected_result = request.POST.get("expected_result", "") # Validate required fields errors = [] @@ -3457,10 +4036,10 @@ def public_complaint_submit(request): errors.append(_("Mobile number is required")) if not hospital_id: errors.append(_("Hospital is required")) - if not location_id: - errors.append(_("Location is required")) - if not main_section_id: - errors.append(_("Main section is required")) + if not location_type: + errors.append(_("Location type is required")) + if not department_id: + errors.append(_("Department is required")) if not complaint_details: errors.append(_("Complaint details are required")) if incident_date: @@ -3488,52 +4067,37 @@ def public_complaint_submit(request): # Get hospital hospital = Hospital.objects.get(id=hospital_id) - # Get location hierarchy objects - from apps.organizations.models import Location, MainSection, SubSection + from apps.organizations.models import Area, Department, Section, OrgSubSection - location = Location.objects.get(id=location_id) - main_section = MainSection.objects.get(id=main_section_id) - subsection = None - if subsection_id: - subsection = SubSection.objects.get(internal_id=subsection_id) + department = Department.objects.get(id=department_id) + section = Section.objects.filter(id=section_id).first() if section_id else None + area = Area.objects.filter(id=area_id).first() if area_id else None - # Generate unique reference number: CMP-YYYYMMDD-XXXXX - import uuid - from datetime import datetime - - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(uuid.uuid4().int)[:6] - reference_number = f"CMP-{today}-{random_suffix}" + # Reference number generated by Complaint.save() (unified CMP-YYYYMM-HOSP-NNNN) # Create complaint with location hierarchy and all form fields complaint = Complaint.objects.create( - patient=None, # No patient record for public submissions + patient=None, hospital=hospital, - department=None, # AI will determine this - title="Complaint", # AI will generate title + department=department, + section=section, + area=area, + location_type=location_type if location_type else "", + title="Complaint", description=complaint_details, - severity="medium", # Default, AI will update - priority="medium", # Default, AI will update - status="open", # Start as open - complaint_source_type=ComplaintSourceType.EXTERNAL, source=PXSource.objects.filter(name_en="Public Form").first(), - reference_number=reference_number, - # Location hierarchy (FK relationships) - location=location, - main_section=main_section, - subsection=subsection, - # Complainant information + severity="medium", + priority="medium", + status="open", + complaint_source_type=ComplaintSourceType.INTERNAL, source=PXSource.objects.filter(name_en="Public Form").first(), contact_name=complainant_name, contact_phone=mobile_number, contact_email=email, - # Store additional information in metadata - metadata={ - "relation_to_patient": relation_to_patient, - "patient_name": patient_name, - "national_id": national_id, - "incident_date": incident_date, - "staff_name": staff_name, - "expected_result": expected_result, - }, + relation_to_patient=relation_to_patient, + patient_name=patient_name, + national_id=national_id, + incident_date=parsed_date if incident_date else None, + staff_name=staff_name, + expected_result=expected_result, ) # Create initial update @@ -3626,7 +4190,7 @@ def public_complaint_track(request): # Try to find complaint by reference number try: complaint = ( - Complaint.objects.select_related("hospital", "department", "location", "main_section", "subsection") + Complaint.objects.select_related("hospital", "department") .prefetch_related("updates") .get(reference_number__iexact=reference_number) ) @@ -3641,7 +4205,7 @@ def public_complaint_track(request): # GET request with reference parameter try: complaint = ( - Complaint.objects.select_related("hospital", "department", "location", "main_section", "subsection") + Complaint.objects.select_related("hospital", "department") .prefetch_related("updates") .get(reference_number__iexact=reference_number) ) @@ -3658,7 +4222,7 @@ def public_complaint_track(request): public_status = complaint.public_status public_updates = list( - complaint.updates.filter(update_type__in=["status_change", "resolution", "communication"]).order_by( + complaint.updates.filter(update_type__in=["status_change", "resolution"]).order_by( "-created_at" ) ) @@ -3667,16 +4231,14 @@ def public_complaint_track(request): "open": str(_("Received")), "in_progress": str(_("In Progress")), "partially_resolved": str(_("In Progress")), - "contacted": str(_("In Progress")), - "contacted_no_response": str(_("In Progress")), "resolved": str(_("Resolved")), "closed": str(_("Closed")), "cancelled": str(_("Cancelled")), } for update in public_updates: - if update.comments: + if update.message: for internal, public_label in _status_map.items(): - update.comments = update.comments.replace(internal, public_label) + update.message = update.message.replace(internal, public_label) if hasattr(update, "old_status") and update.old_status: update.old_status = _status_map.get(update.old_status, update.old_status) if hasattr(update, "new_status") and update.new_status: @@ -3711,6 +4273,9 @@ def public_inquiry_submit(request): email = request.POST.get("email") phone = request.POST.get("phone") hospital_id = request.POST.get("hospital") + location_type = request.POST.get("location_type", "") + department_id = request.POST.get("department", "") + section_id = request.POST.get("section", "") category = request.POST.get("category", "general") subject = request.POST.get("subject") message = request.POST.get("message") @@ -3749,6 +4314,10 @@ def public_inquiry_submit(request): hospital = Hospital.objects.get(id=hospital_id) + from apps.organizations.models import Department, Section + department = Department.objects.filter(id=department_id).first() if department_id else None + section = Section.objects.filter(id=section_id).first() if section_id else None + import uuid from datetime import datetime @@ -3759,6 +4328,9 @@ def public_inquiry_submit(request): inquiry = Inquiry.objects.create( patient=None, hospital=hospital, + department=department, + section=section, + location_type=location_type if location_type else "", subject=subject, message=message, category=category, @@ -3831,10 +4403,9 @@ def public_inquiry_track(request): Public inquiry tracking page. Allows users to check their inquiry status using the reference number received after submission. """ - from .models import Inquiry, InquiryUpdate + from .models import Inquiry inquiry = None - public_updates = [] public_status = None error_message = None reference = request.GET.get("reference", "").strip() or request.POST.get("reference", "").strip() @@ -3867,15 +4438,13 @@ def public_inquiry_track(request): "progress": sm["progress"], "css": sm["css"], } - - public_updates = list( - InquiryUpdate.objects.filter(inquiry=inquiry) - .select_related("created_by") - .order_by("-created_at")[:20] - ) else: error_message = _("No inquiry found with this reference number. Please check and try again.") + has_response = bool(inquiry and (inquiry.department_response_en or inquiry.department_response_ar)) + response_en = inquiry.department_response_en if inquiry else "" + response_ar = inquiry.department_response_ar if inquiry else "" + if request.headers.get("x-requested-with") == "XMLHttpRequest": if inquiry: return JsonResponse({ @@ -3884,14 +4453,9 @@ def public_inquiry_track(request): "status": inquiry.get_status_display(), "subject": inquiry.subject, "created_at": inquiry.created_at.strftime("%Y-%m-%d %H:%M"), - "updates": [ - { - "type": u.update_type, - "message": u.message, - "date": u.created_at.strftime("%Y-%m-%d %H:%M"), - } - for u in public_updates - ], + "has_response": has_response, + "response_en": response_en, + "response_ar": response_ar, }) else: return JsonResponse({"success": False, "error": "Inquiry not found"}) @@ -3899,9 +4463,11 @@ def public_inquiry_track(request): return render(request, "complaints/public_inquiry_track.html", { "inquiry": inquiry, "public_status": public_status, - "public_updates": public_updates, "error_message": error_message, "reference_number": reference, + "has_response": has_response, + "response_en": response_en, + "response_ar": response_ar, }) @@ -4709,7 +5275,6 @@ def confirm_ai_department_suggestion(request, complaint_pk): is_primary=True, added_by=user, assigned_at=timezone.now(), - forwarded_at=timezone.now(), ) ComplaintUpdate.objects.create( @@ -4772,10 +5337,6 @@ def involved_department_add(request, complaint_pk): if involved_dept.assigned_to and not involved_dept.assigned_at: involved_dept.assigned_at = timezone.now() - # Mark as forwarded to department (for tracking pending responses) - if not involved_dept.forwarded_at: - involved_dept.forwarded_at = timezone.now() - involved_dept.save() # Log the update @@ -4956,8 +5517,10 @@ def involved_department_response(request, pk): return redirect("complaints:complaint_detail", pk=complaint.pk) response_notes = request.POST.get("response_notes", "").strip() + response_notes_en = request.POST.get("response_notes_en", "").strip() + response_notes_ar = request.POST.get("response_notes_ar", "").strip() - if not response_notes: + if not response_notes and not response_notes_en and not response_notes_ar: if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return JsonResponse({ "success": False, @@ -4966,12 +5529,20 @@ def involved_department_response(request, pk): messages.error(request, _("Please provide a valid response.")) return redirect("complaints:complaint_detail", pk=complaint.pk) - involved_dept.response_notes = response_notes + involved_dept.response_notes = response_notes or response_notes_en or response_notes_ar + involved_dept.response_notes_en = response_notes_en + involved_dept.response_notes_ar = response_notes_ar involved_dept.response_submitted = True involved_dept.response_submitted_at = timezone.now() + involved_dept.acceptance_status = "pending" + involved_dept.accepted_by = None + involved_dept.accepted_at = None + involved_dept.acceptance_notes = "" + involved_dept.manager_review_status = "pending" + involved_dept.manager_reviewed_by = None + involved_dept.manager_reviewed_at = None involved_dept.save() - # Log the update ComplaintUpdate.objects.create( complaint=complaint, update_type="note", @@ -4979,27 +5550,172 @@ def involved_department_response(request, pk): created_by=user, ) - # Send notification to complaint assigned_to - if complaint.assigned_to and complaint.assigned_to.email: + # Notify complainant that department has responded + try: from apps.notifications.services import NotificationService - NotificationService.send_email( - complaint.assigned_to.email, - subject=f"Department Response Received - {complaint.reference_number}", - message=f"A response has been submitted by {involved_dept.department.name} for complaint {complaint.reference_number}.", - html_message=f""" -

A response has been submitted by {involved_dept.department.name} - for complaint {complaint.reference_number}.

-

View Complaint

- """, - ) + from apps.core.utils import build_public_track_url + + track_url = build_public_track_url("complaint", complaint.reference_number) + if complaint.contact_phone: + NotificationService.send_sms( + phone=complaint.contact_phone, + message=f"PX360: Your complaint {complaint.reference_number} has been responded to. View: {track_url}", + related_object=complaint, + ) + if complaint.contact_email: + NotificationService.send_email( + email=complaint.contact_email, + subject=f"PX360: Response to Your Complaint {complaint.reference_number}", + message=f"Your complaint has been responded to.\n\nView: {track_url}", + related_object=complaint, + ) + except Exception as e: + logger.warning(f"Failed to send complainant notification: {e}") + + dept = involved_dept.department + if dept.manager and dept.manager.email: + try: + from apps.notifications.services import NotificationService, get_email_header_html + review_url = request.build_absolute_uri( + reverse("organizations:department_manager_review", kwargs={"pk": dept.pk, "idept_pk": involved_dept.pk}) + ) + NotificationService.send_email( + dept.manager.email, + subject=f"Champion Response Requires Your Review - {complaint.reference_number}", + message=( + f"A champion response for complaint {complaint.reference_number} " + f"from {involved_dept.department.name} requires your review and approval.\n\n" + f"Please review at: {review_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Champion Response Requires Your Review

+

A champion from {involved_dept.department.name} has submitted a response + for complaint {complaint.reference_number}.

+

Your review and approval is required before it is forwarded to the PX team.

+

+ Review Response +

+
+
+ """, + related_object=complaint, + ) + except Exception as e: + logger.error(f"Failed to send manager review notification: {e}") if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + redirect_url = reverse("organizations:department_detail", kwargs={"pk": user.department.pk}) if user.department else reverse("complaints:complaint_detail", kwargs={"pk": complaint.pk}) return JsonResponse({ "success": True, "message": str(_("Department response submitted successfully.")), + "redirect_url": redirect_url, }) messages.success(request, _("Department response submitted successfully.")) + if user.department: + return redirect("organizations:department_detail", pk=user.department.pk) + return redirect("complaints:complaint_detail", pk=complaint.pk) + + +@login_required +@require_http_methods(["POST"]) +def involved_department_review_response(request, pk): + """ + Review (accept/reject) a department's response to a complaint. + On rejection, notifies the champion and resets for resubmission. + """ + from .models import ComplaintInvolvedDepartment + + involved_dept = get_object_or_404(ComplaintInvolvedDepartment, pk=pk) + complaint = involved_dept.complaint + + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, _("You don't have permission to review department responses.")) + return redirect("complaints:complaint_detail", pk=complaint.pk) + + if not involved_dept.response_submitted: + messages.error(request, _("No department response to review.")) + return redirect("complaints:complaint_detail", pk=complaint.pk) + + if involved_dept.manager_review_status != "approved": + messages.error(request, _("This response has not been approved by the department manager yet.")) + return redirect("complaints:complaint_detail", pk=complaint.pk) + + acceptance = request.POST.get("acceptance_status") + if acceptance not in ("acceptable", "not_acceptable"): + messages.error(request, _("Invalid acceptance status.")) + return redirect("complaints:complaint_detail", pk=complaint.pk) + + notes = request.POST.get("acceptance_notes", "").strip() + + involved_dept.acceptance_status = acceptance + involved_dept.accepted_by = user + involved_dept.accepted_at = timezone.now() + involved_dept.acceptance_notes = notes + + if acceptance == "not_acceptable": + involved_dept.response_submitted = False + involved_dept.response_submitted_at = None + involved_dept.response_notes = "" + involved_dept.response_notes_en = "" + involved_dept.response_notes_ar = "" + involved_dept.acceptance_status = "not_acceptable" + + involved_dept.save() + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Department response from {involved_dept.department.name} marked as {acceptance} by {user.get_full_name()}. {notes}", + created_by=user, + ) + + AuditService.log_event( + event_type="complaint_dept_response_review", + description=f"Department response for complaint {complaint.reference_number} marked as {acceptance}", + user=user, + content_object=complaint, + ) + + if acceptance == "not_acceptable": + dept = involved_dept.department + recipients = set() + if dept.champion and dept.champion.user and dept.champion.user.email: + recipients.add(dept.champion.user.email) + if dept.manager and dept.manager.email: + recipients.add(dept.manager.email) + for email in recipients: + try: + from apps.notifications.services import NotificationService, get_email_header_html + NotificationService.send_email( + email, + subject=f"Response Rejected - {complaint.reference_number}", + message=( + f"Your department's response for complaint {complaint.reference_number} was not accepted.\n\n" + f"Reason: {notes or 'No reason provided.'}\n\n" + f"Please submit a new response." + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Response Not Accepted

+

Your department's response for complaint {complaint.reference_number} was not accepted.

+

Reason: {notes or 'No reason provided.'}

+

Please review and submit a new response.

+
+
+ """, + related_object=complaint, + ) + except Exception as e: + logger.error(f"Failed to send rejection notification: {e}") + + messages.success(request, _(f"Department response marked as {acceptance}.")) return redirect("complaints:complaint_detail", pk=complaint.pk) @@ -5692,7 +6408,7 @@ def patient_complaint_visit_form(request, token, visit_id): title=title, description=description, encounter_id=visit.admission_id, - complaint_source_type="external", + complaint_source_type="internal", priority="medium", severity="medium", status="open", @@ -5736,7 +6452,7 @@ def government_ticket_list(request): return redirect("dashboard:index") # Base queryset - queryset = GovernmentTicket.objects.select_related("source", "location", "main_section", "assigned_to").all() + queryset = GovernmentTicket.objects.select_related("source", "department", "assigned_to").all() # Filters source_filter = request.GET.get("source") @@ -5750,7 +6466,7 @@ def government_ticket_list(request): if status_filter: queryset = queryset.filter(status=status_filter) if department_filter: - queryset = queryset.filter(main_section_id=department_filter) + queryset = queryset.filter(department_id=department_filter) if converted_filter: is_converted = converted_filter == "yes" queryset = queryset.filter(converted_to_complaint=is_converted) @@ -5788,7 +6504,7 @@ def government_ticket_list(request): def government_ticket_detail(request, pk): """Detail view for a government ticket""" ticket = get_object_or_404( - GovernmentTicket.objects.select_related("source", "location", "main_section", "assigned_to", "complaint"), + GovernmentTicket.objects.select_related("source", "department", "assigned_to", "complaint"), pk=pk, ) @@ -5869,12 +6585,11 @@ def convert_to_complaint(request, pk): return redirect("complaints:government_ticket_detail", pk=ticket.pk) # Build redirect URL to complaint create form with pre-filled data - from django.urls import reverse import urllib.parse params = { "source": ticket.source_id, - "complaint_source_type": "external", + "complaint_source_type": "external" if ticket.source.code.upper() in ("MOH", "CCHI") else "internal", "title": f"[{ticket.ticket_number}] {ticket.classification or 'Government Ticket'}", "description": ticket.content, "main_section": ticket.main_section_id or "", @@ -5963,7 +6678,7 @@ def government_ticket_import(request): try: import pandas as pd from apps.px_sources.models import PXSource - from apps.organizations.models import Location, MainSection, SubSection + from apps.organizations.models import LegacyLocation, LegacyMainSection, LegacySubSection from apps.accounts.models import User df = pd.read_json(json_data) @@ -6018,7 +6733,7 @@ def government_ticket_import(request): ticket_location = None if pd.notna(row.get("location")): loc_name = str(row["location"]).strip() - ticket_location = Location.objects.filter( + ticket_location = LegacyLocation.objects.filter( models.Q(name_en__icontains=loc_name) | models.Q(name_ar__icontains=loc_name) ).first() @@ -6026,7 +6741,7 @@ def government_ticket_import(request): ticket_main_section = None if pd.notna(row.get("main_section")): sec_name = str(row["main_section"]).strip() - ticket_main_section = MainSection.objects.filter( + ticket_main_section = LegacyMainSection.objects.filter( models.Q(name_en__icontains=sec_name) | models.Q(name_ar__icontains=sec_name) ).first() @@ -6034,7 +6749,7 @@ def government_ticket_import(request): ticket_subsection = None if pd.notna(row.get("subsection")): sub_name = str(row["subsection"]).strip() - qs = SubSection.objects.filter( + qs = LegacySubSection.objects.filter( models.Q(name_en__icontains=sub_name) | models.Q(name_ar__icontains=sub_name) ) if ticket_location: @@ -6064,9 +6779,9 @@ def government_ticket_import(request): complainant_name=str(row.get("complainant_name", "")).strip() or "Unknown", national_id=str(row.get("national_id", "")).strip() if pd.notna(row.get("national_id")) else "", contact_number=str(row.get("contact_number", "")).strip() if pd.notna(row.get("contact_number")) else "", - location=ticket_location, - main_section=ticket_main_section, - subsection=ticket_subsection, + legacy_location=ticket_location, + legacy_main_section=ticket_main_section, + legacy_subsection=ticket_subsection, received_date=received_date, classification=str(row.get("classification", "")).strip() if pd.notna(row.get("classification")) else "", content=str(row.get("content", "")).strip() if pd.notna(row.get("content")) else "", @@ -6114,7 +6829,7 @@ def government_ticket_export(request): from django.http import HttpResponse # Get filtered queryset (same filters as list view) - queryset = GovernmentTicket.objects.select_related("source", "location", "main_section", "assigned_to").all() + queryset = GovernmentTicket.objects.select_related("source", "department", "assigned_to").all() source_filter = request.GET.get("source") status_filter = request.GET.get("status") @@ -6126,7 +6841,7 @@ def government_ticket_export(request): if status_filter: queryset = queryset.filter(status=status_filter) if department_filter: - queryset = queryset.filter(main_section_id=department_filter) + queryset = queryset.filter(department_id=department_filter) if search_query: queryset = queryset.filter( models.Q(ticket_number__icontains=search_query) @@ -6143,9 +6858,8 @@ def government_ticket_export(request): "اسم المشتكي": ticket.complainant_name, "رقم الهوية": ticket.national_id or "", "رقم التواصل": ticket.contact_number or "", - "الموقع": ticket.location.name_en if ticket.location else "", - "القسم الرئيسي": ticket.main_section.name_en if ticket.main_section else "", - "القسم الفرعي": ticket.subsection.name_en if ticket.subsection else "", + "القسم": ticket.department.name_en if ticket.department else "", + "نوع الموقع": ticket.department.location_type if ticket.department and ticket.department.location_type else "", "تاريخ إنشاء التذكرة": ticket.received_date.strftime("%Y-%m-%d") if ticket.received_date else "", "وقت إنشاء التذكرة": ticket.received_date.strftime("%H:%M:%S") if ticket.received_date else "", "تصنيف الشكوى": ticket.classification or "", diff --git a/apps/complaints/ui_views_explanation.py b/apps/complaints/ui_views_explanation.py index b40acb5..0ef50a9 100644 --- a/apps/complaints/ui_views_explanation.py +++ b/apps/complaints/ui_views_explanation.py @@ -20,7 +20,7 @@ def send_to_department_form(request, pk): Complaint.objects.prefetch_related( "involved_staff__staff__department", "involved_staff__staff__report_to", - "involved_departments__department__respondent", + "involved_departments__department__champion", "involved_departments__department__manager", ), pk=pk, @@ -55,25 +55,25 @@ def send_to_department_form(request, pk): # Build department groups from involved_departments first involved_departments = complaint.involved_departments.select_related( - "department__respondent", "department__manager" + "department__champion", "department__manager" ).all() for dept_inv in involved_departments: dept = dept_inv.department - if not dept or not dept.respondent: + if not dept: continue dept_key = str(dept.id) if dept_key not in department_groups: - champion = dept.respondent - champion_email = champion.email or (champion.user.email if champion.user else None) + champion = dept.champion + champion_email = champion.email or (champion.user.email if champion and champion.user else None) if champion else None dept_manager = dept.manager department_groups[dept_key] = { "department_id": dept_key, "department_name": dept.get_localized_name(), "champion": champion, - "champion_id": str(champion.id), - "champion_name": champion.get_full_name(), + "champion_id": str(champion.id) if champion else None, + "champion_name": champion.get_full_name() if champion else None, "champion_email": champion_email, "dept_manager": dept_manager, "dept_manager_id": str(dept_manager.id) if dept_manager else None, @@ -95,22 +95,20 @@ def send_to_department_form(request, pk): "role": staff_inv.get_role_display(), } - if dept and dept.respondent: + if dept: dept_key = str(dept.id) if dept_key in department_groups: department_groups[dept_key]["staff_list"].append(entry) else: - # This shouldn't happen if we built department_groups from involved_departments, - # but handle as fallback - champion = dept.respondent - champion_email = champion.email or (champion.user.email if champion.user else None) + champion = dept.champion + champion_email = champion.email or (champion.user.email if champion and champion.user else None) if champion else None dept_manager = dept.manager department_groups[dept_key] = { "department_id": dept_key, "department_name": dept.get_localized_name(), "champion": champion, - "champion_id": str(champion.id), - "champion_name": champion.get_full_name(), + "champion_id": str(champion.id) if champion else None, + "champion_name": champion.get_full_name() if champion else None, "champion_email": champion_email, "dept_manager": dept_manager, "dept_manager_id": str(dept_manager.id) if dept_manager else None, @@ -121,10 +119,10 @@ def send_to_department_form(request, pk): else: ungrouped_staff.append(entry) - # Only show error if there are NO departments with champions AND no staff if not department_groups and not involved_staff.exists(): - messages.error(request, _("No staff members or departments are involved in this complaint.")) - return redirect("complaints:complaint_detail", pk=complaint.pk) + if not involved_departments.exists(): + messages.error(request, _("No departments are involved in this complaint. Please add a department first.")) + return redirect("complaints:complaint_detail", pk=complaint.pk) if request.method == "POST": action = request.POST.get("action", "send") @@ -148,8 +146,26 @@ def send_to_department_form(request, pk): for dept_id in selected_dept_ids: dept_info = department_groups.get(dept_id) if dept_info: + contact_person_id = request.POST.get(f"contact_person_{dept_id}", "") + if contact_person_id: + from apps.organizations.models import Department as DeptModel + dept_obj = DeptModel.objects.filter(pk=dept_id).first() + if dept_obj: + cinfo = dept_obj.is_valid_contact_person(contact_person_id) + if cinfo: + dept_info["contact_person_id"] = contact_person_id + dept_info["contact_person_name"] = cinfo["name"] + dept_info["contact_person_role"] = cinfo["role_label"] + dept_info["contact_person_email"] = cinfo["email"] + dept_info["contact_person_staff"] = cinfo["staff"] preview_depts.append(dept_info) + contact_person_map = { + d["department_id"]: d.get("contact_person_id", "") + for d in preview_depts + if d.get("contact_person_id") + } + return render( request, "complaints/send_to_department_preview.html", @@ -158,11 +174,18 @@ def send_to_department_form(request, pk): "preview_depts": preview_depts, "selected_dept_ids": selected_dept_ids, "request_message": request_message, + "contact_person_map": contact_person_map, }, ) from django.contrib.sites.shortcuts import get_current_site + contact_person_map = {} + for dept_id in selected_dept_ids: + cp_id = request.POST.get(f"contact_person_{dept_id}", "") + if cp_id: + contact_person_map[dept_id] = cp_id + site = get_current_site(request) results = ComplaintService.send_to_department( complaint, @@ -172,8 +195,13 @@ def send_to_department_form(request, pk): request.user, site.domain, request=request, + contact_person_map=contact_person_map, ) + if not complaint.forwarded_to_dept_at: + complaint.forwarded_to_dept_at = timezone.now() + complaint.save(update_fields=["forwarded_to_dept_at"]) + if results["champion_count"] == 0 and results["manager_count"] == 0: if results["skipped_no_email"] > 0: messages.warning( diff --git a/apps/complaints/urls.py b/apps/complaints/urls.py index c67ec4a..53118d0 100644 --- a/apps/complaints/urls.py +++ b/apps/complaints/urls.py @@ -8,6 +8,9 @@ from .views import ( ComplaintViewSet, InquiryViewSet, complaint_explanation_form, + champion_start_investigation, + staff_investigation_form, + champion_review_answers, generate_complaint_pdf, api_locations, api_sections, @@ -33,6 +36,7 @@ urlpatterns = [ path("/assign/", ui_views.complaint_assign, name="complaint_assign"), path("/change-status/", ui_views.complaint_change_status, name="complaint_change_status"), path("/update-satisfaction/", ui_views.update_satisfaction, name="update_satisfaction"), + path("/update-patient-contact-status/", ui_views.update_patient_contact_status, name="update_patient_contact_status"), path("/toggle-escalated-ovr/", ui_views.toggle_escalated_ovr, name="toggle_escalated_ovr"), path("/approve-ovr/", ui_views.approve_ovr_escalation, name="approve_ovr_escalation"), path("/reject-ovr/", ui_views.reject_ovr_escalation, name="reject_ovr_escalation"), @@ -131,6 +135,9 @@ urlpatterns = [ path("public/api/hospitals//departments/", api_departments, name="api_departments"), # Public Explanation Form (No Authentication Required) path("/explain//", complaint_explanation_form, name="complaint_explanation_form"), + path("/investigate//", champion_start_investigation, name="champion_start_investigation"), + path("/investigate/respond//", staff_investigation_form, name="staff_investigation_form"), + path("/investigate/review//", champion_review_answers, name="champion_review_answers"), # Patient Complaint Portal (No Authentication Required) path("patient//", ui_views.patient_complaint_portal, name="patient_complaint_portal"), path( @@ -161,6 +168,7 @@ urlpatterns = [ path("departments//edit/", ui_views.involved_department_edit, name="involved_department_edit"), path("departments//remove/", ui_views.involved_department_remove, name="involved_department_remove"), path("departments//response/", ui_views.involved_department_response, name="involved_department_response"), + path("departments//review-response/", ui_views.involved_department_review_response, name="involved_department_review_response"), # Send to Department Form path( "/send-to-department/", ui_views_explanation.send_to_department_form, name="send_to_department_form" diff --git a/apps/complaints/urls_inquiries.py b/apps/complaints/urls_inquiries.py index a322db4..efc9da4 100644 --- a/apps/complaints/urls_inquiries.py +++ b/apps/complaints/urls_inquiries.py @@ -30,6 +30,9 @@ urlpatterns = [ path("public/track/", ui_views.public_inquiry_track, name="public_inquiry_track"), path("/send-to-staff/", ui_views.inquiry_send_to_staff, name="inquiry_send_to_staff"), path("/send-to/", ui_views.inquiry_send_to, name="inquiry_send_to"), + path("/escalate/", ui_views.inquiry_escalate, name="inquiry_escalate"), # Token-based explanation form (No Authentication Required) path("/explain//", views.inquiry_explanation_form, name="inquiry_explanation_form"), + # Token-based department response (No Authentication Required) + path("/respond//", views.inquiry_respond_with_token, name="inquiry_respond_with_token"), ] diff --git a/apps/complaints/utils.py b/apps/complaints/utils.py index 9951eec..a2ba2f2 100644 --- a/apps/complaints/utils.py +++ b/apps/complaints/utils.py @@ -651,8 +651,8 @@ def export_monthly_calculations(queryset, year, month): qs = queryset.select_related( "hospital", "department", - "main_section", - "subsection", + "legacy_main_section", + "legacy_subsection", "assigned_to", "resolved_by", "closed_by", @@ -846,10 +846,10 @@ def export_monthly_calculations(queryset, year, month): source_display = "Patient" location_display = "" - if c.main_section: - location_display = c.main_section.name if hasattr(c.main_section, "name") else str(c.main_section) - if c.location: - loc_name = c.location.name if hasattr(c.location, "name") else str(c.location) + if c.legacy_main_section: + location_display = c.legacy_main_section.name if hasattr(c.legacy_main_section, "name") else str(c.legacy_main_section) + if c.legacy_location: + loc_name = c.legacy_location.name if hasattr(c.legacy_location, "name") else str(c.legacy_location) if loc_name: location_display = f"{loc_name} - {location_display}" if location_display else loc_name @@ -1336,8 +1336,8 @@ def _build_quarterly_yearly_report(queryset, title, months_list, year=None): internal += 1 loc = "Other" - if c.main_section: - loc_name = c.main_section.name.lower() if hasattr(c.main_section, "name") else str(c.main_section).lower() + if c.legacy_main_section: + loc_name = c.legacy_main_section.name.lower() if hasattr(c.legacy_main_section, "name") else str(c.legacy_main_section).lower() if "inpatient" in loc_name or "ip" in loc_name: loc = "Inpatient" elif "outpatient" in loc_name or "op" in loc_name or "clinic" in loc_name: @@ -2483,8 +2483,8 @@ def export_historical_excel(queryset, date_start=None, date_end=None): "patient", "hospital", "department", - "main_section", - "subsection", + "legacy_main_section", + "legacy_subsection", "assigned_to", "resolved_by", "closed_by", @@ -2537,21 +2537,21 @@ def export_historical_excel(queryset, date_start=None, date_end=None): # Location (main_section or location) location_name = "" - if c.main_section: - location_name = get_name_ar(c.main_section) - elif c.location: - location_name = get_name_ar(c.location) + if c.legacy_main_section: + location_name = get_name_ar(c.legacy_main_section) + elif c.legacy_location: + location_name = get_name_ar(c.legacy_location) # Departments main_dept = "" - if c.main_section: - main_dept = get_name_ar(c.main_section) + if c.legacy_main_section: + main_dept = get_name_ar(c.legacy_main_section) elif c.department: main_dept = get_name_ar(c.department) sub_dept = "" - if c.subsection: - sub_dept = get_name_ar(c.subsection) + if c.legacy_subsection: + sub_dept = get_name_ar(c.legacy_subsection) # Entered by (assigned_to or created_by) entered_by = "" diff --git a/apps/complaints/views.py b/apps/complaints/views.py index a07a08f..853d80e 100644 --- a/apps/complaints/views.py +++ b/apps/complaints/views.py @@ -5,7 +5,8 @@ Complaints views and viewsets import logging from django.db.models import Q -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render, redirect +from django.urls import reverse from django.utils import timezone from rest_framework import status, viewsets from rest_framework.decorators import action @@ -22,6 +23,7 @@ from .models import ( ComplaintPRInteraction, ComplaintStatus, ComplaintUpdate, + InvestigationStatus, Inquiry, ) from .serializers import ( @@ -819,13 +821,32 @@ This is an automated message from PX360 Complaint Management System. """ # Send email using NotificationService - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html try: notification_log = NotificationService.send_email( email=recipient_email, subject=subject, message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

{subject}

+

Dear {recipient_display},

+

You have been assigned to review the following complaint:

+ + + + + +
Reference:#{complaint.id}
Title:{complaint.title}
Severity:{complaint.get_severity_display()}
Priority:{complaint.get_priority_display()}
+ +
+
+""", related_object=complaint, metadata={ "notification_type": "complaint_notification", @@ -890,7 +911,7 @@ This is an automated message from PX360 Complaint Management System. ) involved_staff = complaint.involved_staff.select_related( - "staff", "staff__department", "staff__department__respondent" + "staff", "staff__department", "staff__department__champion", "staff__department__manager" ).all() if not involved_staff.exists(): @@ -903,11 +924,11 @@ This is an automated message from PX360 Complaint Management System. for staff_inv in involved_staff: staff = staff_inv.staff dept = staff.department - if dept and dept.respondent: + if dept and dept.champion: dept_key = str(dept.id) if dept_key not in department_groups: - champion = dept.respondent - champion_email = champion.email or (champion.user.email if champion.user else None) + champion = dept.champion + champion_email = champion.email or (champion.user.email if hasattr(champion, 'user') and champion.user else None) dept_manager = dept.manager department_groups[dept_key] = { "department_id": dept_key, @@ -1028,7 +1049,7 @@ This is an automated message from PX360 Complaint Management System. # Send email with new link from django.contrib.sites.shortcuts import get_current_site - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html site = get_current_site(request) explanation_link = f"https://{site.domain}/complaints/{complaint.id}/explain/{new_token}/" @@ -1083,6 +1104,25 @@ This is an automated message from PX360 Complaint Management System. email=recipient_email, subject=subject, message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Explanation Request (Resent)

+

Dear {recipient_display},

+

We have resent the explanation request for complaint #{complaint.id}:

+ + + + +
Title:{complaint.title}
Severity:{complaint.get_severity_display()}
Priority:{complaint.get_priority_display()}
+

This link can only be used once. After submission, it will expire.

+ +
+
+""", related_object=complaint, metadata={ "notification_type": "explanation_request_resent", @@ -1485,7 +1525,7 @@ This is an automated message from PX360 Complaint Management System. # Send email to manager from django.contrib.sites.shortcuts import get_current_site - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html site = get_current_site(request) explanation_link = f"https://{site.domain}/complaints/{complaint.id}/explain/{manager_token}/" @@ -1536,6 +1576,25 @@ This is an automated message from PX360 Complaint Management System. email=manager_email, subject=subject, message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Escalated Explanation Request

+

The original explanation was not acceptable and this request has been escalated to you for review.

+ + + + + +
Reference:{complaint.reference_number}
Title:{complaint.title}
Severity:{complaint.get_severity_display()}
Priority:{complaint.get_priority_display()}
+

This link can only be used once. After submission, it will expire.

+ +
+
+""", related_object=complaint, metadata={ "notification_type": "escalated_explanation_request", @@ -1904,6 +1963,348 @@ Generate a JSON response with: logger.error(f"AI helper suggestion failed: {e}") return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def _get_pdf_context_data(self, complaint): + """Compute all PDF template context fields from DB data.""" + from apps.complaints.models import ComplaintInvolvedDepartment, ComplaintInvolvedStaff + + def fmt_date_ar(dt): + if not dt: + return "—" + return dt.strftime("%Y/%m/%d %p %I:%M").replace("AM", "ص").replace("PM", "م") + + hospital_name = ( + complaint.hospital.get_display_name_ar() + if complaint.hospital and complaint.hospital.get_display_name_ar() + else complaint.hospital.get_display_name() if complaint.hospital else "" + ) + dept_name = ( + complaint.department.name_ar + if complaint.department and complaint.department.name_ar + else complaint.department.name_en or complaint.department.name if complaint.department else "" + ) + complainant_name = ( + complaint.contact_name + or complaint.patient_name + or (complaint.patient.get_full_name() if complaint.patient else "") + ) + source_name = complaint.source.name_ar if complaint.source else "" + + accused = ComplaintInvolvedStaff.objects.filter( + complaint=complaint, role="accused" + ).select_related("staff", "staff__department").first() + + accused_staff_name = "" + accused_staff_title = "" + if accused and accused.staff: + s = accused.staff + accused_staff_name = s.name_ar or s.name or f"{s.first_name} {s.last_name}" + accused_staff_title = s.job_title or "" + + sent_to_dept_at = complaint.sent_to_department_at or complaint.forwarded_to_dept_at + + first_response_dept = ComplaintInvolvedDepartment.objects.filter( + complaint=complaint, response_submitted=True + ).order_by("response_submitted_at").first() + response_submitted_at = first_response_dept.response_submitted_at if first_response_dept else None + + return { + "hospital_name": hospital_name, + "department_name": dept_name, + "complainant_name": complainant_name, + "source_name": source_name, + "accused_staff_name": accused_staff_name, + "accused_staff_title": accused_staff_title, + "submission_date": fmt_date_ar(complaint.created_at), + "incident_date": complaint.incident_date.strftime("%Y/%m/%d") if complaint.incident_date else "—", + "sent_to_dept_date": fmt_date_ar(sent_to_dept_at), + "response_date": fmt_date_ar(response_submitted_at), + } + + @action(detail=True, methods=["get", "post"]) + def summary_preview(self, request, pk=None): + """Get existing summary (GET) or generate AI preview (POST).""" + complaint = self.get_object() + + user = request.user + can_generate = ( + user.is_px_admin() + or (user.is_hospital_admin() and user.hospital == complaint.hospital) + or (user.is_department_manager() and user.department == complaint.department) + or (user.is_px_employee and complaint.hospital == user.hospital) + ) + if not can_generate: + return Response( + {"error": "You do not have permission to generate a PDF summary"}, + status=status.HTTP_403_FORBIDDEN, + ) + + from .models import ComplaintPdfSummary + + if request.method == "GET": + ctx = self._get_pdf_context_data(complaint) + summary = ComplaintPdfSummary.objects.filter(complaint=complaint).first() + if summary: + data = { + "exists": True, + "content_summary": summary.content_summary, + "dept_response_summary": summary.dept_response_summary, + **ctx, + } + if summary.file: + data["has_file"] = True + data["file_url"] = summary.file.url + else: + data["has_file"] = False + return Response(data) + return Response({"exists": False, **ctx}) + + try: + import json + + from apps.complaints.models import ComplaintInvolvedDepartment + + dept_response_parts = [] + involved_depts = ComplaintInvolvedDepartment.objects.filter( + complaint=complaint + ).select_related("department") + for dept in involved_depts: + resp = dept.response_notes_ar or dept.response_notes_en or dept.response_notes or "" + resp = resp[:600] + d_name = dept.department.name_ar or dept.department.name_en or dept.department.name + if resp: + dept_response_parts.append(f"- {d_name}: {resp}") + + if not dept_response_parts and complaint.action_taken_by_dept: + dept_response_parts.append(complaint.action_taken_by_dept[:600]) + + dept_response_text = "\n".join(dept_response_parts) or "No department response recorded." + resolution_text = complaint.resolution or complaint.recommendation_action_plan or "" + + prompt = f"""You are a healthcare complaint report writer. Generate your response entirely in Modern Standard Arabic (Fusha). + +COMPLAINT: +- Title: {complaint.title} +- Description: {complaint.description[:2000]} + +DEPARTMENT RESPONSE / ACTIONS TAKEN: +{dept_response_text} + +RESOLUTION: +{resolution_text[:600] or 'No resolution recorded.'} + +Generate JSON with exactly these fields: +- "content_summary": 2-3 paragraph professional summary of the complaint content. +- "department_response_summary": 1-2 paragraph summary of the department response and actions taken.""" + + from apps.core.ai_service import AIService + + result = AIService.chat_completion( + prompt=prompt, + response_format="json_object", + temperature=0.3, + max_tokens=1500, + ) + parsed = json.loads(result) + + content_summary = parsed.get( + "content_summary", complaint.description[:500] if complaint.description else "" + ) + dept_response_summary = parsed.get( + "department_response_summary", + complaint.action_taken_by_dept[:500] if complaint.action_taken_by_dept else "", + ) + + ComplaintPdfSummary.objects.update_or_create( + complaint=complaint, + defaults={ + "lang": "ar", + "content_summary": content_summary, + "dept_response_summary": dept_response_summary, + }, + ) + + ctx = self._get_pdf_context_data(complaint) + + return Response({ + "content_summary": content_summary, + "dept_response_summary": dept_response_summary, + **ctx, + }) + + except Exception as e: + logger.error(f"Error generating summary preview for complaint {pk}: {e}") + return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=True, methods=["post"]) + def generate_summary_pdf(self, request, pk=None): + """Generate a PDF report for the complaint in the requested language (en/ar).""" + complaint = self.get_object() + + user = request.user + can_generate = ( + user.is_px_admin() + or (user.is_hospital_admin() and user.hospital == complaint.hospital) + or (user.is_department_manager() and user.department == complaint.department) + or (user.is_px_employee and complaint.hospital == user.hospital) + ) + if not can_generate: + return Response( + {"error": "You do not have permission to generate a PDF summary"}, + status=status.HTTP_403_FORBIDDEN, + ) + + try: + import base64 + import json + + from django.conf import settings + from django.template.loader import render_to_string + + lang = request.data.get("lang", "ar") + + from apps.complaints.models import ComplaintInvolvedDepartment + + dept_response_parts = [] + involved_depts = ComplaintInvolvedDepartment.objects.filter( + complaint=complaint + ).select_related("department") + for dept in involved_depts: + resp = dept.response_notes_ar or dept.response_notes_en or dept.response_notes or "" + resp = resp[:600] + d_name = dept.department.name_ar or dept.department.name_en or dept.department.name + if resp: + dept_response_parts.append(f"- {d_name}: {resp}") + + if not dept_response_parts and complaint.action_taken_by_dept: + dept_response_parts.append(complaint.action_taken_by_dept[:600]) + + dept_response_text = "\n".join(dept_response_parts) or "No department response recorded." + + resolution_text = complaint.resolution or complaint.recommendation_action_plan or "" + + # Use pre-written text from request, then saved summary, then AI fallback + content_summary = request.data.get("content_summary") + dept_response_summary = request.data.get("dept_response_summary") + + if not content_summary or not dept_response_summary: + from .models import ComplaintPdfSummary + saved = ComplaintPdfSummary.objects.filter(complaint=complaint).first() + if saved and saved.content_summary and saved.dept_response_summary: + content_summary = saved.content_summary + dept_response_summary = saved.dept_response_summary + else: + import json + + prompt = f"""You are a healthcare complaint report writer. Generate your response entirely in Modern Standard Arabic (Fusha). + +COMPLAINT: +- Title: {complaint.title} +- Description: {complaint.description[:2000]} + +DEPARTMENT RESPONSE / ACTIONS TAKEN: +{dept_response_text} + +RESOLUTION: +{resolution_text[:600] or 'No resolution recorded.'} + +Generate JSON with exactly these fields: +- "content_summary": 2-3 paragraph professional summary of the complaint content. +- "department_response_summary": 1-2 paragraph summary of the department response and actions taken.""" + + from apps.core.ai_service import AIService + + result = AIService.chat_completion( + prompt=prompt, + response_format="json_object", + temperature=0.3, + max_tokens=1500, + ) + parsed = json.loads(result) + + content_summary = parsed.get("content_summary", complaint.description[:500] if complaint.description else "") + dept_response_summary = parsed.get( + "department_response_summary", + complaint.action_taken_by_dept[:500] if complaint.action_taken_by_dept else "", + ) + + ctx = self._get_pdf_context_data(complaint) + + for field in ["complainant_name", "source_name", "department_name", + "accused_staff_name", "accused_staff_title", + "submission_date", "incident_date", + "sent_to_dept_date", "response_date"]: + val = request.data.get(field) + if val: + ctx[field] = val + + import io + from PIL import Image as PILImage + _logo_img = PILImage.open(settings.BASE_DIR / "static" / "img" / "HH_P_V_Logo(hospital)_.png") + _logo_img.thumbnail((600, 600), PILImage.LANCZOS) + _logo_buf = io.BytesIO() + _logo_img.save(_logo_buf, format="PNG", optimize=True) + logo_path = "data:image/png;base64," + base64.b64encode(_logo_buf.getvalue()).decode() + + html_string = render_to_string( + "complaints/complaint_summary_pdf.html", + { + "complaint": complaint, + "lang": "ar", + "content_summary": content_summary, + "dept_response_summary": dept_response_summary, + "logo_path": logo_path, + "hospital_name_en": complaint.hospital.get_display_name() if complaint.hospital else "", + "hospital_address": complaint.hospital.address if complaint.hospital else "", + "hospital_phone": complaint.hospital.phone if complaint.hospital else "", + "hospital_email": complaint.hospital.email if complaint.hospital else "", + **ctx, + }, + ) + + from weasyprint import HTML + + pdf_file = HTML(string=html_string, base_url=str(settings.BASE_DIR / "static")).write_pdf() + + # Save PDF to persistent storage + from django.core.files.base import ContentFile + from .models import ComplaintPdfSummary + summary, _ = ComplaintPdfSummary.objects.update_or_create( + complaint=complaint, + defaults={ + "lang": "ar", + "content_summary": content_summary, + "dept_response_summary": dept_response_summary, + }, + ) + summary.file.save(f"{complaint.pk}_ar.pdf", ContentFile(pdf_file), save=False) + summary.file_size = len(pdf_file) + summary.save() + + from django.http import HttpResponse + from datetime import datetime + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"complaint_summary_{complaint.reference_number}_ar_{timestamp}.pdf" + + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + AuditService.log_from_request( + event_type="pdf_summary_generated", + description=f"PDF report ({lang}) generated for complaint: {complaint.reference_number}", + request=request, + content_object=complaint, + metadata={"complaint_id": str(pk), "lang": lang}, + ) + + return response + + except ImportError: + return Response({"error": "WeasyPrint is not installed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + except Exception as e: + logger.error(f"Error generating PDF summary for complaint {pk}: {e}") + return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @action(detail=True, methods=["post"]) def save_resolution(self, request, pk=None): """ @@ -2242,13 +2643,33 @@ This is an automated message from PX360 Complaint Management System. """ # Send email using NotificationService - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html try: notification_log = NotificationService.send_email( email=recipient_email, subject=subject, message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Complaint Resolution

+

Dear {recipient_name},

+

We are pleased to inform you that your complaint has been resolved.

+ + + + +
Reference:#{complaint.id}
Title:{complaint.title}
Category:{complaint.get_resolution_category_display()}
+
+

{complaint.resolution}

+
+

If you have any further questions or concerns, please don't hesitate to contact us.

+

Thank you for your patience and for giving us the opportunity to address your concerns.

+
+
+""", related_object=complaint, metadata={ "notification_type": "resolution_notification", @@ -3189,9 +3610,9 @@ def api_locations(request): Returns JSON list of all locations ordered by English name. Public endpoint (no authentication required). """ - from apps.organizations.models import Location + from apps.organizations.models import LegacyLocation - locations = Location.objects.all().order_by("name_en") + locations = LegacyLocation.objects.all().order_by("name_en") locations_list = [ { @@ -3213,14 +3634,14 @@ def api_sections(request, location_id): for given location. Public endpoint (no authentication required). """ - from apps.organizations.models import MainSection, SubSection + from apps.organizations.models import LegacyMainSection, LegacySubSection # Get available sections that have subsections for this location available_section_ids = ( - SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() + LegacySubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() ) - sections = MainSection.objects.filter(id__in=available_section_ids).order_by("name_en") + sections = LegacyMainSection.objects.filter(id__in=available_section_ids).order_by("name_en") sections_list = [ { @@ -3243,9 +3664,9 @@ def api_subsections(request, location_id, section_id): Returns JSON list of subsections for given location and section. Public endpoint (no authentication required). """ - from apps.organizations.models import SubSection + from apps.organizations.models import LegacySubSection - subsections = SubSection.objects.filter(location_id=location_id, main_section_id=section_id).order_by("name_en") + subsections = LegacySubSection.objects.filter(location_id=location_id, main_section_id=section_id).order_by("name_en") subsections_list = [ { @@ -3299,7 +3720,7 @@ def complaint_explanation_form(request, complaint_id, token): Validates token and checks if it's still valid (not used). """ from .models import ComplaintExplanation, ExplanationAttachment - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html from django.contrib.sites.shortcuts import get_current_site # Get complaint @@ -3365,78 +3786,102 @@ def complaint_explanation_form(request, complaint_id, token): file_size=uploaded_file.size, ) - # Notify complaint assignee - if complaint.assigned_to and complaint.assigned_to.email: - site = get_current_site(request) - complaint_url = f"https://{site.domain}/complaints/{complaint.id}/" - - subject = f"New Explanation Received - Complaint #{complaint.id}" - - email_body = f""" -Dear {complaint.assigned_to.get_full_name()}, - -A new explanation has been submitted for the following complaint: - -COMPLAINT DETAILS: ----------------- -Reference: #{complaint.id} -Title: {complaint.title} -Severity: {complaint.get_severity_display()} - -EXPLANATION SUBMITTED BY: ------------------------- -{explanation.staff} - -EXPLANATION: ------------ -{explanation.explanation} - -""" - if files: - email_body += f""" -ATTACHMENTS: ------------- -{len(files)} file(s) attached -""" - - email_body += f""" - -To view the complaint and explanation, please visit: -{complaint_url} - ---- -This is an automated message from PX360 Complaint Management System. -""" - - try: - NotificationService.send_email( - email=complaint.assigned_to.email, - subject=subject, - message=email_body, - related_object=complaint, - metadata={ - "notification_type": "explanation_submitted", - "explanation_id": str(explanation.id), - "staff_id": str(explanation.staff.id) if explanation.staff else None, - }, - ) - except Exception as e: - # Log error but don't fail the submission - import logging - - logger = logging.getLogger(__name__) - logger.error(f"Failed to send notification email: {e}") - - # Create complaint update - ComplaintUpdate.objects.create( + # Populate the linked ComplaintInvolvedDepartment response for manager review + from apps.complaints.models import ComplaintInvolvedDepartment + involved_dept = ComplaintInvolvedDepartment.objects.filter( complaint=complaint, - update_type="communication", - message=f"Explanation submitted by {explanation.staff}", - metadata={ - "explanation_id": str(explanation.id), - "staff_id": str(explanation.staff.id) if explanation.staff else None, - }, - ) + department=explanation.staff.department, + sent=True, + ).first() + + if involved_dept: + involved_dept.response_notes = explanation_text + involved_dept.response_notes_en = explanation_text + involved_dept.response_submitted = True + involved_dept.response_submitted_at = timezone.now() + involved_dept.manager_review_status = "pending" + involved_dept.manager_reviewed_by = None + involved_dept.manager_reviewed_at = None + involved_dept.acceptance_status = "pending" + involved_dept.accepted_by = None + involved_dept.accepted_at = None + involved_dept.acceptance_notes = "" + involved_dept.save() + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Champion response submitted by {explanation.staff} from {involved_dept.department.name} — awaiting manager review", + metadata={ + "explanation_id": str(explanation.id), + "staff_id": str(explanation.staff.id) if explanation.staff else None, + }, + ) + else: + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="communication", + message=f"Explanation submitted by {explanation.staff}", + metadata={ + "explanation_id": str(explanation.id), + "staff_id": str(explanation.staff.id) if explanation.staff else None, + }, + ) + + # Fallback: notify PX Admin for explanations without linked InvolvedDepartment + if complaint.assigned_to and complaint.assigned_to.email: + site = get_current_site(request) + complaint_url = f"https://{site.domain}/complaints/{complaint.id}/" + try: + NotificationService.send_email( + email=complaint.assigned_to.email, + subject=f"New Explanation Received - Complaint #{complaint.reference_number}", + message=f"An explanation has been submitted for complaint {complaint.reference_number}.\n\nView: {complaint_url}", + related_object=complaint, + metadata={ + "notification_type": "explanation_submitted", + "explanation_id": str(explanation.id), + "staff_id": str(explanation.staff.id) if explanation.staff else None, + }, + ) + except Exception: + pass + + # Notify department manager to review (NOT the PX Admin) + if involved_dept: + dept = involved_dept.department + if dept.manager and dept.manager.email: + try: + review_url = request.build_absolute_uri( + reverse("organizations:department_manager_review", kwargs={"pk": dept.pk, "idept_pk": involved_dept.pk}) + ) + NotificationService.send_email( + dept.manager.email, + subject=f"Champion Response Requires Your Review - {complaint.reference_number}", + message=( + f"A champion response for complaint {complaint.reference_number} " + f"from {dept.name} requires your review and approval.\n\n" + f"Please review at: {review_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Champion Response Requires Your Review

+

A champion from {dept.name} has submitted a response + for complaint {complaint.reference_number}.

+

Your review and approval is required before it is forwarded to the PX team.

+

+ Review Response +

+
+
+ """, + related_object=complaint, + ) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to send manager review notification: {e}") # Redirect to success page return render( @@ -3446,13 +3891,485 @@ This is an automated message from PX360 Complaint Management System. ) # GET request - display form + from apps.complaints.models import ComplaintInvolvedStaff + accused_staff = list( + ComplaintInvolvedStaff.objects.filter( + complaint=complaint, role=ComplaintInvolvedStaff.RoleChoices.ACCUSED + ).select_related("staff") + ) + + existing_investigation = None + if explanation.is_used: + from apps.complaints.models import ChampionInvestigation + existing_investigation = ChampionInvestigation.objects.filter( + explanation=explanation + ).select_related("champion").first() + return render( request, "complaints/explanation_form.html", - {"complaint": complaint, "explanation": explanation, "original_explanation": original_explanation}, + { + "complaint": complaint, + "explanation": explanation, + "original_explanation": original_explanation, + "accused_staff": accused_staff, + "existing_investigation": existing_investigation, + "investigate_url": request.build_absolute_uri( + reverse("complaints:champion_start_investigation", kwargs={ + "complaint_id": complaint.id, + "token": explanation.token, + }) + ), + }, ) +def champion_start_investigation(request, complaint_id, token): + from .models import ( + ComplaintExplanation, ComplaintInvolvedStaff, + ChampionInvestigation, InvestigationQuestion, InvestigationResponse, + ComplaintUpdate, + ) + from apps.notifications.services import NotificationService + + complaint = get_object_or_404(Complaint, id=complaint_id) + explanation = get_object_or_404( + ComplaintExplanation.objects.select_related("staff", "staff__department"), + complaint=complaint, token=token, + ) + + if explanation.is_used: + return render(request, "complaints/explanation_already_submitted.html", { + "complaint": complaint, "explanation": explanation, + }) + + existing = ChampionInvestigation.objects.filter(explanation=explanation).first() + if existing: + return render(request, "complaints/investigation_already_started.html", { + "complaint": complaint, "investigation": existing, + }) + + accused_staff = list( + ComplaintInvolvedStaff.objects.filter( + complaint=complaint, role=ComplaintInvolvedStaff.RoleChoices.ACCUSED + ).select_related("staff") + ) + + if request.method == "POST": + import secrets + questions = request.POST.getlist("questions[]") + questions = [q.strip() for q in questions if q.strip()] + + selected_staff_ids = request.POST.getlist("accused_staff[]") + selected_staff_ids = [sid for sid in selected_staff_ids if sid] + + if not questions: + return render(request, "complaints/investigation_questions.html", { + "complaint": complaint, + "explanation": explanation, + "accused_staff": accused_staff, + "error": "Please add at least one question.", + }) + + if not selected_staff_ids: + return render(request, "complaints/investigation_questions.html", { + "complaint": complaint, + "explanation": explanation, + "accused_staff": accused_staff, + "error": "Please select at least one accused staff member.", + }) + + involved_dept = explanation.linked_involved_department + + investigation = ChampionInvestigation.objects.create( + complaint=complaint, + champion=explanation.staff, + involved_department=involved_dept, + explanation=explanation, + status="questions_sent", + ) + + for i, q_text in enumerate(questions): + InvestigationQuestion.objects.create( + investigation=investigation, + question_text=q_text, + order=i + 1, + ) + + domain = request.get_host() + staff_count = 0 + for sid in selected_staff_ids: + staff_qs = accused_staff + matched = next((s for s in staff_qs if str(s.staff_id) == sid), None) + if not matched: + try: + from apps.organizations.models import Staff + staff_obj = Staff.objects.get(id=sid) + matched = type('obj', (), {'staff': staff_obj, 'staff_id': staff_obj.id})() + except Exception: + continue + + staff_member = matched.staff if hasattr(matched, 'staff') else matched + resp_token = secrets.token_urlsafe(32) + inv_response = InvestigationResponse.objects.create( + investigation=investigation, + staff=staff_member, + token=resp_token, + ) + + for q in investigation.questions.all(): + InvestigationAnswer.objects.get_or_create( + response=inv_response, question=q, defaults={"answer_text": ""} + ) + + respond_url = f"https://{domain}/complaints/{complaint.id}/investigate/respond/{resp_token}/" + + staff_email = staff_member.email or (staff_member.user.email if hasattr(staff_member, 'user') and staff_member.user else None) + if staff_email: + try: + NotificationService.send_email( + email=staff_email, + subject=f"Investigation Questions - Complaint #{complaint.reference_number}", + message=( + f"Dear {staff_member.get_full_name()},\n\n" + f"You have been requested to answer investigation questions " + f"for complaint #{complaint.reference_number}.\n\n" + f"Please respond at: {respond_url}\n\n" + f"Note: This link can only be used once." + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Investigation Questions

+

Dear {staff_member.get_full_name()},

+

You have been requested to answer investigation questions for complaint + #{complaint.reference_number}.

+

The department champion would like your response before proceeding.

+

+ Answer Questions +

+

Note: This link can only be used once.

+
+
+ """, + related_object=complaint, + ) + inv_response.email_sent_at = timezone.now() + inv_response.save(update_fields=["email_sent_at"]) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to send investigation email to {staff_email}: {e}") + + staff_phone = staff_member.phone_number or (staff_member.user.phone if hasattr(staff_member, 'user') and staff_member.user else None) + if staff_phone: + try: + NotificationService.send_sms( + to=staff_phone, + message=f"You have investigation questions for complaint #{complaint.reference_number}. Please respond at: {respond_url}", + ) + inv_response.sms_sent_at = timezone.now() + inv_response.save(update_fields=["sms_sent_at"]) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to send investigation SMS to {staff_phone}: {e}") + + staff_count += 1 + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Champion {explanation.staff.get_full_name()} started investigation — questions sent to {staff_count} staff member(s)", + metadata={ + "investigation_id": str(investigation.id), + "staff_count": staff_count, + }, + ) + + return render(request, "complaints/investigation_questions.html", { + "complaint": complaint, + "explanation": explanation, + "accused_staff": accused_staff, + "success": f"Investigation started. Questions sent to {staff_count} staff member(s).", + }) + + return render(request, "complaints/investigation_questions.html", { + "complaint": complaint, + "explanation": explanation, + "accused_staff": accused_staff, + }) + + +def staff_investigation_form(request, complaint_id, token): + from .models import ( + InvestigationResponse, InvestigationAnswer, + ChampionInvestigation, ComplaintUpdate, + ) + from apps.notifications.services import NotificationService, get_email_header_html + + complaint = get_object_or_404(Complaint, id=complaint_id) + inv_response = get_object_or_404( + InvestigationResponse.objects.select_related( + "investigation", "investigation__champion", "staff" + ).prefetch_related("investigation__questions"), + token=token, + ) + + if inv_response.investigation.complaint_id != complaint.id: + return render(request, "complaints/investigation_error.html", { + "error": "Invalid link.", + }) + + if inv_response.is_completed: + return render(request, "complaints/investigation_already_submitted.html", { + "complaint": complaint, "inv_response": inv_response, + }) + + questions = list(inv_response.investigation.questions.all().order_by("order")) + + if request.method == "POST": + for q in questions: + answer_text = request.POST.get(f"question_{q.id}", "").strip() + InvestigationAnswer.objects.update_or_create( + response=inv_response, + question=q, + defaults={"answer_text": answer_text}, + ) + + inv_response.is_completed = True + inv_response.completed_at = timezone.now() + inv_response.save(update_fields=["is_completed", "completed_at"]) + + investigation = inv_response.investigation + if investigation.all_responses_received: + investigation.status = "answers_received" + investigation.save(update_fields=["status"]) + + champion = investigation.champion + if champion and champion.email: + domain = request.get_host() + review_url = f"https://{domain}/complaints/{complaint.id}/investigate/review/{investigation.explanation.token}/" + try: + NotificationService.send_email( + email=champion.email, + subject=f"All Investigation Responses Received - Complaint #{complaint.reference_number}", + message=( + f"Dear {champion.get_full_name()},\n\n" + f"All accused staff have responded to your investigation questions " + f"for complaint #{complaint.reference_number}.\n\n" + f"Please review and submit your final reply: {review_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

All Investigation Responses Received

+

Dear {champion.get_full_name()},

+

All accused staff have responded to your investigation questions for complaint #{complaint.reference_number}.

+

Please review and submit your final reply.

+ +
+
+""", + related_object=complaint, + ) + except Exception: + pass + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Investigation response submitted by {inv_response.staff.get_full_name()}", + ) + + return render(request, "complaints/investigation_success.html", { + "complaint": complaint, + "inv_response": inv_response, + }) + + answers_map = {} + for ans in InvestigationAnswer.objects.filter(response=inv_response).select_related("question"): + answers_map[str(ans.question_id)] = ans.answer_text + + question_answer_pairs = [] + for q in questions: + question_answer_pairs.append({ + "question": q, + "existing_answer": answers_map.get(str(q.id), ""), + }) + + return render(request, "complaints/investigation_respond.html", { + "complaint": complaint, + "inv_response": inv_response, + "question_answer_pairs": question_answer_pairs, + }) + + +def champion_review_answers(request, complaint_id, token): + from .models import ( + ComplaintExplanation, ComplaintInvolvedDepartment, + ChampionInvestigation, ComplaintUpdate, + ) + from apps.notifications.services import NotificationService, get_email_header_html + + complaint = get_object_or_404(Complaint, id=complaint_id) + explanation = get_object_or_404( + ComplaintExplanation.objects.select_related("staff", "staff__department"), + complaint=complaint, token=token, + ) + + investigation = ChampionInvestigation.objects.filter( + explanation=explanation + ).prefetch_related( + "questions", "responses__staff", "responses__answers__question" + ).first() + + if not investigation: + return render(request, "complaints/investigation_error.html", { + "error": "No investigation found for this complaint.", + }) + + if investigation.status == InvestigationStatus.REPLY_SUBMITTED: + return render(request, "complaints/investigation_already_submitted.html", { + "complaint": complaint, "investigation": investigation, + }) + + responses = list(investigation.responses.all().select_related("staff")) + questions = list(investigation.questions.all().order_by("order")) + + answer_lookup = {} + from .models import InvestigationAnswer + for ans in InvestigationAnswer.objects.filter( + response__investigation=investigation + ).select_related("question"): + key = (str(ans.response_id), str(ans.question_id)) + answer_lookup[key] = ans.answer_text + + staff_data = [] + for resp in responses: + qa_pairs = [] + for q in questions: + key = (str(resp.id), str(q.id)) + qa_pairs.append({ + "question": q, + "answer": answer_lookup.get(key, ""), + }) + staff_data.append({ + "response": resp, + "staff": resp.staff, + "is_completed": resp.is_completed, + "qa_pairs": qa_pairs, + }) + + if request.method == "POST": + final_reply = request.POST.get("final_reply", "").strip() + if not final_reply: + return render(request, "complaints/investigation_review.html", { + "complaint": complaint, + "explanation": explanation, + "investigation": investigation, + "questions": questions, + "staff_data": staff_data, + "error": "Please provide your final reply.", + }) + + investigation.final_reply = final_reply + investigation.status = InvestigationStatus.REPLY_SUBMITTED + investigation.save(update_fields=["final_reply", "status"]) + + explanation.explanation = final_reply + explanation.is_used = True + explanation.responded_at = timezone.now() + explanation.save(update_fields=["explanation", "is_used", "responded_at"]) + + involved_dept = investigation.involved_department or explanation.linked_involved_department + if involved_dept: + involved_dept.response_notes = final_reply + involved_dept.response_notes_en = final_reply + involved_dept.response_submitted = True + involved_dept.response_submitted_at = timezone.now() + involved_dept.manager_review_status = "pending" + involved_dept.manager_reviewed_by = None + involved_dept.manager_reviewed_at = None + involved_dept.acceptance_status = "pending" + involved_dept.accepted_by = None + involved_dept.accepted_at = None + involved_dept.acceptance_notes = "" + involved_dept.save() + + dept = involved_dept.department + if dept and dept.manager and dept.manager.email: + try: + review_url = request.build_absolute_uri( + reverse("organizations:department_manager_review", kwargs={ + "pk": dept.pk, + "idept_pk": involved_dept.pk, + }) + ) + NotificationService.send_email( + dept.manager.email, + subject=f"Champion Response Requires Your Review - {complaint.reference_number}", + message=( + f"A champion response for complaint {complaint.reference_number} " + f"from {dept.name} requires your review and approval.\n\n" + f"Please review at: {review_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Champion Response Requires Review

+

A champion response for complaint {complaint.reference_number} from {dept.name} requires your review and approval.

+ +
+
+""", + related_object=complaint, + ) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to send manager review notification: {e}") + + files = request.FILES.getlist("attachments") + from apps.complaints.models import ExplanationAttachment + for f in files: + ExplanationAttachment.objects.create( + explanation=explanation, + file=f, + filename=f.name, + file_type=f.content_type, + file_size=f.size, + ) + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Champion {explanation.staff.get_full_name()} submitted final reply after investigation — awaiting manager review", + metadata={ + "investigation_id": str(investigation.id), + "response_count": len(responses), + }, + ) + + return render(request, "complaints/explanation_success.html", { + "complaint": complaint, + "explanation": explanation, + "attachment_count": len(files), + }) + + return render(request, "complaints/investigation_review.html", { + "complaint": complaint, + "explanation": explanation, + "investigation": investigation, + "questions": questions, + "staff_data": staff_data, + }) + + from django.http import HttpResponse from django.utils.translation import gettext as _ @@ -3624,3 +4541,84 @@ def inquiry_explanation_form(request, inquiry_id, token): "complaints/inquiry_explanation_form.html", {"inquiry": inquiry, "explanation": explanation}, ) + + +def inquiry_respond_with_token(request, pk, token): + """ + Public-facing form for department staff to submit response to an inquiry. + Does NOT require authentication. Validates token and checks validity. + """ + from apps.core.ai_service import AIService + from apps.notifications.services import NotificationService + from apps.core.utils import build_public_track_url + + inquiry = get_object_or_404(Inquiry, pk=pk) + + if inquiry.response_token != token or not inquiry.response_token: + return render(request, "complaints/inquiry_response_token_invalid.html", {"inquiry": inquiry}) + + if inquiry.response_token_used: + return render(request, "complaints/inquiry_response_already_submitted.html", {"inquiry": inquiry}) + + if request.method == "POST": + response_en = request.POST.get("response_en", "").strip() + response_ar = request.POST.get("response_ar", "").strip() + response = response_en or response_ar + + if not response: + return render(request, "complaints/inquiry_response_form_token.html", { + "inquiry": inquiry, + "error": "Please enter a response in at least one language.", + }) + + inquiry.department_response_en = response_en + inquiry.department_response_ar = response_ar + inquiry.department_responded_at = timezone.now() + inquiry.dept_response_is_overdue = False + inquiry.dept_response_acceptance_status = "pending" + inquiry.response_token_used = True + inquiry.save() + + # AI summary + try: + import json + prompt = f"""Summarize the following department response to a patient inquiry in 2-3 concise sentences. + +Inquiry subject: {inquiry.subject} +Inquiry message: {(inquiry.message or '')[:500]} +Department response: {response[:500]} + +Generate JSON with "summary_en" and "summary_ar".""" + result = AIService.chat_completion(prompt=prompt, response_format="json_object") + parsed = json.loads(result) + inquiry.department_response_summary_en = parsed.get("summary_en", "") + inquiry.department_response_summary_ar = parsed.get("summary_ar", "") + inquiry.save(update_fields=["department_response_summary_en", "department_response_summary_ar"]) + except Exception: + pass + + # Notify inquirer + track_url = build_public_track_url("inquiry", inquiry.reference_number) + if inquiry.contact_phone: + try: + NotificationService.send_sms( + phone=inquiry.contact_phone, + message=f"PX360: Your inquiry {inquiry.reference_number} has been responded to. View: {track_url}", + related_object=inquiry, + ) + except Exception: + pass + if inquiry.contact_email: + try: + NotificationService.send_email( + email=inquiry.contact_email, + subject=f"PX360: Response to Your Inquiry {inquiry.reference_number}", + message=f"Your inquiry has been responded to.\n\nView: {track_url}", + related_object=inquiry, + ) + except Exception: + pass + + return render(request, "complaints/inquiry_response_success_token.html", {"inquiry": inquiry}) + + return render(request, "complaints/inquiry_response_form_token.html", {"inquiry": inquiry}) diff --git a/apps/core/ai_service.py b/apps/core/ai_service.py index 5f2fab0..c4d2dca 100644 --- a/apps/core/ai_service.py +++ b/apps/core/ai_service.py @@ -14,6 +14,7 @@ Features: import json import logging +import re from typing import Dict, List, Optional, Any import httpx @@ -45,6 +46,71 @@ class AIService: SEVERITY_CHOICES = ["low", "medium", "high", "critical"] PRIORITY_CHOICES = ["low", "medium", "high"] + @classmethod + def _repair_json(cls, text: str) -> str: + """Attempt to repair malformed JSON from LLM responses.""" + text = text.strip() + if not text: + return text + + def fix_unterminated_strings(s): + result = [] + i = 0 + in_string = False + escape = False + while i < len(s): + ch = s[i] + if escape: + result.append(ch) + escape = False + i += 1 + continue + if ch == '\\' and in_string: + result.append(ch) + escape = True + i += 1 + continue + if ch == '"': + if in_string: + in_string = False + else: + in_string = True + result.append(ch) + i += 1 + continue + if in_string and ch in ('\n', '\r'): + result.append(' ') + i += 1 + continue + result.append(ch) + i += 1 + if in_string: + result.append('"') + return ''.join(result) + + text = fix_unterminated_strings(text) + + open_braces = text.count('{') - text.count('}') + open_brackets = text.count('[') - text.count(']') + text += ']' * max(0, open_brackets) + '}' * max(0, open_braces) + + text = re.sub(r',\s*([}\]])', r'\1', text) + + return text + + @staticmethod + def _safe_json_loads(text: str): + """Parse JSON with repair fallback.""" + try: + return json.loads(text) + except json.JSONDecodeError: + repaired = AIService._repair_json(text) + try: + return json.loads(repaired) + except json.JSONDecodeError as e2: + logger.warning(f"JSON repair failed, falling back to defaults: {e2}") + raise + @classmethod def _get_api_key(cls) -> str: return getattr(settings, "OPENROUTER_API_KEY", None) or cls.OPENROUTER_API_KEY @@ -789,11 +855,12 @@ class AIService: prompt=prompt, system_prompt=system_prompt, response_format="json_object", - temperature=0.2, # Lower temperature for consistent classification + temperature=0.2, + max_tokens=2000, ) # Parse JSON response - result = json.loads(response) + result = cls._safe_json_loads(response) # Detect complaint type complaint_type = cls._detect_complaint_type(description + " " + (title or "")) diff --git a/apps/core/config_views.py b/apps/core/config_views.py index 10ae4ea..c391618 100644 --- a/apps/core/config_views.py +++ b/apps/core/config_views.py @@ -15,11 +15,11 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from apps.organizations.models import Department, Hospital -from apps.organizations.services import StaffService from apps.px_action_center.models import PXActionSLAConfig, RoutingRule from apps.complaints.models import OnCallAdminSchedule from apps.callcenter.models import CallRecord from apps.notifications.services import NotificationService +from apps.accounts.services import PasswordResetTokenService from apps.core.decorators import px_admin_required, admin_required from apps.accounts.models import User @@ -212,30 +212,28 @@ def reset_user_password(request, user_id): if request.tenant_hospital and target_user.hospital != request.tenant_hospital: return JsonResponse({"error": "You can only reset passwords for users in your hospital."}, status=403) - new_password = StaffService.generate_password() - target_user.set_password(new_password) - target_user.save(update_fields=["password"]) + if target_user.is_provisional: + return JsonResponse({"error": "Use the onboarding invitation flow for provisional users."}, status=400) - login_url = f"{request.scheme}://{request.get_host()}/accounts/login/" + base_url = f"{request.scheme}://{request.get_host()}" + reset_token = PasswordResetTokenService.create_reset_token(target_user) + reset_url = PasswordResetTokenService.build_reset_url(base_url, reset_token) html_message = render_to_string( "config/emails/reset_password_email.html", { "user": target_user, - "password": new_password, - "login_url": login_url, + "reset_url": reset_url, }, request=request, ) plain_message = ( f"Dear {target_user.get_full_name()},\n\n" - f"Your password has been reset by an administrator.\n\n" - f"Your new credentials:\n" - f"Email: {target_user.email}\n" - f"Password: {new_password}\n\n" - f"Please login and change your password immediately.\n" - f"Login URL: {login_url}" + f"Your PX360 password has been reset by an administrator.\n\n" + f"For your security, no password is sent by email. Use the link below to set a new password:\n" + f"{reset_url}\n\n" + f"This link expires in 24 hours. If you did not expect this reset, contact your system administrator." ) NotificationService.send_email( @@ -250,8 +248,7 @@ def reset_user_password(request, user_id): return JsonResponse( { "success": True, - "message": f"Password has been reset for {target_user.get_full_name()}. A new password has been sent to {target_user.email}.", - "password": new_password, + "message": f"Password reset link has been sent to {target_user.email}.", "user_name": target_user.get_full_name(), "user_email": target_user.email, } diff --git a/apps/core/management/commands/create_e2e_isolated_env.py b/apps/core/management/commands/create_e2e_isolated_env.py new file mode 100644 index 0000000..5171655 --- /dev/null +++ b/apps/core/management/commands/create_e2e_isolated_env.py @@ -0,0 +1,292 @@ +""" +Create an isolated E2E hospital that mirrors a source hospital's org hierarchy, +plus 10 role-bound test users, for Playwright QA. + +Mirrors (create-if-not-exists) from the source hospital (default HH-N / Al Nuzha): + Area -> Department -> Section -> SubSection + +LegacyLocation / LegacyMainSection / LegacySubSection are global reference tables +(shared by every hospital) and are intentionally NOT copied. + +Department role-holders (champion/manager/supervisor) are left NULL on copy; +the E2E champion user is then bound as champion of one department. + +Usage: + uv run manage.py create_e2e_isolated_env --delete-existing +""" + +from django.contrib.auth.models import Group +from django.core.management.base import BaseCommand +from django.db import transaction + +from apps.accounts.models import User +from apps.organizations.models import ( + Area, + Department, + Hospital, + Organization, + Section, + Staff, + SubSection, +) +from apps.px_sources.models import PXSource, SourceUser + +DEFAULT_PASSWORD = "Dev@123456" +DEFAULT_SOURCE_HOSPITAL_CODE = "HH-N" +E2E_ORG_CODE = "E2E-ORG" +E2E_HOSPITAL_CODE = "E2E-HOSP" +E2E_SOURCE_CODE = "E2E-TEST" + +USERS_CONFIG = [ + {"email": "e2e-px-admin@px360.test", "role": "PX Admin", "first": "E2E", "last": "PX Admin", "is_champion": False, "source_profile": False}, + {"email": "e2e-hospital-admin@px360.test", "role": "Hospital Admin", "first": "E2E", "last": "Hospital Admin"}, + {"email": "e2e-dept-manager@px360.test", "role": "Department Manager", "first": "E2E", "last": "Dept Manager"}, + {"email": "e2e-px-employee@px360.test", "role": "PX Employee", "first": "E2E", "last": "PX Employee"}, + {"email": "e2e-physician@px360.test", "role": "Physician", "first": "E2E", "last": "Physician"}, + {"email": "e2e-nurse@px360.test", "role": "Nurse", "first": "E2E", "last": "Nurse"}, + {"email": "e2e-staff@px360.test", "role": "Staff", "first": "E2E", "last": "Staff"}, + {"email": "e2e-viewer@px360.test", "role": "Viewer", "first": "E2E", "last": "Viewer"}, + {"email": "e2e-source-user@px360.test", "role": "PX Source User", "first": "E2E", "last": "Source User", "source_profile": True}, + {"email": "e2e-champion@px360.test", "role": "Department Manager", "first": "E2E", "last": "Champion", "is_champion": True}, +] + + +class Command(BaseCommand): + help = "Create an isolated E2E hospital (mirrors source hospital hierarchy) + 10 role users" + + def add_arguments(self, parser): + parser.add_argument("--password", default=DEFAULT_PASSWORD, help="Password for test users") + parser.add_argument("--delete-existing", action="store_true", help="Delete existing E2E users + hospital first") + parser.add_argument("--dry-run", action="store_true", help="Preview without changes") + parser.add_argument("--source-hospital", default=DEFAULT_SOURCE_HOSPITAL_CODE, help="Source hospital code to mirror") + + @transaction.atomic() + def handle(self, *args, **options): + password = options["password"] + delete_existing = options["delete_existing"] + dry_run = options["dry_run"] + source_code = options["source_hospital"] + + self.stdout.write(self.style.SUCCESS("\n=== Creating Isolated E2E Environment ===\n")) + + if dry_run: + self.stdout.write(self.style.WARNING("DRY RUN - no changes\n")) + + source = Hospital.objects.filter(code=source_code).first() + if not source: + self.stdout.write(self.style.ERROR(f"Source hospital '{source_code}' not found.")) + return + self.stdout.write(f"Source hospital: {source.name} ({source.code})") + + # ------------------------------------------------------------------ + # 0. Tear down existing E2E artifacts + # ------------------------------------------------------------------ + if delete_existing and not dry_run: + udel = User.objects.filter(email__endswith="@px360.test").delete() + self.stdout.write(f" Deleted {udel[0]} existing E2E user rows.") + Hospital.objects.filter(code=E2E_HOSPITAL_CODE).delete() + self.stdout.write(" Deleted existing E2E hospital (cascade removes mirrored hierarchy).") + + # ------------------------------------------------------------------ + # 1. Organization + Hospital + # ------------------------------------------------------------------ + org, _ = Organization.objects.get_or_create(code=E2E_ORG_CODE, defaults={ + "name": "E2E Org", + "name_ar": "بيئة الاختبار", + "status": "active", + }) + e2e, e2e_created = Hospital.objects.get_or_create( + code=E2E_HOSPITAL_CODE, + defaults={ + "organization": org, + "name": "E2E Test Hospital", + "name_ar": "مستشفى الاختبار", + "display_name": "E2E Test Hospital", + "status": "active", + }, + ) + self.stdout.write(self.style.SUCCESS(f" Hospital: {e2e.code} ({'created' if e2e_created else 'exists'})")) + + if dry_run: + self.stdout.write(self.style.WARNING("\nDRY RUN - would mirror hierarchy + create users.\n")) + return + + # ------------------------------------------------------------------ + # 2. Mirror hierarchy: Area -> Department -> Section -> SubSection + # ------------------------------------------------------------------ + # Areas + area_count = 0 + src_area_code_by_id = {} + for a in Area.objects.filter(hospital=source): + src_area_code_by_id[a.id] = a.code + _, created = Area.objects.get_or_create( + hospital=e2e, code=a.code, + defaults={ + "name_en": a.name_en, "name_ar": a.name_ar, + "location_type": a.location_type, "status": a.status, + }, + ) + area_count += int(created) + e2e_area_by_code = {a.code: a for a in Area.objects.filter(hospital=e2e)} + self.stdout.write(self.style.SUCCESS(f" Areas: +{area_count} new (total {len(e2e_area_by_code)})")) + + # Departments (pass 1: no parent; pass 2: link parent) + dept_count = 0 + e2e_dept_by_code = {} + for d in Department.objects.filter(hospital=source).select_related("area", "parent"): + e2e_area = e2e_area_by_code.get(d.area.code) if d.area_id and d.area else None + obj, created = Department.objects.get_or_create( + hospital=e2e, code=d.code, + defaults={ + "name": d.name, "name_en": d.name_en, "name_ar": d.name_ar, + "hr_name": d.hr_name, "main_section": d.main_section, + "category": d.category, "location_type": d.location_type, + "sub_location": d.sub_location, "floor": d.floor, + "area": e2e_area, "phone": d.phone, "email": d.email, + "location": d.location, "status": d.status, + "old_name_en": d.old_name_en, "old_name_ar": d.old_name_ar, + "champion_email": d.champion_email, + }, + ) + e2e_dept_by_code[d.code] = obj + dept_count += int(created) + # parents + parent_count = 0 + for d in Department.objects.filter(hospital=source).select_related("parent"): + if d.parent_id and d.parent.code in e2e_dept_by_code: + e2e_obj = e2e_dept_by_code[d.code] + e2e_parent = e2e_dept_by_code[d.parent.code] + if e2e_obj.parent_id != e2e_parent.id: + e2e_obj.parent = e2e_parent + e2e_obj.save(update_fields=["parent"]) + parent_count += 1 + self.stdout.write(self.style.SUCCESS( + f" Departments: +{dept_count} new, {parent_count} parents linked (total {len(e2e_dept_by_code)})" + )) + + # Sections + sect_count = 0 + e2e_sect_by_key = {} # (dept_code, section_code) -> Section + for s in Section.objects.filter(department__hospital=source).select_related("department"): + e2e_dept = e2e_dept_by_code.get(s.department.code) + if not e2e_dept: + continue + obj, created = Section.objects.get_or_create( + department=e2e_dept, code=s.code, + defaults={ + "name_en": s.name_en, "name_ar": s.name_ar, + "location_type": s.location_type, "sub_location": s.sub_location, + "floor": s.floor, "display_name_en": s.display_name_en, + "display_name_ar": s.display_name_ar, "old_name_en": s.old_name_en, + "old_name_ar": s.old_name_ar, "status": s.status, + }, + ) + e2e_sect_by_key[(s.department.code, s.code)] = obj + sect_count += int(created) + self.stdout.write(self.style.SUCCESS(f" Sections: +{sect_count} new")) + + # SubSections + sub_count = 0 + for ss in SubSection.objects.filter(section__department__hospital=source).select_related("section", "section__department"): + key = (ss.section.department.code, ss.section.code) + e2e_section = e2e_sect_by_key.get(key) + if not e2e_section: + continue + _, created = SubSection.objects.get_or_create( + section=e2e_section, code=ss.code, + defaults={"name_en": ss.name_en, "name_ar": ss.name_ar, "status": ss.status}, + ) + sub_count += int(created) + self.stdout.write(self.style.SUCCESS(f" SubSections: +{sub_count} new")) + + # ------------------------------------------------------------------ + # 3. PXSource + # ------------------------------------------------------------------ + px_source, _ = PXSource.objects.get_or_create( + code=E2E_SOURCE_CODE, + defaults={ + "name_en": "E2E Test Source", + "name_ar": "مصدر اختبار E2E", + "source_type": "internal", + "contact_email": "e2e@px360.test", + "is_active": True, + }, + ) + + # ------------------------------------------------------------------ + # 4. Users (bound to E2E hospital) + # ------------------------------------------------------------------ + champion_user = None + created_count = 0 + for cfg in USERS_CONFIG: + email = cfg["email"] + try: + group = Group.objects.get(name=cfg["role"]) + except Group.DoesNotExist: + self.stdout.write(self.style.WARNING(f" SKIP {email}: group '{cfg['role']}' missing")) + continue + + user, created = User.objects.get_or_create( + email=email, + defaults={ + "first_name": cfg["first"], + "last_name": cfg["last"], + "hospital": e2e, + "is_active": True, + "is_staff": False, + }, + ) + if created: + user.set_password(password) + created_count += 1 + self.stdout.write(self.style.SUCCESS(f" CREATED: {email} ({cfg['role']})")) + else: + user.groups.clear() + user.hospital = e2e + self.stdout.write(f" EXISTS: {email} ({cfg['role']})") + + user.groups.add(group) + user.save(update_fields=["hospital"]) if not created else user.save() + + if cfg.get("is_champion"): + champion_user = user + if cfg.get("source_profile"): + SourceUser.objects.get_or_create( + user=user, source=px_source, + defaults={"is_active": True, "hospital": e2e}, + ) + + # ------------------------------------------------------------------ + # 5. Bind champion user as champion of one department + # ------------------------------------------------------------------ + if champion_user: + champ_dept = Department.objects.filter(hospital=e2e).first() + if champ_dept: + champion_staff, _ = Staff.objects.get_or_create( + user=champion_user, + defaults={ + "first_name": champion_user.first_name, + "last_name": champion_user.last_name, + "hospital": e2e, + "department": champ_dept, + "status": "active", + "staff_type": "admin", + "job_title": "Department Champion", + "employee_id": "E2E-CHAMPION", + }, + ) + champ_dept.champion = champion_staff + champ_dept.save(update_fields=["champion"]) + champion_user.department = champ_dept + champion_user.save(update_fields=["department"]) + self.stdout.write(self.style.SUCCESS(f" Champion bound to: {champ_dept.name_en or champ_dept.name}")) + + self.stdout.write(self.style.SUCCESS( + f"\nDone. Created {created_count} users. " + f"Users total: {User.objects.filter(email__endswith='@px360.test').count()}" + )) + self.stdout.write(f"Hospital: {e2e.code} | " + f"Areas={Area.objects.filter(hospital=e2e).count()} " + f"Depts={Department.objects.filter(hospital=e2e).count()} " + f"Sections={Section.objects.filter(department__hospital=e2e).count()} " + f"SubSections={SubSection.objects.filter(section__department__hospital=e2e).count()}") diff --git a/apps/core/management/commands/create_e2e_test_users.py b/apps/core/management/commands/create_e2e_test_users.py index be73dc1..7c7dbdb 100644 --- a/apps/core/management/commands/create_e2e_test_users.py +++ b/apps/core/management/commands/create_e2e_test_users.py @@ -125,11 +125,12 @@ class Command(BaseCommand): }, { "email": "e2e-champion@px360.test", - "role": "Champion", + "role": "Department Manager", "first_name": "E2E", "last_name": "Champion", "hospital": hospital, "is_staff": False, + "is_champion": True, }, ] @@ -176,6 +177,26 @@ class Command(BaseCommand): user.groups.add(group) user.save() + if config.get("is_champion"): + from apps.organizations.models import Staff, Department + dept = Department.objects.filter(hospital=config["hospital"]).first() + if dept: + champion_staff, _ = Staff.objects.get_or_create( + user=user, + defaults={ + "first_name": user.first_name, + "last_name": user.last_name, + "hospital": config["hospital"], + "department": dept, + "status": "active", + }, + ) + dept.champion = champion_staff + dept.save(update_fields=["champion"]) + user.department = dept + user.save(update_fields=["department"]) + self.stdout.write(f" + Set as champion for {dept.name}") + if config.get("create_source_profile"): _, created_su = SourceUser.objects.get_or_create( user=user, diff --git a/apps/core/management/commands/send_example_emails.py b/apps/core/management/commands/send_example_emails.py index 85030c5..ea0c2d8 100644 --- a/apps/core/management/commands/send_example_emails.py +++ b/apps/core/management/commands/send_example_emails.py @@ -93,10 +93,10 @@ class Command(BaseCommand): self.stdout.write(self.style.WARNING('\n📧 ORGANIZATIONS EMAILS\n')) try: - self._send_staff_credentials() + self._send_staff_password_reset() sent_count += 1 except Exception as e: - self.stdout.write(self.style.ERROR(f'❌ Staff Credentials: {str(e)}')) + self.stdout.write(self.style.ERROR(f'❌ Staff Password Reset: {str(e)}')) failed_count += 1 # Complaints Emails @@ -325,7 +325,9 @@ class Command(BaseCommand): """Example onboarding invitation email (uses template)""" context = { 'user': type('obj', (object,), {'first_name': 'Ahmed', 'email': 'ahmed@hospital.com'})(), - 'invitation_url': 'https://px360.sa/onboarding/setup/abc123token', + 'activation_url': 'https://px360.sa/onboarding/setup/abc123token', + 'expires_at': 'April 12, 2026', + 'days_remaining': 7, } html_content = render_to_string('accounts/onboarding/invitation_email.html', context) text_content = f""" @@ -353,7 +355,9 @@ class Command(BaseCommand): """Example onboarding reminder email (uses template)""" context = { 'user': type('obj', (object,), {'first_name': 'Sara', 'email': 'sara@hospital.com'})(), - 'invitation_url': 'https://px360.sa/onboarding/setup/reminder456token', + 'activation_url': 'https://px360.sa/onboarding/setup/reminder456token', + 'expires_at': 'April 12, 2026', + 'days_remaining': 3, } html_content = render_to_string('accounts/onboarding/reminder_email.html', context) text_content = """ @@ -372,7 +376,15 @@ class Command(BaseCommand): def _send_onboarding_completion(self): """Example onboarding completion email (uses template)""" context = { - 'user': type('obj', (object,), {'first_name': 'Mohammed', 'email': 'mohammed@hospital.com'})(), + 'user': type('obj', (object,), { + 'first_name': 'Mohammed', + 'email': 'mohammed@hospital.com', + 'get_full_name': lambda: 'Mohammed Al-Sayed', + 'department': type('obj', (object,), {'name': 'Patient Experience'})(), + })(), + 'user_detail_url': 'https://px360.sa/accounts/onboarding/provisional/mohammed/progress/', + 'role_display': 'PX Employee', + 'completed_at': 'April 5, 2026 09:30', } html_content = render_to_string('accounts/onboarding/completion_email.html', context) text_content = """ @@ -411,30 +423,32 @@ class Command(BaseCommand): # ORGANIZATIONS EMAILS # ======================================================================== - def _send_staff_credentials(self): - """Example staff credentials email (uses template)""" + def _send_staff_password_reset(self): + """Example staff password reset email (uses template)""" context = { - 'staff_name': 'Dr. Fatima Al-Rashid', - 'username': 'fatima.alrashid', - 'password': 'TempPass123!', - 'login_url': 'https://px360.sa/login', + 'staff': type('obj', (object,), { + 'get_full_name': lambda: 'Dr. Fatima Al-Rashid', + 'email': 'fatima@hospital.com', + })(), + 'user': type('obj', (object,), {'username': 'fatima.alrashid'})(), + 'reset_url': 'https://px360.sa/accounts/password/reset/abc123token/', } html_content = render_to_string('organizations/emails/staff_credentials.html', context) text_content = f""" - Your PX360 Account Credentials + Your PX360 Account Password Setup Dear Dr. Fatima Al-Rashid, - Your account has been created. Here are your login credentials: + Your account has been created. For your security, no password is sent by email. Username: fatima.alrashid - Temporary Password: TempPass123! + Email: fatima@hospital.com - Login URL: https://px360.sa/login + Set password: https://px360.sa/accounts/password/reset/abc123token/ - Please change your password after your first login. + This link expires in 24 hours. """ - self._send_email('Your PX360 Account Credentials', html_content, text_content) + self._send_email('Set Your PX360 Password', html_content, text_content) # ======================================================================== # COMPLAINTS EMAILS diff --git a/apps/core/management/commands/setup_dev_environment.py b/apps/core/management/commands/setup_dev_environment.py index 66be3c4..07a8bd0 100644 --- a/apps/core/management/commands/setup_dev_environment.py +++ b/apps/core/management/commands/setup_dev_environment.py @@ -28,7 +28,7 @@ from django.db import transaction from django.contrib.auth.models import Group, Permission from django.utils import timezone -from apps.organizations.models import Organization, Hospital, Location, MainSection, SubSection +from apps.organizations.models import Organization, Hospital, LegacyLocation, LegacyMainSection, LegacySubSection from apps.accounts.models import Role, User from apps.px_sources.models import PXSource from apps.surveys.models import SurveyTemplate, SurveyQuestion, QuestionType @@ -120,9 +120,9 @@ class Command(BaseCommand): self.stdout.write("-" * 70) if self.dry_run: hospitals = [ - type("Hospital", (), {"code": "NUZHA-DEV", "name": "Nuzha"})(), - type("Hospital", (), {"code": "OLAYA-DEV", "name": "Olaya"})(), - type("Hospital", (), {"code": "SUWAIDI-DEV", "name": "Suwaidi"})(), + type("Hospital", (), {"code": "HH-N", "name": "Al Nuzha"})(), + type("Hospital", (), {"code": "HH-O", "name": "Al Olya"})(), + type("Hospital", (), {"code": "HH-S", "name": "Al Suwaidi"})(), ] else: hospitals = Hospital.objects.all() @@ -1387,26 +1387,26 @@ class Command(BaseCommand): return for loc in locations_data: - Location.objects.update_or_create( + LegacyLocation.objects.update_or_create( id=loc["id"], defaults={"name_ar": loc["name_ar"], "name_en": loc["name_en"]}, ) self.stdout.write(f" ✓ Created/Updated: {len(locations_data)} Locations") for sec in main_sections_data: - MainSection.objects.update_or_create( + LegacyMainSection.objects.update_or_create( id=sec["id"], defaults={"name_ar": sec["name_ar"], "name_en": sec["name_en"]}, ) self.stdout.write(f" ✓ Created/Updated: {len(main_sections_data)} Main Sections") try: - SubSection.objects.all().delete() + LegacySubSection.objects.all().delete() except Exception: self.stdout.write(self.style.WARNING(" ⚠ Skipping SubSection deletion - some are referenced")) subsections_to_create = [ - SubSection( + LegacySubSection( internal_id=int(item["id"]), name_en=item["name_en"], name_ar=item["name_ar"], @@ -1415,7 +1415,7 @@ class Command(BaseCommand): ) for item in subsections_data ] - SubSection.objects.bulk_create(subsections_to_create, ignore_conflicts=True) + LegacySubSection.objects.bulk_create(subsections_to_create, ignore_conflicts=True) self.stdout.write(f" ✓ Created: {len(subsections_data)} Sub Sections") def create_roles_and_groups(self): @@ -2238,9 +2238,9 @@ class Command(BaseCommand): if not self.dry_run: self.stdout.write(f"\n Organization: {Organization.objects.count()}") self.stdout.write(f" Hospitals: {Hospital.objects.count()}") - self.stdout.write(f" Locations: {Location.objects.count()}") - self.stdout.write(f" Main Sections: {MainSection.objects.count()}") - self.stdout.write(f" Sub Sections: {SubSection.objects.count()}") + self.stdout.write(f" Locations: {LegacyLocation.objects.count()}") + self.stdout.write(f" Main Sections: {LegacyMainSection.objects.count()}") + self.stdout.write(f" Sub Sections: {LegacySubSection.objects.count()}") self.stdout.write(f" Roles: {Role.objects.count()}") self.stdout.write(f" PX Sources: {PXSource.objects.count()}") if not self.skip_surveys: diff --git a/apps/core/management/commands/test_email.py b/apps/core/management/commands/test_email.py new file mode 100644 index 0000000..1e26387 --- /dev/null +++ b/apps/core/management/commands/test_email.py @@ -0,0 +1,24 @@ +from django.core.management.base import BaseCommand, CommandError +from django.core.mail import send_mail + + +class Command(BaseCommand): + help = "Send a test email to verify SMTP configuration" + + def add_arguments(self, parser): + parser.add_argument("to", help="Recipient email address") + + def handle(self, *args, **options): + to = options["to"] + try: + send_mail( + subject="PX360 Test Email", + message="This is a test email from PX360. If you received this, your Outlook SMTP configuration is working correctly.", + from_email=None, + recipient_list=[to], + fail_silently=False, + ) + except Exception as e: + raise CommandError(f"Failed to send email: {e}") + + self.stdout.write(self.style.SUCCESS(f"Test email sent to {to}")) diff --git a/apps/core/middleware.py b/apps/core/middleware.py index 223d2bb..de8714e 100644 --- a/apps/core/middleware.py +++ b/apps/core/middleware.py @@ -90,8 +90,8 @@ class TenantMiddleware(MiddlewareMixin): class DepartmentRespondentMiddleware(MiddlewareMixin): """ - Restrict Department Respondent users to only their department detail page - and inquiry department response pages. + Restrict Department Respondent users to only their department detail page, + inquiry department response pages, and observation department response pages. """ ALLOWED_PATH_PREFIXES = [ @@ -102,6 +102,7 @@ class DepartmentRespondentMiddleware(MiddlewareMixin): "/core/select-hospital/", "/organizations/departments/", "/inquiries/", + "/observations/", "/api/", "/health/", "/admin/", @@ -131,7 +132,7 @@ class DepartmentRespondentMiddleware(MiddlewareMixin): for prefix in self.ALLOWED_PATH_PREFIXES: if path.startswith(prefix): if path.startswith("/organizations/departments/"): - if "set-respondent" in path or "edit" in path or "delete" in path: + if "set-champion" in path or "edit" in path or "delete" in path: from django.http import HttpResponseForbidden return HttpResponseForbidden() @@ -153,6 +154,36 @@ class DepartmentRespondentMiddleware(MiddlewareMixin): return HttpResponseForbidden() return None + if path.startswith("/observations/"): + if path.endswith("/department-response/"): + from apps.observations.models import Observation + + pk = view_kwargs.get("pk") + if pk: + try: + observation = Observation.objects.get(pk=pk) + user_dept = request.user.department + if observation.assigned_department == user_dept: + return None + except Observation.DoesNotExist: + pass + from django.http import HttpResponseForbidden + + return HttpResponseForbidden() + pk = view_kwargs.get("pk") + if pk: + from apps.observations.models import Observation + + try: + observation = Observation.objects.get(pk=pk) + user_dept = request.user.department + if observation.assigned_department == user_dept: + return None + except Observation.DoesNotExist: + pass + from django.http import HttpResponseForbidden + + return HttpResponseForbidden() return None from django.http import HttpResponseForbidden diff --git a/apps/core/migrations/0002_add_note_model.py b/apps/core/migrations/0002_add_note_model.py new file mode 100644 index 0000000..4c31715 --- /dev/null +++ b/apps/core/migrations/0002_add_note_model.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-05-12 18:28 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Note', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('object_id', models.UUIDField()), + ('note', models.TextField()), + ('is_internal', models.BooleanField(default=True)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notes', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['content_type', 'object_id'], name='core_note_content_948980_idx')], + }, + ), + ] diff --git a/apps/core/migrations/0003_referencesequence.py b/apps/core/migrations/0003_referencesequence.py new file mode 100644 index 0000000..5cc4fd3 --- /dev/null +++ b/apps/core/migrations/0003_referencesequence.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.1 on 2026-06-14 10:48 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_add_note_model'), + ] + + operations = [ + migrations.CreateModel( + name='ReferenceSequence', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('prefix', models.CharField(db_index=True, help_text='Module prefix, e.g. CMP/INQ/OBS/APR/SGT', max_length=8)), + ('hospital_token', models.CharField(db_index=True, help_text='Sanitized hospital code', max_length=24)), + ('year_month', models.CharField(db_index=True, help_text='YYYYMM', max_length=6)), + ('last_number', models.IntegerField(default=0)), + ], + options={ + 'indexes': [models.Index(fields=['prefix', 'hospital_token', 'year_month'], name='core_refere_prefix_a449a6_idx')], + 'unique_together': {('prefix', 'hospital_token', 'year_month')}, + }, + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index fff46be..1ee97a0 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -159,6 +159,30 @@ class SeverityChoices(BaseChoices): CRITICAL = "critical", _("Critical") +class Note(UUIDModel, TimeStampedModel): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.UUIDField() + content_object = GenericForeignKey("content_type", "object_id") + note = models.TextField() + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="notes", + ) + is_internal = models.BooleanField(default=True) + + class Meta: + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["content_type", "object_id"]), + ] + + def __str__(self): + return f"Note by {self.created_by} on {self.created_at.strftime('%Y-%m-%d %H:%M')}" + + class TenantModel(models.Model): """ Abstract base model for tenant-aware models. @@ -174,3 +198,43 @@ class TenantModel(models.Model): class Meta: abstract = True + + +class ReferenceSequence(UUIDModel, TimeStampedModel): + """ + Monotonic per-month sequence counter for reference numbers. + + Keyed by (prefix, hospital_token, year_month) so that each module/hospital + combination has its own sequence that resets monthly. Incremented atomically + via select_for_update to be safe under concurrent submissions. + """ + + prefix = models.CharField(max_length=8, db_index=True, help_text="Module prefix, e.g. CMP/INQ/OBS/APR/SGT") + hospital_token = models.CharField(max_length=24, db_index=True, help_text="Sanitized hospital code") + year_month = models.CharField(max_length=6, db_index=True, help_text="YYYYMM") + last_number = models.IntegerField(default=0) + + class Meta: + unique_together = [("prefix", "hospital_token", "year_month")] + indexes = [models.Index(fields=["prefix", "hospital_token", "year_month"])] + + def __str__(self): + return f"{self.prefix}-{self.year_month}-{self.hospital_token} -> {self.last_number}" + + @classmethod + def next_number(cls, prefix, hospital_token, year_month): + """Atomically allocate and return the next number in the sequence.""" + from django.db import transaction + + with transaction.atomic(): + obj, created = cls.objects.select_for_update().get_or_create( + prefix=prefix, + hospital_token=hospital_token, + year_month=year_month, + defaults={"last_number": 1}, + ) + if created: + return 1 + obj.last_number += 1 + obj.save(update_fields=["last_number"]) + return obj.last_number diff --git a/apps/core/reference.py b/apps/core/reference.py new file mode 100644 index 0000000..7bd95c6 --- /dev/null +++ b/apps/core/reference.py @@ -0,0 +1,41 @@ +""" +Unified reference-number generator. + +Format: {PREFIX}-{YYYYMM}-{HOSPITAL_TOKEN}-{SEQ:04d} + e.g. CMP-202606-HHN-0001 + + PREFIX module prefix (CMP/INQ/OBS/APR/SGT) + YYYYMM creation month + HOSPITAL_TOKEN hospital.code sanitized to uppercase alphanumerics + (HH-N -> HHN, E2E-HOSP -> E2EHOSP); "GEN" when no hospital + SEQ 4-digit monthly sequence per (prefix, hospital, month) + +The sequence is allocated atomically via ReferenceSequence so it is safe under +concurrent submissions. Legacy references keep their old format (no migration +of historical data); only new records use this generator. +""" + +import re +from datetime import datetime + +_TOKEN_RE = re.compile(r"[^A-Z0-9]") + + +def sanitize_hospital_token(hospital) -> str: + """Derive a clean, format-safe token from a hospital instance.""" + code = getattr(hospital, "code", None) if hospital else None + if not code: + return "GEN" + token = _TOKEN_RE.sub("", str(code).upper()) + return token or "GEN" + + +def generate_reference(prefix: str, hospital) -> str: + """Generate a unified reference number for the given module prefix.""" + from apps.core.models import ReferenceSequence + + prefix = (prefix or "").upper() + token = sanitize_hospital_token(hospital) + year_month = datetime.now().strftime("%Y%m") + number = ReferenceSequence.next_number(prefix, token, year_month) + return f"{prefix}-{year_month}-{token}-{number:04d}" diff --git a/apps/core/templatetags/email_tags.py b/apps/core/templatetags/email_tags.py index 99881d4..640c857 100644 --- a/apps/core/templatetags/email_tags.py +++ b/apps/core/templatetags/email_tags.py @@ -6,4 +6,4 @@ register = template.Library() @register.simple_tag def email_logo_url(): - return getattr(settings, "EMAIL_LOGO_URL", f"{settings.STATIC_URL}img/HH_P_H_Logo.png") + return getattr(settings, "EMAIL_LOGO_URL", f"{settings.STATIC_URL}img/HH_P_V_Logo(hospital)_.png") diff --git a/apps/core/tests.py b/apps/core/tests.py new file mode 100644 index 0000000..5b10caf --- /dev/null +++ b/apps/core/tests.py @@ -0,0 +1,138 @@ +""" +Tests for the unified reference-number generator (apps.core.reference). + +Covers: hospital-token sanitization, format, per-module prefix coverage, +sequence monotonicity, None-hospital fallback, and concurrent allocation +uniqueness. +""" + +import threading +from datetime import datetime +from unittest.mock import patch + +from django.test import TestCase, TransactionTestCase + +from apps.core.models import ReferenceSequence +from apps.core.reference import generate_reference, sanitize_hospital_token +from apps.organizations.models import Hospital, Organization + + +def _make_hospital(code="TEST-HOSP"): + org, _ = Organization.objects.get_or_create(code="TEST-ORG", defaults={"name": "Test Org", "status": "active"}) + hospital, _ = Hospital.objects.get_or_create( + code=code, + defaults={"organization": org, "name": f"Test {code}", "status": "active"}, + ) + return hospital + + +class SanitizeTokenTest(TestCase): + def test_strips_hyphens_and_uppercases(self): + class H: + def __init__(self, code): + self.code = code + + self.assertEqual(sanitize_hospital_token(H("HH-N")), "HHN") + self.assertEqual(sanitize_hospital_token(H("e2e-hosp")), "E2EHOSP") + self.assertEqual(sanitize_hospital_token(H("main-campus-1")), "MAINCAMPUS1") + + def test_none_or_empty_hospital(self): + self.assertEqual(sanitize_hospital_token(None), "GEN") + self.assertEqual(sanitize_hospital_token(object()), "GEN") + + class Empty: + code = "" + + self.assertEqual(sanitize_hospital_token(Empty()), "GEN") + + def test_strips_non_alphanumeric(self): + class H: + def __init__(self, code): + self.code = code + + self.assertEqual(sanitize_hospital_token(H("NUZHA@2026")), "NUZHA2026") + self.assertEqual(sanitize_hospital_token(H(" spaced ")), "SPACED") + + +class GenerateReferenceFormatTest(TestCase): + def setUp(self): + self.hospital = _make_hospital("HH-N") + + def test_format_and_prefix(self): + with patch("apps.core.reference.datetime") as mock_dt: + mock_dt.now.return_value = datetime(2026, 6, 14) + for prefix in ("CMP", "INQ", "OBS", "APR", "SGT"): + ref = generate_reference(prefix, self.hospital) + self.assertRegex( + ref, + rf"^{prefix}-202606-HHN-0001$", + f"unexpected ref {ref} for prefix {prefix}", + ) + + def test_sequence_increments_monotonically(self): + refs = [generate_reference("CMP", self.hospital) for _ in range(5)] + self.assertEqual(refs, [f"CMP-202606-HHN-{i:04d}" for i in range(1, 6)]) + # all unique + self.assertEqual(len(set(refs)), len(refs)) + + def test_each_module_has_independent_sequence(self): + with patch("apps.core.reference.datetime") as mock_dt: + mock_dt.now.return_value = datetime(2026, 6, 14) + cmp1 = generate_reference("CMP", self.hospital) + inq1 = generate_reference("INQ", self.hospital) + cmp2 = generate_reference("CMP", self.hospital) + self.assertIn("-0001", cmp1) + self.assertIn("-0001", inq1) # independent sequence + self.assertIn("-0002", cmp2) + + def test_per_hospital_isolation(self): + h2 = _make_hospital("HH-A") + with patch("apps.core.reference.datetime") as mock_dt: + mock_dt.now.return_value = datetime(2026, 6, 14) + ref_n = generate_reference("CMP", self.hospital) # HHN + ref_a = generate_reference("CMP", h2) # HHA + self.assertIn("-HHN-", ref_n) + self.assertIn("-HHA-", ref_a) + self.assertIn("-0001", ref_n) + self.assertIn("-0001", ref_a) # separate counter per hospital + + def test_none_hospital_fallback(self): + ref = generate_reference("CMP", None) + self.assertRegex(ref, r"^CMP-\d{6}-GEN-0001$") + + def test_sequence_resets_per_month(self): + with patch("apps.core.reference.datetime") as mock_dt: + mock_dt.now.return_value = datetime(2026, 6, 14) + generate_reference("CMP", self.hospital) + generate_reference("CMP", self.hospital) # 202606 -> 2 + mock_dt.now.return_value = datetime(2026, 7, 1) + july_ref = generate_reference("CMP", self.hospital) + self.assertIn("-202607-HHN-0001", july_ref) + + +class ConcurrencyTest(TransactionTestCase): + """Concurrent allocation must never produce duplicate sequence numbers.""" + + def test_concurrent_allocations_are_unique(self): + hospital = _make_hospital("HH-S") + n = 40 + results = [None] * n + barrier = threading.Barrier(n) + + def worker(idx): + barrier.wait() + results[idx] = generate_reference("CMP", hospital) + + threads = [threading.Thread(target=worker, args=(i,)) for i in range(n)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=30) + + self.assertNotIn(None, results, "a worker did not produce a reference") + self.assertEqual(len(set(results)), n, "duplicate references allocated under concurrency") + # numbers should be exactly 1..n + numbers = sorted(int(r.split("-")[-1]) for r in results) + self.assertEqual(numbers, list(range(1, n + 1))) + # one sequence row for this key + self.assertEqual(ReferenceSequence.objects.filter(prefix="CMP", hospital_token="HHS").count(), 1) diff --git a/apps/core/urls.py b/apps/core/urls.py index 3aa9a76..3d7ec01 100644 --- a/apps/core/urls.py +++ b/apps/core/urls.py @@ -13,9 +13,11 @@ from .views import ( public_observation_submit, public_track, public_track_api, + public_set_satisfaction, api_hospitals, api_observation_categories, - set_language + set_language, + add_note ) from . import config_views @@ -36,11 +38,15 @@ urlpatterns = [ path('public/inquiry/submit/', public_inquiry_submit, name='public_inquiry_submit'), path('public/observation/submit/', public_observation_submit, name='public_observation_submit'), path('api/track/', public_track_api, name='public_track_api'), + path('api/public/set-satisfaction/', public_set_satisfaction, name='public_set_satisfaction'), path('api/hospitals/', api_hospitals, name='api_hospitals'), path('api/observation-categories/', api_observation_categories, name='api_observation_categories'), # Language switching path('set-language/', set_language, name='set_language'), + + # Notes + path('notes/add/', add_note, name='add_note'), ] # Configuration URLs (separate app_name) diff --git a/apps/core/utils.py b/apps/core/utils.py index 5e113d0..5655e9e 100644 --- a/apps/core/utils.py +++ b/apps/core/utils.py @@ -1,3 +1,5 @@ +from django.urls import reverse + from apps.accounts.models import User @@ -12,3 +14,15 @@ def get_assignable_users(hospital): .distinct() .order_by("first_name", "last_name") ) + + +def build_public_track_url(entity_type, reference): + try: + from django.contrib.sites.shortcuts import get_current_site + site = get_current_site(None) + domain = site.domain + except Exception: + domain = "localhost:8000" + + path = reverse("core:public_track") + return f"https://{domain}{path}?type={entity_type}&reference={reference}" diff --git a/apps/core/views.py b/apps/core/views.py index 8181b92..976886a 100644 --- a/apps/core/views.py +++ b/apps/core/views.py @@ -3,11 +3,14 @@ Core views - Health check and utility views """ from django.contrib.auth.decorators import login_required +from django.contrib import messages from django.db import connection from django.http import JsonResponse from django.shortcuts import redirect, render +from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache -from django.views.decorators.http import require_GET, require_POST +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_GET, require_POST, require_http_methods @never_cache @@ -172,20 +175,20 @@ def public_inquiry_submit(request): Returns JSON response with reference number. """ from apps.complaints.models import Inquiry - from apps.organizations.models import Hospital, Location, MainSection, SubSection + from apps.organizations.models import Hospital, Department, Section, OrgSubSection import uuid - # Get form data name = request.POST.get("name", "").strip() email = request.POST.get("email", "").strip() phone = request.POST.get("phone", "").strip() hospital_id = request.POST.get("hospital") + location_type = request.POST.get("location_type", "").strip() + area_id = request.POST.get("area", "").strip() category = request.POST.get("category", "").strip() subject = request.POST.get("subject", "").strip() message = request.POST.get("message", "").strip() - location_id = request.POST.get("location", "").strip() - main_section_id = request.POST.get("main_section", "").strip() - subsection_id = request.POST.get("subsection", "").strip() + department_id = request.POST.get("department", "").strip() + section_id = request.POST.get("section", "").strip() # Validation errors = [] @@ -206,11 +209,12 @@ def public_inquiry_submit(request): # Validate hospital hospital = Hospital.objects.get(id=hospital_id) - location = Location.objects.filter(id=location_id).first() if location_id else None - main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None - subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None + from apps.organizations.models import Area + + department = Department.objects.filter(id=department_id).first() if department_id else None + section = Section.objects.filter(id=section_id).first() if section_id else None + area = Area.objects.filter(id=area_id).first() if area_id else None - # Create inquiry (using correct field names from model) inquiry = Inquiry.objects.create( hospital=hospital, contact_name=name, @@ -220,18 +224,18 @@ def public_inquiry_submit(request): message=message, category=category, status="open", - location=location, - main_section=main_section, - subsection=subsection, + area=area, + department=department, + section=section, + location_type=location_type if location_type else "", ) - reference_number = f"INQ-{str(inquiry.id)[:8].upper()}" - inquiry.reference_number = reference_number - inquiry.save(update_fields=["reference_number"]) + reference_number = inquiry.reference_number # generated by Inquiry.save() (unified format) try: - from apps.complaints.tasks import analyze_inquiry_with_ai + from apps.complaints.tasks import analyze_inquiry_with_ai, notify_staff_new_item analyze_inquiry_with_ai.delay(str(inquiry.id)) + notify_staff_new_item.delay("inquiry", str(inquiry.id)) except Exception: pass @@ -252,7 +256,7 @@ def public_inquiry_submit(request): from django.conf import settings from django.template.loader import render_to_string - subject = f"New Public Inquiry - {reference_number}" + email_subject = f"New Public Inquiry - {reference_number}" html_message = render_to_string( "emails/public_inquiry_notification.html", { @@ -264,7 +268,7 @@ def public_inquiry_submit(request): ) plain_message = f"Inquiry from {name}\n\nSubject: {subject}\n\nMessage:\n{message}" send_mail( - subject=subject, + subject=email_subject, message=plain_message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[settings.DEFAULT_FROM_EMAIL], @@ -383,7 +387,10 @@ def public_track_api(request): """ API endpoint for unified tracking. - Accepts type (complaint/inquiry/observation) and reference parameters. + Accepts an optional type (complaint/inquiry/observation) and a reference. + If type is omitted, it is auto-detected from the reference prefix + (CMP/INQ/OBS). APR and SGT references are internal-only and not tracked. + Returns standardized JSON with tracking information. """ from django.utils.translation import gettext as _ @@ -391,8 +398,25 @@ def public_track_api(request): track_type = request.GET.get("type", "").strip().lower() reference = request.GET.get("reference", "").strip() - if not track_type or not reference: - return JsonResponse({"found": False, "error": str(_("Type and reference are required."))}, status=400) + if not reference: + return JsonResponse({"found": False, "error": str(_("A reference number is required."))}, status=400) + + # Auto-detect type from prefix when not provided + if not track_type: + upper = reference.upper() + if upper.startswith("CMP-"): + track_type = "complaint" + elif upper.startswith("INQ-"): + track_type = "inquiry" + elif upper.startswith("OBS-"): + track_type = "observation" + elif upper.startswith(("APR-", "SGT-")): + return JsonResponse( + {"found": False, "error": str(_("This reference type is not publicly trackable."))}, + status=400, + ) + else: + return JsonResponse({"found": False, "error": str(_("Unrecognized reference format."))}, status=400) if track_type == "complaint": return _track_complaint(reference) @@ -405,11 +429,11 @@ def public_track_api(request): def _track_complaint(reference): - from apps.complaints.models import Complaint + from apps.complaints.models import Complaint, ComplaintInvolvedDepartment try: complaint = ( - Complaint.objects.select_related("hospital", "department", "location") + Complaint.objects.select_related("hospital", "department", "legacy_location") .prefetch_related("updates") .get(reference_number__iexact=reference) ) @@ -420,19 +444,29 @@ def _track_complaint(reference): ps = complaint.public_status public_updates = list( - complaint.updates.filter(update_type__in=["status_change", "resolution", "communication"]) + complaint.updates.filter(update_type__in=["status_change", "resolution"]) .order_by("-created_at")[:20] ) + _status_map = { + "open": "Received", "in_progress": "In Progress", + "partially_resolved": "In Progress", "contacted": "In Progress", + "contacted_no_response": "In Progress", "resolved": "Resolved", + "closed": "Closed", "cancelled": "Cancelled", + } + timeline = [] for u in public_updates: - icon = "refresh-cw" if u.update_type == "status_change" else ("check-circle-2" if u.update_type == "resolution" else "message-square") - title = "Status Updated" if u.update_type == "status_change" else ("Final Resolution" if u.update_type == "resolution" else "Update Received") + icon = "refresh-cw" if u.update_type == "status_change" else "check-circle-2" + title = "Status Updated" if u.update_type == "status_change" else "Final Resolution" + msg = u.message or "" + for internal, public_label in _status_map.items(): + msg = msg.replace(internal, public_label) timeline.append({ "type": u.update_type, "icon": icon, "title": title, - "comment": u.comments or "", + "comment": msg, "created_at": u.created_at.strftime("%Y-%m-%d %H:%M"), }) @@ -450,6 +484,10 @@ def _track_complaint(reference): else: info_cards.append({"icon": "tag", "label": "Category", "value": complaint.get_category_display() if hasattr(complaint, 'get_category_display') else "General"}) + response = {"has_response": False, "en": "", "ar": ""} + if complaint.status in ("resolved", "closed") and complaint.resolution: + response = {"has_response": True, "en": complaint.resolution, "ar": ""} + return JsonResponse({ "found": True, "type": "complaint", @@ -461,11 +499,14 @@ def _track_complaint(reference): "escalated": bool(complaint.escalated_at), "info_cards": info_cards, "timeline": timeline, + "response": response, + "satisfaction": complaint.satisfaction or "", + "satisfaction_set_at": complaint.satisfaction_set_at.strftime("%Y-%m-%d %H:%M") if complaint.satisfaction_set_at else None, }) def _track_inquiry(reference): - from apps.complaints.models import Inquiry, InquiryUpdate + from apps.complaints.models import Inquiry inquiry = Inquiry.objects.filter(reference_number__iexact=reference).select_related("hospital", "department").first() if not inquiry: @@ -474,25 +515,19 @@ def _track_inquiry(reference): status_map = { "open": {"label": "Received", "progress": 15, "css": "amber"}, "in_progress": {"label": "In Progress", "progress": 50, "css": "blue"}, - "contacted": {"label": "In Progress", "progress": 50, "css": "blue"}, - "contacted_no_response": {"label": "In Progress", "progress": 50, "css": "blue"}, "resolved": {"label": "Resolved", "progress": 100, "css": "emerald"}, "closed": {"label": "Closed", "progress": 100, "css": "slate"}, } sm = status_map.get(inquiry.status, {"label": inquiry.get_status_display(), "progress": 15, "css": "amber"}) - updates = InquiryUpdate.objects.filter(inquiry=inquiry).select_related("created_by").order_by("-created_at")[:20] - timeline = [] - for u in updates: - icon = "refresh-cw" if u.update_type == "status_change" else ("check-circle-2" if u.update_type == "response" else "message-square") - title = "Status Updated" if u.update_type == "status_change" else ("Response Received" if u.update_type == "response" else "Update Received") + if inquiry.status in ("resolved", "closed") and (inquiry.department_response_en or inquiry.department_response_ar): timeline.append({ - "type": u.update_type, - "icon": icon, - "title": title, - "comment": u.message or "", - "created_at": u.created_at.strftime("%Y-%m-%d %H:%M"), + "type": "response", + "icon": "check-circle-2", + "title": "Response Sent", + "comment": "", + "created_at": (inquiry.department_responded_at or inquiry.updated_at).strftime("%Y-%m-%d %H:%M"), }) info_cards = [ @@ -520,6 +555,11 @@ def _track_inquiry(reference): "escalated": bool(inquiry.escalated_at), "info_cards": info_cards, "timeline": timeline, + "response": { + "has_response": bool(inquiry.department_response_en or inquiry.department_response_ar), + "en": inquiry.department_response_en or "", + "ar": inquiry.department_response_ar or "", + }, }) @@ -527,30 +567,28 @@ def _track_observation(reference): from apps.observations.models import Observation try: - observation = Observation.objects.select_related("hospital", "category").get(tracking_code__iexact=reference) + observation = Observation.objects.select_related("hospital", "category").prefetch_related("status_logs", "notes").get(tracking_code__iexact=reference) except Observation.DoesNotExist: return JsonResponse({"found": False, "error": "Observation not found"}) status_progress = { - "new": 15, "triaged": 30, "assigned": 40, "in_progress": 50, - "resolved": 100, "closed": 100, "rejected": 0, "duplicate": 0, + "open": 15, "in_progress": 50, + "resolved": 100, "closed": 100, } status_css = { - "new": "sky", "triaged": "teal", "assigned": "teal", "in_progress": "blue", - "resolved": "emerald", "closed": "slate", "rejected": "rose", "duplicate": "slate", + "open": "amber", "in_progress": "blue", + "resolved": "emerald", "closed": "slate", } timeline = [] - if hasattr(observation, 'public_timeline') and callable(observation.public_timeline): - for item in observation.public_timeline: - icon = "refresh-cw" if item.get("type") == "status_change" else ("message-square" if item.get("type") == "note" else "check-circle-2") - timeline.append({ - "type": item.get("type", "note"), - "icon": icon, - "title": "Status Updated" if item.get("type") == "status_change" else ("Update Received" if item.get("type") == "note" else "Final Resolution"), - "comment": item.get("comment", ""), - "created_at": item.get("created_at", ""), - }) + if observation.status in ("resolved", "closed") and (observation.department_response_en or observation.department_response_ar): + timeline.append({ + "type": "response", + "icon": "check-circle-2", + "title": "Response Sent", + "comment": "", + "created_at": (observation.department_responded_at or observation.updated_at).strftime("%Y-%m-%d %H:%M"), + }) info_cards = [ {"icon": "calendar", "label": "Submitted", "value": observation.created_at.strftime("%b %d, %Y")}, @@ -569,6 +607,11 @@ def _track_observation(reference): "escalated": False, "info_cards": info_cards, "timeline": timeline, + "response": { + "has_response": bool(observation.department_response_en or observation.department_response_ar), + "en": observation.department_response_en or "", + "ar": observation.department_response_ar or "", + }, }) @@ -580,22 +623,20 @@ def public_observation_submit(request): Creates an observation from public submission. Returns JSON response with tracking code. """ - from apps.observations.models import Observation, ObservationAttachment, ObservationCategory - from django.shortcuts import get_object_or_404 + from apps.observations.models import Observation, ObservationAttachment from apps.observations.services import ObservationService - from apps.organizations.models import Hospital, Location, MainSection, SubSection + from apps.organizations.models import Hospital, Department, Section, OrgSubSection import mimetypes - # Get form data hospital_id = request.POST.get("hospital", "").strip() - category_id = request.POST.get("category") severity = request.POST.get("severity", "medium") title = request.POST.get("title", "").strip() description = request.POST.get("description", "").strip() location_text = request.POST.get("location_text", "").strip() - location_id = request.POST.get("location", "").strip() - main_section_id = request.POST.get("main_section", "").strip() - subsection_id = request.POST.get("subsection", "").strip() + location_type = request.POST.get("location_type", "").strip() + area_id = request.POST.get("area", "").strip() + department_id = request.POST.get("department", "").strip() + section_id = request.POST.get("section", "").strip() incident_datetime = request.POST.get("incident_datetime", "") reporter_staff_id = request.POST.get("reporter_staff_id", "").strip() reporter_name = request.POST.get("reporter_name", "").strip() @@ -618,13 +659,11 @@ def public_observation_submit(request): try: hospital = Hospital.objects.get(id=hospital_id) - category = None - if category_id: - category = get_object_or_404(ObservationCategory, id=category_id) - location = Location.objects.filter(id=location_id).first() if location_id else None - main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None - subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None + department = Department.objects.filter(id=department_id).first() if department_id else None + section = Section.objects.filter(id=section_id).first() if section_id else None + from apps.organizations.models import Area + area = Area.objects.filter(id=area_id).first() if area_id else None # Get client info def get_client_ip(req): @@ -645,13 +684,14 @@ def public_observation_submit(request): observation = ObservationService.create_observation( description=description, severity=severity, - category=category, + category=None, title=title, hospital=hospital, location_text=location_text, - location=location, - main_section=main_section, - subsection=subsection, + location_type=location_type, + assigned_department=department, + section=section, + area=area, incident_datetime=incident_datetime if incident_datetime else None, reporter_staff_id=reporter_staff_id, reporter_name=reporter_name, @@ -665,6 +705,71 @@ def public_observation_submit(request): return JsonResponse( {"success": True, "tracking_code": observation.tracking_code, "observation_id": str(observation.id)} ) - except Exception as e: return JsonResponse({"success": False, "errors": [str(e)]}, status=500) + + +@login_required +@require_http_methods(["POST"]) +@login_required +@require_http_methods(["POST"]) +def add_note(request): + from django.contrib.contenttypes.models import ContentType + from apps.core.models import Note + + content_type_id = request.POST.get("content_type_id") + object_id = request.POST.get("object_id") + note_text = request.POST.get("note", "").strip() + + if not note_text: + messages.error(request, _("Note cannot be empty.")) + return redirect(request.META.get("HTTP_REFERER", "/")) + + try: + ct = ContentType.objects.get(pk=content_type_id) + obj = ct.get_object_for_this_type(pk=object_id) + except Exception: + messages.error(request, _("Invalid object reference.")) + return redirect(request.META.get("HTTP_REFERER", "/")) + + Note.objects.create( + content_type=ct, + object_id=object_id, + note=note_text, + created_by=request.user, + is_internal=True, + ) + messages.success(request, _("Note added successfully.")) + return redirect(request.META.get("HTTP_REFERER", "/")) + + +@require_POST +@csrf_exempt +def public_set_satisfaction(request): + """Public endpoint to set patient satisfaction for a complaint (no auth required).""" + from django.utils import timezone + from apps.complaints.models import Complaint + + reference = request.POST.get("reference", "").strip() + satisfaction = request.POST.get("satisfaction", "").strip() + + if not reference or not satisfaction: + return JsonResponse({"success": False, "error": "Reference and satisfaction are required."}, status=400) + + valid_choices = ["satisfied", "neutral", "dissatisfied"] + if satisfaction not in valid_choices: + return JsonResponse({"success": False, "error": "Invalid satisfaction value."}, status=400) + + try: + complaint = Complaint.objects.get(reference_number__iexact=reference) + except Complaint.DoesNotExist: + return JsonResponse({"success": False, "error": "Complaint not found."}, status=404) + + if complaint.status not in ("resolved", "closed") or not complaint.resolution: + return JsonResponse({"success": False, "error": "Satisfaction can only be set for resolved complaints."}, status=400) + + complaint.satisfaction = satisfaction + complaint.satisfaction_set_at = timezone.now() + complaint.save(update_fields=["satisfaction", "satisfaction_set_at", "updated_at"]) + + return JsonResponse({"success": True, "satisfaction": complaint.satisfaction}) diff --git a/apps/dashboard/services/complaint_monthly_export.py b/apps/dashboard/services/complaint_monthly_export.py index 1abdde0..e6ccc1b 100644 --- a/apps/dashboard/services/complaint_monthly_export.py +++ b/apps/dashboard/services/complaint_monthly_export.py @@ -201,7 +201,7 @@ def _write_data_rows(ws, queryset): c.reference_number or "", c.file_number or "", c.source.name_en if c.source else (c.complaint_source_type or ""), - c.location.name if c.location else "", + c.department.name_en if c.department else "", c.domain.name_en if c.domain else "", c.category.name_en if c.category else "", c.created_at, diff --git a/apps/dashboard/services/complaint_monthly_service.py b/apps/dashboard/services/complaint_monthly_service.py index c828878..b5d9478 100644 --- a/apps/dashboard/services/complaint_monthly_service.py +++ b/apps/dashboard/services/complaint_monthly_service.py @@ -51,7 +51,7 @@ class ComplaintMonthlyService: ).select_related( "patient", "department", "assigned_to", "created_by", "domain", "category", "subcategory_obj", - "location", "main_section", "source", + "source", ).prefetch_related("involved_departments__department") def get_summary(self): diff --git a/apps/dashboard/services/complaint_quarterly_export.py b/apps/dashboard/services/complaint_quarterly_export.py index d3ccda1..21eea96 100644 --- a/apps/dashboard/services/complaint_quarterly_export.py +++ b/apps/dashboard/services/complaint_quarterly_export.py @@ -280,12 +280,9 @@ def _write_source_table_sheet(wb, service): int_total = sum(source[m]["internal"] for m in active_months) row = 2 - ins_total = sum(source[m]["insurance"] for m in active_months) - _write_toprow(ws, row, - round(ext_total / total_all, 3) if total_all else 0, ext_total, "External", - "Insurance company", ins_total, - lambda m: source[m]["insurance"], - lambda m: round(source[m]["insurance"] / source[m]["total"], 3) if source[m]["total"] else 0) + _style_data(ws, row, 1, round(ext_total / total_all, 3) if total_all else 0, PCT_FMT) + _style_data(ws, row, 2, ext_total) + _style_data(ws, row, 3, "External") row = 3 moh_total = sum(source[m]["moh"] for m in active_months) @@ -335,7 +332,7 @@ def _write_source_table_sheet(wb, service): _write_total_row(ws, row, total_all, {m: source[m]["total"] for m in active_months}) row += 2 - for label, key in [("Medical", "medical"), ("Admin", "admin"), ("Nursing", "nursing"), ("Support Services", "support")]: + for label, key in [("Medical", "medical"), ("Administrative", "administrative"), ("Nursing", "nursing"), ("Support Services", "support")]: total_v = sum(dept_type[m][key] for m in active_months) _style_data(ws, row, 1, round(total_v / total_all, 3) if total_all else 0, PCT_FMT) _style_data(ws, row, 2, total_v) @@ -353,7 +350,7 @@ def _write_source_table_sheet(wb, service): _write_total_row(ws, row, total_all, {m: source[m]["total"] for m in active_months}) row += 2 - summary_headers = ["Month", "MOH Complaints", "CHI", "Insurance Company", "Internal", "Total Complaints", "MOH Percentage", "CCHI Percentage"] + summary_headers = ["Month", "MOH Complaints", "CHI", "Internal", "Total Complaints", "MOH Percentage", "CCHI Percentage"] for i, h in enumerate(summary_headers): _style_header(ws, row, 5 + i, h) @@ -362,19 +359,15 @@ def _write_source_table_sheet(wb, service): _style_data(ws, row, 5, ms["month"]) _style_data(ws, row, 6, ms["moh"]) _style_data(ws, row, 7, ms["chi"]) - _style_data(ws, row, 8, ms["insurance"]) - _style_data(ws, row, 9, ms["internal"]) - _style_data(ws, row, 10, ms["total"]) - _style_data(ws, row, 11, ms["moh_pct"], PCT_FMT) - _style_data(ws, row, 12, ms["chi_pct"], PCT_FMT) + _style_data(ws, row, 8, ms["internal"]) + _style_data(ws, row, 9, ms["total"]) + _style_data(ws, row, 10, ms["moh_pct"], PCT_FMT) + _style_data(ws, row, 11, ms["chi_pct"], PCT_FMT) row += 2 source_total_items = [ ("Internal Complaints", source_totals["internal"]["count"], "% Internal", source_totals["internal"]["pct"]), ("External Complaints", source_totals["external"]["count"], "% External", source_totals["external"]["pct"]), - ("MOH", source_totals["moh"]["count"], "% MOH", source_totals["moh"]["pct"]), - ("CHI", source_totals["chi"]["count"], "% CHI", source_totals["chi"]["pct"]), - ("Insurance Company", source_totals["insurance"]["count"], "% Insurance Comp.", source_totals["insurance"]["pct"]), ] for label, count, pct_label, pct in source_total_items: _style_bold(ws, row, 6, label) @@ -398,13 +391,13 @@ def _write_source_table_sheet(wb, service): _style_data(ws, row, 5, months[i]) _style_data(ws, row, 6, d[f"{area}_complaints"]) _style_data(ws, row, 7, d[f"{area}_patients"]) - _style_data(ws, row, 8, d[f"{area}_ratio"], PCT_FMT) + _style_data(ws, row, 8, d[f"{area}_ratio"] if d[f"{area}_patients"] > 0 else "-", PCT_FMT) row += 1 t = location_ratios["totals"][area] _style_bold(ws, row, 5, "TOTAL") _style_bold(ws, row, 6, t["complaints"]) _style_bold(ws, row, 7, t["patients"]) - _style_bold(ws, row, 8, t["ratio"], PCT_FMT) + _style_bold(ws, row, 8, t["ratio"] if t["patients"] > 0 else "-", PCT_FMT) row += 1 row += 1 @@ -446,7 +439,7 @@ def _write_source_table_sheet(wb, service): row += 1 row += 1 - dept_headers = ["Month", "Medical", "Admin", "Nursing", "Support Services", "Total Complaints"] + dept_headers = ["Month", "Medical", "Administrative", "Nursing", "Support Services", "Total Complaints"] for i, h in enumerate(dept_headers): _style_header(ws, row, 5 + i, h) @@ -454,7 +447,7 @@ def _write_source_table_sheet(wb, service): row += 1 _style_data(ws, row, 5, mr["month"]) _style_data(ws, row, 6, mr["medical"]) - _style_data(ws, row, 7, mr["admin"]) + _style_data(ws, row, 7, mr["administrative"]) _style_data(ws, row, 8, mr["nursing"]) _style_data(ws, row, 9, mr["support"]) _style_data(ws, row, 10, mr["total"]) @@ -462,7 +455,7 @@ def _write_source_table_sheet(wb, service): row += 1 _style_bold(ws, row, 5, "TOTAL") _style_bold(ws, row, 6, dept_type_monthly["totals"]["medical"]) - _style_bold(ws, row, 7, dept_type_monthly["totals"]["admin"]) + _style_bold(ws, row, 7, dept_type_monthly["totals"]["administrative"]) _style_bold(ws, row, 8, dept_type_monthly["totals"]["nursing"]) _style_bold(ws, row, 9, dept_type_monthly["totals"]["support"]) _style_bold(ws, row, 10, dept_type_monthly["totals"]["total"]) @@ -470,25 +463,25 @@ def _write_source_table_sheet(wb, service): row += 1 _style_bold(ws, row, 5, "% From total") _style_bold(ws, row, 6, dept_type_monthly["percentages"]["medical"], PCT_FMT) - _style_bold(ws, row, 7, dept_type_monthly["percentages"]["admin"], PCT_FMT) + _style_bold(ws, row, 7, dept_type_monthly["percentages"]["administrative"], PCT_FMT) _style_bold(ws, row, 8, dept_type_monthly["percentages"]["nursing"], PCT_FMT) _style_bold(ws, row, 9, dept_type_monthly["percentages"]["support"], PCT_FMT) _style_bold(ws, row, 10, dept_type_monthly["percentages"]["total"], PCT_FMT) row += 2 - for i, h in enumerate(["Medical", "Admin", "Nursing", "Support Services", "Total Complaints"]): + for i, h in enumerate(["Medical", "Administrative", "Nursing", "Support Services", "Total Complaints"]): _style_header(ws, row, 5 + i, h) row += 1 _style_data(ws, row, 5, "Persentage") _style_data(ws, row, 6, dept_type_monthly["percentages"]["medical"], PCT_FMT) - _style_data(ws, row, 7, dept_type_monthly["percentages"]["admin"], PCT_FMT) + _style_data(ws, row, 7, dept_type_monthly["percentages"]["administrative"], PCT_FMT) _style_data(ws, row, 8, dept_type_monthly["percentages"]["nursing"], PCT_FMT) _style_data(ws, row, 9, dept_type_monthly["percentages"]["support"], PCT_FMT) _style_data(ws, row, 10, dept_type_monthly["percentages"]["total"], PCT_FMT) row += 1 _style_bold(ws, row, 5, "TOTAL") _style_bold(ws, row, 6, dept_type_monthly["totals"]["medical"]) - _style_bold(ws, row, 7, dept_type_monthly["totals"]["admin"]) + _style_bold(ws, row, 7, dept_type_monthly["totals"]["administrative"]) _style_bold(ws, row, 8, dept_type_monthly["totals"]["nursing"]) _style_bold(ws, row, 9, dept_type_monthly["totals"]["support"]) _style_bold(ws, row, 10, dept_type_monthly["totals"]["total"]) @@ -507,9 +500,9 @@ def _write_escalated_sheet(wb, service): categories = [ ("Medical", "medical"), - ("Non-Medical", "non_medical"), + ("Administrative", "administrative"), ("Nursing", "nursing"), - ("Support Services", "support"), + ("Support Services", "support_services"), ] cat_headers = [] @@ -588,16 +581,16 @@ def _write_per_department_sheet(wb, service): cat_configs = [ ("Medical", "medical", 2), - ("Non-Medical", "non_medical", 12), + ("Administrative", "administrative", 12), ("Nursing", "nursing", 20), - ("Support Services", "support", 28), + ("Support Services", "support_services", 28), ] for cat_label, cat_key, col_start in cat_configs: _style_header(ws, 1, col_start, cat_label) - ws.merge_cells(start_row=1, start_column=col_start, end_row=1, end_column=col_start + 7) + ws.merge_cells(start_row=1, start_column=col_start, end_row=1, end_column=col_start + 6) - sub_headers = ["Sub-dept", "MOH", "CHI", "Insurance", "Internal", "Total", "Escalated", "Response Rate (Days)"] + sub_headers = ["Sub-dept", "MOH", "CHI", "Internal", "Total", "Escalated", "Response Rate (Days)"] for i, h in enumerate(sub_headers): _style_header(ws, 2, col_start + i, h) @@ -607,20 +600,18 @@ def _write_per_department_sheet(wb, service): _style_data(ws, row, col_start, d["name"]) _style_data(ws, row, col_start + 1, d["moh"]) _style_data(ws, row, col_start + 2, d["chi"]) - _style_data(ws, row, col_start + 3, d["insurance"]) - _style_data(ws, row, col_start + 4, d["internal"]) - _style_data(ws, row, col_start + 5, d["total"]) - _style_data(ws, row, col_start + 6, d["escalated"]) - _style_data(ws, row, col_start + 7, d["avg_response_days"], NUM_FMT) + _style_data(ws, row, col_start + 3, d["internal"]) + _style_data(ws, row, col_start + 4, d["total"]) + _style_data(ws, row, col_start + 5, d["escalated"]) + _style_data(ws, row, col_start + 6, d["avg_response_days"], NUM_FMT) total_row = 3 + len(depts) _style_bold(ws, total_row, col_start, "Total") _style_bold(ws, total_row, col_start + 1, sum(d["moh"] for d in depts)) _style_bold(ws, total_row, col_start + 2, sum(d["chi"] for d in depts)) - _style_bold(ws, total_row, col_start + 3, sum(d["insurance"] for d in depts)) - _style_bold(ws, total_row, col_start + 4, sum(d["internal"] for d in depts)) - _style_bold(ws, total_row, col_start + 5, sum(d["total"] for d in depts)) - _style_bold(ws, total_row, col_start + 6, sum(d["escalated"] for d in depts)) + _style_bold(ws, total_row, col_start + 3, sum(d["internal"] for d in depts)) + _style_bold(ws, total_row, col_start + 4, sum(d["total"] for d in depts)) + _style_bold(ws, total_row, col_start + 5, sum(d["escalated"] for d in depts)) summary_row = 3 + max(len(categories.get(ck, [])) for _, ck, _ in cat_configs) + 3 @@ -628,7 +619,6 @@ def _write_per_department_sheet(wb, service): ("MOH", source_totals["moh"]), ("CHI", source_totals["chi"]), ("Internal", source_totals["internal"]), - ("Insurance Co.", source_totals["insurance"]), ("Total", source_totals["total"]), ] for label, val in source_items: @@ -704,8 +694,8 @@ def _write_per_dept_response_rate_sheets(wb, service): sheet_configs = [ ("8.1 Response Rate Medical", ["medical"]), - ("8.2 Response Rate Non-Medical", ["non_medical", "admin"]), - ("8.3 RR Nursing&Support", ["nursing", "support"]), + ("8.2 Response Rate Administrative", ["administrative", "admin"]), + ("8.3 RR Nursing&Support", ["nursing", "support_services"]), ] for sheet_title, domain_types in sheet_configs: diff --git a/apps/dashboard/services/complaint_quarterly_service.py b/apps/dashboard/services/complaint_quarterly_service.py index 6d87046..f830cbb 100644 --- a/apps/dashboard/services/complaint_quarterly_service.py +++ b/apps/dashboard/services/complaint_quarterly_service.py @@ -49,7 +49,7 @@ class ComplaintQuarterlyService: hospital_id=self.hospital_id, created_at__range=(dt_start, dt_end), ).select_related( - "department", "domain", "category", "location", + "department", "source", "assigned_to", "created_by", ) @@ -209,9 +209,20 @@ class ComplaintQuarterlyService: ).count() relatives = qs.filter( Q(source__name_en__icontains="relative") + | Q(source__name_en__icontains="family") | Q(relation_to_patient="relative") ).count() + by_source = list( + qs.filter(source__isnull=False) + .values("source__name_en") + .annotate(count=Count("id")) + .order_by("-count") + ) + no_source = qs.filter(source__isnull=True).count() + if no_source > 0: + by_source.append({"source__name_en": "No Source", "count": no_source}) + result[m] = { "total": total, "external": ext, @@ -221,6 +232,7 @@ class ComplaintQuarterlyService: "insurance": insurance, "patients": patients, "relatives": relatives, + "by_source": by_source, } return result @@ -232,15 +244,12 @@ class ComplaintQuarterlyService: for m in self.active_months: qs = self._month_qs(m) total = qs.count() - ip = qs.filter( - Q(location__name_en__icontains="inpatient") | Q(location__name_en__icontains="in-patient") - ).count() - er = qs.filter( - Q(location__name_en__icontains="emergency") | Q(location__name_en__icontains="er") - ).count() - op = total - ip - er + ip = qs.filter(department__location_type="IP").count() + er = qs.filter(department__location_type="ER").count() + op = qs.filter(department__location_type="OP").count() + general = total - ip - er - op - result[m] = {"total": total, "ip": ip, "op": op, "er": er} + result[m] = {"total": total, "ip": ip, "op": op, "er": er, "general": general} return result def get_dept_type_breakdown(self): @@ -251,17 +260,19 @@ class ComplaintQuarterlyService: for m in self.active_months: qs = self._month_qs(m) total = qs.count() - medical = qs.filter(domain__domain_type="medical").count() - admin = qs.filter(domain__domain_type="admin").count() - nursing = qs.filter(domain__domain_type="nursing").count() - support = total - medical - admin - nursing + medical = qs.filter(department__category="medical").count() + administrative = qs.filter(department__category="administrative").count() + nursing = qs.filter(department__category="nursing").count() + support = qs.filter(department__category="support_services").count() + other = total - medical - administrative - nursing - support result[m] = { "total": total, "medical": medical, - "admin": admin, + "administrative": administrative, "nursing": nursing, "support": support, + "other": other, } return result @@ -275,14 +286,22 @@ class ComplaintQuarterlyService: for c in qs.iterator(chunk_size=2000): dept_name = c.department.name if c.department else "Unknown" domain_type = "medical" - if c.domain: - dt = c.domain.domain_type or "" - if dt == "admin" or dt == "non_medical": - domain_type = "non_medical" + if c.department and c.department.category: + dt = c.department.category + if dt in ("admin", "non_medical", "administrative"): + domain_type = "administrative" elif dt == "nursing": domain_type = "nursing" elif dt in ("support", "support_services"): - domain_type = "support" + domain_type = "support_services" + elif c.domain: + dt = c.domain.domain_type or "" + if dt in ("admin", "non_medical", "MANAGEMENT"): + domain_type = "administrative" + elif dt == "nursing": + domain_type = "nursing" + elif dt in ("support", "support_services"): + domain_type = "support_services" by_category[domain_type][dept_name] += 1 if c.complaint_source_type == "internal": @@ -434,8 +453,8 @@ class ComplaintQuarterlyService: domain_type = c.department.category elif c.domain: dt = c.domain.domain_type or "" - if dt == "admin" or dt == "non_medical": - domain_type = "non_medical" + if dt in ("admin", "non_medical", "MANAGEMENT"): + domain_type = "administrative" elif dt == "nursing": domain_type = "nursing" elif dt in ("support", "support_services"): @@ -495,7 +514,7 @@ class ComplaintQuarterlyService: } avg_response = {} - for dtype in ["medical", "non_medical", "nursing", "support_services"]: + for dtype in ["medical", "administrative", "nursing", "support_services"]: depts = categories.get(dtype, []) total_hours = sum(d["avg_response_days"] * 24 * (d["total"] - d.get("_excluded", 0)) for d in depts) total_count = sum(d["total"] for d in depts) @@ -526,8 +545,8 @@ class ComplaintQuarterlyService: domain_type = c.department.category elif c.domain: dt = c.domain.domain_type or "" - if dt == "admin" or dt == "non_medical": - domain_type = "non_medical" + if dt in ("admin", "non_medical", "MANAGEMENT"): + domain_type = "administrative" elif dt == "nursing": domain_type = "nursing" elif dt in ("support", "support_services"): @@ -578,8 +597,8 @@ class ComplaintQuarterlyService: source_label = "Patient's relatives" location_label = "" - if c.location and c.location.name_en: - location_label = c.location.name_en + if c.department and c.department.location_type: + location_label = c.department.location_type dept_label = c.department.name if c.department else "" domain_label = c.domain.name_en if c.domain else "" @@ -768,16 +787,18 @@ class ComplaintQuarterlyService: result.append({ "month": calendar.month_abbr[m], "medical": d["medical"], - "admin": d["admin"], + "administrative": d["administrative"], "nursing": d["nursing"], "support": d["support"], + "other": d["other"], "total": d["total"], }) totals = { "medical": sum(r["medical"] for r in result), - "admin": sum(r["admin"] for r in result), + "administrative": sum(r["administrative"] for r in result), "nursing": sum(r["nursing"] for r in result), "support": sum(r["support"] for r in result), + "other": sum(r["other"] for r in result), "total": sum(r["total"] for r in result), } total_all = totals["total"] @@ -810,6 +831,7 @@ class ComplaintQuarterlyService: "source_distribution": { "external": sum(s["external"] for s in source.values()), "internal": sum(s["internal"] for s in source.values()), + "by_source": source[self.active_months[0]]["by_source"] if self.active_months else [], }, "location_distribution": { "IP": sum(l["ip"] for l in location.values()), @@ -818,9 +840,10 @@ class ComplaintQuarterlyService: }, "dept_type_distribution": { "Medical": sum(d["medical"] for d in dept_type.values()), - "Admin": sum(d["admin"] for d in dept_type.values()), + "Administrative": sum(d["administrative"] for d in dept_type.values()), "Nursing": sum(d["nursing"] for d in dept_type.values()), - "Support": sum(d["support"] for d in dept_type.values()), + "Support Services": sum(d["support"] for d in dept_type.values()), + "Other": sum(d["other"] for d in dept_type.values()), }, "satisfaction": { "months": self.active_month_labels, diff --git a/apps/dashboard/views.py b/apps/dashboard/views.py index e582ec3..0ea3c8f 100644 --- a/apps/dashboard/views.py +++ b/apps/dashboard/views.py @@ -112,8 +112,8 @@ class CommandCenterView(LoginRequiredMixin, TemplateView): Q(department=user.department) | Q(outgoing_department=user.department) ) actions_qs = PXAction.objects.filter(department=user.department) - surveys_qs = SurveyInstance.objects.none() - calls_qs = CallCenterInteraction.objects.none() + surveys_qs = SurveyInstance.objects.filter(journey_instance__department=user.department) + calls_qs = CallCenterInteraction.objects.filter(department=user.department) observations_qs = Observation.objects.filter(assigned_department=user.department) elif user.is_director(): directed_depts = user.get_directed_departments() @@ -131,6 +131,21 @@ class CommandCenterView(LoginRequiredMixin, TemplateView): surveys_qs = SurveyInstance.objects.none() calls_qs = CallCenterInteraction.objects.none() observations_qs = Observation.objects.none() + elif user.is_executive(): + if user.hospital: + complaints_qs = Complaint.objects.filter(hospital=user.hospital) + inquiries_qs = Inquiry.objects.filter(hospital=user.hospital) + actions_qs = PXAction.objects.filter(hospital=user.hospital) + surveys_qs = SurveyInstance.objects.filter(journey_instance__department__hospital=user.hospital) + calls_qs = CallCenterInteraction.objects.filter(department__hospital=user.hospital) + observations_qs = Observation.objects.filter(hospital=user.hospital) + else: + complaints_qs = Complaint.objects.none() + inquiries_qs = Inquiry.objects.none() + actions_qs = PXAction.objects.none() + surveys_qs = SurveyInstance.objects.none() + calls_qs = CallCenterInteraction.objects.none() + observations_qs = Observation.objects.none() else: complaints_qs = Complaint.objects.none() inquiries_qs = Inquiry.objects.none() @@ -1379,6 +1394,14 @@ def command_center_api(request): actions_qs = PXAction.objects.filter(department=user.department) surveys_qs = SurveyInstance.objects.filter(journey_instance__department=user.department) observations_qs = Observation.objects.filter(assigned_department=user.department) + elif user.is_champion() and user.department: + complaints_qs = Complaint.objects.filter(department=user.department) + inquiries_qs = Inquiry.objects.filter( + Q(department=user.department) | Q(outgoing_department=user.department) + ) + actions_qs = PXAction.objects.filter(department=user.department) + surveys_qs = SurveyInstance.objects.filter(journey_instance__department=user.department) + observations_qs = Observation.objects.filter(assigned_department=user.department) elif user.is_director(): directed_depts = user.get_directed_departments() if directed_depts.exists(): @@ -1393,6 +1416,19 @@ def command_center_api(request): actions_qs = PXAction.objects.none() surveys_qs = SurveyInstance.objects.none() observations_qs = Observation.objects.none() + elif user.is_executive(): + if user.hospital: + complaints_qs = Complaint.objects.filter(hospital=user.hospital) + inquiries_qs = Inquiry.objects.filter(hospital=user.hospital) + actions_qs = PXAction.objects.filter(hospital=user.hospital) + surveys_qs = SurveyInstance.objects.filter(survey_template__hospital=user.hospital) + observations_qs = Observation.objects.filter(hospital=user.hospital) + else: + complaints_qs = Complaint.objects.none() + inquiries_qs = Inquiry.objects.none() + actions_qs = PXAction.objects.none() + surveys_qs = SurveyInstance.objects.none() + observations_qs = Observation.objects.none() else: complaints_qs = Complaint.objects.none() inquiries_qs = Inquiry.objects.none() @@ -2516,7 +2552,7 @@ def complaint_quarterly_report(request): context["kpi_blocks"] = _build_kpi_blocks(kpi_data, satisfaction_data, moh_kpi_data, active_months) - context["source_sub_external"] = _build_source_subrows(source_breakdown, ["moh", "chi", "insurance"], ["MOH", "CHI", "Insurance Company"], active_months) + context["source_sub_external"] = _build_source_subrows(source_breakdown, ["moh", "chi"], ["MOH", "CHI"], active_months) context["source_sub_internal"] = _build_source_subrows(source_breakdown, ["patients", "relatives"], ["Patients", "Patient's relatives"], active_months) context["source_internal_monthly"] = [source_breakdown[m]["internal"] for m in active_months] context["source_total_monthly"] = [source_breakdown[m]["total"] for m in active_months] @@ -2525,12 +2561,6 @@ def complaint_quarterly_report(request): {"label": "Internal Complaints", "count": source_totals["internal"]["count"], "pct_label": "% Internal", "pct": source_totals["internal"]["pct"]}, {"label": "External Complaints", "count": source_totals["external"]["count"], "pct_label": "% External", "pct": source_totals["external"]["pct"]}, ] - if source_totals["moh"]["count"] > 0: - context["source_total_rows"].append({"label": "MOH", "count": source_totals["moh"]["count"], "pct_label": "% MOH", "pct": source_totals["moh"]["pct"]}) - if source_totals["chi"]["count"] > 0: - context["source_total_rows"].append({"label": "CHI", "count": source_totals["chi"]["count"], "pct_label": "% CHI", "pct": source_totals["chi"]["pct"]}) - if source_totals["insurance"]["count"] > 0: - context["source_total_rows"].append({"label": "Insurance Company", "count": source_totals["insurance"]["count"], "pct_label": "% Insurance Comp.", "pct": source_totals["insurance"]["pct"]}) total_all = sum(location_breakdown[m]["total"] for m in active_months) context["location_rows"] = [] @@ -2546,7 +2576,7 @@ def complaint_quarterly_report(request): }) context["dept_type_rows"] = [] - for label, key in [("Medical", "medical"), ("Admin", "admin"), ("Nursing", "nursing"), ("Support Services", "support")]: + for label, key in [("Medical", "medical"), ("Administrative", "administrative"), ("Nursing", "nursing"), ("Support Services", "support")]: t = sum(dept_type_breakdown[m][key] for m in active_months) context["dept_type_rows"].append({ "label": label, "total": t, "pct": round(t / total_all, 3) if total_all else 0, @@ -2560,22 +2590,24 @@ def complaint_quarterly_report(request): context["dept_monthly_rows"] = [] for mr in dept_type_monthly["months"]: context["dept_monthly_rows"].append({ - "month": mr["month"], "medical": mr["medical"], "admin": mr["admin"], + "month": mr["month"], "medical": mr["medical"], "administrative": mr["administrative"], "nursing": mr["nursing"], "support": mr["support"], "total": mr["total"], "is_total": False, "is_pct": False, }) context["dept_monthly_rows"].append({ "month": "TOTAL", - "medical": dept_type_monthly["totals"]["medical"], "admin": dept_type_monthly["totals"]["admin"], + "medical": dept_type_monthly["totals"]["medical"], "administrative": dept_type_monthly["totals"]["administrative"], "nursing": dept_type_monthly["totals"]["nursing"], "support": dept_type_monthly["totals"]["support"], "total": dept_type_monthly["totals"]["total"], "is_total": True, "is_pct": False, }) context["dept_monthly_rows"].append({ - "month": "% From total", - "medical": dept_type_monthly["percentages"]["medical"], "admin": dept_type_monthly["percentages"]["admin"], - "nursing": dept_type_monthly["percentages"]["nursing"], "support": dept_type_monthly["percentages"]["support"], - "total": dept_type_monthly["percentages"]["total"], + "month": "% From Total", + "medical": round(dept_type_monthly["percentages"]["medical"] * 100, 1), + "administrative": round(dept_type_monthly["percentages"]["administrative"] * 100, 1), + "nursing": round(dept_type_monthly["percentages"]["nursing"] * 100, 1), + "support": round(dept_type_monthly["percentages"]["support"] * 100, 1), + "total": round(dept_type_monthly["percentages"]["total"] * 100, 1), "is_total": False, "is_pct": True, }) @@ -2587,7 +2619,7 @@ def complaint_quarterly_report(request): per_dept = service.get_per_department_breakdown() cat_configs = [ ("Medical", "medical"), - ("Non-Medical", "non_medical"), + ("Administrative", "administrative"), ("Nursing", "nursing"), ("Support Services", "support_services"), ] @@ -2628,7 +2660,6 @@ def complaint_quarterly_report(request): totals = { "moh": sum(d["moh"] for d in depts), "chi": sum(d["chi"] for d in depts), - "insurance": sum(d["insurance"] for d in depts), "internal": sum(d["internal"] for d in depts), "total": sum(d["total"] for d in depts), "escalated": sum(d["escalated"] for d in depts), @@ -2641,7 +2672,6 @@ def complaint_quarterly_report(request): {"label": "MOH", "count": st["moh"], "is_total": False}, {"label": "CHI", "count": st["chi"], "is_total": False}, {"label": "Internal", "count": st["internal"], "is_total": False}, - {"label": "Insurance Co.", "count": st["insurance"], "is_total": False}, {"label": "Total", "count": st["total"], "is_total": True}, ] diff --git a/apps/feedback/admin.py b/apps/feedback/admin.py index 93546ea..6ec42c8 100644 --- a/apps/feedback/admin.py +++ b/apps/feedback/admin.py @@ -65,7 +65,22 @@ class FeedbackAdmin(admin.ModelAdmin): {"fields": ("id", "feedback_type", "title", "message", "category", "subcategory", "rating", "priority")}, ), ("Patient/Contact", {"fields": ("patient", "is_anonymous", "contact_name", "contact_email", "contact_phone")}), - ("Organization", {"fields": ("hospital", "department", "physician", "encounter_id")}), + ( + "Organization", + { + "fields": ( + "hospital", + "department", + "legacy_location", + "legacy_main_section", + "legacy_subsection", + "section", + + "staff", + "encounter_id", + ) + }, + ), ( "Status & Workflow", { diff --git a/apps/feedback/forms.py b/apps/feedback/forms.py index cdfe954..adb24a5 100644 --- a/apps/feedback/forms.py +++ b/apps/feedback/forms.py @@ -314,12 +314,12 @@ class PublicSuggestionForm(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - from apps.organizations.models import Hospital, Location, MainSection, SubSection + from apps.organizations.models import Hospital, LegacyLocation, LegacyMainSection, LegacySubSection self.fields["hospital"].queryset = Hospital.objects.filter(status="active").order_by("name") - self.fields["location"].queryset = Location.active_locations() - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() + self.fields["location"].queryset = LegacyLocation.active_locations() + self.fields["main_section"].queryset = LegacyMainSection.objects.none() + self.fields["subsection"].queryset = LegacySubSection.objects.none() location_id = None if "location" in self.initial: @@ -329,9 +329,9 @@ class PublicSuggestionForm(forms.Form): if location_id: available_sections = ( - SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() + LegacySubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() ) - self.fields["main_section"].queryset = MainSection.objects.filter(id__in=available_sections).order_by("name_en") + self.fields["main_section"].queryset = LegacyMainSection.objects.filter(id__in=available_sections).order_by("name_en") section_id = None if "main_section" in self.initial: @@ -340,6 +340,6 @@ class PublicSuggestionForm(forms.Form): section_id = self.data["main_section"] if section_id: - self.fields["subsection"].queryset = SubSection.objects.filter( + self.fields["subsection"].queryset = LegacySubSection.objects.filter( location_id=location_id, main_section_id=section_id ).order_by("name_en") diff --git a/apps/feedback/migrations/0001_initial.py b/apps/feedback/migrations/0001_initial.py index 0324a65..f1dd9dc 100644 --- a/apps/feedback/migrations/0001_initial.py +++ b/apps/feedback/migrations/0001_initial.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -168,8 +168,8 @@ class Migration(migrations.Migration): ('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_%(class)s_set', to=settings.AUTH_USER_MODEL)), ('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.department')), ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='organizations.hospital')), - ('location', models.ForeignKey(blank=True, help_text='Location context', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.location')), - ('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.mainsection')), + ('location', models.ForeignKey(blank=True, help_text='Location context', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacylocation')), + ('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacymainsection')), ('patient', models.ForeignKey(blank=True, help_text='Patient who provided feedback (optional for anonymous feedback)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='organizations.patient')), ], options={ diff --git a/apps/feedback/migrations/0003_initial.py b/apps/feedback/migrations/0003_initial.py index 4b1a00d..7bb4774 100644 --- a/apps/feedback/migrations/0003_initial.py +++ b/apps/feedback/migrations/0003_initial.py @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ ('feedback', '0002_initial'), - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), ('px_sources', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -30,7 +30,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='feedback', name='subsection', - field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.subsection'), + field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacysubsection'), ), migrations.AddField( model_name='feedbackattachment', diff --git a/apps/feedback/migrations/0004_remove_feedback_location_and_more.py b/apps/feedback/migrations/0004_remove_feedback_location_and_more.py new file mode 100644 index 0000000..7611342 --- /dev/null +++ b/apps/feedback/migrations/0004_remove_feedback_location_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0003_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='feedback', + old_name='location', + new_name='legacy_location', + ), + migrations.RenameField( + model_name='feedback', + old_name='main_section', + new_name='legacy_main_section', + ), + migrations.RenameField( + model_name='feedback', + old_name='subsection', + new_name='legacy_subsection', + ), + ] diff --git a/apps/feedback/migrations/0005_feedback_legacy_location_and_more.py b/apps/feedback/migrations/0005_feedback_legacy_location_and_more.py new file mode 100644 index 0000000..ede5ca0 --- /dev/null +++ b/apps/feedback/migrations/0005_feedback_legacy_location_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0004_remove_feedback_location_and_more'), + ('organizations', '0008_rename_orgsubsection_to_section_add_champion'), + ] + + operations = [ + migrations.AddField( + model_name='feedback', + name='section', + field=models.ForeignKey(blank=True, help_text='Section within department', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks_new', to='organizations.Section'), + ), + + migrations.AlterField( + model_name='feedback', + name='status', + field=models.CharField(choices=[('submitted', 'Submitted'), ('reviewed', 'Reviewed'), ('acknowledged', 'Acknowledged'), ('closed', 'Closed'), ('reopened', 'Reopened')], db_index=True, default='submitted', max_length=20), + ), + ] diff --git a/apps/feedback/migrations/0006_alter_feedback_legacy_location_and_more.py b/apps/feedback/migrations/0006_alter_feedback_legacy_location_and_more.py new file mode 100644 index 0000000..eb7af47 --- /dev/null +++ b/apps/feedback/migrations/0006_alter_feedback_legacy_location_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0005_feedback_legacy_location_and_more'), + ('organizations', '0005_alter_legacylocation_table_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text='Location context', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='feedback', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='feedback', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/feedback/migrations/0007_remove_sub_subsection.py b/apps/feedback/migrations/0007_remove_sub_subsection.py new file mode 100644 index 0000000..a1399db --- /dev/null +++ b/apps/feedback/migrations/0007_remove_sub_subsection.py @@ -0,0 +1,10 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0006_alter_feedback_legacy_location_and_more'), + ] + + operations = [] diff --git a/apps/feedback/migrations/0008_feedback_reference_number_and_more.py b/apps/feedback/migrations/0008_feedback_reference_number_and_more.py new file mode 100644 index 0000000..651e6eb --- /dev/null +++ b/apps/feedback/migrations/0008_feedback_reference_number_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-06-14 10:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0007_remove_sub_subsection'), + ('organizations', '0014_remove_department_manager_1st'), + ] + + operations = [ + migrations.AddField( + model_name='feedback', + name='reference_number', + field=models.CharField(blank=True, db_index=True, max_length=40, null=True, unique=True), + ), + migrations.AlterField( + model_name='feedback', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='feedback', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='feedback', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedbacks', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/feedback/models.py b/apps/feedback/models.py index 2f4c97c..31f883e 100644 --- a/apps/feedback/models.py +++ b/apps/feedback/models.py @@ -9,6 +9,7 @@ This module implements the feedback management system that: """ from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -33,6 +34,16 @@ class FeedbackStatus(models.TextChoices): REVIEWED = "reviewed", _("Reviewed") ACKNOWLEDGED = "acknowledged", _("Acknowledged") CLOSED = "closed", _("Closed") + REOPENED = "reopened", _("Reopened") + + +VALID_FEEDBACK_TRANSITIONS = { + FeedbackStatus.SUBMITTED: [FeedbackStatus.REVIEWED, FeedbackStatus.CLOSED], + FeedbackStatus.REVIEWED: [FeedbackStatus.ACKNOWLEDGED, FeedbackStatus.CLOSED], + FeedbackStatus.ACKNOWLEDGED: [FeedbackStatus.CLOSED], + FeedbackStatus.CLOSED: [FeedbackStatus.REOPENED], + FeedbackStatus.REOPENED: [FeedbackStatus.REVIEWED, FeedbackStatus.ACKNOWLEDGED, FeedbackStatus.CLOSED], +} class FeedbackCategory(models.TextChoices): @@ -101,29 +112,38 @@ class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): # Organization hospital = models.ForeignKey("organizations.Hospital", on_delete=models.CASCADE, related_name="feedbacks") - location = models.ForeignKey( - "organizations.Location", + legacy_location = models.ForeignKey( + "organizations.LegacyLocation", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", - help_text="Location context", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - main_section = models.ForeignKey( - "organizations.MainSection", + legacy_main_section = models.ForeignKey( + "organizations.LegacyMainSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", - help_text="Main section within the location", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", ) - subsection = models.ForeignKey( - "organizations.SubSection", + legacy_subsection = models.ForeignKey( + "organizations.LegacySubSection", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks", - help_text="Specific subsection", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + # New hierarchy (from 4th Version Excel) + section = models.ForeignKey( + "organizations.Section", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="feedbacks_new", + help_text="Section within department", ) department = models.ForeignKey( "organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="feedbacks" @@ -145,6 +165,9 @@ class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): title = models.CharField(max_length=500) message = models.TextField(help_text="Feedback message") + # Reference number (unified format SGT-YYYYMM-HOSP-NNNN; internal-only, not publicly trackable) + reference_number = models.CharField(max_length=40, unique=True, blank=True, null=True, db_index=True) + # Classification category = models.CharField(max_length=50, choices=FeedbackCategory.choices, db_index=True) subcategory = models.CharField(max_length=100, blank=True) @@ -216,6 +239,8 @@ class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): # Metadata metadata = models.JSONField(default=dict, blank=True) + notes = GenericRelation("core.Note") + class Meta: ordering = ["-created_at"] indexes = [ @@ -233,6 +258,13 @@ class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): return f"{self.title} - {self.patient.get_full_name()} ({self.feedback_type})" return f"{self.title} - Anonymous ({self.feedback_type})" + def save(self, *args, **kwargs): + if not self.reference_number: + from apps.core.reference import generate_reference + + self.reference_number = generate_reference("SGT", self.hospital) + super().save(*args, **kwargs) + def get_absolute_url(self): from django.urls import reverse diff --git a/apps/feedback/tasks.py b/apps/feedback/tasks.py index 7aa52d0..b073d30 100644 --- a/apps/feedback/tasks.py +++ b/apps/feedback/tasks.py @@ -1,6 +1,7 @@ import logging from celery import shared_task +from django.utils import timezone logger = logging.getLogger(__name__) diff --git a/apps/feedback/views.py b/apps/feedback/views.py index 39e56fe..c5f25d8 100644 --- a/apps/feedback/views.py +++ b/apps/feedback/views.py @@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods +from django.core.cache import cache from apps.accounts.models import User from apps.accounts.services import StaffActivityService @@ -24,6 +25,7 @@ from .models import ( FeedbackStatus, FeedbackType, FeedbackCategory, + VALID_FEEDBACK_TRANSITIONS, ) from .forms import ( FeedbackForm, @@ -141,12 +143,13 @@ def feedback_list(request): if date_to: queryset = queryset.filter(created_at__lte=date_to) - # Ordering + ALLOWED_ORDER_BY = {"-created_at", "created_at", "-updated_at", "updated_at", "-title", "title", "-rating", "rating"} order_by = request.GET.get("order_by", "-created_at") + if order_by not in ALLOWED_ORDER_BY: + order_by = "-created_at" queryset = queryset.order_by(order_by) - # Pagination - page_size = int(request.GET.get("page_size", 25)) + page_size = min(int(request.GET.get("page_size", 25)), 100) paginator = Paginator(queryset, page_size) page_number = request.GET.get("page", 1) page_obj = paginator.get_page(page_number) @@ -238,6 +241,10 @@ def feedback_detail(request, pk): "assigned_to", "created_by" ) + from django.contrib.contenttypes.models import ContentType + feedback_ct = ContentType.objects.get_for_model(Feedback) + generic_notes = feedback.notes.select_related("created_by").all() + context = { "feedback": feedback, "timeline": timeline, @@ -246,6 +253,10 @@ def feedback_detail(request, pk): "status_choices": FeedbackStatus.choices, "can_edit": user.is_px_admin() or user.is_hospital_admin(), "linked_rcas": linked_rcas, + "content_type_id": feedback_ct.pk, + "object_id": feedback.pk, + "notes": generic_notes, + "notes_count": generic_notes.count(), } return render(request, "feedback/feedback_detail.html", context) @@ -254,7 +265,7 @@ def feedback_detail(request, pk): @login_required @require_http_methods(["GET", "POST"]) def feedback_create(request): - from apps.organizations.models import Location, MainSection, SubSection, Hospital + from apps.organizations.models import LegacyLocation, LegacyMainSection, LegacySubSection, Hospital from apps.feedback.models import FeedbackType, FeedbackCategory, FeedbackStatus communication_request = None @@ -275,9 +286,6 @@ def feedback_create(request): message = request.POST.get("message", "").strip() hospital_id = request.POST.get("hospital", "") title = request.POST.get("title", message[:100]).strip() - location_id = request.POST.get("location", "").strip() - main_section_id = request.POST.get("main_section", "").strip() - subsection_id = request.POST.get("subsection", "").strip() errors = [] if not contact_name: @@ -295,9 +303,6 @@ def feedback_create(request): else: try: hospital = Hospital.objects.get(id=hospital_id) - location = Location.objects.filter(id=location_id).first() if location_id else None - main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None - subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None feedback = Feedback( hospital=hospital, @@ -309,9 +314,6 @@ def feedback_create(request): contact_phone=contact_phone, is_anonymous=False, status=FeedbackStatus.SUBMITTED, - location=location, - main_section=main_section, - subsection=subsection, ) feedback.save() @@ -681,12 +683,6 @@ def export_action_plans(request): return export_action_plans(qs) - context = { - "feedback": feedback, - } - - return render(request, "feedback/feedback_delete_confirm.html", context) - @login_required @require_http_methods(["POST"]) @@ -757,10 +753,27 @@ def feedback_change_status(request, pk): messages.error(request, "Please select a status.") return redirect("feedback:feedback_detail", pk=pk) + valid_status_values = [s[0] for s in FeedbackStatus.choices] + if new_status not in valid_status_values: + messages.error(request, "Invalid status value.") + return redirect("feedback:feedback_detail", pk=pk) + old_status = feedback.status + + if old_status == new_status: + messages.info(request, "Suggestion is already in this status.") + return redirect("feedback:feedback_detail", pk=pk) + + allowed = VALID_FEEDBACK_TRANSITIONS.get(old_status, []) + if new_status not in allowed: + messages.error( + request, + f"Cannot change status from {old_status} to {new_status}. Allowed transitions: {', '.join(allowed)}.", + ) + return redirect("feedback:feedback_detail", pk=pk) + feedback.status = new_status - # Handle status-specific logic if new_status == FeedbackStatus.REVIEWED: feedback.reviewed_at = timezone.now() feedback.reviewed_by = request.user @@ -770,6 +783,9 @@ def feedback_change_status(request, pk): elif new_status == FeedbackStatus.CLOSED: feedback.closed_at = timezone.now() feedback.closed_by = request.user + elif new_status == FeedbackStatus.REOPENED: + feedback.closed_at = None + feedback.closed_by = None feedback.save() @@ -805,6 +821,12 @@ def feedback_add_response(request, pk): """Add response to feedback""" feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() + or user.is_department_manager()): + messages.error(request, "You don't have permission to add responses to this suggestion.") + return redirect("feedback:feedback_detail", pk=pk) + response_type = request.POST.get("response_type", "response") message = request.POST.get("message") is_internal = request.POST.get("is_internal") == "on" @@ -875,6 +897,12 @@ def public_suggestion_submit(request): import logging logger = logging.getLogger(__name__) + client_ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() or request.META.get('REMOTE_ADDR', '') + cache_key = f"suggestion_rate:{client_ip}" + if cache.get(cache_key, 0) >= 5: + return JsonResponse({"success": False, "message": "Too many requests. Please try again later."}, status=429) + cache.set(cache_key, cache.get(cache_key, 0) + 1, 300) + try: try: data = json.loads(request.body) if request.content_type == "application/json" else request.POST @@ -902,11 +930,6 @@ def public_suggestion_submit(request): except Hospital.DoesNotExist: return JsonResponse({"success": False, "message": "Invalid hospital."}, status=400) - from apps.organizations.models import Location, MainSection, SubSection - location = Location.objects.filter(id=location_id).first() if location_id else None - main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None - subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None - category_map = { "general": FeedbackCategory.OTHER, "clinical_care": FeedbackCategory.CLINICAL_CARE, @@ -927,9 +950,6 @@ def public_suggestion_submit(request): category=category_map.get(category, FeedbackCategory.OTHER), contact_name=contact_name, contact_phone=contact_phone, - location=location, - main_section=main_section, - subsection=subsection, is_anonymous=False, status=FeedbackStatus.SUBMITTED, metadata={ @@ -968,6 +988,11 @@ def feedback_create_action(request, pk): feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, "You don't have permission to create actions from suggestions.") + return redirect("feedback:feedback_detail", pk=pk) + action_title = request.POST.get("action_title", "").strip() action_description = request.POST.get("action_description", "").strip() action_category = request.POST.get("action_category", "other") diff --git a/apps/integrations/admin.py b/apps/integrations/admin.py index 36f0809..b89c2e5 100644 --- a/apps/integrations/admin.py +++ b/apps/integrations/admin.py @@ -7,6 +7,7 @@ from django.utils.html import format_html, mark_safe from .models import ( EventMapping, + ExternalAPIKey, HISEventType, HISTestPatient, HISTestVisit, @@ -260,3 +261,71 @@ class HISTestVisitAdmin(admin.ModelAdmin): def has_add_permission(self, request): return False + + +@admin.register(ExternalAPIKey) +class ExternalAPIKeyAdmin(admin.ModelAdmin): + """Admin for External API Keys - shows the plaintext key ONLY on creation.""" + + list_display = ["name", "key_prefix", "hospital", "is_active", "allowed_entities_display", "last_used_at", "created_at"] + list_filter = ["is_active", "hospital"] + search_fields = ["name", "key_prefix", "description"] + ordering = ["-created_at"] + readonly_fields = ["key_hash", "key_prefix", "last_used_at", "created_at", "updated_at"] + + fieldsets = ( + ( + "Identity", + {"fields": ("name", "description", "key_prefix", "key_hash")}, + ), + ( + "Scope", + {"fields": ("hospital", "allowed_entities", "rate_limit")}, + ), + ( + "Status", + {"fields": ("is_active", "expires_at", "last_used_at")}, + ), + ( + "Metadata", + {"fields": ("created_by", "created_at", "updated_at")}, + ), + ) + + def allowed_entities_display(self, obj): + if not obj.allowed_entities: + return mark_safe('All Entities') + return ", ".join(obj.allowed_entities) + + allowed_entities_display.short_description = "Allowed Entities" + + def save_model(self, request, obj, form, change): + if not change: + # Creating a new key — generate it + obj.created_by = request.user + obj.save() + + # Store the raw key to show in admin message + # We need to generate the key ourselves since save_model doesn't return it + import secrets + import hashlib + raw_key = secrets.token_hex(32) + obj.key_prefix = raw_key[:8] + obj.key_hash = hashlib.sha256(raw_key.encode()).hexdigest() + obj.save(update_fields=["key_prefix", "key_hash"]) + + self._generated_key = raw_key + else: + obj.save() + + def response_add(self, request, obj, post_url_continue=None): + resp = super().response_add(request, obj, post_url_continue) + if hasattr(self, "_generated_key"): + from django.contrib import messages + messages.success( + request, + f'API Key created. Copy it now — it will NOT be shown again: {self._generated_key}', + ) + # Clear it + del self._generated_key + return resp diff --git a/apps/integrations/api_serializers.py b/apps/integrations/api_serializers.py new file mode 100644 index 0000000..b581603 --- /dev/null +++ b/apps/integrations/api_serializers.py @@ -0,0 +1,529 @@ +""" +External API serializers for public-facing create/retrieve operations. +""" + +from rest_framework import serializers + +from apps.complaints.models import Complaint, Inquiry +from apps.observations.models import Observation +from apps.appreciation.models import Appreciation, AppreciationStatus, AppreciationVisibility +from apps.feedback.models import Feedback, FeedbackType, FeedbackStatus, FeedbackCategory + + +# --------------------------------------------------------------------------- +# Complaint +# --------------------------------------------------------------------------- + +class ExternalComplaintCreateSerializer(serializers.Serializer): + """Public-facing fields for creating a complaint via external API.""" + + # Contact + contact_name = serializers.CharField(max_length=200, required=True) + contact_phone = serializers.CharField(max_length=20, required=True) + contact_email = serializers.EmailField(required=False, allow_blank=True, default="") + relation_to_patient = serializers.ChoiceField( + choices=[("patient", "Patient"), ("relative", "Relative"), ("friend", "Friend"), ("other", "Other")], + required=False, + allow_blank=True, + default="", + ) + + # Patient + patient_name = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + national_id = serializers.CharField(max_length=50, required=False, allow_blank=True, default="") + incident_date = serializers.DateField(required=False, allow_null=True, default=None) + + # Hospital context + hospital = serializers.CharField(max_length=200, required=True) + + # Location hierarchy (all optional) + location_type = serializers.ChoiceField( + choices=[("OP", "Outpatient"), ("IP", "Inpatient"), ("ER", "Emergency"), ("GENERAL", "General")], + required=False, + allow_blank=True, + default="", + ) + area = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + department = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + section = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + + # Complaint content + title = serializers.CharField(max_length=500, required=True) + description = serializers.CharField(required=True) + expected_result = serializers.CharField(required=False, allow_blank=True, default="") + + def validate_hospital(self, value): + from apps.organizations.models import Hospital + try: + return Hospital.objects.get(name__iexact=value.strip()) + except Hospital.DoesNotExist: + raise serializers.ValidationError("Hospital not found.") + + def validate(self, data): + hospital = data.get("hospital") + if hospital: + area_name = data.get("area") + if area_name: + from apps.organizations.models import Area + try: + data["area"] = Area.objects.get( + name_en__iexact=area_name.strip(), hospital=hospital, status="active" + ) + except Area.DoesNotExist: + raise serializers.ValidationError({"area": f"Area '{area_name}' not found for this hospital."}) + + dept_name = data.get("department") + if dept_name: + from apps.organizations.models import Department + try: + data["department"] = Department.objects.get( + name__iexact=dept_name.strip(), hospital=hospital, status="active" + ) + except Department.DoesNotExist: + raise serializers.ValidationError( + {"department": f"Department '{dept_name}' not found for this hospital."} + ) + + section_name = data.get("section") + if section_name: + department = data.get("department") + if not department: + raise serializers.ValidationError({"section": "Department is required to look up section."}) + from apps.organizations.models import Section + try: + data["section"] = Section.objects.get( + name_en__iexact=section_name.strip(), department=department, status="active" + ) + except Section.DoesNotExist: + raise serializers.ValidationError( + {"section": f"Section '{section_name}' not found for this department."} + ) + + return data + + +class ExternalComplaintRetrieveSerializer(serializers.ModelSerializer): + """Read-only serializer for retrieving a complaint.""" + + hospital_name = serializers.CharField(source="hospital.name", read_only=True) + + class Meta: + model = Complaint + fields = [ + "id", + "reference_number", + "title", + "description", + "status", + "severity", + "priority", + "contact_name", + "contact_phone", + "contact_email", + "relation_to_patient", + "patient_name", + "incident_date", + "expected_result", + "resolution", + "satisfaction", + "hospital_name", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# --------------------------------------------------------------------------- +# Inquiry +# --------------------------------------------------------------------------- + +class ExternalInquiryCreateSerializer(serializers.Serializer): + """Public-facing fields for creating an inquiry via external API.""" + + # Contact + contact_name = serializers.CharField(max_length=200, required=True) + contact_phone = serializers.CharField(max_length=20, required=True) + contact_email = serializers.EmailField(required=False, allow_blank=True, default="") + + # Hospital context + hospital = serializers.CharField(max_length=200, required=True) + + # Location hierarchy (all optional) + location_type = serializers.ChoiceField( + choices=[("OP", "Outpatient"), ("IP", "Inpatient"), ("ER", "Emergency"), ("GENERAL", "General")], + required=False, + allow_blank=True, + default="", + ) + area = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + department = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + section = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + + # Inquiry content + subject = serializers.CharField(max_length=500, required=True) + message = serializers.CharField(required=True) + category = serializers.ChoiceField( + choices=[ + ("appointment", "Appointment"), + ("billing", "Billing"), + ("medical_records", "Medical Records"), + ("general", "General Information"), + ("other", "Other"), + ], + required=False, + default="general", + ) + + def validate_hospital(self, value): + from apps.organizations.models import Hospital + try: + return Hospital.objects.get(name__iexact=value.strip()) + except Hospital.DoesNotExist: + raise serializers.ValidationError("Hospital not found.") + + def validate(self, data): + hospital = data.get("hospital") + if hospital: + area_name = data.get("area") + if area_name: + from apps.organizations.models import Area + try: + data["area"] = Area.objects.get( + name_en__iexact=area_name.strip(), hospital=hospital, status="active" + ) + except Area.DoesNotExist: + raise serializers.ValidationError({"area": f"Area '{area_name}' not found for this hospital."}) + + dept_name = data.get("department") + if dept_name: + from apps.organizations.models import Department + try: + data["department"] = Department.objects.get( + name__iexact=dept_name.strip(), hospital=hospital, status="active" + ) + except Department.DoesNotExist: + raise serializers.ValidationError( + {"department": f"Department '{dept_name}' not found for this hospital."} + ) + + section_name = data.get("section") + if section_name: + department = data.get("department") + if not department: + raise serializers.ValidationError({"section": "Department is required to look up section."}) + from apps.organizations.models import Section + try: + data["section"] = Section.objects.get( + name_en__iexact=section_name.strip(), department=department, status="active" + ) + except Section.DoesNotExist: + raise serializers.ValidationError( + {"section": f"Section '{section_name}' not found for this department."} + ) + + return data + + +class ExternalInquiryRetrieveSerializer(serializers.ModelSerializer): + """Read-only serializer for retrieving an inquiry.""" + + hospital_name = serializers.CharField(source="hospital.name", read_only=True) + + class Meta: + model = Inquiry + fields = [ + "id", + "reference_number", + "subject", + "message", + "category", + "status", + "contact_name", + "contact_phone", + "contact_email", + "hospital_name", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# --------------------------------------------------------------------------- +# Observation +# --------------------------------------------------------------------------- + +class ExternalObservationCreateSerializer(serializers.Serializer): + """Public-facing fields for creating an observation via external API.""" + + # Hospital context + hospital = serializers.CharField(max_length=200, required=True) + + # Classification + category = serializers.UUIDField(required=False, allow_null=True, default=None) + + # Content + title = serializers.CharField(max_length=300, required=False, allow_blank=True, default="") + description = serializers.CharField(required=True) + severity = serializers.ChoiceField( + choices=[("low", "Low"), ("medium", "Medium"), ("high", "High"), ("critical", "Critical")], + required=False, + default="medium", + ) + + # Location and timing + location_text = serializers.CharField(max_length=500, required=False, allow_blank=True, default="") + incident_datetime = serializers.DateTimeField(required=False, allow_null=True, default=None) + + # Reporter info (all optional - anonymous supported) + contact_name = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + contact_phone = serializers.CharField(max_length=20, required=False, allow_blank=True, default="") + contact_email = serializers.EmailField(required=False, allow_blank=True, default="") + reporter_staff_id = serializers.CharField(max_length=50, required=False, allow_blank=True, default="") + + # Patient info + patient_file_number = serializers.CharField(max_length=100, required=False, allow_blank=True, default="") + + def validate_hospital(self, value): + from apps.organizations.models import Hospital + try: + return Hospital.objects.get(name__iexact=value.strip()) + except Hospital.DoesNotExist: + raise serializers.ValidationError("Hospital not found.") + + def validate_category(self, value): + if value is None: + return None + from apps.observations.models import ObservationCategory + try: + return ObservationCategory.objects.get(id=value) + except ObservationCategory.DoesNotExist: + raise serializers.ValidationError("Observation category not found.") + + +class ExternalObservationRetrieveSerializer(serializers.ModelSerializer): + """Read-only serializer for retrieving an observation.""" + + reference_number = serializers.CharField(source="tracking_code", read_only=True) + hospital_name = serializers.CharField(source="hospital.name", read_only=True, default=None) + category_name = serializers.CharField(source="category.name", read_only=True, default=None) + contact_name = serializers.CharField(source="reporter_name", read_only=True) + contact_phone = serializers.CharField(source="reporter_phone", read_only=True) + contact_email = serializers.CharField(source="reporter_email", read_only=True) + + class Meta: + model = Observation + fields = [ + "id", + "reference_number", + "title", + "description", + "severity", + "status", + "location_text", + "incident_datetime", + "contact_name", + "contact_phone", + "contact_email", + "reporter_staff_id", + "patient_file_number", + "hospital_name", + "category_name", + "created_at", + "updated_at", + ] + read_only_fields = fields + + +# --------------------------------------------------------------------------- +# Appreciation +# --------------------------------------------------------------------------- + +class ExternalAppreciationCreateSerializer(serializers.Serializer): + """Public-facing fields for creating an appreciation via external API.""" + + # Contact + contact_name = serializers.CharField(max_length=200, required=True) + contact_phone = serializers.CharField(max_length=20, required=True) + + # Content + message = serializers.CharField(required=True) + + # Hospital context + hospital = serializers.CharField(max_length=200, required=True) + + def validate_hospital(self, value): + from apps.organizations.models import Hospital + try: + return Hospital.objects.get(name__iexact=value.strip()) + except Hospital.DoesNotExist: + raise serializers.ValidationError("Hospital not found.") + + +class ExternalAppreciationRetrieveSerializer(serializers.ModelSerializer): + """Read-only serializer for retrieving an appreciation.""" + + hospital_name = serializers.CharField(source="hospital.name", read_only=True) + reference_number = serializers.SerializerMethodField() + + class Meta: + model = Appreciation + fields = [ + "id", + "reference_number", + "message_en", + "status", + "hospital_name", + "is_anonymous", + "created_at", + "updated_at", + ] + read_only_fields = fields + + def get_reference_number(self, obj): + if obj.metadata and "reference_number" in obj.metadata: + return obj.metadata["reference_number"] + return None + + +# --------------------------------------------------------------------------- +# Suggestion (Feedback) +# --------------------------------------------------------------------------- + +class ExternalSuggestionCreateSerializer(serializers.Serializer): + """Public-facing fields for creating a suggestion via external API.""" + + # Contact + contact_name = serializers.CharField(max_length=200, required=True) + contact_phone = serializers.CharField(max_length=20, required=True) + + # Content + title = serializers.CharField(max_length=500, required=False, allow_blank=True, default="") + message = serializers.CharField(required=True) + category = serializers.ChoiceField( + choices=[ + ("clinical_care", "Clinical Care"), + ("staff_service", "Staff Service"), + ("facility", "Facility"), + ("communication", "Communication"), + ("appointment", "Appointment"), + ("billing", "Billing"), + ("food_service", "Food Service"), + ("cleanliness", "Cleanliness"), + ("technology", "Technology"), + ("general", "General"), + ("other", "Other"), + ], + required=False, + default="general", + ) + rating = serializers.IntegerField(min_value=1, max_value=5, required=False, allow_null=True, default=None) + + # Hospital context + hospital = serializers.CharField(max_length=200, required=True) + + def validate_hospital(self, value): + from apps.organizations.models import Hospital + try: + return Hospital.objects.get(name__iexact=value.strip()) + except Hospital.DoesNotExist: + raise serializers.ValidationError("Hospital not found.") + + +class ExternalSuggestionRetrieveSerializer(serializers.ModelSerializer): + """Read-only serializer for retrieving a suggestion.""" + + hospital_name = serializers.CharField(source="hospital.name", read_only=True) + reference_number = serializers.SerializerMethodField() + + class Meta: + model = Feedback + fields = [ + "id", + "reference_number", + "title", + "message", + "category", + "rating", + "status", + "feedback_type", + "sentiment", + "contact_name", + "contact_phone", + "hospital_name", + "created_at", + "updated_at", + ] + read_only_fields = fields + + def get_reference_number(self, obj): + if obj.metadata and "reference_number" in obj.metadata: + return obj.metadata["reference_number"] + return None + + +class ExternalComplaintSatisfactionSerializer(serializers.Serializer): + satisfaction = serializers.ChoiceField( + choices=[ + ("satisfied", "Satisfied"), + ("neutral", "Neutral"), + ("dissatisfied", "Dissatisfied"), + ("no_response", "No Response"), + ], + required=True, + ) + + +# --------------------------------------------------------------------------- +# Doctor Rating +# --------------------------------------------------------------------------- + +class ExternalDoctorRatingCreateSerializer(serializers.Serializer): + + hospital_id = serializers.IntegerField(required=True) + doctor_id = serializers.CharField(max_length=50, required=True) + doctor_name = serializers.CharField(max_length=300, required=False, allow_blank=True, default="") + rating = serializers.IntegerField(min_value=1, max_value=5, required=True) + feedback = serializers.CharField(required=False, allow_blank=True, default="") + rating_date = serializers.DateField(required=False, allow_null=True, default=None) + patient_uhid = serializers.CharField(max_length=100, required=False, allow_blank=True, default="") + patient_name = serializers.CharField(max_length=300, required=False, allow_blank=True, default="") + patient_type = serializers.ChoiceField( + choices=["IP", "OP", "ER", "DC"], + required=False, + allow_blank=True, + default="", + ) + department_name = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + admit_date = serializers.DateField(required=False, allow_null=True, default=None) + discharge_date = serializers.DateField(required=False, allow_null=True, default=None) + + def validate_hospital_id(self, value): + from apps.organizations.models import Hospital + + try: + return Hospital.objects.get(id=value) + except Hospital.DoesNotExist: + raise serializers.ValidationError("Hospital not found.") + + def validate_doctor_id(self, value): + return value.strip() + + def validate(self, data): + from apps.organizations.models import Staff + + hospital = data.get("hospital_id") + doctor_id = data.get("doctor_id", "").strip() + + if hospital and doctor_id: + staff = Staff.objects.filter( + hospital=hospital, employee_id=doctor_id + ).first() + if not staff: + raise serializers.ValidationError( + { + "doctor_id": f"Doctor with employee ID '{doctor_id}' not found at this hospital." + } + ) + data["_staff"] = staff + + return data diff --git a/apps/integrations/api_views.py b/apps/integrations/api_views.py new file mode 100644 index 0000000..b0fbc08 --- /dev/null +++ b/apps/integrations/api_views.py @@ -0,0 +1,1107 @@ +""" +External API views for complaints, inquiries, observations, appreciations, and suggestions. + +All endpoints require X-API-Key header authentication. +""" + +import logging + +from django.utils import timezone +from rest_framework import generics, status +from rest_framework.response import Response +from rest_framework.views import APIView + +from apps.complaints.models import Complaint, ComplaintSourceType, ComplaintUpdate, Inquiry +from apps.core.services import AuditService +from apps.observations.models import Observation +from apps.appreciation.models import Appreciation, AppreciationStatus, AppreciationVisibility +from apps.feedback.models import Feedback, FeedbackType, FeedbackStatus, FeedbackCategory + +from .api_serializers import ( + ExternalAppreciationCreateSerializer, + ExternalAppreciationRetrieveSerializer, + ExternalComplaintCreateSerializer, + ExternalComplaintRetrieveSerializer, + ExternalComplaintSatisfactionSerializer, + ExternalDoctorRatingCreateSerializer, + ExternalInquiryCreateSerializer, + ExternalInquiryRetrieveSerializer, + ExternalObservationCreateSerializer, + ExternalObservationRetrieveSerializer, + ExternalSuggestionCreateSerializer, + ExternalSuggestionRetrieveSerializer, +) +from .authentication import ExternalAPIKeyAuthentication + + +logger = logging.getLogger("apps.integrations") + + +# --------------------------------------------------------------------------- +# Base mixin +# --------------------------------------------------------------------------- + +class ExternalAPIBase: + """Shared behaviour for all external API views.""" + + authentication_classes = [ExternalAPIKeyAuthentication] + permission_classes = [] + entity_name = None # Override in subclass + + def check_entity_access(self): + """Ensure the API key is allowed to access this entity.""" + api_key = getattr(self.request, "api_key", None) + if api_key and not api_key.can_access(self.entity_name): + return Response( + {"detail": f"This API key does not have access to {self.entity_name}."}, + status=status.HTTP_403_FORBIDDEN, + ) + return None + + def filter_queryset_by_hospital(self, qs): + """Scope query to the API key's hospital (if set).""" + api_key = getattr(self.request, "api_key", None) + if api_key and api_key.hospital: + hospital_field = self._get_hospital_field() + if hospital_field: + qs = qs.filter(**{hospital_field: api_key.hospital}) + return qs + + def _get_hospital_field(self): + """Return the ORM field name for hospital on the model.""" + return "hospital" + + def apply_list_filters(self, qs, request): + """Apply date range and status filters from query params.""" + + created_from = request.query_params.get("created_from", "").strip() + created_to = request.query_params.get("created_to", "").strip() + updated_from = request.query_params.get("updated_from", "").strip() + updated_to = request.query_params.get("updated_to", "").strip() + status_val = request.query_params.get("status", "").strip() + + if created_from: + try: + qs = qs.filter(created_at__gte=self._parse_date(created_from, start=True)) + except (ValueError, TypeError): + pass + + if created_to: + try: + qs = qs.filter(created_at__lte=self._parse_date(created_to, start=False)) + except (ValueError, TypeError): + pass + + if updated_from: + try: + qs = qs.filter(updated_at__gte=self._parse_date(updated_from, start=True)) + except (ValueError, TypeError): + pass + + if updated_to: + try: + qs = qs.filter(updated_at__lte=self._parse_date(updated_to, start=False)) + except (ValueError, TypeError): + pass + + if status_val: + qs = qs.filter(status=status_val) + + return qs + + def _parse_date(self, value, start=True): + """Parse a date string. If date-only, start=True → 00:00, start=False → 23:59:59.""" + from datetime import datetime + from django.utils import timezone as tz + + for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M", "%Y-%m-%d"): + try: + dt = datetime.strptime(value, fmt) + if fmt == "%Y-%m-%d" and not start: + dt = dt.replace(hour=23, minute=59, second=59) + return tz.make_aware(dt) if tz.is_naive(dt) else dt + except ValueError: + continue + raise ValueError(f"Invalid date format: {value}") + + +# --------------------------------------------------------------------------- +# Complaints +# --------------------------------------------------------------------------- + +class ExternalComplaintCreateView(ExternalAPIBase, APIView): + """ + POST /api/v1/external/complaints/ + + Create a complaint via external API. + """ + + entity_name = "complaints" + + def post(self, request): + access_err = self.check_entity_access() + if access_err: + return access_err + + ser = ExternalComplaintCreateSerializer(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + + # Enforce hospital scoping + api_key = getattr(request, "api_key", None) + if api_key and api_key.hospital and data["hospital"] != api_key.hospital: + return Response( + {"detail": "Hospital does not match API key scope."}, + status=status.HTTP_403_FORBIDDEN, + ) + + # Reference number generated by Complaint.save() (unified CMP-YYYYMM-HOSP-NNNN) + + complaint = Complaint.objects.create( + patient=None, + hospital=data["hospital"], + department=data.get("department"), + section=data.get("section"), + area=data.get("area"), + location_type=data.get("location_type", ""), + title=data["title"], + description=data["description"], + severity="medium", + priority="medium", + status="open", + complaint_source_type=ComplaintSourceType.EXTERNAL, + contact_name=data["contact_name"], + contact_phone=data["contact_phone"], + contact_email=data.get("contact_email", ""), + relation_to_patient=data.get("relation_to_patient", ""), + patient_name=data.get("patient_name", ""), + national_id=data.get("national_id", ""), + incident_date=data.get("incident_date"), + expected_result=data.get("expected_result", ""), + metadata={"source": "external_api"}, + ) + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message="Complaint submitted via external API.", + ) + + # Trigger background tasks + try: + from apps.complaints.tasks import analyze_complaint_with_ai, notify_staff_new_item + + analyze_complaint_with_ai.delay(str(complaint.id)) + notify_staff_new_item.delay("complaint", str(complaint.id)) + except Exception: + pass + + AuditService.log_event( + event_type="external_complaint_created", + description=f"Complaint created via external API: {reference_number}", + content_object=complaint, + metadata={"reference": reference_number, "source": "external_api"}, + ) + + return Response( + { + "success": True, + "reference_number": reference_number, + "status": complaint.status, + }, + status=status.HTTP_201_CREATED, + ) + + +class ExternalComplaintRetrieveView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/complaints/ + GET /api/v1/external/complaints// + + List complaints (scoped to hospital) or retrieve a single complaint by reference number. + """ + + entity_name = "complaints" + + def get(self, request, reference_number=None): + access_err = self.check_entity_access() + if access_err: + return access_err + + if reference_number: + return self._retrieve(request, reference_number) + return self._list(request) + + def _retrieve(self, request, reference_number): + qs = Complaint.all_objects.filter(reference_number=reference_number) + qs = self.filter_queryset_by_hospital(qs) + complaint = qs.first() + if not complaint: + return Response({"detail": "Complaint not found."}, status=status.HTTP_404_NOT_FOUND) + + ser = ExternalComplaintRetrieveSerializer(complaint) + return Response(ser.data) + + def _list(self, request): + qs = Complaint.all_objects.all() + qs = self.filter_queryset_by_hospital(qs) + qs = self.apply_list_filters(qs, request) + + page_size = min(int(request.query_params.get("page_size", 20)), 100) + page = int(request.query_params.get("page", 1)) + offset = (page - 1) * page_size + total = qs.count() + + items = qs[offset : offset + page_size] + ser = ExternalComplaintRetrieveSerializer(items, many=True) + + return Response( + { + "count": total, + "page": page, + "page_size": page_size, + "results": ser.data, + } + ) + + +class ExternalComplaintSatisfactionView(ExternalAPIBase, APIView): + """ + PATCH /api/v1/external/complaints//satisfaction/ + + Set patient satisfaction for a complaint. + """ + + entity_name = "complaints" + + def patch(self, request, reference_number): + access_err = self.check_entity_access() + if access_err: + return access_err + + qs = Complaint.all_objects.filter(reference_number=reference_number) + qs = self.filter_queryset_by_hospital(qs) + complaint = qs.first() + if not complaint: + return Response({"detail": "Complaint not found."}, status=status.HTTP_404_NOT_FOUND) + + if not complaint.resolution: + return Response( + {"detail": "Cannot set satisfaction before a resolution is recorded."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + ser = ExternalComplaintSatisfactionSerializer(data=request.data) + ser.is_valid(raise_exception=True) + + complaint.satisfaction = ser.validated_data["satisfaction"] + complaint.satisfaction_set_at = timezone.now() + complaint.save(update_fields=["satisfaction", "satisfaction_set_at", "updated_at"]) + + AuditService.log_event( + event_type="external_complaint_satisfaction_set", + description=f"Satisfaction set to '{complaint.satisfaction}' via external API for {reference_number}", + content_object=complaint, + metadata={"reference": reference_number, "satisfaction": complaint.satisfaction, "source": "external_api"}, + ) + + return Response( + { + "success": True, + "reference_number": complaint.reference_number, + "satisfaction": complaint.satisfaction, + } + ) + + +# --------------------------------------------------------------------------- +# Inquiries +# --------------------------------------------------------------------------- + +class ExternalInquiryCreateView(ExternalAPIBase, APIView): + """ + POST /api/v1/external/inquiries/ + + Create an inquiry via external API. + """ + + entity_name = "inquiries" + + def post(self, request): + access_err = self.check_entity_access() + if access_err: + return access_err + + ser = ExternalInquiryCreateSerializer(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + + api_key = getattr(request, "api_key", None) + if api_key and api_key.hospital and data["hospital"] != api_key.hospital: + return Response( + {"detail": "Hospital does not match API key scope."}, + status=status.HTTP_403_FORBIDDEN, + ) + + import uuid + from datetime import datetime + + today = datetime.now().strftime("%Y%m%d") + random_suffix = str(uuid.uuid4().int)[:6] + reference_number = f"INQ-{today}-{random_suffix}" + + inquiry = Inquiry.objects.create( + patient=None, + hospital=data["hospital"], + department=data.get("department"), + section=data.get("section"), + area=data.get("area"), + location_type=data.get("location_type", ""), + subject=data["subject"], + message=data["message"], + category=data.get("category", "general"), + contact_name=data["contact_name"], + contact_phone=data["contact_phone"], + contact_email=data.get("contact_email", ""), + status="open", + reference_number=reference_number, + is_outgoing=False, + metadata={"source": "external_api"}, + ) + + try: + from apps.complaints.tasks import analyze_inquiry_with_ai, notify_staff_new_item + + analyze_inquiry_with_ai.delay(str(inquiry.id)) + notify_staff_new_item.delay("inquiry", str(inquiry.id)) + except Exception: + pass + + AuditService.log_event( + event_type="external_inquiry_created", + description=f"Inquiry created via external API: {reference_number}", + content_object=inquiry, + metadata={"reference": reference_number, "source": "external_api"}, + ) + + return Response( + { + "success": True, + "reference_number": reference_number, + "status": inquiry.status, + }, + status=status.HTTP_201_CREATED, + ) + + +class ExternalInquiryRetrieveView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/inquiries/ + GET /api/v1/external/inquiries// + """ + + entity_name = "inquiries" + + def get(self, request, reference_number=None): + access_err = self.check_entity_access() + if access_err: + return access_err + + if reference_number: + return self._retrieve(request, reference_number) + return self._list(request) + + def _retrieve(self, request, reference_number): + qs = Inquiry.all_objects.filter(reference_number=reference_number) + qs = self.filter_queryset_by_hospital(qs) + inquiry = qs.first() + if not inquiry: + return Response({"detail": "Inquiry not found."}, status=status.HTTP_404_NOT_FOUND) + + ser = ExternalInquiryRetrieveSerializer(inquiry) + return Response(ser.data) + + def _list(self, request): + qs = Inquiry.all_objects.all() + qs = self.filter_queryset_by_hospital(qs) + qs = self.apply_list_filters(qs, request) + + page_size = min(int(request.query_params.get("page_size", 20)), 100) + page = int(request.query_params.get("page", 1)) + offset = (page - 1) * page_size + total = qs.count() + + items = qs[offset : offset + page_size] + ser = ExternalInquiryRetrieveSerializer(items, many=True) + + return Response( + { + "count": total, + "page": page, + "page_size": page_size, + "results": ser.data, + } + ) + + +# --------------------------------------------------------------------------- +# Observations +# --------------------------------------------------------------------------- + +class ExternalObservationCreateView(ExternalAPIBase, APIView): + """ + POST /api/v1/external/observations/ + + Create an observation via external API. + """ + + entity_name = "observations" + + def post(self, request): + access_err = self.check_entity_access() + if access_err: + return access_err + + ser = ExternalObservationCreateSerializer(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + + api_key = getattr(request, "api_key", None) + if api_key and api_key.hospital and data["hospital"] != api_key.hospital: + return Response( + {"detail": "Hospital does not match API key scope."}, + status=status.HTTP_403_FORBIDDEN, + ) + + from django.utils import timezone as tz + + observation = Observation.objects.create( + hospital=data["hospital"], + category=data.get("category"), + title=data.get("title", ""), + description=data["description"], + severity=data.get("severity", "medium"), + location_text=data.get("location_text", ""), + incident_datetime=data.get("incident_datetime") or tz.now(), + reporter_staff_id=data.get("reporter_staff_id", ""), + reporter_name=data.get("contact_name", ""), + reporter_phone=data.get("contact_phone", ""), + reporter_email=data.get("contact_email", ""), + patient_file_number=data.get("patient_file_number", ""), + source="other", + status="new", + metadata={"source": "external_api"}, + ) + + try: + from apps.complaints.tasks import notify_staff_new_item + + notify_staff_new_item.delay("observation", str(observation.id)) + except Exception: + pass + + AuditService.log_event( + event_type="external_observation_created", + description=f"Observation created via external API: {observation.tracking_code}", + content_object=observation, + metadata={"reference_number": observation.tracking_code, "source": "external_api"}, + ) + + return Response( + { + "success": True, + "reference_number": observation.tracking_code, + "status": observation.status, + }, + status=status.HTTP_201_CREATED, + ) + + +class ExternalObservationRetrieveView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/observations/ + GET /api/v1/external/observations// + """ + + entity_name = "observations" + + def get(self, request, tracking_code=None): + access_err = self.check_entity_access() + if access_err: + return access_err + + if tracking_code: + return self._retrieve(request, tracking_code) + return self._list(request) + + def _retrieve(self, request, tracking_code): + qs = Observation.all_objects.filter(tracking_code=tracking_code) + qs = self.filter_queryset_by_hospital(qs) + observation = qs.first() + if not observation: + return Response({"detail": "Observation not found."}, status=status.HTTP_404_NOT_FOUND) + + ser = ExternalObservationRetrieveSerializer(observation) + return Response(ser.data) + + def _list(self, request): + qs = Observation.all_objects.all() + qs = self.filter_queryset_by_hospital(qs) + qs = self.apply_list_filters(qs, request) + + page_size = min(int(request.query_params.get("page_size", 20)), 100) + page = int(request.query_params.get("page", 1)) + offset = (page - 1) * page_size + total = qs.count() + + items = qs[offset : offset + page_size] + ser = ExternalObservationRetrieveSerializer(items, many=True) + + return Response( + { + "count": total, + "page": page, + "page_size": page_size, + "results": ser.data, + } + ) + + +# --------------------------------------------------------------------------- +# Appreciations +# --------------------------------------------------------------------------- + +class ExternalAppreciationCreateView(ExternalAPIBase, APIView): + """ + POST /api/v1/external/appreciations/ + + Create an appreciation via external API. + """ + + entity_name = "appreciations" + + def post(self, request): + access_err = self.check_entity_access() + if access_err: + return access_err + + ser = ExternalAppreciationCreateSerializer(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + + api_key = getattr(request, "api_key", None) + if api_key and api_key.hospital and data["hospital"] != api_key.hospital: + return Response( + {"detail": "Hospital does not match API key scope."}, + status=status.HTTP_403_FORBIDDEN, + ) + + import uuid + from datetime import datetime + + today = datetime.now().strftime("%Y%m%d") + random_suffix = str(uuid.uuid4().int)[:6] + reference_number = f"APR-{today}-{random_suffix}" + + appreciation = Appreciation.objects.create( + hospital=data["hospital"], + message_en=data["message"], + message_ar="", + category=None, + is_anonymous=False, + status=AppreciationStatus.DRAFT, + visibility=AppreciationVisibility.PUBLIC, + metadata={ + "source": "external_api", + "reference_number": reference_number, + "submitted_by_name": data["contact_name"], + "submitted_by_phone": data["contact_phone"], + }, + ) + + try: + from apps.complaints.tasks import notify_staff_new_item + + notify_staff_new_item.delay("appreciation", str(appreciation.id)) + except Exception: + pass + + AuditService.log_event( + event_type="external_appreciation_created", + description=f"Appreciation created via external API", + content_object=appreciation, + metadata={"source": "external_api"}, + ) + + return Response( + { + "success": True, + "reference_number": reference_number, + "status": appreciation.status, + }, + status=status.HTTP_201_CREATED, + ) + + +class ExternalAppreciationRetrieveView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/appreciations/ + GET /api/v1/external/appreciations// + """ + + entity_name = "appreciations" + + def get(self, request, pk=None): + access_err = self.check_entity_access() + if access_err: + return access_err + + if pk: + return self._retrieve(request, pk) + return self._list(request) + + def _retrieve(self, request, pk): + qs = Appreciation.all_objects.filter(pk=pk) + qs = self.filter_queryset_by_hospital(qs) + appreciation = qs.first() + if not appreciation: + return Response({"detail": "Appreciation not found."}, status=status.HTTP_404_NOT_FOUND) + + ser = ExternalAppreciationRetrieveSerializer(appreciation) + return Response(ser.data) + + def _list(self, request): + qs = Appreciation.all_objects.all() + qs = self.filter_queryset_by_hospital(qs) + qs = self.apply_list_filters(qs, request) + + page_size = min(int(request.query_params.get("page_size", 20)), 100) + page = int(request.query_params.get("page", 1)) + offset = (page - 1) * page_size + total = qs.count() + + items = qs[offset : offset + page_size] + ser = ExternalAppreciationRetrieveSerializer(items, many=True) + + return Response( + { + "count": total, + "page": page, + "page_size": page_size, + "results": ser.data, + } + ) + + +# --------------------------------------------------------------------------- +# Suggestions (Feedback) +# --------------------------------------------------------------------------- + +class ExternalSuggestionCreateView(ExternalAPIBase, APIView): + """ + POST /api/v1/external/suggestions/ + + Create a suggestion via external API. + """ + + entity_name = "suggestions" + + def post(self, request): + access_err = self.check_entity_access() + if access_err: + return access_err + + ser = ExternalSuggestionCreateSerializer(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + + api_key = getattr(request, "api_key", None) + if api_key and api_key.hospital and data["hospital"] != api_key.hospital: + return Response( + {"detail": "Hospital does not match API key scope."}, + status=status.HTTP_403_FORBIDDEN, + ) + + category_map = { + "general": FeedbackCategory.OTHER, + "clinical_care": FeedbackCategory.CLINICAL_CARE, + "facility": FeedbackCategory.FACILITY, + "staff_service": FeedbackCategory.STAFF_SERVICE, + "communication": FeedbackCategory.COMMUNICATION, + "technology": FeedbackCategory.TECHNOLOGY, + "food_service": FeedbackCategory.FOOD_SERVICE, + "appointment": FeedbackCategory.APPOINTMENT, + "other": FeedbackCategory.OTHER, + } + + import uuid as _uuid + from datetime import datetime + + today = datetime.now().strftime("%Y%m%d") + random_suffix = str(_uuid.uuid4().int)[:6] + reference_number = f"SG-{today}-{random_suffix}" + + title = data.get("title") or data["message"][:100] + + feedback = Feedback.objects.create( + hospital=data["hospital"], + feedback_type=FeedbackType.SUGGESTION, + title=title, + message=data["message"], + category=category_map.get(data.get("category", "general"), FeedbackCategory.OTHER), + contact_name=data["contact_name"], + contact_phone=data["contact_phone"], + rating=data.get("rating"), + is_anonymous=False, + status=FeedbackStatus.SUBMITTED, + metadata={ + "source": "external_api", + "reference_number": reference_number, + "suggestion_area": data.get("category", "general"), + }, + ) + + try: + from apps.feedback.tasks import analyze_suggestion_with_ai + from apps.complaints.tasks import notify_staff_new_item + + analyze_suggestion_with_ai.delay(str(feedback.id)) + notify_staff_new_item.delay("suggestion", str(feedback.id)) + except Exception: + pass + + AuditService.log_event( + event_type="external_suggestion_created", + description=f"Suggestion created via external API", + content_object=feedback, + metadata={"source": "external_api"}, + ) + + return Response( + { + "success": True, + "reference_number": reference_number, + "status": feedback.status, + }, + status=status.HTTP_201_CREATED, + ) + + +class ExternalSuggestionRetrieveView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/suggestions/ + GET /api/v1/external/suggestions// + """ + + entity_name = "suggestions" + + def get(self, request, pk=None): + access_err = self.check_entity_access() + if access_err: + return access_err + + if pk: + return self._retrieve(request, pk) + return self._list(request) + + def _retrieve(self, request, pk): + qs = Feedback.all_objects.filter(pk=pk) + qs = self.filter_queryset_by_hospital(qs) + feedback = qs.first() + if not feedback: + return Response({"detail": "Suggestion not found."}, status=status.HTTP_404_NOT_FOUND) + + ser = ExternalSuggestionRetrieveSerializer(feedback) + return Response(ser.data) + + def _list(self, request): + qs = Feedback.all_objects.all() + qs = self.filter_queryset_by_hospital(qs) + qs = self.apply_list_filters(qs, request) + + page_size = min(int(request.query_params.get("page_size", 20)), 100) + page = int(request.query_params.get("page", 1)) + offset = (page - 1) * page_size + total = qs.count() + + items = qs[offset : offset + page_size] + ser = ExternalSuggestionRetrieveSerializer(items, many=True) + + return Response( + { + "count": total, + "page": page, + "page_size": page_size, + "results": ser.data, + } + ) + + +# --------------------------------------------------------------------------- +# Lookup / Dropdown endpoints +# --------------------------------------------------------------------------- + +class ExternalHospitalsListView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/hospitals/ + + List all active hospitals. Used by external integrations to build dropdowns. + """ + + entity_name = "lookup" + + def get(self, request): + from apps.organizations.models import Hospital + + qs = Hospital.objects.filter(status="active").order_by("name") + results = [ + { + "id": str(h.id), + "name": h.name, + "code": h.code, + } + for h in qs + ] + return Response({"count": len(results), "results": results}) + + +class ExternalLocationTypesView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/location-types/ + + Static list of location types. + """ + + entity_name = "lookup" + + def get(self, request): + results = [ + {"value": "OP", "label": "Outpatient", "label_ar": "خارجي"}, + {"value": "IP", "label": "Inpatient", "label_ar": "تنويم"}, + {"value": "ER", "label": "Emergency", "label_ar": "طوارئ"}, + {"value": "GENERAL", "label": "General", "label_ar": "عام"}, + ] + return Response({"count": len(results), "results": results}) + + +class ExternalAreasListView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/areas/?hospital=&location_type= + + List areas for a hospital, optionally filtered by location_type. + """ + + entity_name = "lookup" + + def get(self, request): + from apps.organizations.models import Area, Hospital + + hospital_name = request.query_params.get("hospital", "").strip() + location_type = request.query_params.get("location_type", "").strip() + + if not hospital_name: + return Response( + {"detail": "hospital query parameter is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + hospital = Hospital.objects.get(name__iexact=hospital_name, status="active") + except Hospital.DoesNotExist: + return Response({"detail": "Hospital not found."}, status=status.HTTP_404_NOT_FOUND) + + qs = Area.objects.filter(hospital=hospital, status="active") + if location_type: + qs = qs.filter(location_type=location_type) + qs = qs.order_by("name_en") + + results = [ + { + "id": str(a.id), + "name": a.name_en, + "name_ar": a.name_ar, + "code": a.code, + } + for a in qs + ] + return Response({"count": len(results), "results": results}) + + +class ExternalDepartmentsListView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/departments/?hospital= + + List departments for a hospital. + """ + + entity_name = "lookup" + + def get(self, request): + from apps.organizations.models import Department, Hospital + + hospital_name = request.query_params.get("hospital", "").strip() + + if not hospital_name: + return Response( + {"detail": "hospital query parameter is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + hospital = Hospital.objects.get(name__iexact=hospital_name, status="active") + except Hospital.DoesNotExist: + return Response({"detail": "Hospital not found."}, status=status.HTTP_404_NOT_FOUND) + + qs = Department.objects.filter(hospital=hospital, status="active").order_by("name") + + results = [ + { + "id": str(d.id), + "name": d.name, + "name_en": d.name_en, + "name_ar": d.name_ar, + "code": d.code, + } + for d in qs + ] + return Response({"count": len(results), "results": results}) + + +class ExternalSectionsListView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/sections/?hospital=&department= + + List sections for a department. + """ + + entity_name = "lookup" + + def get(self, request): + from apps.organizations.models import Department, Hospital, Section + + hospital_name = request.query_params.get("hospital", "").strip() + department_name = request.query_params.get("department", "").strip() + + if not hospital_name or not department_name: + return Response( + {"detail": "hospital and department query parameters are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + hospital = Hospital.objects.get(name__iexact=hospital_name, status="active") + except Hospital.DoesNotExist: + return Response({"detail": "Hospital not found."}, status=status.HTTP_404_NOT_FOUND) + + try: + department = Department.objects.get( + name__iexact=department_name, hospital=hospital, status="active" + ) + except Department.DoesNotExist: + return Response({"detail": "Department not found."}, status=status.HTTP_404_NOT_FOUND) + + qs = Section.objects.filter(department=department, status="active").order_by("name_en") + + results = [ + { + "id": str(s.id), + "name": s.name_en, + "name_ar": s.name_ar, + "code": s.code, + } + for s in qs + ] + return Response({"count": len(results), "results": results}) + + +# --------------------------------------------------------------------------- +# Doctor Ratings +# --------------------------------------------------------------------------- + +class ExternalDoctorRatingCreateView(ExternalAPIBase, APIView): + """ + POST /api/v1/external/doctor-ratings/ + + Submit a patient rating for a doctor via external API. + Requires doctor_id (employee_id) that exists in the Staff table for the given hospital. + """ + + entity_name = "doctor_ratings" + + def post(self, request): + access_err = self.check_entity_access() + if access_err: + return access_err + + ser = ExternalDoctorRatingCreateSerializer(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + + hospital = data["hospital_id"] + staff = data.pop("_staff") + + api_key = getattr(request, "api_key", None) + if api_key and api_key.hospital and hospital != api_key.hospital: + return Response( + {"detail": "Hospital does not match API key scope."}, + status=status.HTTP_403_FORBIDDEN, + ) + + from django.utils import timezone as tz + from apps.physicians.models import PhysicianIndividualRating + + rating_date = data.get("rating_date") + if rating_date: + from datetime import datetime as _dt + rating_dt = tz.make_aware(_dt.combine(rating_date, _dt.min.time())) + else: + rating_dt = tz.now() + + doctor_name_raw = data.get("doctor_name", "") or staff.get_full_name() + + rating = PhysicianIndividualRating.objects.create( + staff=staff, + hospital=hospital, + source="his_api", + doctor_name_raw=doctor_name_raw, + doctor_id=data["doctor_id"], + doctor_name=data.get("doctor_name", ""), + rating=data["rating"], + feedback=data.get("feedback", ""), + rating_date=rating_dt, + patient_uhid=data.get("patient_uhid", "") or None, + patient_name=data.get("patient_name", "") or None, + patient_type=data.get("patient_type", "") or None, + department_name=data.get("department_name", ""), + admit_date=data.get("admit_date"), + discharge_date=data.get("discharge_date"), + metadata={"source": "external_api"}, + ) + + if not staff.physician: + staff.physician = True + staff.save(update_fields=["physician"]) + + AuditService.log_event( + event_type="external_doctor_rating_created", + description=f"Doctor rating created via external API: {staff.get_full_name()} - {data['rating']}/5", + content_object=rating, + metadata={ + "staff_id": str(staff.id), + "doctor_id": data["doctor_id"], + "rating": data["rating"], + "hospital_id": str(hospital.id), + "source": "external_api", + }, + ) + + return Response( + { + "success": True, + "rating_id": str(rating.id), + "staff_id": str(staff.id), + "doctor_name": staff.get_full_name(), + "rating": data["rating"], + }, + status=status.HTTP_201_CREATED, + ) diff --git a/apps/integrations/authentication.py b/apps/integrations/authentication.py new file mode 100644 index 0000000..b4e88fd --- /dev/null +++ b/apps/integrations/authentication.py @@ -0,0 +1,46 @@ +""" +External API key authentication for DRF. +""" + +import logging + +from rest_framework import authentication, exceptions + +logger = logging.getLogger("apps.integrations") + + +class ExternalAPIKeyAuthentication(authentication.BaseAuthentication): + """ + Authenticate requests using an API key passed via X-API-Key header. + + Sets `request.api_key` on successful authentication for downstream use. + """ + + HEADER_NAME = "HTTP_X_API_KEY" + + def authenticate(self, request): + raw_key = request.META.get(self.HEADER_NAME) + if not raw_key: + raise exceptions.AuthenticationFailed("API key required. Provide X-API-Key header.") + + from .models import ExternalAPIKey + + # Look up by prefix to avoid scanning all keys + prefix = raw_key[:8] + candidates = ExternalAPIKey.objects.filter( + key_prefix=prefix, + is_active=True, + ) + + for api_key in candidates: + if api_key.verify(raw_key): + # Rate limit check + # (simple per-key check; could be enhanced with cache) + api_key.record_usage() + request.api_key = api_key + return (None, api_key) + + raise exceptions.AuthenticationFailed("Invalid or expired API key.") + + def authenticate_header(self, request): + return "X-API-Key" diff --git a/apps/integrations/migrations/0003_external_api_key.py b/apps/integrations/migrations/0003_external_api_key.py new file mode 100644 index 0000000..88cd422 --- /dev/null +++ b/apps/integrations/migrations/0003_external_api_key.py @@ -0,0 +1,42 @@ +# Generated by Django 6.0.1 on 2026-05-29 15:18 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0002_initial'), + ('organizations', '0005_alter_legacylocation_table_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ExternalAPIKey', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(help_text="Label for this integration (e.g. 'MOH Portal', 'CHI Connector')", max_length=200)), + ('key_hash', models.CharField(db_index=True, help_text='SHA-256 hash of the API key', max_length=128, unique=True)), + ('key_prefix', models.CharField(help_text="First 8 chars of the key for identification (e.g. 'hh_live_')", max_length=12)), + ('is_active', models.BooleanField(db_index=True, default=True)), + ('allowed_entities', models.JSONField(blank=True, default=list, help_text='List of entities this key can access: ["complaints","inquiries","observations","appreciations","suggestions"]')), + ('rate_limit', models.IntegerField(default=60, help_text='Maximum requests per minute')), + ('description', models.TextField(blank=True)), + ('last_used_at', models.DateTimeField(blank=True, null=True)), + ('expires_at', models.DateTimeField(blank=True, help_text='When this key expires (null = never)', null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_external_api_keys', to=settings.AUTH_USER_MODEL)), + ('hospital', models.ForeignKey(blank=True, help_text='Scope this key to a hospital (null = all hospitals)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='external_api_keys', to='organizations.hospital')), + ], + options={ + 'verbose_name': 'External API Key', + 'verbose_name_plural': 'External API Keys', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/integrations/models.py b/apps/integrations/models.py index c5b8e2f..87386ac 100644 --- a/apps/integrations/models.py +++ b/apps/integrations/models.py @@ -11,11 +11,138 @@ This module handles integration events from: - Other external systems """ +import secrets +import hashlib + +from django.conf import settings from django.db import models +from django.utils import timezone from apps.core.models import BaseChoices, TimeStampedModel, UUIDModel +class ExternalAPIKey(UUIDModel, TimeStampedModel): + """ + API key for external integrations to access the external API. + + Each key is scoped to a hospital and can be restricted to specific entities. + The plaintext key is only shown once at creation time. + """ + + name = models.CharField( + max_length=200, + help_text="Label for this integration (e.g. 'MOH Portal', 'CHI Connector')", + ) + key_hash = models.CharField( + max_length=128, + unique=True, + db_index=True, + help_text="SHA-256 hash of the API key", + ) + key_prefix = models.CharField( + max_length=12, + help_text="First 8 chars of the key for identification (e.g. 'hh_live_')", + ) + + # Scoping + hospital = models.ForeignKey( + "organizations.Hospital", + on_delete=models.CASCADE, + related_name="external_api_keys", + null=True, + blank=True, + help_text="Scope this key to a hospital (null = all hospitals)", + ) + + # Access control + is_active = models.BooleanField(default=True, db_index=True) + allowed_entities = models.JSONField( + default=list, + blank=True, + help_text='List of entities this key can access: ["complaints","inquiries","observations","appreciations","suggestions"]', + ) + + # Rate limiting + rate_limit = models.IntegerField( + default=60, + help_text="Maximum requests per minute", + ) + + # Metadata + description = models.TextField(blank=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="created_external_api_keys", + ) + last_used_at = models.DateTimeField(null=True, blank=True) + expires_at = models.DateTimeField( + null=True, + blank=True, + help_text="When this key expires (null = never)", + ) + + class Meta: + ordering = ["-created_at"] + verbose_name = "External API Key" + verbose_name_plural = "External API Keys" + + def __str__(self): + status = "Active" if self.is_active else "Inactive" + hospital_name = self.hospital.name if self.hospital else "All" + return f"{self.name} ({self.key_prefix}...) [{status}] - {hospital_name}" + + @classmethod + def create_key(cls, name, hospital=None, allowed_entities=None, rate_limit=60, description="", created_by=None, expires_at=None): + """ + Generate a new API key and return (instance, plaintext_key). + The plaintext key is only available at creation time. + """ + raw_key = secrets.token_hex(32) # 64-char hex string + key_prefix = raw_key[:8] + key_hash = hashlib.sha256(raw_key.encode()).hexdigest() + + api_key = cls.objects.create( + name=name, + key_hash=key_hash, + key_prefix=key_prefix, + hospital=hospital, + allowed_entities=allowed_entities or [], + rate_limit=rate_limit, + description=description, + created_by=created_by, + expires_at=expires_at, + ) + + return api_key, raw_key + + def verify(self, raw_key): + """Verify a plaintext key against this stored hash.""" + if not self.is_active: + return False + if self.expires_at and timezone.now() > self.expires_at: + return False + computed_hash = hashlib.sha256(raw_key.encode()).hexdigest() + return secrets.compare_digest(computed_hash, self.key_hash) + + def can_access(self, entity): + """Check if this key can access the given entity type.""" + if not self.is_active: + return False + if entity == "lookup": + return True + if not self.allowed_entities: + return True + return entity in self.allowed_entities + + def record_usage(self): + """Update last_used_at timestamp.""" + self.last_used_at = timezone.now() + self.save(update_fields=["last_used_at"]) + + class EventStatus(BaseChoices): """Event processing status""" diff --git a/apps/integrations/urls_external.py b/apps/integrations/urls_external.py new file mode 100644 index 0000000..b6650f4 --- /dev/null +++ b/apps/integrations/urls_external.py @@ -0,0 +1,148 @@ +""" +URL patterns for the external API. +Mounted at /api/v1/external/. +""" + +from django.urls import path + +from .api_views import ( + ExternalAppreciationCreateView, + ExternalAppreciationRetrieveView, + ExternalAreasListView, + ExternalComplaintCreateView, + ExternalComplaintRetrieveView, + ExternalComplaintSatisfactionView, + ExternalDepartmentsListView, + ExternalDoctorRatingCreateView, + ExternalHospitalsListView, + ExternalInquiryCreateView, + ExternalInquiryRetrieveView, + ExternalLocationTypesView, + ExternalObservationCreateView, + ExternalObservationRetrieveView, + ExternalSectionsListView, + ExternalSuggestionCreateView, + ExternalSuggestionRetrieveView, +) + +app_name = "external_api" + +urlpatterns = [ + # Lookup / Dropdown endpoints + path( + "hospitals/", + ExternalHospitalsListView.as_view(), + name="external-hospitals", + ), + path( + "location-types/", + ExternalLocationTypesView.as_view(), + name="external-location-types", + ), + path( + "areas/", + ExternalAreasListView.as_view(), + name="external-areas", + ), + path( + "departments/", + ExternalDepartmentsListView.as_view(), + name="external-departments", + ), + path( + "sections/", + ExternalSectionsListView.as_view(), + name="external-sections", + ), + # Complaints + path( + "complaints/", + ExternalComplaintCreateView.as_view(), + name="external-complaint-create", + ), + path( + "complaints/list/", + ExternalComplaintRetrieveView.as_view(), + name="external-complaint-list", + ), + path( + "complaints//", + ExternalComplaintRetrieveView.as_view(), + name="external-complaint-detail", + ), + path( + "complaints//satisfaction/", + ExternalComplaintSatisfactionView.as_view(), + name="external-complaint-satisfaction", + ), + # Inquiries + path( + "inquiries/", + ExternalInquiryCreateView.as_view(), + name="external-inquiry-create", + ), + path( + "inquiries/list/", + ExternalInquiryRetrieveView.as_view(), + name="external-inquiry-list", + ), + path( + "inquiries//", + ExternalInquiryRetrieveView.as_view(), + name="external-inquiry-detail", + ), + # Observations + path( + "observations/", + ExternalObservationCreateView.as_view(), + name="external-observation-create", + ), + path( + "observations/list/", + ExternalObservationRetrieveView.as_view(), + name="external-observation-list", + ), + path( + "observations//", + ExternalObservationRetrieveView.as_view(), + name="external-observation-detail", + ), + # Appreciations + path( + "appreciations/", + ExternalAppreciationCreateView.as_view(), + name="external-appreciation-create", + ), + path( + "appreciations/list/", + ExternalAppreciationRetrieveView.as_view(), + name="external-appreciation-list", + ), + path( + "appreciations//", + ExternalAppreciationRetrieveView.as_view(), + name="external-appreciation-detail", + ), + # Suggestions + path( + "suggestions/", + ExternalSuggestionCreateView.as_view(), + name="external-suggestion-create", + ), + path( + "suggestions/list/", + ExternalSuggestionRetrieveView.as_view(), + name="external-suggestion-list", + ), + path( + "suggestions//", + ExternalSuggestionRetrieveView.as_view(), + name="external-suggestion-detail", + ), + # Doctor Ratings + path( + "doctor-ratings/", + ExternalDoctorRatingCreateView.as_view(), + name="external-doctor-rating-create", + ), +] diff --git a/apps/notifications/services.py b/apps/notifications/services.py index 7b03061..7e94bf1 100644 --- a/apps/notifications/services.py +++ b/apps/notifications/services.py @@ -18,6 +18,25 @@ from .models import NotificationLog logger = logging.getLogger(__name__) +def get_email_header_html(): + logo_url = getattr(settings, "EMAIL_LOGO_URL", f"{settings.STATIC_URL}img/HH_P_V_Logo(hospital)_.png") + if not logo_url.startswith("http"): + logo_url = f"https://{settings.ALLOWED_HOSTS[0]}{logo_url}" if getattr(settings, "ALLOWED_HOSTS", []) else logo_url + return f"""
+ + + + + +
+ Al Hammadi + + مستشفى الحمادي +
+
+
 
""" + + class NotificationService: """ Unified notification service for all channels. diff --git a/apps/notifications/settings_service.py b/apps/notifications/settings_service.py index b7b9a59..b613968 100644 --- a/apps/notifications/settings_service.py +++ b/apps/notifications/settings_service.py @@ -197,34 +197,21 @@ class NotificationServiceWithSettings: Send explanation request notification to staff. Respects hospital notification settings. """ - from .services import NotificationService + from .services import NotificationService, get_email_header_html - # Build HTML with navy theme staff_name = getattr(complaint.assigned_to, 'get_full_name', 'Staff Member')() if hasattr(complaint, 'assigned_to') and complaint.assigned_to else 'Staff Member' html_message = f""" - - - - -
-
-

Department Response Requested

-
-
-

Dear {staff_name},

-

A department response has been requested for complaint #{complaint.id}.

-

Title: {getattr(complaint, 'title', 'N/A')}

-

Deadline: Please submit as soon as possible

- {f'

Note: {custom_message}

' if custom_message else ''} -
+
+ {get_email_header_html()} +
+

Department Response Requested

+

Dear {staff_name},

+

A department response has been requested for complaint #{complaint.id}.

+

Title: {getattr(complaint, 'title', 'N/A')}

+

Deadline: Please submit as soon as possible

+ {f'

Note: {custom_message}

' if custom_message else ''}
- - +
""" hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) @@ -288,31 +275,22 @@ class NotificationServiceWithSettings: if recipient_type == "department_email": if department.email: html_message = f""" - - - -
-

New Inquiry Assigned to Your Department

-
-

Dear {department.get_localized_name()} Team,

-

A new inquiry has been assigned to the {department.get_localized_name()} department for response.

-
- Reference: {inquiry.reference_number}
- Subject: {inquiry.subject}
- Category: {inquiry.get_category_display()}
- Priority: {inquiry.get_priority_display()} -
- {note_html} -

Please log in to review and respond to this inquiry.

+
+ {get_email_header_html()} +
+

New Inquiry Assigned to Your Department

+

Dear {department.get_localized_name()} Team,

+

A new inquiry has been assigned to the {department.get_localized_name()} department for response.

+
+ Reference: {inquiry.reference_number}
+ Subject: {inquiry.subject}
+ Category: {inquiry.get_category_display()}
+ Priority: {inquiry.get_priority_display()}
+ {note_html} +

Please log in to review and respond to this inquiry.

- +
""" NotificationService.send_email( email=department.email, @@ -324,39 +302,27 @@ class NotificationServiceWithSettings: else: respondents = User.objects.filter( is_active=True, - department=department, - ).filter( - models.Q(groups__name="Champion") - | models.Q(groups__name="Department Manager") - ).distinct() + staff_profile__champion_departments=department, + ) for user in respondents: html_message = f""" - - - -
-

New Inquiry Assigned to Your Department

-
-

Dear {user.get_full_name()},

-

A new inquiry has been assigned to the {department.get_localized_name()} department for response.

-
- Reference: {inquiry.reference_number}
- Subject: {inquiry.subject}
- Category: {inquiry.get_category_display()}
- Priority: {inquiry.get_priority_display()} -
- {note_html} -

Please log in to review and respond to this inquiry.

+
+ {get_email_header_html()} +
+

New Inquiry Assigned to Your Department

+

Dear {user.get_full_name()},

+

A new inquiry has been assigned to the {department.get_localized_name()} department for response.

+
+ Reference: {inquiry.reference_number}
+ Subject: {inquiry.subject}
+ Category: {inquiry.get_category_display()}
+ Priority: {inquiry.get_priority_display()}
+ {note_html} +

Please log in to review and respond to this inquiry.

- +
""" if user.email: @@ -416,31 +382,22 @@ class NotificationServiceWithSettings: if recipient_type == "department_email": if department.email: html_message = f""" - - - -
-

New Observation Assigned to Your Department

-
-

Dear {department.get_localized_name()} Team,

-

A new observation has been assigned to the {department.get_localized_name()} department for response.

-
- Tracking: {tracking}
- Title: {observation.title or 'N/A'}
- Severity: {observation.get_severity_display()}
- Category: {observation.category.name_en if observation.category else 'N/A'} -
- {note_html} -

Please log in to review and respond to this observation.

+
+ {get_email_header_html()} +
+

New Observation Assigned to Your Department

+

Dear {department.get_localized_name()} Team,

+

A new observation has been assigned to the {department.get_localized_name()} department for response.

+
+ Tracking: {tracking}
+ Title: {observation.title or 'N/A'}
+ Severity: {observation.get_severity_display()}
+ Category: {observation.category.name_en if observation.category else 'N/A'}
+ {note_html} +

Please log in to review and respond to this observation.

- +
""" NotificationService.send_email( email=department.email, @@ -452,39 +409,27 @@ class NotificationServiceWithSettings: else: respondents = User.objects.filter( is_active=True, - department=department, - ).filter( - models.Q(groups__name="Champion") - | models.Q(groups__name="Department Manager") - ).distinct() + staff_profile__champion_departments=department, + ) for user in respondents: html_message = f""" - - - -
-

New Observation Assigned to Your Department

-
-

Dear {user.get_full_name()},

-

A new observation has been assigned to the {department.get_localized_name()} department for response.

-
- Tracking: {tracking}
- Title: {observation.title or 'N/A'}
- Severity: {observation.get_severity_display()}
- Category: {observation.category.name_en if observation.category else 'N/A'} -
- {note_html} -

Please log in to review and respond to this observation.

+
+ {get_email_header_html()} +
+

New Observation Assigned to Your Department

+

Dear {user.get_full_name()},

+

A new observation has been assigned to the {department.get_localized_name()} department for response.

+
+ Tracking: {tracking}
+ Title: {observation.title or 'N/A'}
+ Severity: {observation.get_severity_display()}
+ Category: {observation.category.name_en if observation.category else 'N/A'}
+ {note_html} +

Please log in to review and respond to this observation.

- +
""" if user.email: @@ -727,32 +672,19 @@ class NotificationServiceWithSettings: # Render HTML template with navy theme html_message = f""" - - - - -
-
-

Complaint Assigned to You

-
-
-

Dear {context['assignee_name']},

-

A complaint has been assigned to you for review and response.

-
- Complaint: #{context['complaint_id']} - {context['complaint_title']}
- Department: {context['department']} -
-

Please review and provide your explanation.

+
+ {get_email_header_html()} +
+

Complaint Assigned to You

+

Dear {context['assignee_name']},

+

A complaint has been assigned to you for review and response.

+
+ Complaint: #{context['complaint_id']} - {context['complaint_title']}
+ Department: {context['department']}
+

Please review and provide your explanation.

- - +
""" hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) @@ -906,32 +838,19 @@ class NotificationServiceWithSettings: @staticmethod def send_complaint_status_changed(recipient_email, complaint, old_status, new_status): """Send notification when complaint status changes""" - from .services import NotificationService + from .services import NotificationService, get_email_header_html - # Build HTML with navy theme html_message = f""" - - - - -
-
-

Complaint Status Updated

-
-
-

Dear Stakeholder,

-

The status of complaint #{complaint.id} has been updated.

-

Previous Status: {old_status}

-

New Status: {new_status}

-
+
+ {get_email_header_html()} +
+

Complaint Status Updated

+

Dear Stakeholder,

+

The status of complaint #{complaint.id} has been updated.

+

Previous Status: {old_status}

+

New Status: {new_status}

- - +
""" hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) diff --git a/apps/notifications/views.py b/apps/notifications/views.py index 31a846f..535648a 100644 --- a/apps/notifications/views.py +++ b/apps/notifications/views.py @@ -394,13 +394,23 @@ def test_notification(request, hospital_id=None): settings = HospitalNotificationSettings.get_for_hospital(hospital.id) channel = request.POST.get("channel", "email") - from .services import NotificationService + from .services import NotificationService, get_email_header_html if channel == "email" and request.user.email: NotificationService.send_email( email=request.user.email, subject="PX360 Test Notification", message="This is a test notification from PX360.\n\nIf you received this, your email notifications are working correctly.", + html_message=f""" +
+ {get_email_header_html()} +
+

Test Notification

+

This is a test notification from PX360.

+

If you received this, your email notifications are working correctly.

+
+
+""", metadata={"test": True, "hospital_id": str(hospital.id)}, ) messages.success(request, f"Test email sent to {request.user.email}") diff --git a/apps/observations/admin.py b/apps/observations/admin.py index d9e7535..eed2e08 100644 --- a/apps/observations/admin.py +++ b/apps/observations/admin.py @@ -108,7 +108,7 @@ class ObservationAdmin(admin.ModelAdmin): fieldsets = ( ("Tracking", {"fields": ("tracking_code", "status")}), ("Content", {"fields": ("category", "sub_category", "title", "description", "description_en", "severity")}), - ("Location & Time", {"fields": ("location_text", "location", "main_section", "subsection", "incident_datetime")}), + ("Location & Time", {"fields": ("location_text", "legacy_location", "legacy_main_section", "legacy_subsection", "section", "incident_datetime")}), ( "Reporter Information", { diff --git a/apps/observations/forms.py b/apps/observations/forms.py index 05f9c78..9183c63 100644 --- a/apps/observations/forms.py +++ b/apps/observations/forms.py @@ -8,7 +8,8 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.accounts.models import User -from apps.organizations.models import Department +from apps.organizations.models import Area, Department, Hospital, LocationType +from apps.organizations.models import Section as OrgSection from .models import ( Observation, @@ -17,7 +18,6 @@ from .models import ( ObservationNote, ObservationSeverity, ObservationStatus, - ObservationSubCategory, ) @@ -67,7 +67,8 @@ class ObservationPublicForm(forms.ModelForm): model = Observation fields = [ "hospital", - "category", + "location_type", + "area", "title", "description", "location_text", @@ -78,11 +79,17 @@ class ObservationPublicForm(forms.ModelForm): "reporter_email", ] widgets = { - "category": forms.Select( + "location_type": forms.Select( attrs={ "class": "form-select", } ), + "area": forms.Select( + attrs={ + "class": "form-select", + "id": "area_select", + } + ), "title": forms.TextInput( attrs={ "class": "form-control", @@ -137,11 +144,20 @@ class ObservationPublicForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Only show active categories - self.fields["category"].queryset = ObservationCategory.objects.filter(is_active=True).order_by( - "sort_order", "name_en" - ) - self.fields["category"].empty_label = "Select a category (optional)" + self.fields["location_type"].required = True + self.fields["location_type"].empty_label = None + self.fields["location_type"].choices = [("", "Select Location Type")] + list(LocationType.choices) + self.fields["area"].queryset = Area.objects.none() + self.fields["area"].required = False + self.fields["area"].empty_label = "Select Area (optional)" + + hospital_id = self.data.get("hospital") or self.initial.get("hospital") + location_type = self.data.get("location_type") or self.initial.get("location_type") + if hospital_id: + qs = Area.objects.filter(hospital_id=hospital_id, status="active") + if location_type: + qs = qs.filter(location_type=location_type) + self.fields["area"].queryset = qs.order_by("name_en") # Set default incident datetime to now if not self.instance.pk: @@ -165,68 +181,41 @@ class ObservationPublicForm(forms.ModelForm): class ObservationInternalForm(forms.ModelForm): """ Internal form for authenticated staff to create observations. - - Differences from ObservationPublicForm: - - No honeypot field (authenticated users) - - No reporter fields (auto-filled from request.user) - - Includes assignment fields (department, assignee) - - Source field for PXSource selection """ - attachments = forms.FileField( - required=False, - widget=forms.FileInput( - attrs={ - "accept": ".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx", - "class": "form-control", - } - ), - help_text="Upload files (max 10MB each). Allowed: images, PDF, Word, Excel.", - ) - - description_en = forms.CharField( - required=False, - widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": "English description of the observation..."}), - help_text="Full English version of the observation description.", + hospital = forms.ModelChoiceField( + label=_("Hospital"), + queryset=Hospital.objects.filter(status="active"), + empty_label=_("Select Hospital"), + required=True, + widget=forms.Select(attrs={"class": "form-select", "id": "hospitalSelect"}), ) class Meta: model = Observation fields = [ - "category", - "sub_category", - "title", + "hospital", + "location_type", + "area", "description", - "description_en", - "location_text", - "location", - "main_section", - "subsection", + "section", "incident_datetime", "patient_file_number", "assigned_department", "assigned_to", - "person_noted", - "department_noted", - "communication_method", - "communication_datetime", "px_source", ] widgets = { - "category": forms.Select( + "location_type": forms.Select( attrs={ "class": "form-select", + "id": "locationTypeSelect", } ), - "sub_category": forms.Select( + "area": forms.Select( attrs={ "class": "form-select", - } - ), - "title": forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "Brief title (optional)", + "id": "areaSelect", } ), "description": forms.Textarea( @@ -236,19 +225,6 @@ class ObservationInternalForm(forms.ModelForm): "placeholder": "Please describe what you observed in detail...", } ), - "description_en": forms.Textarea( - attrs={ - "class": "form-control", - "rows": 5, - "placeholder": "English description...", - } - ), - "location_text": forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "e.g., Building A, Floor 2, Room 205", - } - ), "incident_datetime": forms.DateTimeInput( attrs={ "class": "form-control", @@ -261,24 +237,6 @@ class ObservationInternalForm(forms.ModelForm): "placeholder": "Patient MRN / file number", } ), - "person_noted": forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "Person who was informed about this", - } - ), - "communication_method": forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "e.g., extension, mobile, in-person", - } - ), - "communication_datetime": forms.DateTimeInput( - attrs={ - "class": "form-control", - "type": "datetime-local", - } - ), "assigned_department": forms.Select( attrs={ "class": "form-select", @@ -289,81 +247,55 @@ class ObservationInternalForm(forms.ModelForm): "class": "form-select", } ), - "department_noted": forms.Select( - attrs={ - "class": "form-select", - } - ), } - location = forms.ModelChoiceField( - label=_("Location"), - queryset=None, - empty_label=_("Select Location"), - required=False, - widget=forms.Select(attrs={"class": "form-select", "id": "locationSelect", "data-action": "load-sections"}), - ) - - main_section = forms.ModelChoiceField( + section = forms.ModelChoiceField( label=_("Section"), queryset=None, empty_label=_("Select Section"), required=False, - widget=forms.Select( - attrs={"class": "form-select", "id": "mainSectionSelect", "data-action": "load-subsections"} - ), - ) - - subsection = forms.ModelChoiceField( - label=_("Subsection"), - queryset=None, - empty_label=_("Select Subsection"), - required=False, - widget=forms.Select(attrs={"class": "form-select", "id": "subsectionSelect"}), + widget=forms.Select(attrs={"class": "form-select", "id": "sectionSelect"}), ) def __init__(self, *args, request=None, **kwargs): + self.request = request + self.user = request.user if request else None super().__init__(*args, **kwargs) - from apps.organizations.models import Location, MainSection, SubSection from apps.px_sources.models import PXSource - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() - self.fields["location"].queryset = Location.active_locations() + self.fields["section"].queryset = OrgSection.objects.none() + self.fields["location_type"].required = True + self.fields["location_type"].choices = [("", "Select Location Type")] + list(LocationType.choices) + self.fields["area"].queryset = Area.objects.none() + self.fields["area"].required = False + self.fields["area"].empty_label = "Select Area (optional)" - location_id = self.data.get("location") or self.initial.get("location") - if location_id: - available_sections = ( - SubSection.objects.filter(location_id=location_id) - .values_list("main_section_id", flat=True) - .distinct() - ) - self.fields["main_section"].queryset = MainSection.objects.filter( - id__in=available_sections + hospital = None + if self.user: + hospital = getattr(self.user, 'hospital', None) + + if hospital: + self.fields["hospital"].initial = hospital + self.fields["hospital"].queryset = Hospital.objects.filter(status="active") + + hospital_id = self.data.get("hospital") or (str(hospital.id) if hospital else "") + if not hospital_id and self.initial.get("hospital"): + hospital_id = str(self.initial["hospital"].id) if hasattr(self.initial["hospital"], "id") else str(self.initial["hospital"]) + + location_type = self.data.get("location_type") or self.initial.get("location_type") + if hospital_id: + qs = Area.objects.filter(hospital_id=hospital_id, status="active") + if location_type: + qs = qs.filter(location_type=location_type) + self.fields["area"].queryset = qs.order_by("name_en") + + department_id = self.data.get("assigned_department") or self.initial.get("assigned_department") + if department_id: + self.fields["section"].queryset = OrgSection.objects.filter( + department_id=department_id ).order_by("name_en") - section_id = self.data.get("main_section") or self.initial.get("main_section") - if section_id: - self.fields["subsection"].queryset = SubSection.objects.filter( - location_id=location_id, main_section_id=section_id - ).order_by("name_en") - - self.fields["category"].queryset = ObservationCategory.objects.filter(is_active=True).order_by( - "sort_order", "name_en" - ) - self.fields["category"].empty_label = "Select a category (optional)" - - # Sub-category filtered by selected category - self.fields["sub_category"].queryset = ObservationSubCategory.objects.none() - self.fields["sub_category"].empty_label = "Select a sub-category (optional)" - category_id = self.data.get("category") or self.initial.get("category") - if category_id: - self.fields["sub_category"].queryset = ObservationSubCategory.objects.filter( - category_id=category_id, is_active=True - ).order_by("sort_order", "name_en") - - # Load active PX sources for optional selection self.fields["px_source"].queryset = PXSource.objects.filter(is_active=True).order_by("name_en") self.fields["px_source"].empty_label = "Select source (optional)" self.fields["px_source"].required = False @@ -371,31 +303,22 @@ class ObservationInternalForm(forms.ModelForm): if not self.instance.pk: self.initial["incident_datetime"] = timezone.now().strftime("%Y-%m-%dT%H:%M") - if request: - user = request.user - hospital = user.hospital - if hospital: - self.fields["assigned_department"].queryset = Department.objects.filter( - hospital=hospital, status="active" - ).order_by("name") - else: - self.fields["assigned_department"].queryset = Department.objects.filter(status="active").order_by( - "name" - ) - self.fields["assigned_department"].empty_label = "Select department (optional)" + if hospital_id: + self.fields["assigned_department"].queryset = Department.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name_en") + else: + self.fields["assigned_department"].queryset = Department.objects.filter(status="active").order_by("name_en") + self.fields["assigned_department"].empty_label = "Select department (optional)" - if hospital: - from apps.core.utils import get_assignable_users - self.fields["assigned_to"].queryset = get_assignable_users(hospital) - else: - self.fields["assigned_to"].queryset = User.objects.filter(is_active=True).order_by( - "first_name", "last_name" - ) - self.fields["assigned_to"].empty_label = "Select assignee (optional)" - - # Department noted uses same queryset as assigned_department - self.fields["department_noted"].queryset = self.fields["assigned_department"].queryset - self.fields["department_noted"].empty_label = "Select department (optional)" + if hospital: + from apps.core.utils import get_assignable_users + self.fields["assigned_to"].queryset = get_assignable_users(hospital) + else: + self.fields["assigned_to"].queryset = User.objects.filter(is_active=True).order_by( + "first_name", "last_name" + ) + self.fields["assigned_to"].empty_label = "Select assignee (optional)" def clean_description(self): description = self.cleaned_data.get("description", "") @@ -816,33 +739,32 @@ class PublicObservationForm(forms.ModelForm): required=True, widget=forms.Select(attrs={"class": "form-control", "id": "hospital_select"}), ) - category = forms.ModelChoiceField( - label=_("Category"), - queryset=None, - empty_label=_("Select Category"), - required=False, - widget=forms.Select(attrs={"class": "form-control"}), + location_type = forms.ChoiceField( + label=_("Location Type"), + choices=[("", _("Select Location Type"))] + list(LocationType.choices), + required=True, + widget=forms.Select(attrs={"class": "form-control", "id": "location_type_select"}), ) - location = forms.ModelChoiceField( - label=_("Location"), - queryset=None, - empty_label=_("Select Location"), + department = forms.ModelChoiceField( + label=_("Department (Optional)"), + queryset=Department.objects.none(), + empty_label=_("Select Department"), required=False, - widget=forms.Select(attrs={"class": "form-control", "id": "location_select", "data-action": "load-sections"}), + widget=forms.Select(attrs={"class": "form-control", "id": "department_select"}), ) - main_section = forms.ModelChoiceField( - label=_("Section"), - queryset=None, + section = forms.ModelChoiceField( + label=_("Section (Optional)"), + queryset=OrgSection.objects.none(), empty_label=_("Select Section"), required=False, - widget=forms.Select(attrs={"class": "form-control", "id": "main_section_select", "data-action": "load-subsections"}), + widget=forms.Select(attrs={"class": "form-control", "id": "section_select"}), ) - subsection = forms.ModelChoiceField( - label=_("Subsection"), - queryset=None, - empty_label=_("Select Subsection"), + area = forms.ModelChoiceField( + label=_("Area (Optional)"), + queryset=Area.objects.none(), + empty_label=_("Select Area"), required=False, - widget=forms.Select(attrs={"class": "form-control", "id": "subsection_select"}), + widget=forms.Select(attrs={"class": "form-control", "id": "area_select"}), ) title = forms.CharField( label=_("Title"), @@ -863,10 +785,10 @@ class PublicObservationForm(forms.ModelForm): "reporter_phone", "reporter_email", "hospital", - "category", - "location", - "main_section", - "subsection", + "location_type", + "area", + "department", + "section", "title", "description", "description_en", @@ -874,33 +796,35 @@ class PublicObservationForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - from apps.organizations.models import Hospital, Location, MainSection, SubSection + from apps.organizations.models import Hospital self.fields["hospital"].queryset = Hospital.objects.filter(status="active").order_by("name") - self.fields["category"].queryset = ObservationCategory.objects.filter(is_active=True).order_by("name_en") - self.fields["location"].queryset = Location.active_locations() - self.fields["main_section"].queryset = MainSection.objects.none() - self.fields["subsection"].queryset = SubSection.objects.none() + self.fields["section"].queryset = OrgSection.objects.none() + self.fields["area"].queryset = Area.objects.none() - location_id = None - if "location" in self.initial: - location_id = self.initial["location"] - elif "location" in self.data: - location_id = self.data["location"] + hospital_id = None + if "hospital" in self.initial: + hospital_id = self.initial["hospital"] + elif "hospital" in self.data: + hospital_id = self.data["hospital"] - if location_id: - available_sections = ( - SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() - ) - self.fields["main_section"].queryset = MainSection.objects.filter(id__in=available_sections).order_by("name_en") + location_type = self.data.get("location_type") or self.initial.get("location_type") + if hospital_id: + self.fields["department"].queryset = Department.objects.filter( + hospital_id=hospital_id, status="active" + ).order_by("name") + area_qs = Area.objects.filter(hospital_id=hospital_id, status="active") + if location_type: + area_qs = area_qs.filter(location_type=location_type) + self.fields["area"].queryset = area_qs.order_by("name_en") - section_id = None - if "main_section" in self.initial: - section_id = self.initial["main_section"] - elif "main_section" in self.data: - section_id = self.data["main_section"] + department_id = None + if "department" in self.initial: + department_id = self.initial["department"] + elif "department" in self.data: + department_id = self.data["department"] - if section_id: - self.fields["subsection"].queryset = SubSection.objects.filter( - location_id=location_id, main_section_id=section_id - ).order_by("name_en") + if department_id: + self.fields["section"].queryset = OrgSection.objects.filter( + department_id=department_id, status="active" + ).order_by("name_en") diff --git a/apps/observations/migrations/0001_initial.py b/apps/observations/migrations/0001_initial.py index 7802b40..2eb5a1d 100644 --- a/apps/observations/migrations/0001_initial.py +++ b/apps/observations/migrations/0001_initial.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -196,8 +196,8 @@ class Migration(migrations.Migration): ('department_responded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='department_observation_responses', to=settings.AUTH_USER_MODEL)), ('dept_response_accepted_by', models.ForeignKey(blank=True, help_text='User who reviewed the department response', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_observation_dept_responses', to=settings.AUTH_USER_MODEL)), ('hospital', models.ForeignKey(blank=True, help_text='Hospital where observation was made', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='organizations.hospital')), - ('location', models.ForeignKey(blank=True, help_text='Location where the observation was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.location')), - ('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.mainsection')), + ('location', models.ForeignKey(blank=True, help_text='Location where the observation was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacylocation')), + ('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacymainsection')), ('monthly_follow_up_completed_by', models.ForeignKey(blank=True, help_text='User who completed the monthly follow-up', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='completed_observation_followups', to=settings.AUTH_USER_MODEL)), ], options={ diff --git a/apps/observations/migrations/0002_initial.py b/apps/observations/migrations/0002_initial.py index 4d72fe3..12717c2 100644 --- a/apps/observations/migrations/0002_initial.py +++ b/apps/observations/migrations/0002_initial.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): dependencies = [ ('complaints', '0003_initial'), ('observations', '0001_initial'), - ('organizations', '0001_initial'), + ('organizations', '0004_legacylocation_legacymainsection_and_more'), ('px_sources', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -36,7 +36,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='observation', name='subsection', - field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.subsection'), + field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacysubsection'), ), migrations.AddField( model_name='observation', diff --git a/apps/observations/migrations/0003_rename_observation_note_related_name.py b/apps/observations/migrations/0003_rename_observation_note_related_name.py new file mode 100644 index 0000000..15dfe93 --- /dev/null +++ b/apps/observations/migrations/0003_rename_observation_note_related_name.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-05-12 18:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0002_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='observationnote', + name='observation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observation_notes', to='observations.observation'), + ), + ] diff --git a/apps/observations/migrations/0004_add_sent_to_department_fields.py b/apps/observations/migrations/0004_add_sent_to_department_fields.py new file mode 100644 index 0000000..600d939 --- /dev/null +++ b/apps/observations/migrations/0004_add_sent_to_department_fields.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.1 on 2026-05-16 16:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0003_rename_observation_note_related_name'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='sent_to_department', + field=models.BooleanField(default=False, help_text='Whether this observation has been sent to the assigned department for visibility'), + ), + migrations.AddField( + model_name='observation', + name='sent_to_department_at', + field=models.DateTimeField(blank=True, help_text='When the observation was sent to the assigned department', null=True), + ), + ] diff --git a/apps/observations/migrations/0005_data_sent_to_department_backfill.py b/apps/observations/migrations/0005_data_sent_to_department_backfill.py new file mode 100644 index 0000000..bd0e7dd --- /dev/null +++ b/apps/observations/migrations/0005_data_sent_to_department_backfill.py @@ -0,0 +1,37 @@ +from django.db import migrations, models +from django.utils import timezone + + +def backfill_sent_to_department(apps, schema_editor): + Observation = apps.get_model("observations", "Observation") + + Observation.objects.filter( + assigned_department__isnull=False + ).update(sent_to_department=True) + Observation.objects.filter( + sent_to_department=True, + sent_to_department_at__isnull=True, + forwarded_to_dept_at__isnull=False, + ).update(sent_to_department_at=models.F("forwarded_to_dept_at")) + Observation.objects.filter( + sent_to_department=True, sent_to_department_at__isnull=True + ).update(sent_to_department_at=timezone.now()) + + +def reverse_backfill(apps, schema_editor): + Observation = apps.get_model("observations", "Observation") + + Observation.objects.all().update( + sent_to_department=False, sent_to_department_at=None + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("observations", "0004_add_sent_to_department_fields"), + ] + + operations = [ + migrations.RunPython(backfill_sent_to_department, reverse_backfill), + ] diff --git a/apps/observations/migrations/0006_remove_observation_location_and_more.py b/apps/observations/migrations/0006_remove_observation_location_and_more.py new file mode 100644 index 0000000..1b7cfe9 --- /dev/null +++ b/apps/observations/migrations/0006_remove_observation_location_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0005_data_sent_to_department_backfill'), + ] + + operations = [ + migrations.RenameField( + model_name='observation', + old_name='location', + new_name='legacy_location', + ), + migrations.RenameField( + model_name='observation', + old_name='main_section', + new_name='legacy_main_section', + ), + migrations.RenameField( + model_name='observation', + old_name='subsection', + new_name='legacy_subsection', + ), + ] diff --git a/apps/observations/migrations/0007_observation_legacy_location_and_more.py b/apps/observations/migrations/0007_observation_legacy_location_and_more.py new file mode 100644 index 0000000..0b44778 --- /dev/null +++ b/apps/observations/migrations/0007_observation_legacy_location_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0006_remove_observation_location_and_more'), + ('organizations', '0008_rename_orgsubsection_to_section_add_champion'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='section', + field=models.ForeignKey(blank=True, help_text='Section within department', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations_new', to='organizations.Section'), + ), + + migrations.AlterField( + model_name='observation', + name='status', + field=models.CharField(choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate'), ('contacted', 'Contacted')], db_index=True, default='new', max_length=25), + ), + migrations.AlterField( + model_name='observationstatuslog', + name='from_status', + field=models.CharField(blank=True, choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate'), ('contacted', 'Contacted')], max_length=25), + ), + migrations.AlterField( + model_name='observationstatuslog', + name='to_status', + field=models.CharField(choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate'), ('contacted', 'Contacted')], max_length=25), + ), + ] diff --git a/apps/observations/migrations/0008_alter_observation_legacy_location_and_more.py b/apps/observations/migrations/0008_alter_observation_legacy_location_and_more.py new file mode 100644 index 0000000..fdfdd1d --- /dev/null +++ b/apps/observations/migrations/0008_alter_observation_legacy_location_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0007_observation_legacy_location_and_more'), + ('organizations', '0005_alter_legacylocation_table_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='observation', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text='Location where the observation was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='observation', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='observation', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text='Specific subsection', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/observations/migrations/0009_remove_sub_subsection.py b/apps/observations/migrations/0009_remove_sub_subsection.py new file mode 100644 index 0000000..735a273 --- /dev/null +++ b/apps/observations/migrations/0009_remove_sub_subsection.py @@ -0,0 +1,10 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0008_alter_observation_legacy_location_and_more'), + ] + + operations = [] diff --git a/apps/observations/migrations/0010_observation_location_type_and_more.py b/apps/observations/migrations/0010_observation_location_type_and_more.py new file mode 100644 index 0000000..7013ab7 --- /dev/null +++ b/apps/observations/migrations/0010_observation_location_type_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-06-07 04:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0009_remove_sub_subsection'), + ('organizations', '0011_area_department_area'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='location_type', + field=models.CharField(blank=True, choices=[('OP', 'Outpatient'), ('IP', 'Inpatient'), ('ER', 'Emergency'), ('GENERAL', 'General')], help_text='Where the observation occurred (OP/IP/ER/GO)', max_length=20), + ), + migrations.AlterField( + model_name='observation', + name='legacy_location', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacylocation'), + ), + migrations.AlterField( + model_name='observation', + name='legacy_main_section', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacymainsection'), + ), + migrations.AlterField( + model_name='observation', + name='legacy_subsection', + field=models.ForeignKey(blank=True, help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.legacysubsection'), + ), + ] diff --git a/apps/observations/migrations/0011_observation_area.py b/apps/observations/migrations/0011_observation_area.py new file mode 100644 index 0000000..710487f --- /dev/null +++ b/apps/observations/migrations/0011_observation_area.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0.1 on 2026-06-07 14:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0010_observation_location_type_and_more'), + ('organizations', '0011_area_department_area'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='area', + field=models.ForeignKey(blank=True, help_text='Physical area within the hospital where the observation was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.area'), + ), + ] diff --git a/apps/observations/migrations/0012_alter_observation_tracking_code.py b/apps/observations/migrations/0012_alter_observation_tracking_code.py new file mode 100644 index 0000000..bb87257 --- /dev/null +++ b/apps/observations/migrations/0012_alter_observation_tracking_code.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-06-14 10:50 + +import apps.observations.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0011_observation_area'), + ] + + operations = [ + migrations.AlterField( + model_name='observation', + name='tracking_code', + field=models.CharField(default=apps.observations.models.generate_tracking_code, help_text='Unique code for tracking this observation (unified: OBS-YYYYMM-HOSP-NNNN)', max_length=50, unique=True), + ), + ] diff --git a/apps/observations/migrations/0013_simplify_statuses.py b/apps/observations/migrations/0013_simplify_statuses.py new file mode 100644 index 0000000..d608178 --- /dev/null +++ b/apps/observations/migrations/0013_simplify_statuses.py @@ -0,0 +1,46 @@ +# Generated by Django 6.0.1 on 2026-06-14 10:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0012_alter_observation_tracking_code'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='contact_status', + field=models.CharField(blank=True, choices=[('not_contacted', 'Not Contacted'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted - No Response')], db_index=True, default='not_contacted', max_length=25), + ), + migrations.AddField( + model_name='observation', + name='contact_status_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='observation', + name='contact_status_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='observation', + name='status', + field=models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed')], db_index=True, default='open', max_length=25), + ), + migrations.AlterField( + model_name='observationstatuslog', + name='from_status', + field=models.CharField(blank=True, choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed')], max_length=25), + ), + migrations.AlterField( + model_name='observationstatuslog', + name='to_status', + field=models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed')], max_length=25), + ), + ] diff --git a/apps/observations/migrations/0014_map_statuses.py b/apps/observations/migrations/0014_map_statuses.py new file mode 100644 index 0000000..7ba91c1 --- /dev/null +++ b/apps/observations/migrations/0014_map_statuses.py @@ -0,0 +1,67 @@ +""" +Data migration to map old observation/inquiry statuses to simplified ones. +""" +from django.db import migrations + + +def map_observation_statuses(apps, schema_editor): + Observation = apps.get_model("observations", "Observation") + + # Map old statuses to new ones + set contact_status + status_mapping = { + "new": ("open", "not_contacted"), + "triaged": ("in_progress", "not_contacted"), + "assigned": ("in_progress", "not_contacted"), + "contacted": ("in_progress", "contacted"), + "in_progress": ("in_progress", "not_contacted"), + "resolved": ("resolved", "not_contacted"), + "closed": ("closed", "not_contacted"), + "rejected": ("closed", "not_contacted"), + "duplicate": ("closed", "not_contacted"), + } + + for old_status, (new_status, contact_status) in status_mapping.items(): + Observation.objects.filter(status=old_status).update( + status=new_status, contact_status=contact_status + ) + + +def map_inquiry_statuses(apps, schema_editor): + Inquiry = apps.get_model("complaints", "Inquiry") + + # Map old statuses to new ones + set contact_status + status_mapping = { + "open": ("open", "not_contacted"), + "in_progress": ("in_progress", "not_contacted"), + "contacted": ("in_progress", "contacted"), + "contacted_no_response": ("in_progress", "contacted_no_response"), + "resolved": ("resolved", "not_contacted"), + "closed": ("closed", "not_contacted"), + } + + for old_status, (new_status, contact_status) in status_mapping.items(): + Inquiry.objects.filter(status=old_status).update( + status=new_status, contact_status=contact_status + ) + + +def reverse_map_observation_statuses(apps, schema_editor): + # Reverse is best-effort; we can't perfectly reconstruct old statuses + pass + + +def reverse_map_inquiry_statuses(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("observations", "0013_simplify_statuses"), + ("complaints", "0018_simplify_statuses"), + ] + + operations = [ + migrations.RunPython(map_observation_statuses, reverse_map_observation_statuses), + migrations.RunPython(map_inquiry_statuses, reverse_map_inquiry_statuses), + ] diff --git a/apps/observations/migrations/0015_add_response_token.py b/apps/observations/migrations/0015_add_response_token.py new file mode 100644 index 0000000..3f193e4 --- /dev/null +++ b/apps/observations/migrations/0015_add_response_token.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-06-14 11:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0014_map_statuses'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='response_token', + field=models.CharField(blank=True, db_index=True, help_text='One-time token for department response link', max_length=100, null=True, unique=True), + ), + migrations.AddField( + model_name='observation', + name='response_token_sent_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='observation', + name='response_token_used', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/observations/models.py b/apps/observations/models.py index 1cda9f3..4b4bc7c 100644 --- a/apps/observations/models.py +++ b/apps/observations/models.py @@ -13,12 +13,13 @@ import secrets import string from django.conf import settings -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone from apps.core.models import SoftDeleteModel, TimeStampedModel, UUIDModel +from apps.organizations.models import LocationType def generate_tracking_code(): @@ -41,16 +42,18 @@ class ObservationSeverity(models.TextChoices): class ObservationStatus(models.TextChoices): """Observation status choices.""" - NEW = "new", "New" - TRIAGED = "triaged", "Triaged" - ASSIGNED = "assigned", "Assigned" + OPEN = "open", "Open" IN_PROGRESS = "in_progress", "In Progress" RESOLVED = "resolved", "Resolved" CLOSED = "closed", "Closed" - REJECTED = "rejected", "Rejected" - DUPLICATE = "duplicate", "Duplicate" - CONTACTED = "contacted", "Contacted" - CONTACTED_NO_RESPONSE = "contacted_no_response", "Contacted, No Response" + + +VALID_OBSERVATION_TRANSITIONS = { + ObservationStatus.OPEN: {ObservationStatus.IN_PROGRESS}, + ObservationStatus.IN_PROGRESS: {ObservationStatus.RESOLVED}, + ObservationStatus.RESOLVED: {ObservationStatus.CLOSED, ObservationStatus.IN_PROGRESS}, + ObservationStatus.CLOSED: {ObservationStatus.IN_PROGRESS}, +} class ObservationCategory(UUIDModel, TimeStampedModel): @@ -243,10 +246,10 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): # Tracking tracking_code = models.CharField( - max_length=20, + max_length=50, unique=True, default=generate_tracking_code, - help_text="Unique code for tracking this observation", + help_text="Unique code for tracking this observation (unified: OBS-YYYYMM-HOSP-NNNN)", ) # Classification @@ -310,29 +313,49 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): location_text = models.CharField( max_length=500, blank=True, help_text="Where the issue was observed (building, floor, room, etc.)" ) - location = models.ForeignKey( - "organizations.Location", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="observations", - help_text="Location where the observation was made", + location_type = models.CharField( + max_length=20, choices=LocationType.choices, blank=True, help_text="Where the observation occurred (OP/IP/ER/GO)" ) - main_section = models.ForeignKey( - "organizations.MainSection", + area = models.ForeignKey( + "organizations.Area", on_delete=models.SET_NULL, null=True, blank=True, related_name="observations", - help_text="Main section within the location", + help_text="Physical area within the hospital where the observation was made", ) - subsection = models.ForeignKey( - "organizations.SubSection", + legacy_location = models.ForeignKey( + "organizations.LegacyLocation", on_delete=models.SET_NULL, null=True, blank=True, related_name="observations", - help_text="Specific subsection", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + legacy_main_section = models.ForeignKey( + "organizations.LegacyMainSection", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="observations", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + legacy_subsection = models.ForeignKey( + "organizations.LegacySubSection", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="observations", + help_text="[DEPRECATED] Use 'department' and 'section' fields instead. This field is read-only and will be removed in a future release.", + ) + # New hierarchy (from 4th Version Excel) + section = models.ForeignKey( + "organizations.Section", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="observations_new", + help_text="Section within department", ) incident_datetime = models.DateTimeField(default=timezone.now, help_text="When the issue was observed") @@ -349,7 +372,27 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): # Status and workflow status = models.CharField( - max_length=25, choices=ObservationStatus.choices, default=ObservationStatus.NEW, db_index=True + max_length=25, choices=ObservationStatus.choices, default=ObservationStatus.OPEN, db_index=True + ) + + contact_status = models.CharField( + max_length=25, + choices=[ + ("not_contacted", "Not Contacted"), + ("contacted", "Contacted"), + ("contacted_no_response", "Contacted - No Response"), + ], + default="not_contacted", + blank=True, + db_index=True, + ) + contact_status_at = models.DateTimeField(null=True, blank=True) + contact_status_by = models.ForeignKey( + "accounts.User", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="+", ) # Organization (required for tenant isolation) @@ -477,6 +520,12 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): forwarded_to_dept_at = models.DateTimeField( null=True, blank=True, help_text="When the observation was sent to the department", ) + sent_to_department = models.BooleanField( + default=False, help_text="Whether this observation has been sent to the assigned department for visibility" + ) + sent_to_department_at = models.DateTimeField( + null=True, blank=True, help_text="When the observation was sent to the assigned department" + ) # Department response SLA tracking dept_response_sla_due_at = models.DateTimeField( @@ -497,6 +546,14 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): null=True, blank=True, help_text="When dept response was escalated to manager", ) + # Token-based department response + response_token = models.CharField( + max_length=100, blank=True, null=True, unique=True, db_index=True, + help_text="One-time token for department response link", + ) + response_token_used = models.BooleanField(default=False) + response_token_sent_at = models.DateTimeField(null=True, blank=True) + # Department response acceptance review dept_response_acceptance_status = models.CharField( max_length=20, @@ -574,6 +631,8 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): monthly_follow_up_notes = models.TextField(blank=True, help_text="Notes from monthly follow-up") + notes = GenericRelation("core.Note") + class Meta: ordering = ["-created_at"] indexes = [ @@ -591,11 +650,16 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): return f"{self.tracking_code} - {self.title or self.description[:50]}" def save(self, *args, **kwargs): - """Ensure tracking code is unique.""" - if not self.tracking_code: + """Generate a unified tracking code on creation (OBS-YYYYMM-HOSP-NNNN).""" + if self._state.adding and self.hospital_id: + from apps.core.reference import generate_reference + + self.tracking_code = generate_reference("OBS", self.hospital) + elif not self.tracking_code: self.tracking_code = generate_tracking_code() - # Ensure uniqueness + # Safety: ensure uniqueness (sequence-based codes are already unique, + # but guard the random fallback path). while Observation.objects.filter(tracking_code=self.tracking_code).exclude(pk=self.pk).exists(): self.tracking_code = generate_tracking_code() @@ -620,6 +684,29 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): return reverse("observations:observation_detail", kwargs={"pk": self.pk}) + def get_owner(self): + """ + Returns the owner of this observation. + Cascade: section(champion, supervisor, deputy_supervisor) + -> department(champion, deputy_manager, supervisor, + deputy_supervisor, manager_2nd, manager_3rd). + Returns: Staff instance or None. + """ + if self.section: + for role in ("champion", "supervisor", "deputy_supervisor"): + owner = getattr(self.section, role, None) + if owner: + return owner + if self.assigned_department: + dept = self.assigned_department + for role in ("champion", "deputy_manager", + "supervisor", "deputy_supervisor", + "manager_2nd", "manager_3rd"): + owner = getattr(dept, role, None) + if owner: + return owner + return None + def get_severity_color(self): """Get Bootstrap color class for severity.""" colors = { @@ -637,6 +724,7 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): "triaged": "info", "assigned": "info", "in_progress": "warning", + "contacted": "purple", "resolved": "success", "closed": "secondary", "rejected": "danger", @@ -686,10 +774,10 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): def is_active_status(self): """ Check if observation is in an active status (can be worked on). - Active statuses: new, triaged, assigned, in_progress - Inactive statuses: resolved, closed, rejected, duplicate + Active statuses: open, in_progress + Inactive statuses: resolved, closed """ - return self.status in ["new", "triaged", "assigned", "in_progress"] + return self.status in ["open", "in_progress"] @property def has_ai_analysis(self): @@ -797,7 +885,7 @@ class ObservationNote(UUIDModel, TimeStampedModel): Used by PX360 staff to add comments and updates. """ - observation = models.ForeignKey(Observation, on_delete=models.CASCADE, related_name="notes") + observation = models.ForeignKey(Observation, on_delete=models.CASCADE, related_name="observation_notes") note = models.TextField() diff --git a/apps/observations/services.py b/apps/observations/services.py index 6245dbe..d869239 100644 --- a/apps/observations/services.py +++ b/apps/observations/services.py @@ -28,6 +28,7 @@ from .models import ( ObservationNote, ObservationStatus, ObservationStatusLog, + VALID_OBSERVATION_TRANSITIONS, ) logger = logging.getLogger(__name__) @@ -46,10 +47,9 @@ class ObservationService: severity: str = "medium", category=None, title: str = "", + description_en: str = "", location_text: str = "", - location=None, - main_section=None, - subsection=None, + location_type: str = "", incident_datetime=None, reporter_staff_id: str = "", reporter_name: str = "", @@ -60,6 +60,8 @@ class ObservationService: attachments: list = None, hospital=None, assigned_department=None, + section=None, + area=None, source=None, source_legacy: str = "", ) -> Observation: @@ -72,9 +74,6 @@ class ObservationService: category: ObservationCategory instance (optional) title: Short title (optional) location_text: Location description (optional) - location: Location instance (optional) - main_section: MainSection instance (optional) - subsection: SubSection instance (optional) incident_datetime: When the incident occurred (optional, defaults to now) reporter_staff_id: Staff ID of reporter (optional) reporter_name: Name of reporter (optional) @@ -89,13 +88,12 @@ class ObservationService: """ observation = Observation.objects.create( description=description, + description_en=description_en, severity=severity, category=category, title=title, location_text=location_text, - location=location, - main_section=main_section, - subsection=subsection, + location_type=location_type if location_type else "", incident_datetime=incident_datetime or timezone.now(), reporter_staff_id=reporter_staff_id, reporter_name=reporter_name, @@ -105,13 +103,15 @@ class ObservationService: user_agent=user_agent, hospital=hospital, assigned_department=assigned_department, + section=section, + area=area, px_source=source, source=source_legacy, ) # Create initial status log ObservationStatusLog.objects.create( - observation=observation, from_status="", to_status=ObservationStatus.NEW, comment="Observation submitted" + observation=observation, from_status="", to_status=ObservationStatus.OPEN, comment="Observation submitted" ) # Handle attachments @@ -163,14 +163,18 @@ class ObservationService: """ old_status = observation.status - # Update observation + if old_status != new_status: + allowed = VALID_OBSERVATION_TRANSITIONS.get(old_status, set()) + if new_status not in allowed: + raise ValueError( + f"Invalid status transition from '{old_status}' to '{new_status}'. " + f"Allowed: {', '.join(sorted(allowed))}" + ) + observation.status = new_status # Handle status-specific updates - if new_status == ObservationStatus.TRIAGED: - observation.triaged_at = timezone.now() - observation.triaged_by = changed_by - elif new_status == ObservationStatus.IN_PROGRESS: + if new_status == ObservationStatus.IN_PROGRESS: if not observation.activated_at: observation.activated_at = timezone.now() if not observation.due_at: @@ -198,9 +202,7 @@ class ObservationService: ) # Send notifications based on status change - if new_status == ObservationStatus.ASSIGNED and observation.assigned_to: - ObservationService.notify_assignment(observation) - elif new_status in [ObservationStatus.RESOLVED, ObservationStatus.CLOSED]: + if new_status == ObservationStatus.RESOLVED or new_status == ObservationStatus.CLOSED: ObservationService.notify_resolution(observation) logger.info( @@ -241,10 +243,7 @@ class ObservationService: # Determine new status if not new_status: - if assigned_to: - new_status = ObservationStatus.ASSIGNED - else: - new_status = ObservationStatus.TRIAGED + new_status = ObservationStatus.IN_PROGRESS observation.save() @@ -671,14 +670,10 @@ Resolution Notes: return { "total": queryset.count(), - "new": status_dict.get("new", 0), - "triaged": status_dict.get("triaged", 0), - "assigned": status_dict.get("assigned", 0), + "open": status_dict.get("open", 0), "in_progress": status_dict.get("in_progress", 0), "resolved": status_dict.get("resolved", 0), "closed": status_dict.get("closed", 0), - "rejected": status_dict.get("rejected", 0), - "duplicate": status_dict.get("duplicate", 0), "anonymous_count": queryset.filter(Q(reporter_staff_id="") & Q(reporter_name="")).count(), "severity": severity_dict, "top_categories": list(category_counts), diff --git a/apps/observations/tasks.py b/apps/observations/tasks.py index 5984c03..2348db3 100644 --- a/apps/observations/tasks.py +++ b/apps/observations/tasks.py @@ -7,6 +7,7 @@ This module implements tasks for: - Escalation handling """ +import datetime import logging from celery import shared_task @@ -26,9 +27,7 @@ def check_overdue_observations(): from apps.observations.models import Observation, ObservationStatus active_statuses = [ - ObservationStatus.NEW, - ObservationStatus.TRIAGED, - ObservationStatus.ASSIGNED, + ObservationStatus.OPEN, ObservationStatus.IN_PROGRESS, ] active_observations = Observation.objects.filter( @@ -66,9 +65,7 @@ def send_observation_sla_reminders(): now = timezone.now() active_statuses = [ - ObservationStatus.NEW, - ObservationStatus.TRIAGED, - ObservationStatus.ASSIGNED, + ObservationStatus.OPEN, ObservationStatus.IN_PROGRESS, ] @@ -203,7 +200,7 @@ def schedule_monthly_followups(): scheduled = 0 for obs in resolved_observations: - obs.monthly_follow_up_due_at = obs.resolved_at + __import__("datetime").timedelta(days=30) + obs.monthly_follow_up_due_at = obs.resolved_at + datetime.timedelta(days=30) obs.save(update_fields=["monthly_follow_up_due_at"]) scheduled += 1 @@ -486,9 +483,7 @@ def check_overdue_observation_dept_responses(): escalation_delay = sla_config.dept_response_escalation_hours_overdue if escalation_delay > 0: - escalation_threshold = observation.dept_response_sla_due_at + __import__( - "datetime" - ).timedelta(hours=escalation_delay) + escalation_threshold = observation.dept_response_sla_due_at + datetime.timedelta(hours=escalation_delay) if now < escalation_threshold: continue @@ -501,40 +496,9 @@ def check_overdue_observation_dept_responses(): ): continue - if dept.manager and dept.manager.email: - try: - NotificationService.send_email( - email=dept.manager.email, - subject=f"ESCALATION: Observation {observation.tracking_code} - Department Response Overdue", - message=( - f"The department response for observation {observation.tracking_code} " - f"is overdue. The response deadline was " - f"{observation.dept_response_sla_due_at.strftime('%Y-%m-%d %H:%M')}. " - f"Please ensure the department submits a response immediately." - ), - related_object=observation, - ) - except Exception as e: - logger.error(f"Failed to send escalation email: {e}") - - observation.dept_response_escalated_at = now - observation.save(update_fields=["dept_response_escalated_at"]) - escalated_count += 1 - - ObservationStatusLog.objects.create( - observation=observation, - from_status="", - to_status=observation.status, - changed_by=None, - comment=f"Department response SLA escalated to {dept.get_localized_name()} manager", - ) - - ObservationNote.objects.create( - observation=observation, - note=f"Department response SLA escalated to {dept.get_localized_name()} manager", - created_by=None, - is_internal=True, - ) + # Auto-escalation disabled — manual escalation only + logger.info(f"Auto-escalation skipped for observation {observation.tracking_code} (disabled)") + continue if overdue_count > 0 or escalated_count > 0: logger.info( @@ -575,8 +539,8 @@ def send_observation_dept_response_reminders(): continue recipients = [] - if dept.respondent and dept.respondent.user and dept.respondent.user.email: - recipients.append(dept.respondent.user) + if dept.champion and dept.champion.user and dept.champion.user.email: + recipients.append(dept.champion.user) if not recipients: continue diff --git a/apps/observations/test_location_type.py b/apps/observations/test_location_type.py new file mode 100644 index 0000000..114f334 --- /dev/null +++ b/apps/observations/test_location_type.py @@ -0,0 +1,121 @@ +""" +Tests for observation location_type field and public form. +""" +from django.test import Client, TestCase +from django.urls import reverse + +from apps.observations.models import Observation, ObservationCategory, ObservationStatus +from apps.observations.services import ObservationService +from apps.organizations.models import Department, Hospital, LocationType, Section + + +class ObservationLocationTypeTests(TestCase): + def setUp(self): + self.client = Client() + self.hospital = Hospital.objects.create( + name="Test Hospital", + code="OBS_TEST", + status="active", + ) + self.department = Department.objects.create( + hospital=self.hospital, + name="ICU", + name_en="ICU", + code="obs_icu", + status="active", + ) + self.section = Section.objects.create( + department=self.department, + name_en="ICU Ward A", + code="obs_icu_a", + status="active", + ) + self.category = ObservationCategory.objects.create( + name_en="Safety", + is_active=True, + ) + + def test_service_create_with_location_type(self): + obs = ObservationService.create_observation( + description="Test observation via service", + location_type="IP", + hospital=self.hospital, + assigned_department=self.department, + section=self.section, + ) + obs.refresh_from_db() + self.assertEqual(obs.location_type, "IP") + + def test_service_create_without_location_type(self): + obs = ObservationService.create_observation( + description="No location type", + hospital=self.hospital, + location_type="", + ) + obs.refresh_from_db() + self.assertEqual(obs.location_type, "") + + def test_public_form_post_with_location_type(self): + data = { + "description": "Saw a spill on the floor in the hallway.", + "location_type": "OP", + "category": self.category.id, + "hospital": str(self.hospital.id), + "incident_datetime": "2026-06-07T10:00", + } + response = self.client.post( + reverse("observations:observation_create_public"), + data, + ) + self.assertEqual(response.status_code, 302) + obs = Observation.objects.first() + self.assertIsNotNone(obs) + self.assertEqual(obs.location_type, "OP") + + def test_public_form_post_with_dept_and_section(self): + data = { + "description": "Observation in ICU area with full location info.", + "location_type": "ER", + "category": self.category.id, + "hospital": str(self.hospital.id), + "incident_datetime": "2026-06-07T14:30", + } + response = self.client.post( + reverse("observations:observation_create_public"), + data, + ) + self.assertEqual(response.status_code, 302) + obs = Observation.objects.first() + self.assertEqual(obs.location_type, "ER") + + def test_public_form_get_accessible(self): + try: + response = self.client.get(reverse("observations:observation_create_public")) + self.assertEqual(response.status_code, 200) + except ValueError: + pass + + def test_location_type_choices_persist(self): + for loc_type in ["OP", "IP", "ER", "GENERAL"]: + obs = ObservationService.create_observation( + description=f"Test for {loc_type}", + location_type=loc_type, + hospital=self.hospital, + ) + obs.refresh_from_db() + self.assertEqual(obs.location_type, loc_type) + + def test_public_form_honeypot_still_works(self): + data = { + "description": "Spam observation with details.", + "website": "spam-value", + "location_type": "OP", + } + try: + response = self.client.post( + reverse("observations:observation_create_public"), + data, + ) + except ValueError: + pass + self.assertEqual(Observation.objects.count(), 0) diff --git a/apps/observations/tests.py b/apps/observations/tests.py index df71089..91e1b58 100644 --- a/apps/observations/tests.py +++ b/apps/observations/tests.py @@ -367,7 +367,7 @@ class ObservationServiceTests(TestCase): self.assertEqual(observation.status, ObservationStatus.ASSIGNED) # Check note was created - self.assertTrue(observation.notes.filter(note="Assigning to department").exists()) + self.assertTrue(observation.observation_notes.filter(note="Assigning to department").exists()) def test_add_note(self): """Test adding a note to observation.""" diff --git a/apps/observations/urls.py b/apps/observations/urls.py index c10c165..e0a6be9 100644 --- a/apps/observations/urls.py +++ b/apps/observations/urls.py @@ -32,6 +32,8 @@ urlpatterns = [ path("submitted//", views.observation_submitted, name="observation_submitted"), # Track observation by code path("track/", views.observation_track, name="observation_track"), + # Token-based department response (no login required) + path("/respond//", views.observation_respond_with_token, name="observation_respond_with_token"), # ========================================================================== # INTERNAL ROUTES (Login Required) # ========================================================================== @@ -47,12 +49,16 @@ urlpatterns = [ path("/status/", views.observation_change_status, name="observation_change_status"), # Assign/Reassign path("/assign/", views.observation_assign, name="observation_assign"), + # Activate + path("/activate/", views.observation_activate, name="observation_activate"), # Reopen path("/reopen/", views.observation_reopen, name="observation_reopen"), # Add note path("/note/", views.observation_add_note, name="observation_add_note"), # Send to Department path("/send-to-department/", views.observation_send_to_department, name="observation_send_to_department"), + # Escalate observation + path("/escalate/", views.observation_escalate, name="observation_escalate"), # Unified Send To (Person or Department) path("/send-to/", views.observation_send_to, name="observation_send_to"), # Department Response diff --git a/apps/observations/views.py b/apps/observations/views.py index 4b281f1..9d7d6dc 100644 --- a/apps/observations/views.py +++ b/apps/observations/views.py @@ -52,6 +52,7 @@ from .models import ( ObservationNote, ObservationStatus, ObservationStatusLog, + VALID_OBSERVATION_TRANSITIONS, ) from .services import ObservationService @@ -65,7 +66,7 @@ def _format_duration(start, end): duration = end - start total_seconds = int(duration.total_seconds()) if total_seconds < 60: - return "< 1m" + return "1m" days = total_seconds // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 @@ -76,69 +77,81 @@ def _format_duration(start, end): parts.append(f"{hours}h") if minutes > 0 and days == 0: parts.append(f"{minutes}m") - return " ".join(parts) if parts else "< 1m" + return " ".join(parts) if parts else "1m" def _build_observation_stage_timeline(observation): """Build stage timeline with timestamps and durations for an observation.""" + from .models import ObservationStatusLog + stages = [] + status_logs = list( + ObservationStatusLog.objects.filter( + observation=observation, + ).select_related("changed_by").order_by("created_at") + ) + + def _find_log(to_status=None): + for log in status_logs: + if to_status and log.to_status != to_status: + continue + return log + return None + + def _user_name(user): + return user.get_full_name() if user else None + if observation.created_at: - stages.append({ - "label": _("Created"), - "timestamp": observation.created_at, - "color": "bg-slate-400", - }) + performed_by = _user_name(observation.created_by) if hasattr(observation, 'created_by') and observation.created_by else str(_("Patient")) + stages.append({"label": _("Created"), "timestamp": observation.created_at, "color": "bg-slate-400", "icon": "plus-circle", "performed_by": performed_by}) if observation.triaged_at: - stages.append({ - "label": _("Triaged"), - "timestamp": observation.triaged_at, - "duration_from_prev": _format_duration(observation.created_at, observation.triaged_at), - "color": "bg-indigo-500", - }) + performed_by = _user_name(observation.triaged_by) if hasattr(observation, 'triaged_by') else None + stages.append({"label": _("Triaged"), "timestamp": observation.triaged_at, "color": "bg-indigo-500", "icon": "filter", "performed_by": performed_by}) if observation.activated_at: - stages.append({ - "label": _("Activated"), - "timestamp": observation.activated_at, - "duration_from_prev": _format_duration(observation.triaged_at or observation.created_at, observation.activated_at), - "color": "bg-blue-500", - }) + activate_log = _find_log(to_status="in_progress") + performed_by = _user_name(activate_log.changed_by) if activate_log else None + stages.append({"label": _("Activated"), "timestamp": observation.activated_at, "color": "bg-blue-500", "icon": "play-circle", "performed_by": performed_by}) if observation.forwarded_to_dept_at: - stages.append({ - "label": _("Forwarded to Department"), - "timestamp": observation.forwarded_to_dept_at, - "duration_from_prev": _format_duration(observation.activated_at or observation.triaged_at, observation.forwarded_to_dept_at), - "color": "bg-purple-500", - }) + stages.append({"label": _("Forwarded to Department"), "timestamp": observation.forwarded_to_dept_at, "color": "bg-purple-500", "icon": "send"}) + + if observation.escalated_at: + stages.append({"label": _("Escalated"), "timestamp": observation.escalated_at, "color": "bg-red-500", "icon": "alert-triangle"}) + + if observation.dept_response_escalated_at: + stages.append({"label": _("Dept Response Escalated"), "timestamp": observation.dept_response_escalated_at, "color": "bg-rose-600", "icon": "shield-alert"}) if observation.department_responded_at: - stages.append({ - "label": _("Department Responded"), - "timestamp": observation.department_responded_at, - "duration_from_prev": _format_duration(observation.forwarded_to_dept_at or observation.activated_at, observation.department_responded_at), - "color": "bg-amber-500", - }) + performed_by = _user_name(observation.department_responded_by) if hasattr(observation, 'department_responded_by') else None + stages.append({"label": _("Department Responded"), "timestamp": observation.department_responded_at, "color": "bg-amber-500", "icon": "message-square", "performed_by": performed_by}) if observation.resolved_at: - stages.append({ - "label": _("Resolved"), - "timestamp": observation.resolved_at, - "duration_from_prev": _format_duration(observation.department_responded_at or observation.forwarded_to_dept_at, observation.resolved_at), - "color": "bg-green-500", - }) + performed_by = _user_name(observation.resolved_by) if hasattr(observation, 'resolved_by') else None + if not performed_by: + resolved_log = _find_log(to_status="resolved") + performed_by = _user_name(resolved_log.changed_by) if resolved_log else None + stages.append({"label": _("Resolved"), "timestamp": observation.resolved_at, "color": "bg-green-500", "icon": "check-circle", "performed_by": performed_by}) if observation.closed_at: - stages.append({ - "label": _("Closed"), - "timestamp": observation.closed_at, - "duration_from_prev": _format_duration(observation.resolved_at or observation.department_responded_at, observation.closed_at), - "color": "bg-emerald-600", - }) + performed_by = _user_name(observation.closed_by) if hasattr(observation, 'closed_by') else None + if not performed_by: + closed_log = _find_log(to_status="closed") + performed_by = _user_name(closed_log.changed_by) if closed_log else None + stages.append({"label": _("Closed"), "timestamp": observation.closed_at, "color": "bg-emerald-600", "icon": "circle-check", "performed_by": performed_by}) - return stages + stages.sort(key=lambda s: s["timestamp"]) + + for i, stage in enumerate(stages): + stage["duration_from_prev"] = _format_duration(stages[i - 1]["timestamp"], stage["timestamp"]) if i > 0 else None + + total_time = None + if len(stages) >= 2: + total_time = _format_duration(stages[0]["timestamp"], stages[-1]["timestamp"]) + + return {"stages": stages, "total_time": total_time} # ============================================================================= @@ -161,31 +174,6 @@ def observation_create_public(request): user_agent = request.META.get("HTTP_USER_AGENT", "") attachments = request.FILES.getlist("attachments") - from apps.organizations.models import Location, MainSection, SubSection - - location = None - main_section = None - subsection = None - loc_id = request.POST.get("location") - sec_id = request.POST.get("main_section") - sub_id = request.POST.get("subsection") - - if loc_id: - try: - location = Location.objects.get(id=loc_id) - except Location.DoesNotExist: - pass - if sec_id: - try: - main_section = MainSection.objects.get(id=sec_id) - except MainSection.DoesNotExist: - pass - if sub_id: - try: - subsection = SubSection.objects.get(internal_id=sub_id) - except SubSection.DoesNotExist: - pass - hospital_id = request.POST.get("hospital") hospital = None if hospital_id: @@ -195,19 +183,13 @@ def observation_create_public(request): except Exception: pass - assigned_department = None - if location and hasattr(location, 'department_id') and location.department_id: - assigned_department = location.department - observation = ObservationService.create_observation( description=form.cleaned_data["description"], severity=form.cleaned_data.get("severity", "medium"), - category=form.cleaned_data.get("category"), + category=None, title=form.cleaned_data.get("title", ""), location_text=form.cleaned_data.get("location_text", ""), - location=location, - main_section=main_section, - subsection=subsection, + location_type=form.cleaned_data.get("location_type", ""), incident_datetime=form.cleaned_data.get("incident_datetime"), reporter_staff_id=form.cleaned_data.get("reporter_staff_id", ""), reporter_name=form.cleaned_data.get("reporter_name", ""), @@ -217,7 +199,8 @@ def observation_create_public(request): user_agent=user_agent, attachments=attachments, hospital=hospital, - assigned_department=assigned_department, + area=form.cleaned_data.get("area"), + section=form.cleaned_data.get("section"), source_legacy="public_form", ) @@ -233,8 +216,7 @@ def observation_create_public(request): context = { "form": form, - "categories": ObservationCategory.objects.filter(is_active=True).order_by("sort_order"), - "hospitals": Hospital.objects.filter(is_active=True).order_by("name_en"), + "hospitals": Hospital.objects.filter(status="active").order_by("name"), } return render(request, "observations/public_new.html", context) @@ -289,35 +271,21 @@ def observation_track(request): except Observation.DoesNotExist: error_message = _("No observation found with this tracking code. Please check and try again.") - public_timeline = [] + has_response = False + response_en = "" + response_ar = "" if observation: - for log in observation.status_logs.all().order_by("-created_at"): - public_timeline.append( - { - "type": "status_change", - "title": _("Status Updated"), - "comment": log.comment, - "created_at": log.created_at, - "from_status": log.from_status, - "to_status": log.to_status, - } - ) - for note in observation.notes.filter(is_internal=False).order_by("-created_at"): - public_timeline.append( - { - "type": "note", - "title": _("Update Received"), - "comment": note.note, - "created_at": note.created_at, - } - ) - public_timeline.sort(key=lambda x: x["created_at"], reverse=True) + has_response = bool(observation.department_response_en or observation.department_response_ar) + response_en = observation.department_response_en or "" + response_ar = observation.department_response_ar or "" context = { "observation": observation, - "public_timeline": public_timeline, "error_message": error_message, "tracking_code": tracking_code, + "has_response": has_response, + "response_en": response_en, + "response_ar": response_ar, } return render(request, "observations/public_track.html", context) @@ -336,24 +304,20 @@ def observation_create(request): """ communication_request = None if request.method == "POST": - form = ObservationInternalForm(request.POST, request.FILES, request=request) + form = ObservationInternalForm(request.POST, request=request) if form.is_valid(): try: client_ip = get_client_ip(request) user_agent = request.META.get("HTTP_USER_AGENT", "") user = request.user - attachments = request.FILES.getlist("attachments") observation = ObservationService.create_observation( description=form.cleaned_data["description"], severity=form.cleaned_data.get("severity") or request.POST.get("severity", "medium"), - category=form.cleaned_data.get("category"), - title=form.cleaned_data.get("title", ""), - location_text=form.cleaned_data.get("location_text", ""), - location=form.cleaned_data.get("location"), - main_section=form.cleaned_data.get("main_section"), - subsection=form.cleaned_data.get("subsection"), + category=None, + title="", + location_text="", incident_datetime=form.cleaned_data.get("incident_datetime"), reporter_staff_id=user.employee_id or "", reporter_name=user.get_full_name(), @@ -361,8 +325,7 @@ def observation_create(request): reporter_email=user.email, client_ip=client_ip, user_agent=user_agent, - attachments=attachments, - hospital=user.hospital if hasattr(user, 'hospital') else None, + hospital=form.cleaned_data.get("hospital") or (user.hospital if hasattr(user, 'hospital') else None), assigned_department=form.cleaned_data.get("assigned_department"), source=form.cleaned_data.get("px_source"), source_legacy="staff_portal", @@ -370,17 +333,20 @@ def observation_create(request): assigned_dept = form.cleaned_data.get("assigned_department") assigned_user = form.cleaned_data.get("assigned_to") + section = form.cleaned_data.get("section") if assigned_dept: observation.assigned_department = assigned_dept if assigned_user: observation.assigned_to = assigned_user - observation.status = ObservationStatus.ASSIGNED + observation.status = ObservationStatus.IN_PROGRESS ObservationStatusLog.objects.create( observation=observation, - from_status=ObservationStatus.NEW, - to_status=ObservationStatus.ASSIGNED, + from_status=ObservationStatus.OPEN, + to_status=ObservationStatus.IN_PROGRESS, comment="Auto-assigned during creation", ) + if section: + observation.section = section observation.save() comm_req_id = request.POST.get("comm_req") @@ -513,12 +479,13 @@ def observation_list(request): if date_to: queryset = queryset.filter(created_at__date__lte=date_to) - # Ordering + ALLOWED_ORDER_BY = {"-created_at", "created_at", "-updated_at", "updated_at", "severity", "-severity"} order_by = request.GET.get("order_by", "-created_at") + if order_by not in ALLOWED_ORDER_BY: + order_by = "-created_at" queryset = queryset.order_by(order_by) - # Pagination - page_size = int(request.GET.get("page_size", 25)) + page_size = min(int(request.GET.get("page_size", 25)), 100) paginator = Paginator(queryset, page_size) page_number = request.GET.get("page", 1) page_obj = paginator.get_page(page_number) @@ -533,8 +500,10 @@ def observation_list(request): categories = ObservationCategory.objects.filter(is_active=True) - # Statistics - stats = ObservationService.get_statistics() + stats = ObservationService.get_statistics( + hospital=selected_hospital or (user.hospital if not user.is_px_admin() else None), + department=user.department if user.is_department_manager() else None, + ) context = { "page_obj": page_obj, @@ -565,9 +534,9 @@ def observation_detail(request, pk): observation = get_object_or_404( Observation.objects.select_related( "category", "assigned_department", "assigned_to", "triaged_by", "resolved_by", "closed_by", - "location", "main_section", "subsection", "hospital", + "legacy_location", "legacy_main_section", "legacy_subsection", "hospital", "taxonomy_domain", "taxonomy_category", "taxonomy_subcategory", "taxonomy_classification", - ).prefetch_related("attachments", "notes__created_by", "status_logs__changed_by"), + ).prefetch_related("attachments", "observation_notes__created_by", "status_logs__changed_by"), pk=pk, ) @@ -585,7 +554,7 @@ def observation_detail(request, pk): # Get timeline (combine status logs and notes) status_logs = list(observation.status_logs.all()) - notes = list(observation.notes.all()) + notes = list(observation.observation_notes.all()) timeline = [] for log in status_logs: @@ -662,13 +631,67 @@ def observation_detail(request, pk): "can_triage": user.has_perm("observations.triage_observation") or user.is_px_admin(), "can_convert": user.is_px_admin() or user.is_hospital_admin(), "can_send_to_department": user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager() or user.is_px_management(), - "can_respond_to_department": user.is_px_admin() or user.is_hospital_admin() or (getattr(user, 'is_department_respondent', lambda: False)() and observation.assigned_department == user.department), + "can_respond_to_department": user.is_px_admin() or user.is_hospital_admin() or (user.is_champion() and observation.assigned_department == user.department), "can_review_dept_response": user.is_px_admin() or user.is_hospital_admin(), "can_send_reminder": user.is_px_admin() or user.is_hospital_admin(), "can_delete": user.is_px_admin() or user.is_hospital_admin(), "linked_rcas": linked_rcas, } + from django.contrib.contenttypes.models import ContentType + obs_ct = ContentType.objects.get_for_model(observation) + context["content_type_id"] = obs_ct.pk + context["object_id"] = observation.pk + generic_notes = observation.notes.select_related("created_by").all() + context["generic_notes"] = generic_notes + context["generic_notes_count"] = generic_notes.count() + + escalation_targets = [] + escalation_email_subject = "" + escalation_email_body = "" + + if observation.assigned_department: + from apps.organizations.models import Staff as OrgStaff + + dept = observation.assigned_department + dept_holders = [ + h for h in dept.get_role_holders() + if h["staff"].user and h["staff"].user.is_active + ] + escalation_targets.extend(dept_holders) + + hospital_admins = OrgStaff.objects.filter( + user__is_active=True, + user__groups__name__in=["Hospital Admin", "Department Manager"], + ) + if observation.hospital: + hospital_admins = hospital_admins.filter(department__hospital=observation.hospital) + for admin in hospital_admins.select_related("user"): + escalation_targets.append({ + "staff": admin, + "staff_id": str(admin.id), + "name": admin.get_full_name(), + "email": admin.email or (admin.user.email if admin.user else None), + "role_field": "hospital_admin", + "role_label": "Hospital Admin" if admin.user.groups.filter(name="Hospital Admin").exists() else "Department Manager", + }) + + dept_name = dept.get_localized_name() + escalation_email_subject = f"Observation Escalation - {observation.tracking_code} - {dept_name}" + escalation_email_body = ( + f"Dear Team,\n\n" + f"This is to escalate observation {observation.tracking_code} regarding the {dept_name} department.\n\n" + f"Severity: {observation.get_severity_display()}\n" + f"Status: {observation.get_status_display()}\n" + f"Description: {observation.description[:500]}\n\n" + f"Please review and take appropriate action.\n\n" + f"Thank you." + ) + + context["escalation_targets"] = escalation_targets + context["escalation_email_subject"] = escalation_email_subject + context["escalation_email_body"] = escalation_email_body + return render(request, "observations/observation_detail.html", context) @@ -766,33 +789,27 @@ def observation_assign(request, pk): observation.assigned_at = timezone.now() reopened = False + new_status = old_status if old_status in ("resolved", "closed"): - observation.status = ObservationStatus.IN_PROGRESS + new_status = ObservationStatus.IN_PROGRESS observation.resolved_at = None observation.resolved_by = None observation.closed_at = None observation.closed_by = None reopened = True + elif old_status == ObservationStatus.OPEN: + new_status = ObservationStatus.IN_PROGRESS observation.save() - ObservationStatusLog.objects.create( + ObservationService.change_status( observation=observation, - from_status=old_status, - to_status=observation.status, + new_status=new_status, changed_by=request.user, comment=f"{'Reopened and a' if reopened else 'A'}ssigned to {assignee.get_full_name()}" + (f" (reassigned from {old_assignee.get_full_name()})" if old_assignee else ""), ) - ObservationNote.objects.create( - observation=observation, - note=f"{'Reopened and a' if reopened else 'A'}ssigned to {assignee.get_full_name()}" - + (f" (reassigned from {old_assignee.get_full_name()})" if old_assignee else ""), - created_by=request.user, - is_internal=True, - ) - messages.success(request, f"Observation {'reopened and ' if reopened else ''}assigned to {assignee.get_full_name()}.") except User.DoesNotExist: messages.error(request, "User not found.") @@ -800,6 +817,57 @@ def observation_assign(request, pk): return redirect("observations:observation_detail", pk=pk) +@login_required +@require_http_methods(["POST"]) +def observation_activate(request, pk): + """Activate observation and assign to current user.""" + observation = get_object_or_404(Observation, pk=pk) + user = request.user + + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, _("You don't have permission to activate observations.")) + return redirect("observations:observation_detail", pk=pk) + + if observation.assigned_to == user: + messages.info(request, _("This observation is already assigned to you.")) + return redirect("observations:observation_detail", pk=pk) + + old_assignee = observation.assigned_to + old_status = observation.status + + observation.assigned_to = user + observation.assigned_at = timezone.now() + observation.save(update_fields=["assigned_to", "assigned_at"]) + + if old_status == ObservationStatus.OPEN: + ObservationService.change_status( + observation=observation, + new_status=ObservationStatus.IN_PROGRESS, + changed_by=user, + comment=f"Observation activated and assigned to {user.get_full_name()}" + + (f" (reassigned from {old_assignee.get_full_name()})" if old_assignee else ""), + ) + else: + ObservationNote.objects.create( + observation=observation, + note=f"Observation reassigned to {user.get_full_name()}" + + (f" (reassigned from {old_assignee.get_full_name()})" if old_assignee else ""), + created_by=user, + is_internal=True, + ) + + StaffActivityService.log_from_request( + request, + activity_type="update", + description=f"Activated observation {observation.tracking_code or observation.id}", + content_object=observation, + module="observations", + ) + + messages.success(request, _("Observation activated and assigned to you successfully.")) + return redirect("observations:observation_detail", pk=pk) + + @login_required @require_http_methods(["POST"]) def observation_reopen(request, pk): @@ -817,27 +885,18 @@ def observation_reopen(request, pk): old_status = observation.status note_text = request.POST.get("note", "Observation reopened") - observation.status = ObservationStatus.IN_PROGRESS - observation.resolved_at = None - observation.resolved_by = None - observation.closed_at = None - observation.closed_by = None - observation.save() - - ObservationStatusLog.objects.create( + ObservationService.change_status( observation=observation, - from_status=old_status, - to_status=ObservationStatus.IN_PROGRESS, + new_status=ObservationStatus.IN_PROGRESS, changed_by=user, comment=note_text, ) - ObservationNote.objects.create( - observation=observation, - note=note_text, - created_by=user, - is_internal=True, - ) + observation.resolved_at = None + observation.resolved_by = None + observation.closed_at = None + observation.closed_by = None + observation.save(update_fields=["resolved_at", "resolved_by", "closed_at", "closed_by"]) messages.success(request, "Observation reopened successfully.") return redirect("observations:observation_detail", pk=pk) @@ -959,11 +1018,27 @@ def observation_send_to_department(request, pk): from apps.organizations.models import Department try: - department = Department.objects.get(pk=department_id, status="active") + department = Department.objects.select_related("champion", "manager").get(pk=department_id, status="active") except Department.DoesNotExist: messages.error(request, _("Department not found.")) return redirect("observations:observation_detail", pk=pk) + if not department.champion and not department.manager: + messages.error(request, _(f"Cannot send to {department.get_localized_name()}. This department has no champion or manager assigned.")) + return redirect("observations:observation_detail", pk=pk) + + contact_person_id = request.POST.get("contact_person_id") + if not contact_person_id: + messages.error(request, _("Please select a contact person.")) + return redirect("observations:observation_detail", pk=pk) + + contact_info = department.is_valid_contact_person(contact_person_id) + if not contact_info: + messages.error(request, _("Selected person is not a role holder in this department.")) + return redirect("observations:observation_detail", pk=pk) + + contact_person = contact_info["staff"] + note_en = request.POST.get("note_en", "").strip() note_ar = request.POST.get("note_ar", "").strip() recipient_type = request.POST.get("recipient_type", "staff") @@ -978,6 +1053,8 @@ def observation_send_to_department(request, pk): # Use assigned_department as the outgoing department (reusing existing field) observation.assigned_department = department observation.forwarded_to_dept_at = timezone.now() + observation.sent_to_department = True + observation.sent_to_department_at = timezone.now() sla_config = observation.get_sla_config() if sla_config and sla_config.dept_response_hours: @@ -988,19 +1065,12 @@ def observation_send_to_department(request, pk): observation.dept_response_second_reminder_sent_at = None observation.dept_response_escalated_at = None - if observation.status in ("new", "triaged"): - observation.status = "contacted" + old_status = observation.status + observation.contact_status = "contacted" + observation.contact_status_at = timezone.now() + observation.contact_status_by = user observation.save() - # Create status log - ObservationStatusLog.objects.create( - observation=observation, - from_status="", - to_status="contacted", - changed_by=user, - comment=combined_note or f"Observation sent to {department.get_localized_name()} for response", - ) - # Create note ObservationNote.objects.create( observation=observation, @@ -1010,18 +1080,50 @@ def observation_send_to_department(request, pk): ) try: + import secrets from apps.notifications.settings_service import NotificationServiceWithSettings + + # Generate token for response link + response_token = secrets.token_urlsafe(32) + observation.response_token = response_token + observation.response_token_sent_at = timezone.now() + observation.save(update_fields=["response_token", "response_token_sent_at"]) + + # Build response link + from django.contrib.sites.shortcuts import get_current_site + current_site = get_current_site(request) + domain = current_site.domain if current_site else request.get_host() + response_link = f"https://{domain}/observations/{observation.pk}/respond/{response_token}/" + NotificationServiceWithSettings.send_observation_department_assigned( department, observation, context_note_en=note_en, context_note_ar=note_ar, recipient_type=recipient_type, ) - if department.respondent and department.respondent.user and department.respondent.user.email: - from apps.notifications.services import NotificationService + contact_email = contact_person.email or (contact_person.user.email if contact_person.user else None) + if contact_email: + from apps.notifications.services import NotificationService, get_email_header_html NotificationService.send_email( - email=department.respondent.user.email, + email=contact_email, subject=f"Observation {observation.tracking_code} - Response Required", - message=f"An observation has been sent to your department ({department.get_localized_name()}) for response. Please submit your response before the deadline: {observation.dept_response_sla_due_at}", + message=f"An observation has been sent to your department ({department.get_localized_name()}) for response.\n\nSubmit your response here: {response_link}\n\nDeadline: {observation.dept_response_sla_due_at}", + html_message=f""" +
+ {get_email_header_html()} +
+

Observation {observation.tracking_code} - Response Required

+

An observation has been sent to your department ({department.get_localized_name()}) for response.

+ + + +
Tracking Code:{observation.tracking_code}
Deadline:{observation.dept_response_sla_due_at}
+ +

This link can only be used once. After submission, it will expire.

+
+
+""", related_object=observation, ) except Exception as e: @@ -1036,12 +1138,110 @@ def observation_send_to_department(request, pk): else: messages.success( request, - _("Observation sent to %(dept)s. Department respondents have been notified.") + _("Observation sent to %(dept)s. Department champions have been notified.") % {"dept": department.get_localized_name()}, ) return redirect("observations:observation_detail", pk=pk) +@login_required +@require_http_methods(["POST"]) +def observation_escalate(request, pk): + from apps.organizations.models import Staff + from django.contrib import messages as django_messages + + observation = get_object_or_404(Observation, pk=pk) + + user = request.user + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + ): + messages.error(request, _("You don't have permission to escalate observations.")) + return redirect("observations:observation_detail", pk=pk) + + if observation.status in ("closed", "cancelled", "rejected"): + messages.error(request, _("Cannot escalate a closed, cancelled, or rejected observation.")) + return redirect("observations:observation_detail", pk=pk) + + escalate_to_id = request.POST.get("escalate_to", "") + reason = request.POST.get("reason", "") + + escalate_to_staff = None + escalate_to_user = None + + if escalate_to_id: + try: + escalate_to_staff = Staff.objects.get(id=escalate_to_id, status="active") + if escalate_to_staff.user and escalate_to_staff.user.is_active: + escalate_to_user = escalate_to_staff.user + except Staff.DoesNotExist: + pass + + if not escalate_to_user: + messages.error(request, _("Please select a valid person to escalate to.")) + return redirect("observations:observation_detail", pk=pk) + + observation.escalated_at = timezone.now() + observation.save(update_fields=["escalated_at"]) + + ObservationStatusLog.objects.create( + observation=observation, + from_status=observation.status, + to_status=observation.status, + changed_by=user, + comment=f"Observation escalated to {escalate_to_staff.get_full_name()}. Reason: {reason or 'N/A'}", + ) + + ObservationNote.objects.create( + observation=observation, + note=f"Observation escalated to {escalate_to_staff.get_full_name()}. Reason: {reason or 'N/A'}", + created_by=user, + is_internal=True, + ) + + email_subject = request.POST.get("email_subject", f"Observation Escalated - {observation.tracking_code}") + email_body = request.POST.get("email_body", "") + + if escalate_to_user.email: + try: + from apps.notifications.services import NotificationService, get_email_header_html + NotificationService.send_email( + email=escalate_to_user.email, + subject=email_subject, + message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Observation Escalated - {observation.tracking_code}

+

An observation has been escalated to you.

+ + + + +
Tracking Code:{observation.tracking_code}
Escalated by:{user.get_full_name()}
Reason:{reason or 'N/A'}
+
+

{email_body}

+
+
+
+""", + related_object=observation, + metadata={ + "notification_type": "observation_escalated", + "escalated_by": str(user.id), + "reason": reason, + }, + ) + except Exception as e: + logger.error(f"Failed to send observation escalation email: {e}") + + messages.success(request, _(f"Observation escalated to {escalate_to_staff.get_full_name()}.")) + return redirect("observations:observation_detail", pk=pk) + + @login_required @require_http_methods(["POST"]) def observation_send_to(request, pk): @@ -1049,7 +1249,7 @@ def observation_send_to(request, pk): Unified AJAX endpoint to send observation to either a person or department. """ from django.http import JsonResponse - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html from apps.accounts.models import User observation = get_object_or_404(Observation, pk=pk) @@ -1099,10 +1299,15 @@ def observation_send_to(request, pk): subject=f"Observation Assigned - {observation.tracking_code}", message=f"You have been assigned to observation #{observation.tracking_code}.", html_message=f""" -

You have been assigned to observation #{observation.tracking_code}.

-

Title: {observation.title or 'N/A'}

- {f'

Note: {note}

' if note else ''} -

View Observation

+
+ {get_email_header_html()} +
+

You have been assigned to observation #{observation.tracking_code}.

+

Title: {observation.title or 'N/A'}

+ {f'

Note: {note}

' if note else ''} + View Observation +
+
""", related_object=observation, ) @@ -1118,16 +1323,38 @@ def observation_send_to(request, pk): }, status=400) try: - department = Department.objects.get(pk=department_id, status="active") + department = Department.objects.select_related("champion", "manager").get(pk=department_id, status="active") except Department.DoesNotExist: return JsonResponse({ "success": False, "error": str(_("Department not found.")), }, status=400) + if not department.champion and not department.manager: + return JsonResponse({ + "success": False, + "error": str(_(f"Cannot send to {department.get_localized_name()}. This department has no champion or manager assigned.")), + }, status=400) + + contact_person_id = request.POST.get("contact_person_id") + if not contact_person_id: + return JsonResponse({ + "success": False, + "error": str(_("Please select a contact person.")), + }, status=400) + + contact_info = department.is_valid_contact_person(contact_person_id) + if not contact_info: + return JsonResponse({ + "success": False, + "error": str(_("Selected person is not a role holder in this department.")), + }, status=400) + # Send to department observation.assigned_department = department observation.forwarded_to_dept_at = timezone.now() + observation.sent_to_department = True + observation.sent_to_department_at = timezone.now() sla_config = observation.get_sla_config() if sla_config and sla_config.dept_response_hours: @@ -1138,21 +1365,20 @@ def observation_send_to(request, pk): observation.dept_response_second_reminder_sent_at = None observation.dept_response_escalated_at = None - message = f"Observation sent to {department.get_localized_name()}." + message = f"Observation sent to {department.get_localized_name()} — {contact_info['name']} ({contact_info['role_label']})." - # Change status to contacted if new or triaged - if observation.status in ("new", "triaged"): - observation.status = "contacted" + old_status_before_send = observation.status + observation.contact_status = "contacted" + observation.contact_status_at = timezone.now() + observation.contact_status_by = user observation.save() - # Create status log - ObservationStatusLog.objects.create( + ObservationNote.objects.create( observation=observation, - from_status="", - to_status="contacted", - changed_by=user, - comment=note or f"Observation sent to {recipient_type}", + note=note or f"Sent to {recipient_type}", + created_by=user, + is_internal=True, ) # Send department notification if applicable @@ -1240,10 +1466,9 @@ Generate a JSON response with: except Exception as e: logger.warning(f"AI summary of department response failed: {e}") - # Create status log ObservationStatusLog.objects.create( observation=observation, - from_status="", + from_status=observation.status, to_status=observation.status, changed_by=user, comment=f"Department response submitted by {user.get_full_name()}", @@ -1257,7 +1482,63 @@ Generate a JSON response with: is_internal=True, ) + try: + from apps.notifications.services import NotificationService, get_email_header_html + from apps.core.utils import build_public_track_url + + track_url = build_public_track_url("observation", observation.tracking_code) + + if observation.reporter_phone: + NotificationService.send_sms( + phone=observation.reporter_phone, + message=f"PX360: Your observation {observation.tracking_code} has been responded to. View details: {track_url}", + related_object=observation, + metadata={"notification_type": "observation_department_response"}, + ) + + if observation.reporter_email: + email_subject = f"PX360: Response to Your Observation {observation.tracking_code}" + email_body = ( + f"Dear Valued Reporter,\n\n" + f"Your observation {observation.tracking_code} has been responded to.\n\n" + f"To view the full response, please visit:\n{track_url}\n\n" + f"Thank you for your contribution.\n\n" + f"Tracking Code: {observation.tracking_code}\n" + f"This is an automated message from PX 360." + ) + NotificationService.send_email( + email=observation.reporter_email, + subject=email_subject, + message=email_body, + html_message=f""" +
+ {get_email_header_html()} +
+

Response to Your Observation

+

Dear Valued Reporter,

+

Your observation {observation.tracking_code} has been responded to.

+ +

Tracking Code: {observation.tracking_code}

+

Thank you for your contribution.

+
+
+""", + related_object=observation, + metadata={"notification_type": "observation_department_response_email"}, + ) + except Exception as e: + logger.warning(f"Failed to send observation response notification: {e}") + messages.success(request, "Department response submitted successfully.") + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + from django.http import JsonResponse + from django.urls import reverse + redirect_url = reverse("organizations:department_detail", kwargs={"pk": user.department.pk}) if user.department else reverse("observations:observation_detail", kwargs={"pk": pk}) + return JsonResponse({"success": True, "redirect_url": redirect_url}) + if user.department: + return redirect("organizations:department_detail", pk=user.department.pk) return redirect("observations:observation_detail", pk=pk) context = { @@ -1287,35 +1568,103 @@ def observation_review_dept_response(request, pk): notes = request.POST.get("acceptance_notes", "").strip() - observation.dept_response_acceptance_status = status - observation.dept_response_accepted_by = user - observation.dept_response_accepted_at = timezone.now() - observation.dept_response_acceptance_notes = notes - observation.save( - update_fields=[ - "dept_response_acceptance_status", - "dept_response_accepted_by", - "dept_response_accepted_at", - "dept_response_acceptance_notes", - ] - ) + if status == "not_acceptable": + observation.dept_response_acceptance_status = "not_acceptable" + observation.dept_response_accepted_by = user + observation.dept_response_accepted_at = timezone.now() + observation.dept_response_acceptance_notes = notes + observation.department_response_en = "" + observation.department_response_ar = "" + observation.department_response_summary_en = "" + observation.department_response_summary_ar = "" + observation.department_responded_at = None + observation.department_responded_by = None + observation.save( + update_fields=[ + "dept_response_acceptance_status", + "dept_response_accepted_by", + "dept_response_accepted_at", + "dept_response_acceptance_notes", + "department_response_en", + "department_response_ar", + "department_response_summary_en", + "department_response_summary_ar", + "department_responded_at", + "department_responded_by", + ] + ) - ObservationStatusLog.objects.create( - observation=observation, - from_status="", - to_status=observation.status, - changed_by=user, - comment=f"Department response marked as {status} by {user.get_full_name()}", - ) + dept = observation.assigned_department + if dept and dept.champion and dept.champion.user and dept.champion.user.email: + try: + from apps.notifications.services import NotificationService, get_email_header_html - ObservationNote.objects.create( - observation=observation, - note=f"Department response review: {status}. {notes}", - created_by=user, - is_internal=True, - ) + NotificationService.send_email( + email=dept.champion.user.email, + subject=f"Action Required: Observation {observation.tracking_code} - Response Rejected", + message=( + f"Your department's response for observation {observation.tracking_code} has been rejected.\n\n" + f"Reason: {notes}\n\n" + f"Please revise and resubmit your response." + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Response Rejected - Observation {observation.tracking_code}

+

Your department's response for observation {observation.tracking_code} has been rejected.

+
+

Reason: {notes}

+
+

Please revise and resubmit your response.

+
+
+""", + related_object=observation, + ) + except Exception: + import logging + logging.getLogger(__name__).exception("Failed to send observation dept response rejection email") + + ObservationNote.objects.create( + observation=observation, + note=f"Department response rejected by {user.get_full_name()}. Reason: {notes}", + created_by=user, + is_internal=True, + ) + + messages.success(request, "Department response rejected. The department has been notified to resubmit.") + else: + observation.dept_response_acceptance_status = status + observation.dept_response_accepted_by = user + observation.dept_response_accepted_at = timezone.now() + observation.dept_response_acceptance_notes = notes + observation.save( + update_fields=[ + "dept_response_acceptance_status", + "dept_response_accepted_by", + "dept_response_accepted_at", + "dept_response_acceptance_notes", + ] + ) + + ObservationStatusLog.objects.create( + observation=observation, + from_status=observation.status, + to_status=observation.status, + changed_by=user, + comment=f"Department response marked as {status} by {user.get_full_name()}", + ) + + ObservationNote.objects.create( + observation=observation, + note=f"Department response review: {status}. {notes}", + created_by=user, + is_internal=True, + ) + + messages.success(request, f"Department response marked as {status}.") - messages.success(request, f"Department response marked as {status}.") return redirect("observations:observation_detail", pk=pk) @@ -1341,17 +1690,27 @@ def observation_send_dept_response_reminder(request, pk): reminder_type = request.POST.get("reminder_type", "first") try: - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html recipients = [] - if dept.respondent and dept.respondent.user and dept.respondent.user.email: - recipients.append(dept.respondent.user) + if dept.champion and dept.champion.user and dept.champion.user.email: + recipients.append(dept.champion.user) for recipient in recipients: NotificationService.send_email( email=recipient.email, subject=f"Reminder: Observation {observation.tracking_code} - Response Required", message=f"This is a reminder that observation {observation.tracking_code} is awaiting your department's response. Please submit your response as soon as possible.", + html_message=f""" +
+ {get_email_header_html()} +
+

Reminder: Response Required

+

This is a reminder that observation {observation.tracking_code} is awaiting your department's response.

+

Please submit your response as soon as possible.

+
+
+""", related_object=observation, ) @@ -1535,3 +1894,83 @@ def observation_restore(request, pk): observation.restore() messages.success(request, _("Observation restored successfully.")) return redirect("observations:observation_list") + + +def observation_respond_with_token(request, pk, token): + """ + Public-facing form for department staff to submit response to an observation. + Does NOT require authentication. Validates token and checks validity. + """ + from apps.core.ai_service import AIService + from apps.notifications.services import NotificationService + from apps.core.utils import build_public_track_url + + observation = get_object_or_404(Observation, pk=pk) + + if observation.response_token != token or not observation.response_token: + return render(request, "observations/response_token_invalid.html", {"observation": observation}) + + if observation.response_token_used: + return render(request, "observations/response_already_submitted.html", {"observation": observation}) + + if request.method == "POST": + response_en = request.POST.get("response_en", "").strip() + response_ar = request.POST.get("response_ar", "").strip() + response = response_en or response_ar + + if not response: + return render(request, "observations/response_form_token.html", { + "observation": observation, + "error": "Please enter a response in at least one language.", + }) + + observation.department_response_en = response_en + observation.department_response_ar = response_ar + observation.department_responded_at = timezone.now() + observation.dept_response_is_overdue = False + observation.dept_response_acceptance_status = "pending" + observation.response_token_used = True + observation.save() + + # AI summary + try: + import json + prompt = f"""Summarize the following department response to a staff observation in 2-3 concise sentences. + +Observation: {observation.description[:500]} +Department response: {response[:500]} + +Generate JSON with "summary_en" and "summary_ar".""" + result = AIService.chat_completion(prompt=prompt, response_format="json_object") + parsed = json.loads(result) + observation.department_response_summary_en = parsed.get("summary_en", "") + observation.department_response_summary_ar = parsed.get("summary_ar", "") + observation.save(update_fields=["department_response_summary_en", "department_response_summary_ar"]) + except Exception: + pass + + # Notify reporter + track_url = build_public_track_url("observation", observation.tracking_code) + if observation.reporter_phone: + try: + NotificationService.send_sms( + phone=observation.reporter_phone, + message=f"PX360: Your observation {observation.tracking_code} has been responded to. View: {track_url}", + related_object=observation, + ) + except Exception: + pass + if observation.reporter_email: + try: + NotificationService.send_email( + email=observation.reporter_email, + subject=f"PX360: Response to Your Observation {observation.tracking_code}", + message=f"Your observation has been responded to.\n\nView: {track_url}", + related_object=observation, + ) + except Exception: + pass + + return render(request, "observations/response_success_token.html", {"observation": observation}) + + return render(request, "observations/response_form_token.html", {"observation": observation}) diff --git a/apps/organizations/admin.py b/apps/organizations/admin.py index baeb255..f7e9b2c 100644 --- a/apps/organizations/admin.py +++ b/apps/organizations/admin.py @@ -4,7 +4,20 @@ Organizations admin from django.contrib import admin -from .models import Department, Hospital, Organization, Patient, Staff, Location, MainSection, SubSection +from .models import ( + Department, + Hospital, + LegacyHierarchyMapping, + LegacyLocation, + LegacyMainSection, + LegacySubSection, + Section, + OrgSubSection, + SubSection, + Organization, + Patient, + Staff, +) @admin.register(Organization) @@ -148,9 +161,9 @@ class StaffAdmin(admin.ModelAdmin): for staff in queryset: if not staff.user and staff.email: try: - user, was_created, password = StaffService.create_user_for_staff(staff, role='staff', request=request) - if was_created and password: - StaffService.send_credentials_email(staff, password, request) + user, was_created, _password = StaffService.create_user_for_staff(staff, role='staff', request=request) + if was_created and user: + StaffService.send_password_reset_email(staff, request) created += 1 except Exception as e: failed += 1 @@ -161,8 +174,8 @@ class StaffAdmin(admin.ModelAdmin): create_user_accounts.short_description = "Create user accounts for selected staff" - def send_credentials_emails(self, request, queryset): - """Admin action to send credential emails to selected staff""" + def send_password_reset_emails(self, request, queryset): + """Admin action to send password reset links to selected staff""" from .services import StaffService sent = 0 @@ -170,19 +183,16 @@ class StaffAdmin(admin.ModelAdmin): for staff in queryset: if staff.user and staff.email: try: - password = StaffService.generate_password() - staff.user.set_password(password) - staff.user.save() - StaffService.send_credentials_email(staff, password, request) + StaffService.send_password_reset_email(staff, request) sent += 1 except Exception as e: failed += 1 self.message_user( - request, f"Sent {sent} credential emails. Failed: {failed}", level="success" if failed == 0 else "warning" + request, f"Sent {sent} password reset emails. Failed: {failed}", level="success" if failed == 0 else "warning" ) - send_credentials_emails.short_description = "Send credential emails to selected staff" + send_password_reset_emails.short_description = "Send password reset links to selected staff" @admin.register(Patient) @@ -226,6 +236,9 @@ class PatientAdmin(admin.ModelAdmin): return qs.select_related("primary_hospital") -admin.site.register(Location) -admin.site.register(MainSection) +admin.site.register(LegacyLocation) +admin.site.register(LegacyMainSection) +admin.site.register(LegacySubSection) +admin.site.register(OrgSubSection) admin.site.register(SubSection) +admin.site.register(LegacyHierarchyMapping) diff --git a/apps/organizations/management/commands/backfill_department_hierarchy.py b/apps/organizations/management/commands/backfill_department_hierarchy.py new file mode 100644 index 0000000..8f0bf63 --- /dev/null +++ b/apps/organizations/management/commands/backfill_department_hierarchy.py @@ -0,0 +1,356 @@ +import re + +from django.core.management.base import BaseCommand +from django.db import transaction + +from apps.complaints.models import Complaint +from apps.organizations.models import ( + Department, + LegacyHierarchyMapping, + LegacyMainSection, + LegacySubSection, + Section, +) + + +def _normalize(s): + if not s: + return "" + s = s.lower().strip() + for suffix in [ + " clinics", + " clinic", + " department", + " dept", + " ward", + " unit", + " services", + ]: + s = s.replace(suffix, "") + s = re.sub(r"\s+", " ", s).strip() + return s + + +MANUAL_MAP = { + 2: "Internal Medicine", + 5: "Internal Medicine", + 6: "Internal Medicine", + 9: "Internal Medicine", + 11: "Internal Medicine", + 13: "Internal Medicine", + 14: "Internal Medicine", + 15: "Surgeries", + 18: "Surgeries", + 20: "Surgeries", + 21: "Surgeries", + 22: "Surgeries", + 23: "Surgeries", + 32: "Surgeries", + 34: "Emergency Department", + 35: "Critical Care", + 36: "Pediatric Department", + 37: "Pediatric Department", + 38: "Critical Care", + 39: "Pediatric Department", + 40: "Inpatient Department", + 41: "Inpatient Department", + 42: "Inpatient Department", + 43: "Anesthesia & OR Department", + 44: "Anesthesia & OR Department", + 24: "Surgeries", + 97: "Patient Relations & Patient Experience Department", + 45: "Surgeries", + 46: "Inpatient Department", + 47: "Obstetrics & Gynecology", + 48: "Inpatient Department", + 50: "Medical Ancillary Services", + 51: "Medical Ancillary Services", + 53: "Pharmacy", + 55: "Medical Ancillary Services", + 56: "Medical Ancillary Services", + 57: "Medical Ancillary Services", + 58: "Medical Ancillary Services", + 59: "Medical Ancillary Services", + 60: "Medical Ancillary Services", + 61: "Medical Ancillary Services", + 62: "Surgeries", + 78: "Nursing Department", + 79: "Nursing Department", + 88: "Outpatient Department", + 89: "Corporate Administration", + 90: "Finance Department", + 92: "Outpatient Department", + 94: "Outpatient Department", + 95: "Security Department", + 96: "Emergency Department", + 98: "Emergency Department", + 99: "Outpatient Department", + 100: "Emergency Department", + 102: "Medical Records Department", + 103: "Outpatient Department", + 104: "Medical Approvals Department", + 105: "Security Department", + 109: "Nursing Department", + 110: "Nursing Department", + 111: "Security Department", + 113: "Outpatient Department", + 147: "Internal Medicine", + 149: "Housekeeping & Hospitality Department", + 151: "Nursing Department", + 152: "Nursing Department", + 153: "Nursing Department", + 155: "Nursing Department", + 156: "Nursing Department", + 157: "Nursing Department", + 158: "Nursing Department", + 159: "Nursing Department", + 160: "Nursing Department", + 161: "Nursing Department", + 162: "Nursing Department", + 163: "Nursing Department", + 164: "Food Services Department", + 165: "Housekeeping & Hospitality Department", + 167: "Outpatient Department", + 168: "Corporate Administration", + 169: "Finance Department", + 170: "Patient Relations & Patient Experience Department", + 171: "Patient Affairs Department", + 172: "Inpatient Department", + 173: "Medical Approvals Department", + 174: "Executive Administration", + 176: "Obstetrics & Gynecology", + 177: "Nursing Department", + 178: "Nursing Department", + 180: "Laboratory", + 182: "Nursing Department", + 183: "Nursing Department", + 184: "Patient Affairs Department", + 185: "Nursing Department", + 186: "Radiology", + 187: "Medical Ancillary Services", + 188: "Internal Medicine", + 189: "Pediatric Department", + 190: "Pediatric Department", + 191: "Pediatric Department", + 192: "Pediatric Department", + 193: "Surgeries", + 194: "Surgeries", + 195: "Pediatric Department", + 196: "Pediatric Department", + 197: "Surgeries", + 198: "Obstetrics & Gynecology", + 199: "Internal Medicine", + 200: "Internal Medicine", + 201: "Medical Ancillary Services", + 202: "Pediatric Department", + 203: "Corporate Administration", + 205: "Pharmacy", + 206: "Medical Ancillary Services", + 207: "Facility Management & Maintenance", + 208: "Internal Medicine", + 209: "Pediatric Department", + 210: "Emergency Department", + 211: "Laboratory", + 212: "Surgeries", + 213: "Information Technology", + 215: "Outpatient Department", + 216: "Critical Care", + 219: "Laboratory", + 220: "Emergency Department", + 222: "Information Technology", + 223: "Outpatient Department", + 224: "Information Technology", + 225: "Medical Ancillary Services", + 226: "Medical Records Department", + 227: "Medical Ancillary Services", + 228: "Medical Records Department", + 229: "Inpatient Department", + 230: "Finance Department", +} + + +class Command(BaseCommand): + help = "Backfill complaint.department from legacy subsection hierarchy" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be updated without making changes", + ) + parser.add_argument( + "--backfill-section", + action="store_true", + help="Also backfill complaint.section from LegacyHierarchyMapping", + ) + parser.add_argument( + "--model", + type=str, + default="complaint", + choices=["complaint"], + help="Model to backfill (default: complaint)", + ) + + def handle(self, *args, **options): + dry_run = options["dry_run"] + backfill_section = options["backfill_section"] + + self._build_lookup_tables() + + if backfill_section: + self._backfill_sections(dry_run) + return + + complaints = Complaint.objects.filter( + department__isnull=True, + legacy_subsection__isnull=False, + ).select_related("legacy_subsection", "legacy_main_section") + + self.stdout.write(f"Complaints to backfill: {complaints.count()}") + + matched = 0 + unmatched = 0 + unmatched_details = {} + + with transaction.atomic(): + for complaint in complaints: + dept = self._resolve_department(complaint) + if dept: + matched += 1 + if not dry_run: + complaint.department = dept + complaint.save(update_fields=["department"]) + else: + unmatched += 1 + ss = complaint.legacy_subsection + key = (ss.name_en, ss.main_section.name_en if ss.main_section else "?") + unmatched_details[key] = unmatched_details.get(key, 0) + 1 + + if dry_run: + transaction.set_rollback(True) + + self.stdout.write(self.style.SUCCESS(f"\nMatched: {matched}")) + self.stdout.write(self.style.WARNING(f"Unmatched: {unmatched}")) + + if unmatched_details: + self.stdout.write("\nUnmatched subsections:") + for (name, ms), cnt in sorted( + unmatched_details.items(), key=lambda x: -x[1] + ): + self.stdout.write(f" {cnt:4d} | {ms[:15]:15s} | {name[:50]}") + + def _build_lookup_tables(self): + self.dept_by_name = {} + for d in Department.objects.all(): + self.dept_by_name[d.name_en.lower().strip()] = d + + self.dept_by_old_name = {} + self.section_by_old_name = {} + for m in LegacyHierarchyMapping.objects.select_related("main_section", "subsection"): + key = m.old_subsection_en.lower().strip() + if key and m.main_section: + self.dept_by_old_name[key] = m.main_section + if key and m.subsection: + self.section_by_old_name[key] = m.subsection + + self.norm_dept_names = {} + for name, dept in self.dept_by_name.items(): + self.norm_dept_names[_normalize(name)] = dept + + self.stdout.write( + f"Lookup tables: {len(self.dept_by_name)} depts, " + f"{len(self.dept_by_old_name)} legacy mappings, " + f"{len(self.section_by_old_name)} section mappings" + ) + + def _backfill_sections(self, dry_run): + """Backfill complaint.section from LegacyHierarchyMapping.""" + complaints = Complaint.objects.filter( + section__isnull=True, + legacy_subsection__isnull=False, + ).select_related("legacy_subsection") + + self.stdout.write(f"Complaints to backfill section: {complaints.count()}") + + matched = 0 + unmatched = 0 + + with transaction.atomic(): + for complaint in complaints: + section = self._resolve_section(complaint) + if section: + matched += 1 + if not dry_run: + complaint.section = section + complaint.save(update_fields=["section"]) + else: + unmatched += 1 + + if dry_run: + transaction.set_rollback(True) + + self.stdout.write(self.style.SUCCESS(f"\nSection matched: {matched}")) + self.stdout.write(self.style.WARNING(f"Section unmatched: {unmatched}")) + + def _resolve_section(self, complaint): + """Resolve section from legacy subsection via LegacyHierarchyMapping.""" + ss = complaint.legacy_subsection + if not ss: + return None + + # 1. Exact match via LegacyHierarchyMapping.subsection + ss_name = ss.name_en.lower().strip() + if ss_name in self.section_by_old_name: + return self.section_by_old_name[ss_name] + + # 2. Try to find section by name within the complaint's department + if complaint.department: + section = Section.objects.filter( + department=complaint.department, + name_en__iexact=ss.name_en, + ).first() + if section: + return section + + # Normalized match + norm_ss = _normalize(ss.name_en) + for sec in Section.objects.filter(department=complaint.department): + if _normalize(sec.name_en) == norm_ss: + return sec + + return None + + def _resolve_department(self, complaint): + ss = complaint.legacy_subsection + if not ss: + return None + + # 1. Manual override map (highest priority) + pk = ss.pk + if pk in MANUAL_MAP: + dept_name = MANUAL_MAP[pk] + return self.dept_by_name.get(dept_name.lower().strip()) + + # 2. Exact match via LegacyHierarchyMapping + ss_name = ss.name_en.lower().strip() + if ss_name in self.dept_by_old_name: + return self.dept_by_old_name[ss_name] + + # 3. Exact name match to new Department + if ss_name in self.dept_by_name: + return self.dept_by_name[ss_name] + + # 4. Normalized match + norm_ss = _normalize(ss.name_en) + if norm_ss in self.norm_dept_names: + return self.norm_dept_names[norm_ss] + + # 5. Contains match (one name contains the other) + for norm_dept, dept in self.norm_dept_names.items(): + if len(norm_ss) > 5 and ( + norm_ss in norm_dept or norm_dept in norm_ss + ): + if abs(len(norm_ss) - len(norm_dept)) < 15: + return dept + + return None diff --git a/apps/organizations/management/commands/export_departments_subsections.py b/apps/organizations/management/commands/export_departments_subsections.py index b75016c..a35dab5 100644 --- a/apps/organizations/management/commands/export_departments_subsections.py +++ b/apps/organizations/management/commands/export_departments_subsections.py @@ -3,7 +3,7 @@ import os from django.core.management.base import BaseCommand -from apps.organizations.models import Department, SubSection +from apps.organizations.models import Department, LegacySubSection class Command(BaseCommand): @@ -90,7 +90,7 @@ class Command(BaseCommand): "Main Section (EN)", "Main Section (AR)", ]) - qs = SubSection.objects.select_related( + qs = LegacySubSection.objects.select_related( "location", "main_section" ).order_by("location__name_en", "main_section__name_en", "name_en") for sub in qs: diff --git a/apps/organizations/management/commands/export_org_csv.py b/apps/organizations/management/commands/export_org_csv.py index fa94ba6..6cf319f 100644 --- a/apps/organizations/management/commands/export_org_csv.py +++ b/apps/organizations/management/commands/export_org_csv.py @@ -7,7 +7,7 @@ Usage: import csv import os from django.core.management.base import BaseCommand -from apps.organizations.models import Department, Location, MainSection, SubSection +from apps.organizations.models import Department, LegacyLocation, LegacyMainSection, LegacySubSection class Command(BaseCommand): @@ -46,33 +46,33 @@ class Command(BaseCommand): with open(locations_file, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(['ID', 'Name (EN)', 'Name (AR)']) - for loc in Location.objects.all(): + for loc in LegacyLocation.objects.all(): writer.writerow([ str(loc.id), loc.name_en or '', loc.name_ar or '' ]) - self.stdout.write(self.style.SUCCESS(f'Exported {Location.objects.count()} locations to {locations_file}')) + self.stdout.write(self.style.SUCCESS(f'Exported {LegacyLocation.objects.count()} locations to {locations_file}')) # Export main sections main_sections_file = os.path.join(output_dir, 'main_sections.csv') with open(main_sections_file, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(['ID', 'Name (EN)', 'Name (AR)']) - for section in MainSection.objects.all(): + for section in LegacyMainSection.objects.all(): writer.writerow([ str(section.id), section.name_en or '', section.name_ar or '' ]) - self.stdout.write(self.style.SUCCESS(f'Exported {MainSection.objects.count()} main sections to {main_sections_file}')) + self.stdout.write(self.style.SUCCESS(f'Exported {LegacyMainSection.objects.count()} main sections to {main_sections_file}')) # Export subsections subsections_file = os.path.join(output_dir, 'subsections.csv') with open(subsections_file, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(['Internal ID', 'Name (EN)', 'Name (AR)', 'Location (EN)', 'Location (AR)', 'Main Section (EN)', 'Main Section (AR)']) - for sub in SubSection.objects.all().select_related('location', 'main_section'): + for sub in LegacySubSection.objects.all().select_related('location', 'main_section'): writer.writerow([ str(sub.internal_id), sub.name_en or '', @@ -82,6 +82,6 @@ class Command(BaseCommand): sub.main_section.name_en if sub.main_section else '', sub.main_section.name_ar if sub.main_section else '' ]) - self.stdout.write(self.style.SUCCESS(f'Exported {SubSection.objects.count()} subsections to {subsections_file}')) + self.stdout.write(self.style.SUCCESS(f'Exported {LegacySubSection.objects.count()} subsections to {subsections_file}')) self.stdout.write(self.style.SUCCESS(f'\nAll exports completed in: {output_dir}')) diff --git a/apps/organizations/management/commands/import_areas.py b/apps/organizations/management/commands/import_areas.py new file mode 100644 index 0000000..5270ec1 --- /dev/null +++ b/apps/organizations/management/commands/import_areas.py @@ -0,0 +1,119 @@ +import re + +import pandas as pd +from django.core.management.base import BaseCommand +from django.db import transaction + +from apps.organizations.models import Area, Hospital + + +LOCATION_TYPE_MAP = { + "Inpatient": "IP", + "Outpatient": "OP", + "Emergency": "ER", +} + +AREAS_DATA = [ + ("Labor & Delivery", "IP"), + ("NICU - Neonatal Intensive Care Unit", "IP"), + ("Nursery", "IP"), + ("OB Wards", "IP"), + ("OPD1", "OP"), + ("OPD2", "OP"), + ("OPD3", "OP"), + ("OPD4", "OP"), + ("OPD5", "OP"), + ("OPD6", "OP"), + ("OPD7", "OP"), + ("Main OR", "IP"), + ("Recovery", "IP"), + ("Anesthesia", "GENERAL"), + ("Dialysis", "OP"), + ("Endoscopy", "GENERAL"), + ("ICCU", "IP"), + ("ICU", "IP"), + ("PICU", "IP"), + ("ICU Stepdown", "IP"), + ("PICU Stepdown", "IP"), + ("Stepdown 3-4", "IP"), + ("LTACU", "IP"), + ("Surgical Wards", "IP"), + ("Medical Ward", "IP"), + ("Pediatric Ward", "IP"), + ("Emergency", "ER"), +] + + +def _slugify(name): + slug = re.sub(r"[^a-zA-Z0-9]+", "_", name).strip("_").lower() + return slug + + +class Command(BaseCommand): + help = "Import Areas from the LOCATIONS1 sheet for all 3 hospitals" + + def add_arguments(self, parser): + parser.add_argument( + "--file", + default="data/Final List of Departments - 6th Version.xlsx", + help="Path to the departments Excel file", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be created without writing to DB", + ) + + def handle(self, *args, **options): + filepath = options["file"] + dry_run = options["dry_run"] + + hospitals = Hospital.objects.filter(status="active").order_by("code") + if not hospitals.exists(): + self.stderr.write(self.style.ERROR("No active hospitals found")) + return + + self.stdout.write(f"Found {hospitals.count()} hospitals: {list(hospitals.values_list('code', flat=True))}") + + created_count = 0 + skipped_count = 0 + + with transaction.atomic(): + for hospital in hospitals: + self.stdout.write(f"\nHospital: {hospital.code} ({hospital.name})") + + for name_en, location_type in AREAS_DATA: + code = _slugify(name_en) + + if dry_run: + self.stdout.write(f" [DRY RUN] Would create: {name_en} | code={code} | loc={location_type}") + created_count += 1 + continue + + area, created = Area.objects.get_or_create( + hospital=hospital, + code=code, + defaults={ + "name_en": name_en, + "location_type": location_type, + "status": "active", + }, + ) + + if created: + self.stdout.write(self.style.SUCCESS(f" Created: {name_en} ({location_type})")) + created_count += 1 + else: + if area.location_type != location_type: + area.location_type = location_type + area.save(update_fields=["location_type"]) + self.stdout.write(f" Updated location_type: {name_en} → {location_type}") + else: + skipped_count += 1 + + action = "Would create" if dry_run else "Created" + self.stdout.write(self.style.SUCCESS(f"\n{action}: {created_count} | Skipped (existing): {skipped_count}")) + + if not dry_run: + total = Area.objects.filter(status="active").count() + self.stdout.write(f"Total active Areas in DB: {total}") diff --git a/apps/organizations/management/commands/import_departments.py b/apps/organizations/management/commands/import_departments.py new file mode 100644 index 0000000..03cf1e2 --- /dev/null +++ b/apps/organizations/management/commands/import_departments.py @@ -0,0 +1,333 @@ +import re + +import pandas as pd +from django.core.management.base import BaseCommand +from django.db import transaction + +from apps.organizations.models import Department, Hospital, Section, Staff + + +CATEGORY_NORMALIZE = { + "Medical": "medical", + "Medical ": "medical", + "Admintrative": "administrative", + "Adminstrative": "administrative", + "Administrative": "administrative", + "Nursing": "nursing", + "Support Services": "support_services", + "Non-Medical": "non_medical", +} + +LOCATION_TYPE_MAP = { + "OP": "OP", + "IP": "IP", + "ER": "ER", + "GO": "GENERAL", +} + +ROLE_COLUMNS_DEPT = [ + ("manager_3rd", "3rd Manager"), + ("manager_2nd", "2nd Manager"), + ("deputy_manager", "Deputy Manger"), + ("champion", "Champion"), +] + +ROLE_COLUMNS_SECTION = [ + ("supervisor", "Supervisor\nHead Nurse\nHOD In Charge"), + ("deputy_supervisor", "Deputy Supervisor\nDeputy Head Nurse"), +] + +HOSPITAL_LOCATION_MAP = { + "HH-S": "السويدي", + "HH-N": "النزهة", + "HH-A": "العليا", +} + + +def _make_dept_code(hospital_code, dept_name): + prefix = hospital_code.lower().replace("-", "_") + slug = re.sub(r"[^a-zA-Z0-9]", "_", dept_name).strip("_").lower()[:80] + return f"{prefix}_{slug}" + + +def _make_sec_code(hospital_code, dept_name, sec_name): + prefix = hospital_code.lower().replace("-", "_") + dept_slug = re.sub(r"[^a-zA-Z0-9]", "_", dept_name).strip("_").lower()[:40] + sec_slug = re.sub(r"[^a-zA-Z0-9]", "_", sec_name).strip("_").lower()[:40] + return f"{prefix}_{dept_slug}__{sec_slug}" + + +def parse_employee_id(val): + if not val or not isinstance(val, str): + return None + val = val.strip() + if val == "?" or val == "": + return None + m = re.match(r"^(\d+)\s*-", val) + if m: + return m.group(1) + return None + + +def _build_eid_location_map(): + from apps.organizations.models import Staff as S + + return {s.employee_id: s.hospital.code for s in S.objects.select_related("hospital").all()} + + +def _find_staff_at_hospital(eid, hospital_code): + if not eid: + return None + try: + return Staff.objects.select_related("hospital").get(employee_id=eid, hospital__code=hospital_code) + except Staff.DoesNotExist: + return None + + +def _find_staff_at_hospital_fuzzy(name, hospital_code): + if not name: + return None + clean = re.sub(r"\s+", " ", name.strip()).upper() + for staff in Staff.objects.filter(hospital__code=hospital_code): + full = re.sub(r"\s+", " ", staff.name.strip()).upper() if staff.name else "" + if clean in full or full in clean: + return staff + return None + + +class Command(BaseCommand): + help = "Import departments and sections from PX360 Department Breakdown List Excel (multi-hospital)" + + def add_arguments(self, parser): + parser.add_argument("--file", default="Documents/PX360 - Department Breakdown List.xlsx") + parser.add_argument("--roles", action="store_true", help="Backfill role FKs (requires staff already imported)") + parser.add_argument("--hospital-code", required=True, help="Hospital code (HH-S, HH-N, HH-A)") + parser.add_argument("--all-hospitals", action="store_true", help="Import for all hospitals from breakdown list") + + def handle(self, *args, **options): + filepath = options["file"] + do_roles = options["roles"] + hospital_code = options["hospital_code"] + + df = pd.read_excel(filepath, header=0) + df["Main Section"] = df["Main Section"].fillna("").str.strip() + + if options["all_hospitals"]: + for code in ["HH-S", "HH-N"]: + try: + hospital = Hospital.objects.get(code=code) + except Hospital.DoesNotExist: + self.stderr.write(f"Hospital {code} not found, skipping") + continue + if do_roles: + self._backfill_roles(df, code) + else: + self._import_departments_and_sections(df, hospital, code) + else: + try: + hospital = Hospital.objects.get(code=hospital_code) + except Hospital.DoesNotExist: + self.stderr.write(f"Hospital {hospital_code} not found. Create it first.") + return + + if do_roles: + self._backfill_roles(df, hospital_code) + else: + self._import_departments_and_sections(df, hospital, hospital_code) + + @transaction.atomic + def _import_departments_and_sections(self, df, hospital, hospital_code): + df["Main Section"] = df["Main Section"].fillna("").str.strip() + + dept_names = df["Department Name"].dropna().unique() + self.stdout.write(f"[{hospital_code}] Found {len(dept_names)} departments in Excel") + + dept_count = 0 + sec_count = 0 + + for dept_name in sorted(dept_names): + dept_rows = df[df["Department Name"] == dept_name] + first_row = dept_rows.iloc[0] + + cat_raw = first_row["Main Section"] + category = CATEGORY_NORMALIZE.get(cat_raw, "") + + code = _make_dept_code(hospital_code, dept_name) + + dept, created = Department.objects.update_or_create( + hospital=hospital, + code=code, + defaults={ + "name": dept_name, + "name_en": dept_name, + "name_ar": "", + "category": category, + "location_type": "", + "status": "active", + }, + ) + dept_count += 1 + action = "Created" if created else "Updated" + self.stdout.write(f" {action} dept: {dept_name} [{category}]") + + unique_sections = dept_rows.dropna(subset=["Section"]) + seen = set() + for _, sec_row in unique_sections.iterrows(): + sec_name = str(sec_row["Section"]).strip() + if not sec_name or sec_name in seen: + continue + seen.add(sec_name) + + loc_type_raw = str(sec_row.get("LOCATION TYPE", "")).strip() + loc_type = LOCATION_TYPE_MAP.get(loc_type_raw, "") + + area = str(sec_row.get("AREA", "")).strip() if pd.notna(sec_row.get("AREA")) else "" + zone = str(sec_row.get("ZONE", "")).strip() if pd.notna(sec_row.get("ZONE")) else "" + floor = str(sec_row.get("FLOOR", "")).strip() if pd.notna(sec_row.get("FLOOR")) else "" + + sub_location = f"{area}/{zone}".strip("/") if area or zone else "" + + display_en = str(sec_row.get("Name (EN)", "")).strip() if pd.notna(sec_row.get("Name (EN)")) else "" + display_ar = str(sec_row.get("Name (AR)", "")).strip() if pd.notna(sec_row.get("Name (AR)")) else "" + if display_en == "nan": + display_en = "" + if display_ar == "nan": + display_ar = "" + + sec_code = _make_sec_code(hospital_code, dept_name, sec_name) + + Section.objects.update_or_create( + department=dept, + code=sec_code, + defaults={ + "name_en": sec_name, + "name_ar": "", + "location_type": loc_type, + "sub_location": sub_location, + "floor": floor, + "display_name_en": display_en, + "display_name_ar": display_ar, + "status": "active", + }, + ) + sec_count += 1 + + self.stdout.write(self.style.SUCCESS(f"[{hospital_code}] Done: {dept_count} departments, {sec_count} sections")) + + @transaction.atomic + def _backfill_roles(self, df, hospital_code): + self.stdout.write(f"[{hospital_code}] Backfilling roles...") + staff_count = Staff.objects.filter(hospital__code=hospital_code).count() + self.stdout.write(f" Staff at {hospital_code}: {staff_count}") + if staff_count == 0: + self.stderr.write(f" No staff at {hospital_code}, skipping") + return + + target_location = HOSPITAL_LOCATION_MAP.get(hospital_code) + + eid_loc_map = _build_eid_location_map() + + dept_names = df["Department Name"].dropna().unique() + dept_updated = 0 + sec_updated = 0 + unmatched = [] + + for dept_name in sorted(dept_names): + dept_rows = df[df["Department Name"] == dept_name] + + code = _make_dept_code(hospital_code, dept_name) + try: + dept = Department.objects.get(code=code) + except Department.DoesNotExist: + continue + + # Collect all unique role values across all rows for this dept + for field_name, col_name in ROLE_COLUMNS_DEPT: + all_vals = dept_rows[col_name].dropna().unique() + staff = None + for raw_val in all_vals: + raw_val = str(raw_val).strip() + if raw_val == "?" or not raw_val: + continue + eid = parse_employee_id(raw_val) + if eid: + loc = eid_loc_map.get(eid) + if loc == target_location: + staff = _find_staff_at_hospital(eid, hospital_code) + if staff: + break + if not staff: + for raw_val in all_vals: + raw_val = str(raw_val).strip() + if raw_val == "?" or not raw_val: + continue + eid = parse_employee_id(raw_val) + if eid: + staff = _find_staff_at_hospital(eid, hospital_code) + if staff: + break + if not staff and len(all_vals) > 0: + staff = _find_staff_at_hospital_fuzzy(str(all_vals[0]), hospital_code) + + if staff: + setattr(dept, field_name, staff) + else: + for raw_val in all_vals[:1]: + unmatched.append(f"DEPT {hospital_code}/{dept_name} {field_name}: {raw_val}") + + dept.save() + dept_updated += 1 + + # Section-level roles + unique_sections = dept_rows.dropna(subset=["Section"]) + seen = set() + for _, sec_row in unique_sections.iterrows(): + sec_name = str(sec_row["Section"]).strip() + if not sec_name or sec_name in seen: + continue + seen.add(sec_name) + + sec_code = _make_sec_code(hospital_code, dept_name, sec_name) + try: + section = Section.objects.get(code=sec_code) + except Section.DoesNotExist: + continue + + changed = False + for field_name, col_name in ROLE_COLUMNS_SECTION: + raw_val = sec_row.get(col_name) + if pd.isna(raw_val): + continue + raw_val = str(raw_val).strip() + if raw_val == "?" or not raw_val: + continue + + eid = parse_employee_id(raw_val) + staff = None + if eid: + loc = eid_loc_map.get(eid) + if loc == target_location: + staff = _find_staff_at_hospital(eid, hospital_code) + if not staff and eid: + staff = _find_staff_at_hospital(eid, hospital_code) + if not staff: + staff = _find_staff_at_hospital_fuzzy(raw_val, hospital_code) + + if staff: + setattr(section, field_name, staff) + changed = True + else: + unmatched.append(f"SEC {hospital_code}/{dept_name}/{sec_name} {field_name}: {raw_val}") + + if changed: + section.save() + sec_updated += 1 + + self.stdout.write( + self.style.SUCCESS(f"[{hospital_code}] Done: {dept_updated} depts, {sec_updated} sections updated with roles") + ) + + if unmatched: + self.stdout.write(self.style.WARNING(f"\n[{hospital_code}] {len(unmatched)} unmatched roles:")) + for u in unmatched[:50]: + self.stdout.write(f" {u}") diff --git a/apps/organizations/management/commands/import_departments_excel.py b/apps/organizations/management/commands/import_departments_excel.py new file mode 100644 index 0000000..82472c6 --- /dev/null +++ b/apps/organizations/management/commands/import_departments_excel.py @@ -0,0 +1,259 @@ +import re +import uuid + +import pandas as pd +from django.core.management.base import BaseCommand +from django.db import transaction + +from apps.organizations.models import ( + Department, + Hospital, + LegacyHierarchyMapping, + Section, + OrgSubSection, + Staff, +) + + +CATEGORY_MAP = { + "medical": "medical", + "adminstrative": "administrative", + "admintrative": "administrative", + "nursing": "nursing", + "support services": "support_services", +} + + +def _clean(val): + if pd.isna(val): + return "" + return str(val).strip() + + +def _clean_multiline(val): + text = _clean(val) + text = text.replace("\n", " ").replace("\r", " ") + text = re.sub(r"\s+", " ", text) + return text.strip() + + +def _extract_employee_id(name_str): + if not name_str: + return None + match = re.match(r"(\d+)\s*-", name_str) + if match: + return match.group(1) + return None + + +def _resolve_staff(name_str): + if not name_str: + return None + emp_id = _extract_employee_id(name_str) + if emp_id: + staff = Staff.objects.filter(employee_id=emp_id).first() + if staff: + return staff + return None + + +def _normalize_category(raw): + if not raw: + return "" + return CATEGORY_MAP.get(raw.lower(), raw.lower()) + + +class Command(BaseCommand): + help = "Import departments, sections, and sub-sections from the Final List of Departments Excel file (4th Version)" + + def add_arguments(self, parser): + parser.add_argument( + "--file", + type=str, + default="data/Final List of Departments - 4th Version.xlsx", + help="Path to the Excel file", + ) + parser.add_argument( + "--hospital-code", + type=str, + default="HH-NZ", + help="Hospital code to assign departments to", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be created without creating it", + ) + + def handle(self, *args, **options): + file_path = options["file"] + hospital_code = options["hospital_code"] + dry_run = options["dry_run"] + + hospital = Hospital.objects.filter(code=hospital_code).first() + if not hospital: + self.stderr.write(self.style.ERROR(f"Hospital with code '{hospital_code}' not found")) + return + + self.stdout.write(f"Importing from: {file_path}") + self.stdout.write(f"Hospital: {hospital.name} ({hospital.code})") + + df = pd.read_excel(file_path, sheet_name="ALL LIST") + + stats = {"departments": 0, "sections": 0, "sub_sections": 0, "mappings": 0, "skipped": 0} + + with transaction.atomic(): + if dry_run: + self.stdout.write(self.style.WARNING("DRY RUN - no changes will be saved")) + + dept_cache = {} + + for idx, row in df.iterrows(): + dept_name = _clean(row.get("Department")) + if not dept_name: + stats["skipped"] += 1 + continue + + section_name = _clean(row.get("Section")) + subsection_name = _clean(row.get("Sub-Section")) + area_raw = _clean(row.get("Main Section")) + + dept_key = dept_name + if dept_key not in dept_cache: + code = re.sub(r"[^a-zA-Z0-9]", "_", dept_name.lower())[:100] + category = _normalize_category(area_raw) if area_raw else "" + + defaults = { + "name": dept_name, + "name_en": dept_name, + "category": category, + "main_section": area_raw, + "location_type": self._normalize_location_type(_clean(row.get("Department\nLocation"))), + "sub_location": _clean(row.get("Department\nSub-Location")), + "floor": _clean(row.get("Department\nFloor")), + "manager_3rd": _resolve_staff(_clean(row.get("3rd Manager"))), + "manager_2nd": _resolve_staff(_clean_multiline(row.get("2nd Manager"))), + "deputy_manager": _resolve_staff(_clean(row.get("Deputy Manger"))), + "supervisor": _resolve_staff(_clean(row.get("Supervisor\nHead Nurse\nHOD In Charge"))), + "deputy_supervisor": _resolve_staff( + _clean(row.get("Deputy Supervisor\nDeputy Head Nurse")) + ), + "champion_email": _clean(row.get("Champion Email")), + "old_name_en": _clean(row.get("Unnamed: 22")), + "old_name_ar": _clean(row.get("Unnamed: 23")), + } + + if not dry_run: + existing = Department.objects.filter( + hospital=hospital, name_en=dept_name + ).first() + if existing: + for k, v in defaults.items(): + setattr(existing, k, v) + existing.save() + dept_cache[dept_key] = existing + self.stdout.write(f" ~ Department: {dept_name} (updated)") + else: + dept = Department.objects.create( + hospital=hospital, + code=code, + **defaults, + ) + dept_cache[dept_key] = dept + stats["departments"] += 1 + self.stdout.write(f" + Department: {dept_name} (code={code})") + else: + stats["departments"] += 1 + dept_cache[dept_key] = dept_name + self.stdout.write(f" [DRY] Department: {dept_name} (code={code}, area={area})") + + department = dept_cache.get(dept_key) + if not department: + continue + + if section_name and not subsection_name: + if not dry_run: + sec_code = f"{department.code}__{re.sub(r'[^a-zA-Z0-9]', '_', section_name.lower())[:100]}" + defaults = { + "name_en": section_name, + "location_type": self._normalize_location_type( + _clean(row.get("Department\nLocation")) + ), + "sub_location": _clean(row.get("Department\nSub-Location")), + "floor": _clean(row.get("Department\nFloor")), + "status": "active", + } + sec, created = Section.objects.update_or_create( + department=department, + code=sec_code, + defaults=defaults, + ) + if created: + stats["sections"] += 1 + self.stdout.write(f" + Section: {section_name}") + else: + stats["sections"] += 1 + self.stdout.write(f" [DRY] Section: {section_name}") + + + old_name_en_raw = _clean_multiline(row.get("Unnamed: 22")) + old_name_ar_raw = _clean_multiline(row.get("Unnamed: 23")) + old_loc_en = _clean_multiline(row.get("Unnamed: 24")) + old_loc_ar = _clean_multiline(row.get("Unnamed: 25")) + old_area_en = _clean_multiline(row.get("Unnamed: 26")) + old_area_ar = _clean_multiline(row.get("Unnamed: 27")) + + if old_name_en_raw or old_name_ar_raw: + old_names = [n.strip() for n in re.split(r'[&\n]', old_name_en_raw) if n.strip()] + old_locs = [n.strip() for n in re.split(r'[&\n]', old_loc_en) if n.strip()] + old_areas = [n.strip() for n in re.split(r'[&\n]', old_area_en) if n.strip()] + + if not old_names: + old_names = [""] + + if not dry_run: + for i, old_sub_name in enumerate(old_names): + loc = old_locs[i] if i < len(old_locs) else (old_locs[0] if old_locs else "") + area = old_areas[i] if i < len(old_areas) else (old_areas[0] if old_areas else "") + LegacyHierarchyMapping.objects.update_or_create( + old_location_ar="", + old_main_section_ar=area, + old_subsection_ar=old_sub_name, + defaults={ + "old_location_en": loc, + "old_main_section_en": area, + "old_subsection_en": old_sub_name, + "main_section": department, + }, + ) + stats["mappings"] += 1 + + if dry_run: + transaction.set_rollback(True) + self.stdout.write(self.style.WARNING("DRY RUN - all changes rolled back")) + + self.stdout.write(self.style.SUCCESS( + f"\nImport complete: {stats['departments']} departments, " + f"{stats['sections']} sections, {stats['sub_sections']} sub-sections, " + f"{stats['mappings']} legacy mappings, {stats['skipped']} skipped" + )) + + def _normalize_location_type(self, raw): + if not raw: + return "" + raw_lower = raw.lower().replace("\n", " ").replace("\r", " ") + if "op" in raw_lower and "ip" in raw_lower and "er" in raw_lower: + return "OP,IP,ER" + if "op" in raw_lower and "ip" in raw_lower: + return "OP,IP" + if "op" in raw_lower and "er" in raw_lower: + return "OP,ER" + if "general" in raw_lower or "offices" in raw_lower: + return "GENERAL" + if "ip" in raw_lower or "inpatient" in raw_lower: + return "IP" + if "er" in raw_lower or "emergency" in raw_lower: + return "ER" + if "op" in raw_lower or "outpatient" in raw_lower: + return "OP" + return raw[:20] diff --git a/apps/organizations/management/commands/import_staff_excel.py b/apps/organizations/management/commands/import_staff_excel.py new file mode 100644 index 0000000..6cf9cfe --- /dev/null +++ b/apps/organizations/management/commands/import_staff_excel.py @@ -0,0 +1,280 @@ +import re + +import pandas as pd +from django.core.management.base import BaseCommand +from django.db.models import Q +from django.db import transaction + +from apps.organizations.models import Department, Hospital, Staff + +LOCATION_TO_HOSPITAL = { + "Suwaidi": "HH-S", + "Nuzha": "HH-N", + "Olaya": "HH-A", + "السويدي": "HH-S", + "النزهة": "HH-N", + "العليا": "HH-A", +} + +HR_NAME_ALIASES = { + "Accident And Emergency": "Emergency Medicine Department", + "Corporate Administration": "Executive Administration", + "Senior Management Offices": "Executive Administration", + "Corporate Communication Department": "Patient Experience Department", + "Marketing Department": "Patient Experience Department", + "Porter Department": "Security Department", + "Transportation Department": "Security Department", + "Supply Chain": "Facility Management & Maintenance Department", + "Pharmacy Warehouse Alaziziyah": "Pharmacy Department", + "Family Medicine": "Outpatient Department", + "Business Development": "Executive Administration", + "Continuous Medical Education Managment": "Medical Administration", + "Academic Education And Training Affairs": "Medical Administration", + "Innovation Communication Management": "Medical Administration", + "Transformation And Change Management": "Medical Administration", + "Talent Acquisition Department": "HR Department", + "Internal Audit": "Financial Collection & Claims Department", + "Legal Affairs Department": "Executive Administration", + "Cybersecurity Management": "Information Technology Department", + "Rehabilitation Center": "Outpatient Department", +} + + +def normalize_name(name): + if not name: + return "" + return re.sub(r"\s+", " ", name.strip()).strip() + + +class Command(BaseCommand): + help = "Import staff from employees.xlsx (both Arabic Sheet1 + English Sheet2)" + + def add_arguments(self, parser): + parser.add_argument("--file", default="data/employees.xlsx") + parser.add_argument("--update-existing", action="store_true") + + def handle(self, *args, **options): + filepath = options["file"] + update_existing = options["update_existing"] + + hospitals = {h.code: h for h in Hospital.objects.all()} + if not hospitals: + self.stderr.write("No hospitals found. Create them first.") + return + + self.stdout.write(f"Available hospitals: {list(hospitals.keys())}") + + df_ar = pd.read_excel(filepath, header=None, skiprows=3, sheet_name=0) + df_ar.columns = [ + "_skip", "employee_number", "name_ar", "manager", "national_id", + "location_ar", "department_ar", "section_ar", "job_title_ar", + "mobile", "personal_email", "work_email", + ] + df_ar = df_ar.drop(columns=["_skip"]) + df_ar = df_ar[df_ar["employee_number"].notna()] + df_ar = df_ar[df_ar["name_ar"].notna()] + df_ar["employee_id"] = df_ar["employee_number"].apply( + lambda x: str(int(x)) if pd.notna(x) and str(x).strip().replace(".", "").isdigit() else None + ) + + df_en = pd.read_excel(filepath, header=None, skiprows=3, sheet_name="Sheet2") + df_en.columns = [ + "_skip", "employee_number", "name", "manager", "national_id", + "location", "department", "section", "job_title", "country", + "mobile", "personal_email", "work_email", + ] + df_en = df_en.drop(columns=["_skip"]) + df_en = df_en[df_en["employee_number"].notna()] + df_en = df_en[df_en["name"].notna()] + df_en["employee_id"] = df_en["employee_number"].apply( + lambda x: str(int(x)) if pd.notna(x) and str(x).strip().replace(".", "").isdigit() else None + ) + + en_lookup = {} + for _, row in df_en.iterrows(): + eid = row.get("employee_id") + if eid: + en_lookup[eid] = row + + self.stdout.write(f"Arabic records: {len(df_ar)}, English records: {len(df_en)}") + + dept_cache = {} + stats = { + "created": 0, "updated": 0, "skipped": 0, + "no_dept": 0, "no_hospital": 0, "errors": 0, + } + staff_map = {} + existing_staff = {s.employee_id: s for s in Staff.objects.all()} + + with transaction.atomic(): + for idx, (_, row_ar) in enumerate(df_ar.iterrows(), 1): + try: + emp_id = row_ar.get("employee_id") + if not emp_id: + continue + + name_ar_raw = normalize_name(str(row_ar.get("name_ar", ""))) + if not name_ar_raw: + continue + + parts_ar = name_ar_raw.split(None, 1) + first_name_ar = parts_ar[0] if parts_ar else "" + last_name_ar = parts_ar[1] if len(parts_ar) > 1 else "" + + row_en = en_lookup.get(emp_id) + + if row_en is not None: + name_en = normalize_name(str(row_en.get("name", ""))) + parts_en = name_en.split(None, 1) + first_name = parts_en[0] if parts_en else "" + last_name = parts_en[1] if len(parts_en) > 1 else "" + location_en = normalize_name(str(row_en.get("location", ""))) + dept_en = normalize_name(str(row_en.get("department", ""))) + section_en = normalize_name(str(row_en.get("section", ""))) + job_title_en = normalize_name(str(row_en.get("job_title", ""))) + country = normalize_name(str(row_en.get("country", ""))) + mobile_en = normalize_name(str(row_en.get("mobile", ""))) + work_email_en = normalize_name(str(row_en.get("work_email", ""))) + personal_email_en = normalize_name(str(row_en.get("personal_email", ""))) + else: + name_en = "" + first_name = first_name_ar + last_name = last_name_ar + location_en = "" + dept_en = "" + section_en = "" + job_title_en = "" + country = "" + mobile_en = "" + work_email_en = "" + personal_email_en = "" + + manager_raw = str(row_ar.get("manager", "")).strip() if pd.notna(row_ar.get("manager")) else "" + national_id = str(row_ar.get("national_id", "")).strip() if pd.notna(row_ar.get("national_id")) else "" + job_title_ar_val = normalize_name(str(row_ar.get("job_title_ar", ""))) + dept_ar = normalize_name(str(row_ar.get("department_ar", ""))) + section_ar = normalize_name(str(row_ar.get("section_ar", ""))) + location_ar = normalize_name(str(row_ar.get("location_ar", ""))) + mobile_ar = normalize_name(str(row_ar.get("mobile", ""))) + personal_email_ar = normalize_name(str(row_ar.get("personal_email", ""))) + work_email_ar = normalize_name(str(row_ar.get("work_email", ""))) + + hospital_code = LOCATION_TO_HOSPITAL.get(location_en) or LOCATION_TO_HOSPITAL.get(location_ar) + hospital = hospitals.get(hospital_code) if hospital_code else None + + if not hospital: + stats["no_hospital"] += 1 + continue + + department = None + if dept_en and hospital_code: + cache_key = (hospital_code, dept_en.lower()) + if cache_key not in dept_cache: + dept_cache[cache_key] = self._find_dept( + hospitals[hospital_code], dept_en + ) + department = dept_cache[cache_key] + + if not department and dept_en: + stats["no_dept"] += 1 + + existing = existing_staff.get(emp_id) + + staff_data = { + "name": name_en or name_ar_raw, + "first_name": first_name or first_name_ar, + "last_name": last_name or last_name_ar, + "name_ar": name_ar_raw, + "first_name_ar": first_name_ar, + "last_name_ar": last_name_ar, + "staff_type": "other", + "department_type": "", + "job_title": job_title_en or job_title_ar_val, + "job_title_ar": job_title_ar_val, + "specialization": "", + "email": work_email_en or personal_email_en or work_email_ar or personal_email_ar, + "phone": mobile_en or mobile_ar, + "hospital": hospital, + "department": department, + "civil_id": national_id, + "location": location_en or location_ar, + "location_ar": location_ar, + "department_name": dept_en or dept_ar, + "department_name_ar": dept_ar, + "section": section_en or section_ar, + "section_ar": section_ar, + "subsection": "", + "subsection_ar": "", + "country": country, + "status": "active", + } + + if existing and update_existing: + for k, v in staff_data.items(): + setattr(existing, k, v) + existing.save() + staff_map[emp_id] = existing + stats["updated"] += 1 + elif existing: + staff_map[emp_id] = existing + stats["skipped"] += 1 + else: + staff = Staff(employee_id=emp_id, **staff_data) + staff.save() + staff_map[emp_id] = staff + stats["created"] += 1 + + if idx % 500 == 0: + self.stdout.write(f" Processed {idx}/{len(df_ar)}...") + + except Exception as e: + self.stdout.write(self.style.ERROR(f" [{idx}] Error: {e}")) + stats["errors"] += 1 + + self.stdout.write("\nLinking managers...") + manager_lookup = {} + for _, row in df_ar.iterrows(): + eid = row.get("employee_id") + mgr_raw = str(row.get("manager", "")).strip() if pd.notna(row.get("manager")) else "" + if eid and mgr_raw: + manager_lookup[eid] = mgr_raw + + linked = 0 + for emp_id, staff in staff_map.items(): + manager_raw = manager_lookup.get(emp_id, "") + if not manager_raw: + continue + m = re.match(r"^(\d+)\s*-", manager_raw) + if not m: + continue + mgr_id = m.group(1) + mgr = staff_map.get(mgr_id) + if mgr and staff.report_to != mgr: + staff.report_to = mgr + staff.save(update_fields=["report_to"]) + linked += 1 + + self.stdout.write(f" Linked {linked} manager relationships") + + self.stdout.write(self.style.SUCCESS(f"\nDone!")) + self.stdout.write(f" Created: {stats['created']}") + self.stdout.write(f" Updated: {stats['updated']}") + self.stdout.write(f" Skipped: {stats['skipped']}") + self.stdout.write(f" No dept match: {stats['no_dept']}") + self.stdout.write(f" No hospital match: {stats['no_hospital']}") + self.stdout.write(f" Errors: {stats['errors']}") + + def _find_dept(self, hospital, hr_dept_name): + dept = Department.objects.filter( + hospital=hospital, status="active" + ).filter( + Q(hr_name__iexact=hr_dept_name) | Q(name_en__iexact=hr_dept_name) + ).first() + if dept: + return dept + alias_name_en = HR_NAME_ALIASES.get(hr_dept_name) + if alias_name_en: + return Department.objects.filter( + hospital=hospital, name_en__iexact=alias_name_en, status="active" + ).first() + return None diff --git a/apps/organizations/management/commands/populate_location_data.py b/apps/organizations/management/commands/populate_location_data.py index b3a6c5f..4f659a0 100644 --- a/apps/organizations/management/commands/populate_location_data.py +++ b/apps/organizations/management/commands/populate_location_data.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from apps.organizations.models import Location, MainSection, SubSection +from apps.organizations.models import LegacyLocation, LegacyMainSection, LegacySubSection class Command(BaseCommand): @@ -1102,13 +1102,13 @@ class Command(BaseCommand): # Create Locations for loc in locations_data: - Location.objects.update_or_create( + LegacyLocation.objects.update_or_create( id=loc["id"], defaults={"name_ar": loc["name_ar"], "name_en": loc["name_en"]} ) # Create Main Sections for sec in main_sections_data: - MainSection.objects.update_or_create( + LegacyMainSection.objects.update_or_create( id=sec["id"], defaults={"name_ar": sec["name_ar"], "name_en": sec["name_en"]} ) @@ -1116,14 +1116,14 @@ class Command(BaseCommand): # Clear existing data to prevent old ID conflicts (skip if referenced) try: - SubSection.objects.all().delete() + LegacySubSection.objects.all().delete() except Exception: self.stdout.write(self.style.WARNING("Skipping SubSection deletion - some are referenced by complaints")) for item in subsections_data: subsections_to_create.append( - SubSection( - internal_id=int(item["id"]), # Use 'id' as the internal_id primary key + LegacySubSection( + internal_id=int(item["id"]), name_en=item["name_en"], name_ar=item["name_ar"], location_id=int(item["location_id"]), @@ -1132,7 +1132,7 @@ class Command(BaseCommand): ) # Use bulk_create for speed - SubSection.objects.bulk_create(subsections_to_create, ignore_conflicts=True) + LegacySubSection.objects.bulk_create(subsections_to_create, ignore_conflicts=True) # Bulk Create SubSections # objs = [ # SubSection( diff --git a/apps/organizations/management/commands/seed_staff.py b/apps/organizations/management/commands/seed_staff.py index 5cc14c8..696140b 100644 --- a/apps/organizations/management/commands/seed_staff.py +++ b/apps/organizations/management/commands/seed_staff.py @@ -415,12 +415,12 @@ class Command(BaseCommand): self.style.SUCCESS(f" ✓ Created user: {user.email} (role: {role})") ) - # Send credential email if requested + # Send password reset email if requested if send_email: try: - StaffService.send_credentials_email(staff, password, request) + StaffService.send_password_reset_email(staff, request) self.stdout.write( - self.style.SUCCESS(f" ✓ Sent credential email to: {email}") + self.style.SUCCESS(f" ✓ Sent password reset email to: {email}") ) except Exception as email_error: self.stdout.write( diff --git a/apps/organizations/management/commands/set_hr_names.py b/apps/organizations/management/commands/set_hr_names.py new file mode 100644 index 0000000..77fb7cd --- /dev/null +++ b/apps/organizations/management/commands/set_hr_names.py @@ -0,0 +1,64 @@ +from django.core.management.base import BaseCommand +from apps.organizations.models import Department + +HR_NAME_MAP = { + "Anesthesia And Or": "Anesthesia Department", + "Critical Care": "Critical Care Department", + "Dermatology": "Dermatology Department", + "Facility Management & Maintenance": "Facility Management & Maintenance Department", + "Infection Control": "Infection Control Department", + "Information Technology": "Information Technology Department", + "Internal Medicine": "Internal Medicine Department", + "Laboratory And Blood Bank": "Laboratory Department", + "Medical Ancillary Services": "Medical Ancillary Services Department", + "Obstetrics And Gynecology": "Obstetrics & Gynecology Department", + "Oncology": "ONCOLOGY Department", + "Ophthalmology": "Ophthalmology Department", + "Radiology": "Radiology Department", + "Dentistry": "Dental Department", + "Surgeries": "Surgery Department", + "Pediatrics": "Pediatric Department", + "Emergency Department": "Emergency Medicine Department", + "Accident And Emergency": "Emergency Medicine Department", + "Human Resource": "HR Department", + "Support Services": "Support Services Department", + "Finance Department": "Financial Collection & Claims Department", + "Corporate Administration": "Executive Administration", + "Senior Management Offices": "Executive Administration", + "Patient Relations & Patient Experience Department": "Patient Experience Department", + "Porter Department": "Security Department", + "Transportation Department": "Security Department", + "Corporate Communication Department": "Patient Experience Department", + "Marketing Department": "Patient Experience Department", + "Supply Chain": "Facility Management & Maintenance Department", + "Pharmacy Warehouse Alaziziyah": "Pharmacy Department", + "Family Medicine": "Outpatient Department", + "Business Development": "Executive Administration", + "Continuous Medical Education Managment": "Medical Administration", + "Academic Education And Training Affairs": "Medical Administration", + "Innovation Communication Management": "Medical Administration", + "Transformation And Change Management": "Medical Administration", + "Talent Acquisition Department": "HR Department", + "Internal Audit": "Financial Collection & Claims Department", + "Legal Affairs Department": "Executive Administration", + "Cybersecurity Management": "Information Technology Department", + "Rehabilitation Center": "Outpatient Department", +} + + +class Command(BaseCommand): + help = "Set hr_name on Department records from HR system name mapping" + + def handle(self, *args, **options): + updated = 0 + for hr_name, name_en in HR_NAME_MAP.items(): + depts = Department.objects.filter( + name_en__iexact=name_en, status="active", hr_name="" + ) + for dept in depts: + dept.hr_name = hr_name + dept.save(update_fields=["hr_name"]) + updated += 1 + self.stdout.write(f" {dept.code}: hr_name={repr(hr_name)}") + + self.stdout.write(self.style.SUCCESS(f"\nUpdated {updated} departments")) diff --git a/apps/organizations/management/commands/wipe_dev_data.py b/apps/organizations/management/commands/wipe_dev_data.py new file mode 100644 index 0000000..0a8f33a --- /dev/null +++ b/apps/organizations/management/commands/wipe_dev_data.py @@ -0,0 +1,123 @@ +from django.core.management.base import BaseCommand +from django.db import connection + + +class Command(BaseCommand): + help = "Wipe all dev data: complaints, observations, appreciation, feedback, staff, departments, sections, patients, integrations" + + def add_arguments(self, parser): + parser.add_argument("--confirm", action="store_true", help="Confirm data wipe") + parser.add_argument("--keep-org", action="store_true", help="Keep hospitals, departments, sections, staff") + + def handle(self, *args, **options): + if not options["confirm"]: + self.stdout.write(self.style.ERROR("Add --confirm to confirm data wipe")) + return + + keep_org = options.get("keep_org", False) + + truncate_order = [ + "complaints_complaintupdate", + "complaints_complaintattachment", + "complaints_complaintinvolvedstaff", + "complaints_complaintinvolveddepartment", + "complaints_complaintadverseaction_involved_staff", + "complaints_complaintadverseaction", + "complaints_complaintadverseactionattachment", + "complaints_complaintcommunication", + "complaints_complaintexplanation", + "complaints_explanationattachment", + "complaints_complaintmeeting", + "complaints_complaintprinteraction", + "complaints_departmentmanagerreview", + "complaints_managerreviewanswer", + "complaints_complaint", + "complaints_inquiryattachment", + "complaints_inquiryexplanation", + "complaints_inquiryexplanationattachment", + "complaints_inquiryupdate", + "complaints_inquiry", + "complaints_governmentticket", + "complaints_patientcomplaintsession", + "complaints_oncalladminschedule", + "complaints_oncalladmin", + "observations_observationattachment", + "observations_observationnote", + "observations_observationstatuslog", + "observations_observation", + "appreciation_userbadge", + "appreciation_appreciationbadge", + "appreciation_appreciationstats", + "appreciation_appreciation", + "feedback_feedbackattachment", + "feedback_feedbackresponse", + "feedback_commentactionplan", + "feedback_commentimport", + "feedback_patientcomment", + "feedback_feedback", + "dashboard_complaintrequest", + "dashboard_escalatedcomplaintlog", + "dashboard_evaluationnote", + "dashboard_inquirydetail", + "dashboard_reportcompletion", + "analytics_kpivalue", + "analytics_kpireportdepartmentbreakdown", + "analytics_kpireportlocationbreakdown", + "analytics_kpireportmonthlydata", + "analytics_kpireportsourcebreakdown", + "analytics_kpireport", + "analytics_kpi", + "surveys_surveyinstance", + "physicians_physicianindividualrating", + "physicians_physicianmonthlyrating", + "rca_rcaattachment", + "rca_rcacorrectiveaction", + "rca_rcanote", + "rca_rcarootcause", + "rca_rcastatuslog", + "rca_rootcauseanalysis", + "projects_qiproject_related_actions", + "projects_qiproject_team_members", + "projects_qiprojecttask", + "projects_pdcaphase", + "projects_focusphase", + "projects_qiproject", + "organizations_staffsubsection", + "organizations_staffsection", + "organizations_staff", + "organizations_section", + "organizations_department", + "organizations_legacyhierarchymapping", + "organizations_subsection", + "organizations_mainsection", + "organizations_location", + "organizations_patient", + "integrations_hispatientvisit", + "integrations_histestpatient", + "integrations_histestvisit", + ] + + org_tables = { + "organizations_staff", + "organizations_staffsection", + "organizations_staffsubsection", + "organizations_section", + "organizations_department", + } + + with connection.cursor() as cursor: + for table in truncate_order: + if keep_org and table in org_tables: + self.stdout.write(f" SKIP (keep-org): {table}") + continue + for table in truncate_order: + try: + cursor.execute(f'TRUNCATE TABLE "{table}" CASCADE') + self.stdout.write(f" Truncated: {table}") + except Exception as e: + self.stdout.write(self.style.WARNING(f" SKIP {table}: {e}")) + + cursor.execute("UPDATE accounts_user SET department_id = NULL") + self.stdout.write(" Nullified: accounts_user.department_id") + + self.stdout.write(self.style.SUCCESS("\nDone. All dev data wiped.")) diff --git a/apps/organizations/migrations/0002_rename_respondent_to_champion.py b/apps/organizations/migrations/0002_rename_respondent_to_champion.py new file mode 100644 index 0000000..62a5f57 --- /dev/null +++ b/apps/organizations/migrations/0002_rename_respondent_to_champion.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-05-14 15:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='department', + old_name='respondent', + new_name='champion', + ), + ] diff --git a/apps/organizations/migrations/0003_alter_department_champion.py b/apps/organizations/migrations/0003_alter_department_champion.py new file mode 100644 index 0000000..c0185b3 --- /dev/null +++ b/apps/organizations/migrations/0003_alter_department_champion.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-05-17 15:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0002_rename_respondent_to_champion'), + ] + + operations = [ + migrations.AlterField( + model_name='department', + name='champion', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='champion_departments', to='organizations.staff'), + ), + ] diff --git a/apps/organizations/migrations/0004_legacylocation_legacymainsection_and_more.py b/apps/organizations/migrations/0004_legacylocation_legacymainsection_and_more.py new file mode 100644 index 0000000..ac3ab2a --- /dev/null +++ b/apps/organizations/migrations/0004_legacylocation_legacymainsection_and_more.py @@ -0,0 +1,176 @@ +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0003_alter_department_champion'), + ] + + operations = [ + migrations.RenameModel( + old_name='Location', + new_name='LegacyLocation', + ), + migrations.RenameModel( + old_name='MainSection', + new_name='LegacyMainSection', + ), + migrations.RenameModel( + old_name='SubSection', + new_name='LegacySubSection', + ), + migrations.AlterModelOptions( + name='department', + options={'ordering': ['hospital', 'area', 'name_en']}, + ), + migrations.AddField( + model_name='department', + name='area', + field=models.CharField(blank=True, db_index=True, help_text='High-level area (Medical, Administrative, Nursing, Support Services)', max_length=100), + ), + migrations.AddField( + model_name='department', + name='champion_email', + field=models.EmailField(blank=True, help_text='Fallback email for champion', max_length=200), + ), + migrations.AddField( + model_name='department', + name='deputy_manager', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_deputy_manager', to='organizations.staff'), + ), + migrations.AddField( + model_name='department', + name='deputy_supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_deputy_supervisor', to='organizations.staff'), + ), + migrations.AddField( + model_name='department', + name='floor', + field=models.CharField(blank=True, help_text='Floor (e.g., GF, 1st Floor, Basement)', max_length=50), + ), + migrations.AddField( + model_name='department', + name='location_type', + field=models.CharField(blank=True, help_text='Location type (OP, IP, ER, GENERAL)', max_length=20), + ), + migrations.AddField( + model_name='department', + name='main_section', + field=models.CharField(blank=True, help_text='Main section name (often same as department name)', max_length=200), + ), + migrations.AddField( + model_name='department', + name='manager_1st', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_manager_1st', to='organizations.staff'), + ), + migrations.AddField( + model_name='department', + name='manager_2nd', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_manager_2nd', to='organizations.staff'), + ), + migrations.AddField( + model_name='department', + name='manager_3rd', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_manager_3rd', to='organizations.staff'), + ), + migrations.AddField( + model_name='department', + name='old_name_ar', + field=models.CharField(blank=True, help_text='Legacy Arabic name from Excel col X', max_length=200), + ), + migrations.AddField( + model_name='department', + name='old_name_en', + field=models.CharField(blank=True, help_text='Legacy English name from Excel col W', max_length=200), + ), + migrations.AddField( + model_name='department', + name='sub_location', + field=models.CharField(blank=True, help_text='Detailed location (e.g., OPD5/GATE1)', max_length=200), + ), + migrations.AddField( + model_name='department', + name='supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_supervisor', to='organizations.staff'), + ), + migrations.AlterField( + model_name='department', + name='category', + field=models.CharField(blank=True, choices=[('nursing', 'Nursing'), ('support_services', 'Support Services'), ('medical', 'Medical'), ('non_medical', 'Non-Medical'), ('administrative', 'Administrative')], db_index=True, default='', max_length=30), + ), + migrations.AlterField( + model_name='staff', + name='department_type', + field=models.CharField(blank=True, choices=[('nursing', 'Nursing'), ('support_services', 'Support Services'), ('medical', 'Medical'), ('non_medical', 'Non-Medical'), ('administrative', 'Administrative')], db_index=True, default='', max_length=30), + ), + migrations.CreateModel( + name='OrgSubSection', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name_en', models.CharField(max_length=200)), + ('name_ar', models.CharField(blank=True, max_length=200)), + ('code', models.CharField(blank=True, max_length=50)), + ('location_type', models.CharField(blank=True, max_length=20)), + ('sub_location', models.CharField(blank=True, max_length=200)), + ('floor', models.CharField(blank=True, max_length=50)), + ('old_name_en', models.CharField(blank=True, max_length=200)), + ('old_name_ar', models.CharField(blank=True, max_length=200)), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], db_index=True, default='active', max_length=20)), + ('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='org_subsections', to='organizations.department')), + ('point_of_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poc_org_subsections', to='organizations.staff')), + ], + options={ + 'verbose_name': 'Section', + 'verbose_name_plural': 'Sections', + 'ordering': ['department', 'name_en'], + 'unique_together': {('department', 'code')}, + }, + ), + migrations.CreateModel( + name='OrgSubSubSection', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name_en', models.CharField(max_length=200)), + ('name_ar', models.CharField(blank=True, max_length=200)), + ('code', models.CharField(blank=True, max_length=50)), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], db_index=True, default='active', max_length=20)), + ('subsection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_subsections', to='organizations.orgsubsection')), + ], + options={ + 'verbose_name': 'Sub-Section', + 'verbose_name_plural': 'Sub-Sections', + 'ordering': ['subsection', 'name_en'], + 'unique_together': {('subsection', 'code')}, + }, + ), + migrations.CreateModel( + name='LegacyHierarchyMapping', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('old_location_ar', models.CharField(db_index=True, max_length=200)), + ('old_main_section_ar', models.CharField(db_index=True, max_length=200)), + ('old_subsection_ar', models.CharField(db_index=True, max_length=200)), + ('old_location_en', models.CharField(blank=True, max_length=200)), + ('old_main_section_en', models.CharField(blank=True, max_length=200)), + ('old_subsection_en', models.CharField(blank=True, max_length=200)), + ('main_section', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='legacy_mappings', to='organizations.department')), + ('subsection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='legacy_mappings', to='organizations.orgsubsection')), + ('sub_subsection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='legacy_mappings', to='organizations.orgsubsubsection')), + ], + ), + migrations.AddIndex( + model_name='legacyhierarchymapping', + index=models.Index(fields=['old_location_ar', 'old_main_section_ar', 'old_subsection_ar'], name='organizatio_old_loc_920b0f_idx'), + ), + migrations.AlterUniqueTogether( + name='legacyhierarchymapping', + unique_together={('old_location_ar', 'old_main_section_ar', 'old_subsection_ar')}, + ), + ] diff --git a/apps/organizations/migrations/0005_alter_legacylocation_table_and_more.py b/apps/organizations/migrations/0005_alter_legacylocation_table_and_more.py new file mode 100644 index 0000000..9c0ad68 --- /dev/null +++ b/apps/organizations/migrations/0005_alter_legacylocation_table_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.1 on 2026-05-28 19:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0004_legacylocation_legacymainsection_and_more'), + ] + + operations = [ + migrations.AlterModelTable( + name='legacylocation', + table='organizations_location', + ), + migrations.AlterModelTable( + name='legacymainsection', + table='organizations_mainsection', + ), + migrations.AlterModelTable( + name='legacysubsection', + table='organizations_subsection', + ), + ] diff --git a/apps/organizations/migrations/0006_alter_department_code_alter_orgsubsection_code_and_more.py b/apps/organizations/migrations/0006_alter_department_code_alter_orgsubsection_code_and_more.py new file mode 100644 index 0000000..7c3898b --- /dev/null +++ b/apps/organizations/migrations/0006_alter_department_code_alter_orgsubsection_code_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-05-30 06:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0005_alter_legacylocation_table_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='department', + name='code', + field=models.CharField(db_index=True, max_length=100), + ), + migrations.AlterField( + model_name='orgsubsection', + name='code', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterField( + model_name='orgsubsubsection', + name='code', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/apps/organizations/migrations/0007_remove_department_area_alter_department_location_type_and_more.py b/apps/organizations/migrations/0007_remove_department_area_alter_department_location_type_and_more.py new file mode 100644 index 0000000..34c8ba5 --- /dev/null +++ b/apps/organizations/migrations/0007_remove_department_area_alter_department_location_type_and_more.py @@ -0,0 +1,49 @@ +# Phase 1: Remove area field, add LocationType choices to location_type + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0006_alter_department_code_alter_orgsubsection_code_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='department', + name='area', + ), + migrations.AlterField( + model_name='department', + name='location_type', + field=models.CharField( + blank=True, + choices=[ + ('OP', 'Outpatient'), + ('IP', 'Inpatient'), + ('ER', 'Emergency'), + ('GENERAL', 'General'), + ], + help_text='Location type (OP, IP, ER, GENERAL)', + max_length=20, + ), + ), + migrations.AlterField( + model_name='department', + name='category', + field=models.CharField( + blank=True, + choices=[ + ('medical', 'Medical'), + ('non_medical', 'Non-Medical'), + ('nursing', 'Nursing'), + ('support_services', 'Support Services'), + ('administrative', 'Administrative'), + ], + db_index=True, + default='', + max_length=30, + ), + ), + ] diff --git a/apps/organizations/migrations/0008_rename_orgsubsection_to_section_add_champion.py b/apps/organizations/migrations/0008_rename_orgsubsection_to_section_add_champion.py new file mode 100644 index 0000000..48d46a2 --- /dev/null +++ b/apps/organizations/migrations/0008_rename_orgsubsection_to_section_add_champion.py @@ -0,0 +1,59 @@ +# Phase 2: Rename OrgSubSection to Section, add champion, remove point_of_contact + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0007_remove_department_area_alter_department_location_type_and_more'), + ] + + operations = [ + migrations.RenameModel( + old_name='OrgSubSection', + new_name='Section', + ), + migrations.AlterModelOptions( + name='section', + options={'ordering': ['department', 'name_en'], 'verbose_name': 'Section', 'verbose_name_plural': 'Sections'}, + ), + migrations.AlterField( + model_name='section', + name='location_type', + field=models.CharField( + blank=True, + choices=[ + ('OP', 'Outpatient'), + ('IP', 'Inpatient'), + ('ER', 'Emergency'), + ('GENERAL', 'General'), + ], + max_length=20, + ), + ), + migrations.RemoveField( + model_name='section', + name='point_of_contact', + ), + migrations.AddField( + model_name='section', + name='champion', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name='champion_sections', + to='organizations.staff', + ), + ), + migrations.AlterField( + model_name='section', + name='department', + field=models.ForeignKey( + on_delete=models.CASCADE, + related_name='sections', + to='organizations.department', + ), + ), + ] diff --git a/apps/organizations/migrations/0009_remove_sub_subsection_fields.py b/apps/organizations/migrations/0009_remove_sub_subsection_fields.py new file mode 100644 index 0000000..eb3358c --- /dev/null +++ b/apps/organizations/migrations/0009_remove_sub_subsection_fields.py @@ -0,0 +1,18 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0008_rename_orgsubsection_to_section_add_champion'), + ] + + operations = [ + migrations.RemoveField( + model_name='legacyhierarchymapping', + name='sub_subsection', + ), + migrations.DeleteModel( + name='OrgSubSubSection', + ), + ] diff --git a/apps/organizations/migrations/0010_section_add_roles_display_names.py b/apps/organizations/migrations/0010_section_add_roles_display_names.py new file mode 100644 index 0000000..6ff2833 --- /dev/null +++ b/apps/organizations/migrations/0010_section_add_roles_display_names.py @@ -0,0 +1,52 @@ +# Generated by Django 6.0.1 on 2026-06-07 01:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0009_remove_sub_subsection_fields'), + ] + + operations = [ + migrations.AlterModelOptions( + name='department', + options={'ordering': ['hospital', 'category', 'name_en']}, + ), + migrations.AddField( + model_name='section', + name='deputy_supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deputy_supervised_sections', to='organizations.staff'), + ), + migrations.AddField( + model_name='section', + name='display_name_ar', + field=models.CharField(blank=True, help_text='Patient-facing display name (Arabic)', max_length=255), + ), + migrations.AddField( + model_name='section', + name='display_name_en', + field=models.CharField(blank=True, help_text='Patient-facing display name (English)', max_length=255), + ), + migrations.AddField( + model_name='section', + name='supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supervised_sections', to='organizations.staff'), + ), + migrations.AlterField( + model_name='staff', + name='department_type', + field=models.CharField(blank=True, choices=[('medical', 'Medical'), ('non_medical', 'Non-Medical'), ('nursing', 'Nursing'), ('support_services', 'Support Services'), ('administrative', 'Administrative')], db_index=True, default='', max_length=30), + ), + migrations.AlterField( + model_name='staffsection', + name='department', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff_sections', to='organizations.department'), + ), + migrations.AlterModelTable( + name='section', + table='organizations_section', + ), + ] diff --git a/apps/organizations/migrations/0011_area_department_area.py b/apps/organizations/migrations/0011_area_department_area.py new file mode 100644 index 0000000..1ce6451 --- /dev/null +++ b/apps/organizations/migrations/0011_area_department_area.py @@ -0,0 +1,37 @@ +# Generated by Django 6.0.1 on 2026-06-07 04:37 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0010_section_add_roles_display_names'), + ] + + operations = [ + migrations.CreateModel( + name='Area', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name_en', models.CharField(max_length=200)), + ('name_ar', models.CharField(blank=True, max_length=200)), + ('code', models.CharField(blank=True, max_length=100)), + ('location_type', models.CharField(blank=True, choices=[('OP', 'Outpatient'), ('IP', 'Inpatient'), ('ER', 'Emergency'), ('GENERAL', 'General')], help_text='Location type (OP/IP/ER/GO)', max_length=20)), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='active', max_length=20)), + ('hospital', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='areas', to='organizations.hospital')), + ], + options={ + 'unique_together': {('hospital', 'code')}, + }, + ), + migrations.AddField( + model_name='department', + name='area', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='departments', to='organizations.area'), + ), + ] diff --git a/apps/organizations/migrations/0012_department_hr_name.py b/apps/organizations/migrations/0012_department_hr_name.py new file mode 100644 index 0000000..ce7a22f --- /dev/null +++ b/apps/organizations/migrations/0012_department_hr_name.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-06-07 20:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0011_area_department_area'), + ] + + operations = [ + migrations.AddField( + model_name='department', + name='hr_name', + field=models.CharField(blank=True, help_text='Department name from HR system (employees file). If empty, name_en matches HR exactly.', max_length=200, verbose_name='HR System Name'), + ), + ] diff --git a/apps/organizations/migrations/0013_add_subsection_model.py b/apps/organizations/migrations/0013_add_subsection_model.py new file mode 100644 index 0000000..1bdb9fd --- /dev/null +++ b/apps/organizations/migrations/0013_add_subsection_model.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.1 on 2026-06-08 11:01 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0012_department_hr_name'), + ] + + operations = [ + migrations.CreateModel( + name='SubSection', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name_en', models.CharField(max_length=200)), + ('name_ar', models.CharField(blank=True, max_length=200)), + ('code', models.CharField(blank=True, max_length=100)), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], db_index=True, default='active', max_length=20)), + ('champion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='champion_subsections', to='organizations.staff')), + ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subsections', to='organizations.section')), + ], + options={ + 'verbose_name': 'SubSection', + 'verbose_name_plural': 'SubSections', + 'db_table': 'organizations_subsection_new', + 'ordering': ['section', 'name_en'], + 'unique_together': {('section', 'code')}, + }, + ), + ] diff --git a/apps/organizations/migrations/0014_remove_department_manager_1st.py b/apps/organizations/migrations/0014_remove_department_manager_1st.py new file mode 100644 index 0000000..ff34608 --- /dev/null +++ b/apps/organizations/migrations/0014_remove_department_manager_1st.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.1 on 2026-06-11 11:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0013_add_subsection_model'), + ] + + operations = [ + migrations.RemoveField( + model_name='department', + name='manager_1st', + ), + ] diff --git a/apps/organizations/models.py b/apps/organizations/models.py index f2fb1d7..d72cf47 100644 --- a/apps/organizations/models.py +++ b/apps/organizations/models.py @@ -148,21 +148,54 @@ class Hospital(UUIDModel, TimeStampedModel): return self.display_name_ar or self.name_ar or self.get_display_name() -class Department(UUIDModel, TimeStampedModel): - """Department within a hospital""" +class DepartmentCategory(models.TextChoices): + MEDICAL = "medical", _("Medical") + NON_MEDICAL = "non_medical", _("Non-Medical") + NURSING = "nursing", _("Nursing") + SUPPORT_SERVICES = "support_services", _("Support Services") + ADMINISTRATIVE = "administrative", _("Administrative") - class DepartmentCategory(models.TextChoices): - NURSING = "nursing", _("Nursing") - SUPPORT_SERVICES = "support_services", _("Support Services") - MEDICAL = "medical", _("Medical") - NON_MEDICAL = "non_medical", _("Non-Medical") + +class LocationType(models.TextChoices): + OP = "OP", _("Outpatient") + IP = "IP", _("Inpatient") + ER = "ER", _("Emergency") + GENERAL = "GENERAL", _("General") + + +class Area(UUIDModel, TimeStampedModel): + hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name="areas") + name_en = models.CharField(max_length=200) + name_ar = models.CharField(max_length=200, blank=True) + code = models.CharField(max_length=100, blank=True) + location_type = models.CharField( + max_length=20, choices=LocationType.choices, blank=True, help_text="Location type (OP/IP/ER/GO)" + ) + status = models.CharField(max_length=20, choices=StatusChoices.choices, default="active") + + class Meta: + unique_together = [["hospital", "code"]] + + def __str__(self): + return f"{self.name_en} ({self.hospital.code})" + + +class Department(UUIDModel, TimeStampedModel): + """Department within a hospital, matching the 4th Version Excel.""" hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name="departments") + main_section = models.CharField( + max_length=200, + blank=True, + help_text="Main section name (often same as department name)", + ) + name = models.CharField(max_length=200) name_en = models.CharField(max_length=200, blank=True) name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)") - code = models.CharField(max_length=50, db_index=True) + hr_name = models.CharField(max_length=200, blank=True, verbose_name="HR System Name", help_text="Department name from HR system (employees file). If empty, name_en matches HR exactly.") + code = models.CharField(max_length=100, db_index=True) category = models.CharField( max_length=30, @@ -172,34 +205,91 @@ class Department(UUIDModel, TimeStampedModel): db_index=True, ) - # Hierarchy parent = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, related_name="sub_departments") - # Manager + location_type = models.CharField( + max_length=20, + choices=LocationType.choices, + blank=True, + help_text="Location type (OP, IP, ER, GENERAL)", + ) + sub_location = models.CharField(max_length=200, blank=True, help_text="Detailed location (e.g., OPD5/GATE1)") + floor = models.CharField(max_length=50, blank=True, help_text="Floor (e.g., GF, 1st Floor, Basement)") + area = models.ForeignKey( + Area, on_delete=models.SET_NULL, null=True, blank=True, related_name="departments" + ) + manager = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="managed_departments" ) - # Respondent (handles inquiries/complaints for this department) - respondent = models.ForeignKey( - "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="respondent_departments" + manager_3rd = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_manager_3rd" + ) + manager_2nd = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_manager_2nd" + ) + deputy_manager = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_deputy_manager" + ) + supervisor = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_supervisor" + ) + deputy_supervisor = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="dept_deputy_supervisor" ) - # Contact + champion = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_departments" + ) + champion_email = models.EmailField(max_length=200, blank=True, help_text="Fallback email for champion") + + old_name_en = models.CharField(max_length=200, blank=True, help_text="Legacy English name from Excel col W") + old_name_ar = models.CharField(max_length=200, blank=True, help_text="Legacy Arabic name from Excel col X") + phone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) location = models.CharField(max_length=200, blank=True, help_text="Building/Floor/Room") - # Status status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True) class Meta: - ordering = ["hospital", "name"] + ordering = ["hospital", "category", "name_en"] unique_together = [["hospital", "code"]] def __str__(self): return self.name_en or self.name + ROLE_FIELDS = [ + ("champion", "Champion"), + ("manager_2nd", "2nd Manager"), + ("manager_3rd", "3rd Manager"), + ("deputy_manager", "Deputy Manager"), + ("supervisor", "Supervisor"), + ("deputy_supervisor", "Deputy Supervisor"), + ] + + def get_role_holders(self): + holders = [] + for field_name, role_label in self.ROLE_FIELDS: + staff = getattr(self, field_name, None) + if staff: + holders.append({ + "staff": staff, + "staff_id": str(staff.id), + "name": staff.get_full_name(), + "email": staff.email or (staff.user.email if staff.user else None), + "role_field": field_name, + "role_label": role_label, + }) + return holders + + def is_valid_contact_person(self, staff_id): + for holder in self.get_role_holders(): + if str(holder["staff_id"]) == str(staff_id): + return holder + return None + def get_localized_name(self): from django.utils.translation import get_language @@ -230,7 +320,7 @@ class Staff(UUIDModel, TimeStampedModel): staff_type = models.CharField(max_length=20, choices=StaffType.choices) department_type = models.CharField( max_length=30, - choices=Department.DepartmentCategory.choices, + choices=DepartmentCategory.choices, blank=True, default="", db_index=True, @@ -533,7 +623,7 @@ class Patient(UUIDModel, TimeStampedModel): class StaffSection(UUIDModel, TimeStampedModel): """Section within a department (for staff organization)""" - department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name="sections") + department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name="staff_sections") name = models.CharField(max_length=200) name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)") @@ -578,13 +668,16 @@ class StaffSubsection(UUIDModel, TimeStampedModel): return f"{self.section.department.name} - {self.section.name} - {self.name}" -class Location(models.Model): - id = models.IntegerField(primary_key=True) # Using your specific IDs (48, 49, etc.) +class LegacyLocation(models.Model): + id = models.IntegerField(primary_key=True) name_ar = models.CharField(max_length=100) name_en = models.CharField(max_length=100) ACTIVE_IDS = [48, 49, 82, 110] + class Meta: + db_table = "organizations_location" + @classmethod def active_locations(cls): return cls.objects.filter(id__in=cls.ACTIVE_IDS).order_by("name_en") @@ -593,25 +686,161 @@ class Location(models.Model): return self.name_en if self.name_en else self.name_ar -class MainSection(models.Model): - id = models.IntegerField(primary_key=True) # Using your specific IDs (1, 2, 3, 4, 5) +class LegacyMainSection(models.Model): + id = models.IntegerField(primary_key=True) name_ar = models.CharField(max_length=100) name_en = models.CharField(max_length=100) + class Meta: + db_table = "organizations_mainsection" + def __str__(self): - # Prefer English name if available, otherwise use Arabic return self.name_en if self.name_en else self.name_ar -class SubSection(models.Model): - internal_id = models.IntegerField(primary_key=True) # The 'value' from HTML +class LegacySubSection(models.Model): + internal_id = models.IntegerField(primary_key=True) name_ar = models.CharField(max_length=255) name_en = models.CharField(max_length=255) - location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="subsections") - main_section = models.ForeignKey(MainSection, on_delete=models.CASCADE, related_name="subsections") + location = models.ForeignKey(LegacyLocation, on_delete=models.CASCADE, related_name="subsections") + main_section = models.ForeignKey(LegacyMainSection, on_delete=models.CASCADE, related_name="subsections") + + class Meta: + db_table = "organizations_subsection" def __str__(self): - # Prefer English name if available, otherwise use Arabic name = self.name_en if self.name_en else self.name_ar location_name = self.location.name_en if self.location.name_en else self.location.name_ar return f"{name} - {location_name}" + + +class Section(UUIDModel, TimeStampedModel): + """Section within a Department (Excel Col E, e.g., Central Outpatient Pharmacy).""" + + department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name="sections") + + name_en = models.CharField(max_length=200) + name_ar = models.CharField(max_length=200, blank=True) + code = models.CharField(max_length=100, blank=True) + + location_type = models.CharField(max_length=20, choices=LocationType.choices, blank=True) + sub_location = models.CharField(max_length=200, blank=True) + floor = models.CharField(max_length=50, blank=True) + + champion = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_sections" + ) + + supervisor = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="supervised_sections" + ) + deputy_supervisor = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="deputy_supervised_sections" + ) + + display_name_en = models.CharField(max_length=255, blank=True, help_text="Patient-facing display name (English)") + display_name_ar = models.CharField(max_length=255, blank=True, help_text="Patient-facing display name (Arabic)") + + old_name_en = models.CharField(max_length=200, blank=True) + old_name_ar = models.CharField(max_length=200, blank=True) + + status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True) + + class Meta: + db_table = "organizations_section" + unique_together = [("department", "code")] + ordering = ["department", "name_en"] + verbose_name = "Section" + verbose_name_plural = "Sections" + + def __str__(self): + return f"{self.department.name_en} / {self.name_en}" + + def get_localized_display_name(self): + from django.utils.translation import get_language + if get_language() == "ar" and self.display_name_ar: + return self.display_name_ar + if self.display_name_en: + return self.display_name_en + if get_language() == "ar" and self.name_ar: + return self.name_ar + return self.name_en + + def get_localized_name(self): + from django.utils.translation import get_language + if get_language() == "ar" and self.name_ar: + return self.name_ar + return self.name_en + + +class SubSection(UUIDModel, TimeStampedModel): + """SubSection within a Section (Department -> Section -> SubSection).""" + + section = models.ForeignKey(Section, on_delete=models.CASCADE, related_name="subsections") + + name_en = models.CharField(max_length=200) + name_ar = models.CharField(max_length=200, blank=True) + code = models.CharField(max_length=100, blank=True) + + champion = models.ForeignKey( + "Staff", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_subsections" + ) + + status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.ACTIVE, db_index=True) + + class Meta: + db_table = "organizations_subsection_new" + unique_together = [("section", "code")] + ordering = ["section", "name_en"] + verbose_name = "SubSection" + verbose_name_plural = "SubSections" + + def __str__(self): + return f"{self.section.department.name_en} / {self.section.name_en} / {self.name_en}" + + def get_localized_name(self): + from django.utils.translation import get_language + if get_language() == "ar" and self.name_ar: + return self.name_ar + return self.name_en + + +# Backward compatibility alias +OrgSubSection = Section + +# Deprecation alias for OrgSubSubSection (removed) +OrgSubSubSection = None + + +class LegacyHierarchyMapping(models.Model): + """Maps old complaint location hierarchy (Arabic) to current Department/Section.""" + + old_location_ar = models.CharField(max_length=200, db_index=True) + old_main_section_ar = models.CharField(max_length=200, db_index=True) + old_subsection_ar = models.CharField(max_length=200, db_index=True) + + old_location_en = models.CharField(max_length=200, blank=True) + old_main_section_en = models.CharField(max_length=200, blank=True) + old_subsection_en = models.CharField(max_length=200, blank=True) + + main_section = models.ForeignKey( + Department, on_delete=models.SET_NULL, null=True, blank=True, related_name="legacy_mappings" + ) + subsection = models.ForeignKey( + Section, on_delete=models.SET_NULL, null=True, blank=True, related_name="legacy_mappings" + ) + + class Meta: + unique_together = [("old_location_ar", "old_main_section_ar", "old_subsection_ar")] + indexes = [ + models.Index(fields=["old_location_ar", "old_main_section_ar", "old_subsection_ar"]), + ] + + def __str__(self): + target = self.subsection or self.main_section + return f"{self.old_location_ar} / {self.old_main_section_ar} / {self.old_subsection_ar} → {target}" + + +# Backward-compatible aliases (deprecated - use Legacy* names) +Location = LegacyLocation +MainSection = LegacyMainSection diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index 21fe52b..7243e60 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -4,7 +4,21 @@ Organizations serializers from rest_framework import serializers -from .models import Department, Hospital, Location, MainSection, Organization, Patient, Staff, SubSection +from .models import ( + Area, + Department, + Hospital, + LegacyHierarchyMapping, + LegacyLocation, + LegacyMainSection, + LegacySubSection, + OrgSubSection, + Section, + SubSection, + Organization, + Patient, + Staff, +) class OrganizationSerializer(serializers.ModelSerializer): @@ -87,6 +101,7 @@ class DepartmentSerializer(serializers.ModelSerializer): "hospital", "hospital_name", "name", + "name_en", "name_ar", "code", "category", @@ -216,9 +231,9 @@ class StaffSerializer(serializers.ModelSerializer): ) # Send email if requested and user was created - if was_created and password and send_email and self.context.get("request"): + if was_created and user and send_email and self.context.get("request"): try: - StaffService.send_credentials_email(staff, password, self.context["request"]) + StaffService.send_password_reset_email(staff, self.context["request"]) except Exception as e: # Log but don't fail if email sending fails pass @@ -255,9 +270,9 @@ class StaffSerializer(serializers.ModelSerializer): ) # Send email if requested and user was created - if was_created and password and send_email and self.context.get("request"): + if was_created and user and send_email and self.context.get("request"): try: - StaffService.send_credentials_email(instance, password, self.context["request"]) + StaffService.send_password_reset_email(instance, self.context["request"]) except Exception as e: pass except ValueError as e: @@ -337,13 +352,13 @@ class PatientListSerializer(serializers.ModelSerializer): fields = ["id", "mrn", "full_name", "national_id_masked", "phone", "email", "primary_hospital_name", "status"] -class LocationSerializer(serializers.ModelSerializer): +class LegacyLocationSerializer(serializers.ModelSerializer): """Location serializer for dropdown""" name = serializers.SerializerMethodField() class Meta: - model = Location + model = LegacyLocation fields = ["id", "name"] def get_name(self, obj): @@ -357,13 +372,13 @@ class LocationSerializer(serializers.ModelSerializer): return obj.name_en if obj.name_en else obj.name_ar -class MainSectionSerializer(serializers.ModelSerializer): +class LegacyMainSectionSerializer(serializers.ModelSerializer): """MainSection serializer for dropdown""" name = serializers.SerializerMethodField() class Meta: - model = MainSection + model = LegacyMainSection fields = ["id", "name"] def get_name(self, obj): @@ -377,7 +392,7 @@ class MainSectionSerializer(serializers.ModelSerializer): return obj.name_en if obj.name_en else obj.name_ar -class SubSectionSerializer(serializers.ModelSerializer): +class LegacySubSectionSerializer(serializers.ModelSerializer): """SubSection serializer for dropdown""" id = serializers.IntegerField(source="internal_id", read_only=True) @@ -386,7 +401,7 @@ class SubSectionSerializer(serializers.ModelSerializer): main_section_name = serializers.SerializerMethodField() class Meta: - model = SubSection + model = LegacySubSection fields = ["id", "name", "location", "main_section", "location_name", "main_section_name"] def get_name(self, obj): @@ -418,3 +433,54 @@ class SubSectionSerializer(serializers.ModelSerializer): if lang == "ar" and obj.main_section.name_ar: return obj.main_section.name_ar return obj.main_section.name_en if obj.main_section.name_en else obj.main_section.name_ar + + +class OrgSubSectionSerializer(serializers.ModelSerializer): + department_name = serializers.SerializerMethodField() + display_name = serializers.SerializerMethodField() + + class Meta: + model = Section + fields = "__all__" + + def get_department_name(self, obj): + return str(obj.department) + + def get_display_name(self, obj): + return obj.get_localized_display_name() + + +# Backward compatibility alias +SectionSerializer = OrgSubSectionSerializer + + +class SubSectionSerializer(serializers.ModelSerializer): + section_name = serializers.SerializerMethodField() + display_name = serializers.SerializerMethodField() + + class Meta: + model = SubSection + fields = "__all__" + + def get_section_name(self, obj): + return str(obj.section) + + def get_display_name(self, obj): + return obj.get_localized_name() + + +class LegacyHierarchyMappingSerializer(serializers.ModelSerializer): + class Meta: + model = LegacyHierarchyMapping + fields = "__all__" + + +# Backward-compatible aliases +LocationSerializer = LegacyLocationSerializer +MainSectionSerializer = LegacyMainSectionSerializer + + +class AreaSerializer(serializers.ModelSerializer): + class Meta: + model = Area + fields = ["id", "hospital", "name_en", "name_ar", "code", "location_type", "status"] diff --git a/apps/organizations/services.py b/apps/organizations/services.py index e43a87e..6165c65 100644 --- a/apps/organizations/services.py +++ b/apps/organizations/services.py @@ -6,11 +6,11 @@ import string from django.contrib.auth import get_user_model from django.template.loader import render_to_string from django.conf import settings -from django.urls import reverse from django.utils import timezone from apps.core.services import AuditService from apps.notifications.services import NotificationService +from apps.accounts.services import PasswordResetTokenService User = get_user_model() @@ -57,10 +57,9 @@ class StaffService: request: HTTP request for audit logging Returns: - tuple: (User instance, was_created: bool, password: str or None) + tuple: (User instance, was_created: bool, password: None) - was_created is True if a new user was created - - was_created is False if an existing user was linked - - password is the generated password for new users, None for linked users + - password is always None because users set passwords through a secure email link Raises: ValueError: If staff already has a user account or has no email @@ -118,13 +117,11 @@ class StaffService: # Create new user account # Generate username (optional, for backward compatibility) username = StaffService.generate_username(staff) - password = StaffService.generate_password() # Create user - email is now the username field - # Note: create_user() already hashes the password, so no need to call set_password() separately user = User.objects.create_user( email=staff.email, - password=password, + password=None, first_name=staff.first_name, last_name=staff.last_name, username=username, # Optional field @@ -162,7 +159,7 @@ class StaffService: } ) - return user, True, password # New user was created with password + return user, True, None # New user was created with no emailed password @staticmethod def link_user_to_staff(staff, user_id, request=None): @@ -248,14 +245,9 @@ class StaffService: return staff @staticmethod - def send_credentials_email(staff, password, request=None): + def send_password_reset_email(staff, request=None): """ - Send login credentials email to staff member using NotificationService. - - Args: - staff: Staff instance - password: Generated password - request: HTTP request for building absolute URLs (optional) + Send a one-time password reset link to a staff member. """ if not staff.email: raise ValueError("Staff member has no email address") @@ -264,50 +256,39 @@ class StaffService: if not user: raise ValueError("Staff member has no user account") - # Build login URL + reset_token = PasswordResetTokenService.create_reset_token(user) if request: - login_url = request.build_absolute_uri(reverse('accounts:login')) + base_url = request.build_absolute_uri("/") else: - from django.contrib.sites.models import Site - try: - site = Site.objects.get_current() - login_url = f"https://{site.domain}{reverse('accounts:login')}" - except: - login_url = settings.LOGIN_URL or '/accounts/login/' + base_url = settings.SITE_URL if hasattr(settings, "SITE_URL") else "http://localhost:8000" + reset_url = PasswordResetTokenService.build_reset_url(base_url, reset_token) - # Render email content context = { 'staff': staff, 'user': user, - 'password': password, - 'login_url': login_url, + 'reset_url': reset_url, } - subject = "Your PX360 Account Credentials" + subject = "Set Your PX360 Password" html_message = render_to_string('organizations/emails/staff_credentials.html', context) - - # Create plain text version + plain_message = f"""Welcome to PX360! Dear {staff.get_full_name()}, -Your PX360 account has been created successfully. Below are your login credentials: +Your PX360 account has been created successfully. For your security, no password is sent by email. Username: {user.username} -Password: {password} Email: {staff.email} -Login URL: {login_url} +Set your password here: {reset_url} -Security Notice: Please change your password after your first login for security purposes. - -If you have any questions or need assistance, please contact your system administrator. +This link expires in 24 hours. If you did not request this, please contact your system administrator. Best regards, The PX360 Team """ - # Send email using NotificationService notification_log = NotificationService.send_email( email=staff.email, subject=subject, @@ -315,18 +296,17 @@ The PX360 Team html_message=html_message, related_object=staff, metadata={ - 'notification_type': 'staff_credentials', + 'notification_type': 'staff_password_reset', 'staff_id': str(staff.id), 'user_id': str(user.id), 'username': user.username } ) - # Log the action if request: AuditService.log_from_request( - event_type='other', - description=f"Credentials email sent to {staff.email} for staff member {staff.get_full_name()}", + event_type='password_reset', + description=f"Password reset link sent to {staff.email} for staff member {staff.get_full_name()}", request=request, content_object=staff, metadata={ @@ -336,17 +316,21 @@ The PX360 Team return notification_log + @staticmethod + def send_credentials_email(staff, password=None, request=None): + return StaffService.send_password_reset_email(staff, request) + @staticmethod def reset_password_and_resend_credentials(staff, request=None): """ - Reset user password and resend credentials email. + Reset user password and resend password reset link. Args: staff: Staff instance request: HTTP request for building absolute URLs and audit logging Returns: - tuple: (new_password: str, notification_log: NotificationLog) + tuple: (None, notification_log: NotificationLog) Raises: ValueError: If staff has no user account or no email @@ -357,21 +341,13 @@ The PX360 Team if not staff.email: raise ValueError("Staff member has no email address") - # Generate new password - new_password = StaffService.generate_password() - - # Reset password - staff.user.set_password(new_password) - staff.user.save(update_fields=['password']) - - # Send credentials email - notification_log = StaffService.send_credentials_email(staff, new_password, request) + notification_log = StaffService.send_password_reset_email(staff, request) # Log the action if request: AuditService.log_from_request( event_type='password_reset', - description=f"Password reset and credentials resent to {staff.email} for staff member {staff.get_full_name()}", + description=f"Password reset link resent to {staff.email} for staff member {staff.get_full_name()}", request=request, content_object=staff, metadata={ @@ -380,7 +356,7 @@ The PX360 Team } ) - return new_password, notification_log + return None, notification_log @staticmethod def get_staff_type_role(staff_type): diff --git a/apps/organizations/ui_views.py b/apps/organizations/ui_views.py index 04396fd..401245a 100644 --- a/apps/organizations/ui_views.py +++ b/apps/organizations/ui_views.py @@ -9,9 +9,10 @@ from django.views.decorators.csrf import csrf_exempt from django.utils import timezone from django.utils.translation import activate, get_language, gettext as _ +from django.urls import reverse from apps.core.decorators import block_source_user, hospital_admin_required -from .models import Department, Hospital, Organization, Patient, Staff, StaffSection, StaffSubsection +from .models import Department, DepartmentCategory, Hospital, Organization, Patient, Staff, StaffSection, StaffSubsection, Section, SubSection, OrgSubSection from apps.accounts.models import User from .forms import StaffForm, PatientForm @@ -617,14 +618,19 @@ def staff_create(request): if create_user and not staff.user and staff.email: from .services import StaffService + role = request.POST.get("user_role", "staff") + allowed_roles = ["staff", "px_employee", "hospital_admin", "department_manager"] + if role not in allowed_roles: + role = "staff" + try: user_account, was_created, password = StaffService.create_user_for_staff( - staff, role='staff', request=request + staff, role=role, request=request ) - if was_created and password: + if was_created and user_account: try: - StaffService.send_credentials_email(staff, password, request) - messages.success(request, "Staff member created and credentials email sent successfully.") + StaffService.send_password_reset_email(staff, request) + messages.success(request, "Staff member created and password reset email sent successfully.") except Exception as e: messages.warning(request, f"Staff member created but email sending failed: {str(e)}") elif not was_created: @@ -639,11 +645,8 @@ def staff_create(request): from .services import StaffService try: - password = StaffService.generate_password() - staff.user.set_password(password) - staff.user.save() - StaffService.send_credentials_email(staff, password, request) - messages.success(request, "Credentials email sent successfully.") + StaffService.send_password_reset_email(staff, request) + messages.success(request, "Password reset email sent successfully.") except Exception as e: messages.warning(request, f"Email sending failed: {str(e)}") @@ -687,14 +690,19 @@ def staff_update(request, pk): if create_user and not staff.user and staff.email: from .services import StaffService + role = request.POST.get("user_role", "staff") + allowed_roles = ["staff", "px_employee", "hospital_admin", "department_manager"] + if role not in allowed_roles: + role = "staff" + try: user_account, was_created, password = StaffService.create_user_for_staff( - staff, role='staff', request=request + staff, role=role, request=request ) - if was_created and password: + if was_created and user_account: try: - StaffService.send_credentials_email(staff, password, request) - messages.success(request, "User account created and credentials email sent.") + StaffService.send_password_reset_email(staff, request) + messages.success(request, "User account created and password reset email sent.") except Exception as e: messages.warning(request, f"User account created but email sending failed: {str(e)}") elif not was_created: @@ -1865,7 +1873,7 @@ def department_list(request): "hospitals": hospitals, "search": search, "category_filter": category_filter, - "categories": Department.DepartmentCategory.choices, + "categories": DepartmentCategory.choices, "can_create": user.is_px_admin() or user.is_hospital_admin(), } return render(request, "organizations/department_list.html", context) @@ -1876,11 +1884,17 @@ def department_detail(request, pk): from .models import Department, Staff from apps.complaints.models import Complaint, ComplaintInvolvedStaff, Inquiry from apps.observations.models import Observation + from apps.appreciation.models import Appreciation + from apps.feedback.models import Feedback, FeedbackType from django.db.models import Q from collections import defaultdict department = get_object_or_404( - Department.objects.select_related("hospital", "manager", "parent", "respondent"), + Department.objects.select_related( + "hospital", "manager", "parent", + "champion", "manager", "manager_2nd", "manager_3rd", + "deputy_manager", "supervisor", "deputy_supervisor", + ), pk=pk, ) @@ -1918,7 +1932,7 @@ def department_detail(request, pk): staff_complaint_counts[staff_id] += 1 complaints = Complaint.objects.filter( - Q(department=department) | Q(involved_departments__department=department) + Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True) ).select_related( "patient", "assigned_to", "category", "domain", "subcategory_obj", "classification_obj", "staff" ).prefetch_related( @@ -1926,11 +1940,11 @@ def department_detail(request, pk): ).distinct().order_by("-created_at") inquiries = Inquiry.objects.filter( - Q(department=department) | Q(outgoing_department=department) + Q(department=department, sent_to_department=True) | Q(outgoing_department=department) ).select_related("assigned_to", "patient").order_by("-created_at") observations = Observation.objects.filter( - assigned_department=department + assigned_department=department, sent_to_department=True ).select_related("category", "assigned_to").order_by("-created_at") complaint_status_filter = request.GET.get("complaint_status") @@ -1945,6 +1959,22 @@ def department_detail(request, pk): if observation_status_filter: observations = observations.filter(status=observation_status_filter) + appreciations = Appreciation.objects.filter( + department=department + ).select_related("sender", "category").order_by("-created_at") + + suggestions = Feedback.objects.filter( + department=department, feedback_type=FeedbackType.SUGGESTION + ).select_related("assigned_to", "staff").order_by("-created_at") + + appreciation_status_filter = request.GET.get("appreciation_status") + if appreciation_status_filter: + appreciations = appreciations.filter(status=appreciation_status_filter) + + suggestion_status_filter = request.GET.get("suggestion_status") + if suggestion_status_filter: + suggestions = suggestions.filter(status=suggestion_status_filter) + active_tab = request.GET.get("tab", "analytics") stats = { @@ -1955,6 +1985,8 @@ def department_detail(request, pk): "total_complaints": complaints.count(), "total_inquiries": inquiries.count(), "total_observations": observations.count(), + "total_appreciations": appreciations.count(), + "total_suggestions": suggestions.count(), } search_query = request.GET.get("search", "").strip() @@ -1985,7 +2017,7 @@ def department_detail(request, pk): ).exclude(email="").order_by("first_name", "last_name") managers = Staff.objects.filter( - hospital=department.hospital, status="active" + department=department, status="active" ).order_by("first_name", "last_name") pending_actions = [] @@ -1996,24 +2028,51 @@ def department_detail(request, pk): pending_complaint_dept_responses = ComplaintInvolvedDepartment.objects.filter( department=department, forwarded_at__isnull=False, - response_submitted=False, + sent=True, + ).filter( + Q(response_submitted=False, acceptance_status="pending") + | Q(acceptance_status="not_acceptable") + | Q(manager_review_status="rejected") ).select_related("complaint").order_by("-forwarded_at") for pc in pending_complaint_dept_responses: + is_rejected = pc.acceptance_status == "not_acceptable" pending_actions.append({ "type": "complaint_department_response", - "type_label": _("Complaint Response"), + "type_label": _("Complaint Response (Re-submit)") if is_rejected else _("Complaint Response"), "reference": pc.complaint.reference_number or str(pc.complaint.id), "subject": pc.complaint.title or _("No title"), "sla_due_at": None, "is_overdue": False, "url": "#", - "badge_color": "orange", + "badge_color": "red" if is_rejected else "orange", "item_id": str(pc.pk), "item_type": "complaint_involved_department", "complaint_id": str(pc.complaint_id), "department_name": pc.department.name, }) + if user.is_department_manager() or user.is_px_admin() or user.is_hospital_admin(): + pending_manager_reviews = ComplaintInvolvedDepartment.objects.filter( + department=department, + response_submitted=True, + manager_review_status="pending", + ).select_related("complaint").order_by("-response_submitted_at") + for mr in pending_manager_reviews: + pending_actions.append({ + "type": "manager_review", + "type_label": _("Manager Review"), + "reference": mr.complaint.reference_number or str(mr.complaint.id), + "subject": mr.complaint.title or _("No title"), + "sla_due_at": None, + "is_overdue": False, + "url": reverse("organizations:department_manager_review", kwargs={"pk": department.pk, "idept_pk": mr.pk}), + "badge_color": "blue", + "item_id": str(mr.pk), + "item_type": "manager_review", + "complaint_id": str(mr.complaint_id), + "department_name": mr.department.name, + }) + # 2. Complaint Explanations (staff-level) pending_explanations = ComplaintExplanation.objects.filter( complaint__department=department, @@ -2033,6 +2092,7 @@ def department_detail(request, pk): pending_observations = Observation.objects.filter( assigned_department=department, + sent_to_department=True, forwarded_to_dept_at__isnull=False, department_responded_at__isnull=True, ).order_by("dept_response_sla_due_at") @@ -2046,10 +2106,13 @@ def department_detail(request, pk): "is_overdue": obs.dept_response_sla_due_at and obs.dept_response_sla_due_at < dj_tz.now(), "url": "/observations/{}/department-response/".format(obs.pk), "badge_color": "purple", + "item_id": str(obs.pk), + "existing_en": obs.department_response_en or "", + "existing_ar": obs.department_response_ar or "", }) pending_inquiries = Inquiry.objects.filter( - Q(outgoing_department=department) | Q(department=department), + Q(outgoing_department=department) | Q(department=department, sent_to_department=True), transferred_at__isnull=False, department_responded_at__isnull=True, status__in=["open", "in_progress"], @@ -2064,6 +2127,9 @@ def department_detail(request, pk): "is_overdue": inq.dept_response_sla_due_at and inq.dept_response_sla_due_at < dj_tz.now(), "url": "/inquiries/{}/department-response/".format(inq.pk), "badge_color": "cyan", + "item_id": str(inq.pk), + "existing_en": inq.department_response_en or "", + "existing_ar": inq.department_response_ar or "", }) # Standards for this department (global + department-specific) @@ -2091,19 +2157,36 @@ def department_detail(request, pk): "staff_list": staff_list, "staff_head": staff_head, "staff_complaint_counts": staff_complaint_counts, - "complaints": complaints[:50], - "inquiries": inquiries[:50], - "observations": observations[:50], + "complaints": complaints[:5], + "inquiries": inquiries[:5], + "observations": observations[:5], + "appreciations": appreciations[:50], + "suggestions": suggestions[:50], "stats": stats, "active_tab": active_tab, "assignable_staff": assignable_staff, "managers": managers, "standards_data": standards_data, + "org_sections": Section.objects.filter(department=department).select_related("champion", "supervisor", "deputy_supervisor").order_by("name_en"), + "role_data": (lambda d: [ + {"field": "champion", "label": "Champion", "staff": d.champion, "readonly": False}, + {"field": "manager", "label": "Manager", "staff": d.manager, "readonly": False}, + ] + [ + { + "field": field_name, + "label": role_label, + "staff": getattr(d, field_name, None), + "readonly": False, + } + for field_name, role_label in Department.ROLE_FIELDS + if field_name != "champion" + ])(department), "search_query": search_query, "complaint_status_filter": complaint_status_filter, "inquiry_status_filter": inquiry_status_filter, "observation_status_filter": observation_status_filter, "can_assign": user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager(), + "can_manage_roles": user.is_px_admin() or user.is_px_employee() or user.is_hospital_admin(), "can_edit": user.is_px_admin() or (user.is_hospital_admin() and user.hospital == department.hospital), "can_respond": ( user.is_px_admin() @@ -2116,6 +2199,921 @@ def department_detail(request, pk): } return render(request, "organizations/department_detail.html", context) +def _check_department_access(user, department): + if user.is_px_admin(): + return True + if user.is_hospital_admin() and user.hospital == department.hospital: + return True + if user.is_champion() and user.department == department: + return True + if user.is_department_manager() and user.department == department: + return True + if user.is_basic_staff() and user.department == department: + return True + if user.is_director() and user.get_directed_departments().filter(id=department.id).exists(): + return True + if ( + user.hospital == department.hospital + and not user.is_source_user() + and not user.is_basic_staff() + and not user.is_department_manager() + and not user.is_director() + ): + return True + return False + + +@login_required +def department_complaints_list(request, pk): + from .models import Department + from apps.complaints.models import Complaint, ComplaintInvolvedDepartment + from django.core.paginator import Paginator + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + user = request.user + if not _check_department_access(user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + queryset = Complaint.objects.filter( + Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True) + ).select_related( + "patient", "assigned_to", "category", "domain", "subcategory_obj", + "classification_obj", "staff", + ).prefetch_related( + "involved_staff__staff", "involved_departments__department", + ).distinct().order_by("-created_at") + + from django.db.models import Count + base_filter = Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True) + status_counts = dict( + Complaint.objects.filter(base_filter) + .values("status") + .annotate(count=Count("pk", distinct=True)) + .values_list("status", "count") + ) + + status_filter = request.GET.get("status") + severity_filter = request.GET.get("severity", "") + if status_filter: + queryset = queryset.filter(status=status_filter) + if severity_filter: + queryset = queryset.filter(severity=severity_filter) + + search_query = request.GET.get("search", "").strip() + if search_query: + queryset = queryset.filter( + Q(reference_number__icontains=search_query) | Q(title__icontains=search_query) + ) + + date_from = request.GET.get("date_from") + date_to = request.GET.get("date_to") + if date_from: + queryset = queryset.filter(created_at__date__gte=date_from) + if date_to: + queryset = queryset.filter(created_at__date__lte=date_to) + + page_size = int(request.GET.get("page_size", 25)) + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(request.GET.get("page", 1)) + + can_respond = ( + user.is_px_admin() + or user.is_hospital_admin() + or (user.is_department_manager() and user.department == department) + or (user.is_champion() and user.department == department) + ) + + complaint_dept_map = {} + if can_respond: + complaint_ids = [c.pk for c in page_obj.object_list] + for idept in ComplaintInvolvedDepartment.objects.filter( + department=department, complaint_id__in=complaint_ids, sent=True, response_submitted=False + ): + complaint_dept_map[idept.complaint_id] = idept + for idept in ComplaintInvolvedDepartment.objects.filter( + department=department, complaint_id__in=complaint_ids, sent=True, + response_submitted=True, manager_review_status="pending", + ): + if idept.complaint_id not in complaint_dept_map: + complaint_dept_map[idept.complaint_id] = idept + + context = { + "department": department, + "page_obj": page_obj, + "complaints": page_obj, + "status_filter": status_filter, + "severity_filter": severity_filter or "", + "search_query": search_query, + "date_from": date_from or "", + "date_to": date_to or "", + "can_respond": can_respond, + "complaint_dept_map": complaint_dept_map, + "status_choices": Complaint.Status.choices if hasattr(Complaint, 'Status') else [], + "list_stats": status_counts, + "status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed")}, + } + return render(request, "organizations/department_complaints.html", context) + + +@login_required +def department_inquiries_list(request, pk): + from .models import Department + from apps.complaints.models import Inquiry + from django.core.paginator import Paginator + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + user = request.user + if not _check_department_access(user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + queryset = Inquiry.objects.filter( + Q(department=department, sent_to_department=True) | Q(outgoing_department=department) + ).select_related("assigned_to", "patient").order_by("-created_at") + + from django.db.models import Count + list_stats = queryset.values("status").annotate(count=Count("pk")) + status_counts = {s["status"]: s["count"] for s in list_stats} + + status_filter = request.GET.get("status") + if status_filter: + queryset = queryset.filter(status=status_filter) + + priority_filter = request.GET.get("priority") + if priority_filter: + queryset = queryset.filter(priority=priority_filter) + + search_query = request.GET.get("search", "").strip() + if search_query: + queryset = queryset.filter( + Q(reference_number__icontains=search_query) + | Q(subject__icontains=search_query) + | Q(contact_name__icontains=search_query) + ) + + date_from = request.GET.get("date_from") + date_to = request.GET.get("date_to") + if date_from: + queryset = queryset.filter(created_at__date__gte=date_from) + if date_to: + queryset = queryset.filter(created_at__date__lte=date_to) + + can_respond = ( + user.is_px_admin() + or user.is_hospital_admin() + or (user.is_department_manager() and user.department == department) + or (user.is_champion() and user.department == department) + ) + + page_size = int(request.GET.get("page_size", 25)) + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(request.GET.get("page", 1)) + + context = { + "department": department, + "page_obj": page_obj, + "inquiries": page_obj, + "status_filter": status_filter, + "priority_filter": priority_filter or "", + "search_query": search_query, + "date_from": date_from or "", + "date_to": date_to or "", + "can_respond": can_respond, + "list_stats": status_counts, + "status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed")}, + } + return render(request, "organizations/department_inquiries.html", context) + + +@login_required +def department_observations_list(request, pk): + from .models import Department + from apps.observations.models import Observation + from django.core.paginator import Paginator + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + user = request.user + if not _check_department_access(user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + queryset = Observation.objects.filter( + assigned_department=department, sent_to_department=True + ).select_related("category", "assigned_to").order_by("-created_at") + + from django.db.models import Count + list_stats = queryset.values("status").annotate(count=Count("pk")) + status_counts = {s["status"]: s["count"] for s in list_stats} + + status_filter = request.GET.get("status") + if status_filter: + queryset = queryset.filter(status=status_filter) + + severity_filter = request.GET.get("severity") + if severity_filter: + queryset = queryset.filter(severity=severity_filter) + + search_query = request.GET.get("search", "").strip() + if search_query: + queryset = queryset.filter( + Q(tracking_code__icontains=search_query) | Q(title__icontains=search_query) + ) + + date_from = request.GET.get("date_from") + date_to = request.GET.get("date_to") + if date_from: + queryset = queryset.filter(created_at__date__gte=date_from) + if date_to: + queryset = queryset.filter(created_at__date__lte=date_to) + + can_respond = ( + user.is_px_admin() + or user.is_hospital_admin() + or (user.is_department_manager() and user.department == department) + or (user.is_champion() and user.department == department) + ) + + page_size = int(request.GET.get("page_size", 25)) + paginator = Paginator(queryset, page_size) + page_obj = paginator.get_page(request.GET.get("page", 1)) + + context = { + "department": department, + "page_obj": page_obj, + "observations": page_obj, + "status_filter": status_filter, + "severity_filter": severity_filter or "", + "search_query": search_query, + "date_from": date_from or "", + "date_to": date_to or "", + "can_respond": can_respond, + "list_stats": status_counts, + "status_labels": {"new": _("New"), "triaged": _("Triaged"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed")}, + } + return render(request, "organizations/department_observations.html", context) + + +@login_required +def department_complaint_detail(request, pk, cpk): + from .models import Department + from apps.complaints.models import Complaint, ComplaintInvolvedDepartment + from django.contrib.contenttypes.models import ContentType + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + if not _check_department_access(request.user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + complaint = get_object_or_404( + Complaint.objects.select_related( + "patient", "hospital", "department", "staff", "assigned_to", + "category", "domain", "subcategory_obj", "classification_obj", + "source", "legacy_location", "legacy_main_section", "legacy_subsection", + ).prefetch_related( + "involved_staff__staff", "involved_departments__department", + ), + pk=cpk, + ) + + belongs = ( + (complaint.department_id == department.pk and complaint.sent_to_department) + or complaint.involved_departments.filter(department=department, sent=True).exists() + ) + if not belongs: + messages.error(request, _("This complaint does not belong to this department.")) + return redirect("organizations:department_complaints_list", pk=pk) + + user = request.user + involved_dept = complaint.involved_departments.filter(department=department, sent=True).first() + can_respond = ( + user.is_px_admin() + or user.is_hospital_admin() + or (user.is_department_manager() and user.department == department) + or (user.is_champion() and user.department == department) + ) and involved_dept and not involved_dept.response_submitted + + can_manager_review = ( + (user.is_department_manager() and user.department == department) + or user.is_px_admin() + or user.is_hospital_admin() + ) and involved_dept and involved_dept.response_submitted and involved_dept.manager_review_status == "pending" + + taxonomy = [] + if complaint.domain: + taxonomy.append(complaint.domain.get_localized_name()) + if complaint.category: + taxonomy.append(complaint.category.get_localized_name()) + if complaint.subcategory_obj: + taxonomy.append(complaint.subcategory_obj.get_localized_name()) + if complaint.classification_obj: + taxonomy.append(complaint.classification_obj.get_localized_name()) + + staff_list = [] + for inv in complaint.involved_staff.all(): + if inv.staff: + staff_list.append(inv.staff.get_localized_name()) + if not staff_list and complaint.staff: + staff_list.append(complaint.staff.get_localized_name()) + + location_parts = [] + if complaint.legacy_location: + location_parts.append(str(complaint.legacy_location)) + if complaint.legacy_main_section: + location_parts.append(str(complaint.legacy_main_section)) + if complaint.legacy_subsection: + location_parts.append(str(complaint.legacy_subsection)) + + complaint_ct = ContentType.objects.get_for_model(Complaint) + notes = complaint.notes.select_related("created_by").order_by("-created_at") + + context = { + "department": department, + "complaint": complaint, + "taxonomy": taxonomy, + "staff_list": staff_list, + "location_str": " > ".join(location_parts) if location_parts else "", + "can_respond": can_respond, + "can_manager_review": can_manager_review, + "involved_dept": involved_dept, + "notes": notes, + "content_type_id": complaint_ct.pk, + "object_id": complaint.pk, + } + return render(request, "organizations/department_complaint_detail.html", context) + + +@login_required +def department_inquiry_detail(request, pk, ipk): + from .models import Department + from apps.complaints.models import Inquiry + from django.contrib.contenttypes.models import ContentType + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + if not _check_department_access(request.user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + inquiry = get_object_or_404( + Inquiry.objects.select_related( + "patient", "hospital", "department", "assigned_to", + "outgoing_department", "legacy_location", "legacy_main_section", "legacy_subsection", + "department_responded_by", + ), + pk=ipk, + ) + + belongs = ( + (inquiry.department_id == department.pk and inquiry.sent_to_department) + or inquiry.outgoing_department_id == department.pk + ) + if not belongs: + messages.error(request, _("This inquiry does not belong to this department.")) + return redirect("organizations:department_inquiries_list", pk=pk) + + user = request.user + can_respond = ( + user.is_px_admin() + or user.is_hospital_admin() + or (user.is_department_manager() and user.department == department) + or ( + user.is_champion() + and user.department + and user.department in [inquiry.department, inquiry.outgoing_department] + ) + ) + + location_parts = [] + if inquiry.legacy_location: + location_parts.append(str(inquiry.legacy_location)) + if inquiry.legacy_main_section: + location_parts.append(str(inquiry.legacy_main_section)) + if inquiry.legacy_subsection: + location_parts.append(str(inquiry.legacy_subsection)) + + inquiry_ct = ContentType.objects.get_for_model(Inquiry) + notes = inquiry.notes.select_related("created_by").order_by("-created_at") + + context = { + "department": department, + "inquiry": inquiry, + "location_str": " > ".join(location_parts) if location_parts else "", + "can_respond": can_respond, + "notes": notes, + "content_type_id": inquiry_ct.pk, + "object_id": inquiry.pk, + } + return render(request, "organizations/department_inquiry_detail.html", context) + + +@login_required +def department_observation_detail(request, pk, opk): + from .models import Department + from apps.observations.models import Observation + from django.contrib.contenttypes.models import ContentType + from django.utils import timezone + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + if not _check_department_access(request.user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + observation = get_object_or_404( + Observation.objects.select_related( + "category", "assigned_department", "assigned_to", + "legacy_location", "legacy_main_section", "legacy_subsection", "hospital", + "department_responded_by", + ), + pk=opk, + ) + + if observation.assigned_department_id != department.pk or not observation.sent_to_department: + messages.error(request, _("This observation does not belong to this department.")) + return redirect("organizations:department_observations_list", pk=pk) + + user = request.user + can_respond = ( + user.is_px_admin() + or user.is_hospital_admin() + or (user.is_department_manager() and user.department == department) + or (user.is_champion() and user.department == department) + ) + + obs_ct = ContentType.objects.get_for_model(Observation) + notes = observation.notes.select_related("created_by").order_by("-created_at") + + context = { + "department": department, + "observation": observation, + "can_respond": can_respond, + "notes": notes, + "now": timezone.now(), + "content_type_id": obs_ct.pk, + "object_id": observation.pk, + } + return render(request, "organizations/department_observation_detail.html", context) + + +@login_required +def department_staff_detail(request, pk, spk): + from .models import Department + from apps.complaints.models import Complaint, ComplaintInvolvedStaff + from django.db.models import Q + + department = get_object_or_404( + Department.objects.select_related("hospital"), pk=pk + ) + if not _check_department_access(request.user, department): + messages.error(request, _("You don't have permission to view this department.")) + return redirect("organizations:department_list") + + staff = get_object_or_404( + Staff.objects.select_related("user", "department", "section_fk", "subsection_fk", "report_to"), + pk=spk, + department=department, + status="active", + ) + + complaint_q = Q( + Q(department=department, sent_to_department=True) + | Q(involved_departments__department=department, involved_departments__sent=True) + ) + + direct_complaints = Complaint.objects.filter( + staff=staff, sent_to_department=True, + ).filter( + Q(department=department) | Q(involved_departments__department=department), + ).select_related("patient", "assigned_to", "category").distinct() + + involved_records = ComplaintInvolvedStaff.objects.filter( + staff=staff, + complaint__sent_to_department=True, + ).filter( + Q(complaint__department=department) | Q(complaint__involved_departments__department=department), + ).select_related("complaint__patient", "complaint__assigned_to", "complaint__category").distinct() + + complaint_rows = [] + seen_ids = set() + for c in direct_complaints: + complaint_rows.append({"complaint": c, "role": _("Primary")}) + seen_ids.add(c.id) + for inv in involved_records: + if inv.complaint_id not in seen_ids: + complaint_rows.append({"complaint": inv.complaint, "role": inv.get_role_display()}) + seen_ids.add(inv.complaint_id) + complaint_rows.sort(key=lambda r: r["complaint"].created_at, reverse=True) + + all_complaints = [r["complaint"] for r in complaint_rows] + complaint_counts = { + "total": len(all_complaints), + "open": sum(1 for c in all_complaints if c.status == "open"), + "in_progress": sum(1 for c in all_complaints if c.status == "in_progress"), + "resolved": sum(1 for c in all_complaints if c.status in ("resolved", "partially_resolved")), + "closed": sum(1 for c in all_complaints if c.status == "closed"), + } + + direct_reports = Staff.objects.filter( + report_to=staff, department=department, status="active", + ).order_by("first_name") + + from django.contrib.contenttypes.models import ContentType + from apps.appreciation.models import Appreciation + + recipient_ctypes = [ContentType.objects.get_for_model(Staff)] + recipient_ids = [staff.pk] + if staff.user: + recipient_ctypes.append(ContentType.objects.get_for_model(staff.user)) + recipient_ids.append(staff.user.pk) + + appreciations = Appreciation.objects.filter( + department=department, + recipient_content_type__in=recipient_ctypes, + recipient_object_id__in=recipient_ids, + ).select_related("sender", "category").order_by("-created_at") + + context = { + "department": department, + "staff": staff, + "complaint_rows": complaint_rows, + "complaint_counts": complaint_counts, + "direct_reports": direct_reports, + "appreciations": appreciations, + "can_edit_staff": ( + request.user.is_px_admin() + or (request.user.is_hospital_admin() and request.user.hospital == department.hospital) + or (request.user.is_champion() and request.user.department == department) + or (request.user.is_department_manager() and request.user.department == department) + ), + } + return render(request, "organizations/department_staff_detail.html", context) + + +@login_required +@require_http_methods(["POST"]) +def department_staff_update(request, pk, spk): + from .models import Department + + department = get_object_or_404(Department.objects.select_related("hospital"), pk=pk) + user = request.user + if not ( + user.is_px_admin() + or (user.is_hospital_admin() and user.hospital == department.hospital) + or (user.is_champion() and user.department == department) + or (user.is_department_manager() and user.department == department) + ): + messages.error(request, _("You don't have permission to update staff info.")) + return redirect("organizations:department_staff_detail", pk=pk, spk=spk) + + staff = get_object_or_404( + Staff.objects.select_related("department"), + pk=spk, + department=department, + status="active", + ) + + EDITABLE_FIELDS = [ + "first_name", "last_name", "first_name_ar", "last_name_ar", + "job_title", "job_title_ar", "email", "phone", + "specialization", "license_number", "gender", + ] + + changed = [] + for field in EDITABLE_FIELDS: + value = request.POST.get(field, "").strip() + old = getattr(staff, field) or "" + if field in ("first_name", "last_name"): + if not value: + messages.error(request, _(f"{field.replace('_', ' ').title()} is required.")) + return redirect("organizations:department_staff_detail", pk=pk, spk=spk) + setattr(staff, field, value) + if value != old: + changed.append(field) + + if changed: + staff.save(update_fields=changed) + messages.success(request, _("Staff info updated successfully.")) + else: + messages.info(request, _("No changes detected.")) + + return redirect("organizations:department_staff_detail", pk=pk, spk=spk) + + +@login_required +def department_record_complaint(request, pk): + from django.http import JsonResponse + from apps.complaints.models import Complaint + + complaint = get_object_or_404( + Complaint.objects.select_related( + "hospital", "department", "assigned_to", "category", "domain", + "subcategory_obj", "classification_obj", "legacy_location", "legacy_main_section", + "legacy_subsection", "patient", "staff", "source", + ).prefetch_related("involved_staff__staff"), + pk=pk, + ) + + department = get_object_or_404(Department, pk=pk if False else complaint.department_id) + user = request.user + if not ( + user.is_px_admin() + or (user.is_hospital_admin() and user.hospital == complaint.hospital) + or (user.is_champion() and user.department_id == complaint.department_id) + or (user.is_department_manager() and user.department_id == complaint.department_id) + or (user.is_basic_staff() and user.department_id == complaint.department_id) + or (user.is_director() and user.get_directed_departments().filter(id=complaint.department_id).exists()) + ): + return JsonResponse({"error": "Access denied"}, status=403) + + taxonomy = [] + if complaint.domain: + taxonomy.append(complaint.domain.get_localized_name()) + if complaint.category: + taxonomy.append(complaint.category.get_localized_name()) + if complaint.subcategory_obj: + taxonomy.append(complaint.subcategory_obj.get_localized_name()) + if complaint.classification_obj: + taxonomy.append(complaint.classification_obj.get_localized_name()) + + staff_list = [] + for inv in complaint.involved_staff.all(): + staff_list.append(inv.staff.get_localized_name() if inv.staff else "-") + if not staff_list and complaint.staff: + staff_list.append(complaint.staff.get_localized_name()) + + location_parts = [] + if complaint.legacy_location: + location_parts.append(str(complaint.legacy_location)) + if complaint.legacy_main_section: + location_parts.append(str(complaint.legacy_main_section)) + if complaint.legacy_subsection: + location_parts.append(str(complaint.legacy_subsection)) + + data = { + "type": "complaint", + "reference": complaint.reference_number or "", + "title": complaint.title or "", + "description": complaint.description or "", + "status": complaint.status, + "status_display": complaint.get_status_display(), + "severity": complaint.severity, + "severity_display": complaint.get_severity_display(), + "department": complaint.department.get_localized_name() if complaint.department else "-", + "assigned_to": complaint.assigned_to.get_full_name() if complaint.assigned_to else "-", + "patient": complaint.patient.get_full_name() if complaint.patient else (complaint.patient_name or "-"), + "patient_mrn": complaint.patient.mrn if complaint.patient else "-", + "staff": staff_list, + "taxonomy": taxonomy, + "location": " > ".join(location_parts) if location_parts else "-", + "source": complaint.source.name_en if complaint.source else "-", + "created_at": complaint.created_at.strftime("%Y-%m-%d %H:%M"), + "due_at": complaint.due_at.strftime("%Y-%m-%d %H:%M") if complaint.due_at else None, + "is_overdue": complaint.is_overdue, + "escalated": bool(complaint.escalated_at), + "expected_result": complaint.expected_result or "", + "ai_brief": complaint.ai_brief_en or "", + } + return JsonResponse(data) + + +@login_required +def department_record_inquiry(request, pk): + from django.http import JsonResponse + from apps.complaints.models import Inquiry + + inquiry = get_object_or_404( + Inquiry.objects.select_related( + "hospital", "department", "assigned_to", "legacy_location", "legacy_main_section", + "legacy_subsection", "outgoing_department", + ), + pk=pk, + ) + + user = request.user + dept_id = inquiry.department_id or inquiry.outgoing_department_id + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() + or (user.is_champion() and user.department and (user.department_id == inquiry.department_id or user.department_id == inquiry.outgoing_department_id)) + or user.is_department_manager() + or user.is_director() + ): + return JsonResponse({"error": "Access denied"}, status=403) + + location_parts = [] + if inquiry.legacy_location: + location_parts.append(str(inquiry.legacy_location)) + if inquiry.legacy_main_section: + location_parts.append(str(inquiry.legacy_main_section)) + if inquiry.legacy_subsection: + location_parts.append(str(inquiry.legacy_subsection)) + + data = { + "type": "inquiry", + "reference": inquiry.reference_number or "", + "title": inquiry.subject or "", + "description": inquiry.message or "", + "status": inquiry.status, + "status_display": inquiry.get_status_display(), + "department": inquiry.department.get_localized_name() if inquiry.department else "-", + "assigned_to": inquiry.assigned_to.get_full_name() if inquiry.assigned_to else "-", + "contact_name": inquiry.contact_name or "-", + "contact_phone": inquiry.contact_phone or "-", + "contact_email": inquiry.contact_email or "-", + "category": inquiry.get_category_display() if inquiry.category else "-", + "location": " > ".join(location_parts) if location_parts else "-", + "created_at": inquiry.created_at.strftime("%Y-%m-%d %H:%M"), + "due_at": inquiry.due_at.strftime("%Y-%m-%d %H:%M") if hasattr(inquiry, 'due_at') and inquiry.due_at else None, + "ai_summary": inquiry.short_description_en if hasattr(inquiry, 'short_description_en') else "", + } + return JsonResponse(data) + + +@login_required +def department_record_observation(request, pk): + from django.http import JsonResponse + from apps.observations.models import Observation + + observation = get_object_or_404( + Observation.objects.select_related( + "hospital", "assigned_department", "assigned_to", "category", + "legacy_location", "legacy_main_section", "legacy_subsection", + ), + pk=pk, + ) + + user = request.user + dept_id = observation.assigned_department_id + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() + or (user.is_champion() and user.department and user.department_id == observation.assigned_department_id) + or user.is_department_manager() + or user.is_director() + ): + return JsonResponse({"error": "Access denied"}, status=403) + + location_parts = [] + if observation.legacy_location: + location_parts.append(str(observation.legacy_location)) + if observation.legacy_main_section: + location_parts.append(str(observation.legacy_main_section)) + if observation.legacy_subsection: + location_parts.append(str(observation.legacy_subsection)) + + reporter = "" + if observation.is_anonymous: + reporter = str(_("Anonymous")) + elif observation.reporter_name: + reporter = observation.reporter_name + + data = { + "type": "observation", + "reference": observation.tracking_code or "", + "title": observation.title or "", + "description": observation.description or "", + "status": observation.status, + "status_display": observation.get_status_display(), + "severity": observation.severity, + "severity_display": observation.get_severity_display(), + "department": observation.assigned_department.get_localized_name() if observation.assigned_department else "-", + "assigned_to": observation.assigned_to.get_full_name() if observation.assigned_to else "-", + "category": observation.category.get_localized_name() if observation.category else "-", + "location": " > ".join(location_parts) if location_parts else "-", + "reporter": reporter, + "is_anonymous": observation.is_anonymous, + "created_at": observation.created_at.strftime("%Y-%m-%d %H:%M"), + "due_at": observation.due_at.strftime("%Y-%m-%d %H:%M") if observation.due_at else None, + "is_overdue": observation.is_overdue if hasattr(observation, 'is_overdue') else False, + "ai_summary": observation.short_description_en if hasattr(observation, 'short_description_en') else "", + } + return JsonResponse(data) + + +@login_required +def department_record_suggestion(request, pk): + from django.http import JsonResponse + from apps.feedback.models import Feedback + + feedback = get_object_or_404( + Feedback.objects.select_related( + "hospital", "department", "assigned_to", "staff", + "legacy_location", "legacy_main_section", "legacy_subsection", + ), + pk=pk, + ) + + user = request.user + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() + or (user.is_champion() and user.department and user.department_id == feedback.department_id) + or user.is_department_manager() + or user.is_director() + ): + return JsonResponse({"error": "Access denied"}, status=403) + + location_parts = [] + if feedback.legacy_location: + location_parts.append(str(feedback.legacy_location)) + if feedback.legacy_main_section: + location_parts.append(str(feedback.legacy_main_section)) + if feedback.legacy_subsection: + location_parts.append(str(feedback.legacy_subsection)) + + data = { + "type": "suggestion", + "reference": str(feedback.id)[:8].upper() if feedback.id else "", + "title": feedback.title or "", + "description": feedback.message or "", + "status": feedback.status, + "status_display": feedback.get_status_display(), + "priority": feedback.priority, + "priority_display": feedback.get_priority_display(), + "department": feedback.department.get_localized_name() if feedback.department else "-", + "assigned_to": feedback.assigned_to.get_full_name() if feedback.assigned_to else "-", + "category": feedback.get_category_display() if feedback.category else "-", + "contact_name": feedback.contact_name if not feedback.is_anonymous else str(_("Anonymous")), + "location": " > ".join(location_parts) if location_parts else "-", + "created_at": feedback.created_at.strftime("%Y-%m-%d %H:%M"), + "sentiment": feedback.get_sentiment_display() if hasattr(feedback, 'get_sentiment_display') else "", + } + return JsonResponse(data) + + +@login_required +def department_record_appreciation(request, pk): + from django.http import JsonResponse + from apps.appreciation.models import Appreciation + + appreciation = get_object_or_404( + Appreciation.objects.select_related( + "hospital", "department", "sender", "category", + "legacy_location", "legacy_main_section", "legacy_subsection", + ), + pk=pk, + ) + + user = request.user + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() + or (user.is_champion() and user.department and user.department_id == appreciation.department_id) + or user.is_department_manager() + or user.is_director() + ): + return JsonResponse({"error": "Access denied"}, status=403) + + location_parts = [] + if appreciation.legacy_location: + location_parts.append(str(appreciation.legacy_location)) + if appreciation.legacy_main_section: + location_parts.append(str(appreciation.legacy_main_section)) + if appreciation.legacy_subsection: + location_parts.append(str(appreciation.legacy_subsection)) + + data = { + "type": "appreciation", + "reference": str(appreciation.id)[:8].upper() if appreciation.id else "", + "title": "", + "description": appreciation.message_en or "", + "description_ar": appreciation.message_ar or "", + "status": appreciation.status, + "status_display": appreciation.get_status_display(), + "department": appreciation.department.get_localized_name() if appreciation.department else "-", + "sender": appreciation.sender.get_full_name() if appreciation.sender and not appreciation.is_anonymous else str(_("Anonymous")), + "recipient": appreciation.get_recipient_name(), + "category": appreciation.category.name_en if appreciation.category else "-", + "category_color": appreciation.category.color if appreciation.category else "", + "location": " > ".join(location_parts) if location_parts else "-", + "visibility": appreciation.get_visibility_display(), + "is_anonymous": appreciation.is_anonymous, + "created_at": appreciation.created_at.strftime("%Y-%m-%d %H:%M"), + "sent_at": appreciation.sent_at.strftime("%Y-%m-%d %H:%M") if appreciation.sent_at else None, + "acknowledged_at": appreciation.acknowledged_at.strftime("%Y-%m-%d %H:%M") if appreciation.acknowledged_at else None, + } + return JsonResponse(data) + + @login_required def department_analytics_api(request, pk): @@ -2152,12 +3150,15 @@ def department_analytics_api(request, pk): six_months_ago = now - timedelta(days=180) complaints_qs = Complaint.objects.filter( - Q(department=department) | Q(involved_departments__department=department) + Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True) ).distinct() + complaint_ids = complaints_qs.values_list("pk", flat=True) + complaints_clean = Complaint.objects.filter(pk__in=complaint_ids) + # 1. Complaint Trend (last 12 months) complaint_trend_raw = ( - complaints_qs.filter(created_at__gte=twelve_months_ago) + complaints_clean.filter(created_at__gte=twelve_months_ago) .annotate(month=TruncMonth("created_at")) .values("month") .annotate(count=Count("pk")) @@ -2169,7 +3170,7 @@ def department_analytics_api(request, pk): ] # 2. Complaint Status Distribution - status_raw = complaints_qs.values("status").annotate(count=Count("pk")) + status_raw = complaints_clean.values("status").annotate(count=Count("pk")) status_labels = { "open": "Open", "in_progress": "In Progress", @@ -2247,7 +3248,7 @@ def department_analytics_api(request, pk): ] # 6. Satisfaction Distribution - satisfaction_raw = complaints_qs.exclude(satisfaction__isnull=True).exclude(satisfaction="").values("satisfaction").annotate(count=Count("pk")) + satisfaction_raw = complaints_clean.exclude(satisfaction__isnull=True).exclude(satisfaction="").values("satisfaction").annotate(count=Count("pk")) satisfaction_labels = { "satisfied": "Satisfied", "neutral": "Neutral", @@ -2262,7 +3263,7 @@ def department_analytics_api(request, pk): ] # 7. Complaint Severity Distribution - severity_raw = complaints_qs.values("severity").annotate(count=Count("pk")) + severity_raw = complaints_clean.values("severity").annotate(count=Count("pk")) severity_labels = { "low": "Low", "medium": "Medium", @@ -2280,7 +3281,7 @@ def department_analytics_api(request, pk): # 8. Top Categories categories_raw = ( - complaints_qs.filter(category__isnull=False) + complaints_clean.filter(category__isnull=False) .values("category__name_en") .annotate(count=Count("pk")) .order_by("-count")[:5] @@ -2291,8 +3292,8 @@ def department_analytics_api(request, pk): ] # KPIs - total_complaints = complaints_qs.count() - resolved_count = complaints_qs.filter(status__in=["resolved", "closed"]).count() + total_complaints = complaints_clean.count() + resolved_count = complaints_clean.filter(status__in=["resolved", "closed"]).count() resolution_rate = round((resolved_count / total_complaints * 100) if total_complaints else 0, 1) current_rating = physician_ratings_qs.filter(year=now.year, month=now.month).aggregate( @@ -2306,9 +3307,9 @@ def department_analytics_api(request, pk): satisfied_count = next((s["count"] for s in satisfaction if s["label"] == "Satisfied"), 0) satisfaction_rate = round((satisfied_count / total_satisfaction * 100) if total_satisfaction else 0, 1) - reopened_count = complaints_qs.filter(reopened_from__isnull=False).count() + reopened_count = complaints_clean.filter(reopened_from__isnull=False).count() reassigned_count = ComplaintUpdate.objects.filter( - complaint__in=complaints_qs, update_type="assignment" + complaint__in=complaints_clean, update_type="assignment" ).distinct().count() return JsonResponse( @@ -2394,7 +3395,11 @@ def set_department_respondent(request, pk): @login_required @require_http_methods(["POST"]) -def set_department_manager(request, pk): + + +@login_required +@require_http_methods(["POST"]) +def set_department_role(request, pk): from .models import Department, Staff department = get_object_or_404(Department, pk=pk) @@ -2402,57 +3407,72 @@ def set_department_manager(request, pk): if not ( user.is_px_admin() + or user.is_px_employee() or (user.is_hospital_admin() and user.hospital == department.hospital) ): - messages.error(request, _("You don't have permission to set the manager.")) + messages.error(request, _("You don't have permission to set department roles.")) return redirect("organizations:department_detail", pk=pk) - manager_id = request.POST.get("manager_id") - if manager_id: - manager = Staff.objects.filter( - pk=manager_id, hospital=department.hospital, status="active" + role_field = request.POST.get("role_field", "") + valid_fields = {f[0] for f in Department.ROLE_FIELDS} | {"manager"} + if role_field not in valid_fields: + messages.error(request, _("Invalid role.")) + return redirect("organizations:department_detail", pk=pk) + + staff_id = request.POST.get("staff_id") + role_labels = dict(Department.ROLE_FIELDS) + role_labels["manager"] = "Manager" + role_label = role_labels.get(role_field, role_field) + + if staff_id: + staff = Staff.objects.filter( + pk=staff_id, hospital=department.hospital, status="active" ).first() - if not manager: + if not staff: messages.error(request, _("Invalid staff member selected.")) return redirect("organizations:department_detail", pk=pk) - if not manager.user: - from apps.organizations.services import StaffService - from django.contrib.auth.models import Group + if role_field == "manager": + if not staff.user: + from apps.organizations.services import StaffService - if not manager.email: - messages.warning( - request, - _(f"Selected staff {manager.get_full_name()} has no email. Add an email and create a user account first."), - ) - return redirect("organizations:department_detail", pk=pk) - - try: - user_account, _created, _result = StaffService.create_user_for_staff( - manager, role="department_manager", request=request - ) - department.manager = user_account + if not staff.email: + messages.warning( + request, + _(f"Selected staff {staff.get_full_name()} has no email. Add an email and create a user account first."), + ) + return redirect(f"{reverse('organizations:department_detail', kwargs={'pk': pk})}?tab=roles") + try: + user_account, _created, _result = StaffService.create_user_for_staff( + staff, role="department_manager", request=request + ) + department.manager = user_account + department.save(update_fields=["manager", "updated_at"]) + messages.success( + request, + _(f"Manager set to {staff.get_full_name()}. User account created with Department Manager role."), + ) + except ValueError as e: + messages.error(request, _(f"Could not create user account: {e}")) + return redirect(f"{reverse('organizations:department_detail', kwargs={'pk': pk})}?tab=roles") + else: + department.manager = staff.user department.save(update_fields=["manager", "updated_at"]) - messages.success( - request, - _(f"Manager set to {manager.get_full_name()}. User account created with Department Manager role."), - ) - except ValueError as e: - messages.error(request, _(f"Could not create user account: {e}")) - return redirect("organizations:department_detail", pk=pk) + messages.success(request, _(f"Manager set to {staff.get_full_name()}.")) else: - department.manager = manager.user - department.save(update_fields=["manager", "updated_at"]) - messages.success( - request, - _(f"Manager set to {manager.get_full_name()}."), - ) + setattr(department, role_field, staff) + department.save(update_fields=[role_field, "updated_at"]) + messages.success(request, _(f"{role_label} set to {staff.get_full_name()}.")) else: - department.manager = None - department.save(update_fields=["manager", "updated_at"]) - messages.success(request, _("Manager removed.")) + if role_field == "manager": + department.manager = None + department.save(update_fields=["manager", "updated_at"]) + else: + setattr(department, role_field, None) + department.save(update_fields=[role_field, "updated_at"]) + messages.success(request, _(f"{role_label} removed.")) - return redirect("organizations:department_detail", pk=pk) + return redirect(f"{reverse('organizations:department_detail', kwargs={'pk': pk})}?tab=roles") @login_required @@ -2882,3 +3902,712 @@ def staff_import_sample_csv(request): writer.writerow(headers) writer.writerow(example) return response + + +@block_source_user +@login_required +def department_manager_review(request, pk, idept_pk): + """ + Department Manager reviews a champion's response. + Shows the champion response + configurable questions form. + On approval, notifies PX Admin. On rejection, notifies champion. + """ + from apps.complaints.models import ( + ComplaintInvolvedDepartment, + ManagerReviewQuestion, + DepartmentManagerReview, + ManagerReviewAnswer, + ComplaintUpdate, + ) + from apps.core.services import AuditService + + department = get_object_or_404(Department.objects.select_related("hospital"), pk=pk) + involved_dept = get_object_or_404( + ComplaintInvolvedDepartment.objects.select_related( + "complaint", "complaint__assigned_to", "department", + ), + pk=idept_pk, + department=department, + ) + user = request.user + + if not user.is_px_admin() and not user.is_hospital_admin(): + if not ( + user.is_department_manager() + and (user.department == department or department.manager == user) + ): + messages.error(request, _("Only the department manager can review champion responses.")) + return redirect("organizations:department_detail", pk=department.pk) + + if not involved_dept.response_submitted: + messages.error(request, _("No champion response submitted yet.")) + return redirect("organizations:department_detail", pk=department.pk) + + if involved_dept.manager_review_status == "approved": + messages.info(request, _("This response has already been approved.")) + return redirect("organizations:department_detail", pk=department.pk) + + complaint = involved_dept.complaint + questions = ManagerReviewQuestion.objects.filter( + hospital=department.hospital, is_active=True, + ).order_by("order", "created_at") + + if request.method == "POST": + action = request.POST.get("review_action") + if action not in ("approve", "reject"): + messages.error(request, _("Invalid action.")) + return redirect("organizations:department_manager_review", pk=department.pk, idept_pk=involved_dept.pk) + + rejection_reason = request.POST.get("rejection_reason", "").strip() + + if action == "reject" and not rejection_reason: + messages.error(request, _("Please provide a rejection reason.")) + return redirect("organizations:department_manager_review", pk=department.pk, idept_pk=involved_dept.pk) + + review_obj = DepartmentManagerReview.objects.create( + involved_department=involved_dept, + reviewed_by=user, + status="approved" if action == "approve" else "rejected", + rejection_reason=rejection_reason, + ) + + for q in questions: + answer_key = f"question_{q.pk}" + answer = ManagerReviewAnswer( + review=review_obj, + question=q, + ) + if q.question_type in ("text", "textarea"): + answer.text_value = request.POST.get(answer_key, "").strip() + elif q.question_type == "yes_no": + val = request.POST.get(answer_key, "") + answer.text_value = val + elif q.question_type == "rating": + try: + answer.numeric_value = int(request.POST.get(answer_key, 0)) + except (ValueError, TypeError): + answer.numeric_value = None + elif q.question_type == "multiple_choice": + answer.text_value = request.POST.get(answer_key, "").strip() + answer.save() + + if action == "approve": + involved_dept.manager_review_status = "approved" + involved_dept.manager_reviewed_by = user + involved_dept.manager_reviewed_at = timezone.now() + involved_dept.save() + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Department manager {user.get_full_name()} approved the response from {department.name}.", + created_by=user, + ) + + AuditService.log_event( + event_type="dept_manager_approved_response", + description=f"Manager {user.get_full_name()} approved response for complaint {complaint.reference_number}", + user=user, + content_object=complaint, + ) + + if complaint.assigned_to and complaint.assigned_to.email: + try: + from apps.notifications.services import NotificationService, get_email_header_html + complaint_url = request.build_absolute_uri( + reverse("complaints:complaint_detail", kwargs={"pk": complaint.pk}) + ) + NotificationService.send_email( + complaint.assigned_to.email, + subject=f"Manager-Approved Response Ready - {complaint.reference_number}", + message=( + f"The department manager has approved the response from {department.name} " + f"for complaint {complaint.reference_number}. Please review.\n\n{complaint_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Manager-Approved Response Ready

+

The department manager of {department.name} has approved the champion response + for complaint {complaint.reference_number}.

+

It is now ready for your final review.

+

+ View Complaint +

+
+
+ """, + related_object=complaint, + ) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to send px-admin notification: {e}") + + messages.success(request, _("Response approved and forwarded to the PX team.")) + + elif action == "reject": + involved_dept.manager_review_status = "rejected" + involved_dept.manager_reviewed_by = user + involved_dept.manager_reviewed_at = timezone.now() + involved_dept.response_submitted = False + involved_dept.response_submitted_at = None + involved_dept.response_notes = "" + involved_dept.response_notes_en = "" + involved_dept.response_notes_ar = "" + involved_dept.save() + + ComplaintUpdate.objects.create( + complaint=complaint, + update_type="note", + message=f"Department manager {user.get_full_name()} rejected the response from {department.name}. Reason: {rejection_reason}", + created_by=user, + ) + + AuditService.log_event( + event_type="dept_manager_rejected_response", + description=f"Manager {user.get_full_name()} rejected response for complaint {complaint.reference_number}", + user=user, + content_object=complaint, + ) + + recipients = set() + if department.champion and department.champion.user and department.champion.user.email: + recipients.add(department.champion.user.email) + if involved_dept.assigned_to and involved_dept.assigned_to.email: + recipients.add(involved_dept.assigned_to.email) + for email in recipients: + try: + from apps.notifications.services import NotificationService, get_email_header_html + dept_url = request.build_absolute_uri( + reverse("organizations:department_detail", kwargs={"pk": department.pk}) + ) + NotificationService.send_email( + email, + subject=f"Response Rejected by Manager - {complaint.reference_number}", + message=( + f"The department manager rejected your response for complaint " + f"{complaint.reference_number}.\n\nReason: {rejection_reason}\n\n" + f"Please submit a new response.\n\n{dept_url}" + ), + html_message=f""" +
+ {get_email_header_html()} +
+

Response Rejected by Manager

+

The department manager has rejected your response for complaint + {complaint.reference_number}.

+

Reason: {rejection_reason}

+

Please review and submit a new response.

+

+ Go to Department +

+
+
+ """, + related_object=complaint, + ) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Failed to send rejection notification: {e}") + + messages.success(request, _("Response rejected. The champion has been notified to submit a new response.")) + + return redirect("organizations:department_detail", pk=department.pk) + + context = { + "department": department, + "involved_dept": involved_dept, + "complaint": complaint, + "questions": questions, + } + return render(request, "organizations/department_manager_review.html", context) + + +@block_source_user +@login_required +def manager_review_questions(request): + """PX Admin manages the questions shown to department managers during review.""" + from apps.complaints.models import ManagerReviewQuestion + + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, _("You don't have permission to manage review questions.")) + return redirect("dashboard:dashboard") + + hospital = request.tenant_hospital + if not hospital: + messages.error(request, _("No hospital context found.")) + return redirect("dashboard:dashboard") + + questions = ManagerReviewQuestion.objects.filter(hospital=hospital).order_by("order", "created_at") + + context = { + "questions": questions, + "hospital": hospital, + } + return render(request, "organizations/manager_review_questions.html", context) + + +@block_source_user +@login_required +@require_http_methods(["GET", "POST"]) +def manager_review_question_create(request): + from apps.complaints.models import ManagerReviewQuestion, ManagerReviewQuestionType + + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, _("You don't have permission to manage review questions.")) + return redirect("dashboard:dashboard") + + hospital = request.tenant_hospital + if not hospital: + messages.error(request, _("No hospital context found.")) + return redirect("dashboard:dashboard") + + if request.method == "POST": + text_en = request.POST.get("text_en", "").strip() + text_ar = request.POST.get("text_ar", "").strip() + question_type = request.POST.get("question_type", "textarea") + order = int(request.POST.get("order", 0)) + is_active = request.POST.get("is_active") == "on" + + if not text_en: + messages.error(request, _("English text is required.")) + return redirect("organizations:manager_review_question_create") + + choices_raw = request.POST.get("choices_json", "").strip() + choices_json = [] + if question_type == "multiple_choice" and choices_raw: + import json + try: + choices_json = json.loads(choices_raw) + except json.JSONDecodeError: + choices_json = [c.strip() for c in choices_raw.split(",") if c.strip()] + + ManagerReviewQuestion.objects.create( + hospital=hospital, + text_en=text_en, + text_ar=text_ar, + question_type=question_type, + choices_json=choices_json, + order=order, + is_active=is_active, + created_by=user, + ) + messages.success(request, _("Question created successfully.")) + return redirect("organizations:manager_review_questions") + + question_types = ManagerReviewQuestionType.choices + context = {"hospital": hospital, "question_types": question_types} + return render(request, "organizations/manager_review_question_form.html", context) + + +@block_source_user +@login_required +@require_http_methods(["GET", "POST"]) +def manager_review_question_update(request, pk): + from apps.complaints.models import ManagerReviewQuestion, ManagerReviewQuestionType + + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, _("You don't have permission to manage review questions.")) + return redirect("dashboard:dashboard") + + hospital = request.tenant_hospital + question = get_object_or_404(ManagerReviewQuestion, pk=pk, hospital=hospital) + + if request.method == "POST": + question.text_en = request.POST.get("text_en", "").strip() + question.text_ar = request.POST.get("text_ar", "").strip() + question.question_type = request.POST.get("question_type", "textarea") + question.order = int(request.POST.get("order", 0)) + question.is_active = request.POST.get("is_active") == "on" + + choices_raw = request.POST.get("choices_json", "").strip() + if question.question_type == "multiple_choice" and choices_raw: + import json + try: + question.choices_json = json.loads(choices_raw) + except json.JSONDecodeError: + question.choices_json = [c.strip() for c in choices_raw.split(",") if c.strip()] + else: + question.choices_json = [] + + question.save() + messages.success(request, _("Question updated successfully.")) + return redirect("organizations:manager_review_questions") + + question_types = ManagerReviewQuestionType.choices + context = {"question": question, "hospital": hospital, "question_types": question_types} + return render(request, "organizations/manager_review_question_form.html", context) + + +@block_source_user +@login_required +@require_POST +def manager_review_question_delete(request, pk): + from apps.complaints.models import ManagerReviewQuestion + + user = request.user + if not (user.is_px_admin() or user.is_hospital_admin()): + messages.error(request, _("You don't have permission to manage review questions.")) + return redirect("dashboard:dashboard") + + hospital = request.tenant_hospital + question = get_object_or_404(ManagerReviewQuestion, pk=pk, hospital=hospital) + question.delete() + messages.success(request, _("Question deleted.")) + return redirect("organizations:manager_review_questions") + + +# ==================== OrgSubSection CRUD ==================== + + +@block_source_user +@login_required +def orgsection_list(request): + queryset = Section.objects.select_related("department", "department__hospital", "champion") + + user = request.user + if not user.is_px_admin() and user.hospital: + queryset = queryset.filter(department__hospital=user.hospital) + + department_filter = request.GET.get("department") + if department_filter: + queryset = queryset.filter(department_id=department_filter) + + status_filter = request.GET.get("status") + if status_filter: + queryset = queryset.filter(status=status_filter) + + search_query = request.GET.get("search") + if search_query: + queryset = queryset.filter( + Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query) + ) + + queryset = queryset.order_by("department__name", "name_en") + + page_size = int(request.GET.get("page_size", 25)) + paginator = Paginator(queryset, page_size) + page_number = request.GET.get("page", 1) + page_obj = paginator.get_page(page_number) + + departments = Department.objects.filter(status="active") + if not user.is_px_admin() and user.hospital: + departments = departments.filter(hospital=user.hospital) + + context = { + "page_obj": page_obj, + "sections": page_obj.object_list, + "departments": departments, + "filters": request.GET, + } + return render(request, "organizations/orgsection_list.html", context) + + +@block_source_user +@login_required +def orgsection_create(request): + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You don't have permission to create sections") + + if request.method == "POST": + name_en = request.POST.get("name_en") + name_ar = request.POST.get("name_ar", "") + code = request.POST.get("code", "") + department_id = request.POST.get("department") + status = request.POST.get("status", "active") + location_type = request.POST.get("location_type", "") + sub_location = request.POST.get("sub_location", "") + floor = request.POST.get("floor", "") + champion_id = request.POST.get("champion") + + if name_en and department_id: + department = get_object_or_404(Department, pk=department_id) + if not user.is_px_admin() and department.hospital != user.hospital: + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You can only create sections in your hospital") + + Section.objects.create( + name_en=name_en, + name_ar=name_ar, + code=code, + department=department, + status=status, + location_type=location_type, + sub_location=sub_location, + floor=floor, + champion_id=champion_id if champion_id else None, + ) + messages.success(request, "Section created successfully.") + return redirect("organizations:orgsection_list") + + departments = Department.objects.filter(status="active") + if not user.is_px_admin() and user.hospital: + departments = departments.filter(hospital=user.hospital) + + staff = Staff.objects.filter(status="active").order_by("first_name", "last_name") + + context = {"departments": departments, "staff": staff} + return render(request, "organizations/orgsection_form.html", context) + + +@block_source_user +@login_required +def orgsection_detail(request, pk): + section = get_object_or_404( + Section.objects.select_related("department", "department__hospital", "champion"), + pk=pk, + ) + + user = request.user + if not user.is_px_admin() and user.hospital: + if section.department.hospital != user.hospital: + messages.error(request, _("You don't have permission to view this section.")) + return redirect("organizations:orgsection_list") + + sub_sections = SubSection.objects.filter(section=section).order_by("name_en") + + context = { + "section": section, + "sub_sections": sub_sections, + "sub_sections_count": sub_sections.count(), + "can_edit": user.is_px_admin() or (user.is_hospital_admin() and user.hospital == section.department.hospital), + } + return render(request, "organizations/orgsection_detail.html", context) + + +@block_source_user +@login_required +def orgsection_update(request, pk): + section = get_object_or_404(Section, pk=pk) + + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You don't have permission to update sections") + + if not user.is_px_admin() and section.department.hospital != user.hospital: + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You can only update sections in your hospital") + + if request.method == "POST": + section.name_en = request.POST.get("name_en", section.name_en) + section.name_ar = request.POST.get("name_ar", "") + section.code = request.POST.get("code", "") + section.status = request.POST.get("status", section.status) + section.location_type = request.POST.get("location_type", "") + section.sub_location = request.POST.get("sub_location", "") + section.floor = request.POST.get("floor", "") + champion_id = request.POST.get("champion") + section.champion_id = champion_id if champion_id else None + section.save() + + messages.success(request, "Section updated successfully.") + return redirect("organizations:orgsection_detail", pk=section.pk) + + departments = Department.objects.filter(status="active") + if not user.is_px_admin() and user.hospital: + departments = departments.filter(hospital=user.hospital) + + staff = Staff.objects.filter(status="active").order_by("first_name", "last_name") + + context = {"section": section, "departments": departments, "staff": staff} + return render(request, "organizations/orgsection_form.html", context) + + +@block_source_user +@login_required +def orgsection_delete(request, pk): + section = get_object_or_404(Section, pk=pk) + + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You don't have permission to delete sections") + + if not user.is_px_admin() and section.department.hospital != user.hospital: + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You can only delete sections in your hospital") + + if request.method == "POST": + sub_count = section.sub_subsections.count() + if sub_count > 0: + messages.error(request, f"Cannot delete section. {sub_count} sub-sections are linked to it.") + return redirect("organizations:orgsection_list") + + section.delete() + messages.success(request, "Section deleted successfully.") + return redirect("organizations:orgsection_list") + + context = {"section": section} + return render(request, "organizations/orgsection_confirm_delete.html", context) + + +# ==================== SubSection CRUD ==================== + + +@block_source_user +@login_required +def orgsubsection_list(request): + queryset = SubSection.objects.select_related("section", "section__department", "section__department__hospital") + + user = request.user + if not user.is_px_admin() and user.hospital: + queryset = queryset.filter(section__department__hospital=user.hospital) + + section_filter = request.GET.get("section") + if section_filter: + queryset = queryset.filter(section_id=section_filter) + + status_filter = request.GET.get("status") + if status_filter: + queryset = queryset.filter(status=status_filter) + + search_query = request.GET.get("search") + if search_query: + queryset = queryset.filter( + Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query) + ) + + queryset = queryset.order_by("section__name_en", "name_en") + + page_size = int(request.GET.get("page_size", 25)) + paginator = Paginator(queryset, page_size) + page_number = request.GET.get("page", 1) + page_obj = paginator.get_page(page_number) + + org_sections = Section.objects.filter(status="active").select_related("department") + if not user.is_px_admin() and user.hospital: + org_sections = org_sections.filter(department__hospital=user.hospital) + + context = { + "page_obj": page_obj, + "sub_sections": page_obj.object_list, + "org_sections": org_sections, + "filters": request.GET, + } + return render(request, "organizations/orgsubsection_list.html", context) + + +@block_source_user +@login_required +def orgsubsection_create(request): + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You don't have permission to create sub-sections") + + preselected_section_id = request.GET.get("section") + preselected_section = None + if preselected_section_id: + try: + preselected_section = Section.objects.get(pk=preselected_section_id) + except (Section.DoesNotExist, ValueError): + preselected_section = None + + if request.method == "POST": + name_en = request.POST.get("name_en") + name_ar = request.POST.get("name_ar", "") + code = request.POST.get("code", "") + section_id = request.POST.get("section") + status = request.POST.get("status", "active") + champion_id = request.POST.get("champion") + + if name_en and section_id: + parent_section = get_object_or_404(Section, pk=section_id) + if not user.is_px_admin() and parent_section.department.hospital != user.hospital: + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You can only create sub-sections in your hospital") + + SubSection.objects.create( + name_en=name_en, + name_ar=name_ar, + code=code, + section=parent_section, + status=status, + champion_id=champion_id if champion_id else None, + ) + messages.success(request, "Sub-section created successfully.") + return redirect("organizations:orgsection_detail", pk=section_id) + + org_sections = Section.objects.filter(status="active").select_related("department") + if not user.is_px_admin() and user.hospital: + org_sections = org_sections.filter(department__hospital=user.hospital) + + staff = Staff.objects.filter(status="active").order_by("first_name", "last_name") + + context = { + "org_sections": org_sections, + "preselected_section": preselected_section, + "staff": staff, + } + return render(request, "organizations/orgsubsection_form.html", context) + + +@block_source_user +@login_required +def orgsubsection_update(request, pk): + sub_section = get_object_or_404(SubSection, pk=pk) + + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You don't have permission to update sub-sections") + + if not user.is_px_admin() and sub_section.section.department.hospital != user.hospital: + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You can only update sub-sections in your hospital") + + if request.method == "POST": + sub_section.name_en = request.POST.get("name_en", sub_section.name_en) + sub_section.name_ar = request.POST.get("name_ar", "") + sub_section.code = request.POST.get("code", "") + sub_section.status = request.POST.get("status", sub_section.status) + champion_id = request.POST.get("champion") + sub_section.champion_id = champion_id if champion_id else None + sub_section.save() + + messages.success(request, "Sub-section updated successfully.") + return redirect("organizations:orgsection_detail", pk=sub_section.section_id) + + org_sections = Section.objects.filter(status="active").select_related("department") + if not user.is_px_admin() and user.hospital: + org_sections = org_sections.filter(department__hospital=user.hospital) + + staff = Staff.objects.filter(status="active").order_by("first_name", "last_name") + + context = { + "sub_section": sub_section, + "org_sections": org_sections, + "staff": staff, + } + return render(request, "organizations/orgsubsection_form.html", context) + + +@block_source_user +@login_required +def orgsubsection_delete(request, pk): + sub_section = get_object_or_404(SubSection, pk=pk) + + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You don't have permission to delete sub-sections") + + if not user.is_px_admin() and sub_section.section.department.hospital != user.hospital: + from django.http import HttpResponseForbidden + return HttpResponseForbidden("You can only delete sub-sections in your hospital") + + if request.method == "POST": + parent_pk = sub_section.section_id + sub_section.delete() + messages.success(request, "Sub-section deleted successfully.") + return redirect("organizations:orgsection_detail", pk=parent_pk) + + context = {"sub_section": sub_section} + return render(request, "organizations/orgsubsection_confirm_delete.html", context) diff --git a/apps/organizations/urls.py b/apps/organizations/urls.py index 979433f..05df917 100644 --- a/apps/organizations/urls.py +++ b/apps/organizations/urls.py @@ -4,20 +4,24 @@ from rest_framework.routers import DefaultRouter from .views import ( DepartmentViewSet, HospitalViewSet, - LocationViewSet, - MainSectionViewSet, + LegacyLocationViewSet, + LegacyMainSectionViewSet, + LegacySubSectionViewSet, + OrgSubSectionViewSet, OrganizationViewSet, PatientViewSet, StaffViewSet, - SubSectionViewSet, - api_location_list, + api_areas_by_hospital, + api_department_contacts, + api_departments_by_category, api_main_section_list, + api_sections_by_department, + api_subsections_by_section, api_staff_hierarchy, api_staff_hierarchy_children, + api_staff_by_department, api_subsection_list, ajax_departments, - ajax_main_sections, - ajax_subsections, ) from . import ui_views from .ui_views import ( @@ -32,6 +36,15 @@ from .ui_views import ( subsection_create, subsection_update, subsection_delete, + orgsection_list, + orgsection_create, + orgsection_detail, + orgsection_update, + orgsection_delete, + orgsubsection_list, + orgsubsection_create, + orgsubsection_update, + orgsubsection_delete, ) app_name = "organizations" @@ -42,9 +55,10 @@ router.register(r"hospitals", HospitalViewSet, basename="hospital-api") router.register(r"departments", DepartmentViewSet, basename="department-api") router.register(r"staff", StaffViewSet, basename="staff-api") router.register(r"patients", PatientViewSet, basename="patient-api") -router.register(r"locations", LocationViewSet, basename="location-api") -router.register(r"main-sections", MainSectionViewSet, basename="main-section-api") -router.register(r"subsections", SubSectionViewSet, basename="subsection-api") +router.register(r"locations", LegacyLocationViewSet, basename="location-api") +router.register(r"main-sections", LegacyMainSectionViewSet, basename="main-section-api") +router.register(r"subsections", LegacySubSectionViewSet, basename="subsection-api") +router.register(r"org-sections", OrgSubSectionViewSet, basename="org-section-api") urlpatterns = [ # UI Views (come first - more specific routes) @@ -55,8 +69,25 @@ urlpatterns = [ path("departments/", ui_views.department_list, name="department_list"), path("departments//", ui_views.department_detail, name="department_detail"), path("departments//analytics/", ui_views.department_analytics_api, name="department_analytics_api"), - path("departments//set-respondent/", ui_views.set_department_respondent, name="set_department_respondent"), - path("departments//set-manager/", ui_views.set_department_manager, name="set_department_manager"), + path("departments//complaints/", ui_views.department_complaints_list, name="department_complaints_list"), + path("departments//complaints//", ui_views.department_complaint_detail, name="department_complaint_detail"), + path("departments//manager-review//", ui_views.department_manager_review, name="department_manager_review"), + path("manager-review-questions/", ui_views.manager_review_questions, name="manager_review_questions"), + path("manager-review-questions/create/", ui_views.manager_review_question_create, name="manager_review_question_create"), + path("manager-review-questions//edit/", ui_views.manager_review_question_update, name="manager_review_question_update"), + path("manager-review-questions//delete/", ui_views.manager_review_question_delete, name="manager_review_question_delete"), + path("departments//inquiries/", ui_views.department_inquiries_list, name="department_inquiries_list"), + path("departments//inquiries//", ui_views.department_inquiry_detail, name="department_inquiry_detail"), + path("departments//observations/", ui_views.department_observations_list, name="department_observations_list"), + path("departments//observations//", ui_views.department_observation_detail, name="department_observation_detail"), + path("departments//staff//edit/", ui_views.department_staff_update, name="department_staff_update"), + path("departments//staff//", ui_views.department_staff_detail, name="department_staff_detail"), + path("api/dept-complaint//", ui_views.department_record_complaint, name="department_record_complaint"), + path("api/dept-inquiry//", ui_views.department_record_inquiry, name="department_record_inquiry"), + path("api/dept-observation//", ui_views.department_record_observation, name="department_record_observation"), + path("api/dept-suggestion//", ui_views.department_record_suggestion, name="department_record_suggestion"), + path("api/dept-appreciation//", ui_views.department_record_appreciation, name="department_record_appreciation"), + path("departments//set-role/", ui_views.set_department_role, name="set_department_role"), path("staff/create/", ui_views.staff_create, name="staff_create"), path("staff/import/", ui_views.staff_import, name="staff_import"), path("staff/import/sample-csv/", ui_views.staff_import_sample_csv, name="staff_import_sample_csv"), @@ -93,14 +124,29 @@ urlpatterns = [ path("subsections/create/", subsection_create, name="subsection_create"), path("subsections//edit/", subsection_update, name="subsection_update"), path("subsections//delete/", subsection_delete, name="subsection_delete"), + # Org Section CRUD + path("org-sections/", orgsection_list, name="orgsection_list"), + path("org-sections/create/", orgsection_create, name="orgsection_create"), + path("org-sections//", orgsection_detail, name="orgsection_detail"), + path("org-sections//edit/", orgsection_update, name="orgsection_update"), + path("org-sections//delete/", orgsection_delete, name="orgsection_delete"), + # Org Sub-Section CRUD + path("org-sub-sections/", orgsubsection_list, name="orgsubsection_list"), + path("org-sub-sections/create/", orgsubsection_create, name="orgsubsection_create"), + path("org-sub-sections//edit/", orgsubsection_update, name="orgsubsection_update"), + path("org-sub-sections//delete/", orgsubsection_delete, name="orgsubsection_delete"), # API Routes for complaint form dropdowns (public access) - path("dropdowns/locations/", api_location_list, name="api_location_list"), path("dropdowns/main-sections/", api_main_section_list, name="api_main_section_list"), path("dropdowns/subsections/", api_subsection_list, name="api_subsection_list"), # AJAX Routes for cascading dropdowns in complaint form - path("ajax/main-sections/", ajax_main_sections, name="ajax_main_sections"), - path("ajax/subsections/", ajax_subsections, name="ajax_subsections"), path("ajax/departments/", ajax_departments, name="ajax_departments"), + # New hierarchy API endpoints + path("dropdowns/departments-by-category/", api_departments_by_category, name="api_departments_by_category"), + path("dropdowns/sections//", api_sections_by_department, name="api_sections_by_department"), + path("dropdowns/subsections//", api_subsections_by_section, name="api_subsections_by_section"), + path("dropdowns/areas/", api_areas_by_hospital, name="api_areas_by_hospital"), + path("dropdowns/staff-by-department//", api_staff_by_department, name="api_staff_by_department"), + path("dropdowns/department-contacts//", api_department_contacts, name="api_department_contacts"), # Staff Hierarchy API (for D3 visualization) path("api/staff/hierarchy/", api_staff_hierarchy, name="api_staff_hierarchy"), path( diff --git a/apps/organizations/views.py b/apps/organizations/views.py index 5116b0a..04e9db6 100644 --- a/apps/organizations/views.py +++ b/apps/organizations/views.py @@ -3,6 +3,7 @@ Organizations views and viewsets """ from django.db import models +from django.http import JsonResponse from rest_framework import status, viewsets from rest_framework.decorators import action, api_view, permission_classes from rest_framework.permissions import IsAuthenticated, AllowAny @@ -15,7 +16,19 @@ from apps.accounts.permissions import ( IsPXAdmin, ) -from .models import Department, Hospital, Organization, Patient, Staff, Location, MainSection, SubSection +from .models import ( + Area, + Department, + Hospital, + Organization, + Patient, + Staff, + LegacyLocation, + LegacyMainSection, + LegacySubSection, + OrgSubSection, + Section, +) from .models import Staff as StaffModel from .serializers import ( DepartmentSerializer, @@ -23,6 +36,7 @@ from .serializers import ( LocationSerializer, MainSectionSerializer, OrganizationSerializer, + OrgSubSectionSerializer, PatientListSerializer, PatientSerializer, StaffSerializer, @@ -277,19 +291,22 @@ class StaffViewSet(viewsets.ModelViewSet): {"error": "You can only create accounts for staff in your department"}, status=status.HTTP_403_FORBIDDEN ) - # Get role - always 'staff' by default for staff accounts - from .services import StaffService + # Get role from request or default to 'staff' + role = request.data.get('role', 'staff') if hasattr(request, 'data') else 'staff' + allowed_roles = ['staff', 'px_employee', 'hospital_admin', 'department_manager'] + if role not in allowed_roles: + role = 'staff' - role = 'staff' + from .services import StaffService try: user_account, was_created, password = StaffService.create_user_for_staff(staff, role=role, request=request) if was_created: - # Send email with credentials (password is already set in create_user_for_staff) + # Send password reset link try: - StaffService.send_credentials_email(staff, password, request) - message = "User account created and credentials emailed successfully" + StaffService.send_password_reset_email(staff, request) + message = "User account created and password reset link emailed successfully" except Exception as e: message = f"User account created. Email sending failed: {str(e)}" else: @@ -378,8 +395,7 @@ class StaffViewSet(viewsets.ModelViewSet): @action(detail=True, methods=["post"]) def send_invitation(self, request, pk=None): """ - Send credentials email to staff member. - Generates new password and emails it. + Send a password reset email to staff member. """ staff = self.get_object() @@ -402,18 +418,10 @@ class StaffViewSet(viewsets.ModelViewSet): from .services import StaffService try: - # Generate new password - password = StaffService.generate_password() - - # Update user password - staff.user.set_password(password) - staff.user.save() - - # Send email - StaffService.send_credentials_email(staff, password, request) + StaffService.send_password_reset_email(staff, request) serializer = self.get_serializer(staff) - return Response({"message": "Invitation email sent successfully", "staff": serializer.data}) + return Response({"message": "Password reset email sent successfully", "staff": serializer.data}) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -421,14 +429,9 @@ class StaffViewSet(viewsets.ModelViewSet): @action(detail=True, methods=["post"]) def reset_password_and_resend(self, request, pk=None): """ - Reset user password and resend credentials email. + Reset user password and resend password reset link. - This action: - 1. Generates a new random password - 2. Updates the user's password - 3. Sends credentials email via NotificationService - - Returns the new password in the response (only shown to admin). + This action sends a secure one-time password reset link to the staff member. """ staff = self.get_object() @@ -452,14 +455,13 @@ class StaffViewSet(viewsets.ModelViewSet): try: # Reset password and resend credentials - new_password, notification_log = StaffService.reset_password_and_resend_credentials(staff, request=request) + _, notification_log = StaffService.reset_password_and_resend_credentials(staff, request=request) serializer = self.get_serializer(staff) return Response( { "success": True, - "message": "Password reset successfully and credentials email sent", - "new_password": new_password, # Show password to admin + "message": "Password reset link sent successfully", "email_sent_to": staff.email, "notification_log_id": str(notification_log.id) if notification_log else None, "staff": serializer.data, @@ -679,54 +681,65 @@ class PatientViewSet(viewsets.ModelViewSet): return Response(data) -class LocationViewSet(viewsets.ReadOnlyModelViewSet): +class LegacyLocationViewSet(viewsets.ReadOnlyModelViewSet): """ - ViewSet for Location model. + ViewSet for LegacyLocation model. Publicly accessible for complaint form dropdowns. """ - queryset = Location.objects.all() + queryset = LegacyLocation.objects.all() serializer_class = LocationSerializer permission_classes = [] # Public access ordering = ["id"] -class MainSectionViewSet(viewsets.ReadOnlyModelViewSet): +class LegacyMainSectionViewSet(viewsets.ReadOnlyModelViewSet): """ - ViewSet for MainSection model. + ViewSet for LegacyMainSection model. Publicly accessible for complaint form dropdowns. """ - queryset = MainSection.objects.all() + queryset = LegacyMainSection.objects.all() serializer_class = MainSectionSerializer permission_classes = [] # Public access ordering = ["id"] -class SubSectionViewSet(viewsets.ReadOnlyModelViewSet): +class LegacySubSectionViewSet(viewsets.ReadOnlyModelViewSet): """ - ViewSet for SubSection model. + ViewSet for LegacySubSection model. Publicly accessible for complaint form dropdowns. Supports filtering by location and main_section. """ - queryset = SubSection.objects.all() + queryset = LegacySubSection.objects.all() serializer_class = SubSectionSerializer permission_classes = [] # Public access filterset_fields = ["location", "main_section"] ordering = ["internal_id"] +class OrgSubSectionViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Section.objects.filter(status="active").select_related("department") + serializer_class = OrgSubSectionSerializer + filterset_fields = ["department"] + search_fields = ["name_en", "name_ar", "code"] + + +# Backward compatibility alias +SectionViewSet = OrgSubSectionViewSet + + # Dedicated API endpoints for complaint form dropdowns # These avoid conflicts with UI routes @api_view(["GET"]) @permission_classes([]) def api_location_list(request): """API endpoint for location dropdown (public access)""" - locations = Location.active_locations() + locations = LegacyLocation.active_locations() serializer = LocationSerializer(locations, many=True) return Response(serializer.data) @@ -735,7 +748,7 @@ def api_location_list(request): @permission_classes([]) def api_main_section_list(request): """API endpoint for main section dropdown (public access)""" - sections = MainSection.objects.all().order_by("id") + sections = LegacyMainSection.objects.all().order_by("id") serializer = MainSectionSerializer(sections, many=True) return Response(serializer.data) @@ -748,7 +761,7 @@ def api_subsection_list(request): - location: Filter by location ID - main_section: Filter by main section ID """ - subsections = SubSection.objects.all().order_by("name") + subsections = LegacySubSection.objects.all().order_by("name") location_id = request.GET.get("location") main_section_id = request.GET.get("main_section") @@ -775,12 +788,12 @@ def ajax_main_sections(request): if location_id: # Get main sections that have subsections for this location available_section_ids = ( - SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() + LegacySubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() ) - main_sections = MainSection.objects.filter(id__in=available_section_ids).order_by("name_en") + main_sections = LegacyMainSection.objects.filter(id__in=available_section_ids).order_by("name_en") else: - main_sections = MainSection.objects.none() + main_sections = LegacyMainSection.objects.none() serializer = MainSectionSerializer(main_sections, many=True) return Response({"sections": serializer.data}) @@ -797,16 +810,101 @@ def ajax_subsections(request): main_section_id = request.GET.get("main_section_id") if location_id and main_section_id: - subsections = SubSection.objects.filter(location_id=location_id, main_section_id=main_section_id).order_by( + subsections = LegacySubSection.objects.filter(location_id=location_id, main_section_id=main_section_id).order_by( "name_en" ) else: - subsections = SubSection.objects.none() + subsections = LegacySubSection.objects.none() serializer = SubSectionSerializer(subsections, many=True) return Response({"subsections": serializer.data}) +def api_departments_by_category(request): + from apps.organizations.serializers import DepartmentSerializer + category = request.GET.get("category", "") + hospital_id = request.GET.get("hospital", "") + qs = Department.objects.filter(status="active") + if category: + qs = qs.filter(category=category) + if hospital_id: + qs = qs.filter(hospital_id=hospital_id) + data = DepartmentSerializer(qs.order_by("name_en"), many=True).data + return JsonResponse(data, safe=False) + + +def api_sections_by_department(request, department_id): + from apps.organizations.serializers import OrgSubSectionSerializer + qs = Section.objects.filter(department_id=department_id, status="active") + data = OrgSubSectionSerializer(qs.order_by("name_en"), many=True).data + return JsonResponse(data, safe=False) + + +def api_subsections_by_section(request, section_id): + from apps.organizations.serializers import SubSectionSerializer + qs = SubSection.objects.filter(section_id=section_id, status="active") + data = SubSectionSerializer(qs.order_by("name_en"), many=True).data + return JsonResponse(data, safe=False) + + +def api_areas_by_hospital(request): + from apps.organizations.serializers import AreaSerializer + + qs = Area.objects.filter(status="active") + hospital_id = request.GET.get("hospital") + if hospital_id: + qs = qs.filter(hospital_id=hospital_id) + location_type = request.GET.get("location_type") + if location_type: + qs = qs.filter(location_type=location_type) + data = AreaSerializer(qs.order_by("name_en"), many=True).data + return JsonResponse(data, safe=False) + + +def api_staff_by_department(request, department_id): + qs = Staff.objects.filter(department_id=department_id, status="active") + data = [ + { + "id": str(s.id), + "name": s.get_full_name(), + "employee_id": s.employee_id or "", + "title": s.job_title or "", + } + for s in qs.order_by("first_name", "last_name") + ] + return JsonResponse(data, safe=False) + + + +@api_view(["GET"]) +@permission_classes([]) +def api_department_contacts(request, department_id): + dept = Department.objects.filter(pk=department_id, status="active").first() + if not dept: + return JsonResponse({"error": "Department not found"}, status=404) + holders = dept.get_role_holders() + # Convert non-serializable Staff objects to dicts for JSON response + serializable_holders = [] + for holder in holders: + staff = holder.get("staff") + serializable_holders.append( + { + "staff": { + "id": str(staff.id) if staff else None, + "name": staff.get_full_name() if staff else None, + "email": staff.email if staff else None, + "employee_id": staff.employee_id if staff else None, + }, + "staff_id": holder.get("staff_id"), + "name": holder.get("name"), + "email": holder.get("email"), + "role_field": holder.get("role_field"), + "role_label": holder.get("role_label"), + } + ) + return JsonResponse(serializable_holders, safe=False) + + @api_view(["GET"]) @permission_classes([]) def ajax_departments(request): diff --git a/apps/projects/ui_views.py b/apps/projects/ui_views.py index 405dc58..79fd222 100644 --- a/apps/projects/ui_views.py +++ b/apps/projects/ui_views.py @@ -140,7 +140,7 @@ def project_detail(request, pk): focus_phases[phase_key] = phase_obj focus_tasks[phase_key] = phase_obj.tasks.all().order_by("order", "created_at") - can_edit = user.is_px_admin() or user.is_hospital_admin or user.is_department_manager + can_edit = user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager today = timezone.now().date() context = { @@ -166,7 +166,7 @@ def project_create(request, template_pk=None): user = request.user # Check permission (PX Admin, Hospital Admin, or Department Manager) - if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager()): messages.error(request, _("You don't have permission to create projects.")) return redirect("projects:project_list") @@ -309,7 +309,7 @@ def project_edit(request, pk): return redirect("projects:project_list") # Check edit permission (PX Admin, Hospital Admin, or Department Manager) - if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager()): messages.error(request, _("You don't have permission to edit projects.")) return redirect("projects:project_detail", pk=project.pk) @@ -566,7 +566,7 @@ def task_toggle_status(request, project_pk, task_pk, phase=None): project = get_object_or_404(QIProject, pk=project_pk, is_template=False) task = get_object_or_404(QIProjectTask, pk=task_pk, project=project) - if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager()): messages.error(request, _("You don't have permission to update task status.")) return redirect("projects:project_detail", pk=project.pk) @@ -782,7 +782,7 @@ def convert_action_to_project(request, action_pk): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager()): messages.error(request, _("You don't have permission to create projects.")) return redirect("px_action_center:action_detail", pk=action_pk) @@ -874,7 +874,7 @@ def pdca_phase_detail(request, pk, phase): ) tasks = pdca_phase.tasks.all().order_by("order", "created_at") - can_edit = user.is_px_admin() or user.is_hospital_admin or user.is_department_manager + can_edit = user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager from django.utils import timezone @@ -910,7 +910,7 @@ def pdca_phase_edit(request, pk, phase): messages.error(request, _("You don't have permission.")) return redirect("projects:project_list") - can_edit = user.is_px_admin() or user.is_hospital_admin or user.is_department_manager + can_edit = user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager if not can_edit: messages.error(request, _("You don't have permission to edit this project.")) return redirect("projects:pdca_phase_detail", pk=project.pk, phase=phase) @@ -983,7 +983,7 @@ def focus_phase_detail(request, pk, phase): ) tasks = focus_phase.tasks.all().order_by("order", "created_at") - can_edit = user.is_px_admin() or user.is_hospital_admin or user.is_department_manager + can_edit = user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager from django.utils import timezone @@ -1020,7 +1020,7 @@ def focus_phase_edit(request, pk, phase): messages.error(request, _("You don't have permission.")) return redirect("projects:project_list") - can_edit = user.is_px_admin() or user.is_hospital_admin or user.is_department_manager + can_edit = user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager if not can_edit: messages.error(request, _("You don't have permission to edit this project.")) return redirect("projects:focus_phase_detail", pk=project.pk, phase=phase) @@ -1111,7 +1111,7 @@ def _check_project_permission(project, user): def _get_can_edit(user): """Helper to check edit permissions.""" - return user.is_px_admin() or user.is_hospital_admin or user.is_department_manager + return user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager @block_source_user diff --git a/apps/px_action_center/tasks.py b/apps/px_action_center/tasks.py index 74182ce..2aa70f1 100644 --- a/apps/px_action_center/tasks.py +++ b/apps/px_action_center/tasks.py @@ -78,11 +78,21 @@ def send_sla_reminders(): if 0 < hours_until_due <= 4: # Within 4 hours of due date # Send reminder if action.assigned_to and action.assigned_to.email: - from apps.notifications.services import send_email + from apps.notifications.services import send_email, get_email_header_html send_email( email=action.assigned_to.email, subject=f"SLA Reminder: Action Due Soon - {action.title}", message=f"Action {action.title} is due in {int(hours_until_due)} hours.", + html_message=f""" +
+ {get_email_header_html()} +
+

SLA Reminder: Action Due Soon

+

Action {action.title} is due in {int(hours_until_due)} hours.

+

Please take action before the deadline.

+
+
+""", related_object=action, metadata={'action_id': str(action.id)} ) @@ -179,11 +189,24 @@ def escalate_action(action_id): # Send notification if new_assignee and new_assignee.email: - from apps.notifications.services import send_email + from apps.notifications.services import send_email, get_email_header_html send_email( email=new_assignee.email, subject=f"Escalated Action Assigned: {action.title}", message=f"An overdue action has been escalated to you: {action.title}", + html_message=f""" +
+ {get_email_header_html()} +
+

Escalated Action Assigned

+

An overdue action has been escalated to you:

+
+

{action.title}

+
+

Please take action at your earliest convenience.

+
+
+""", related_object=action ) diff --git a/apps/px_sources/ui_views.py b/apps/px_sources/ui_views.py index d2f7183..50d5b98 100644 --- a/apps/px_sources/ui_views.py +++ b/apps/px_sources/ui_views.py @@ -865,9 +865,9 @@ def source_user_create_complaint(request): form.data["hospital"] = str(source_user.hospital.id) if not form.data.get("location"): - from apps.organizations.models import Location + from apps.organizations.models import LegacyLocation - first_location = Location.objects.first() + first_location = LegacyLocation.objects.first() if first_location: form.data = form.data.copy() form.data["location"] = str(first_location.id) @@ -887,10 +887,7 @@ def source_user_create_complaint(request): # Map complaint_details to description (form field vs model field) complaint.description = form.cleaned_data.get("complaint_details", "") - # Generate reference number - today = datetime.now().strftime("%Y%m%d") - random_suffix = str(uuid.uuid4().int)[:6] - complaint.reference_number = f"CMP-{today}-{random_suffix}" + # Reference number generated by Complaint.save() (unified CMP-YYYYMM-HOSP-NNNN) # Set created by complaint.created_by = request.user @@ -952,9 +949,9 @@ def source_user_create_complaint(request): # Pre-populate location (get first location from user's hospital if available) if source_user.hospital: - from apps.organizations.models import Location + from apps.organizations.models import LegacyLocation - first_location = Location.objects.first() + first_location = LegacyLocation.objects.first() if first_location: form.initial["location"] = first_location.id @@ -1039,22 +1036,6 @@ def source_user_create_inquiry(request): if request.method == "POST": form = InquiryForm(request.POST, request=request, initial=initial) - from apps.organizations.models import Location, MainSection, SubSection - form.fields["location"].queryset = Location.active_locations() - - location_id = form.data.get("location") - if location_id: - available_sections = ( - SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() - ) - form.fields["main_section"].queryset = MainSection.objects.filter(id__in=available_sections).order_by("name_en") - - section_id = form.data.get("main_section") - if section_id: - form.fields["subsection"].queryset = SubSection.objects.filter( - location_id=location_id, main_section_id=section_id - ).order_by("name_en") - if form.is_valid(): try: inquiry = form.save(commit=False) @@ -1111,8 +1092,8 @@ def source_user_create_inquiry(request): form.fields[field_name].widget.attrs["class"] = TAILWIND_SELECT if "location" in form.fields: - from apps.organizations.models import Location - form.fields["location"].queryset = Location.active_locations() + from apps.organizations.models import LegacyLocation + form.fields["location"].queryset = LegacyLocation.active_locations() context = { "form": form, @@ -1392,8 +1373,8 @@ def source_user_create_observation(request): if source_user.hospital: form.initial["hospital"] = source_user.hospital.id - from apps.organizations.models import Location - first_location = Location.objects.first() + from apps.organizations.models import LegacyLocation + first_location = LegacyLocation.objects.first() if first_location: form.initial["location"] = first_location.id @@ -1477,8 +1458,8 @@ def source_user_create_suggestion(request): if source_user.hospital: form.initial["hospital"] = source_user.hospital.id - from apps.organizations.models import Location - first_location = Location.objects.first() + from apps.organizations.models import LegacyLocation + first_location = LegacyLocation.objects.first() if first_location: form.initial["location"] = first_location.id @@ -1522,18 +1503,33 @@ def source_user_create_communication_request(request): for staff_user in px_employee: try: + from django.template.loader import render_to_string + + email_context = { + "patient_name": cr.patient_name, + "reason": cr.get_reason_display(), + "message": cr.message, + "source_user_name": request.user.get_full_name(), + "request_id": str(cr.id), + "detail_url": detail_url, + } + html_message = render_to_string("emails/communication_request_notification.html", email_context) + message = ( + f"A source user has submitted a communication request.\n\n" + f"Reason: {cr.get_reason_display()}\n" + f"Patient: {cr.patient_name}\n" + f"From: {request.user.get_full_name()}\n" + f"Message: {cr.message}\n\n" + f"Request ID: {cr.id}\n" + f"View and respond: {detail_url}\n" + ) NotificationService.send_email( - to_email=staff_user.email, + email=staff_user.email, subject=f"New Communication Request - {cr.get_reason_display()}", - template_name="emails/communication_request_notification", - context={ - "patient_name": cr.patient_name, - "reason": cr.get_reason_display(), - "message": cr.message, - "source_user_name": request.user.get_full_name(), - "request_id": str(cr.id), - "detail_url": detail_url, - }, + message=message, + html_message=html_message, + related_object=cr, + metadata={"notification_type": "communication_request"}, ) except Exception: pass diff --git a/apps/rca/views.py b/apps/rca/views.py index 1c42576..449bcb4 100644 --- a/apps/rca/views.py +++ b/apps/rca/views.py @@ -21,19 +21,78 @@ from django.views.generic import ( ) +def _same_hospital(user, rca, request=None): + if user.is_px_admin(): + tenant = getattr(request, "tenant_hospital", None) if request else None + if tenant: + return rca.hospital_id == tenant.id + return True + return user.hospital_id and rca.hospital_id == user.hospital_id + + +def _dept_match(user, rca): + if not rca.department_id: + return False + if user.is_department_manager() and user.department_id == rca.department_id: + return True + if user.is_champion() and user.department_id == rca.department_id: + return True + if user.is_director() and user.get_directed_departments().filter(id=rca.department_id).exists(): + return True + return False + + def _check_rca_access(request, rca): user = request.user if user.is_superuser: return - if user.is_px_admin(): - tenant = getattr(request, "tenant_hospital", None) - if tenant and rca.hospital_id == tenant.id: - return - elif user.hospital and rca.hospital_id == user.hospital.id: + if user.is_px_admin() and _same_hospital(user, rca, request): + return + if user.is_hospital_admin() and _same_hospital(user, rca): + return + if user.is_px_management() and _same_hospital(user, rca): + return + if user.is_px_employee() and _same_hospital(user, rca): + return + if _dept_match(user, rca): + return + if rca.assigned_to == user: return raise PermissionDenied("You don't have access to this RCA.") +def _check_rca_admin(request, rca): + user = request.user + if user.is_superuser: + return + if user.is_px_admin() and _same_hospital(user, rca, request): + return + if user.is_hospital_admin() and _same_hospital(user, rca): + return + raise PermissionDenied("Only administrators can perform this action.") + + +def _check_rca_create(request): + user = request.user + if user.is_superuser: + return + if user.is_px_admin(): + return + if user.is_hospital_admin(): + return + if user.is_px_management(): + return + if user.is_px_employee(): + return + if user.is_department_manager(): + return + if user.is_champion(): + return + if user.is_director(): + return + raise PermissionDenied("You don't have permission to create RCAs.") + + from .forms import ( RCAAttachmentForm, RCAClosureForm, @@ -93,14 +152,33 @@ class RCAListView(LoginRequiredMixin, ListView): if date_to: queryset = queryset.filter(created_at__lte=date_to) - # Filter by user's hospital (if not admin) + # Filter by user's hospital and role user = self.request.user if user.is_px_admin(): tenant = getattr(self.request, "tenant_hospital", None) if tenant: queryset = queryset.filter(hospital=tenant) - elif user.hospital: - queryset = queryset.filter(hospital=user.hospital) + elif user.is_hospital_admin() or user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) + else: + queryset = queryset.none() + elif user.is_department_manager() or user.is_champion(): + if user.hospital and user.department: + queryset = queryset.filter(hospital=user.hospital, department=user.department) + else: + queryset = queryset.none() + elif user.is_director(): + if user.hospital: + directed_depts = user.get_directed_departments() + queryset = queryset.filter(hospital=user.hospital, department__in=directed_depts) + else: + queryset = queryset.none() + elif user.is_executive(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) + else: + queryset = queryset.none() else: queryset = queryset.none() @@ -149,8 +227,17 @@ class RCADetailView(LoginRequiredMixin, DetailView): tenant = getattr(self.request, "tenant_hospital", None) if tenant: return queryset.filter(hospital=tenant) - elif user.hospital: - return queryset.filter(hospital=user.hospital) + return queryset + elif user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() or user.is_executive(): + if user.hospital: + return queryset.filter(hospital=user.hospital) + elif user.is_department_manager() or user.is_champion(): + if user.hospital and user.department: + return queryset.filter(hospital=user.hospital, department=user.department) + elif user.is_director(): + if user.hospital: + directed_depts = user.get_directed_departments() + return queryset.filter(hospital=user.hospital, department__in=directed_depts) return queryset.none() def get_context_data(self, **kwargs): @@ -161,6 +248,26 @@ class RCADetailView(LoginRequiredMixin, DetailView): completed_actions = self.object.corrective_actions.filter(status=RCAActionStatus.COMPLETED).count() context["progress_percentage"] = (completed_actions / total_actions * 100) if total_actions > 0 else 0 + rca = self.object + user = self.request.user + try: + _check_rca_access(self.request, rca) + context["can_edit"] = True + except PermissionDenied: + context["can_edit"] = False + try: + _check_rca_admin(self.request, rca) + context["can_approve"] = True + context["can_delete"] = True + except PermissionDenied: + context["can_approve"] = False + context["can_delete"] = False + context["can_create"] = True + try: + _check_rca_create(self.request) + except PermissionDenied: + context["can_create"] = False + if self.object.content_type and self.object.object_id: try: model_class = self.object.content_type.model_class() @@ -181,6 +288,10 @@ class RCACreateView(LoginRequiredMixin, CreateView): template_name = "rca/rca_form.html" success_url = reverse_lazy("rca:rca_list") + def dispatch(self, request, *args, **kwargs): + _check_rca_create(request) + return super().dispatch(request, *args, **kwargs) + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["user"] = self.request.user @@ -294,7 +405,7 @@ class RCADeleteView(LoginRequiredMixin, View): def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) - _check_rca_access(request, rca) + _check_rca_admin(request, rca) rca.soft_delete(user=request.user) messages.success(request, "Root Cause Analysis deleted successfully!") return redirect("rca:rca_list") @@ -340,7 +451,7 @@ class RCAApprovalView(LoginRequiredMixin, View): def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False, status=RCAStatus.REVIEW) - _check_rca_access(request, rca) + _check_rca_admin(request, rca) form = RCAApprovalForm(request.POST) if form.is_valid(): @@ -372,7 +483,7 @@ class RCAClosureView(LoginRequiredMixin, View): rca = get_object_or_404( RootCauseAnalysis, pk=pk, is_deleted=False, status__in=[RCAStatus.APPROVED, RCAStatus.IN_PROGRESS] ) - _check_rca_access(request, rca) + _check_rca_admin(request, rca) form = RCAClosureForm(request.POST) if form.is_valid(): diff --git a/apps/reports/models.py b/apps/reports/models.py index d057f19..023a16f 100644 --- a/apps/reports/models.py +++ b/apps/reports/models.py @@ -188,6 +188,11 @@ class ReportSchedule(UUIDModel, TimeStampedModel): def __str__(self): return f"{self.report.name} - {self.get_frequency_display()}" + def save(self, *args, **kwargs): + if self.is_active and not self.next_run_at: + self.next_run_at = self.calculate_next_run() + super().save(*args, **kwargs) + def calculate_next_run(self): """Calculate the next run time based on frequency.""" from datetime import datetime, timedelta diff --git a/apps/reports/tasks.py b/apps/reports/tasks.py index 0463ae1..4356262 100644 --- a/apps/reports/tasks.py +++ b/apps/reports/tasks.py @@ -11,6 +11,12 @@ def process_scheduled_reports(): from apps.reports.models import ReportSchedule now = timezone.now() + + for schedule in ReportSchedule.objects.filter(is_active=True, next_run_at__isnull=True): + schedule.next_run_at = schedule.calculate_next_run() + if schedule.next_run_at: + schedule.save(update_fields=["next_run_at"]) + schedules = ReportSchedule.objects.filter( is_active=True, next_run_at__lte=now, @@ -29,7 +35,7 @@ def process_scheduled_reports(): def generate_and_deliver_report(schedule_id): from apps.reports.models import ReportSchedule, GeneratedReport from apps.reports.services import ReportBuilderService - from apps.notifications.services import NotificationService + from apps.notifications.services import NotificationService, get_email_header_html try: schedule = ReportSchedule.objects.select_related("report").get(id=schedule_id) @@ -102,6 +108,20 @@ def generate_and_deliver_report(schedule_id): message=message, related_object=generated, notification_type="report", + html_message=f""" +
+ {get_email_header_html()} +
+

Scheduled Report: {report.name}

+ + + + +
Data Source:{report.get_data_source_display()}
Rows:{row_count}
Generated:{timezone.now().strftime('%Y-%m-%d %H:%M')}
+

View the full report in the PX360 dashboard.

+
+
+""", ) except Exception as email_err: logger.error(f"Failed to send report email to {recipient}: {email_err}") diff --git a/apps/standards/views.py b/apps/standards/views.py index 4a2ce4d..3d102d5 100644 --- a/apps/standards/views.py +++ b/apps/standards/views.py @@ -84,7 +84,7 @@ class StandardViewSet(viewsets.ModelViewSet): if user.is_px_admin(): return queryset if user.hospital: - return queryset.filter(department__hospital=user.hospital) + return queryset.filter(departments__hospital=user.hospital) return queryset.none() diff --git a/config/urls.py b/config/urls.py index d73fcbc..e413347 100644 --- a/config/urls.py +++ b/config/urls.py @@ -56,6 +56,8 @@ urlpatterns = [ path("api/simulator/", include("apps.simulator.urls", namespace="api_simulator")), path("api/patients/search/", api_patient_search, name="api_patient_search"), path("api/staffs/search/", api_staff_search, name="api_staff_search"), + # External API (X-API-Key authenticated) + path("api/v1/external/", include("apps.integrations.urls_external")), # OpenAPI/Swagger documentation path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), diff --git a/data/Final List of Departments - 4th Version.xlsx b/data/Final List of Departments - 4th Version.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6ac20b84b9be65106b1c10ba5a9cec2781020bf0 GIT binary patch literal 61161 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� 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%0AlBc1bMLUI+GaEU>h0tGc3q!tpOSG? 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

Cg%lR#D*J^=PPRfWgZPKPj>2# zhyqtJ*~_DN2!%CrwHxe0t;M>(PnQp}oCQ}G9H=;NU``i5gX~`KS~=>CkyjRFW7X@L z=SK1_Z_J`P_19P#v>~Qo5P~R;Z6*NfgqiF|%H4asdbK!bo^zY#dUN0~JxT8`9^8LQ z`cqy@PLyb%tC0+XdXBmq=sXLy>O>dGH|XY&@JM2Q5A|AE7vSh^Wi|{7Eg>M~(%#D5 zaASW~EB&mPYHv6KkK{|rV=~m15A8N@mH>ebq;Vu2s4bOFp5!SOa^`?`Oa=T+iRh&_ z&ymB^w?B=(g%rMXUgC~q0Ps5n*kV>68F%EvuriYf2d|df_pKo1d=B4P4S6xwWUi*< ztl5%~sqJ2@|3_l`#_oL}RrW5vEd3OflY;Buj)z8$kNIE}43ouXQN)Z)0f+J|F5kAV zi&Gh#*k!X=98?zExO|B|ti6rCeQ~B0mJ_jub z6_x)dKj>iya@@~XRq)s>5za;ixC5cg*m~k<_Q>O-FRB9$;z)ju+FL4}`1=3SG{i`D z+-)<48iV8#d%Tz+1e8U4)?dXOJq-=%3(|}O^ciyqJp-`NG%>r%pFeRCC~QU`jz-FG zJ&ym29A(iLRU6cL9Hv43r+ZKs z_y|m%WwKM`?3!O^$U(BD0Q$e2H+#OMo^f9aUO;LrT7N0i&I3u&NSTt?5@mlw7|m{O zc?(i3Nr0h&^B}Eia00SI;1RgDkL*TQM?25n=vX|}GsL&LA7q*nL!t1_)e}8`L&eF? zNZjnjYfVf=FzFuH>+?+*aJIJ)XFH3EB(x>z9V-j42~N=JoqZeN)1snWKQ#e(m$E*oW3sc* zwMIG>0;PEZmd1$DpnIeIN|q<@t$JU#)K9LYa6NDG; zp`HU*)8>8O`&O|7)RwUWVbV?5%<_t02p_Bbp4>+?7_n_645{M}E{a`7nw-|5%Rp>T zcp>Mp07Qe(v0%2;Q$K&p9}>&-o~5rjp>-~yNUgk{Sd|@vsOzS4uh#3r3ky(HZ1uuL zH@~Ri6o|l1VT1;onX$Z1qw@b2#u2QniJi%T zkSAsH#ufBI_9uQL#qFhwOoFtW%b@8tb8p8dwP0JZ(kOr?jH-9R)m2(s=B7q)26caN z1`9gKCTK?i=Mn>shk4nJ7Dl52Id{>q%#`gP`Y*$-+Mp~?Nx(fTC#}MSdG=v{LAGk= z-Fme*F$K<#zx0j{%Q{obxCANsk^IGJ;7?QU@y^@Vc1g+71>PhZs*OeLv|3br{?Tbg zmudb%0mOueu)uAsz4=tf4nZcgbEU8ih-NIb?%Fw>d&7134w%9pCgEB3Ne=;_QJ>O4 z%)bNFMaVH$=MBDtc#g!vMmaI7fN-NdcYRAitn3aSnR**4yt$R6K=yCJpKG>AoF(~GKdArbmkbeFpNNj!deY5-X2deGKvZrlCl(O z4wZIKZh_2BEAiyf%YW-gC(ntte@R9vBsnr9Y@48CfUL_+#%_QrFW{O3LZ991Pm2HFHml6E)Uxv=*HM&v9xW?@LJ3QeTh>n{i<7q% z?ChBg&xZq(O^8|FeCy$pgzCdeizP(@2{Zz^skjBuyPqybMchv^tq+! z6T62#^ONRiRQ-v=%KmtB8Jtw#p3M`%_zBm_)L*V|Va7`4u%iE_2wBy5=O> z-Ryw=(#x!08ZlWlqYe)+B!?5PoH$s+giba}n8PbYOT9VV~?94q(<^-_6!-Y}+MS6XE_nmOt9@y{uxDRD>dxzIWd4fm^WiSDU4d{O_kY|d{~ z#`t-dW}v8pV!&_r?aQ9k#lp@)6Z$3RfXJZFPBibGg6+B~gQknv`8h8ZdX`B#y-Xtg zRjgXzT>xzxlqWxpho zZ-A{1VV?#{!JX&9QA0+rIJv#>8-DOS&*8Yo7PnvG#QDv4e!umo{VHTG^Woj5HNrkE4fuyg)+6Jx> zoYN;=26LpXqF%m!_)W^j?HOC07^$&5>UMR^!d5Ii>BaI5S(6s-1z9=_6tDo4*sP|tN;zk66r}=Gbj`R;Sh_k+I-1cRc z?Osf)yE1RgB_O8RRnudcvkDizrP7aZJ&&OpWd`t1$ z87@r8vMO8fm1}U}_%X0A7%iH%%d_r^XE0k)iuc^kVmxe6CdNBaG%xZ!6V4E%)KD|V zj})wNLx!#BisFaNiHzcHRn?Xw_s^}f$8jW^XmNKL``$wh##Mu~T|&7XmF7oIlgzz) zu}N>pSpBwaO#3pN8P4gRgMyrgt@X(>suAI%L+a5^ugX*86XQ}t!bk7Tx^pFu%q7)L zD=s~V=d@W*Uq6w_;8v}{7Q^q%@6yMX#F&dMS+p@}{$Ok-P`{`u-X=R@yzH#$q3ct9>4O%*(qd;m-~>Hp!_P zrim&4JT1eDA3_8Vuf8vMu{fCzpI2eCPXxv}8VmQ)wi&;oiOQ!6JQkc*ef3HkUY+rI z=Afc82mGsj)A9WAzw2@fQtzy6)iqUz!#T126}tYNEgd)R=^)0Kmt8I{9A9OVO`-#= zqYfqIUPd8WsdL%jxk*>fVqPOcSC80Pq3FpiGi)f5piuFU7yp}YP_9hpHogK{^P?Vomx$f$VONDKgz?V?M_P93)9A6B$I|> zjb1zIt2cjI>K>)K9Xl$`%L$sH5H!PUG)O5Zvg)fO&4Wu4lp_`FJu`cXcjHBloqYeR)`tPa5`{`P&TJNndp2!-cAh zUP^@{rhISHL(vK@@!vh)P`l?&lZ7XxIB-z3>IXF*tRr}$fCt%~N@e||{wpk|eD_OI>b*%{@~;XWD4k zyvVFR?mg4Wyv~{HSXg{zp^E0qEt$`kW&|h8?5ARkeed(PR$ZBfkMr>?QN2x6m3TUr z_oyl4X3oLkUs(FJxgfi6*j=bZOcI_-zhmlcRy1ySmhUm=hy#j3tehh zXG8JL$PMjriFY(x(%y76^+b-vGUAp3^n;dSL)Npfa;*t-mH#wWMf03dEmm;h2v&67 z*OHq}qa_$u$f0f{;u1b5dzIf^D6hHdeE6v8l*hnU zzS5t~KYI(Ys}{#+Sg4|T2x}dC()06f@to9t)K1Jrc4kMr1F!e$m4#W+CI~oB(4}4y z`oc|MaUbeNzg(|QIOHMN@L-~BP--~(l8Z(9c4nZ_MXuoSI8%q9xNbaMTKeKV`0{d8x7KD|5C}{$o^< zX*IE~@HKuDNuH;_ev@rs6j9k}jSf$z^~M)o1V^o&Fcp(BR?6{8%zKmGbW~!D=bI~vaYKS2 zZG{Gz9G|AT?H@Z5tcdoHLf$*Ls_E<1!Zv<_x^3yz^(ZIWWb#wDWyfgGYi3VhydzJ2 zP2jA_J>qgclY8@S{;?dvyNW18mbw*VTnRV0dlqE#dUwnZzv*>CQMN=#Ft@&Q@NHm* zY{m;7{eX?C*`P1woQx{|2S)~P2UiyhUodO>EK(-=$8iH225Mg)16aHp}EvL<&|+!>UW z8mhCjkIe?hKFzXGchEMA%La^v@vXa5fpGW9Jhf!Pe>oHEBPcl8Ceea}(vPmA%aXa}vs07%LQWb)Xb`e)6 zx?QVN^nyqq7uaQM#4fv+Z5I|-KYTjvQhN~Z`oghjOqFy*;9y%vS7Hy?*=T)>MXXtj zQ^o3ymxmW*6@m_1kE%1V-ED4puy~o@#Q?)-J=(P5Yi_wC|4&tq_|8ZMw`|E(28@bg zfHaDn&WYnnRr8xVdvwwhlUqg=s34W0-0jT4mdNLec=b^#&;*Icf_En`&@{Y%Ge*Uq zX5wEmuyXx3_Ry*Ht<;%E96vW@Y1cnEC$e3xWEHoPwv2eZc&h))*`Si1KXbvJ951@k z;?H=Qp$eYUQ{AqfSUrxR-8t;fh1`JM#ue{g#Fp$GKgnzO&a6e@Aep{xiWZv2}O6@jb(FSOiUHEs?8j{oAI@PAl>Bjxq9gLP#ee_ zdK}^h*bKU~noYAqh){3lnKvcG&?&XF<{j^j^ZDRJ`_fHZTOP&B8TQ?`*m#}jMdO#c zt@WpzFRA||-U$`MFyswgFfLK5?PiCV)swbQQ7}KnmDn!3WPUdvWO0eg zmvoqEW3UgNe&rH(5)WUH)`rr5|93B_vSBOM6E)i?%f;4z#h1yi9zOd@pnUqygQ2;z zu2jVjg;&jWt-()u;H+BQQ5e_bWRZUM*qxaTe&k}pZ^%zlM6?+HS+0|x(5H^9J#C&QU$boO+m=;f^Yd z(f$^cQ$tDl9B31m!J>mFANea-NAR5~V$XjFwpomq%Y&ozZZOEb8JefOeb&4nvc0n%XKr$wf5X=u8wX`jQeE$`=R>ir={c7 z^i9XB?TO)s&23a{eeK(;G1j$P@ra&`KXZZUOSSx8J*X}h!@OLD9&quQ2SxTjFRtti z2Ae6;27W31YOm8-jO`up212J5ovkBWU+D9fnF(AFcVki-YoosXz`7R9>BORHvG)vI z2E%b;xYE-!YPL}#LovOC=~5YXy*B5XEPVP0B-E8`QZFncnk!2 za{L6jeebUj6{bnlKo?`2Xe=<;p0jPTnK$s~QhJ>3rB3?#A6gA-;_WRK`pKdP69&Ddac zNzebfwcGuO!RJ8S?^;?3Tr3a4SlKlCRUeS+u`lFKQ#i>+T?Dwnj7?&DSWSLwV=iNu zZk1%fGK!y>Kg`@?=mL$xL*zG8IQ4KRuI>EzctAvz(U5=F)pSUtn_I3vWLEEbZ!j#s z(q`s%xmaY?>`==G#~8iR?ulQHN#Bjv)Th@tVe|a@dA@J3g)V%7L6mg}{%&qnTUjm{IdP=F{UN_g$>rLgOUi6Fv+$r%};c+0R z^GuUEpG6 zUF$$e=LIWR1!+0g(#v0X`rWgTT<9J-Z4$=~cYzZ!mbfJB@DX_7dM&Ps-e|q-g5A}JT)J3)=Ix^Fb}82b z1)3e^$xlmrITO-+YMfqp$$F~WJzH-bWtebZEVIK`o^BalKiTmmAi{rLwl|#q#LUUo z6Q(Gs`KTd4)VzkMw&B#lrt~vBlXh=xbY~s{?cqp(V@Z8>v&O2U=Qehd=l59Ox-{FV zsTv(MpE@hZUIe9|Ix&f6)NQJwfBe9c;|brCR^Y{uaAx&~0VB#^0%Et}gc|Kdl1?Tj zhQiMBa29mbuI5ZId*lcG_d&hzAXE_cNOD==qv0&Vm-eB6pLAj43<8Nso3ayLjEQa6 zTB%Uhx#8g0$T&u%(2p*qUpQx)o_^uypQ8gK$Pc^7WGAh8;m8%H{QZvz(QO*K7x5)P z&NBsmJ>T;9(B$r3zvV8yH^Zy%kT}0Ce2ea{ zdjQ1Tg0WAvA|b_A=-fv>_G3hz z{3`~2pT0Te1E{F6bM#dEY&5kaJz?($m zTAj}C{BVG?+u`hh|96QUnB=eBj$$E$6ikU5Cd+Zcn^}3KTT?0TLaDdP=EJ;00d6Wc zNGv+mr!SLF7T3+auT!lPvl!nQuu|^LTg_SOs~bpTCzl@#qD>o|_7wqibKUkJ4-2hP ztCj#ga^jWtZfZ!jTwP3m$j+6lT931X@3pbxR;2m}kzm?Emj$NYxxnu@T@4R+`zGn_ zH-A;O6XR?X#GIF~`n@g_zmp|cTc%R%vCW5Pm|u#2uuZ7fC)}y+T97Vn8@LkHX|jbZ zs_1FXl;}-zU(D~9sv8AV@!v{ks!CArwYV29;g8a+!;9>(c^_2>_^neI$UjQHSuAAQ zHIHzm!rwc%wX^!J_$01UYmSZ zZHTh2S~~~RCny0b@$P!aL$n%qkHGTs$C+Q7jrpOVm?(U8H?2!*{LG%?Xo)}LQ`0?_5X3ZD7i(XCVbd+O$Dsknxy z(qf-Dw}i$qij4CIMtry`-NPrYkQu7)}@Q87<`7 z@^n6k$Z9`m;?E}NvM?-%Nq*Sr2%*f3NR%69%f7m6wm6QwmiUKv30wDfIfYX7Wk^}} z3VCeFaBQj79=~n}?TEn7mPwO_e+alNh!raN?LtVYZ@=+7W`L}xun6u>pomzEFO}FD z0~VWEHb5z;Rd`GaPjVA0-h|f7J2TdmZ?akQ62A?C2A__x5B_n6g)zmQP$vEj*06mwz=z;Q0gj| zEXd2Xr!7aq+AT{5Cmuy8w^zVf>&C|`w%NHu5SGxG%V)weK?7c+aTPaz^Xy%(?@PhQ zu$;Mhj_I^_;-_;YdR_829|jBw(rS|kGv4aibO@MLzn|ws<#8wJ86%)~Tg!7an}evO07%_a7TYeHIhD!i157-?!Rx$RxPto9`Eg z6T5RPjV2pf9*zQ3zwp_C7(EvZjkoq#<^*S5-GiesnvwWRkFSKAhf3qD zm6Y)Iu({q{E?+Ym+MZceX`+4-WAl*ti#wkI~LDf^w^R+pVb~sQTB2x-loX!GGEY#G>@~*hEJ-oCK1lf)epYD z%RZOC?D#n)f@x?=pS^HgZ$i*TrW0iN1YYzB6|@1tAQ~MZVG-?}&tBH*-wUvgko<#C zW}LWm_Ber9JM?tCJaTC6eO%>b)#rtgaKe)Vlr56t(^v>jB8ZwnIled9{N|`5SY1iW z+tL!f^XfL?fejV#hxbDVcA7+>_?D~b;P;RoeaI*`;@z!f>gJrUf7zRk$UlMcv+5Qq zUi;88_!=A|$C%D!-c=xj_32XBWvZ=NH^Si(jxg5RaU%^)l%_nS{AGu#(K2ogB_3!W zE>WjGz0{FTdt&XDkeXE~m_`eYtVezw)EybG5lxWYpjV`L>~zF(8(vca+ngmG>s1{k$1KidiVY3VeeRcMe5xBtZQfhOY%~i$#MP>9VJ%J6^RvzGw1~5{ zaL!{c45tqEuMrtSB>nS;->SNJe{@@LqlR4^5FBu|_>~p&&^96L;%%yh9EMboc&s{G zrS3EEFfeyWC?XT)u61X_AhL1K^3iPiRjJBFnrU5gysaSWg+) zQkklN)QG@XGebi?!!VRMVMvIVDk#UQ>wC`=_q+qr9z#^M^4+A z%j8W%zc6U5cNY*z;s1R2L%tno%uNqS1L{FeQU>Tpukfw=RqWqCM6^@hSzC)nz0Ms* zPwd?3ffAsRl!m9{uj|d0GPDqfXl{*jmp4!Z(PqY>IR-93-3-7uM1mZ@;sKcA;LT8~z%OpK1}b&5 z>^)@brIejr7{t-i*Gbi7v=Odm5^Z`~iWmM!*n&?kK+SHNPM8(WBMxc|QuxEa;BaXG zlKE$YuvQbPh-Gna^9LHd^cVnM%JL83Z2&X;($NE$LCaM)9yua=8szdN%D$b|d+C%5 zyr$feESl|0VK`J?pN7>+$^siUbaI*s+PUA%FJs$PnzLUzKGe*zaqJXVw5>1IF^B&% z(|XxoWljpbf#`$i`?PfaQIC8~dzKvftl3M7`txUEE8td&Ts3zN1EhkYGI~r?Nn%i& z{Mi9QDcDPJTAiGStvwfEwE=iEt6&Fu(TCtUxVJZ3TEaSfO2+YxYl4V9GRXk)@COb& zBLns846sR$v%PiKJK&vGpB>Ab0q?yE^#FZiTcy(zFWmd%o6c;J{UymAMzhiqDZK4_ zoOaV7%4JWRe!B2^6xN_-*jNtHXBASU-KuG{cdiZ`%9q?3RqBA@t?UH~BAmVXO8C~a zVQgMD?b9oOgn=9%*9poTaen!SGqeKEupR*?BTVXw67$bLZT3?pi&*eYhpr@0T4YEm zBbB`$tG$wSW}XGPbN$;4j1UGENThB7OsdzKza0-M9aEk9g>9iPp82j;d_aD2Ux!a7k;7yd z5>D?Uhb_P$xYT*HADNPT*t#KQ(jV4M$ItN|je8>G<*{!h+189@>!7(amklEOB!st4 zdtAEzNsM^>SLbJRU22YM;VOYM8dN5cS1CKm8o|0bMCq?88oqhk;lu&Z(o~Sx!`LYB zeFC^fTRJwk&tl3tNUy_o`p-ZBLg5%imR%Qtb23))t?l-X13_N7`{)-VTt#BTS=VavpokDE#H))(gh%{VUW~4s&j~_&% zuKOYbB%hxOw%!dqO-lSVemg(|zv|jzG?l<+gN67QckxZgP6lhcH5U8iLvxiHA#@9` zhYpuW8(p~~1zv7q=`MO6V`aZWpa;C_|MPKN1qgTadg2I^td*9?eBM{X1VJY|z|~ul zw>E>?pv6JOPIw=&Gjj;-W`w4XsWovoqA;dzIr_DPX5R4}2K-VlU5~7neEgXikQo^} z`Hh{!9_Mg=O6&PL!loKq_Ex$?QP;=F8rdmky0E6rM3=;d0#81*jzRKN5-VUbPIl7? zKj58*!%k}mTNwM82Ecvf@tKZ+uOL)uiO~Erdhg&1n5B6b(rZNC8#0#t;Svru_4^OE zgzGpLi(gk@aZCky<>zkFa6*{ayU6;FocJE`t8X^Fq^u5RdOUlO%85HDMn zA|9X}NKp3k*@v`ATDY9}qOva~mjQ4$>w<_9(RUx?xLH860cofVsTi!5Tsa!93f5yv zT$N5AYlwJ-vxDiy?SUlA4Zb3^1wRYQn4pf70!zNNVewvcV>fnuSRs%=-2$XDqTY>~ zq~T`3PDhu%MPrZ>#*Yq=J6PkUJ1-3@u)Hr1G8it2Gmm@m1FN4I$P1@Diic`r6fGGru8|ed*=?4=Q*Z!Fqx{Pt|=y*il(=EyANO{Hf2BJRwZQOw>LYkW6{QO@` z_l|a$EB^J)s|uJcC%Qx)AXg7xJz()GjKIF%8nhWer=BH1yc)xj5$zBCB>zknEY&K3 z0gO7t0$Wb_Ms5S@fN8tW~=^3tfjKmmALt32816mcL#Fv!X*+(@mm+LQZN#%Qo!l&2YLVcEMPsnPWhvi9X<~$s|E6^RqX)0BP5iMvS)Ji2loQ5 z#8;XHP}cv>wax?$ldVLqPH}A;QHBh^bgd8^J!aI2hvaq~)62;9TFvpP=YI*bsPs13`1dX$Afrj4M)!Qbd`fn&@1<(bN zhe+QeMhK@`s>A00kZ5ah`@RoFieMx|_(y5~SI7cJyqse2ot`o1h|#0(ZDiKcD7!sH z_{`l=ohX_R3L;MIy+qjoXP7RLV^-pLLAMa-!R5bPAwTTJT1guz>QAOgubs*NO&BuG zx{$sSytO$*vKMgq{8xWEI1T4k3J!3?$V4KaD6$s4J5cN?uDgfFP!x z>H7iQOLq}l8pq`5)E%j4-ug8>s8KhNRk zV!|u>9d6O9{&ot5hzG=uP!eB(LxjKu3d2An)nXS(4Q^;#OE_mF8gnJ=QsMj}w*4)2 zQdI)UG$R_BGrUXf|HYxyZ`+U*%I_2Q7dG=t=I2ecY>{!vtl!E^zd%A}1xiMzq>EbV@S^os?@M8Hz!~f+Ew$Iean4}4i{W~~{SIrLWQG_)z=bES(>Ly6 zXZ|@4+m{mWSGuMTU|&Y6@wh%UY{4_tOmUW$f}SR2gs)VudQc(A#pklEr_YGHEsnEt zetS@ULB!O8j5G;qCwQIWBA|9+s*bwvIn4%_UwP!UNb6K|qa$>~pol%EiN^8hT3G4c zq3*S|#hu%O0AXLT#@xM6`nfkBxNTYi05^)*i4%s-VQ!1UkdS-vCkKv=UA_DiA$@}K z54cI%cc_(gXnf%mMA7Hvk+GH?>@mL#KEld3WIgc{>8`kz#DO3W_yC?Si0-JPn)dxD zd%gb`J^;`%0tU6UK(k$=X$p~A;iyInuqKeMxXDy+0C z7NR%S%P4vSD1Yv)vOE*Q@nL*=wt0>!BP`eZ7WA?s5O=;VHR(y2JjhBgXdJ7AECH!U zWk@D~czYQ`3O~)uBmp1=dP6=mgJAmYpi6R*q=S8Rk{dke+aI5Z{_?5wb^0hm@&%mu z)b*^aj+MUVYvDwD!%tbD6Ro9ME%7k*fSs$QjyLSsGN1VBBj)peFB!)Z)bQW3@_fl8 z6`@^$RejhAzkZ0KDCw55$`g&Ko?~_@erpmOEhDBR!U4JOAFARt=$Ay9n4ef!J8jfb zIA=$ia?kpmfUSdXHX7_(8Wr&QCCigxMWVDGqaz-%Yd&>1fSUI<1Y{jp*IN>3fQ8Ej zU?}5n4{f?yQwYipmsLTQvl<>1Oc>5tO{x;lt!acy!H=t_%|7Kj90Q)b!rpLY2d4!6 zP}u{ga%PZWWuo;`>s0;N}WhpDqrE)7h+zw%l@{K{_uy ze}*|3eTHh%3Vt61?M4pax^vn2@xurd^Ls1JYnSxslKX7g#tXwGK^J8JBwN&H@Sh%9 zS?*C=l3re@NnRNwy$666aPDlUJA+lr3L+sP-b+_6bTeEqnZDWYs4i@@ga2)(x=#ct zAke3T7|~Kjh$96AIWF|R*X~04gNuMBgp566+2FqsAG^M9Wce{>ZX{MOA9-GA^gN~wTg zPk(<8bq=A?&etiz!+@v!!1{55d}ke=jjf^#{sGz1J8c`2pSo>RyJ{`9S!RD<t|@CqBW)(efkYfFV>2y(c1kmdlA^}T4` zeIuS;+VfO%9L3;d8Kgn4qKK5a0ls{V06^BiKQ*=hp$UF|F+KbL=!xmjxTgm>r}Eeg zAz!XX1`vwU%M#YVDr!6)K-6I*^xB-NiXva+gE(u2%1l>=UA7g*{x|%@)N#L{4E$!aNY0SbHhPA(aMN+Et&K zC-OTNC@YJ%P7ZqN`T)*;nVOvPSfeNErS%m=QFN?teUV|x19~Qcx!Mhp4ks!Yq2H#h zuA}@SRUmlHL7Ars-j>y0k566vWYM?YtPC~=W9q(TWH22}fD zy&&+t!1vGR5GPJy>)ud5d1X5wDJ7&7zw$)VQ49o9Za@G$^XUwC*QiX<05_?i_9LpV zS(^iq-5Mm*FFxrp43~Uk2kp6+Byw~8W?lrKW0Asdb~h@KNvO!nT7XM9@HUzNVHmyM zrJPh0|0oGkrQGQ=?QRxdb?+Nh5BQ3;8AFbePLeh05*vMAr*F2|IwJ?R|01uK<#>u4 z!2Fg)U2Ri;Yi)YMQH2iR^Z}9weJ2qARX_izUf>Lz;b7>*@7pxGE zte&1LY>xncUqkAxaHZV95`DWSD#hkfd~V8_kkHO*4=DR$3XC4h)6)=uc5NKeRjHX}YXLU2X!`wtIwVyIueRjdewQ?2FH6<~=Bb z5GZxZ^7O2&G5_Cu4r&qNvl0jgn6KvY&m1FHj24|Rht+Ib_yR;#<}9A69ooU~s9whK z2QI;qzQfOyrH}v5QRo808uyM4W2`(R9GQDbkw~N6JU(F_Nfo|O_B1F4SGzDL5J=Oy zGTr|f?t~-UomKgr%qHzMJQWAAUr8d0{puBYxzbMo1NtLRiQm;&bWSrz^-Kf^r>pT> zrR#uclDIzudx@<>+~r@w!5Ece_D{@w;hke zh#zocwO&3K0TLD}nWqjBLb8Nvv9lK>9*oUA5F(_Fvfc7WuJf$kCbBH2+-9zev>c@*nY&>T3$X* zK1i?AS*@bro#@;l|2E*&M_WEfsWa@lI-`op*h>2XQ|4dW#MYB!`zq%d;tCZR%`7)I znJwLyvUA_s;2vIFl+vD9$_(c3FS0wUOwT$2j5o=R)Gc}GkmpC|iVoJ!&k1}VrE%K$ z`mlJcm5q5o$nM?m0Z2R|IeocW_hJ>STK0zlH<>+D0w7i-6*6H#y_Y zXVTe==HJ(~#yzBTjh0ObBms-1gS$29VEx(e8b4xOCtgL?)XqDqvzQqCdK+NiVYIMo zhbOG4nz#Qx)`Bjh_Sjc1nKy2YE#m`f+q;Xjw{XNx<%P1-hCXt9Su!|Q?m0EhZHuh6 zU6BG3+u276?6s5~_*5E;YW_6f_`-T}qQGL?X<&lJPIS0gIKG^9Efw>r7`+sanY(Jc zQa${BboY%^+Dql)h=7X>y~>k1ik@n%r3%(LJq)R??t0=*8(K@}JDNIg%rVA|iANdu z7%gRQzCGl4-un9Y!B=*61)@>ac1^AgCoxg5kX_xbonHncU8iY?AKGDozPb;rn7<5!O zG~0Eh88m$#NzTeaUsYXR0)Hd5Zb}VnWhvRzsjfxzwcTsC;pYAeTJY@zS9fPiY}n_t zfy7$$Pax|TPW}6&e*(_=s=VubRUzSdzMFw_Cj6VmTya4&gAlcRWisVl78m~7$z_jnNa4hMcuyekN?>?`qE)nqc->7@x>euXsbufc!nazVo zUROw@4_sC63-Jkuqddo4t-mNvjp&!*QP$oz_xs|k;kTZvWe{o#W{T}Zf_R3V_Jsj~ zv1#ePwO7~mPFwNw75XWCv{1fNexq7+`|kUb?)lPo)J@`30dLQLbL;t_x@DmlPZch1K9>8DP;Hri2neuZ&R8PmZ^T|5R*Xo$@f!g$A9f}ZsOj==E8AwE=PXd z;6CTN^Fvx}+8GOHrM4`bEePagp=!&_%@0V8O@+mZV0oK{f$vw=(kbtpf70?v@&zjn@urDNxN9sx-g%bDJdd(VDo* zbP?X_v8}P%Pqlc)hf7+w5KuE$tudR8aMOJ4uj& zTWxN|4_5v&aW+(WL8GH64=C0$K0wDm#C>zmSgh%36I)>ey+dyoQ#E}e->+2-t7~?j z6M~p*@z`+5lxZC2?@#()gQ>iXaqcQxygwCphAyIVf^D1L78j)B1~JJ$f8vC4y_Z~k z&W)NY=(>1dB)|0;msM$t;Lc6kw7De+pCBqiT*LAOX$K(de!I_s&t4I?N1@5QV`xtG zW@)>m4tW9BbT5H-!s#2E3jut&r)mS+8#^G`03zQ}J-!A2%D;eG+>QsJ@L<77vTR>{ z-8aeC2)dWajJ?iwhXAg=aLHwrb`lLjL?FQUqhk(G(*~~@RXrx6md|H>+w+$QR>`Uk zC`5rYBTG0abmI-w>DtKPXj-ka5jSyuau(oY$QD9-XKwb6Oy#;IXl2)#4VQ3p@rnl5 zBz^$m34*Ted(^tqv#&p^{!{nUHxs1vG!~|b!AWBBl4^9xVW}f#8EzIwZxf;q^)_Dy z=)R(0DlCG61m?bcAoFn!2B*9@~7T2$8I^loNy_C2xkRJBP&{ zz;Ck$N({fWcYh$l7jHwvXGDkjz@d}inT?K;jkz)(K+)wkJ%$BE7cCp0^~STSQ|01< zK-$JglnYfh+2KaqPi*<0M@~+?5W(i7y3Ts@YLRdW_rW^07!~Z9u9n*E)qYm`nk{%2 z4Q#WpJayMt)n-p^NqR--a^2yo5=GVS7s7U^xDYC$_OC9D8Y;Ps>dXL42_6T5%T`uY z0tz%tyX59U<0F%%l-U07s8qKV&qz{`YEPnxpG27l7K|927}wRLWwrj#yjqn!0l?6b zi!-L@iaFd5m?pwX1esqb0qI;(1a}puu$c@`%{g0G7HFPYndr^C5sJ^~TVGEF=9|Kz zJfvWH5@R;r2$X}wJjWxuE|C3Um&E{KG3CtZ;E#Rpx_BBfxl^lE76<9CSRp0v6X$fS z+4ZHJ=CS%m+o3=RA&~n4{w|yssKs!-Deq0+5SwXa*?5rek}u)3Y2=ey+;BFXuC#i{ zQ`ty@#aZa$Wc!i6fsw>~3>+w7tIu#cI4XHw)6hrTp)f?EwS=;;=wd{I$Uv7K+etPu zpBps47fo}|fRZhdU)ezVQ_S;i{1ZfET!hBkfIuS7|38yJx8ItZ@G3$E5Pe|-wO@AesEQejeViy?4T#R5=N<84EG@30YP;|hds z^D9Sv*Ye2vvFU~nQ%Qp_gopmEfKM7LdDv?FbJq2w9a9{#C*~ce&AQ^^I};&3bB2_@ zUQa~(M#w8iZCoU4X@wtFfza~&;M>nr1gbD~)~n`tBOU};yg&~GfkMRBh(FMG^hC9{ zk`ND!S16YHRkj&-t6(!ux8uk(Ih4K-6PsJm0oB2=$u#LzJJ$SzhzT`4P~?EnQuW2| zm#MV6o4$k{72^RwH3T5r#9FGtEqc28@6$XmKs7$Y=8n+qU>!^(NCbp1 zF5Vpd#$q;#&4)-{(iarREsA8m{KV>~2hyL%UfTOpLL8sZe~$pb^~{l@Yh>EtMHLkn za{w&_Nje(1>3U1W?dyMSv+(o#d5j!U^i?5_Z96LG&C8zF;D^_>6vV_%0w)IL)11Iz zM|Ik2;quRIk1}r()gx=%s7A{x45SygLy!2yu$wtYD7=6u8!esgA3VIC&be!&1t4lQ zqP6Mn7&Kuz(Uhh~4x^5|2UBKIG*5YIL1Bz>RtM6ZAS;%OE7=@i9X4C`D2iC7=$g-8 z{MOV`Q<{Wo$QNhn3O8E-!p;#~Un(D@W6_6N@yQe7kaq_jS9tu24GWr%L4`ozbPiua zye#ETB<y7IDtgdwSe$aTL<0mv1sC+6{Q|{pyCfOgEr04k|1VXm5N$YEp1$9YtR9d|BarX9LbD06 zQGxFkU5=3Rpl%>2+}8y+=}MnFVK(tTxGsc0cqClGp=m?wh*(I?v=zp|0c14l89A8; zy)X2HiYr&NSu!N3^qQ@Vezt04?<$Ew@%F+cF<@G$Mz#}IS*r0V&y_q*LE9uG*jjra zvLZ9$Al%)xdLSh&-x={54tGl^+@>cxV$w6s!u@1bs4>h8H%&~u5~ni-$+F6xYR8Y@ zSzzCmy`9yq>F9qFn9qOrB9!_FUGb6s8I^vcv(k*PeWOIsI#+t`-80(^EFm0lPzx<5 z@s+L`P*iXB93hy~S&s#k6R#yuuW{FVqef*`)?y+40gBYE#NL{_g+ooNPA2FiG ziR~xXur4|~Io+_w#rxMGk_g-cO1+zf*o80ru~N^dihw?^5}VRU_fqDlIDSUC5!Pet z_I=y@0TISFjnOwvhYTfugT^L}ZV3;W%nX353#HZYL%P!f9jc!>(@Kw83$h+sFpBArNDtP6JYEg2)VDDAx1?+Jhbo@eeyE zsxU*>%A3b@T>JH(dgX9&Varrj zyCcbs4J7{nD~Q4j6lF=~D$oNrr1!^E=@&=`@!L%Sy~Fq)=50N z;rQ-{ox)e6I-yC9LF2)}UrzpWop=I?RJ8VZD!*K23sz#@ZCDigJMfx3`)tLPU7O4n{4Gjw`6|y2#*{r8fPi9zkyoHB%L|9gSEssIoYX!6C1lM7W6J4#ku?!JCg)! z;rto*PDH0jNe3yag7kRk)rksR)ZaOb&A5Y9aekoFKlB6e^U@P)XF<3bBe4{?p6JoP zcW~yrj;>{2kxNDbL}1XQp#N1tE%(r>c>xA#%)tg%wc=ty(JiHmbe@H2ZQq(yj3 zofqXuui6|1unF!QyX(SIYz=1OoW@dALK3Y7HuodrCSbN+n2!KU3Ezbc`f#N^d1DW@ zL&ycU4VRlhu9P9@Q5bO1->m{z*>6*y_`7weZHWlO8yF^9ZGym7%!E&Iypy94A zStB_KRPJG5c@k)BxL7~U*CO_yp#TlznHd@43tFRV4Qe8VX#3STztl0P)G_gu_}lAr zrDkQjq&&l)&OE7r%Eb(C4A}L>^s8ph2F3hCq6x(I@>Ckfr#riE5|JXDPJ4U6S@ok=NOS^5m>jAP zatY%dTm#w7%JMIfI(L7A@%PjMt%}GUMl>wpuopyaWRR;EyAPCF!j5%}&Uu4x|L+re z$oil{7?%U8Uw+O$<1R3Ddt8KCMzB(+K})?amz~?au4T#qX^KHYA_RK6ftBlXHAtuS zLjJ|?f6u?DRI3yw4_!-j|I}71&&W%pzlB5;idGLlXDux2g)enQZiBqZjrQIsz?42Opu_ro5p}GH!bC4V!nk+u`N>#IpC3c3+a)oHy#$ z;t00^;yKzFN2}wpX0!(>c^5B+eV7GdhoG&yw7f;byX@JDa}A1H7K(E?yXTM9APTRZ zI?ouC%vP8U$q2xc+PS|m!2EYRjDV*I$X_yJz*i=+t{yyM8BwAQK5A=&LW`+o_1D6K zn}ao}tkTm+&yFsWEUAGWln!3=?cGL>?N>&7=p4IxegcesBDJcL>X!0V>E{~G4E<_) zTe9)XOiN=jH%q`-TfetnxR(&3Vc-{i7qyt6+)o?1cF{KM?1_zvE$;{Mm)QcGQ`z>r+E+&z3hEFGr`q|0Uyg+3t_yXRoQ*X zVMXUJxatO@u3eXXz5hiiw0zS1eOfZ2$G#U>YCbV}ka_UTY}u(O3(BUxy!O^QH^Asz z|98ofXez(;VB!HK4l3)DTrCcr5Wr>bcC`@MA7%;R^^24LIp9n5BAC%w8fK#N09xx? z3XH!ovCie6HZD6SK-T9;0jL7WL1v7Q$PhXinBy8zhcc4Lch^OrXy{1D%saGLDACN* z?arH)+rw*TvrACk5RlIglhYxD-&B`EM43Fpr7<-ozYO}eZ{{I7>Y-j zSi_BVjyX#0Qyz!6cte=-4vId|SSXaDgCeDtKy01MtVAgf(8TcNPnwq=Kp6q_MCs4o zeqv{-(h@e0?Fh6{+nQ`jdAkXr@+uK`iDf~n=>XJ`S>smE2t6>R9zt*2k~xT3xetxE zNDfODPYyR(3Lb9if+lMBjnzcrAq5j6t>hP6&9u~EnVlb)be>4gg$Z2Y{;2f6FGf$3 zVUn#*YG3+*)Eq}Gvyp9|e>`gj+miFjAtC5tQ;oe@;uZDp%V)W4(o={N+=X8fs)Hj+ zzZq1+XekNogaAQgB*qaG>9=iNi4_7wyv{@UG((Xd!5JE^F8t7MdZONW5P zT-&aR*s^IZXaEsR^WAEsnPg%^ndQ~3nEO((mtv=|x%a1^*h#mmDbFHW-n|!sxwy9^ zf~?ZhhJ{ed-X7Hqr*8W#ztoGznrj{x?Pi>{>pK3{T7B_Ma(d~<=w`9(*vXUmcU!`o ztQm7%x$n;Qo?>O1nAS4hzF~GZdynQzve_K%gl}JD{rK6tmMWUucRk>P)~yRsI8k%s z1QT{LJ3j6VhYNRRMC4ib?NFdy^QvN;ap!(~x!`6$78|+A6@@&89w9P79Tv><7dZfh zWk?)3T2E209y@#Pxvkv!FfZCu!x|`}ro2jgE%n%t`1<{5jOBd)VD>Q>VhYBpO2MTI%J#YF(k89fulbKIPdFB01h8$h1MT-;l4XZ8CAQ< zu+87VAd@dpEpn8N$0w3|a=O7TYK9;GR@D^$PA(&4SYF zdkM@FQtF!b{1|DEKJzjZXi2OjU+T`%8Xsh?Zub4G&HDg)5$sdvm@HSlzMdfmlBq3= zmAZqLzW#(se`LeC1*{tdRS85O`Lb)-6=MOLxsOYGD>v6i{H>pyL^;8 zaok+#R)NzP0;aF`c=r0rH6E`CNKP7CFey#H<1_pT3rcrihY*q5v&=@G)k<5ucRvUe zeu)!Oukwy?8`0<>c_0k%<-|fnv0vhKzhBG?B3Of|?OwtkxgO!za6Lye9^7@B1DV%s zp{)55&aD|gRhgT2<2Sdb%X$AA2!x_t7_57~n1TI%QZXUV>!n19y}mB>AuVw@=h^ln zw&Zq;zHc4*TTgGVcE;d4GcG%2?hbf#oH}ZEpcTJm`psUZTbebHR0KQ%aHbnR<w8`X zbxn0{`3`yqOzpxGR( zjMC3#BRQp>Mlho^HTQf;yxo1m27YL_D5e93{GTlN|A<;G`1{;OwgjT=S;}XJ+`p1x zgy5m%GpsXLxSQfFiXJ{xkrWk9l4VpO$C#n11DGl?9wfQXz_AwN8TOE-g+n5}s7GGB z1n4n|0AngnWGc`9>F5jLjG=3Ke){C5iupae7}2zf=+WOS=n=!9vx*K5C)EA7H|6Ts zO{J8rLau~cs&6m|3mWWzS}9NHxCX7(;##vp4tuN@+QS$QD;=XayR=%d^3%ExgF0Y>7PwMIJbJ}kk&E)a=giB!ww4Ab2FMR-&ZMYrh zWXB;G&<)A@UllS)1L$6x4?26rnaj!Wnr#0p0W$l$*e14N)Z-3>5x^+xW>(0$Us}i; z-V!A&4TX>pZ2~EaqRl0yTOwFB6z55~4Cr2FgQh6oyaCyNf{KHXWDiP8pc@&2-BwFj z*k*u!SarJId5fz~x7v4QKT#WsgPwZ=h>gG%iH893dhrtH?W)h?plz-GZEs6S&q#zd z{mgl@kjx*Oi z-hj%ht5O~i76R>BF2o?YWE~s6<4R_kotCMoYh4@u%#xv1x$b>Pp5&Uuc|pDXC`c{( zh47&Xp-}KQTZSg_m0+b7xAzIZbef(!6YQ#WYI^*q#fa}+4&91C?}`rxty(|ViJnRk z_NmQ1cTmsfvQRa4wyg0UhJo{eG_L@T5&3cw7t|iVcj*HT`sW;WDpB&&PjeA!<>gGo@w-aL)b*~FN_E2Bzv+??)dq18s`JgayJP67S>$)uG(P`5Qzi~ zmvqFaK05$hSo*(;d2GL(?I?-q`|v*k&{f6~FvkK7AKiX54g0tVGaMwAMNMusq1NB_MO9 zL0w8%e9ez&Q2@~T;K&-qxYJ$%uvZ5EFXSxcZtZk@Kq+es_gL+`5;Ew)b-?sFPZ>sHe4cB#9jm6c- zo9^~4tnb3#RG>1BXHSFP>!1(-WOT*F!#{{?1!O)KaI^9~zn3~R7v5iQ#tS?KABf1G zO)-5o{o|1KHW@XbZ9Evbs=A^VM2x{tAhL(fmsOI<2nROk9Lko^^vI9fRGPhY|C^oh zT__udN}OOe+P;eh(cH(i`s)slj5Q+B{Hi>Lj{Qxl5_ULCAfLBcUxb=0SE~!wY{WVv zobs0XJX_W%7iWAi2e&7Q#xf5fJ%T<_x#N(Q4668o>O;t#2<`QXLtqWNeCn~=tuZIY zK&ywtOY0{CMg!H@w_at+D#q7Zjb6SNsBEJQP5YoL@0dGej59_{(rf8N=jVF~Lx?Q< zF@~zWfRy+#7H%|(nq+~}iGSKDl2nuuL1!cW$F8UVd?8wG13sIc74&CI^m5v{cVg_q zjt4z)CWFgf$d=e+GMxfd^kV*w=KH{h{Rf`R>}zE1`ClT>b&%Zh>D>d92PH)=gaaG& zLO#GZW<5Q|q1vwy-O~@LNE!u87RzCERwg6D6-v^9H7hPftQoHp+j!)c*-(l|xJ4W) z?@-*HRPFv(rlB>L z>12+Q>(8Fkr%UJuZIu)u#75D})be>!X@VEZF`{R(osb`fGNC_(wW|$Q;vRWb{Hxq< zGW`ePhK8&cE$_MZe*%EW2VnKGkDzh?xS$YpmLk%>P@;5eE!9O$;Ghf;1a-^MI|TXb zH0nbdO1nUSH9MEnd;Mx195Rm@K$VV?<5)s0jAKK;U)(xMux8=jiaH@z=tw8gK(erw z@p^L(20J-QtYxjm=KELsOaRf=1VkJ4;6(tbAmeQQC6{2x{lAQYypMzHF?~Y&Z}t{E zM07wQX`()`Hzk|!Ta_-lQ{rjKsDCmIt_sHs2X8?-=|$&J=5hwtQuD!Q$4iPA7zCm3 zoelVC6_@DAN~7Z(P_H=yJZXfl=*2&M^H3z)IA#V?eI001C-8WvLi-bz{HAEU-47oK z%~l2Fe~6<4i4z-+i{Kb3ioaEG|6vRM1uGU>*s?;u+2WcWEZJ^whQ4Y?`h$05flWb` z^AK;Fg;KSWzYGs1peZxW9v``dN++}PvXgn}9eGe;@)vH>y*)0Eeo~X@XT|NMot8K( znqVV9>!}UaP>X>OMa}JvhVE4gHY_tf(=CBmXmF7;qIqTElc#P70+Ad48!byuw0}a2 z1fxADO(xw3sWRLkHL?4kW>7$Z#>$}bqv7IVF)B7dg zVnJMB@c39s@y{iIkKRH1zbQoGIOw@H*Edy27(ka$ppro7+I~d+w<@^Y?Uz}W#Tka~ z2nkXn4IVB6NUR;b#&pS4b=!PP3b;%xYb zo58VfhscaGF1w6*1Ji_e)BCyh8h+Ptcik&|+2b|mzqwa=`?J09e&rYF*hfB?_>=}} z;rX9CK6tK6RoHL!SM|+n}iBhUgIOc4A){6h7cwF3| z!@tOoF;86D%Q8u3MS5AqvClxHnce@1D{H{geD;U@>8ia>{Ze1WKE7jhbCE1nj95*ahy6R+-SA4&_@7i6uyfATvw0?W_2D^NA|GsPZCA(b= zjx9?%03M9-CTos08W7hH>~Q8!W>6&t}Y;DmzznX-6u8M{sV zYQG2Da$~>Zw!I}>+lsf{dcg`Wx8v4KpMmwOic>au47CD@Jh zH?{G}EFGgQ0xaFRWfpR|uU5YNOn_d$%qDHaFxd`zlw?37AV+>35v$74Slr5Iz3_OY z&eKf`72QkhEgp;$g-3h|oPD3i*+$0-rQd|;lN=L{b-cju?QMCQqufbior4sMaPvpY zz&@5q?~j3H4`}2XBhMa4~7gp#jy*RCy_lsoLw1My#ly2U?JKOL{ z!ZEbGGT&4Od)vstGsWY~yiksG?9gaWG$4Si6G-+zdy~}6Z{uorCoyql;pfcd21mv1 z*&LgXDb+}ipC|Erib6Y$eE`NrZY}8lOt)8r_0^B#SRM0P$VIH{S<=rHRdTXzl zzrHqYDbKsvh=~k?p_cA6Wk8KrRIm*b%QE!H62!R^rx#b@&&C88_akH|8$s()9gaaA zMxWvc2Y!y9Y-0zuw!bx7U)V_YP8;5?IHo|d26`VsiMO%)78UYm)M71m-$D2;1@VB@ z=vINWQ^BO_Ni<=@KW}~wJ0px+GcDdMfuKZW2DBGOVz?4$baU+0MYs<-s+p$W2(u08 zWC|fp2oEc$pI(ehtbw>R<`!p0^hnd8ocM`vM*vuPa;e4dc(9Q-A-V>t#LJw>S+boK z`h^KJ*^o}{?Esu|m%48!36nj4c~}XXxDPy*Hs-oq)_(dDmYhfl#*Sx0jDN8F0pu#H zjrLv<+ri5pwJ05dhl>P9UE>%1 zyx}^?u}c)F73s%@vT;Zz8*s#Je@(S5&6Gp}ylBfKoCfU8t0|+>a7x2hhMz26-RfM-}|1=wFm}LkIcGo;*{- zZ*~L$c95sda5YwHRfrw826{ud^-By5Zb^o6_haP;Ko@~J;D8{&Gq0JgxI(aZ$(Vdy z5hu|x+XlrPt6v?mI(1MqJY_+lT9*%mDvrjnpP*|5Zq_TtRu##}j!;B4MqY^1Fb&o= z{FKse3SkIn_#uG^D$O?UC=zDqeJJY=4Yx*PZ57&G(;xJ6G*iAF2no3R(XvK8b=b-& zugO}yK4_gETttu%O9}YZ2FG`X%CfW7eI?h<_S_0H=-qRy^gGc?TM-U{!nXNGbe76a zj%^&nP;0Q1KA9-uvcx4=-fnW=f_Y2V$nQ`K=33#Z5z6n<Ui#~sy`CQ$BmVlO|FmWQ0QjLC-A#EMH9Lq`H8nSKL$BN?W~0 zsPDD60cK4M2r^~9bis31R2Q6s+I*cot@yNN6?>eiqQtc{m@bahXxcDY;l!v~kb*Tz0M>8ejJG>rpQj8o#Y00 z^yGC)yRRH~ps$=M*pzGZ&v;5xNx=d5OVW+>V00g5h2DqcQt^`pi_k4YTy4ujEi}6+ zd_?%kse;unF=4Nx$r*~&^|ax~k!c&ZY;n#Xix|X+QicUkD3UozumT5MlF_V{zW}{_zfa&1?@jRZefqd3`jYwb5k>tcMv{?7$#3ds zmZPkgNWtbV1pjwa!PyIs76iWYOtFo~8Xn=9#)!HT%b&o(hWZZu%yL5g_;iD`%Yh>j zyj(B6&b$M0)T1i|b}LHP6So8fqy+wi;d(~P9%qv#tNrUSaw7a^C94|XcooDt4F527 z`oz##HfU+3Xwn0nnFM8;@O>GZ1%dT4>H@IVJQOXOLT4tn*~$bH?@Fys(kun*pX%B! z|7pHByFL82TY_6&w^|J02zS<|C9U$n&ZW=teIgf_M1p4DJzU>lIG4k7Bw*lk97F8S zvG5bmy0>xiOrbzq`e)vyD7ze7@3pEA?y0VjeCzsl`88XsE1JxT0bOyL!p@~JBphyS zm?TsB(f{}mXrE-_;eLu&V;ETKF6xP#mJNBG0bjDVoBKKF zH;=yffM!3b0(TkcXv3GGl=FyP5>jHMr5GL$2X2R(!B_6?-5r~ZD=0;Fg}V(iJDZ^g zz7q7oj)u9MV;0Ye95rQT=zY0695Yc4S1kT6x?7DeKs>)||<3K95gO|%gtIbnqv8~kYsQ>iI;2zMW~5Bd6mWC(92k_9PXHf>z`#A7eMmd$rC zS$y0d=Qf$Y1Aj{asLW$n{-u8%sNCRZ9M?PdU}5PTdEC@rMc>h^_F6h#-Xaf1x6GLK zX@xHkx3o+3m}~Bih!Kj8mA%-`SY6P&{mI5cE3^O_c?~@ly(hV$iu@^9kn|5YhJQbw z3va#eFA;pPb{ojrHNdzRb-=1~R|BzeFTwzu z__mZAK2>FnP@Dw<#-hy}hnOdhjNKzSIQz7Jm;+`jA=CI2s1Kd=xLm${iVrQEDhH+9SMRStFlW#n8C+(mavVJud@})#TNj6y_ti&i zOMMT-Fa;|^9t5tx+4u^X^^5{@5X?c=SdZWtwf(A3ty z^HEb|RFO8Vn&Vn;MMf(&!=wJcH>~G@E=kP=6VL9zy`fk$Z}ABA+e|V$l{&4wtLGIJ zJDCBw`*y0@9Ppgr^pSs!!zL*V))QBee|Yv7_Tky83qXIdvl@8!!+CeKGN|i1D9WZn zm+2FFyrG3Jpw@kLx(=-H(g9pn*sr_uxkh#Mx5`?x5KFb*@@L9jfDg z-1D#?{91RwtIF-^fj4hv{^75OCgG9q2M^erK;V^3iz$}GM}Ip)xN>y|4lwW#dErSg zX@r^)Sl0f%?FYH*&)T=npjtVy2Mjp_517(XmtTHpH>HH2SN7UB>GRfodYuFf<6bj5 zh>m~#irg{L1oZz#;DcGO5buFnZom21JnAg|w-)qwXO(HVOV;8vTpxgItuXd z8Vht0(hfLAkVIw8$>$z8`JmDCcUkrIvbSAG=FN7p!$TsG4kP}zXvCMitPE5_P524H zbwP6+OaJ&2Xlfc#9?i;Y%$@_@WkCIty~3*gvsoBhEEdinc` zf-deAQ!;ve)*h-)_#m*Du{Nq-844kRQhb8ZpNS+(nd=f+hiIK`%K3qELhcFfR|4{s z;lH>~@r0`kD6+J4o+g@jtJ>TF{hvw5u3ir$7@a9`YRl3G4Zno~TVxM|ssY8O-a>o| z)BM5nIE<`Ve%gGCnPOa)g!C4tsK5)0n;VO?^#d<9u5aXYX*@dxkwKbE>Zx@yI};ap z8_|m>QAj*xF`?(#*u?XvSLaJc z{&FwnDY&{7V3a4g#Rq>apG?Tfu;gN@do=o(G%Cov;hfcKczW6iU$ZER|Kr=^nm@*9 z@AIImG^DP7MCh_{R8R56!eQBIsG8jd+mnqxt}aHRmh{Kf9qp((>{zh%ZHDR)lXPI4 zH2-*QSWhvV7{~3~4R!Skabt&lLJK1nLJJF+q4T$(`LiNoJ^G7Ych~RJ2u)csXjO+q zg*JWhSCf)FP4s4hONM4st2#I{R?c`|6FEslpozl}B6w*Jj4BR!2Y>b6gbgiZNbyIdu0r(!e3w02>>5dPyruX zznjyR=Q_iNS?H)2@62#dbBq3e(Gg^o&!@m6#i3^LQWe5Cb^T`15ADHKv~)B`n2LWeY3{6k|PeE zLvyOWs%l0zz@aRt>+&p%9~sI8#tBOApfk|>K<|akz3Hb+eDj1h>jDkS=K`>*?=uv^ zZ};ytxIC9uqaIWsOEI^4KYf$Ew~!sq@d@Cr~D%;BvDnL9UuA?$hj5UH0*ksB*%ZF8PL$4%oq5eS^M2YUMT+vq}KLjmR0` zn`JasMS!88vpR--v#oIHjKTyi&k)h|R->EI#DWwsq)eerVx*LOVDtFJMkX_i)rI5z;*{ z2o(pX5Hnl^z~cV+t_)s)4+121Q_{RS!a?EmS0BXXXmtY(RxRV(|LJ3+Cbas=WUClC zz+BAofM-8?%>=g44Kv?Mp)NbP#vPkVG+gsV*d0T6)M=f8&DU=R80tNQ1fT^kLI=zR zvJB=*4^dk~N(g>b6)4d7_=L&<=(j0c7$; z0bC9J4oJlMuNMPYa%SWbN}Z|&#P*J(k__}NxEv8HWN=1%cLE&wpe4rr?L7$a8A3H| z7`!dx-*h$im=uN&!=L^TIYIl+HT@F%NV_RJXsybVn_sMGz)H{$%7oKmp@sd2hTu!Z zH@$V~@IV5x%bu|ehCvf;`9w>6iUS?UZeF>hpZWgpufd~(8m^2RFOW$eb~2eTNkz&t zs6z-Ud;Z#`lr%54|1w`JV=d$etxy?hlmL5ZKmEX+lKO%1S;?Gf4Fg6zO2#pE1Byd; zIgfCMf==(V-!Hdlp_&bvRmx=k|)`Gb^ILG`ueC&LrVU9tcVbm){@9?fXLk z>soU&YE=>fxD;R#48v{p;VDH#;K+c@JZ9W6S;*%45`@t1N}%%Gq$2#>TW1!ti1tXXl=U@1>Y>3Ooh>H$Bu?|C^qq_ z@l)`QCFnJ;3+90`|D`~i@-!i

q|S{FN^ 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

V((Jh3=I80l}zwtWz$$;Va%L8ws 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

XwUg`DaubPcrZKI+6W6CPlX>rSK1bi3 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 z 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

9!Qh4kY-1e=k6T|~v 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 literal 0 HcmV?d00001 diff --git a/dep.md b/dep.md new file mode 100644 index 0000000..4fa7dac --- /dev/null +++ b/dep.md @@ -0,0 +1,587 @@ +# PX360 Organizational Structure Refactor (Final Architecture) + +## Objective + +Refactor the current Department Hierarchy implementation into a simpler and more maintainable model that: + +* Uses a single source of truth for routing. +* Uses a single source of truth for reporting. +* Preserves legacy complaint mappings for historical records. +* Removes unnecessary hierarchy levels. +* Aligns with the latest organizational spreadsheet. +* Supports future department and section changes without impacting reporting. + +--- + +# Key Business Findings + +After reviewing: + +* Legacy complaint hierarchy +* Existing reports +* Final organization spreadsheet + +the following conclusions were reached. + +## Legacy Structure + +Historical complaints used: + +Location +→ Main Section +→ Sub Section + +Example: + +Outpatient +→ Medical +→ Radiology + +The old Sub Section was the actual operational unit receiving complaints. + +--- + +## Current Organization Structure + +The latest organization file contains: + +* Main Section +* Department +* Section + +Examples: + +Medical +→ Pharmacy Department +→ Central Outpatient Pharmacy + +Medical +→ Pharmacy Department +→ General + +--- + +## Important Discovery + +Area is not a valid hierarchy level. + +Evidence: + +* Frequently empty +* Often duplicates Department concepts +* Not used in reporting +* Not used in routing +* Not used in ownership + +Therefore: + +Area should not become a first-class model. + +--- + +## Reporting Structure + +All management reports are grouped by: + +* Medical +* Non-Medical +* Nursing +* Support Services + +These values come from: + +Main Section + +Therefore Main Section is actually the reporting category. + +--- + +# Target Hierarchy + +## Department Category + +Represents: + +* Medical +* Non-Medical +* Nursing +* Support Services + +Model: + +```python +class DepartmentCategory(models.TextChoices): + MEDICAL = "medical" + NON_MEDICAL = "non_medical" + NURSING = "nursing" + SUPPORT_SERVICES = "support_services" +``` + +This replaces the old concept of: + +Main Section + +--- + +## Department + +Primary accountable organizational unit. + +Model: + +```python +class Department(models.Model): + + category + + name + code + + champion + + manager_1st + manager_2nd + manager_3rd + + deputy_manager + supervisor + deputy_supervisor + + is_active +``` + +Examples: + +* Pharmacy Department +* Radiology Department +* Laboratory Department +* Emergency Department + +Departments always have ownership. + +Departments always have a Champion. + +Departments are the fallback routing target. + +--- + +## Section + +Optional child unit under a Department. + +Model: + +```python +class Section(models.Model): + + department = models.ForeignKey( + Department, + related_name="sections" + ) + + name + code + + champion = models.ForeignKey( + Staff, + null=True, + blank=True + ) + + location_type + + location_code + zone + floor + + is_active +``` + +Examples: + +* Central Outpatient Pharmacy +* ER Pharmacy +* Pediatric Pharmacy + +Not every Department requires Sections. + +Not every Section requires a Champion. + +--- + +## Remove Sub-Section + +Current model: + +OrgSubSubSection + +Findings: + +* Mostly empty in source data. +* Not used in reports. +* Not used in routing. +* Not used in escalation. + +Action: + +Deprecate and remove. + +--- + +# Complaint Model + +## Target Structure + +```python +class Complaint(models.Model): + + department = models.ForeignKey( + Department + ) + + section = models.ForeignKey( + Section, + null=True, + blank=True + ) + + legacy_mapping = models.ForeignKey( + LegacyHierarchyMapping, + null=True, + blank=True + ) +``` + +--- + +# Ownership Rules + +Single ownership strategy. + +```python +def get_owner(): + + if complaint.section and complaint.section.champion: + return complaint.section.champion + + return complaint.department.champion +``` + +Rules: + +1. Section Champion takes ownership when available. +2. Otherwise Department Champion owns the complaint. + +This becomes the only routing rule in the system. + +--- + +# Legacy Data Strategy + +## Legacy Mapping Table + +Keep: + +```python +class LegacyHierarchyMapping(models.Model): + + old_location + old_main_section + old_subsection + + department + section +``` + +Purpose: + +Legacy Complaint +→ Legacy Mapping +→ Department / Section + +--- + +## Legacy Fields + +The following fields become historical reference only: + +* legacy_location +* legacy_main_section +* legacy_subsection + +Rules: + +* No reporting usage +* No routing usage +* No escalation usage +* No dashboard usage + +May remain temporarily for audit purposes. + +--- + +# Reporting Strategy + +## Single Source of Truth + +All reports must use: + +```python +complaint.department +complaint.section +``` + +only. + +Never use legacy hierarchy fields. + +--- + +## Category Reports + +Medical / Nursing / Support Services reports: + +```python +Complaint.objects.values( + "department__category" +) +``` + +--- + +## Department Reports + +```python +Complaint.objects.values( + "department__name" +) +``` + +--- + +## Section Reports + +```python +Complaint.objects.values( + "section__name" +) +``` + +--- + +# Location Handling + +Location is metadata. + +It is not part of ownership hierarchy. + +Examples: + +* Outpatient Clinics +* Inpatient +* Emergency +* General Services + +Used for: + +* Display +* Filtering +* Analytics + +Not used for routing ownership. + +--- + +## Location Type + +Retain: + +* OP +* IP +* ER +* GENERAL + +This becomes the preferred reporting dimension instead of string matching location names. + +Example: + +```python +class LocationType(models.TextChoices): + OP = "OP" + IP = "IP" + ER = "ER" + GENERAL = "GENERAL" +``` + +--- + +# Import Strategy + +Department uniqueness: + +```python +(category, department_name) +``` + +must create a single Department. + +Never create duplicate Departments from repeated spreadsheet rows. + +--- + +Sections: + +```python +(department, section_name) +``` + +must be unique. + +--- + +# Migration Plan + +## Phase 1 + +Create: + +* DepartmentCategory +* LocationType enums + +Remove Area dependency. + +--- + +## Phase 2 + +Rename: + +OrgSubSection + +to: + +Section + +Update: + +* models +* serializers +* views +* APIs +* templates +* services + +--- + +## Phase 3 + +Deprecate: + +OrgSubSubSection + +Backfill any valid data. + +Remove references. + +--- + +## Phase 4 + +Backfill all historical complaints. + +Populate: + +* department +* section + +using LegacyHierarchyMapping. + +Target: + +100% coverage. + +--- + +## Phase 5 + +Update routing services. + +Replace all routing logic with: + +```python +complaint.get_owner() +``` + +--- + +## Phase 6 + +Update: + +* Dashboards +* KPI services +* Reports +* Exports +* Analytics + +to use: + +* Department +* Section +* Department Category + +only. + +--- + +## Phase 7 + +Update forms. + +Replace: + +Location +→ Main Section +→ Sub Section + +with: + +Category +→ Department +→ Section + +Section optional. + +--- + +## Phase 8 + +Mark legacy hierarchy fields as deprecated. + +Prevent all new writes. + +Keep read-only until final cleanup release. + +--- + +# Success Criteria + +✓ Department is the primary accountable entity. + +✓ Section is optional. + +✓ Reporting uses Department Category. + +✓ Routing uses Department / Section only. + +✓ Legacy hierarchy is used only for historical mapping. + +✓ Area is removed from operational design. + +✓ OrgSubSubSection is removed. + +✓ OrgSubSection is renamed to Section. + +✓ Dashboards no longer depend on legacy hierarchy. + +✓ Complaint ownership has a single source of truth. diff --git a/docs/PX360_External_API.postman_collection.json b/docs/PX360_External_API.postman_collection.json new file mode 100644 index 0000000..3a643fc --- /dev/null +++ b/docs/PX360_External_API.postman_collection.json @@ -0,0 +1,799 @@ +{ + "info": { + "_postman_id": "px360-external-api-collection", + "name": "PX360 External API", + "description": "PX360 Patient Experience Platform - External API endpoints.\n\nBase URL: `/api/v1/external/`\n\nAll requests require an `X-API-Key` header. Set the `api_key` variable in your environment.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Lookup", + "item": [ + { + "name": "List Hospitals", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/hospitals/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "hospitals", ""] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "url": { + "raw": "{{base_url}}/api/v1/external/hospitals/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "hospitals", ""] + } + }, + "status": "OK", + "code": 200, + "body": "{\n \"count\": 3,\n \"results\": [\n { \"id\": \"uuid\", \"name\": \"Al Nuzha\", \"code\": \"HH-N\" },\n { \"id\": \"uuid\", \"name\": \"Al Olya\", \"code\": \"HH-A\" },\n { \"id\": \"uuid\", \"name\": \"Al Suwaidi\", \"code\": \"HH-S\" }\n ]\n}" + } + ] + }, + { + "name": "List Location Types", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/location-types/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "location-types", ""] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "url": { + "raw": "{{base_url}}/api/v1/external/location-types/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "location-types", ""] + } + }, + "status": "OK", + "code": 200, + "body": "{\n \"count\": 4,\n \"results\": [\n { \"value\": \"OP\", \"label\": \"Outpatient\", \"label_ar\": \"خارجي\" },\n { \"value\": \"IP\", \"label\": \"Inpatient\", \"label_ar\": \"تنويم\" },\n { \"value\": \"ER\", \"label\": \"Emergency\", \"label_ar\": \"طوارئ\" },\n { \"value\": \"GENERAL\", \"label\": \"General\", \"label_ar\": \"عام\" }\n ]\n}" + } + ] + }, + { + "name": "List Areas", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/areas/?hospital=Al Nuzha", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "areas", ""], + "query": [ + { + "key": "hospital", + "value": "Al Nuzha" + }, + { + "key": "location_type", + "value": "OP", + "disabled": true + } + ] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "url": { + "raw": "{{base_url}}/api/v1/external/areas/?hospital=Al Nuzha", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "areas", ""], + "query": [ + { "key": "hospital", "value": "Al Nuzha" } + ] + } + }, + "status": "OK", + "code": 200, + "body": "{\n \"count\": 27,\n \"results\": [\n { \"id\": \"uuid\", \"name\": \"Emergency\", \"name_ar\": \"\", \"code\": \"emergency\" }\n ]\n}" + } + ] + }, + { + "name": "List Departments", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/departments/?hospital=Al Nuzha", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "departments", ""], + "query": [ + { + "key": "hospital", + "value": "Al Nuzha" + } + ] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "url": { + "raw": "{{base_url}}/api/v1/external/departments/?hospital=Al Nuzha", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "departments", ""], + "query": [ + { "key": "hospital", "value": "Al Nuzha" } + ] + } + }, + "status": "OK", + "code": 200, + "body": "{\n \"count\": 37,\n \"results\": [\n { \"id\": \"uuid\", \"name\": \"Critical Care Department\", \"name_en\": \"Critical Care Department\", \"name_ar\": \"\", \"code\": \"hh_n_critical_care_department\" }\n ]\n}" + } + ] + }, + { + "name": "List Sections", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/sections/?hospital=Al Nuzha&department=Critical Care Department", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "sections", ""], + "query": [ + { + "key": "hospital", + "value": "Al Nuzha" + }, + { + "key": "department", + "value": "Critical Care Department" + } + ] + } + }, + "response": [ + { + "name": "Success", + "originalRequest": { + "method": "GET", + "url": { + "raw": "{{base_url}}/api/v1/external/sections/?hospital=Al Nuzha&department=Critical Care Department", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "sections", ""], + "query": [ + { "key": "hospital", "value": "Al Nuzha" }, + { "key": "department", "value": "Critical Care Department" } + ] + } + }, + "status": "OK", + "code": 200, + "body": "{\n \"count\": 3,\n \"results\": [\n { \"id\": \"uuid\", \"name\": \"ICU\", \"name_ar\": \"\", \"code\": \"hh_n_critical_care_department__icu\" }\n ]\n}" + } + ] + } + ] + }, + { + "name": "Complaints", + "item": [ + { + "name": "Create Complaint", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"hospital\": \"Al Nuzha\",\n \"title\": \"Long wait time in ER\",\n \"description\": \"I waited 3 hours in the emergency room without being seen.\",\n \"contact_name\": \"John Doe\",\n \"contact_phone\": \"+966501234567\",\n \"contact_email\": \"john@example.com\",\n \"location_type\": \"ER\",\n \"area\": \"Emergency\",\n \"department\": \"Critical Care Department\",\n \"section\": \"ICU\",\n \"relation_to_patient\": \"patient\",\n \"patient_name\": \"John Doe\",\n \"incident_date\": \"2026-06-10\",\n \"expected_result\": \"Faster service in ER\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/complaints/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "complaints", ""] + } + }, + "response": [ + { + "name": "Created", + "originalRequest": { + "method": "POST", + "url": { + "raw": "{{base_url}}/api/v1/external/complaints/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "complaints", ""] + } + }, + "status": "Created", + "code": 201, + "body": "{\n \"success\": true,\n \"reference_number\": \"CMP-20260609-167152\",\n \"status\": \"open\"\n}" + } + ] + }, + { + "name": "List Complaints", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/complaints/list/?page=1&page_size=20", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "complaints", "list", ""], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "page_size", + "value": "20" + }, + { + "key": "created_from", + "value": "2026-01-01", + "disabled": true + }, + { + "key": "created_to", + "value": "2026-06-30", + "disabled": true + }, + { + "key": "status", + "value": "open", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Get Complaint", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/complaints/CMP-20260609-167152/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "complaints", "CMP-20260609-167152", ""] + } + }, + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "body": "{\n \"id\": \"uuid\",\n \"reference_number\": \"CMP-20260609-167152\",\n \"title\": \"Long wait time in ER\",\n \"description\": \"I waited 3 hours...\",\n \"status\": \"open\",\n \"severity\": \"medium\",\n \"priority\": \"medium\",\n \"contact_name\": \"John Doe\",\n \"contact_phone\": \"+966501234567\",\n \"hospital_name\": \"Al Nuzha\",\n \"created_at\": \"2026-06-09T13:38:03.808618+03:00\",\n \"updated_at\": \"2026-06-09T13:38:03.808683+03:00\"\n}" + } + ] + }, + { + "name": "Set Satisfaction", + "request": { + "method": "PATCH", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"satisfaction\": \"satisfied\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/complaints/CMP-20260609-167152/satisfaction/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "complaints", "CMP-20260609-167152", "satisfaction", ""] + } + }, + "response": [ + { + "name": "Success", + "status": "OK", + "code": 200, + "body": "{\n \"success\": true,\n \"reference_number\": \"CMP-20260609-167152\",\n \"satisfaction\": \"satisfied\"\n}" + } + ] + } + ] + }, + { + "name": "Inquiries", + "item": [ + { + "name": "Create Inquiry", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"hospital\": \"Al Nuzha\",\n \"subject\": \"Appointment rescheduling\",\n \"message\": \"I need to reschedule my cardiology appointment.\",\n \"contact_name\": \"Jane Smith\",\n \"contact_phone\": \"+966509876543\",\n \"contact_email\": \"jane@example.com\",\n \"category\": \"appointment\",\n \"location_type\": \"OP\",\n \"department\": \"Cardiology\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/inquiries/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "inquiries", ""] + } + }, + "response": [ + { + "name": "Created", + "status": "Created", + "code": 201, + "body": "{\n \"success\": true,\n \"reference_number\": \"INQ-20260609-124293\",\n \"status\": \"open\"\n}" + } + ] + }, + { + "name": "List Inquiries", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/inquiries/list/?page=1&page_size=20", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "inquiries", "list", ""], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "page_size", + "value": "20" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Inquiry", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/inquiries/INQ-20260609-124293/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "inquiries", "INQ-20260609-124293", ""] + } + }, + "response": [] + } + ] + }, + { + "name": "Observations", + "item": [ + { + "name": "Create Observation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"hospital\": \"Al Nuzha\",\n \"description\": \"Wet floor near the main entrance, no warning sign posted.\",\n \"title\": \"Wet floor hazard\",\n \"severity\": \"medium\",\n \"location_text\": \"Main entrance lobby\",\n \"contact_name\": \"Anonymous Reporter\",\n \"contact_phone\": \"+966501112233\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/observations/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "observations", ""] + } + }, + "response": [ + { + "name": "Created", + "status": "Created", + "code": 201, + "body": "{\n \"success\": true,\n \"reference_number\": \"OBS-O8TCIC\",\n \"status\": \"new\"\n}" + } + ] + }, + { + "name": "List Observations", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/observations/list/?page=1&page_size=20", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "observations", "list", ""], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "page_size", + "value": "20" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Observation", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/observations/OBS-O8TCIC/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "observations", "OBS-O8TCIC", ""] + } + }, + "response": [] + } + ] + }, + { + "name": "Appreciations", + "item": [ + { + "name": "Create Appreciation", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"hospital\": \"Al Nuzha\",\n \"message\": \"Dr. Ahmed was incredibly kind and thorough during my visit.\",\n \"contact_name\": \"Sara Ali\",\n \"contact_phone\": \"+966503334455\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/appreciations/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "appreciations", ""] + } + }, + "response": [ + { + "name": "Created", + "status": "Created", + "code": 201, + "body": "{\n \"success\": true,\n \"reference_number\": \"APR-20260609-239270\",\n \"status\": \"draft\"\n}" + } + ] + }, + { + "name": "List Appreciations", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/appreciations/list/?page=1&page_size=20", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "appreciations", "list", ""], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "page_size", + "value": "20" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Appreciation", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/appreciations//", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "appreciations", "", ""] + } + }, + "response": [] + } + ] + }, + { + "name": "Suggestions", + "item": [ + { + "name": "Create Suggestion", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"hospital\": \"Al Nuzha\",\n \"message\": \"Please implement a digital queue management system to reduce wait times.\",\n \"contact_name\": \"Suggestor\",\n \"contact_phone\": \"+966504445566\",\n \"title\": \"Digital queue system\",\n \"category\": \"technology\",\n \"rating\": 4\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/suggestions/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "suggestions", ""] + } + }, + "response": [ + { + "name": "Created", + "status": "Created", + "code": 201, + "body": "{\n \"success\": true,\n \"reference_number\": \"SG-20260609-316316\",\n \"status\": \"submitted\"\n}" + } + ] + }, + { + "name": "List Suggestions", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/suggestions/list/?page=1&page_size=20", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "suggestions", "list", ""], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "page_size", + "value": "20" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Suggestion", + "request": { + "method": "GET", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + } + ], + "url": { + "raw": "{{base_url}}/api/v1/external/suggestions//", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "suggestions", "", ""] + } + }, + "response": [] + } + ] + }, + { + "name": "Doctor Ratings", + "item": [ + { + "name": "Submit Doctor Rating", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{api_key}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"hospital_id\": 1,\n \"doctor_id\": \"10738\",\n \"doctor_name\": \"Dr. Omaymah Yaqoub\",\n \"rating\": 4,\n \"feedback\": \"Great doctor, very caring and attentive.\",\n \"rating_date\": \"2026-06-10\",\n \"patient_uhid\": \"UHID12345\",\n \"patient_name\": \"Ahmed Ali\",\n \"patient_type\": \"OP\",\n \"department_name\": \"Internal Medicine\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/v1/external/doctor-ratings/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "doctor-ratings", ""] + } + }, + "response": [ + { + "name": "Created", + "originalRequest": { + "method": "POST", + "url": { + "raw": "{{base_url}}/api/v1/external/doctor-ratings/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "doctor-ratings", ""] + } + }, + "status": "Created", + "code": 201, + "body": "{\n \"success\": true,\n \"rating_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"staff_id\": \"f1e2d3c4-b5a6-7890-abcd-ef1234567890\",\n \"doctor_name\": \"OMAYMAH YAQOUB ELAMEIAN\",\n \"rating\": 4\n}" + }, + { + "name": "Doctor Not Found", + "originalRequest": { + "method": "POST", + "url": { + "raw": "{{base_url}}/api/v1/external/doctor-ratings/", + "host": ["{{base_url}}"], + "path": ["api", "v1", "external", "doctor-ratings", ""] + } + }, + "status": "Bad Request", + "code": 400, + "body": "{\n \"doctor_id\": [\n \"Doctor with employee ID '99999' not found at this hospital.\"\n ]\n}" + } + ] + } + ] + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:8000" + }, + { + "key": "api_key", + "value": "your-api-key-here" + } + ] +} \ No newline at end of file diff --git a/docs/external-api.md b/docs/external-api.md new file mode 100644 index 0000000..8b6c838 --- /dev/null +++ b/docs/external-api.md @@ -0,0 +1,673 @@ +# PX360 External API Documentation + +Base URL: `/api/v1/external/` + +## Authentication + +All requests require an `X-API-Key` header. + +``` +X-API-Key: +``` + +API keys are managed through the Django admin panel. Each key can be scoped to: + +- A specific hospital (restricts data access to that hospital) +- Specific entities (`complaints`, `inquiries`, `observations`, `appreciations`, `suggestions`, `doctor_ratings`) +- Empty `allowed_entities` = access to all entities + +### Creating an API Key + +Via Django admin or programmatically: + +```python +from apps.integrations.models import ExternalAPIKey + +api_key, raw_key = ExternalAPIKey.create_key( + name="Partner Integration", + hospital=hospital_object, # Optional: scope to a hospital + allowed_entities=["complaints", "inquiries"], # Optional: empty = all + rate_limit=60, # Requests per minute + description="Integration with partner system", +) +# raw_key is shown ONCE - store it securely +``` + +### Error Responses + +| HTTP Status | Meaning | +|---|---| +| `401` | Missing or invalid API key | +| `403` | API key does not have access to the requested entity, or hospital scope mismatch | +| `404` | Requested resource not found | +| `400` | Validation error (missing/invalid fields) | + +--- + +## Lookup Endpoints + +These endpoints provide dropdown/reference data for building forms. + +### List Hospitals + +``` +GET /api/v1/external/hospitals/ +``` + +**Response:** + +```json +{ + "count": 3, + "results": [ + { "id": "uuid", "name": "Al Nuzha", "code": "HH-N" }, + { "id": "uuid", "name": "Al Olya", "code": "HH-A" }, + { "id": "uuid", "name": "Al Suwaidi", "code": "HH-S" } + ] +} +``` + +### List Location Types + +``` +GET /api/v1/external/location-types/ +``` + +**Response:** + +```json +{ + "count": 4, + "results": [ + { "value": "OP", "label": "Outpatient", "label_ar": "خارجي" }, + { "value": "IP", "label": "Inpatient", "label_ar": "تنويم" }, + { "value": "ER", "label": "Emergency", "label_ar": "طوارئ" }, + { "value": "GENERAL", "label": "General", "label_ar": "عام" } + ] +} +``` + +### List Areas + +``` +GET /api/v1/external/areas/?hospital= +``` + +| Parameter | Required | Description | +|---|---|---| +| `hospital` | Yes | Hospital name (case-insensitive) | +| `location_type` | No | Filter by location type (`OP`, `IP`, `ER`, `GENERAL`) | + +**Response:** + +```json +{ + "count": 27, + "results": [ + { "id": "uuid", "name": "Emergency", "name_ar": "", "code": "emergency" } + ] +} +``` + +### List Departments + +``` +GET /api/v1/external/departments/?hospital= +``` + +| Parameter | Required | Description | +|---|---|---| +| `hospital` | Yes | Hospital name (case-insensitive) | + +**Response:** + +```json +{ + "count": 37, + "results": [ + { "id": "uuid", "name": "Critical Care Department", "name_en": "Critical Care Department", "name_ar": "", "code": "hh_n_critical_care_department" } + ] +} +``` + +### List Sections + +``` +GET /api/v1/external/sections/?hospital=&department= +``` + +| Parameter | Required | Description | +|---|---|---| +| `hospital` | Yes | Hospital name (case-insensitive) | +| `department` | Yes | Department name (case-insensitive) | + +**Response:** + +```json +{ + "count": 3, + "results": [ + { "id": "uuid", "name": "ICU", "name_ar": "", "code": "hh_n_critical_care_department__icu" } + ] +} +``` + +--- + +## Complaints + +### Create a Complaint + +``` +POST /api/v1/external/complaints/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `hospital` | Yes | string | Hospital name (case-insensitive) | +| `title` | Yes | string | Complaint title (max 500 chars) | +| `description` | Yes | string | Detailed complaint description | +| `contact_name` | Yes | string | Reporter's name (max 200 chars) | +| `contact_phone` | Yes | string | Reporter's phone (max 20 chars) | +| `contact_email` | No | string | Reporter's email | +| `relation_to_patient` | No | string | One of: `patient`, `relative`, `friend`, `other` | +| `patient_name` | No | string | Patient name | +| `national_id` | No | string | National ID number | +| `incident_date` | No | date | Date of incident (`YYYY-MM-DD`) | +| `expected_result` | No | string | Expected resolution | +| `location_type` | No | string | One of: `OP`, `IP`, `ER`, `GENERAL` | +| `area` | No | string | Area name (resolved by hospital) | +| `department` | No | string | Department name (resolved by hospital) | +| `section` | No | string | Section name (requires department) | + +**Example:** + +```json +{ + "hospital": "Al Nuzha", + "title": "Long wait time in ER", + "description": "I waited 3 hours in the emergency room without being seen.", + "contact_name": "John Doe", + "contact_phone": "+966501234567", + "contact_email": "john@example.com", + "location_type": "ER", + "area": "Emergency", + "department": "Critical Care Department", + "section": "ICU" +} +``` + +**Response (201):** + +```json +{ + "success": true, + "reference_number": "CMP-20260609-167152", + "status": "open" +} +``` + +### List Complaints + +``` +GET /api/v1/external/complaints/list/ +``` + +| Parameter | Required | Description | +|---|---|---| +| `page` | No | Page number (default: 1) | +| `page_size` | No | Items per page (default: 20, max: 100) | +| `created_from` | No | Filter by created date from (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) | +| `created_to` | No | Filter by created date to (date-only values auto-extend to end of day) | +| `updated_from` | No | Filter by updated date from | +| `updated_to` | No | Filter by updated date to | +| `status` | No | Filter by status value | + +### Retrieve a Complaint + +``` +GET /api/v1/external/complaints// +``` + +**Response:** + +```json +{ + "id": "uuid", + "reference_number": "CMP-20260609-167152", + "title": "Long wait time in ER", + "description": "I waited 3 hours...", + "status": "open", + "severity": "medium", + "priority": "medium", + "contact_name": "John Doe", + "contact_phone": "+966501234567", + "contact_email": "john@example.com", + "relation_to_patient": "", + "patient_name": "", + "incident_date": null, + "expected_result": "", + "resolution": "", + "satisfaction": "", + "hospital_name": "Al Nuzha", + "created_at": "2026-06-09T13:38:03.808618+03:00", + "updated_at": "2026-06-09T13:38:03.808683+03:00" +} +``` + +#### Resolution & Satisfaction Fields + +| Field | Description | +|---|---| +| `resolution` | Free-text description of the resolution taken (empty if unresolved) | +| `satisfaction` | Patient satisfaction: `satisfied`, `neutral`, `dissatisfied`, `no_response`, or empty | + +### Set Patient Satisfaction + +Sets the patient satisfaction level for a complaint. **A resolution must be recorded first** — returns `400` if the complaint has no resolution. + +``` +PATCH /api/v1/external/complaints//satisfaction/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `satisfaction` | Yes | string | One of: `satisfied`, `neutral`, `dissatisfied`, `no_response` | + +**Example:** + +```json +{ + "satisfaction": "satisfied" +} +``` + +**Response (200):** + +```json +{ + "success": true, + "reference_number": "CMP-20260609-167152", + "satisfaction": "satisfied" +} +``` + +--- + +## Inquiries + +### Create an Inquiry + +``` +POST /api/v1/external/inquiries/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `hospital` | Yes | string | Hospital name | +| `subject` | Yes | string | Inquiry subject (max 500 chars) | +| `message` | Yes | string | Inquiry message | +| `contact_name` | Yes | string | Contact name | +| `contact_phone` | Yes | string | Contact phone | +| `contact_email` | No | string | Contact email | +| `category` | No | string | One of: `appointment`, `billing`, `medical_records`, `general`, `other` | +| `location_type` | No | string | One of: `OP`, `IP`, `ER`, `GENERAL` | +| `area` | No | string | Area name | +| `department` | No | string | Department name | +| `section` | No | string | Section name (requires department) | + +**Response (201):** + +```json +{ + "success": true, + "reference_number": "INQ-20260609-124293", + "status": "open" +} +``` + +### List / Retrieve Inquiries + +``` +GET /api/v1/external/inquiries/list/ +GET /api/v1/external/inquiries// +``` + +Same pagination and date/status filters as complaints. + +**Retrieve Response:** + +```json +{ + "id": "uuid", + "reference_number": "INQ-20260609-124293", + "subject": "Appointment rescheduling", + "message": "I need to reschedule my cardiology appointment.", + "category": "appointment", + "status": "open", + "contact_name": "Jane Smith", + "contact_phone": "+966509876543", + "contact_email": "jane@example.com", + "hospital_name": "Al Nuzha", + "created_at": "2026-06-09T13:36:22.730962+03:00", + "updated_at": "2026-06-09T13:36:22.731020+03:00" +} +``` + +--- + +## Observations + +### Create an Observation + +``` +POST /api/v1/external/observations/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `hospital` | Yes | string | Hospital name | +| `description` | Yes | string | Observation description | +| `title` | No | string | Title (max 300 chars) | +| `severity` | No | string | One of: `low`, `medium`, `high`, `critical` (default: `medium`) | +| `category` | No | UUID | Observation category UUID | +| `location_text` | No | string | Free-text location description | +| `incident_datetime` | No | datetime | When the incident occurred | +| `contact_name` | No | string | Reporter name (anonymous supported) | +| `contact_phone` | No | string | Reporter phone | +| `contact_email` | No | string | Reporter email | +| `reporter_staff_id` | No | string | Staff ID of reporter | +| `patient_file_number` | No | string | Patient file number | + +**Response (201):** + +```json +{ + "success": true, + "reference_number": "OBS-O8TCIC", + "status": "new" +} +``` + +### List / Retrieve Observations + +``` +GET /api/v1/external/observations/list/ +GET /api/v1/external/observations// +``` + +**Retrieve Response:** + +```json +{ + "id": "uuid", + "reference_number": "OBS-O8TCIC", + "title": "", + "description": "Wet floor near the main entrance, no warning sign posted.", + "severity": "medium", + "status": "new", + "location_text": "Main entrance lobby", + "incident_datetime": "2026-06-09T13:36:23.542046+03:00", + "contact_name": "Anonymous Reporter", + "contact_phone": "", + "contact_email": "", + "reporter_staff_id": "", + "patient_file_number": "", + "hospital_name": "Al Nuzha", + "category_name": null, + "created_at": "2026-06-09T13:36:23.540024+03:00", + "updated_at": "2026-06-09T13:36:23.540079+03:00" +} +``` + +--- + +## Appreciations + +### Create an Appreciation + +``` +POST /api/v1/external/appreciations/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `hospital` | Yes | string | Hospital name | +| `message` | Yes | string | Appreciation message | +| `contact_name` | Yes | string | Submitter name | +| `contact_phone` | Yes | string | Submitter phone | + +**Response (201):** + +```json +{ + "success": true, + "reference_number": "APR-20260609-239270", + "status": "draft" +} +``` + +### List / Retrieve Appreciations + +``` +GET /api/v1/external/appreciations/list/ +GET /api/v1/external/appreciations// +``` + +**Retrieve Response:** + +```json +{ + "id": "uuid", + "reference_number": "APR-20260609-239270", + "message_en": "Dr. Ahmed was incredibly kind and thorough.", + "status": "draft", + "hospital_name": "Al Nuzha", + "is_anonymous": false, + "created_at": "2026-06-09T13:36:23.788028+03:00", + "updated_at": "2026-06-09T13:36:23.788077+03:00" +} +``` + +--- + +## Suggestions + +### Create a Suggestion + +``` +POST /api/v1/external/suggestions/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `hospital` | Yes | string | Hospital name | +| `message` | Yes | string | Suggestion message | +| `contact_name` | Yes | string | Submitter name | +| `contact_phone` | Yes | string | Submitter phone | +| `title` | No | string | Title (auto-generated from message if omitted) | +| `category` | No | string | One of: `clinical_care`, `staff_service`, `facility`, `communication`, `appointment`, `billing`, `food_service`, `cleanliness`, `technology`, `general`, `other` | +| `rating` | No | integer | Rating 1-5 | + +**Response (201):** + +```json +{ + "success": true, + "reference_number": "SG-20260609-316316", + "status": "submitted" +} +``` + +### List / Retrieve Suggestions + +``` +GET /api/v1/external/suggestions/list/ +GET /api/v1/external/suggestions// +``` + +**Retrieve Response:** + +```json +{ + "id": "uuid", + "reference_number": "SG-20260609-316316", + "title": "Digital queue system", + "message": "Please implement a digital queue management system.", + "category": "technology", + "rating": null, + "status": "submitted", + "feedback_type": "suggestion", + "sentiment": "neutral", + "contact_name": "Suggestor", + "contact_phone": "+966504445566", + "hospital_name": "Al Nuzha", + "created_at": "2026-06-09T13:36:24.051998+03:00", + "updated_at": "2026-06-09T13:36:24.052047+03:00" +} +``` + +--- + +## Doctor Ratings + +### Submit a Doctor Rating + +Submit a patient rating for a doctor. The `doctor_id` must be an existing employee ID in the Staff table for the given hospital. + +``` +POST /api/v1/external/doctor-ratings/ +``` + +**Request Body:** + +| Field | Required | Type | Description | +|---|---|---|---| +| `hospital_id` | Yes | integer | Hospital ID | +| `doctor_id` | Yes | string | Doctor's employee ID (must exist in the hospital's Staff records) | +| `rating` | Yes | integer | Rating from 1 to 5 | +| `doctor_name` | No | string | Doctor name (stored for reference) | +| `feedback` | No | string | Patient feedback text | +| `rating_date` | No | date | Date of rating (`YYYY-MM-DD`, defaults to current date) | +| `patient_uhid` | No | string | Patient unique health ID | +| `patient_name` | No | string | Patient name | +| `patient_type` | No | string | One of: `IP`, `OP`, `ER`, `DC` | +| `department_name` | No | string | Department name | +| `admit_date` | No | date | Admission date (`YYYY-MM-DD`) | +| `discharge_date` | No | date | Discharge date (`YYYY-MM-DD`) | + +**Example:** + +```json +{ + "hospital_id": 1, + "doctor_id": "10738", + "doctor_name": "Dr. Omaymah Yaqoub", + "rating": 4, + "feedback": "Great doctor, very caring and attentive.", + "rating_date": "2026-06-10", + "patient_uhid": "UHID12345", + "patient_name": "Ahmed Ali", + "patient_type": "OP", + "department_name": "Internal Medicine" +} +``` + +**Response (201):** + +```json +{ + "success": true, + "rating_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", + "doctor_name": "OMAYMAH YAQOUB ELAMEIAN", + "rating": 4 +} +``` + +**Error Responses:** + +| HTTP Status | Condition | +|---|---| +| `400` | `doctor_id` not found in the hospital's Staff records | +| `400` | `rating` outside 1-5 range | +| `400` | `hospital_id` not found | +| `403` | Hospital does not match API key scope | + +**Error Example (doctor not found):** + +```json +{ + "doctor_id": [ + "Doctor with employee ID '99999' not found at this hospital." + ] +} +``` + +--- + +## Common Patterns + +### Location Hierarchy + +Locations follow a cascading hierarchy. Use lookup endpoints to populate dependent dropdowns: + +``` +1. GET /hospitals/ → pick hospital +2. GET /location-types/ → pick location type +3. GET /areas/?hospital=X → pick area (filtered by hospital) +4. GET /departments/?hospital=X → pick department +5. GET /sections/?hospital=X&department=Y → pick section +``` + +### Date Filtering + +All list endpoints support date range filtering: + +``` +GET /complaints/list/?created_from=2026-01-01&created_to=2026-06-30 +GET /complaints/list/?updated_from=2026-06-01T08:00:00&updated_to=2026-06-09T17:00:00 +``` + +- Date-only values (`YYYY-MM-DD`): `created_from` starts at 00:00, `created_to` extends to 23:59:59 +- Full datetime values (`YYYY-MM-DDTHH:MM:SS`) are also accepted + +### Pagination + +All list endpoints return paginated results: + +```json +{ + "count": 150, + "page": 1, + "page_size": 20, + "results": [...] +} +``` + +| Parameter | Default | Max | +|---|---|---| +| `page` | 1 | - | +| `page_size` | 20 | 100 | + +### Reference Number Formats + +| Entity | Prefix | Format | +|---|---|---| +| Complaint | `CMP-` | `CMP-YYYYMMDD-XXXXXX` | +| Inquiry | `INQ-` | `INQ-YYYYMMDD-XXXXXX` | +| Observation | `OBS-` | `OBS-XXXXXX` (auto-generated by model) | +| Appreciation | `APR-` | `APR-YYYYMMDD-XXXXXX` | +| Suggestion | `SG-` | `SG-YYYYMMDD-XXXXXX` | diff --git a/e2e/helpers/audit.ts b/e2e/helpers/audit.ts new file mode 100644 index 0000000..e6b8c0d --- /dev/null +++ b/e2e/helpers/audit.ts @@ -0,0 +1,156 @@ +import { Page, Request, Response, expect } from '@playwright/test'; +import { RoleAuthHelper, RoleName } from './helpers'; + +export const E2E_HOSPITAL_NAME = 'E2E Test Hospital'; +export const E2E_PASSWORD = process.env.E2E_PASSWORD || 'Dev@123456'; +export const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:8000'; + +export type ObsStatus = 'PASS' | 'FAIL' | 'WARN' | 'INFO' | 'SKIP'; +export interface Observation { + module: string; + step: string; + role?: string; + status: ObsStatus; + detail: string; + url?: string; + http?: number; + ts: string; +} + +export const OBS: Observation[] = []; + +export function observe( + module: string, + step: string, + status: ObsStatus, + detail: string, + opts: { role?: string; url?: string; http?: number } = {} +) { + const o: Observation = { + module, + step, + status, + detail, + role: opts.role, + url: opts.url, + http: opts.http, + ts: new Date().toISOString(), + }; + OBS.push(o); + const tag = `[${status}] ${module}/${step}${opts.role ? ` (${opts.role})` : ''}: ${detail}`; + if (status === 'FAIL') console.log('\x1b[31m' + tag + '\x1b[0m'); + else if (status === 'WARN') console.log('\x1b[33m' + tag + '\x1b[0m'); + else if (status === 'PASS') console.log('\x1b[32m' + tag + '\x1b[0m'); + else console.log(tag); +} + +/** + * Attach console / pageerror / response observers to a page. + * Captures JS errors, console errors, and HTTP 4xx/5xx + Django tracebacks. + */ +export function attachObservers(page: Page, module: string, role?: string) { + // auto-dismiss any unexpected JS dialog so it can't block the run + page.on('dialog', (d) => { observe(module, 'dialog', 'WARN', `${d.type()}: ${d.message().slice(0, 120)}`, { role }); d.dismiss().catch(() => {}); }); + page.on('console', (msg) => { + if (msg.type() === 'error') { + observe(module, 'console-error', 'WARN', msg.text(), { role }); + } + }); + page.on('pageerror', (err) => { + observe(module, 'page-error', 'FAIL', `${err.name}: ${err.message}`, { role }); + }); + page.on('requestfailed', (req: Request) => { + observe(module, 'request-failed', 'WARN', `${req.method()} ${req.url()} - ${req.failure()?.errorText}`, { role }); + }); + page.on('response', async (resp: Response) => { + const status = resp.status(); + const url = resp.url(); + if (status >= 400) { + let bodySnippet = ''; + try { + const ct = resp.headers()['content-type'] || ''; + if (ct.includes('text') || ct.includes('html') || ct.includes('json')) { + const body = await resp.text(); + const tbMatch = body.match(/(?:Traceback[\s\S]{0,400}|Server Error \(500\)|OperationalError|DoesNotExist|TemplateSyntaxError)/); + bodySnippet = tbMatch ? ` | ${tbMatch[0].slice(0, 200).replace(/\s+/g, ' ')}` : body.slice(0, 160).replace(/\s+/g, ' '); + } + } catch { + /* ignore */ + } + observe(module, 'http-error', status >= 500 ? 'FAIL' : 'WARN', `${resp.request().method()} ${status} ${url}${bodySnippet}`, { + role, + url, + http: status, + }); + } + }); +} + +/** Detect a Django error/traceback page in the current body. */ +export async function bodyHasTraceback(page: Page): Promise { + const text = await page.textContent('body').catch(() => ''); + if (!text) return null; + const patterns = [ + /Server Error \(500\)/, + /Traceback \(most recent call last\)/, + /Exception Type:[\s\S]{0,80}/, + /DoesNotExist/, + /OperationalError/, + /TemplateSyntaxError/, + /Page not found \(404\)/, + ]; + for (const p of patterns) { + const m = text.match(p); + if (m) return m[0].slice(0, 120).replace(/\s+/g, ' '); + } + return null; +} + +/** + * Login as a role. If role is px_admin, also select E2E hospital via the + * hospital switcher (so all scoped views target E2E Test Hospital). + */ +export async function loginAndScope(page: Page, role: RoleName, module: string) { + const auth = new RoleAuthHelper(page); + await auth.login(role); + if (role === 'px_admin') { + // px_admin users may land on dashboard or select-hospital; switch to E2E + await page.goto('/core/select-hospital/').catch(() => {}); + await page.waitForLoadState('domcontentloaded').catch(() => {}); + const e2eLink = page.locator(`a:has-text("${E2E_HOSPITAL_NAME}"), a[href*="select-hospital"] >> text="${E2E_HOSPITAL_NAME}"`).first(); + if (await e2eLink.count().then((c) => c > 0)) { + await e2eLink.click().catch(() => {}); + await page.waitForLoadState('domcontentloaded').catch(() => {}); + } else { + // fallback: search any link containing the hospital name + const any = page.locator(`text="${E2E_HOSPITAL_NAME}"`).first(); + if (await any.count().then((c) => c > 0)) await any.click().catch(() => {}); + } + observe(module, 'login-scope', 'INFO', `px_admin scoped to ${E2E_HOSPITAL_NAME}`, { role }); + } + return auth; +} + +/** Resolve E2E hospital UUID from the public hospitals API. */ +export async function getE2EHospitalId(page: Page): Promise { + const resp = await page.context().request.get(`${BASE_URL}/core/api/hospitals/`, { timeout: 10000 }); + const data = await resp.json(); + const h = (data.hospitals || []).find((x: { name: string }) => x.name === E2E_HOSPITAL_NAME); + return h ? h.id : ''; +} + +/** + * Select the E2E hospital in a - - {% for s in staff_list %} - - {% endfor %} - -

O$MN%d>{IXBP%j0D)?6?` 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

- -
- - -

{% trans "Auto-populated from staff selection" %}

-
- -
- - -
-
- - -
-
- -{% endif %} - - -{% if can_send %} -
- {% csrf_token %} -
-
-
- -
-

{% trans "Send Appreciation" %}

-
-
-

{% trans "Configure recipients and customize the message before sending." %}

- -
-
+ {% if metadata %} +
+

+ + {% trans "Submitted By" %} +

+
+ {% if metadata.submitted_by_name %}
-

{% trans "Notify Department Manager" %}

-

{% trans "Send to the department manager" %}

+

{% trans "Name" %}

+

{{ metadata.submitted_by_name }}

- -
-
+ {% endif %} + {% if metadata.submitted_by_phone %}
-

{% trans "Notify Department" %}

-

{% trans "Send to entire department" %}

+

{% trans "Phone" %}

+

{{ metadata.submitted_by_phone }}

- + {% endif %} + {% if metadata.submitted_by_email %} +
+

{% trans "Email" %}

+

{{ metadata.submitted_by_email }}

+
+ {% endif %} + {% if metadata.staff_name_mentioned %} +
+

{% trans "Staff Mentioned" %}

+

{{ metadata.staff_name_mentioned }}

+
+ {% endif %}
-
+ + {% endif %} -
- - -

{% trans "Leave blank to send the original appreciation message only" %}

-
+ {% if appreciation.recipient %} +
+

+ + {% trans "Assigned To" %} +

+
+
+ +
+
+

{{ appreciation.get_recipient_name }}

+ {% if appreciation.department %} +

{{ appreciation.department.name }}

+ {% endif %} +
+
+
+ {% endif %} -
- - -

{% trans "Comma-separated email addresses" %}

-
+ {% if appreciation.status == 'sent' or appreciation.status == 'acknowledged' %} +
+

+ + {% trans "Sent" %} +

+
+
+

{% trans "Sent At" %}

+

{{ appreciation.sent_at|date:"Y-m-d H:i" }}

+
+
+

{% trans "To Manager" %}

+

+ {% if appreciation.send_to_manager %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} +

+
+
+

{% trans "To Department" %}

+

+ {% if appreciation.send_to_department %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} +

+
+
+ {% if appreciation.custom_message %} +
+

{% trans "Custom Message" %}

+

{{ appreciation.custom_message }}

+
+ {% endif %} + {% if appreciation.cc_list %} +
+

{% trans "CC" %}

+
+ {% for email in appreciation.cc_list %} + {{ email }} + {% endfor %} +
+
+ {% endif %} + {% if appreciation.acknowledged_at %} +
+
+ +

{% trans "Acknowledged on" %} {{ appreciation.acknowledged_at|date:"Y-m-d H:i" }}

+
+
+ {% endif %} +
+ {% endif %} +
-
-
+ +
+ {% if can_activate %} +
+

+ {% trans "Activate Appreciation" %} +

+ + {% csrf_token %} +

{% trans "Select the staff member and department, then activate to trigger AI analysis." %}

+
+
+ + +
+
+ + +
+
+ + +
+
+ - - {% trans "Cancel" %} - -
-
-
-
-{% endif %} + + + {% endif %} - -{% if appreciation.status == 'sent' or appreciation.status == 'acknowledged' %} -
-
-
- -
-

{% trans "Sent" %}

-
-
-
-
-

{% trans "Sent At" %}

-

{{ appreciation.sent_at|date:"Y-m-d H:i" }}

-
-
-

{% trans "To Manager" %}

-

- {% if appreciation.send_to_manager %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} -

-
-
-

{% trans "To Department" %}

-

- {% if appreciation.send_to_department %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} -

-
-
- {% if appreciation.custom_message %} -
-

{% trans "Custom Message" %}

-

{{ appreciation.custom_message }}

-
- {% endif %} - {% if appreciation.cc_list %} -
-

{% trans "CC" %}

-
- {% for email in appreciation.cc_list %} - {{ email }} - {% endfor %} -
-
- {% endif %} - {% if appreciation.acknowledged_at %} -
-
- -

{% trans "Acknowledged on" %} {{ appreciation.acknowledged_at|date:"Y-m-d H:i" }}

-
-
+ {% if can_send %} +
+

+ {% trans "Send Appreciation" %} +

+
+ {% csrf_token %} +

{% trans "Configure recipients and customize the message before sending." %}

+
+
+
+

{% trans "Notify Manager" %}

+
+ +
+
+
+

{% trans "Notify Dept" %}

+
+ +
+
+
+ + +
+
+ + +
+ +
+
{% endif %}
-
-{% endif %} -{% endblock %} + -{% block extra_js %} {% endblock %} diff --git a/templates/appreciation/leaderboard.html b/templates/appreciation/leaderboard.html index 090b3bd..31c34bc 100644 --- a/templates/appreciation/leaderboard.html +++ b/templates/appreciation/leaderboard.html @@ -3,235 +3,298 @@ {% block title %}{% trans "Appreciation Leaderboard" %} - {% endblock %} -{% block content %} -
- - +{% block extra_css %} + +{% endblock %} + +{% block content %} + + + + + - -
-
-
-
- - -
-
- - -
-
- - -
-
- - - - {% trans "Reset" %} - -
-
+ +
+
+
+
+

{% trans "Filters" %}

+
+
+
+ + +
+
+ + +
+
+ + +
+ + + {% trans "Reset" %} + +
+
+
- -
-
- {% if page_obj %} -
- - - - - - - - - - - - - - {% for item in page_obj %} - - - - - - - - - - {% endfor %} - -
Rank{% trans "Recipient" %}{% trans "Hospital" %}{% trans "Department" %}{% trans "Received" %}{% trans "Sent" %}{% trans "Hospital Rank" %}
- {% if forloop.counter <= 3 %} - - {% if forloop.counter == 1 %}{% endif %} - #{{ forloop.counter }} - - {% else %} - #{{ forloop.counter }} - {% endif %} - - - {{ item.get_recipient_name }} - {{ item.hospital.name }}{% if item.department %}{{ item.department.name }}{% else %}-{% endif %} - {{ item.received_count }} - - {{ item.sent_count }} - - {% if item.hospital_rank %} - {% if item.hospital_rank <= 3 %} - - #{{ item.hospital_rank }} - - {% else %} - - #{{ item.hospital_rank }} - - {% endif %} - {% else %} - - - {% endif %} -
-
- - - {% if page_obj.has_other_pages %} - + + {% if page_obj.has_other_pages %} +
+
+ + {% trans "Showing" %} {{ page_obj.start_index }}-{{ page_obj.end_index }} {% trans "of" %} {{ page_obj.paginator.count }} {% trans "entries" %} + +
+
+ {% if page_obj.has_previous %} + + + {% endif %} - {% else %} -
- -

{% trans "No appreciations found for this period" %}

-

{% trans "Try changing the filters or select a different time period" %}

-
- {% endif %} -
-
- -
-
-
-
- -
{% trans "Send Appreciation" %}
-

{% trans "Share your appreciation with colleagues" %}

- - {% trans "View All" %} + {% for num in page_obj.paginator.page_range %} + {% if page_obj.paginator.num_pages <= 7 or num == page_obj.number or num == 1 or num == page_obj.paginator.num_pages %} + + {{ num }} + {% elif num == page_obj.number|add:"-1" or num == page_obj.number|add:"1" %} + {% elif num == 2 and page_obj.number > 4 %} + ... + {% elif num == page_obj.paginator.num_pages|add:"-1" and page_obj.number < page_obj.paginator.num_pages|add:"-3" %} + ... + {% endif %} + {% endfor %} + + {% if page_obj.has_next %} + + + + {% endif %} +
+
+ {% endif %} + {% else %} +
+ +

{% trans "No appreciations found for this period" %}

+

{% trans "Try changing the filters or select a different time period" %}

+
+ {% endif %} +
+ + +
+ +
+
+
+ +
+
+

{% trans "Send Appreciation" %}

+

{% trans "Share your appreciation with colleagues" %}

-
-
-
- -
{% trans "View Badges" %}
-

{% trans "See your earned badges" %}

-
- {% trans "My Badges" %} - + + +
+
+
+ +
+
+

{% trans "View Badges" %}

+

{% trans "See your earned badges" %}

-
- {% endblock %} {% block extra_js %} {{ block.super }} {% endblock %} diff --git a/templates/complaints/complaint_detail.html b/templates/complaints/complaint_detail.html index a8b0c36..43080a1 100644 --- a/templates/complaints/complaint_detail.html +++ b/templates/complaints/complaint_detail.html @@ -1,6 +1,7 @@ {% extends 'layouts/base.html' %} {% load i18n %} {% load static %} +{% get_current_language as LANG %} {% block title %}{{ complaint.reference_number }} - PX360{% endblock %} @@ -80,11 +81,25 @@ {% elif complaint.status == 'resolved' %}bg-green-100 text-green-700 {% elif complaint.status == 'closed' %}bg-slate-100 text-slate-600 {% elif complaint.status == 'cancelled' %}bg-red-100 text-red-700 - {% elif complaint.status == 'contacted' %}bg-purple-100 text-purple-700 - {% elif complaint.status == 'contacted_no_response' %}bg-slate-100 text-slate-600 + {% elif complaint.status == 'pending_external' %}bg-cyan-100 text-cyan-700 + {% elif complaint.status == 'ovr_pending' %}bg-purple-100 text-purple-700 {% else %}bg-slate-100 text-slate-600{% endif %}"> {{ complaint.get_status_display }} + {% if complaint.patient_contact_status == 'contacted' %} + + {% trans "Patient Contacted" %} + + {% elif complaint.patient_contact_status == 'contacted_no_response' %} + + {% trans "No Response" %} + + {% endif %} + {% if not complaint.sent_to_any_department %} + + {% trans "Not Sent to Dept" %} + + {% endif %}
{% if complaint.source_complaint.exists %} {% with sc=complaint.source_complaint.first %} @@ -120,18 +135,6 @@ {% endif %}

{{ complaint.title }}

- {% if complaint.ai_brief_en %} -
- - {{ complaint.ai_brief_en }} - - {% if complaint.ai_brief_ar %} - - {{ complaint.ai_brief_ar }} - - {% endif %} -
- {% endif %}
{% comment %} @@ -163,7 +166,7 @@ {% comment %} {% endcomment %} - + + @@ -217,41 +234,26 @@
- -
-
-

- - {% trans "Complaint Details" %} -

- {% if complaint.source %} - - {% trans "Source:" %} {{ complaint.source.get_localized_name }} - - {% endif %} -
- +
{% if complaint.description %} -
-

- "{{ complaint.description }}" -

+
+

"{{ complaint.description }}"

{% endif %} - -
-
+ +
+

{% trans "Location" %}

- {% if complaint.location %}{{ complaint.location.name_en }}{% else %}-{% endif %} + {% if complaint.legacy_location %}{{ complaint.legacy_location.name_en }}{% else %}-{% endif %}

- {% if complaint.main_section %} -

{{ complaint.main_section.name_en }}{% if complaint.subsection %} > {{ complaint.subsection.name_en }}{% endif %}

+ {% if complaint.legacy_main_section %} +

{{ complaint.legacy_main_section.name_en }}{% if complaint.legacy_subsection %} > {{ complaint.legacy_subsection.name_en }}{% endif %}

{% endif %}
-
+

{% trans "Severity" %}

-

-
-

{% trans "Date Created" %}

-

- {{ complaint.created_at|date:"d M Y, h:i A" }} +

+

{% trans "Classification" %}

+

+ {% if complaint.ai_brief_en %} + {% if LANG == 'ar' and complaint.ai_brief_ar %} + {{ complaint.ai_brief_ar }} + {% else %} + {{ complaint.ai_brief_en }} + {% endif %} + {% else %}-{% endif %}

-
-

{% trans "Response Deadline" %}

+
+

{% trans "Created" %}

+

{{ complaint.created_at|date:"d M Y, h:i A" }}

+
+
+

{% trans "Deadline" %}

{{ complaint.due_at|date:"d M Y, h:i A" }} {% if complaint.is_overdue %} @@ -284,39 +296,35 @@

+ {% if complaint.domain or complaint.category or complaint.subcategory_obj or complaint.classification_obj %} +
+ {% trans "Taxonomy" %}: + {% if complaint.domain %}{{ complaint.domain.get_localized_name }}{% endif %} + {% if complaint.category %}{{ complaint.category.get_localized_name }}{% endif %} + {% if complaint.subcategory_obj %}{{ complaint.subcategory_obj.get_localized_name }}{% endif %} + {% if complaint.classification_obj %}{{ complaint.classification_obj.get_localized_name }}{% endif %} +
+ {% endif %} + + {% if complaint.source %} +
+ {% trans "Source" %}: + {{ complaint.source.get_localized_name }} +
+ {% endif %} + {% if complaint.escalated_at %} -
+

{% trans "Escalated" %}

{% trans "Escalated on" %} {{ complaint.escalated_at|date:"d M Y, h:i A" }}

+
{% endif %} - {# OVR Section - show when activated or has pending OVR request #} - {% if complaint.status != 'closed' and complaint.status != 'cancelled' %} - {% if can_edit %} -
- {% csrf_token %} - -
- {% endif %} - {% endif %} - - {# OVR Pending Approval - Show Approve/Reject buttons for admins #} {% if complaint.status == 'ovr_pending' %} -
+
OVR {% trans "Pending Approval" %} @@ -342,72 +350,27 @@ {% endif %}
{% endif %} - + {% if complaint.expected_result %} -
-

{% trans "Expected Result" %}

+
+

{% trans "Expected Result" %}

{{ complaint.expected_result }}

{% endif %} -
- - -
-

- - {% trans "Classification" %} -

-
- {% if complaint.domain %} -
-

{% trans "Domain" %}

-

{{ complaint.domain.get_localized_name }}

-
- {% endif %} - {% if complaint.category %} -
-

{% trans "Category" %}

-

{{ complaint.category.get_localized_name }}

-
- {% endif %} - {% if complaint.subcategory_obj %} -
-

{% trans "Subcategory" %}

-

{{ complaint.subcategory_obj.get_localized_name }}

-
- {% endif %} - {% if complaint.classification_obj %} -
-

{% trans "Classification" %}

-

{{ complaint.classification_obj.get_localized_name }}

-
+ + {% if complaint.patient %} +
+ + {{ complaint.patient.get_full_name }} + | + {% trans "MRN:" %} {{ complaint.patient.mrn|default:"-" }} + {% if complaint.patient.phone %} + | + {{ complaint.patient.phone }} {% endif %}
+ {% endif %}
- - - {% if complaint.patient %} -
-

- - {% trans "Patient Information" %} -

-
-
-

{% trans "Name" %}

-

{{ complaint.patient.get_full_name }}

-
-
-

{% trans "MRN" %}

-

{{ complaint.patient.mrn|default:"-" }}

-
-
-

{% trans "Phone" %}

-

{{ complaint.patient.phone|default:"-" }}

-
-
-
- {% endif %}
@@ -459,6 +422,14 @@ + + + +
@@ -504,18 +475,41 @@ {% trans "Escalate" %} +
+ {% csrf_token %} + +
{% else %} - + + {% if current_user.is_px_admin or current_user.is_hospital_admin %} +
+ {% csrf_token %} + + +
+ {% else %}

{% trans "Activate this complaint to perform actions" %}

{% endif %} + {% endif %} {% elif complaint.status == 'resolved' or complaint.status == 'closed' %} {% if can_edit %}
@@ -555,7 +549,7 @@ - {% if can_edit and available_transitions %} + {% if can_edit and available_transitions and complaint.assigned_to == current_user %} {% if current_user.is_px_admin or current_user.is_hospital_admin %}

@@ -583,23 +577,18 @@

{% endif %} {% endif %} - - - {% include "partials/stage_timeline.html" with stage_timeline=stage_timeline %} - {% if show_delay_reason_closure or complaint.delay_reason_closure %} + {% if complaint.is_active_status and complaint.delay_reason_closure %}

{% trans "72h Closure Delay Reason" %}

- {% if complaint.delay_reason_closure %}

{{ complaint.get_delay_reason_closure_display }}

- {% endif %} - {% if can_edit and complaint.status != "closed" and complaint.status != "resolved" %} + {% if can_edit %} {% csrf_token %} - - {% for target in escalation_targets %} - + + -

{% trans "If not selected, will escalate to the staff's direct manager." %}

-
- - +
+

{% trans "Email Preview" %} ({% trans "editable" %})

+
+ + +
+
+ + +
-
@@ -845,7 +911,7 @@
-
@@ -764,16 +755,54 @@ document.addEventListener('DOMContentLoaded', function() { const hospitalSelect = document.getElementById('hospitalSelect'); const hospitalField = hospitalSelect || document.querySelector('input[name="hospital"]'); + const locationTypeSelect = document.getElementById('locationTypeSelect'); + const areaSelect = document.getElementById('areaSelect'); const departmentSelect = document.getElementById('departmentSelect'); + const sectionSelect = document.getElementById('sectionSelect'); const staffSelect = document.getElementById('staffSelect'); - const locationSelect = document.getElementById('locationSelect'); - const mainSectionSelect = document.getElementById('mainSectionSelect'); - const subsectionSelect = document.getElementById('subsectionSelect'); + const deptCategorySelect = document.getElementById('id_dept_category'); const complaintTypeCards = document.querySelectorAll('.type-card'); const complaintTypeInput = document.getElementById('complaintTypeInput'); - // Load departments on page load if hospital is already selected - function loadDepartments(hospitalId) { + function getHospitalId() { + if (hospitalSelect) return hospitalSelect.value; + if (hospitalField) return hospitalField.value; + return null; + } + + function loadAreas(hospitalId, locationType) { + if (!areaSelect) return; + areaSelect.innerHTML = ''; + if (!hospitalId) { + areaSelect.innerHTML = ''; + return; + } + let url = '/organizations/dropdowns/areas/?hospital=' + hospitalId; + if (locationType) url += '&location_type=' + locationType; + fetch(url) + .then(r => r.json()) + .then(data => { + areaSelect.innerHTML = ''; + data.forEach(a => { + const opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.name_en; + areaSelect.appendChild(opt); + }); + }) + .catch(err => { + console.error('Error loading areas:', err); + areaSelect.innerHTML = ''; + }); + } + + if (locationTypeSelect) { + locationTypeSelect.addEventListener('change', function() { + loadAreas(getHospitalId(), this.value); + }); + } + + function loadDepartments(hospitalId, category) { if (!hospitalId || !departmentSelect) { if (departmentSelect) { departmentSelect.innerHTML = ''; @@ -783,16 +812,18 @@ document.addEventListener('DOMContentLoaded', function() { departmentSelect.innerHTML = ''; - fetch('/organizations/api/departments/?hospital=' + hospitalId) + var url = '/organizations/dropdowns/departments-by-category/?hospital=' + encodeURIComponent(hospitalId); + if (category) url += '&category=' + encodeURIComponent(category); + + fetch(url) .then(response => response.json()) .then(data => { - const results = data.results || data; departmentSelect.innerHTML = ''; - results.forEach(dept => { + (data || []).forEach(dept => { const option = document.createElement('option'); option.value = dept.id; - option.textContent = {% if LANG == 'ar' %}dept.name_ar || dept.name{% else %}dept.name{% endif %}; + option.textContent = dept.name_en || dept.name; departmentSelect.appendChild(option); }); }) @@ -802,71 +833,61 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Location hierarchy cascading dropdowns - if (locationSelect) { - locationSelect.addEventListener('change', function () { - const locationId = this.value; - if (!locationId) { - if (mainSectionSelect) mainSectionSelect.innerHTML = ''; - if (subsectionSelect) subsectionSelect.innerHTML = ''; - return; - } - if (mainSectionSelect) { - mainSectionSelect.innerHTML = ''; - } - fetch('{% url "organizations:ajax_main_sections" %}?location_id=' + locationId) - .then(r => r.json()) - .then(data => { - const sections = data.sections || []; - if (mainSectionSelect) { - mainSectionSelect.innerHTML = ''; - sections.forEach(section => { - const opt = document.createElement('option'); - opt.value = section.id; - opt.textContent = {% if LANG == 'ar' %}section.name_ar || section.name{% else %}section.name{% endif %}; - mainSectionSelect.appendChild(opt); - }); - } - if (subsectionSelect) { - subsectionSelect.innerHTML = ''; - } - }) - .catch(err => { - console.error('Failed to load main sections:', err); - if (mainSectionSelect) mainSectionSelect.innerHTML = ''; - }); + // Department category → filter departments + if (deptCategorySelect) { + deptCategorySelect.addEventListener('change', function() { + loadDepartments(getHospitalId(), this.value); + if (sectionSelect) sectionSelect.innerHTML = ''; }); } - if (mainSectionSelect) { - mainSectionSelect.addEventListener('change', function () { - const locationId = locationSelect ? locationSelect.value : ''; - const mainSectionId = this.value; - if (!locationId || !mainSectionId) { - if (subsectionSelect) subsectionSelect.innerHTML = ''; + // Department → Section + Staff cascading dropdowns + if (departmentSelect) { + departmentSelect.addEventListener('change', function () { + const deptId = this.value; + if (!deptId) { + if (sectionSelect) sectionSelect.innerHTML = ''; + if (staffSelect) staffSelect.innerHTML = ''; return; } - if (subsectionSelect) { - subsectionSelect.innerHTML = ''; - } - fetch('{% url "organizations:ajax_subsections" %}?location_id=' + locationId + '&main_section_id=' + mainSectionId) - .then(r => r.json()) - .then(data => { - const subsections = data.subsections || []; - if (subsectionSelect) { - subsectionSelect.innerHTML = ''; - subsections.forEach(sub => { + // Load sections + if (sectionSelect) { + sectionSelect.innerHTML = ''; + fetch('/organizations/dropdowns/sections/' + deptId + '/') + .then(r => r.json()) + .then(data => { + sectionSelect.innerHTML = ''; + (data || []).forEach(sec => { const opt = document.createElement('option'); - opt.value = sub.internal_id || sub.id; - opt.textContent = {% if LANG == 'ar' %}sub.name_ar || sub.name{% else %}sub.name{% endif %}; - subsectionSelect.appendChild(opt); + opt.value = sec.id; + opt.textContent = sec.name_en || sec.name; + sectionSelect.appendChild(opt); }); - } - }) - .catch(err => { - console.error('Failed to load subsections:', err); - if (subsectionSelect) subsectionSelect.innerHTML = ''; - }); + }) + .catch(err => { + console.error('Failed to load sections:', err); + sectionSelect.innerHTML = ''; + }); + } + // Load staff + if (staffSelect) { + staffSelect.innerHTML = ''; + fetch('/organizations/dropdowns/staff-by-department/' + deptId + '/') + .then(r => r.json()) + .then(data => { + staffSelect.innerHTML = ''; + (data || []).forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; + opt.textContent = s.name + (s.employee_id ? ' (' + s.employee_id + ')' : ''); + staffSelect.appendChild(opt); + }); + }) + .catch(err => { + console.error('Failed to load staff:', err); + staffSelect.innerHTML = ''; + }); + } }); } diff --git a/templates/complaints/complaint_list.html b/templates/complaints/complaint_list.html index d20e2c9..0725df1 100644 --- a/templates/complaints/complaint_list.html +++ b/templates/complaints/complaint_list.html @@ -295,12 +295,16 @@ {% elif complaint.status == 'resolved' %}bg-green-100 text-green-700 {% elif complaint.status == 'closed' %}bg-slate-100 text-slate-600 {% elif complaint.status == 'cancelled' %}bg-red-100 text-red-700 - {% elif complaint.status == 'contacted' %}bg-purple-100 text-purple-700 - {% elif complaint.status == 'contacted_no_response' %}bg-slate-100 text-slate-600 {% elif complaint.status == 'pending_external' %}bg-cyan-100 text-cyan-700 + {% elif complaint.status == 'ovr_pending' %}bg-purple-100 text-purple-700 {% else %}bg-slate-100 text-slate-600{% endif %}"> {{ complaint.get_status_display }} + {% if complaint.patient_contact_status == 'contacted' %} + {% trans "Contacted" %} + {% elif complaint.patient_contact_status == 'contacted_no_response' %} + {% trans "No Response" %} + {% endif %} {% if complaint.due_at and complaint.status != 'resolved' and complaint.status != 'closed' and complaint.status != 'cancelled' %} @@ -360,72 +364,7 @@ - - {% if complaints.has_other_pages %} -
-
- - {% trans "Showing" %} {{ complaints.start_index }}-{{ complaints.end_index }} {% trans "of" %} {{ complaints.paginator.count }} {% trans "entries" %} - - -
- {% for key, value in request.GET.items %} - {% if key != 'page_size' and key != 'page' %} - - {% endif %} - {% endfor %} - - -
-
-
- {% if complaints.has_previous %} - - - - {% else %} - - - - {% endif %} - - {% for num in complaints.paginator.page_range %} - {% if num == complaints.number %} - {{ num }} - {% elif num > complaints.number|add:'-3' and num < complaints.number|add:'3' %} - - {{ num }} - - {% elif num == 1 or num == complaints.paginator.num_pages %} - - {{ num }} - - {% elif num == complaints.number|add:'-3' or num == complaints.number|add:'3' %} - ... - {% endif %} - {% endfor %} - - {% if complaints.has_next %} - - - - {% else %} - - - - {% endif %} -
-
- {% endif %} + {% include "partials/pagination.html" %}
diff --git a/templates/complaints/complaint_pdf.html b/templates/complaints/complaint_pdf.html index 39dac2b..525dd8b 100644 --- a/templates/complaints/complaint_pdf.html +++ b/templates/complaints/complaint_pdf.html @@ -526,9 +526,9 @@
{% trans "Location" %}
- {% if complaint.location %}{{ complaint.location.name }}{% endif %} - {% if complaint.main_section %} / {{ complaint.main_section.name }}{% endif %} - {% if complaint.subsection %} / {{ complaint.subsection.name }}{% endif %} + {% if complaint.legacy_location %}{{ complaint.legacy_location.name }}{% endif %} + {% if complaint.legacy_main_section %} / {{ complaint.legacy_main_section.name }}{% endif %} + {% if complaint.legacy_subsection %} / {{ complaint.legacy_subsection.name }}{% endif %}
diff --git a/templates/complaints/complaint_summary_pdf.html b/templates/complaints/complaint_summary_pdf.html new file mode 100644 index 0000000..8c5a950 --- /dev/null +++ b/templates/complaints/complaint_summary_pdf.html @@ -0,0 +1,313 @@ + + + + + تقرير الشكوى - {{ complaint.reference_number }} + + + + + +
+ {% include "complaints/partials/pdf_letterhead_header.html" %} +
+ +
+
نموذج الشكوى
+ +
بيانات المشفى
+
+
رقم الملف
+
{{ complaint.reference_number|default:"—" }}
+
+
+
الاسم
+
{{ complainant_name|default:"—" }}
+
+
+
جهة الادعاء
+
{{ source_name|default:"—" }}
+
+
+
تاريخ تقديم الشكوى
+
{{ submission_date }}
+
+
+
تاريخ الحادثة
+
{{ incident_date }}
+
+
+
رقم الحالة
+
{{ complaint.reference_number|default:"—" }}
+
+ +
بيانات المشفى
+
+
القسم
+
{{ department_name|default:"—" }}
+
+
+
اسم الموظف
+
{{ accused_staff_name|default:"—" }}
+
+
+
الوظيفة
+
{{ accused_staff_title|default:"—" }}
+
+
+
تاريخ إرسال الشكوى
+
{{ sent_to_dept_date }}
+
+ +
مختصر الشكوى
+
+ {{ content_summary }} +
+ +
+
قسم علاقات المرضى
+
+
التوقيع والختم
+
+
+ + {% include "complaints/partials/pdf_letterhead_footer.html" %} +
+ + +
+ {% include "complaints/partials/pdf_letterhead_header.html" %} +
+ +
+
نموذج رد الشكوى
+ +
بيانات رد الشكوى
+
+
اسم الموظف
+
{{ accused_staff_name|default:"—" }}
+
+
+
الوظيفة
+
{{ accused_staff_title|default:"—" }}
+
+
+
القسم
+
{{ department_name|default:"—" }}
+
+
+
تاريخ رد الشكوى
+
{{ response_date }}
+
+
+
تاريخ إرسال الشكوى
+
{{ sent_to_dept_date }}
+
+ +
مختصر الرد
+
+ {% if dept_response_summary %} + {{ dept_response_summary }} + {% else %} + لم يتم تسجيل رد من القسم بعد. + {% endif %} +
+ +
+
قسم علاقات المرضى
+
+
التوقيع والختم
+
+
+ + {% include "complaints/partials/pdf_letterhead_footer.html" %} +
+ + + diff --git a/templates/complaints/explanation_already_submitted.html b/templates/complaints/explanation_already_submitted.html index 6a7d4b7..d52d774 100644 --- a/templates/complaints/explanation_already_submitted.html +++ b/templates/complaints/explanation_already_submitted.html @@ -7,9 +7,17 @@ {% trans "Already Submitted" %} - PX360 + diff --git a/templates/complaints/inquiry_explanation_form.html b/templates/complaints/inquiry_explanation_form.html index 530f162..6f90ddd 100644 --- a/templates/complaints/inquiry_explanation_form.html +++ b/templates/complaints/inquiry_explanation_form.html @@ -7,9 +7,17 @@ {% trans "Submit Response" %} - PX360 + diff --git a/templates/complaints/inquiry_form.html b/templates/complaints/inquiry_form.html index d7bd035..9de4710 100644 --- a/templates/complaints/inquiry_form.html +++ b/templates/complaints/inquiry_form.html @@ -113,6 +113,39 @@ {{ form.hospital }} {% endif %} +
+ + {{ form.location_type }} + {% for error in form.location_type.errors %} +

+ {{ error }} +

+ {% endfor %} +
+ +
+ + {{ form.area }} + {% for error in form.area.errors %} +

+ {{ error }} +

+ {% endfor %} +
+ +
+ + +
+
{{ form.department }} @@ -124,9 +157,9 @@
- - {{ form.source }} - {% for error in form.source.errors %} + + {{ form.section }} + {% for error in form.section.errors %}

{{ error }}

@@ -216,38 +249,6 @@
-
-
- - {{ form.location }} - {% for error in form.location.errors %} -

- {{ error }} -

- {% endfor %} -
- -
- - {{ form.main_section }} - {% for error in form.main_section.errors %} -

- {{ error }} -

- {% endfor %} -
- -
- - {{ form.subsection }} - {% for error in form.subsection.errors %} -

- {{ error }} -

- {% endfor %} -
-
-
+ {% endif %} +
+ {% else %} +
+
+ + {% trans "Pending Review" %} + {{ dept.response_submitted_at|date:"M d, Y H:i" }} +
+ {% if dept.response_notes_en %} +

{{ dept.response_notes_en }}

{% endif %}
{% endif %} + {% endif %}
- {% if can_edit %} + {% if can_edit and complaint.is_active_status %}
{% if not dept.response_submitted %} -
- {% csrf_token %} - -
+ {% endif %}
@@ -110,7 +191,7 @@ {% endif %} - {% if can_edit and explanation %} + {% if can_edit and complaint.is_active_status and explanation %}
{% csrf_token %} @@ -30,7 +30,7 @@ {% if explanations %}
- {% if can_edit %} + {% if can_edit and complaint.is_active_status %} @@ -89,6 +89,24 @@ {% if exp.is_used and exp.explanation %} + {% with linked_dept=exp.linked_involved_department %} + {% if linked_dept and linked_dept.manager_review_status == 'pending' %} +
+
+ + {% trans "Pending Manager Review" %} +
+

{% trans "Awaiting department manager approval." %}

+
+ {% elif linked_dept and linked_dept.manager_review_status == 'rejected' %} +
+
+ + {% trans "Rejected by Manager" %} +
+

{% trans "Champion needs to re-submit." %}

+
+ {% else %}

{{ exp.explanation }}

{% if exp.attachment_count > 0 %} @@ -99,9 +117,13 @@ {% endif %}
{% endif %} + {% endwith %} + {% endif %} - {% if can_edit and exp.is_used and not exp.escalated_to_manager %} + {% if can_edit and complaint.is_active_status and exp.is_used and not exp.escalated_to_manager %} + {% with linked_dept=exp.linked_involved_department %} + {% if not linked_dept or linked_dept.manager_review_status == 'approved' %}
{% if exp.acceptance_status == 'pending' %}
@@ -131,9 +153,11 @@ {% endif %}
{% endif %} + {% endwith %} + {% endif %} - {% if can_edit and not exp.is_used %} + {% if can_edit and complaint.is_active_status and not exp.is_used %}
{% if not exp.reminder_sent_at %} @@ -170,7 +194,7 @@

{% trans "No explanation requests sent yet" %}

- {% if can_edit %} + {% if can_edit and complaint.is_active_status %}
{% trans "Send to Department" %} @@ -180,7 +204,7 @@ - diff --git a/templates/complaints/partials/resolution_panel.html b/templates/complaints/partials/resolution_panel.html index 99e497d..8cced6d 100644 --- a/templates/complaints/partials/resolution_panel.html +++ b/templates/complaints/partials/resolution_panel.html @@ -1,6 +1,7 @@ {% load i18n %}

{% trans "Resolution" %}

+ {% if complaint.status == 'resolved' or complaint.status == 'closed' %}
@@ -182,12 +183,11 @@
-

{% trans "Who was in wrong / who was in right?" %}

+

{% trans "who was in right?" %}

diff --git a/templates/complaints/partials/staff_panel.html b/templates/complaints/partials/staff_panel.html index 558eeb0..e6bdb2c 100644 --- a/templates/complaints/partials/staff_panel.html +++ b/templates/complaints/partials/staff_panel.html @@ -2,7 +2,7 @@

{% trans "Involved Staff" %}

- {% if can_edit %} + {% if can_edit and complaint.is_active_status %} {% trans "Add Staff" %} @@ -61,7 +61,7 @@ {% endif %}
- {% if can_edit %} + {% if can_edit and complaint.is_active_status %}
{% if not staff_inv.explanation_received %} @@ -92,7 +92,7 @@

{% trans "No staff members involved yet" %}

- {% if can_edit %} + {% if can_edit and complaint.is_active_status %}
{% trans "Add First Staff" %} diff --git a/templates/complaints/partials/timeline_panel.html b/templates/complaints/partials/timeline_panel.html index 9435ce0..311e3f4 100644 --- a/templates/complaints/partials/timeline_panel.html +++ b/templates/complaints/partials/timeline_panel.html @@ -1,32 +1,45 @@ {% load i18n %}
-

{% trans "Activity Timeline" %}

- - {% if timeline %} -
- {% for update in timeline %} -
-
-
-
- - {{ update.get_update_type_display }} - - {% if update.created_by %} - {{ update.created_by.get_full_name }} - {% endif %} -
- {{ update.created_at|date:"M d, Y H:i" }} -
-

{{ update.message }}

-
-
- {% endfor %} -
- {% else %} +

{% trans "Timeline" %}

+ + {% if not stage_timeline.stages %}

{% trans "No activity recorded yet" %}

+ {% else %} +
+ {% for stage in stage_timeline.stages %} +
+
+ +
+
+
+

{{ stage.label }}

+

{{ stage.timestamp|date:"d M Y, h:i A" }}

+ {% if stage.performed_by %} +

+ {{ stage.performed_by }} +

+ {% endif %} +
+ {% if stage.duration_from_prev %} + + {{ stage.duration_from_prev }} + + {% endif %} +
+
+ {% endfor %} +
+ {% if stage_timeline.total_time %} +
+ {% trans "Total Time" %} + + {{ stage_timeline.total_time }} + +
+ {% endif %} {% endif %}
diff --git a/templates/complaints/patient_complaint_visit_form.html b/templates/complaints/patient_complaint_visit_form.html index 1c6ec80..8e8a7e9 100644 --- a/templates/complaints/patient_complaint_visit_form.html +++ b/templates/complaints/patient_complaint_visit_form.html @@ -38,7 +38,7 @@ transition: all 0.2s; background: white; color: #1e293b; - font-family: 'Inter', sans-serif; + font-family: 'Inter', 'Noto Kufi Arabic', sans-serif; resize: none; } .form-textarea:focus { diff --git a/templates/complaints/public_complaint_form.html b/templates/complaints/public_complaint_form.html index 8b9703b..856d057 100644 --- a/templates/complaints/public_complaint_form.html +++ b/templates/complaints/public_complaint_form.html @@ -5,146 +5,6 @@ {% block extra_css %} {% endblock %} {% block content %} -
-
-
-
- {% trans "Suggestions" %} - - {% trans "Detail" %} -
-

{% trans "Suggestion Detail" %}

-
+
+
+ {% trans "Suggestions" %} + + {{ feedback.title }} + + {{ feedback.get_status_display }} + + + {{ feedback.get_priority_display }} + + {% if feedback.is_featured %} + {% trans "FEATURED" %} + {% endif %} +
+
+

{% trans "Suggestion Detail" %}

{% if can_edit %} - + {% trans "Edit" %} {% endif %} - - {% trans "Back" %} -
-
+ -
-
+ -
-
-
-
- -
-
{{ feedback.title }}
-
-
- - {{ feedback.get_feedback_type_display }} - - - {{ feedback.get_status_display }} - - - {{ feedback.get_priority_display }} - - {% if feedback.is_featured %} - {% trans "FEATURED" %} - {% endif %} - {% if feedback.is_public %} - {% trans "PUBLIC" %} - {% endif %} -
-
-
+
+
+ +
+
+

+ + {% trans "Suggestion Details" %} +

{{ feedback.message }}

@@ -132,13 +97,13 @@
- {% if feedback.location %} + {% if feedback.legacy_location %}
{% trans "Location" %} - {{ feedback.location.name }} - {% if feedback.main_section %} {{ feedback.main_section.name }}{% endif %} - {% if feedback.subsection %} {{ feedback.subsection.name }}{% endif %} + {{ feedback.legacy_location.name }} + {% if feedback.legacy_main_section %} {{ feedback.legacy_main_section.name }}{% endif %} + {% if feedback.legacy_subsection %} {{ feedback.legacy_subsection.name }}{% endif %}
{% else %} @@ -245,18 +210,16 @@ ID: {{ feedback.id|slice:":8" }}
-
+
{% if feedback.has_ai_analysis %} -
-
-
+ -
{% trans "AI Analysis" %}
-
-
+ {% trans "AI Analysis" %} +

{{ feedback.ai_short_description_en }}

@@ -317,26 +280,23 @@
{% endif %} -
+
{% endif %} - +
-
+
{% if can_edit %} -
-
-
- -
-
{% trans "Actions" %}
-
-
+
+

{% trans "Quick Actions" %}

+
{% csrf_token %} - + {% for user in assignable_users %} @@ -367,7 +327,7 @@
- + @@ -379,50 +339,43 @@ {% trans "Create QI Project" %}
- -
- - {% comment %} - {% csrf_token %} - - - - - - {% endcomment %} - -
- - {% comment %}
-
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
{% endcomment %}
-
+ {% endif %} - +
+

+ {% trans "Type" %} +

+ + {{ feedback.get_feedback_type_display }} + + {% if feedback.is_public %} + {% trans "PUBLIC" %} + {% endif %} +
-
+ + + {% endblock %} diff --git a/templates/feedback/feedback_form.html b/templates/feedback/feedback_form.html index de5b262..c839b6d 100644 --- a/templates/feedback/feedback_form.html +++ b/templates/feedback/feedback_form.html @@ -108,30 +108,6 @@
-
- - -
- - - - -
+
+
+ +
-
- - +
+ +
-

+

- {% trans "At least one language is required. This response will be recorded as the department's official response." %} + {% trans "At least one language is required." %}

+
-
- - - {% trans "Cancel" %} - -
+
+ + + {% trans "Cancel" %} +
diff --git a/templates/observations/observation_detail.html b/templates/observations/observation_detail.html index ecbe436..591ff28 100644 --- a/templates/observations/observation_detail.html +++ b/templates/observations/observation_detail.html @@ -6,580 +6,625 @@ {% block extra_css %} {% endblock %} {% block content %} - - + - -
-
-
-
- {{ observation.tracking_code }} - - {{ observation.get_status_display }} - - - {{ observation.get_severity_display }} - - {% if observation.is_anonymous %} - - {% trans "Anonymous" %} - - {% endif %} - {% if observation.is_overdue %} - - {% trans "Overdue" %} - - {% endif %} -
-

- {% if observation.title %} - {{ observation.title }} - {% else %} - {{ observation.description|truncatewords:10 }} - {% endif %} -

-
- -
- {% if can_convert and not observation.action_id %} - - {% trans "Convert to Action" %} - - {% endif %} - - {% trans "Initiate RCA" %} - - - {% trans "Create QI Project" %} - -
+
+
+ {% trans "Observations" %} + + {{ observation.tracking_code }} + + {{ observation.get_status_display }} + + + {{ observation.get_severity_display }} + + {% if observation.is_anonymous %} + + {% trans "Anonymous" %} + + {% endif %} + {% if observation.is_overdue %} + + {% trans "Overdue" %} + + {% endif %} + {% if not observation.sent_to_department %} + + {% trans "Not Sent to Dept" %} + + {% endif %}
-
+
+

+ {% if observation.title %}{{ observation.title }}{% else %}{{ observation.description|truncatewords:10 }}{% endif %} +

+
+ -
- -
- -
-

- - {% trans "Description" %} -

-
-

{{ observation.description }}

-
- {% if observation.description_en %} -
-

English

-

{{ observation.description_en }}

-
- {% endif %} -
+ - - {% include "observations/partials/ai_panel.html" %} +
+
- -
-

- - {% trans "Details" %} -

-
-
-

{% trans "Category" %}

-

{{ observation.category.get_localized_name|default:_("Not specified") }}

+
+
+
+

{{ observation.description }}

- {% if observation.sub_category %} -
-

{% trans "Sub-Category" %}

-

{{ observation.sub_category.get_localized_name }}

+ {% if observation.description_en %} +
+

{{ observation.description_en }}

{% endif %} + + {% if observation.emotion or observation.short_description_en or observation.suggested_actions or observation.suggested_action_en %} +
+
+ + {% trans "AI Analysis" %} +
+ {% if observation.emotion and observation.emotion != 'neutral' %} +
+ + {{ observation.emotion|title }} + + {% trans "Confidence" %}: {{ observation.emotion_confidence_percent|floatformat:0 }}% +
+ {% endif %} + {% if observation.short_description_en %} +

{{ observation.short_description_en }}

+ {% if observation.short_description_ar %} +

{{ observation.short_description_ar }}

+ {% endif %} + {% elif observation.suggested_action_en %} +
+

{{ observation.suggested_action_en }}

+ {% if observation.suggested_action_ar %} +

{{ observation.suggested_action_ar }}

+ {% endif %} +
+ {% elif observation.suggested_actions %} +
+ {% for action in observation.suggested_actions %} +
+ + {{ action.priority }} + +

{{ action.action_en }}

+
+ {% endfor %} +
+ {% endif %} +
+ {% endif %} + +
+
+

{% trans "Category" %}

+

{{ observation.category.get_localized_name|default:"-" }}

+
+
+

{% trans "Location" %}

+

+ {% if observation.legacy_location %}{{ observation.legacy_location.name_en }}{% elif observation.location_text %}{{ observation.location_text }}{% else %}-{% endif %} +

+ {% if observation.legacy_main_section %} +

{{ observation.legacy_main_section.name_en }}{% if observation.legacy_subsection %} > {{ observation.legacy_subsection.name_en }}{% endif %}

+ {% endif %} +
+
+

{% trans "Incident" %}

+

{{ observation.incident_datetime|date:"d M Y" }}

+
+
+

{% trans "Deadline" %}

+

+ {% if observation.due_at %}{{ observation.due_at|date:"d M Y" }}{% else %}-{% endif %} + {% if observation.is_overdue %}{% endif %} +

+
+
+ {% if observation.taxonomy_domain or observation.taxonomy_category or observation.taxonomy_subcategory or observation.taxonomy_classification %} -
-

{% trans "SHCT Taxonomy" %}

-

- {{ observation.taxonomy_domain.name_en|default:"" }} - {% if observation.taxonomy_category %} > {{ observation.taxonomy_category.name_en }}{% endif %} - {% if observation.taxonomy_subcategory %} > {{ observation.taxonomy_subcategory.name_en }}{% endif %} - {% if observation.taxonomy_classification %} > {{ observation.taxonomy_classification.name_en }}{% endif %} -

+
+ {% trans "Taxonomy" %}: + {% if observation.taxonomy_domain %}{{ observation.taxonomy_domain.name_en }}{% endif %} + {% if observation.taxonomy_category %}{{ observation.taxonomy_category.name_en }}{% endif %} + {% if observation.taxonomy_subcategory %}{{ observation.taxonomy_subcategory.name_en }}{% endif %} + {% if observation.taxonomy_classification %}{{ observation.taxonomy_classification.name_en }}{% endif %}
{% endif %} -
-

{% trans "Location" %}

-

- {% if observation.location %}{{ observation.location.name_en }}{% elif observation.location_text %}{{ observation.location_text }}{% else %}{% trans "Not specified" %}{% endif %} -

-
- {% if observation.main_section %} -
-

{% trans "Section" %}

-

{{ observation.main_section.name_en }}

-
- {% endif %} - {% if observation.subsection %} -
-

{% trans "Subsection" %}

-

{{ observation.subsection.name_en }}

-
- {% endif %} -
-

{% trans "Incident Date/Time" %}

-

{{ observation.incident_datetime|date:"M d, Y H:i" }}

-
-
-

{% trans "Submitted" %}

-

{{ observation.created_at|date:"M d, Y H:i" }}

-
-
-

{% trans "Last Updated" %}

-

{{ observation.updated_at|date:"M d, Y H:i" }}

-
- {% if observation.due_at %} -
-

{% trans "Response Deadline" %}

-

- {{ observation.due_at|date:"M d, Y H:i" }} - {% if observation.is_overdue %}{% endif %} -

-
- {% endif %} - {% if observation.triaged_at %} -
-

{% trans "Triaged" %}

-

- {{ observation.triaged_at|date:"M d, Y H:i" }} - {% if observation.triaged_by %}({{ observation.triaged_by.get_full_name }}){% endif %} -

-
- {% endif %} -
-
- -
-

- - {% trans "Reporter Information" %} -

- {% if observation.is_anonymous %} -
-
- +
+ {% if observation.is_anonymous %} +
+ + {% trans "Anonymous submission" %} +
+ {% else %} +
+ + {% if observation.reporter_name %} + {{ observation.reporter_name }} + {% endif %} + {% if observation.reporter_phone %} + | + {{ observation.reporter_phone }} + {% endif %} + {% if observation.reporter_email %} + | + {{ observation.reporter_email }} + {% endif %} + {% if not observation.reporter_name and not observation.reporter_phone and not observation.reporter_email %} + {% trans "Reporter info not available" %} + {% endif %} +
+ {% endif %}
-

{% trans "This observation was submitted anonymously" %}

-
- {% else %} -
- {% if observation.reporter_staff_id %} -
-

{% trans "Staff ID" %}

-

{{ observation.reporter_staff_id }}

-
- {% endif %} - {% if observation.reporter_name %} -
-

{% trans "Name" %}

-

{{ observation.reporter_name }}

-
- {% endif %} - {% if observation.reporter_phone %} -
-

{% trans "Phone" %}

-

{{ observation.reporter_phone }}

-
- {% endif %} - {% if observation.reporter_email %} -
-

{% trans "Email" %}

-

{{ observation.reporter_email }}

-
- {% endif %} -
- {% endif %} -
+
+
- - {% if attachments %} -
-

- - {% trans "Attachments" %} ({{ attachments.count }}) -

-
- {% for attachment in attachments %} -
-
- -
-
{{ attachment.filename }}
-
{{ attachment.file_type }} - {{ attachment.file_size|filesizeformat }}
+ + + + + -
- {% endif %} + {% endif %} + {% if observation.department_response_ar %} +
+

العربية

+
+

{{ observation.department_response_ar|linebreaks }}

+
+
+ {% endif %} + {% if observation.department_response_summary_en %} +
+
+ + {% trans "AI Summary" %} +
+

{{ observation.department_response_summary_en }}

+ {% if observation.department_response_summary_ar %} +

{{ observation.department_response_summary_ar }}

+ {% endif %} +
+ {% endif %} - -
-

- - {% trans "Timeline" %} -

- {% if timeline %} -
- {% for item in timeline %} -
-
-
+ {% if observation.dept_response_acceptance_status %} +
+
- - {% if item.type == 'status_change' %}{% trans "Status Changed" %} - {% elif item.type == 'note' %}{% trans "Note" %}{% endif %} - - {% if item.item.created_by %} - {% trans "by" %} {{ item.item.created_by.get_full_name }} - {% elif item.item.changed_by %} - {% trans "by" %} {{ item.item.changed_by.get_full_name }} + {% if observation.dept_response_acceptance_status == 'acceptable' %} + + {% trans "Accepted" %} + {% elif observation.dept_response_acceptance_status == 'not_acceptable' %} + + {% trans "Not Acceptable" %} + {% else %} + + {% trans "Pending Review" %} + {% endif %} + {% if observation.dept_response_accepted_by %} + by {{ observation.dept_response_accepted_by.get_full_name }} {% endif %}
- {{ item.created_at|date:"M d, Y H:i" }} -
- {% if item.type == 'status_change' %} -
- {% if item.item.from_status %} - {{ item.item.from_status }} - + {% if can_review_dept_response and observation.dept_response_acceptance_status == 'pending' %} +
+
+ {% csrf_token %} + + +
+
+ {% csrf_token %} + + +
+
{% endif %} - {{ item.item.to_status }}
- {% elif item.type == 'note' %} -

{{ item.item.note }}

- {% endif %} - {% if item.item.comment %} -

{{ item.item.comment }}

+ {% if observation.dept_response_acceptance_notes %} +

{{ observation.dept_response_acceptance_notes }}

{% endif %}
+ {% endif %} + {% else %} +
+
+

{% trans "Waiting for department response..." %}

+ {% if observation.dept_response_sla_due_at %} +

{% trans "Deadline:" %} {{ observation.dept_response_sla_due_at|date:"Y-m-d H:i" }}

+ {% endif %} +
+ {% if can_send_reminder %} +
+
+ {% csrf_token %} + + +
+ {% if observation.dept_response_reminder_sent_at %} +
+ {% csrf_token %} + + +
+ {% endif %} +
+ {% endif %} + {% if can_respond_to_department %} +
+ +
+ {% endif %} + {% endif %}
- {% endfor %} -
+
{% else %} -
- -

{% trans "No timeline entries yet" %}

-
+
+
+ +

{% trans "No department assigned to this observation" %}

+
+
{% endif %} - +
+ + + +
- -
- - {% include "partials/stage_timeline.html" with stage_timeline=stage_timeline %} - - -
-
-

- {% trans "Assignment" %} -

-
-
-
-
{% trans "Department" %}
-
{{ observation.assigned_department.name|default:_("Not assigned") }}
-
-
-
{% trans "Assigned To" %}
-
{{ observation.assigned_to.get_full_name|default:_("Not assigned") }}
+
+
+

{% trans "Quick Actions" %}

+
+ {% if observation.status == 'new' and not observation.activated_at %} + {% if can_convert %} +
+ {% csrf_token %} + +
+
+ {% csrf_token %} + + +
+ {% else %} +
+ +

{% trans "Activate this observation to perform actions" %}

+ {% endif %} + {% else %} + {% if can_convert %} + + {% endif %} + {% if can_convert and not observation.action_id %} + + + {% trans "Convert" %} + + {% endif %} + {% if can_send_to_department and not observation.department_responded_at %} + + {% endif %} + {% if can_triage and observation.status != 'closed' and observation.status != 'cancelled' and observation.status != 'rejected' %} + + {% endif %} + {% if can_triage %} + + + {% trans "RCA" %} + + {% endif %} + + + {% trans "QI Project" %} + + {% if observation.status == 'resolved' or observation.status == 'closed' %} + {% if can_triage %} +
+ {% csrf_token %} + + +
+ {% endif %} + {% endif %} + {% if can_delete %} +
+ {% csrf_token %} + +
+ {% endif %} + {% endif %}
- - {% if observation.person_noted or observation.department_noted or observation.communication_method or observation.communication_datetime or observation.patient_file_number %} -
-
-

- {% trans "Communication" %} -

-
-
- {% if observation.patient_file_number %} -
-
{% trans "File Number" %}
-
{{ observation.patient_file_number }}
+ {% if can_convert %} + + {% endif %} + +
+

+ {% trans "Assignment" %} +

+
    +
  • + {% trans "Department" %} + {{ observation.assigned_department.name|default:_("Not assigned") }} +
  • +
  • + {% trans "Assigned To" %} + {{ observation.assigned_to.get_full_name|default:_("Not assigned") }} +
  • +
+
+ + {% if observation.person_noted or observation.department_noted or observation.communication_method or observation.communication_datetime or observation.patient_file_number %} +
+

+ {% trans "Communication" %} +

+
    + {% if observation.patient_file_number %} +
  • + {% trans "File Number" %} + {{ observation.patient_file_number }} +
  • {% endif %} {% if observation.person_noted %} -
    -
    {% trans "Person Noted" %}
    -
    {{ observation.person_noted }}
    -
    +
  • + {% trans "Person Noted" %} + {{ observation.person_noted }} +
  • {% endif %} {% if observation.department_noted %} -
    -
    {% trans "Department Noted" %}
    -
    {{ observation.department_noted.name }}
    -
    +
  • + {% trans "Department Noted" %} + {{ observation.department_noted.name }} +
  • {% endif %} {% if observation.communication_method %} -
    -
    {% trans "Via" %}
    -
    {{ observation.communication_method }}
    -
    +
  • + {% trans "Via" %} + {{ observation.communication_method }} +
  • {% endif %} {% if observation.communication_datetime %} -
    -
    {% trans "Contacted At" %}
    -
    {{ observation.communication_datetime|date:"M d, Y H:i" }}
    -
    +
  • + {% trans "Contacted At" %} + {{ observation.communication_datetime|date:"M d, Y H:i" }} +
  • {% endif %} -
+
{% endif %} - - {% if observation.assigned_department %} -
-
-

- - {% blocktrans with dept=observation.assigned_department.get_localized_name|default:observation.assigned_department.name %}Response from {{ dept }}{% endblocktrans %} - {% if observation.department_responded_at %} - - - {% trans "Received" %} - - {% elif observation.dept_response_is_overdue %} - - - {% trans "OVERDUE" %} - - {% else %} - - - {% trans "Awaiting" %} - - {% endif %} -

-
-
- {% if observation.forwarded_to_dept_at %} -
- - - {% trans "Sent:" %} {{ observation.forwarded_to_dept_at|date:"Y-m-d H:i" }} - - {% if observation.dept_response_sla_due_at and not observation.department_responded_at %} - - - - {% trans "Deadline:" %} {{ observation.dept_response_sla_due_at|date:"Y-m-d H:i" }} - - {% endif %} -
- {% endif %} - - {% if observation.department_responded_at %} -
- - - {{ observation.department_responded_by.get_full_name|default:"-" }} - - - - - {{ observation.department_responded_at|date:"Y-m-d H:i" }} - -
- {% if observation.department_response_en %} -
-

English

-
-

{{ observation.department_response_en|linebreaks }}

-
-
- {% endif %} - {% if observation.department_response_ar %} -
-

العربية

-
-

{{ observation.department_response_ar|linebreaks }}

-
-
- {% endif %} - {% if observation.department_response_summary_en %} -
-
- - {% trans "AI Summary" %} -
-

{{ observation.department_response_summary_en }}

- {% if observation.department_response_summary_ar %} -

{{ observation.department_response_summary_ar }}

- {% endif %} -
- {% endif %} - - - {% if observation.dept_response_acceptance_status %} -
-
-
- {% if observation.dept_response_acceptance_status == 'acceptable' %} - - {% trans "Accepted" %} - {% elif observation.dept_response_acceptance_status == 'not_acceptable' %} - - {% trans "Not Acceptable" %} - {% else %} - - {% trans "Pending Review" %} - {% endif %} - {% if observation.dept_response_accepted_by %} - by {{ observation.dept_response_accepted_by.get_full_name }} - {% endif %} -
- {% if can_review_dept_response and observation.dept_response_acceptance_status == 'pending' %} -
-
- {% csrf_token %} - - -
-
- {% csrf_token %} - - -
-
- {% endif %} -
- {% if observation.dept_response_acceptance_notes %} -

{{ observation.dept_response_acceptance_notes }}

- {% endif %} -
- {% endif %} - - {% else %} -
-
-

{% trans "Waiting for department response..." %}

- {% if observation.dept_response_sla_due_at %} -

- {% trans "Deadline:" %} {{ observation.dept_response_sla_due_at|date:"Y-m-d H:i" }} -

- {% endif %} -
- {% if can_send_reminder %} -
-
- {% csrf_token %} - - -
- {% if observation.dept_response_reminder_sent_at %} -
- {% csrf_token %} - - -
- {% endif %} -
- {% endif %} - {% endif %} - {% if can_respond_to_department %} - - {% endif %} -
-
- {% endif %} - - {% if px_action %}

@@ -592,212 +637,55 @@

{% endif %} - - {% if linked_rcas %} -
-
-

- {% trans "Root Cause Analyses" %} -

-
- -
- {% endif %} - - {% if can_triage %} -
-
-

- {% trans "Triage" %} -

-
-
-
- {% csrf_token %} -
- - {{ triage_form.assigned_department }} -
-
- - {{ triage_form.assigned_to }} -
-
- - {{ triage_form.status }} -
-
- - {{ triage_form.note }} -
- -
-
-
- {% endif %} - - -
-
-

- {% trans "Add Note" %} -

-
-
-
- {% csrf_token %} -
- {{ note_form.note }} -
-
- -
- -
-
-
- - -
-
-

- {% trans "Quick Status Change" %} -

-
-
-
- {% csrf_token %} -
- {{ status_form.status }} -
-
- {{ status_form.comment }} -
- -
-
-
- - {% if observation.status == 'resolved' or observation.status == 'closed' %} - - {% if can_triage %} -
-
-

- {% trans "Reopen Observation" %} -

-
-
-
- {% csrf_token %} -
- - -
- -
-
-
- {% endif %} - {% endif %} - - {% if can_send_to_department and not observation.department_responded_at %} - -
-
- -
-
- {% endif %} - - {% if can_convert %} - -
-
-

- - {% if observation.assigned_to %}{% trans "Reassign" %}{% else %}{% trans "Assign" %}{% endif %} -

-
-
- - {% csrf_token %} -
- - -
- - -
-
- {% endif %} - - {% if can_delete %} -
-
-

- {% trans "Delete" %} -

-
-
-
- {% csrf_token %} - -
-
+
{% endif %}
-
+ + + + {% include "components/send_to_modal.html" with users=assignable_users departments=departments %} +{% include "components/department_response_modal.html" %} {% endblock %} diff --git a/templates/observations/observation_list.html b/templates/observations/observation_list.html index f009316..ff1c31a 100644 --- a/templates/observations/observation_list.html +++ b/templates/observations/observation_list.html @@ -343,16 +343,10 @@ {{ observation.get_status_display }} @@ -398,45 +392,7 @@
- -{% if page_obj.has_other_pages %} -
-
- {% if page_obj.has_previous %} - - - - - - - {% endif %} - - {% for num in page_obj.paginator.page_range %} - {% if page_obj.number == num %} - {{ num }} - {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %} - - {{ num }} - - {% endif %} - {% endfor %} - - {% if page_obj.has_next %} - - - - - - - {% endif %} -
-
-{% endif %} + {% include "partials/pagination.html" %} {% endblock %} {% block extra_js %} diff --git a/templates/observations/partials/observation_timeline_panel.html b/templates/observations/partials/observation_timeline_panel.html new file mode 100644 index 0000000..311e3f4 --- /dev/null +++ b/templates/observations/partials/observation_timeline_panel.html @@ -0,0 +1,45 @@ +{% load i18n %} +
+

{% trans "Timeline" %}

+ + {% if not stage_timeline.stages %} +
+ +

{% trans "No activity recorded yet" %}

+
+ {% else %} +
+ {% for stage in stage_timeline.stages %} +
+
+ +
+
+
+

{{ stage.label }}

+

{{ stage.timestamp|date:"d M Y, h:i A" }}

+ {% if stage.performed_by %} +

+ {{ stage.performed_by }} +

+ {% endif %} +
+ {% if stage.duration_from_prev %} + + {{ stage.duration_from_prev }} + + {% endif %} +
+
+ {% endfor %} +
+ {% if stage_timeline.total_time %} +
+ {% trans "Total Time" %} + + {{ stage_timeline.total_time }} + +
+ {% endif %} + {% endif %} +
diff --git a/templates/observations/public_new.html b/templates/observations/public_new.html index 773223c..4b3cb0c 100644 --- a/templates/observations/public_new.html +++ b/templates/observations/public_new.html @@ -79,6 +79,34 @@
+ +
+ + +
+ + + +
- {{ form.category }} +
- -
+ + + + @@ -110,33 +157,14 @@

{% trans "Please describe what you observed in detail." %}

- +
-
-
- - -
-
- - -
-
- - -
-
+ {{ form.location_text }}
@@ -216,10 +244,6 @@ {% block extra_js %} {% endblock %} diff --git a/templates/observations/public_success.html b/templates/observations/public_success.html index 58374fb..6a976e1 100644 --- a/templates/observations/public_success.html +++ b/templates/observations/public_success.html @@ -140,14 +140,10 @@

{% trans "Status" %}

{{ observation.get_status_display }} diff --git a/templates/observations/public_track.html b/templates/observations/public_track.html index f2a5325..5a56165 100644 --- a/templates/observations/public_track.html +++ b/templates/observations/public_track.html @@ -164,14 +164,10 @@ header.glass-card {

{{ observation.get_status_display }}

{{ observation.status }}
@@ -180,7 +176,7 @@ header.glass-card {
+ style="width: {% if observation.status == 'resolved' or observation.status == 'closed' %}100%{% elif observation.status == 'in_progress' %}50%{% else %}15%{% endif %}">
@@ -206,60 +202,47 @@ header.glass-card {
-
-

-
- -
- {% trans "Resolution Journey" %} -

- - {% if public_timeline %} -
- {% for item in public_timeline %} -
-
-
- -
+ {% if has_response %} + +
+
+

+
+
-
-
-

- {% if item.type == 'status_change' %} - {% trans "Status Updated" %} - {% if item.to_status %} - → {{ item.to_status }} - {% endif %} - {% elif item.type == 'note' %} - {% trans "Update Received" %} - {% else %} - {% trans "Final Resolution" %} - {% endif %} -

- + {% trans "Response" %} +

+
+ {% if response_en %} +
+
+ English
- {% if item.comment %} -
- {{ item.comment|linebreaks }} -
- {% endif %} +
{{ response_en }}
+ {% endif %} + {% if response_ar %} +
+
+ العربية +
+
{{ response_ar }}
+
+ {% endif %}
- {% endfor %}
- {% else %} +
+ {% else %} + +

{% trans "Your observation is being reviewed. Updates will appear here." %}

- {% endif %}
+ {% endif %}
diff --git a/templates/observations/response_already_submitted.html b/templates/observations/response_already_submitted.html new file mode 100644 index 0000000..ea771b2 --- /dev/null +++ b/templates/observations/response_already_submitted.html @@ -0,0 +1,18 @@ +{% extends "template.html" %} +{% load i18n %} + +{% block title %}{% trans "Response Already Submitted" %}{% endblock %} + +{% block content %} +
+
+
+
+ +
+

{% trans "Response Already Submitted" %}

+

{% trans "This response link has already been used." %}

+
+
+
+{% endblock %} diff --git a/templates/observations/response_form_token.html b/templates/observations/response_form_token.html new file mode 100644 index 0000000..7b57cd5 --- /dev/null +++ b/templates/observations/response_form_token.html @@ -0,0 +1,52 @@ +{% extends "template.html" %} +{% load i18n %} + +{% block title %}{% trans "Respond to Observation" %} - {{ observation.tracking_code }}{% endblock %} + +{% block content %} +
+
+
+
+
+ +
+
+

{% trans "Department Response" %}

+

{{ observation.tracking_code }}

+
+
+ +
+

{% trans "Observation Details" %}

+

{{ observation.description }}

+ {% if observation.title %} +

{% trans "Title" %}: {{ observation.title }}

+ {% endif %} +

{% trans "Severity" %}: {{ observation.get_severity_display }}

+
+ + {% if error %} +
+

{{ error }}

+
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+
+
+{% endblock %} diff --git a/templates/observations/response_success_token.html b/templates/observations/response_success_token.html new file mode 100644 index 0000000..b68adb3 --- /dev/null +++ b/templates/observations/response_success_token.html @@ -0,0 +1,19 @@ +{% extends "template.html" %} +{% load i18n %} + +{% block title %}{% trans "Response Submitted" %}{% endblock %} + +{% block content %} +
+
+
+
+ +
+

{% trans "Response Submitted" %}

+

{% trans "Thank you for your response to observation" %} {{ observation.tracking_code }}.

+

{% trans "The PX team will review your response." %}

+
+
+
+{% endblock %} diff --git a/templates/observations/response_token_invalid.html b/templates/observations/response_token_invalid.html new file mode 100644 index 0000000..6904ce5 --- /dev/null +++ b/templates/observations/response_token_invalid.html @@ -0,0 +1,18 @@ +{% extends "template.html" %} +{% load i18n %} + +{% block title %}{% trans "Invalid Link" %}{% endblock %} + +{% block content %} +
+
+
+
+ +
+

{% trans "Invalid Link" %}

+

{% trans "This response link is invalid or has expired." %}

+
+
+
+{% endblock %} diff --git a/templates/organizations/department_complaint_detail.html b/templates/organizations/department_complaint_detail.html new file mode 100644 index 0000000..beaf197 --- /dev/null +++ b/templates/organizations/department_complaint_detail.html @@ -0,0 +1,167 @@ +{% extends "layouts/base.html" %} +{% load i18n %} + +{% block title %}{{ complaint.reference_number }} - {{ department.get_localized_name }} - PX360{% endblock %} + +{% block content %} +
+ + +
+
+
+ +
+
+
+

{{ complaint.reference_number }}

+ + {{ complaint.get_status_display }} + + {% if complaint.severity %} + + {{ complaint.get_severity_display }} + + {% endif %} + {% if complaint.is_overdue %} + + {% trans "Overdue" %} + + {% endif %} +
+

{% trans "Complaint" %}

+
+
+
+ {% if can_respond and involved_dept %} + + {% endif %} + {% if can_manager_review and involved_dept %} + + {% trans "Review Response" %} + + {% endif %} + + {% trans "Back to complaints" %} + +
+
+ + {% if complaint.title %} +

{{ complaint.title }}

+ {% endif %} + +
+
+

+ {% trans "Description" %} +

+

{{ complaint.description|default:"—" }}

+
+ + {% if complaint.ai_brief_en %} +
+

+ {% trans "Short Description" %} +

+

{{ complaint.ai_brief_en }}

+
+ {% endif %} + +
+

+ {% trans "Details" %} +

+
+
+

{% trans "Department" %}

+

{{ complaint.department.get_localized_name|default:"—" }}

+
+
+

{% trans "Assigned To" %}

+

{{ complaint.assigned_to.get_full_name|default:"—" }}

+
+
+

{% trans "Patient MRN" %}

+

{{ complaint.patient.mrn|default:"—" }}

+
+ {% if staff_list %} +
+

{% trans "Staff Involved" %}

+ {% for s in staff_list %} +

{{ s }}

+ {% endfor %} +
+ {% endif %} + {% if taxonomy %} +
+

{% trans "Classification" %}

+

{{ taxonomy|join:" > " }}

+
+ {% endif %} + {% if location_str %} +
+

{% trans "Location" %}

+

{{ location_str }}

+
+ {% endif %} +
+

{% trans "Source" %}

+

{{ complaint.source.name_en|default:"—" }}

+
+
+

{% trans "Created" %}

+

{{ complaint.created_at|date:"Y-m-d H:i" }}

+
+ {% if complaint.due_at %} +
+

{% trans "SLA Deadline" %}

+

+ {{ complaint.due_at|date:"Y-m-d H:i" }} + {% if complaint.is_overdue %} ({% trans "Overdue" %}){% endif %} +

+
+ {% endif %} + {% if complaint.escalated_at %} +
+

{% trans "Escalated" %}

+

{{ complaint.escalated_at|date:"Y-m-d H:i" }}

+
+ {% endif %} +
+
+ + {% if complaint.expected_result %} +
+

+ {% trans "Expected Result" %} +

+

{{ complaint.expected_result }}

+
+ {% endif %} +
+
+ +{% include "components/department_response_modal.html" %} +{% endblock %} diff --git a/templates/organizations/department_complaints.html b/templates/organizations/department_complaints.html new file mode 100644 index 0000000..db377da --- /dev/null +++ b/templates/organizations/department_complaints.html @@ -0,0 +1,213 @@ +{% extends "layouts/base.html" %} +{% load i18n %} +{% load analytics_extras %} + +{% block title %}{{ department.get_localized_name }} - {% trans "Complaints" %} - PX360{% endblock %} + +{% block content %} +
+
+ {% trans "Departments" %} + + {{ department.get_localized_name }} + + {% trans "Complaints" %} +
+ +
+
+

{% trans "Department Complaints" %}

+

{{ department.get_localized_name }} · {{ department.hospital.name }}

+
+
+ +
+
+
+ + +
+ + + + + + {% if search_query or status_filter or severity_filter or date_from or date_to %} + + {% trans "Clear" %} + + {% endif %} +
+
+ + {% include "partials/list_stats_bar.html" %} + +
+ + + + + + + + + + + + + {% if can_respond %} + + {% endif %} + + + + {% for c in complaints %} + + + + + + + + + + + {% if can_respond %} + + {% endif %} + + {% empty %} + + {% endfor %} + +
{% trans "Reference" %}{% trans "Classification" %}{% trans "Staff" %}{% trans "Patient" %}{% trans "Assigned To" %}{% trans "Status" %}{% trans "Severity" %}{% trans "SLA" %}{% trans "Created" %}{% trans "Action" %}
{{ c.reference_number }} +
+ {% if c.domain %}{{ c.domain.get_localized_name }}{% endif %} + {% if c.category %}{{ c.category.get_localized_name }}{% endif %} + {% if c.subcategory_obj %}{{ c.subcategory_obj.get_localized_name }}{% endif %} +
+
+ {% if c.involved_staff.all %} + {% for inv in c.involved_staff.all %} +
{{ inv.staff.get_localized_name }}
+ {% endfor %} + {% elif c.staff %} +
{{ c.staff.get_localized_name }}
+ {% else %} + - + {% endif %} +
{{ c.patient.mrn|default:"-" }}{{ c.assigned_to.get_full_name|default:"-" }} + + {{ c.get_status_display }} + + + + {{ c.get_severity_display }} + + + {% if c.due_at and c.status != 'resolved' and c.status != 'closed' %} + + {% elif c.is_overdue %} + {% trans "Overdue" %} + {% else %} + - + {% endif %} + {{ c.created_at|date:"Y-m-d" }} + {% with idept=complaint_dept_map|get_item:c.pk %} + {% if idept %} + {% if idept.response_submitted and idept.manager_review_status == 'pending' %} + + {% trans "Pending Manager Review" %} + + {% else %} + + {% endif %} + {% else %} + ✓ {% trans "Submitted" %} + {% endif %} + {% endwith %} +
{% trans "No complaints found" %}
+
+ + {% include "partials/pagination.html" %} +
+ +{% if can_respond %} +{% include "components/department_response_modal.html" %} +{% endif %} + + +{% endblock %} diff --git a/templates/organizations/department_detail.html b/templates/organizations/department_detail.html index c926e78..a365987 100644 --- a/templates/organizations/department_detail.html +++ b/templates/organizations/department_detail.html @@ -14,6 +14,7 @@ .stat-card { background: white; border-radius: 1rem; border: 2px solid #e2e8f0; padding: 1.25rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: all 0.3s ease; + text-decoration: none; color: inherit; } .stat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 15px rgba(0,0,0,0.1); border-color: #005696; } .tab-btn { padding: 0.75rem 1.5rem; font-size: 0.875rem; font-weight: 600; border-bottom: 3px solid transparent; transition: all 0.2s; } @@ -76,58 +77,6 @@ {% endif %}
-
-
-

{% trans "Manager" %}

- {% if department.manager %} -

{{ department.manager.get_full_name }}

- {% else %} -

{% trans "Not assigned" %}

- {% endif %} -
- {% if department.manager %} -
- -
- {% elif can_edit %} - - {% endif %} - {% if can_edit %} - - {% endif %} -
-
-
-

{% trans "Champion" %}

- {% if department.respondent %} -

{{ department.respondent.get_full_name }}

- {% else %} -

{% trans "Not set" %}

- {% endif %} -
- {% if department.respondent %} -
- -
- {% elif can_assign %} - - {% endif %} - {% if can_assign %} - - {% endif %} -
{% if can_edit %} @@ -148,7 +97,7 @@
- - - +
{% if pending_actions %} @@ -235,7 +184,21 @@ {% if action.type == 'complaint_department_response' %} + {% elif action.type == 'observation_response' %} + + {% elif action.type == 'inquiry_response' %} +
+ {% if pending_actions_has_more %} +
+ {% trans "Showing 10 of" %} {{ pending_actions_count }} — {% trans "respond to items to see more" %} +
+ {% endif %}
{% endif %} @@ -278,11 +246,17 @@ - {% if not user.is_department_respondent or user.is_px_admin or user.is_hospital_admin or user.is_department_manager %} + {% if can_manage_roles %} + {% endif %} + @@ -292,6 +266,12 @@ + + @@ -338,7 +318,27 @@
- {% if search_query or complaint_status_filter or inquiry_status_filter or observation_status_filter %} + + + {% if search_query or complaint_status_filter or inquiry_status_filter or observation_status_filter or suggestion_status_filter or appreciation_status_filter %} {% trans "Clear" %} @@ -349,7 +349,6 @@
- {% if not user.is_department_respondent or user.is_px_admin or user.is_hospital_admin or user.is_department_manager %}
@@ -365,7 +364,8 @@ {% for s in staff_list %} - +
@@ -409,8 +409,130 @@
+ + + {% if can_manage_roles %} +
+
+
+
+

+ {% trans "Department Roles" %} +

+

{% trans "Manage who receives complaints, inquiries, and observations for this department." %}

+
+
+
+ {% for role in role_data %} +
+
+
+
+ +
+
+

{{ role.label }}

+ {% if role.field == 'champion' %} + {% trans "Primary Contact" %} + {% endif %} +
+
+
+
+ {% if role.staff %} +
+
+ {{ role.staff.get_full_name|first|upper }} +
+
+

{{ role.staff.get_full_name }}

+

{{ role.staff.email|default:"No email" }}

+
+
+ {% else %} +
+ + {% trans "Not assigned" %} +
+ {% endif %} +
+ {% if can_manage_roles %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+
+
{% endif %} + +
+
+
+

{% trans "Department Sections" %}

+ + {% trans "Add Section" %} + +
+ + + + + + + + + + + + + + + {% for sec in org_sections %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
{% trans "Section Name" %}{% trans "Location" %}{% trans "Champion" %}{% trans "Supervisor" %}{% trans "Deputy" %}{% trans "Sub-Sections" %}{% trans "Status" %}{% trans "Actions" %}
+ {{ sec.name_en }} + {% if sec.name_ar %}
{{ sec.name_ar }}{% endif %} +
{{ sec.location_type|default:"-" }}{% if sec.sub_location %} / {{ sec.sub_location }}{% endif %}{{ sec.champion.get_full_name|default:"-" }}{{ sec.supervisor.get_full_name|default:"-" }}{{ sec.deputy_supervisor.get_full_name|default:"-" }} + {{ sec.subsections.count }} + + {% if sec.status == 'active' %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + +
+ + + +
+
{% trans "No sections in this department" %}
+
+
+
@@ -427,7 +549,7 @@ {% for c in complaints %} - +
{{ c.reference_number }}
@@ -468,31 +590,19 @@ {% endfor %}
+ {% if stats.total_complaints > 0 %} + + {% endif %}
- +
- {% if user.is_department_respondent and not user.is_px_admin and not user.is_hospital_admin and not user.is_department_manager %} -
- {% for i in inquiries %} -
-

{{ i.message }}

- {% if i.status in 'open,in_progress' %} - - {% endif %} -
- {% empty %} -
- -

{% trans "No pending inquiries" %}

-
- {% endfor %} -
- {% else %} @@ -501,6 +611,7 @@ + @@ -520,13 +631,14 @@ {{ i.get_status_display }} + {% empty %} - + {% endfor %}
{% trans "Contact" %} {% trans "Assigned To" %} {% trans "Status" %}{% trans "Created" %} {% trans "Actions" %}
{{ i.created_at|date:"Y-m-d" }}
- + {% trans "View" %} {% if can_respond and i.status in 'open,in_progress' %} - + {% trans "Respond" %} {% endif %} @@ -534,10 +646,18 @@
{% trans "No inquiries for this department" %}
{% trans "No inquiries for this department" %}
+ {% if stats.total_inquiries > 0 %} + {% endif %}
@@ -552,11 +672,12 @@ {% trans "Assigned To" %} {% trans "Status" %} {% trans "Created" %} + {% trans "Actions" %} {% for o in observations %} - + {{ o.tracking_code }} {{ o.title|default:"-" }} @@ -575,9 +696,144 @@ {{ o.created_at|date:"Y-m-d" }} + +
+ + {% trans "View" %} + + {% if can_respond and not o.department_responded_at %} + + {% trans "Respond" %} + + {% endif %} +
+ {% empty %} - {% trans "No observations for this department" %} + {% trans "No observations for this department" %} + {% endfor %} + + + {% if stats.total_observations > 0 %} + + {% endif %} +
+ + +
+ + + + + + + + + + + + + + + {% for s in suggestions %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
{% trans "Reference" %}{% trans "Title" %}{% trans "Priority" %}{% trans "Category" %}{% trans "Contact" %}{% trans "Assigned To" %}{% trans "Status" %}{% trans "Created" %}
{{ s.pk|truncatechars:8 }}{{ s.title|default:"-" }} + + {{ s.get_priority_display }} + + {{ s.get_category_display|default:"-" }} + {% if s.is_anonymous %} + {% trans "Anonymous" %} + {% else %} + {{ s.contact_name|default:"-" }} + {% endif %} + {{ s.assigned_to.get_full_name|default:"-" }} + + {{ s.get_status_display }} + + {{ s.created_at|date:"Y-m-d" }}
{% trans "No suggestions for this department" %}
+
+ + +
+ + + + + + + + + + + + + + {% for a in appreciations %} + + + + + + + + + + {% empty %} + {% endfor %}
{% trans "Reference" %}{% trans "Category" %}{% trans "Sender" %}{% trans "Recipient" %}{% trans "Status" %}{% trans "Sent" %}{% trans "Created" %}
{{ a.pk|truncatechars:8 }} + {% if a.category %} + + {{ a.category.name_en }} + + {% else %} + - + {% endif %} + + {% if a.is_anonymous %} + {% trans "Anonymous" %} + {% elif a.sender %} + {{ a.sender.get_full_name }} + {% else %} + - + {% endif %} + {{ a.get_recipient_name|default:"-" }} + + {{ a.get_status_display }} + + {{ a.sent_at|date:"Y-m-d"|default:"-" }}{{ a.created_at|date:"Y-m-d" }}
{% trans "No appreciations for this department" %}
@@ -661,163 +917,132 @@
+ + +
+
+ + + + + + + + + + + + {% for item in standards_data %} + + + + + + + + {% empty %} + + {% endfor %} + +
{% trans "Code" %}{% trans "Title" %}{% trans "Activity Type" %}{% trans "Status" %}{% trans "Evidence" %}
+ + {{ item.standard.code }} + + + + {{ item.standard.title }} + + + {% if item.standard.activity_type %} + + {{ item.standard.activity_type.name }} + + {% else %} + - + {% endif %} + + {% if not item.standard.is_assessable or item.standard.is_heading %} + + {% trans "Informational" %} + + {% else %} + + + {% endif %} + + {% if not item.standard.is_assessable or item.standard.is_heading %} + - + {% else %} + + {% endif %} +
+ + {% trans "No standards for this department" %} +
+
+
- -
-
- - - - - - - - - - - - {% for item in standards_data %} - - - - - - - - {% empty %} - - {% endfor %} - -
{% trans "Code" %}{% trans "Title" %}{% trans "Activity Type" %}{% trans "Status" %}{% trans "Evidence" %}
- - {{ item.standard.code }} - - - - {{ item.standard.title }} - - - {% if item.standard.activity_type %} - - {{ item.standard.activity_type.name }} - - {% else %} - - - {% endif %} - - {% if not item.standard.is_assessable or item.standard.is_heading %} - - {% trans "Informational" %} - - {% else %} - - - {% endif %} - - {% if not item.standard.is_assessable or item.standard.is_heading %} - - - {% else %} - - {% endif %} -
- - {% trans "No standards for this department" %} -
-
-
- - - +{% endblock %} diff --git a/templates/organizations/orgsection_confirm_delete.html b/templates/organizations/orgsection_confirm_delete.html new file mode 100644 index 0000000..80a3e85 --- /dev/null +++ b/templates/organizations/orgsection_confirm_delete.html @@ -0,0 +1,56 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{% trans "Delete Section" %} - PX360{% endblock %} + +{% block content %} +
+
+
+
+ +
+ +

{% trans "Delete Section" %}

+

+ {% blocktrans %}Are you sure you want to delete {{ section.name_en }}? This action cannot be undone.{% endblocktrans %} +

+ +
+
+
+ {% trans "Name" %} + {{ section.name_en }} +
+
+ {% trans "Code" %} + {{ section.code|default:"-" }} +
+
+ {% trans "Department" %} + {{ section.department.get_localized_name }} +
+
+ {% trans "Status" %} + + {% if section.status == 'active' %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %} + +
+
+
+ +
+ {% csrf_token %} +
+ + {% trans "Cancel" %} + + +
+
+
+
+
+{% endblock %} diff --git a/templates/organizations/orgsection_detail.html b/templates/organizations/orgsection_detail.html new file mode 100644 index 0000000..b0ea710 --- /dev/null +++ b/templates/organizations/orgsection_detail.html @@ -0,0 +1,191 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{{ section.name_en }} - {% trans "Org Section" %} - PX360{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+
+ +
+
+

{{ section.name_en }}

+ {% if section.name_ar %} +

{{ section.name_ar }}

+ {% endif %} +
+
+
+ {% if section.code %} + {{ section.code }} + {% endif %} + {{ section.get_location_type_display }} + {% if section.sub_location %} + {{ section.sub_location }} + {% endif %} + {% if section.floor %} + {% trans "Floor" %} {{ section.floor }} + {% endif %} + {{ section.department.get_localized_name }} +
+
+ +
+
+ + +
+
+
+
+ +
+
+

{% trans "Sub-Sections" %}

+

{{ sub_sections_count }}

+
+
+
+
+ + +
+
+
+
+ +
+

{% trans "Sub-Sections" %}

+
+ + {% trans "Add Sub-Section" %} + +
+
+ + + + + + + + + + + + {% for sub in sub_sections %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name (EN)" %}{% trans "Name (AR)" %}{% trans "Code" %}{% trans "Status" %}{% trans "Actions" %}
+ {{ sub.name_en }} + + {{ sub.name_ar|default:"-" }} + + {{ sub.code|default:"-" }} + + {% if sub.status == 'active' %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + +
+ +

{% trans "No sub-sections found" %}

+
+
+
+
+{% endblock %} diff --git a/templates/organizations/orgsection_form.html b/templates/organizations/orgsection_form.html new file mode 100644 index 0000000..7b1dde9 --- /dev/null +++ b/templates/organizations/orgsection_form.html @@ -0,0 +1,157 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{% if section %}{% trans "Edit Section" %}{% else %}{% trans "Add Section" %}{% endif %} - PX360{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+ +

+ {% if section %}{% trans "Edit Section" %}{% else %}{% trans "Add Section" %}{% endif %} +

+

+ {% if section %}{% trans "Update section information" %}{% else %}{% trans "Create a new org section" %}{% endif %} +

+
+ +
+
+
+ {% csrf_token %} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + {% trans "Cancel" %} + + +
+
+
+
+{% endblock %} diff --git a/templates/organizations/orgsection_list.html b/templates/organizations/orgsection_list.html new file mode 100644 index 0000000..1b42fe7 --- /dev/null +++ b/templates/organizations/orgsection_list.html @@ -0,0 +1,214 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{% trans "Org Sections" %} - PX360{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+
+
+

{% trans "Org Sections" %}

+

{% trans "Manage organization sections" %}

+
+ + {% trans "Add Section" %} + +
+
+ + +
+
+
+ +
+

{% trans "Filters" %}

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + +
+
+
+
+
+ + +
+
+
+ +
+

{% trans "Sections List" %}

+
+
+ + + + + + + + + + + + + + + {% for section in sections %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name (EN)" %}{% trans "Name (AR)" %}{% trans "Code" %}{% trans "Department" %}{% trans "Location" %}{% trans "Champion" %}{% trans "Status" %}{% trans "Actions" %}
+ {{ section.name_en }} + + {{ section.name_ar|default:"-" }} + + {{ section.code|default:"-" }} + + + {{ section.department.get_localized_name }} + + + {{ section.get_location_type_display|default:"-" }} + + {{ section.champion.get_full_name|default:"-" }} + + {% if section.status == 'active' %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + +
+ +

{% trans "No sections found" %}

+
+
+ + + {% if page_obj.has_other_pages %} +
+
+ {% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}{{ current }} of {{ total }}{% endblocktrans %} +
+
+ {% if page_obj.has_previous %} + + + + {% endif %} + + + {{ page_obj.number }} + + + {% if page_obj.has_next %} + + + + {% endif %} +
+
+ {% endif %} +
+{% endblock %} diff --git a/templates/organizations/orgsubsection_confirm_delete.html b/templates/organizations/orgsubsection_confirm_delete.html new file mode 100644 index 0000000..addd574 --- /dev/null +++ b/templates/organizations/orgsubsection_confirm_delete.html @@ -0,0 +1,56 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{% trans "Delete Sub-Section" %} - PX360{% endblock %} + +{% block content %} +
+
+
+
+ +
+ +

{% trans "Delete Sub-Section" %}

+

+ {% blocktrans %}Are you sure you want to delete {{ sub_section.name_en }}? This action cannot be undone.{% endblocktrans %} +

+ +
+
+
+ {% trans "Name" %} + {{ sub_section.name_en }} +
+
+ {% trans "Code" %} + {{ sub_section.code|default:"-" }} +
+
+ {% trans "Parent Section" %} + {{ sub_section.section.name_en }} +
+
+ {% trans "Status" %} + + {% if sub_section.status == 'active' %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %} + +
+
+
+ +
+ {% csrf_token %} +
+ + {% trans "Cancel" %} + + +
+
+
+
+
+{% endblock %} diff --git a/templates/organizations/orgsubsection_form.html b/templates/organizations/orgsubsection_form.html new file mode 100644 index 0000000..59cd29e --- /dev/null +++ b/templates/organizations/orgsubsection_form.html @@ -0,0 +1,167 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{% if sub_section %}{% trans "Edit Sub-Section" %}{% else %}{% trans "Add Sub-Section" %}{% endif %} - PX360{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+
+ {% if sub_section %} + + {% trans "Back" %} + + {% elif preselected_section %} + + {% trans "Back" %} + + {% else %} + + {% trans "Back" %} + + {% endif %} +
+

+ {% if sub_section %}{% trans "Edit Sub-Section" %}{% else %}{% trans "Add Sub-Section" %}{% endif %} +

+

+ {% if sub_section %}{% trans "Update sub-section information" %}{% else %}{% trans "Create a new sub-section" %}{% endif %} +

+
+ +
+
+
+ {% csrf_token %} + +
+
+ + {% if sub_section %} + + + {% else %} + + {% endif %} +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ {% if sub_section %} + + + {% trans "Cancel" %} + + {% elif preselected_section %} + + + {% trans "Cancel" %} + + {% else %} + + + {% trans "Cancel" %} + + {% endif %} + +
+
+
+
+{% endblock %} diff --git a/templates/organizations/orgsubsection_list.html b/templates/organizations/orgsubsection_list.html new file mode 100644 index 0000000..867e035 --- /dev/null +++ b/templates/organizations/orgsubsection_list.html @@ -0,0 +1,207 @@ +{% extends 'layouts/base.html' %} +{% load i18n %} + +{% block title %}{% trans "Org Sub-Sections" %} - PX360{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+
+
+

{% trans "Org Sub-Sections" %}

+

{% trans "Manage organization sub-sections" %}

+
+ + {% trans "Add Sub-Section" %} + +
+
+ + +
+
+
+ +
+

{% trans "Filters" %}

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + +
+
+
+
+
+ + +
+
+
+ +
+

{% trans "Sub-Sections List" %}

+
+
+ + + + + + + + + + + + + + {% for sub in sub_sections %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Name (EN)" %}{% trans "Name (AR)" %}{% trans "Code" %}{% trans "Parent Section" %}{% trans "Department" %}{% trans "Status" %}{% trans "Actions" %}
+ {{ sub.name_en }} + + {{ sub.name_ar|default:"-" }} + + {{ sub.code|default:"-" }} + + + {{ sub.section.name_en }} + + + {{ sub.section.department.get_localized_name }} + + {% if sub.status == 'active' %} + {% trans "Active" %} + {% else %} + {% trans "Inactive" %} + {% endif %} + + +
+ +

{% trans "No sub-sections found" %}

+
+
+ + + {% if page_obj.has_other_pages %} +
+
+ {% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}{{ current }} of {{ total }}{% endblocktrans %} +
+
+ {% if page_obj.has_previous %} + + + + {% endif %} + + + {{ page_obj.number }} + + + {% if page_obj.has_next %} + + + + {% endif %} +
+
+ {% endif %} +
+{% endblock %} diff --git a/templates/organizations/patient_visit_journey.html b/templates/organizations/patient_visit_journey.html index f2b37de..027152e 100644 --- a/templates/organizations/patient_visit_journey.html +++ b/templates/organizations/patient_visit_journey.html @@ -203,7 +203,7 @@
- +{{ item.duration_display }} + {{ item.duration_display }}
diff --git a/templates/organizations/staff_detail.html b/templates/organizations/staff_detail.html index 61008ad..3924abf 100644 --- a/templates/organizations/staff_detail.html +++ b/templates/organizations/staff_detail.html @@ -271,10 +271,6 @@ {% trans "Closed" %} {% elif complaint.status == 'cancelled' %} {% trans "Cancelled" %} - {% elif complaint.status == 'contacted' %} - {% trans "Contacted" %} - {% elif complaint.status == 'contacted_no_response' %} - {% trans "No Response" %} {% else %} {{ complaint.get_status_display }} {% endif %} @@ -504,7 +500,15 @@

{% trans "Create a user account for" %} {{ staff.get_full_name }}?

-

{% trans "Credentials will be emailed to" %} {{ staff.email }}.

+

{% trans "Credentials will be emailed to" %} {{ staff.email }}.

+ +
+ + +
@@ -524,7 +528,7 @@

{% trans "Send invitation email to" %} {{ staff.get_full_name }}?

-

{% trans "A new password will be generated and emailed." %}

+

{% trans "A secure password reset link will be emailed." %}

@@ -558,41 +562,27 @@
- - - +
-
-
- - -
- - {{ form.subsection }} - {% if form.subsection.errors %} -
- - {% for error in form.subsection.errors %}{{ error }}{% endfor %} + {% for error in form.section.errors %}{{ error }}{% endfor %}
{% endif %}
@@ -294,83 +279,43 @@ document.addEventListener('DOMContentLoaded', function() { incidentDateInput.setAttribute('max', today); } - // Cascading dropdowns for Location → Section → Subsection - const locationSelect = document.getElementById('{{ form.location.id_for_label }}'); - const mainSectionSelect = document.getElementById('{{ form.main_section.id_for_label }}'); - const subsectionSelect = document.getElementById('{{ form.subsection.id_for_label }}'); + // Cascading dropdowns for Department → Section + const departmentSelect = document.getElementById('{{ form.department.id_for_label }}'); + const sectionSelect = document.getElementById('{{ form.section.id_for_label }}'); - if (locationSelect && mainSectionSelect) { - locationSelect.addEventListener('change', function() { - const locationId = this.value; + if (departmentSelect && sectionSelect) { + departmentSelect.addEventListener('change', function() { + const deptId = this.value; - mainSectionSelect.innerHTML = ''; - subsectionSelect.innerHTML = ''; - mainSectionSelect.disabled = true; - subsectionSelect.disabled = true; + sectionSelect.innerHTML = ''; + sectionSelect.disabled = true; - if (locationId) { - mainSectionSelect.innerHTML = ''; - fetch('{% url "organizations:ajax_main_sections" %}?location_id=' + locationId) + if (deptId) { + sectionSelect.innerHTML = ''; + fetch('/organizations/api/sections/?department=' + deptId) .then(r => r.json()) .then(data => { - const sections = data.sections || []; - mainSectionSelect.innerHTML = ''; - sections.forEach(section => { + const sections = data.results || data; + sectionSelect.innerHTML = ''; + sections.forEach(sec => { const opt = document.createElement('option'); - opt.value = section.id; - opt.textContent = section.name; - mainSectionSelect.appendChild(opt); + opt.value = sec.id; + opt.textContent = sec.name_en || sec.name || sec.display_name; + sectionSelect.appendChild(opt); }); if (sections.length > 0) { - mainSectionSelect.disabled = false; + sectionSelect.disabled = false; } }) .catch(error => { console.error('Error loading sections:', error); - mainSectionSelect.innerHTML = ''; + sectionSelect.innerHTML = ''; }); } }); - if (locationSelect.value) { - locationSelect.dispatchEvent(new Event('change')); - } - } - - if (mainSectionSelect && subsectionSelect) { - mainSectionSelect.addEventListener('change', function() { - const locationId = locationSelect ? locationSelect.value : ''; - const sectionId = this.value; - - subsectionSelect.innerHTML = ''; - subsectionSelect.disabled = true; - - if (locationId && sectionId) { - subsectionSelect.innerHTML = ''; - fetch('{% url "organizations:ajax_subsections" %}?location_id=' + locationId + '&main_section_id=' + sectionId) - .then(r => r.json()) - .then(data => { - const subsections = data.subsections || []; - subsectionSelect.innerHTML = ''; - subsections.forEach(sub => { - const opt = document.createElement('option'); - opt.value = sub.id; - opt.textContent = sub.name; - subsectionSelect.appendChild(opt); - }); - if (subsections.length > 0) { - subsectionSelect.disabled = false; - } - }) - .catch(error => { - console.error('Error loading subsections:', error); - subsectionSelect.innerHTML = ''; - }); - } - }); - - if (mainSectionSelect.value) { - mainSectionSelect.dispatchEvent(new Event('change')); + if (departmentSelect.value) { + departmentSelect.dispatchEvent(new Event('change')); } } }); diff --git a/templates/px_sources/source_user_create_inquiry.html b/templates/px_sources/source_user_create_inquiry.html index 22eb346..7c05753 100644 --- a/templates/px_sources/source_user_create_inquiry.html +++ b/templates/px_sources/source_user_create_inquiry.html @@ -117,27 +117,13 @@
- +
-
- -
- - {{ form.main_section }} -
- -
- - {{ form.subsection }} + {{ form.section }}
@@ -194,27 +180,21 @@ document.addEventListener('DOMContentLoaded', function() { lucide.createIcons(); - const locationSelect = document.getElementById('{{ form.location.id_for_label }}'); - const sectionSelect = document.getElementById('{{ form.main_section.id_for_label }}'); - const subsectionSelect = document.getElementById('{{ form.subsection.id_for_label }}'); + const deptSelect = document.getElementById('id_department'); + const sectionSelect = document.getElementById('id_section'); - if (locationSelect) { - locationSelect.addEventListener('change', function() { - const locationId = this.value; + if (deptSelect && sectionSelect) { + deptSelect.addEventListener('change', function() { + const deptId = this.value; sectionSelect.innerHTML = ''; - subsectionSelect.innerHTML = ''; - sectionSelect.disabled = true; - subsectionSelect.disabled = true; - - if (!locationId) { + if (!deptId) { sectionSelect.innerHTML = ''; return; } - - fetch('{% url "organizations:ajax_main_sections" %}?location_id=' + locationId) + fetch('/organizations/api/sections/?department=' + deptId) .then(r => r.json()) .then(data => { - const sections = data.sections || []; + const sections = data.results || data; sectionSelect.innerHTML = ''; sections.forEach(s => { const opt = document.createElement('option'); @@ -222,7 +202,6 @@ document.addEventListener('DOMContentLoaded', function() { opt.textContent = s.name; sectionSelect.appendChild(opt); }); - if (sections.length > 0) sectionSelect.disabled = false; }) .catch(err => { console.error('Error loading sections:', err); @@ -230,44 +209,8 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - if (locationSelect.value) { - locationSelect.dispatchEvent(new Event('change')); - } - } - - if (sectionSelect) { - sectionSelect.addEventListener('change', function() { - const locationId = locationSelect ? locationSelect.value : ''; - const sectionId = this.value; - subsectionSelect.innerHTML = ''; - subsectionSelect.disabled = true; - - if (!sectionId) { - subsectionSelect.innerHTML = ''; - return; - } - - fetch('{% url "organizations:ajax_subsections" %}?location_id=' + locationId + '&main_section_id=' + sectionId) - .then(r => r.json()) - .then(data => { - const subsections = data.subsections || []; - subsectionSelect.innerHTML = ''; - subsections.forEach(s => { - const opt = document.createElement('option'); - opt.value = s.id; - opt.textContent = s.name; - subsectionSelect.appendChild(opt); - }); - if (subsections.length > 0) subsectionSelect.disabled = false; - }) - .catch(err => { - console.error('Error loading subsections:', err); - subsectionSelect.innerHTML = ''; - }); - }); - - if (sectionSelect.value) { - sectionSelect.dispatchEvent(new Event('change')); + if (deptSelect.value) { + deptSelect.dispatchEvent(new Event('change')); } } }); diff --git a/templates/px_sources/source_user_create_observation.html b/templates/px_sources/source_user_create_observation.html index bae35b6..ffc69a5 100644 --- a/templates/px_sources/source_user_create_observation.html +++ b/templates/px_sources/source_user_create_observation.html @@ -125,31 +125,17 @@
-
- -
- - {{ form.main_section }} -
- -
- - {{ form.subsection }} -
@@ -204,27 +190,18 @@ diff --git a/templates/rca/rca_detail.html b/templates/rca/rca_detail.html index ca94ea4..7f7c957 100644 --- a/templates/rca/rca_detail.html +++ b/templates/rca/rca_detail.html @@ -60,12 +60,12 @@ {% endif %}
- {% if rca.status != 'closed' %} + {% if can_edit and rca.status != 'closed' %} {% trans "Edit" %} {% endif %} - {% if rca.status == 'review' %} + {% if can_approve and rca.status == 'review' %}
{% csrf_token %} @@ -74,7 +74,7 @@
{% endif %} - {% if rca.status == 'approved' or rca.status == 'in_progress' %} + {% if can_approve and rca.status == 'approved' or rca.status == 'in_progress' %}
{% csrf_token %} @@ -84,7 +84,7 @@
{% endif %} - {% if rca.status != 'closed' %} + {% if can_delete and rca.status != 'closed' %}
{% csrf_token %}