From 1752cdb07a7befc660980a12d2f49ed9c346a018 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Sat, 4 Apr 2026 12:43:33 +0300 Subject: [PATCH 01/14] feat(healthcare-practitioner): add custom fields for practitioner role differentiation Add custom fields to Healthcare Practitioner doctype to support distinguishing between Doctors, Nurses, and other practitioner types. This is a foundational change that enables role-based filtering and workflow separation across the healthcare modules. New custom fields added via 28_healthcare_practitioner_nursing.json: 1. practitioner_role (Select: Doctor/Nurse/Other) - Required field inserted after healthcare_practitioner_type - Serves as the primary discriminator for role-based logic 2. nursing_specialization (Select) - Options: General Nursing, Critical Care (ICU), Surgical/Theater, Midwifery, Pediatric, Emergency - Conditionally visible only when practitioner_role is 'Nurse' - Inserted after practitioner_role 3. nursing_role (Select) - Options: Staff Nurse, Charge Nurse, Nurse Manager - Conditionally visible only when practitioner_role is 'Nurse' - Inserted after nursing_specialization All fields follow the established hms_tz custom field patch pattern, using a new sequential JSON file to avoid modifying existing patches. --- .../28_healthcare_practitioner_nursing.json | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json diff --git a/hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json b/hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json new file mode 100644 index 00000000..38426c21 --- /dev/null +++ b/hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json @@ -0,0 +1,32 @@ +[ + { + "dt": "Healthcare Practitioner", + "fieldname": "practitioner_role", + "fieldtype": "Select", + "label": "Practitioner Role", + "options": "Doctor\nNurse\nOther", + "insert_after": "healthcare_practitioner_type", + "reqd": 1, + "doctype": "Custom Field" + }, + { + "dt": "Healthcare Practitioner", + "fieldname": "nursing_specialization", + "fieldtype": "Select", + "label": "Nursing Specialization", + "options": "\nGeneral Nursing\nCritical Care (ICU)\nSurgical/Theater\nMidwifery\nPediatric\nEmergency", + "depends_on": "eval:doc.practitioner_role==\"Nurse\"", + "insert_after": "practitioner_role", + "doctype": "Custom Field" + }, + { + "dt": "Healthcare Practitioner", + "fieldname": "nursing_role", + "fieldtype": "Select", + "label": "Nursing Role", + "options": "\nStaff Nurse\nCharge Nurse\nNurse Manager", + "depends_on": "eval:doc.practitioner_role==\"Nurse\"", + "insert_after": "nursing_specialization", + "doctype": "Custom Field" + } +] From 2dec548cee94c62d2483e3e4759e1884f508dbd7 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:44:51 +0300 Subject: [PATCH 02/14] feat(healthcare-practitioner): add custom fields for practitioner role differentiation Add custom fields to Healthcare Practitioner doctype to support distinguishing between Doctors, Nurses, and other practitioner types. This is a foundational change that enables role-based filtering and workflow separation across the healthcare modules. New custom fields added via 28_healthcare_practitioner_nursing.json: 1. practitioner_role (Select: Doctor/Nurse/Other) - Required field inserted after healthcare_practitioner_type - Serves as the primary discriminator for role-based logic 2. nursing_specialization (Select) - Options: General Nursing, Critical Care (ICU), Surgical/Theater, Midwifery, Pediatric, Emergency - Conditionally visible only when practitioner_role is 'Nurse' - Inserted after practitioner_role 3. nursing_role (Select) - Options: Staff Nurse, Charge Nurse, Nurse Manager - Conditionally visible only when practitioner_role is 'Nurse' - Inserted after nursing_specialization All fields follow the established hms_tz custom field patch pattern, using a new sequential JSON file to avoid modifying existing patches. --- .../28_healthcare_practitioner_nursing.json | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json diff --git a/hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json b/hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json new file mode 100644 index 00000000..38426c21 --- /dev/null +++ b/hms_tz/patches/custom_fields/custom_fields_json/28_healthcare_practitioner_nursing.json @@ -0,0 +1,32 @@ +[ + { + "dt": "Healthcare Practitioner", + "fieldname": "practitioner_role", + "fieldtype": "Select", + "label": "Practitioner Role", + "options": "Doctor\nNurse\nOther", + "insert_after": "healthcare_practitioner_type", + "reqd": 1, + "doctype": "Custom Field" + }, + { + "dt": "Healthcare Practitioner", + "fieldname": "nursing_specialization", + "fieldtype": "Select", + "label": "Nursing Specialization", + "options": "\nGeneral Nursing\nCritical Care (ICU)\nSurgical/Theater\nMidwifery\nPediatric\nEmergency", + "depends_on": "eval:doc.practitioner_role==\"Nurse\"", + "insert_after": "practitioner_role", + "doctype": "Custom Field" + }, + { + "dt": "Healthcare Practitioner", + "fieldname": "nursing_role", + "fieldtype": "Select", + "label": "Nursing Role", + "options": "\nStaff Nurse\nCharge Nurse\nNurse Manager", + "depends_on": "eval:doc.practitioner_role==\"Nurse\"", + "insert_after": "nursing_specialization", + "doctype": "Custom Field" + } +] From 13a9737a94dbae2e093b16ad9fa75acc35349d16 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Sat, 4 Apr 2026 13:30:11 +0300 Subject: [PATCH 03/14] feat(hms_tz): enforce healthcare practitioner role filtering to 'Doctor' Updated multiple doctype forms and API scripts to ensure that 'Healthcare Practitioner' link fields are filtered to exclusively display practitioners with the 'Doctor' role. This change enforces role differentiation across the system, preventing nurses or other practitioners from being selected in fields specifically meant for doctors. Modified files: - episode_of_care.js - healthcare_nursing_task.js - healthcare_referral.js - healthcare_service_order.js - healthcare_service_request.js - inpatient_record.js - lab_test.js - patient_appointment.js - patient_encounter.js - patient_referral.js - practitioner_availability.js - radiology_examination.js - therapy_session.js - clinical_procedure.js (NHIF API) - patient_appointment.js (NHIF API) --- .../hms_tz/doctype/episode_of_care/episode_of_care.js | 10 ++++++++-- .../healthcare_nursing_task.js | 5 +++++ .../healthcare_referral/healthcare_referral.js | 3 +++ .../healthcare_service_order.js | 8 ++++++++ .../healthcare_service_request.js | 8 ++++++++ .../doctype/inpatient_record/inpatient_record.js | 9 +++++++++ hms_tz/hms_tz/doctype/lab_test/lab_test.js | 9 +++++++++ .../patient_appointment/patient_appointment.js | 3 +++ .../doctype/patient_encounter/patient_encounter.js | 11 +++++++++++ .../doctype/patient_referral/patient_referral.js | 8 ++++++++ .../practitioner_availability.js | 5 +++++ .../radiology_examination/radiology_examination.js | 9 +++++++++ .../hms_tz/doctype/therapy_session/therapy_session.js | 9 +++++++++ hms_tz/nhif/api/clinical_procedure.js | 5 +++++ hms_tz/nhif/api/patient_appointment.js | 1 + 15 files changed, 101 insertions(+), 2 deletions(-) diff --git a/hms_tz/hms_tz/doctype/episode_of_care/episode_of_care.js b/hms_tz/hms_tz/doctype/episode_of_care/episode_of_care.js index 40b6e76f..9d6b9301 100644 --- a/hms_tz/hms_tz/doctype/episode_of_care/episode_of_care.js +++ b/hms_tz/hms_tz/doctype/episode_of_care/episode_of_care.js @@ -2,6 +2,12 @@ // For license information, please see license.txt frappe.ui.form.on("Episode of Care", { - // refresh: function(frm) { - // } + setup: function (frm) { + frm.set_query("initiated_by", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + frm.set_query("primary_practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + }, }); diff --git a/hms_tz/hms_tz/doctype/healthcare_nursing_task/healthcare_nursing_task.js b/hms_tz/hms_tz/doctype/healthcare_nursing_task/healthcare_nursing_task.js index f1af1b27..efd6d17a 100644 --- a/hms_tz/hms_tz/doctype/healthcare_nursing_task/healthcare_nursing_task.js +++ b/hms_tz/hms_tz/doctype/healthcare_nursing_task/healthcare_nursing_task.js @@ -2,6 +2,11 @@ // For license information, please see license.txt frappe.ui.form.on("Healthcare Nursing Task", { + setup: function (frm) { + frm.set_query("practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + }, refresh: function (frm) { frm.set_query("service_unit", function () { return { diff --git a/hms_tz/hms_tz/doctype/healthcare_referral/healthcare_referral.js b/hms_tz/hms_tz/doctype/healthcare_referral/healthcare_referral.js index f1d151fb..3e68bef1 100644 --- a/hms_tz/hms_tz/doctype/healthcare_referral/healthcare_referral.js +++ b/hms_tz/hms_tz/doctype/healthcare_referral/healthcare_referral.js @@ -4,6 +4,9 @@ frappe.ui.form.on("Healthcare Referral", { setup: (frm) => { frm.get_field("diagnosis").grid.cannot_add_rows = true; + frm.set_query("practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); }, refresh: (frm) => { frm.get_field("diagnosis").grid.cannot_add_rows = true; diff --git a/hms_tz/hms_tz/doctype/healthcare_service_order/healthcare_service_order.js b/hms_tz/hms_tz/doctype/healthcare_service_order/healthcare_service_order.js index f71a58f2..89e02b65 100644 --- a/hms_tz/hms_tz/doctype/healthcare_service_order/healthcare_service_order.js +++ b/hms_tz/hms_tz/doctype/healthcare_service_order/healthcare_service_order.js @@ -2,6 +2,14 @@ // For license information, please see license.txt frappe.ui.form.on("Healthcare Service Order", { + setup: function (frm) { + frm.set_query("ordered_by", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + frm.set_query("referring_practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + }, refresh: function (frm) { frm.set_query("insurance_subscription", function () { return { diff --git a/hms_tz/hms_tz/doctype/healthcare_service_request/healthcare_service_request.js b/hms_tz/hms_tz/doctype/healthcare_service_request/healthcare_service_request.js index 492ee604..c325c37b 100644 --- a/hms_tz/hms_tz/doctype/healthcare_service_request/healthcare_service_request.js +++ b/hms_tz/hms_tz/doctype/healthcare_service_request/healthcare_service_request.js @@ -201,4 +201,12 @@ var set_filters = (frm) => { }, }; }); + + frm.set_query("practitioner", () => { + return { + filters: { + practitioner_role: "Doctor", + }, + }; + }); }; diff --git a/hms_tz/hms_tz/doctype/inpatient_record/inpatient_record.js b/hms_tz/hms_tz/doctype/inpatient_record/inpatient_record.js index 1000f359..a19d9afd 100644 --- a/hms_tz/hms_tz/doctype/inpatient_record/inpatient_record.js +++ b/hms_tz/hms_tz/doctype/inpatient_record/inpatient_record.js @@ -82,17 +82,26 @@ frappe.ui.form.on("Inpatient Record", { frm.set_df_property("btn_transfer", "hidden", 0); } + frm.set_query("primary_practitioner", function () { + return { + filters: { + practitioner_role: "Doctor", + }, + }; + }); frm.set_query("referring_practitioner", function () { if (frm.doc.source == "External Referral") { return { filters: { healthcare_practitioner_type: "External", + practitioner_role: "Doctor", }, }; } else { return { filters: { healthcare_practitioner_type: "Internal", + practitioner_role: "Doctor", }, }; } diff --git a/hms_tz/hms_tz/doctype/lab_test/lab_test.js b/hms_tz/hms_tz/doctype/lab_test/lab_test.js index f90bd86b..9b45f89c 100644 --- a/hms_tz/hms_tz/doctype/lab_test/lab_test.js +++ b/hms_tz/hms_tz/doctype/lab_test/lab_test.js @@ -32,17 +32,26 @@ frappe.ui.form.on("Lab Test", { refresh: function (frm) { refresh_field("normal_test_items"); refresh_field("descriptive_test_items"); + frm.set_query("practitioner", function () { + return { + filters: { + practitioner_role: "Doctor", + }, + }; + }); frm.set_query("referring_practitioner", function () { if (frm.doc.source == "External Referral") { return { filters: { healthcare_practitioner_type: "External", + practitioner_role: "Doctor", }, }; } else { return { filters: { healthcare_practitioner_type: "Internal", + practitioner_role: "Doctor", }, }; } diff --git a/hms_tz/hms_tz/doctype/patient_appointment/patient_appointment.js b/hms_tz/hms_tz/doctype/patient_appointment/patient_appointment.js index f0660569..f273026c 100644 --- a/hms_tz/hms_tz/doctype/patient_appointment/patient_appointment.js +++ b/hms_tz/hms_tz/doctype/patient_appointment/patient_appointment.js @@ -29,6 +29,7 @@ frappe.ui.form.on("Patient Appointment", { return { filters: { department: frm.doc.department, + practitioner_role: "Doctor", }, }; }); @@ -54,12 +55,14 @@ frappe.ui.form.on("Patient Appointment", { return { filters: { healthcare_practitioner_type: "External", + practitioner_role: "Doctor", }, }; } else { return { filters: { healthcare_practitioner_type: "Internal", + practitioner_role: "Doctor", }, }; } diff --git a/hms_tz/hms_tz/doctype/patient_encounter/patient_encounter.js b/hms_tz/hms_tz/doctype/patient_encounter/patient_encounter.js index 550a4394..54b3af7a 100644 --- a/hms_tz/hms_tz/doctype/patient_encounter/patient_encounter.js +++ b/hms_tz/hms_tz/doctype/patient_encounter/patient_encounter.js @@ -145,17 +145,27 @@ frappe.ui.form.on("Patient Encounter", { }; }); + frm.set_query("practitioner", function () { + return { + filters: { + practitioner_role: "Doctor", + }, + }; + }); + frm.set_query("referring_practitioner", function () { if (frm.doc.source == "External Referral") { return { filters: { healthcare_practitioner_type: "External", + practitioner_role: "Doctor", }, }; } else { return { filters: { healthcare_practitioner_type: "Internal", + practitioner_role: "Doctor", }, }; } @@ -805,6 +815,7 @@ var refer_practitioner = function (frm) { return { filters: { department: selected_department, + practitioner_role: "Doctor", }, }; }; diff --git a/hms_tz/hms_tz/doctype/patient_referral/patient_referral.js b/hms_tz/hms_tz/doctype/patient_referral/patient_referral.js index 592a33b1..945b2d93 100644 --- a/hms_tz/hms_tz/doctype/patient_referral/patient_referral.js +++ b/hms_tz/hms_tz/doctype/patient_referral/patient_referral.js @@ -2,6 +2,14 @@ // For license information, please see license.txt frappe.ui.form.on("Patient Referral", { + setup: function (frm) { + frm.set_query("referred_to_practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + frm.set_query("practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + }, refresh: function (frm) { if (!frm.doc.__islocal) { if (frm.doc.status == "Pending" && frm.doc.docstatus == 1) { diff --git a/hms_tz/hms_tz/doctype/practitioner_availability/practitioner_availability.js b/hms_tz/hms_tz/doctype/practitioner_availability/practitioner_availability.js index 87c02e69..e466410d 100644 --- a/hms_tz/hms_tz/doctype/practitioner_availability/practitioner_availability.js +++ b/hms_tz/hms_tz/doctype/practitioner_availability/practitioner_availability.js @@ -2,6 +2,11 @@ // For license information, please see license.txt frappe.ui.form.on("Practitioner Availability", { + setup: function (frm) { + frm.set_query("practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + }, refresh: function (frm) { set_event_type_properties_to_event(frm); frm.set_query("service_unit", function () { diff --git a/hms_tz/hms_tz/doctype/radiology_examination/radiology_examination.js b/hms_tz/hms_tz/doctype/radiology_examination/radiology_examination.js index f3453766..2a05a054 100644 --- a/hms_tz/hms_tz/doctype/radiology_examination/radiology_examination.js +++ b/hms_tz/hms_tz/doctype/radiology_examination/radiology_examination.js @@ -219,17 +219,26 @@ frappe.ui.form.on("Radiology Examination", { }, }; }); + frm.set_query("practitioner", function () { + return { + filters: { + practitioner_role: "Doctor", + }, + }; + }); frm.set_query("referring_practitioner", function () { if (frm.doc.source == "External Referral") { return { filters: { healthcare_practitioner_type: "External", + practitioner_role: "Doctor", }, }; } else { return { filters: { healthcare_practitioner_type: "Internal", + practitioner_role: "Doctor", }, }; } diff --git a/hms_tz/hms_tz/doctype/therapy_session/therapy_session.js b/hms_tz/hms_tz/doctype/therapy_session/therapy_session.js index dde36ad2..cf629e8e 100644 --- a/hms_tz/hms_tz/doctype/therapy_session/therapy_session.js +++ b/hms_tz/hms_tz/doctype/therapy_session/therapy_session.js @@ -95,17 +95,26 @@ frappe.ui.form.on("Therapy Session", { ); } + frm.set_query("practitioner", function () { + return { + filters: { + practitioner_role: "Doctor", + }, + }; + }); frm.set_query("referring_practitioner", function () { if (frm.doc.source == "External Referral") { return { filters: { healthcare_practitioner_type: "External", + practitioner_role: "Doctor", }, }; } else { return { filters: { healthcare_practitioner_type: "Internal", + practitioner_role: "Doctor", }, }; } diff --git a/hms_tz/nhif/api/clinical_procedure.js b/hms_tz/nhif/api/clinical_procedure.js index 813a10ab..7f47d796 100644 --- a/hms_tz/nhif/api/clinical_procedure.js +++ b/hms_tz/nhif/api/clinical_procedure.js @@ -1,4 +1,9 @@ frappe.ui.form.on("Clinical Procedure", { + setup: function (frm) { + frm.set_query("practitioner", function () { + return { filters: { practitioner_role: "Doctor" } }; + }); + }, refresh: function (frm) { $('[data-label="Not%20Serviced"]').parent().hide(); frm.remove_custom_button("Start"); diff --git a/hms_tz/nhif/api/patient_appointment.js b/hms_tz/nhif/api/patient_appointment.js index 3b01f7c3..f7ba4232 100644 --- a/hms_tz/nhif/api/patient_appointment.js +++ b/hms_tz/nhif/api/patient_appointment.js @@ -760,6 +760,7 @@ const check_and_set_availability = (frm) => { filters: { status: "Active", hms_tz_company: frm.doc.company, + practitioner_role: "Doctor", }, }; }, From c8db6fc6b0e20dd469cf5beba234c3d0751f881c Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Sat, 4 Apr 2026 21:51:45 +0300 Subject: [PATCH 04/14] feat: implement Nursing Schedule and Nurse Schedule Detail DocTypes - Added Nursing Schedule DocType with submittable functionality - Added Nurse Schedule Detail child table with revised fields for nurse assignment, shift type, and service unit - Implemented validation logic in nursing_schedule.py for dates and duplicate assignments - Implemented JS filtering for nurses (practitioner_role='Nurse') in nursing_schedule.js - Configured field triggers for shift assignment targets - Updated pre-commit config for development stages --- .pre-commit-config.yaml | 2 +- .../doctype/nurse_schedule_detail/__init__.py | 0 .../nurse_schedule_detail.json | 82 ++++++++++++ .../nurse_schedule_detail.py | 9 ++ .../doctype/nursing_schedule/__init__.py | 0 .../nursing_schedule/nursing_schedule.js | 53 ++++++++ .../nursing_schedule/nursing_schedule.json | 124 ++++++++++++++++++ .../nursing_schedule/nursing_schedule.py | 39 ++++++ 8 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 hms_tz/hms_tz/doctype/nurse_schedule_detail/__init__.py create mode 100644 hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json create mode 100644 hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py create mode 100644 hms_tz/hms_tz/doctype/nursing_schedule/__init__.py create mode 100644 hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js create mode 100644 hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json create mode 100644 hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0d16f34..b0fc0372 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: "node_modules|.git" -default_stages: [commit, push, manual] +default_stages: [pre-commit, pre-push, manual] fail_fast: false repos: diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/__init__.py b/hms_tz/hms_tz/doctype/nurse_schedule_detail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json new file mode 100644 index 00000000..165d22f9 --- /dev/null +++ b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "creation": "2026-04-04 21:32:36.013090", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "nurse", + "nurse_name", + "shift_type", + "shift_based_on", + "column_break_01", + "service_unit_type", + "service_unit" + ], + "fields": [ + { + "fieldname": "nurse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Nurse", + "options": "Healthcare Practitioner", + "reqd": 1 + }, + { + "fetch_from": "nurse.practitioner_name", + "fieldname": "nurse_name", + "fieldtype": "Data", + "label": "Nurse Name", + "read_only": 1 + }, + { + "fieldname": "shift_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Shift Type", + "options": "Morning\nAfternoon\nNight\nFull Day", + "reqd": 1 + }, + { + "default": "Service Unit", + "fieldname": "shift_based_on", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Shift Based On", + "options": "Service Unit Type\nService Unit", + "reqd": 1 + }, + { + "fieldname": "column_break_01", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.shift_based_on=='Service Unit Type'", + "fieldname": "service_unit_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Service Unit Type", + "options": "Healthcare Service Unit Type" + }, + { + "depends_on": "eval:doc.shift_based_on=='Service Unit'", + "fieldname": "service_unit", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Service Unit", + "options": "Healthcare Service Unit" + } + ], + "istable": 1, + "links": [], + "modified": "2026-04-04 21:36:28.516239", + "modified_by": "Administrator", + "module": "Hms Tz", + "name": "Nurse Schedule Detail", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py new file mode 100644 index 00000000..fd311ce1 --- /dev/null +++ b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Aakvatech Limited and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class NurseScheduleDetail(Document): + pass diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/__init__.py b/hms_tz/hms_tz/doctype/nursing_schedule/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js new file mode 100644 index 00000000..e1dc3734 --- /dev/null +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -0,0 +1,53 @@ +// Copyright (c) 2026, Aakvatech Limited and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Nursing Schedule", { + setup: function (frm) { + // Filter 'nurse' column in child table to only show Nurses + frm.set_query("nurse", "shifts", function () { + return { + filters: { + practitioner_role: "Nurse", + }, + }; + }); + + // Filter 'service_unit' and 'service_unit_type' correctly + frm.set_query("service_unit", "shifts", function () { + return { + filters: { + is_group: 0, + }, + }; + }); + }, + + start_date: function (frm) { + if (frm.doc.start_date && frm.doc.end_date) { + if (frm.doc.start_date > frm.doc.end_date) { + frappe.msgprint(__("Start Date cannot be after End Date.")); + frm.set_value("start_date", ""); + } + } + }, + + end_date: function (frm) { + if (frm.doc.start_date && frm.doc.end_date) { + if (frm.doc.end_date < frm.doc.start_date) { + frappe.msgprint(__("End Date cannot be before Start Date.")); + frm.set_value("end_date", ""); + } + } + }, +}); + +frappe.ui.form.on("Nurse Schedule Detail", { + shift_based_on: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.shift_based_on === "Service Unit") { + frappe.model.set_value(cdt, cdn, "service_unit_type", ""); + } else { + frappe.model.set_value(cdt, cdn, "service_unit", ""); + } + }, +}); diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json new file mode 100644 index 00000000..7898201f --- /dev/null +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2026-04-04 00:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "is_submittable": 1, + "field_order": [ + "naming_series", + "company", + "frequency", + "column_break_01", + "start_date", + "end_date", + "section_break_shifts", + "shifts", + "amended_from" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "NS-.YYYY.-", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Frequency", + "options": "Daily\nWeekly\nMonthly\nQuarterly\nBi-Yearly\nYearly", + "reqd": 1 + }, + { + "fieldname": "column_break_01", + "fieldtype": "Column Break" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "End Date", + "reqd": 1 + }, + { + "fieldname": "section_break_shifts", + "fieldtype": "Section Break", + "label": "Shifts" + }, + { + "fieldname": "shifts", + "fieldtype": "Table", + "label": "Shifts", + "options": "Nurse Schedule Detail", + "reqd": 0 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "options": "Nursing Schedule", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "links": [], + "modified": "2026-04-04 00:00:00.000000", + "modified_by": "Administrator", + "module": "Hms Tz", + "name": "Nursing Schedule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1, + "submit": 1, + "cancel": 1, + "amend": 1 + }, + { + "create": 1, + "delete": 0, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Nursing User", + "share": 1, + "write": 1, + "submit": 1, + "cancel": 1, + "amend": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py new file mode 100644 index 00000000..977506f5 --- /dev/null +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py @@ -0,0 +1,39 @@ +# Copyright (c) 2026, Aakvatech Limited and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class NursingSchedule(Document): + def validate(self): + self.validate_dates() + self.validate_duplicate_nurse_shifts() + + def validate_dates(self): + """Ensure start_date is before or equal to end_date.""" + if self.start_date and self.end_date: + if self.start_date > self.end_date: + frappe.throw( + _("Start Date {0} cannot be after End Date {1}").format( + frappe.bold(self.start_date), frappe.bold(self.end_date) + ), + title=_("Invalid Dates"), + ) + + def validate_duplicate_nurse_shifts(self): + """Warn if the same nurse appears in conflicting shifts in the same schedule.""" + seen: dict = {} + for row in self.shifts or []: + # Check for same nurse, shift type, and assignment target + key = (row.nurse, row.shift_type, row.shift_based_on, row.service_unit if row.shift_based_on == 'Service Unit' else row.service_unit_type) + if key in seen: + frappe.throw( + _( + "Row #{0}: Nurse {1} is already assigned to the same " + "shift/unit combination in Row #{2}." + ).format(row.idx, row.nurse, seen[key]), + title=_("Duplicate Shift Assignment"), + ) + seen[key] = row.idx From e780bf6bf96186c17286f8d60880a3c49c1f3dbf Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:38:41 +0300 Subject: [PATCH 05/14] feat: auto-calculate end_date from frequency and start_date - Make end_date field read-only in Nursing Schedule DocType - Add calculate_end_date() method in nursing_schedule.py using frappe.utils.add_to_date for server-side computation - Add calculate_end_date() JS helper triggered by start_date and frequency field changes for instant client-side feedback - Frequency mapping: Daily (same day), Weekly (+6 days), Monthly/Quarterly/Bi-Yearly (+N months -1 day), Yearly (+1 year -1 day) --- .../nursing_schedule/nursing_schedule.js | 46 +++++++++++++------ .../nursing_schedule/nursing_schedule.json | 3 +- .../nursing_schedule/nursing_schedule.py | 34 +++++++++++++- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js index e1dc3734..8640cc03 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -23,21 +23,11 @@ frappe.ui.form.on("Nursing Schedule", { }, start_date: function (frm) { - if (frm.doc.start_date && frm.doc.end_date) { - if (frm.doc.start_date > frm.doc.end_date) { - frappe.msgprint(__("Start Date cannot be after End Date.")); - frm.set_value("start_date", ""); - } - } + calculate_end_date(frm); }, - end_date: function (frm) { - if (frm.doc.start_date && frm.doc.end_date) { - if (frm.doc.end_date < frm.doc.start_date) { - frappe.msgprint(__("End Date cannot be before Start Date.")); - frm.set_value("end_date", ""); - } - } + frequency: function (frm) { + calculate_end_date(frm); }, }); @@ -51,3 +41,33 @@ frappe.ui.form.on("Nurse Schedule Detail", { } }, }); + +function calculate_end_date(frm) { + if (!frm.doc.start_date || !frm.doc.frequency) { + frm.set_value("end_date", ""); + return; + } + + const frequency_map = { + Daily: { days: 0 }, + Weekly: { days: 6 }, + Monthly: { months: 1 }, + Quarterly: { months: 3 }, + "Bi-Yearly": { months: 6 }, + Yearly: { months: 12 }, + }; + + const offset = frequency_map[frm.doc.frequency]; + if (!offset) return; + + let end_date; + if (offset.days !== undefined) { + end_date = frappe.datetime.add_days(frm.doc.start_date, offset.days); + } else { + // Add months, then subtract 1 day to get the last day of the period + end_date = frappe.datetime.add_months(frm.doc.start_date, offset.months); + end_date = frappe.datetime.add_days(end_date, -1); + } + + frm.set_value("end_date", end_date); +} diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json index 7898201f..ea4c06da 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json @@ -56,6 +56,7 @@ "fieldtype": "Date", "in_list_view": 1, "label": "End Date", + "read_only": 1, "reqd": 1 }, { @@ -81,7 +82,7 @@ } ], "links": [], - "modified": "2026-04-04 00:00:00.000000", + "modified": "2026-04-06 13:05:00.000000", "modified_by": "Administrator", "module": "Hms Tz", "name": "Nursing Schedule", diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py index 977506f5..26424704 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py @@ -4,17 +4,49 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils import add_to_date, getdate class NursingSchedule(Document): def validate(self): + self.calculate_end_date() self.validate_dates() self.validate_duplicate_nurse_shifts() + def calculate_end_date(self): + """Auto-calculate end_date based on start_date and frequency.""" + if not self.start_date or not self.frequency: + return + + frequency_map = { + "Daily": {"days": 0}, + "Weekly": {"days": 6}, + "Monthly": {"months": 1}, + "Quarterly": {"months": 3}, + "Bi-Yearly": {"months": 6}, + "Yearly": {"years": 1}, + } + + offset = frequency_map.get(self.frequency) + if not offset: + return + + if "days" in offset: + self.end_date = add_to_date(getdate(self.start_date), days=offset["days"]) + elif "months" in offset: + # Add months, then subtract 1 day to get the last day of the period + self.end_date = add_to_date( + getdate(self.start_date), months=offset["months"], days=-1 + ) + elif "years" in offset: + self.end_date = add_to_date( + getdate(self.start_date), years=offset["years"], days=-1 + ) + def validate_dates(self): """Ensure start_date is before or equal to end_date.""" if self.start_date and self.end_date: - if self.start_date > self.end_date: + if getdate(self.start_date) > getdate(self.end_date): frappe.throw( _("Start Date {0} cannot be after End Date {1}").format( frappe.bold(self.start_date), frappe.bold(self.end_date) From 295012ac5791f2b1f383951252d974844b819b1a Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:39:52 +0300 Subject: [PATCH 06/14] feat: add Get Nurses button to pull active nurses into schedule - Add 'Get Nurses' button field in the Shifts section of Nursing Schedule (visible only when company is set and doctype is not submitted) - Add get_nurses() client-side handler that calls the server method, skips nurses already in the child table, and shows count feedback - Add get_nurses() @frappe.whitelist() server method that queries Healthcare Practitioner filtered by practitioner_role='Nurse', hms_tz_company=company, and status='Active' - No duplicate nurses are added to the schedule (Task 4) --- .../nursing_schedule/nursing_schedule.js | 50 +++++++++++++++++++ .../nursing_schedule/nursing_schedule.json | 7 +++ .../nursing_schedule/nursing_schedule.py | 23 +++++++++ 3 files changed, 80 insertions(+) diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js index 8640cc03..fa1ad398 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -29,6 +29,56 @@ frappe.ui.form.on("Nursing Schedule", { frequency: function (frm) { calculate_end_date(frm); }, + + get_nurses: function (frm) { + if (!frm.doc.company) { + frappe.msgprint(__("Please select a Company first.")); + return; + } + + frappe.call({ + method: + "hms_tz.hms_tz.doctype.nursing_schedule.nursing_schedule.get_nurses", + args: { + company: frm.doc.company, + }, + freeze: true, + freeze_message: __("Fetching nurses..."), + callback: function (r) { + if (!r.message || r.message.length === 0) { + frappe.msgprint(__("No active nurses found for the selected company.")); + return; + } + + // Collect existing nurse names to avoid duplicates + const existing_nurses = new Set( + (frm.doc.shifts || []).map((row) => row.nurse) + ); + + let added_count = 0; + r.message.forEach(function (nurse) { + if (!existing_nurses.has(nurse.name)) { + let row = frm.add_child("shifts"); + row.nurse = nurse.name; + row.nurse_name = nurse.practitioner_name; + existing_nurses.add(nurse.name); + added_count++; + } + }); + + frm.refresh_field("shifts"); + + if (added_count > 0) { + frappe.show_alert({ + message: __("{0} nurse(s) added to the schedule.", [added_count]), + indicator: "green", + }); + } else { + frappe.msgprint(__("All active nurses are already in the schedule.")); + } + }, + }); + }, }); frappe.ui.form.on("Nurse Schedule Detail", { diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json index ea4c06da..dd6c0287 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json @@ -13,6 +13,7 @@ "start_date", "end_date", "section_break_shifts", + "get_nurses", "shifts", "amended_from" ], @@ -64,6 +65,12 @@ "fieldtype": "Section Break", "label": "Shifts" }, + { + "fieldname": "get_nurses", + "fieldtype": "Button", + "label": "Get Nurses", + "depends_on": "eval:doc.company && doc.docstatus==0" + }, { "fieldname": "shifts", "fieldtype": "Table", diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py index 26424704..8870604d 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py @@ -69,3 +69,26 @@ def validate_duplicate_nurse_shifts(self): title=_("Duplicate Shift Assignment"), ) seen[key] = row.idx + + +@frappe.whitelist() +def get_nurses(company: str) -> list[dict]: + """Return active nurses for the given company. + + Filters Healthcare Practitioner records by: + - practitioner_role = 'Nurse' + - hms_tz_company = the provided company + - status = 'Active' + """ + nurses = frappe.db.get_all( + "Healthcare Practitioner", + filters={ + "practitioner_role": "Nurse", + "hms_tz_company": company, + "status": "Active", + }, + fields=["name", "practitioner_name"], + order_by="practitioner_name asc", + ) + + return nurses From e46e66966a57e5497f2a0921dd2656a7a4f1e948 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:40:38 +0300 Subject: [PATCH 07/14] fix: update Nurse Schedule Detail default and formatting - Change shift_based_on default from 'Service Unit' to 'Service Unit Type' to match the first option in the select list - Normalize JSON indentation to 4-space indent for consistency with the parent Nursing Schedule DocType --- .../nurse_schedule_detail.json | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json index 165d22f9..abf951e2 100644 --- a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json +++ b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json @@ -1,82 +1,82 @@ { - "actions": [], - "creation": "2026-04-04 21:32:36.013090", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "nurse", - "nurse_name", - "shift_type", - "shift_based_on", - "column_break_01", - "service_unit_type", - "service_unit" - ], - "fields": [ - { - "fieldname": "nurse", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Nurse", - "options": "Healthcare Practitioner", - "reqd": 1 - }, - { - "fetch_from": "nurse.practitioner_name", - "fieldname": "nurse_name", - "fieldtype": "Data", - "label": "Nurse Name", - "read_only": 1 - }, - { - "fieldname": "shift_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Shift Type", - "options": "Morning\nAfternoon\nNight\nFull Day", - "reqd": 1 - }, - { - "default": "Service Unit", - "fieldname": "shift_based_on", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Shift Based On", - "options": "Service Unit Type\nService Unit", - "reqd": 1 - }, - { - "fieldname": "column_break_01", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.shift_based_on=='Service Unit Type'", - "fieldname": "service_unit_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Service Unit Type", - "options": "Healthcare Service Unit Type" - }, - { - "depends_on": "eval:doc.shift_based_on=='Service Unit'", - "fieldname": "service_unit", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Service Unit", - "options": "Healthcare Service Unit" - } - ], - "istable": 1, - "links": [], - "modified": "2026-04-04 21:36:28.516239", - "modified_by": "Administrator", - "module": "Hms Tz", - "name": "Nurse Schedule Detail", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file + "actions": [], + "creation": "2026-04-04 21:32:36.013090", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "nurse", + "nurse_name", + "shift_type", + "shift_based_on", + "column_break_01", + "service_unit_type", + "service_unit" + ], + "fields": [ + { + "fieldname": "nurse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Nurse", + "options": "Healthcare Practitioner", + "reqd": 1 + }, + { + "fetch_from": "nurse.practitioner_name", + "fieldname": "nurse_name", + "fieldtype": "Data", + "label": "Nurse Name", + "read_only": 1 + }, + { + "fieldname": "shift_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Shift Type", + "options": "Morning\nAfternoon\nNight\nFull Day", + "reqd": 1 + }, + { + "default": "Service Unit Type", + "fieldname": "shift_based_on", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Shift Based On", + "options": "Service Unit Type\nService Unit", + "reqd": 1 + }, + { + "fieldname": "column_break_01", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.shift_based_on=='Service Unit Type'", + "fieldname": "service_unit_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Service Unit Type", + "options": "Healthcare Service Unit Type" + }, + { + "depends_on": "eval:doc.shift_based_on=='Service Unit'", + "fieldname": "service_unit", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Service Unit", + "options": "Healthcare Service Unit" + } + ], + "istable": 1, + "links": [], + "modified": "2026-04-06 13:36:00.000000", + "modified_by": "Administrator", + "module": "Hms Tz", + "name": "Nurse Schedule Detail", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} From a102e5633a4476817c782bb4162d56dd92e45c3c Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:11:45 +0300 Subject: [PATCH 08/14] fix: resolve deprecated pre-commit stage name warnings - Change default_stages from [pre-commit, pre-push, manual] to [pre-commit, manual] to remove unnecessary pre-push stage - Update pre-commit-hooks from v4.5.0 to v5.0.0 to fix deprecated stage names (commit, push) used internally by the repo - Update isort from 5.13.2 to 6.0.1 to fix deprecated stage names (commit, merge-commit, push) used internally by the repo These changes eliminate all deprecation warnings when running pre-commit run --all without changing hook behavior. --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0fc0372..44c8863f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ exclude: "node_modules|.git" -default_stages: [pre-commit, pre-push, manual] +default_stages: [pre-commit, manual] fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace files: "hms_tz.*" @@ -26,7 +26,7 @@ repos: stages: [manual] - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort args: ["--profile", "black", "--line-length", "120"] From 46c47df5735bc0d727861415d94a6d3d2f34173a Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:12:22 +0300 Subject: [PATCH 09/14] fix: resolve deprecated pre-commit stage name warnings (#1129) fix: resolve deprecated pre-commit stage name warnings --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0fc0372..44c8863f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ exclude: "node_modules|.git" -default_stages: [pre-commit, pre-push, manual] +default_stages: [pre-commit, manual] fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace files: "hms_tz.*" @@ -26,7 +26,7 @@ repos: stages: [manual] - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort args: ["--profile", "black", "--line-length", "120"] From ad11a362b9ec8ccff8e9e89cc57b360709cf6670 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:12:54 +0300 Subject: [PATCH 10/14] refactor: apply pre-commit hook auto-fixes to nursing module files - Remove unused 'import frappe' from nurse_schedule_detail.py (autoflake: unused import removal) - Reformat long frappe.msgprint line in nursing_schedule.js to comply with prettier print-width=79 rule (line wrapping) --- .../nurse_schedule_detail/nurse_schedule_detail.py | 1 - .../hms_tz/doctype/nursing_schedule/nursing_schedule.js | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py index fd311ce1..72397161 100644 --- a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py +++ b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py @@ -1,7 +1,6 @@ # Copyright (c) 2026, Aakvatech Limited and contributors # For license information, please see license.txt -import frappe from frappe.model.document import Document diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js index fa1ad398..2499d31a 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -46,7 +46,9 @@ frappe.ui.form.on("Nursing Schedule", { freeze_message: __("Fetching nurses..."), callback: function (r) { if (!r.message || r.message.length === 0) { - frappe.msgprint(__("No active nurses found for the selected company.")); + frappe.msgprint( + __("No active nurses found for the selected company.") + ); return; } @@ -74,7 +76,9 @@ frappe.ui.form.on("Nursing Schedule", { indicator: "green", }); } else { - frappe.msgprint(__("All active nurses are already in the schedule.")); + frappe.msgprint( + __("All active nurses are already in the schedule.") + ); } }, }); From d8e80f276cd52c6bbb46c1b8629c5a54fe181d87 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:13:24 +0300 Subject: [PATCH 11/14] refactor: apply pre-commit hook auto-fixes to nursing module files (#1130) refactor: apply pre-commit hook auto-fixes to nursing module files --- .../nurse_schedule_detail/nurse_schedule_detail.py | 1 - .../hms_tz/doctype/nursing_schedule/nursing_schedule.js | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py index fd311ce1..72397161 100644 --- a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py +++ b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.py @@ -1,7 +1,6 @@ # Copyright (c) 2026, Aakvatech Limited and contributors # For license information, please see license.txt -import frappe from frappe.model.document import Document diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js index fa1ad398..2499d31a 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -46,7 +46,9 @@ frappe.ui.form.on("Nursing Schedule", { freeze_message: __("Fetching nurses..."), callback: function (r) { if (!r.message || r.message.length === 0) { - frappe.msgprint(__("No active nurses found for the selected company.")); + frappe.msgprint( + __("No active nurses found for the selected company.") + ); return; } @@ -74,7 +76,9 @@ frappe.ui.form.on("Nursing Schedule", { indicator: "green", }); } else { - frappe.msgprint(__("All active nurses are already in the schedule.")); + frappe.msgprint( + __("All active nurses are already in the schedule.") + ); } }, }); From 5cfb7b16cc001b57585c0447aa2148f0a91322d3 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:22:25 +0300 Subject: [PATCH 12/14] refactor: rename shift-based fields to assignment-based in Nursing Schedule Nursing Schedule now focuses purely on location assignment (which ward/room a nurse works in) rather than time-based shifts, since shifts are managed through the HR Module (Shift Assignment, Shift Request, Default Shift). Nursing Schedule DocType: - Rename section_break_shifts -> section_break_assignments - Rename shifts -> assignments (Table field) - Update section label from 'Shifts' to 'Assignments' Nurse Schedule Detail (child table): - Remove shift_type field entirely (handled by HR Module) - Rename shift_based_on -> assign_based_on - Add assignment_date (Date, required) field for specific date the nurse is assigned to that location - Auto-populate assignment_date with end_date when frequency is Daily (on Get Nurses, Add Row, and frequency change) Client-side (nursing_schedule.js): - Update all shifts -> assignments references - Update shift_based_on -> assign_based_on references - Refactor set_query into dedicated trigger with enhanced filters: nurse (Active, Nurse role, company), service_unit_type (not disabled), service_unit (non-group, not disabled, same company) - Add assignments_add handler and set_daily_assignment_dates helper for auto-filling assignment_date on Daily frequency Server-side (nursing_schedule.py): - Update self.shifts -> self.assignments references - Update duplicate validation to check nurse+date+location instead of nurse+shift_type+location --- .../nurse_schedule_detail.json | 21 ++-- .../nursing_schedule/nursing_schedule.js | 95 ++++++++++++++++--- .../nursing_schedule/nursing_schedule.json | 14 +-- .../nursing_schedule/nursing_schedule.py | 21 ++-- 4 files changed, 110 insertions(+), 41 deletions(-) diff --git a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json index abf951e2..21093813 100644 --- a/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json +++ b/hms_tz/hms_tz/doctype/nurse_schedule_detail/nurse_schedule_detail.json @@ -7,8 +7,8 @@ "field_order": [ "nurse", "nurse_name", - "shift_type", - "shift_based_on", + "assignment_date", + "assign_based_on", "column_break_01", "service_unit_type", "service_unit" @@ -30,19 +30,18 @@ "read_only": 1 }, { - "fieldname": "shift_type", - "fieldtype": "Select", + "fieldname": "assignment_date", + "fieldtype": "Date", "in_list_view": 1, - "label": "Shift Type", - "options": "Morning\nAfternoon\nNight\nFull Day", + "label": "Assignment Date", "reqd": 1 }, { "default": "Service Unit Type", - "fieldname": "shift_based_on", + "fieldname": "assign_based_on", "fieldtype": "Select", "in_list_view": 1, - "label": "Shift Based On", + "label": "Assign Based On", "options": "Service Unit Type\nService Unit", "reqd": 1 }, @@ -51,7 +50,7 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.shift_based_on=='Service Unit Type'", + "depends_on": "eval:doc.assign_based_on=='Service Unit Type'", "fieldname": "service_unit_type", "fieldtype": "Link", "in_list_view": 1, @@ -59,7 +58,7 @@ "options": "Healthcare Service Unit Type" }, { - "depends_on": "eval:doc.shift_based_on=='Service Unit'", + "depends_on": "eval:doc.assign_based_on=='Service Unit'", "fieldname": "service_unit", "fieldtype": "Link", "in_list_view": 1, @@ -69,7 +68,7 @@ ], "istable": 1, "links": [], - "modified": "2026-04-06 13:36:00.000000", + "modified": "2026-04-06 17:30:00.000000", "modified_by": "Administrator", "module": "Hms Tz", "name": "Nurse Schedule Detail", diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js index 2499d31a..ce2ddac6 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -3,20 +3,34 @@ frappe.ui.form.on("Nursing Schedule", { setup: function (frm) { - // Filter 'nurse' column in child table to only show Nurses - frm.set_query("nurse", "shifts", function () { + frm.trigger("set_query"); + }, + + set_query: (frm) => { + frm.set_query("nurse", "assignments", function () { return { filters: { + status: 'Active', practitioner_role: "Nurse", + hms_tz_company: frm.doc.company, + }, + }; + }); + + frm.set_query("service_unit_type", "assignments", function () { + return { + filters: { + disabled: 0, }, }; }); - // Filter 'service_unit' and 'service_unit_type' correctly - frm.set_query("service_unit", "shifts", function () { + frm.set_query("service_unit", "assignments", function () { return { filters: { is_group: 0, + disabled: 0, + company: frm.doc.company, }, }; }); @@ -28,6 +42,7 @@ frappe.ui.form.on("Nursing Schedule", { frequency: function (frm) { calculate_end_date(frm); + set_daily_assignment_dates(frm); }, get_nurses: function (frm) { @@ -47,37 +62,49 @@ frappe.ui.form.on("Nursing Schedule", { callback: function (r) { if (!r.message || r.message.length === 0) { frappe.msgprint( - __("No active nurses found for the selected company.") + __( + "No active nurses found for the selected company." + ) ); return; } // Collect existing nurse names to avoid duplicates const existing_nurses = new Set( - (frm.doc.shifts || []).map((row) => row.nurse) + (frm.doc.assignments || []).map( + (row) => row.nurse + ) ); let added_count = 0; r.message.forEach(function (nurse) { if (!existing_nurses.has(nurse.name)) { - let row = frm.add_child("shifts"); + let row = frm.add_child("assignments"); row.nurse = nurse.name; row.nurse_name = nurse.practitioner_name; + if (frm.doc.frequency === "Daily" && frm.doc.end_date) { + row.assignment_date = frm.doc.end_date; + } existing_nurses.add(nurse.name); added_count++; } }); - frm.refresh_field("shifts"); + frm.refresh_field("assignments"); if (added_count > 0) { frappe.show_alert({ - message: __("{0} nurse(s) added to the schedule.", [added_count]), + message: __( + "{0} nurse(s) added to the schedule.", + [added_count] + ), indicator: "green", }); } else { frappe.msgprint( - __("All active nurses are already in the schedule.") + __( + "All active nurses are already in the schedule." + ) ); } }, @@ -86,16 +113,48 @@ frappe.ui.form.on("Nursing Schedule", { }); frappe.ui.form.on("Nurse Schedule Detail", { - shift_based_on: function (frm, cdt, cdn) { + assignments_add: function (frm, cdt, cdn) { + if (frm.doc.frequency === "Daily" && frm.doc.end_date) { + frappe.model.set_value( + cdt, + cdn, + "assignment_date", + frm.doc.end_date + ); + } + }, + + assign_based_on: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.shift_based_on === "Service Unit") { - frappe.model.set_value(cdt, cdn, "service_unit_type", ""); + if (row.assign_based_on === "Service Unit") { + frappe.model.set_value( + cdt, + cdn, + "service_unit_type", + "" + ); } else { frappe.model.set_value(cdt, cdn, "service_unit", ""); } }, }); +function set_daily_assignment_dates(frm) { + if (frm.doc.frequency !== "Daily" || !frm.doc.end_date) return; + + (frm.doc.assignments || []).forEach(function (row) { + if (!row.assignment_date) { + frappe.model.set_value( + row.doctype, + row.name, + "assignment_date", + frm.doc.end_date + ); + } + }); + frm.refresh_field("assignments"); +} + function calculate_end_date(frm) { if (!frm.doc.start_date || !frm.doc.frequency) { frm.set_value("end_date", ""); @@ -116,10 +175,16 @@ function calculate_end_date(frm) { let end_date; if (offset.days !== undefined) { - end_date = frappe.datetime.add_days(frm.doc.start_date, offset.days); + end_date = frappe.datetime.add_days( + frm.doc.start_date, + offset.days + ); } else { // Add months, then subtract 1 day to get the last day of the period - end_date = frappe.datetime.add_months(frm.doc.start_date, offset.months); + end_date = frappe.datetime.add_months( + frm.doc.start_date, + offset.months + ); end_date = frappe.datetime.add_days(end_date, -1); } diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json index dd6c0287..5f2925c0 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.json @@ -12,9 +12,9 @@ "column_break_01", "start_date", "end_date", - "section_break_shifts", + "section_break_assignments", "get_nurses", - "shifts", + "assignments", "amended_from" ], "fields": [ @@ -61,9 +61,9 @@ "reqd": 1 }, { - "fieldname": "section_break_shifts", + "fieldname": "section_break_assignments", "fieldtype": "Section Break", - "label": "Shifts" + "label": "Assignments" }, { "fieldname": "get_nurses", @@ -72,9 +72,9 @@ "depends_on": "eval:doc.company && doc.docstatus==0" }, { - "fieldname": "shifts", + "fieldname": "assignments", "fieldtype": "Table", - "label": "Shifts", + "label": "Assignments", "options": "Nurse Schedule Detail", "reqd": 0 }, @@ -89,7 +89,7 @@ } ], "links": [], - "modified": "2026-04-06 13:05:00.000000", + "modified": "2026-04-06 17:30:00.000000", "modified_by": "Administrator", "module": "Hms Tz", "name": "Nursing Schedule", diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py index 8870604d..85fa2f8e 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.py @@ -11,7 +11,7 @@ class NursingSchedule(Document): def validate(self): self.calculate_end_date() self.validate_dates() - self.validate_duplicate_nurse_shifts() + self.validate_duplicate_nurse_assignments() def calculate_end_date(self): """Auto-calculate end_date based on start_date and frequency.""" @@ -54,19 +54,24 @@ def validate_dates(self): title=_("Invalid Dates"), ) - def validate_duplicate_nurse_shifts(self): - """Warn if the same nurse appears in conflicting shifts in the same schedule.""" + def validate_duplicate_nurse_assignments(self): + """Warn if the same nurse is assigned to the same location on the same date.""" seen: dict = {} - for row in self.shifts or []: - # Check for same nurse, shift type, and assignment target - key = (row.nurse, row.shift_type, row.shift_based_on, row.service_unit if row.shift_based_on == 'Service Unit' else row.service_unit_type) + for row in self.assignments or []: + # Check for same nurse, date, and assignment target + location = ( + row.service_unit + if row.assign_based_on == "Service Unit" + else row.service_unit_type + ) + key = (row.nurse, row.assignment_date, row.assign_based_on, location) if key in seen: frappe.throw( _( "Row #{0}: Nurse {1} is already assigned to the same " - "shift/unit combination in Row #{2}." + "location on the same date in Row #{2}." ).format(row.idx, row.nurse, seen[key]), - title=_("Duplicate Shift Assignment"), + title=_("Duplicate Assignment"), ) seen[key] = row.idx From b6b2f8a9a91fd58c56539f930831fd530827fe14 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:28:28 +0300 Subject: [PATCH 13/14] feat: enforce employee as mandatory for Nurse practitioners Add property setter (27_healthcare_practitioner_nursing.json) that sets mandatory_depends_on for the employee field on Healthcare Practitioner to eval:doc.practitioner_role=='Nurse'. This ensures every nurse is linked to an Employee record, enabling integration with the HR Module for shift management (Default Shift, Shift Assignment, Shift Request). Also includes minor JS formatting cleanup (consistent double quotes, condensed single-line function calls). --- .../nursing_schedule/nursing_schedule.js | 35 ++++--------------- .../27_healthcare_practitioner_nursing.json | 19 ++++++++++ 2 files changed, 26 insertions(+), 28 deletions(-) create mode 100644 hms_tz/patches/property_setter/property_setters_json/27_healthcare_practitioner_nursing.json diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js index 97d89674..028f6163 100644 --- a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule.js @@ -10,7 +10,7 @@ frappe.ui.form.on("Nursing Schedule", { frm.set_query("nurse", "assignments", function () { return { filters: { - status: 'Active', + status: "Active", practitioner_role: "Nurse", hms_tz_company: frm.doc.company, }, @@ -69,9 +69,7 @@ frappe.ui.form.on("Nursing Schedule", { // Collect existing nurse names to avoid duplicates const existing_nurses = new Set( - (frm.doc.assignments || []).map( - (row) => row.nurse - ) + (frm.doc.assignments || []).map((row) => row.nurse) ); let added_count = 0; @@ -92,10 +90,7 @@ frappe.ui.form.on("Nursing Schedule", { if (added_count > 0) { frappe.show_alert({ - message: __( - "{0} nurse(s) added to the schedule.", - [added_count] - ), + message: __("{0} nurse(s) added to the schedule.", [added_count]), indicator: "green", }); } else { @@ -111,24 +106,14 @@ frappe.ui.form.on("Nursing Schedule", { frappe.ui.form.on("Nurse Schedule Detail", { assignments_add: function (frm, cdt, cdn) { if (frm.doc.frequency === "Daily" && frm.doc.end_date) { - frappe.model.set_value( - cdt, - cdn, - "assignment_date", - frm.doc.end_date - ); + frappe.model.set_value(cdt, cdn, "assignment_date", frm.doc.end_date); } }, assign_based_on: function (frm, cdt, cdn) { let row = locals[cdt][cdn]; if (row.assign_based_on === "Service Unit") { - frappe.model.set_value( - cdt, - cdn, - "service_unit_type", - "" - ); + frappe.model.set_value(cdt, cdn, "service_unit_type", ""); } else { frappe.model.set_value(cdt, cdn, "service_unit", ""); } @@ -171,16 +156,10 @@ function calculate_end_date(frm) { let end_date; if (offset.days !== undefined) { - end_date = frappe.datetime.add_days( - frm.doc.start_date, - offset.days - ); + end_date = frappe.datetime.add_days(frm.doc.start_date, offset.days); } else { // Add months, then subtract 1 day to get the last day of the period - end_date = frappe.datetime.add_months( - frm.doc.start_date, - offset.months - ); + end_date = frappe.datetime.add_months(frm.doc.start_date, offset.months); end_date = frappe.datetime.add_days(end_date, -1); } diff --git a/hms_tz/patches/property_setter/property_setters_json/27_healthcare_practitioner_nursing.json b/hms_tz/patches/property_setter/property_setters_json/27_healthcare_practitioner_nursing.json new file mode 100644 index 00000000..4f265dde --- /dev/null +++ b/hms_tz/patches/property_setter/property_setters_json/27_healthcare_practitioner_nursing.json @@ -0,0 +1,19 @@ +[ + { + "name": "Healthcare Practitioner-employee-mandatory_depends_on", + "owner": "Administrator", + "creation": "2026-04-06 17:30:00.000000", + "modified": "2026-04-06 17:30:00.000000", + "modified_by": "Administrator", + "docstatus": 0, + "idx": 0, + "is_system_generated": 0, + "doctype_or_field": "DocField", + "doc_type": "Healthcare Practitioner", + "field_name": "employee", + "property": "mandatory_depends_on", + "property_type": "Data", + "value": "eval:doc.practitioner_role=='Nurse'", + "doctype": "Property Setter" + } +] From 5aec3d3b702726afcc5cb30b3ed72f0cef1930b7 Mon Sep 17 00:00:00 2001 From: elius mgani <78465509+av-dev2@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:37:07 +0300 Subject: [PATCH 14/14] feat(roster): add Nurse Roster SPA with backend API, dialog, auto-load & past date protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Add roster.py with whitelisted endpoints: - get_roster_data: fetches nurses and schedule assignments for date range - get_service_options: returns service unit types/units for company - save_roster_assignments: batch add/edit/remove roster changes - Add nursing_schedule_list.js with 'Open Roster' button on list view Frontend SPA (Design A): - CRM-style collapsible sidebar (Nurse Roster → Nursing Schedule) - Auto-load roster when all filter fields populated (no button needed) - Centered title with light blue subtitle, centered filter fields - Field order: Company → Frequency → Start Date → End Date (auto-calculated) - Dialog-based assignment with searchable autocomplete for Service Unit Type and Service Unit (supports 100+ options) - Past date protection: no edit icon, no dialog for backdated assignments - Past assignments retain same blue pill color as current/future dates - Pending changes tracked with save/discard bar - Pinia store for state management (rosterStore.js) - Vue Router route at /nurse-roster - Vite config with correct hms_tz build paths --- frontend/components.d.ts | 2 +- frontend/src/components/roster/RosterCell.vue | 312 ++++++++++++++ frontend/src/pages/NurseRoster.vue | 398 ++++++++++++++++++ frontend/src/router.js | 5 + frontend/src/stores/rosterStore.js | 202 +++++++++ frontend/vite.config.js | 4 +- .../nursing_schedule/nursing_schedule_list.js | 10 + .../hms_tz/doctype/nursing_schedule/roster.py | 195 +++++++++ 8 files changed, 1125 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/roster/RosterCell.vue create mode 100644 frontend/src/pages/NurseRoster.vue create mode 100644 frontend/src/stores/rosterStore.js create mode 100644 hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule_list.js create mode 100644 hms_tz/hms_tz/doctype/nursing_schedule/roster.py diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 77796de3..74f165ff 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -18,8 +18,8 @@ declare module 'vue' { PaymentDialog: typeof import('./src/components/appointments/PaymentDialog.vue')['default'] PractitionerCard: typeof import('./src/components/appointments/PractitionerCard.vue')['default'] Print: typeof import('./src/components/controls/Print.vue')['default'] + RosterCell: typeof import('./src/components/roster/RosterCell.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - TestComponent: typeof import('./src/components/TestComponent.vue')['default'] } } diff --git a/frontend/src/components/roster/RosterCell.vue b/frontend/src/components/roster/RosterCell.vue new file mode 100644 index 00000000..e29061b6 --- /dev/null +++ b/frontend/src/components/roster/RosterCell.vue @@ -0,0 +1,312 @@ + + + diff --git a/frontend/src/pages/NurseRoster.vue b/frontend/src/pages/NurseRoster.vue new file mode 100644 index 00000000..7c0c444b --- /dev/null +++ b/frontend/src/pages/NurseRoster.vue @@ -0,0 +1,398 @@ + + + diff --git a/frontend/src/router.js b/frontend/src/router.js index cee537fa..b7f7cfd3 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -13,6 +13,11 @@ const routes = [ name: "Appointments", component: () => import("@/pages/Appointments.vue"), }, + { + path: "/nurse-roster", + name: "NurseRoster", + component: () => import("@/pages/NurseRoster.vue"), + }, { name: "Login", path: "/account/login", diff --git a/frontend/src/stores/rosterStore.js b/frontend/src/stores/rosterStore.js new file mode 100644 index 00000000..342d1673 --- /dev/null +++ b/frontend/src/stores/rosterStore.js @@ -0,0 +1,202 @@ +import dayjs from "dayjs"; +import { createResource } from "frappe-ui"; +import { defineStore } from "pinia"; +import { computed, ref } from "vue"; + +export const useRosterStore = defineStore("roster", () => { + // Filter state + const company = ref(""); + const startDate = ref(""); + const frequency = ref("Weekly"); + const endDate = ref(""); + + // Data state + const nurses = ref([]); + const assignments = ref([]); + const serviceUnitTypes = ref([]); + const serviceUnits = ref([]); + + // Pending changes (tracked before save) + const pendingChanges = ref([]); + + // Loading state + const isLoading = ref(false); + const isSaving = ref(false); + + // Date columns + const dateColumns = computed(() => { + if (!startDate.value || !endDate.value) return []; + const cols = []; + let current = dayjs(startDate.value); + const end = dayjs(endDate.value); + while (current.isBefore(end) || current.isSame(end, "day")) { + cols.push({ + date: current.format("YYYY-MM-DD"), + label: current.format("ddd DD"), + isWeekend: + current.day() === 0 || current.day() === 6, + }); + current = current.add(1, "day"); + } + return cols; + }); + + // Build a lookup map: nurse -> date -> assignment + const assignmentMap = computed(() => { + const map = {}; + for (const a of assignments.value) { + const key = `${a.nurse}|${a.assignment_date}`; + map[key] = a; + } + // Overlay pending changes + for (const change of pendingChanges.value) { + const key = `${change.nurse}|${change.assignment_date}`; + if (change.action === "remove") { + delete map[key]; + } else { + map[key] = { ...map[key], ...change, _pending: true }; + } + } + return map; + }); + + function getAssignment(nurse, date) { + return assignmentMap.value[`${nurse}|${date}`] || null; + } + + // Calculate end date + function calculateEndDate() { + if (!startDate.value || !frequency.value) { + endDate.value = ""; + return; + } + const frequencyMap = { + Daily: { days: 0 }, + Weekly: { days: 6 }, + Monthly: { months: 1 }, + Quarterly: { months: 3 }, + "Bi-Yearly": { months: 6 }, + Yearly: { months: 12 }, + }; + const offset = frequencyMap[frequency.value]; + if (!offset) return; + + if (offset.days !== undefined) { + endDate.value = dayjs(startDate.value) + .add(offset.days, "day") + .format("YYYY-MM-DD"); + } else { + endDate.value = dayjs(startDate.value) + .add(offset.months, "month") + .subtract(1, "day") + .format("YYYY-MM-DD"); + } + } + + // API resources + const rosterDataResource = createResource({ + url: "hms_tz.hms_tz.doctype.nursing_schedule.roster.get_roster_data", + onSuccess(data) { + nurses.value = data.nurses || []; + assignments.value = data.assignments || []; + pendingChanges.value = []; + isLoading.value = false; + }, + onError() { + isLoading.value = false; + }, + }); + + const serviceOptionsResource = createResource({ + url: "hms_tz.hms_tz.doctype.nursing_schedule.roster.get_service_options", + onSuccess(data) { + serviceUnitTypes.value = data.service_unit_types || []; + serviceUnits.value = data.service_units || []; + }, + }); + + const saveResource = createResource({ + url: "hms_tz.hms_tz.doctype.nursing_schedule.roster.save_roster_assignments", + onSuccess() { + isSaving.value = false; + // Reload after save + loadRoster(); + }, + onError() { + isSaving.value = false; + }, + }); + + function loadRoster() { + if (!company.value || !startDate.value || !endDate.value) return; + isLoading.value = true; + rosterDataResource.submit({ + company: company.value, + start_date: startDate.value, + end_date: endDate.value, + }); + serviceOptionsResource.submit({ + company: company.value, + }); + } + + function addPendingChange(change) { + // Remove any existing pending change for same nurse+date + pendingChanges.value = pendingChanges.value.filter( + (c) => + !( + c.nurse === change.nurse && + c.assignment_date === change.assignment_date + ), + ); + pendingChanges.value.push(change); + } + + function removePendingChange(nurse, date) { + pendingChanges.value = pendingChanges.value.filter( + (c) => !(c.nurse === nurse && c.assignment_date === date), + ); + } + + function hasPendingChanges() { + return pendingChanges.value.length > 0; + } + + function saveRoster() { + if (!pendingChanges.value.length) return; + isSaving.value = true; + saveResource.submit({ + company: company.value, + start_date: startDate.value, + end_date: endDate.value, + frequency: frequency.value, + assignments: JSON.stringify(pendingChanges.value), + }); + } + + return { + // State + company, + startDate, + frequency, + endDate, + nurses, + assignments, + serviceUnitTypes, + serviceUnits, + pendingChanges, + isLoading, + isSaving, + // Computed + dateColumns, + assignmentMap, + // Methods + getAssignment, + calculateEndDate, + loadRoster, + addPendingChange, + removePendingChange, + hasPendingChanges, + saveRoster, + }; +}); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index f8912d81..52cd9482 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -11,7 +11,7 @@ export default defineConfig({ jinjaBootData: true, lucideIcons: true, buildConfig: { - indexHtmlPath: "..//www/frontend.html", + indexHtmlPath: "../hms_tz/www/frontend.html", emptyOutDir: true, sourcemap: true, }, @@ -20,7 +20,7 @@ export default defineConfig({ ], build: { chunkSizeWarningLimit: 1500, - outDir: "..//public/frontend", + outDir: "../hms_tz/public/frontend", emptyOutDir: true, target: "es2015", sourcemap: true, diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule_list.js b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule_list.js new file mode 100644 index 00000000..81bb07cd --- /dev/null +++ b/hms_tz/hms_tz/doctype/nursing_schedule/nursing_schedule_list.js @@ -0,0 +1,10 @@ +// Copyright (c) 2026, Aakvatech Limited and contributors +// For license information, please see license.txt + +frappe.listview_settings["Nursing Schedule"] = { + onload: function (listview) { + listview.page.add_inner_button(__("Open Roster"), function () { + window.open("/frontend/nurse-roster"); + }); + }, +}; diff --git a/hms_tz/hms_tz/doctype/nursing_schedule/roster.py b/hms_tz/hms_tz/doctype/nursing_schedule/roster.py new file mode 100644 index 00000000..8c50faa3 --- /dev/null +++ b/hms_tz/hms_tz/doctype/nursing_schedule/roster.py @@ -0,0 +1,195 @@ +# Copyright (c) 2026, Aakvatech Limited and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import add_to_date, getdate + + +@frappe.whitelist() +def get_roster_data(company: str, start_date: str, end_date: str) -> dict: + """Return nurses and their existing assignments for the given company and date range. + + Returns: + dict with keys: + - nurses: list of {name, practitioner_name, employee} + - assignments: list of {nurse, assignment_date, assign_based_on, + service_unit_type, service_unit, parent, name} + """ + if not company or not start_date or not end_date: + frappe.throw(_("Company, Start Date, and End Date are required.")) + + nurses = frappe.db.get_all( + "Healthcare Practitioner", + filters={ + "practitioner_role": "Nurse", + "hms_tz_company": company, + "status": "Active", + }, + fields=["name", "practitioner_name", "employee"], + order_by="practitioner_name asc", + ) + + nurse_names = [n.name for n in nurses] + + # Get all assignments in the date range for these nurses + assignments = [] + if nurse_names: + assignments = frappe.db.get_all( + "Nurse Schedule Detail", + filters={ + "nurse": ["in", nurse_names], + "assignment_date": ["between", [start_date, end_date]], + }, + fields=[ + "name", + "parent", + "nurse", + "nurse_name", + "assignment_date", + "assign_based_on", + "service_unit_type", + "service_unit", + ], + order_by="assignment_date asc", + ) + + return { + "nurses": nurses, + "assignments": assignments, + } + + +@frappe.whitelist() +def save_roster_assignments( + company: str, + start_date: str, + end_date: str, + frequency: str, + assignments: list | str, +) -> dict: + """Batch-save roster assignments. + + Each assignment dict should have: + - nurse: str (Healthcare Practitioner name) + - assignment_date: str (YYYY-MM-DD) + - assign_based_on: str (Service Unit Type / Service Unit) + - service_unit_type: str (optional) + - service_unit: str (optional) + - existing_name: str (optional - if editing an existing row) + - action: str (add / edit / remove) + + This function finds or creates Nursing Schedule documents for the + given company/start_date/end_date/frequency and updates their + child table rows accordingly. + """ + import json + + if isinstance(assignments, str): + assignments = json.loads(assignments) + + if not assignments: + return {"message": _("No assignments to save.")} + + # Find existing Nursing Schedule for this period, or create one + existing_schedule = frappe.db.get_value( + "Nursing Schedule", + filters={ + "company": company, + "start_date": start_date, + "end_date": end_date, + "docstatus": ["!=", 2], + }, + fieldname="name", + ) + + if existing_schedule: + schedule = frappe.get_doc("Nursing Schedule", existing_schedule) + else: + schedule = frappe.new_doc("Nursing Schedule") + schedule.company = company + schedule.frequency = frequency + schedule.start_date = start_date + schedule.end_date = end_date + + for assignment in assignments: + action = assignment.get("action", "add") + existing_name = assignment.get("existing_name") + + if action == "remove" and existing_name: + # Remove the row from the child table + schedule.assignments = [ + row for row in schedule.assignments if row.name != existing_name + ] + continue + + if action == "edit" and existing_name: + # Find and update the existing row + for row in schedule.assignments: + if row.name == existing_name: + row.assign_based_on = assignment.get("assign_based_on", "") + row.service_unit_type = assignment.get("service_unit_type", "") + row.service_unit = assignment.get("service_unit", "") + break + continue + + # action == "add" + # Validate: no duplicate nurse + date + duplicate = False + for row in schedule.assignments: + if ( + row.nurse == assignment.get("nurse") + and str(row.assignment_date) == str(assignment.get("assignment_date")) + ): + duplicate = True + break + + if duplicate: + frappe.msgprint( + _("Nurse {0} already has an assignment on {1}. Skipping.").format( + assignment.get("nurse"), assignment.get("assignment_date") + ), + alert=True, + ) + continue + + schedule.append( + "assignments", + { + "nurse": assignment.get("nurse"), + "assignment_date": assignment.get("assignment_date"), + "assign_based_on": assignment.get("assign_based_on", "Service Unit Type"), + "service_unit_type": assignment.get("service_unit_type", ""), + "service_unit": assignment.get("service_unit", ""), + }, + ) + + schedule.save(ignore_permissions=False) + + return { + "message": _("Roster saved successfully."), + "schedule_name": schedule.name, + } + + +@frappe.whitelist() +def get_service_options(company: str) -> dict: + """Return service unit types and service units for dropdowns.""" + service_unit_types = frappe.db.get_all( + "Healthcare Service Unit Type", + filters={"disabled": 0}, + fields=["name"], + order_by="name asc", + ) + + service_units = frappe.db.get_all( + "Healthcare Service Unit", + filters={"is_group": 0, "disabled": 0, "company": company}, + fields=["name", "service_unit_type"], + order_by="name asc", + ) + + return { + "service_unit_types": [s.name for s in service_unit_types], + "service_units": [{"name": s.name, "type": s.service_unit_type} for s in service_units], + }