diff --git a/cpone-dashboard/db/migrations/018_fix_patient_active_sync.sql b/cpone-dashboard/db/migrations/018_fix_patient_active_sync.sql new file mode 100644 index 0000000..12cb568 --- /dev/null +++ b/cpone-dashboard/db/migrations/018_fix_patient_active_sync.sql @@ -0,0 +1,283 @@ +-- Migration 018: Cpone-BUG-00024 +-- Fix sp_generate_dashboard_data: +-- 1. mcu_project sync (Mcu_ProjectTotalParticipant, dst) sempat hilang sejak migration 014, dikembalikan. +-- 2. mcu_patient tidak pernah di-nonaktifkan saat baris sumber (cpone.mcu_preregister_patients) +-- berubah jadi IsActive='N' (mis. duplikat/registrasi ulang) — filter WHERE ...IsActive='Y' +-- menyebabkan baris tsb tidak ikut ter-SELECT sehingga ON DUPLICATE KEY UPDATE tidak +-- pernah menyentuhnya lagi. Filter itu dihapus dan Mcu_PatientIsActive ditambahkan ke +-- ON DUPLICATE KEY UPDATE supaya status aktif selalu mengikuti kondisi sumber terkini. +-- 3. Sumber Mcu_PatientPemeriksaan diperbaiki dari p.Mcu_PreregisterPatientsPemeriksaan +-- (kolom itu tidak ada di cpone.mcu_preregister_patients) menjadi +-- p.Mcu_PreregisterPatientsOrders, yang memang tersedia di skema sumber. +-- 4. Kolom PK di INSERT INTO mcu_checkinout diperbaiki dari Mcu_CheckinoutCheckinoutID +-- (tidak ada) menjadi Mcu_CheckinoutID, sesuai skema cpone_dashboard.mcu_checkinout +-- yang sebenarnya. +-- 5. Mcu_ProjectTotalParticipant tidak lagi ikut m.Mgm_McuTotalParticipant (field target +-- manual di source yang bisa 0/belum diisi), melainkan dihitung langsung dari COUNT +-- baris aktif di cpone.mcu_preregister_patients untuk project tsb. + +DROP PROCEDURE IF EXISTS sp_generate_dashboard_data; + +DELIMITER $$ + +CREATE PROCEDURE sp_generate_dashboard_data(IN p_mcu_id INT) +BEGIN + + -- ---------------------------------------------------------- + -- 1. mcu_project + -- ---------------------------------------------------------- + INSERT INTO mcu_project ( + Mcu_ProjectMcuID, Mcu_ProjectCorporateID, Mcu_ProjectCorporateName, + Mcu_ProjectNumber, Mcu_ProjectLabel, Mcu_ProjectBranchID, + Mcu_ProjectStartDate, Mcu_ProjectEndDate, Mcu_ProjectIsActive, + Mcu_ProjectTotalParticipant, Mcu_ProjectSyncedAt + ) + SELECT + m.Mgm_McuID, m.Mgm_McuCorporateID, c.CorporateName, + m.Mgm_McuNumber, m.Mgm_McuLabel, m.Mgm_McuM_BranchID, + m.Mgm_McuStartDate, m.Mgm_McuEndDate, m.Mgm_McuIsActive, + COALESCE(pp.total, 0), NOW() + FROM cpone.mgm_mcu m + LEFT JOIN cpone.corporate c ON c.CorporateID = m.Mgm_McuCorporateID + LEFT JOIN ( + SELECT Mcu_PreregisterPatientsMgm_McuID AS mcu_id, COUNT(*) AS total + FROM cpone.mcu_preregister_patients + WHERE Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY Mcu_PreregisterPatientsMgm_McuID + ) pp ON pp.mcu_id = m.Mgm_McuID + WHERE m.Mgm_McuID = p_mcu_id + ON DUPLICATE KEY UPDATE + Mcu_ProjectCorporateName = VALUES(Mcu_ProjectCorporateName), + Mcu_ProjectLabel = VALUES(Mcu_ProjectLabel), + Mcu_ProjectIsActive = VALUES(Mcu_ProjectIsActive), + Mcu_ProjectTotalParticipant = VALUES(Mcu_ProjectTotalParticipant), + Mcu_ProjectSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 2. mcu_patient (with age + pemeriksaan) + -- Tidak lagi memfilter IsActive='Y' di WHERE, supaya baris yang berubah + -- jadi inactive di sumber tetap ke-SELECT dan status-nya ikut ter-update. + -- ---------------------------------------------------------- + INSERT INTO mcu_patient ( + Mcu_PatientPreregisterID, Mcu_PatientMcuID, Mcu_PatientName, + Mcu_PatientNIP, Mcu_PatientGender, Mcu_PatientDOB, Mcu_PatientAge, + Mcu_PatientDepartment, Mcu_PatientDivision, Mcu_PatientPosisi, + Mcu_PatientPemeriksaan, + Mcu_PatientIsRegistered, Mcu_PatientOrderID, Mcu_PatientIsActive, + Mcu_PatientSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, p.Mcu_PreregisterPatientsMgm_McuID, + p.Mcu_PreregisterPatientsPatientName, p.Mcu_PreregisterPatientsNIP, + p.Mcu_PreregisterPatientsGender, p.Mcu_PreregisterPatientsDOB, + CAST(o.T_OrderHeaderM_PatientAge AS UNSIGNED), + p.Mcu_PreregisterPatientsDepartment, p.Mcu_PreregisterPatientsDivisi, + p.Mcu_PreregisterPatientsPosisi, + p.Mcu_PreregisterPatientsOrders, + p.Mcu_PreregisterPatientsIsRegistered, p.Mcu_PreregisterPatientsT_OrderHeaderID, + p.Mcu_PreregisterPatientsIsActive, NOW() + FROM cpone.mcu_preregister_patients p + LEFT JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + ON DUPLICATE KEY UPDATE + Mcu_PatientName = VALUES(Mcu_PatientName), + Mcu_PatientAge = VALUES(Mcu_PatientAge), + Mcu_PatientPemeriksaan = VALUES(Mcu_PatientPemeriksaan), + Mcu_PatientIsRegistered = VALUES(Mcu_PatientIsRegistered), + Mcu_PatientOrderID = VALUES(Mcu_PatientOrderID), + Mcu_PatientIsActive = VALUES(Mcu_PatientIsActive), + Mcu_PatientSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 3. mcu_patient_schedule + -- ---------------------------------------------------------- + INSERT INTO mcu_patient_schedule ( + Mcu_PatientSchedulePreregisterID, Mcu_PatientScheduleDate, + Mcu_PatientScheduleIsActive, Mcu_PatientScheduleSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, DATE(o.T_OrderHeaderDate), 'Y', NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_PatientScheduleSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 4. mcu_checkinout + -- ---------------------------------------------------------- + INSERT INTO mcu_checkinout ( + Mcu_CheckinoutID, Mcu_CheckinoutPreregisterID, Mcu_CheckinoutOrderID, + Mcu_CheckinoutDate, Mcu_CheckinoutInTime, Mcu_CheckinoutOutTime, + Mcu_CheckinoutNextDate, Mcu_CheckinoutIsActive, Mcu_CheckinoutSyncedAt, Mcu_CheckinoutMcuID + ) + SELECT + src.order_id, + src.preregister_id, + src.order_id, + src.segment_date, + TIME(src.checkin_dt), + TIME(src.checkout_dt), + CASE + WHEN DATE(src.checkout_dt) = src.segment_date THEN NULL + ELSE DATE(src.checkout_dt) + END, + 'Y', NOW(), p_mcu_id + FROM ( + SELECT + o.T_OrderHeaderID AS order_id, + p.Mcu_PreregisterPatientsID AS preregister_id, + o.T_OrderHeaderDate AS header_dt, + ev.segment_date, + CASE + WHEN ev.segment_date = DATE(o.T_OrderHeaderDate) THEN o.T_OrderHeaderDate + ELSE MIN(ev.event_dt) + END AS checkin_dt, + MAX(ev.event_dt) AS checkout_dt + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN ( + SELECT p2.Mcu_PreregisterPatientsID AS preregister_id, o2.T_OrderHeaderID AS order_id, + o2.T_OrderHeaderDate AS checkin_dt, DATE(o2.T_OrderHeaderDate) AS segment_date, + o2.T_OrderHeaderDate AS event_dt + FROM cpone.mcu_preregister_patients p2 + JOIN cpone.t_orderheader o2 ON o2.T_OrderHeaderID = p2.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p2.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p2.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p2.Mcu_PreregisterPatientsIsActive = 'Y' + UNION ALL + SELECT p2.Mcu_PreregisterPatientsID, o2.T_OrderHeaderID, o2.T_OrderHeaderDate, + DATE(TIMESTAMP(s2.T_OrderSampleReceiveDate, s2.T_OrderSampleReceiveTime)), + TIMESTAMP(s2.T_OrderSampleReceiveDate, s2.T_OrderSampleReceiveTime) + FROM cpone.mcu_preregister_patients p2 + JOIN cpone.t_orderheader o2 ON o2.T_OrderHeaderID = p2.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_ordersample s2 ON s2.T_OrderSampleT_OrderHeaderID = o2.T_OrderHeaderID + AND s2.T_OrderSampleIsActive = 'Y' + WHERE p2.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p2.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p2.Mcu_PreregisterPatientsIsActive = 'Y' + AND s2.T_OrderSampleReceiveDate IS NOT NULL + AND s2.T_OrderSampleReceiveTime IS NOT NULL + UNION ALL + SELECT p2.Mcu_PreregisterPatientsID, o2.T_OrderHeaderID, o2.T_OrderHeaderDate, + DATE(TIMESTAMP(ts2.T_SamplingSoDoneDate, ts2.T_SamplingSoDoneTime)), + TIMESTAMP(ts2.T_SamplingSoDoneDate, ts2.T_SamplingSoDoneTime) + FROM cpone.mcu_preregister_patients p2 + JOIN cpone.t_orderheader o2 ON o2.T_OrderHeaderID = p2.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_samplingso ts2 ON ts2.T_SamplingSoT_OrderHeaderID = o2.T_OrderHeaderID + AND ts2.T_SamplingSoIsActive = 'Y' + WHERE p2.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p2.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p2.Mcu_PreregisterPatientsIsActive = 'Y' + AND ts2.T_SamplingSoDoneDate IS NOT NULL + AND ts2.T_SamplingSoDoneTime IS NOT NULL + ) ev + ON ev.preregister_id = p.Mcu_PreregisterPatientsID + AND ev.order_id = o.T_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, o.T_OrderHeaderDate, ev.segment_date + ) AS src + ON DUPLICATE KEY UPDATE + Mcu_CheckinoutOutTime = VALUES(Mcu_CheckinoutOutTime), + Mcu_CheckinoutNextDate = VALUES(Mcu_CheckinoutNextDate), + Mcu_CheckinoutSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 5. mcu_station_progress — lab + -- ---------------------------------------------------------- + INSERT INTO mcu_station_progress ( + Mcu_StationProgressOrderID, Mcu_StationProgressPreregisterID, Mcu_StationProgressMcuID, + Mcu_StationProgressStationID, Mcu_StationProgressStationName, Mcu_StationProgressSource, + Mcu_StationProgressCheckinDate, + Mcu_StationProgressSamplingAt, Mcu_StationProgressReceiveAt, Mcu_StationProgressProcessAt, + Mcu_StationProgressDoneAt, Mcu_StationProgressSyncedAt + ) + SELECT + o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, p_mcu_id, + s.T_OrderSampleT_SampleStationID, ss.T_SampleStationName, 'lab', + DATE(o.T_OrderHeaderDate), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleSamplingDate, s.T_OrderSampleSamplingTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleReceiveDate, s.T_OrderSampleReceiveTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleProcessingDate, s.T_OrderSampleProcessingTime)), '0000-00-00 00:00:00'), + NULL, NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_ordersample s ON s.T_OrderSampleT_OrderHeaderID = o.T_OrderHeaderID AND s.T_OrderSampleIsActive = 'Y' + JOIN cpone.t_samplestation ss ON ss.T_SampleStationID = s.T_OrderSampleT_SampleStationID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, s.T_OrderSampleT_SampleStationID, ss.T_SampleStationName, o.T_OrderHeaderDate + ON DUPLICATE KEY UPDATE + Mcu_StationProgressSamplingAt = VALUES(Mcu_StationProgressSamplingAt), + Mcu_StationProgressReceiveAt = VALUES(Mcu_StationProgressReceiveAt), + Mcu_StationProgressProcessAt = VALUES(Mcu_StationProgressProcessAt), + Mcu_StationProgressSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 6. mcu_station_progress — nonlab + -- ---------------------------------------------------------- + INSERT INTO mcu_station_progress ( + Mcu_StationProgressOrderID, Mcu_StationProgressPreregisterID, Mcu_StationProgressMcuID, + Mcu_StationProgressStationID, Mcu_StationProgressStationName, Mcu_StationProgressSource, + Mcu_StationProgressCheckinDate, + Mcu_StationProgressSamplingAt, Mcu_StationProgressReceiveAt, Mcu_StationProgressProcessAt, + Mcu_StationProgressDoneAt, Mcu_StationProgressSyncedAt + ) + SELECT + o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, p_mcu_id, + ts.T_SamplingSoT_SampleStationID, ss.T_SampleStationName, 'nonlab', + DATE(o.T_OrderHeaderDate), + NULL, NULL, + NULLIF(MAX(TIMESTAMP(ts.T_SamplingSoProcessDate, ts.T_SamplingSoProcessTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(ts.T_SamplingSoDoneDate, ts.T_SamplingSoDoneTime)), '0000-00-00 00:00:00'), + NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_samplingso ts ON ts.T_SamplingSoT_OrderHeaderID = o.T_OrderHeaderID AND ts.T_SamplingSoIsActive = 'Y' + JOIN cpone.t_samplestation ss ON ss.T_SampleStationID = ts.T_SamplingSoT_SampleStationID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, ts.T_SamplingSoT_SampleStationID, ss.T_SampleStationName, o.T_OrderHeaderDate + ON DUPLICATE KEY UPDATE + Mcu_StationProgressProcessAt = VALUES(Mcu_StationProgressProcessAt), + Mcu_StationProgressDoneAt = VALUES(Mcu_StationProgressDoneAt), + Mcu_StationProgressSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 7. mcu_patient_resume_status + -- ---------------------------------------------------------- + INSERT INTO mcu_patient_resume_status ( + Mcu_PatientResumeStatusPreregisterID, Mcu_PatientResumeStatusMcuID, + Mcu_PatientResumeStatusStatus, Mcu_PatientResumeStatusValidated, + Mcu_PatientResumeStatusPublished, Mcu_PatientResumeSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, p_mcu_id, + r.Mcu_ResumeStatus, r.Mcu_ResumeValidation, 'N', NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + LEFT JOIN cpone.mcu_resume r ON r.Mcu_ResumeT_OrderHeaderID = o.T_OrderHeaderID AND r.Mcu_ResumeIsActive = 'Y' + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_PatientResumeStatusStatus = VALUES(Mcu_PatientResumeStatusStatus), + Mcu_PatientResumeStatusValidated = VALUES(Mcu_PatientResumeStatusValidated), + Mcu_PatientResumeSyncedAt = NOW(); + +END $$ + +DELIMITER ; diff --git a/cpone-dashboard/db/migrations/019_fix_checkinout_source.sql b/cpone-dashboard/db/migrations/019_fix_checkinout_source.sql new file mode 100644 index 0000000..34ffc09 --- /dev/null +++ b/cpone-dashboard/db/migrations/019_fix_checkinout_source.sql @@ -0,0 +1,246 @@ +-- Migration 019: Cpone-BUG-00024 +-- Fix sp_generate_dashboard_data step 4 (mcu_checkinout): +-- Sebelumnya Mcu_CheckinoutOutTime "ditebak" dari MAX(event_dt) gabungan +-- T_OrderHeaderDate, T_OrderSampleReceiveDate/Time, dan T_SamplingSoDoneDate/Time. +-- Ini SALAH karena event2 itu adalah aktivitas lab, bukan checkout fisik pasien — +-- akibatnya pasien yang belum checkout (masih di lokasi) bisa muncul sebagai +-- "Checked Out" di dashboard hanya karena sample-nya sudah diterima/diproses lab. +-- +-- Sumber checkout yang benar adalah cpone.preregister_checkin_checkout (lihat +-- komentar kolom di db/migrations/001_init_schema.sql yang memang sudah +-- mendokumentasikan tabel ini sebagai sumber asli mcu_checkinout, tapi belum +-- pernah dipakai oleh stored procedure manapun sebelumnya). +-- PreregisterCheckInCheckOutDate = '0000-00-00' berarti pasien tsb belum checkout. +-- +-- Efek samping perbaikan ini: Mcu_CheckinoutID tidak lagi dipaksa = order_id, +-- supaya project multi-hari (1 preregister/order bisa checkin-checkout di +-- beberapa tanggal berbeda) tidak saling bentrok pada primary key. Idempotensi +-- re-sync tetap dijaga oleh UNIQUE KEY uq_checkinout_segment +-- (Mcu_CheckinoutPreregisterID, Mcu_CheckinoutDate, Mcu_CheckinoutInTime). + +DROP PROCEDURE IF EXISTS sp_generate_dashboard_data; + +DELIMITER $$ + +CREATE PROCEDURE sp_generate_dashboard_data(IN p_mcu_id INT) +BEGIN + + -- ---------------------------------------------------------- + -- 1. mcu_project + -- ---------------------------------------------------------- + INSERT INTO mcu_project ( + Mcu_ProjectMcuID, Mcu_ProjectCorporateID, Mcu_ProjectCorporateName, + Mcu_ProjectNumber, Mcu_ProjectLabel, Mcu_ProjectBranchID, + Mcu_ProjectStartDate, Mcu_ProjectEndDate, Mcu_ProjectIsActive, + Mcu_ProjectTotalParticipant, Mcu_ProjectSyncedAt + ) + SELECT + m.Mgm_McuID, m.Mgm_McuCorporateID, c.CorporateName, + m.Mgm_McuNumber, m.Mgm_McuLabel, m.Mgm_McuM_BranchID, + m.Mgm_McuStartDate, m.Mgm_McuEndDate, m.Mgm_McuIsActive, + COALESCE(pp.total, 0), NOW() + FROM cpone.mgm_mcu m + LEFT JOIN cpone.corporate c ON c.CorporateID = m.Mgm_McuCorporateID + LEFT JOIN ( + SELECT Mcu_PreregisterPatientsMgm_McuID AS mcu_id, COUNT(*) AS total + FROM cpone.mcu_preregister_patients + WHERE Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY Mcu_PreregisterPatientsMgm_McuID + ) pp ON pp.mcu_id = m.Mgm_McuID + WHERE m.Mgm_McuID = p_mcu_id + ON DUPLICATE KEY UPDATE + Mcu_ProjectCorporateName = VALUES(Mcu_ProjectCorporateName), + Mcu_ProjectLabel = VALUES(Mcu_ProjectLabel), + Mcu_ProjectIsActive = VALUES(Mcu_ProjectIsActive), + Mcu_ProjectTotalParticipant = VALUES(Mcu_ProjectTotalParticipant), + Mcu_ProjectSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 2. mcu_patient (with age + pemeriksaan) + -- ---------------------------------------------------------- + INSERT INTO mcu_patient ( + Mcu_PatientPreregisterID, Mcu_PatientMcuID, Mcu_PatientName, + Mcu_PatientNIP, Mcu_PatientGender, Mcu_PatientDOB, Mcu_PatientAge, + Mcu_PatientDepartment, Mcu_PatientDivision, Mcu_PatientPosisi, + Mcu_PatientPemeriksaan, + Mcu_PatientIsRegistered, Mcu_PatientOrderID, Mcu_PatientIsActive, + Mcu_PatientSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, p.Mcu_PreregisterPatientsMgm_McuID, + p.Mcu_PreregisterPatientsPatientName, p.Mcu_PreregisterPatientsNIP, + p.Mcu_PreregisterPatientsGender, p.Mcu_PreregisterPatientsDOB, + CAST(o.T_OrderHeaderM_PatientAge AS UNSIGNED), + p.Mcu_PreregisterPatientsDepartment, p.Mcu_PreregisterPatientsDivisi, + p.Mcu_PreregisterPatientsPosisi, + p.Mcu_PreregisterPatientsOrders, + p.Mcu_PreregisterPatientsIsRegistered, p.Mcu_PreregisterPatientsT_OrderHeaderID, + p.Mcu_PreregisterPatientsIsActive, NOW() + FROM cpone.mcu_preregister_patients p + LEFT JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + ON DUPLICATE KEY UPDATE + Mcu_PatientName = VALUES(Mcu_PatientName), + Mcu_PatientAge = VALUES(Mcu_PatientAge), + Mcu_PatientPemeriksaan = VALUES(Mcu_PatientPemeriksaan), + Mcu_PatientIsRegistered = VALUES(Mcu_PatientIsRegistered), + Mcu_PatientOrderID = VALUES(Mcu_PatientOrderID), + Mcu_PatientIsActive = VALUES(Mcu_PatientIsActive), + Mcu_PatientSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 3. mcu_patient_schedule + -- ---------------------------------------------------------- + INSERT INTO mcu_patient_schedule ( + Mcu_PatientSchedulePreregisterID, Mcu_PatientScheduleDate, + Mcu_PatientScheduleIsActive, Mcu_PatientScheduleSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, DATE(o.T_OrderHeaderDate), 'Y', NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_PatientScheduleSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 4. mcu_checkinout + -- Source: cpone.preregister_checkin_checkout (log checkin/checkout asli + -- dari kiosk/front-desk) — BUKAN ditebak dari event lab. + -- CheckOutDate = '0000-00-00' artinya pasien belum checkout. + -- ---------------------------------------------------------- + INSERT INTO mcu_checkinout ( + Mcu_CheckinoutPreregisterID, Mcu_CheckinoutOrderID, + Mcu_CheckinoutDate, Mcu_CheckinoutInTime, Mcu_CheckinoutOutTime, + Mcu_CheckinoutOutUserID, Mcu_CheckinoutNextDate, Mcu_CheckinoutNote, + Mcu_CheckinoutIsActive, Mcu_CheckinoutSyncedAt, Mcu_CheckinoutMcuID + ) + SELECT + pcc.PreregisterCheckInCheckOutPreregisterID, + pcc.PreregisterCheckInCheckOutT_OrderHeaderID, + pcc.PreregisterCheckInCheckInDate, + pcc.PreregisterCheckInCheckOutInTime, + CASE + WHEN pcc.PreregisterCheckInCheckOutDate = '0000-00-00' THEN NULL + ELSE pcc.PreregisterCheckInCheckOutOutTime + END, + pcc.PreregisterCheckInCheckOutOutUserID, + CASE + WHEN pcc.PreregisterCheckInCheckOutNextPreregisterDate = '0000-00-00' THEN NULL + ELSE pcc.PreregisterCheckInCheckOutNextPreregisterDate + END, + NULLIF(pcc.PreregisterCheckInCheckOutNote, ''), + pcc.PreregisterCheckInCheckOutIsActive, + NOW(), + p_mcu_id + FROM cpone.preregister_checkin_checkout pcc + JOIN cpone.mcu_preregister_patients p + ON p.Mcu_PreregisterPatientsID = pcc.PreregisterCheckInCheckOutPreregisterID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_CheckinoutOrderID = VALUES(Mcu_CheckinoutOrderID), + Mcu_CheckinoutOutTime = VALUES(Mcu_CheckinoutOutTime), + Mcu_CheckinoutOutUserID = VALUES(Mcu_CheckinoutOutUserID), + Mcu_CheckinoutNextDate = VALUES(Mcu_CheckinoutNextDate), + Mcu_CheckinoutNote = VALUES(Mcu_CheckinoutNote), + Mcu_CheckinoutIsActive = VALUES(Mcu_CheckinoutIsActive), + Mcu_CheckinoutSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 5. mcu_station_progress — lab + -- ---------------------------------------------------------- + INSERT INTO mcu_station_progress ( + Mcu_StationProgressOrderID, Mcu_StationProgressPreregisterID, Mcu_StationProgressMcuID, + Mcu_StationProgressStationID, Mcu_StationProgressStationName, Mcu_StationProgressSource, + Mcu_StationProgressCheckinDate, + Mcu_StationProgressSamplingAt, Mcu_StationProgressReceiveAt, Mcu_StationProgressProcessAt, + Mcu_StationProgressDoneAt, Mcu_StationProgressSyncedAt + ) + SELECT + o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, p_mcu_id, + s.T_OrderSampleT_SampleStationID, ss.T_SampleStationName, 'lab', + DATE(o.T_OrderHeaderDate), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleSamplingDate, s.T_OrderSampleSamplingTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleReceiveDate, s.T_OrderSampleReceiveTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleProcessingDate, s.T_OrderSampleProcessingTime)), '0000-00-00 00:00:00'), + NULL, NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_ordersample s ON s.T_OrderSampleT_OrderHeaderID = o.T_OrderHeaderID AND s.T_OrderSampleIsActive = 'Y' + JOIN cpone.t_samplestation ss ON ss.T_SampleStationID = s.T_OrderSampleT_SampleStationID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, s.T_OrderSampleT_SampleStationID, ss.T_SampleStationName, o.T_OrderHeaderDate + ON DUPLICATE KEY UPDATE + Mcu_StationProgressSamplingAt = VALUES(Mcu_StationProgressSamplingAt), + Mcu_StationProgressReceiveAt = VALUES(Mcu_StationProgressReceiveAt), + Mcu_StationProgressProcessAt = VALUES(Mcu_StationProgressProcessAt), + Mcu_StationProgressSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 6. mcu_station_progress — nonlab + -- ---------------------------------------------------------- + INSERT INTO mcu_station_progress ( + Mcu_StationProgressOrderID, Mcu_StationProgressPreregisterID, Mcu_StationProgressMcuID, + Mcu_StationProgressStationID, Mcu_StationProgressStationName, Mcu_StationProgressSource, + Mcu_StationProgressCheckinDate, + Mcu_StationProgressSamplingAt, Mcu_StationProgressReceiveAt, Mcu_StationProgressProcessAt, + Mcu_StationProgressDoneAt, Mcu_StationProgressSyncedAt + ) + SELECT + o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, p_mcu_id, + ts.T_SamplingSoT_SampleStationID, ss.T_SampleStationName, 'nonlab', + DATE(o.T_OrderHeaderDate), + NULL, NULL, + NULLIF(MAX(TIMESTAMP(ts.T_SamplingSoProcessDate, ts.T_SamplingSoProcessTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(ts.T_SamplingSoDoneDate, ts.T_SamplingSoDoneTime)), '0000-00-00 00:00:00'), + NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_samplingso ts ON ts.T_SamplingSoT_OrderHeaderID = o.T_OrderHeaderID AND ts.T_SamplingSoIsActive = 'Y' + JOIN cpone.t_samplestation ss ON ss.T_SampleStationID = ts.T_SamplingSoT_SampleStationID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, ts.T_SamplingSoT_SampleStationID, ss.T_SampleStationName, o.T_OrderHeaderDate + ON DUPLICATE KEY UPDATE + Mcu_StationProgressProcessAt = VALUES(Mcu_StationProgressProcessAt), + Mcu_StationProgressDoneAt = VALUES(Mcu_StationProgressDoneAt), + Mcu_StationProgressSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 7. mcu_patient_resume_status + -- ---------------------------------------------------------- + INSERT INTO mcu_patient_resume_status ( + Mcu_PatientResumeStatusPreregisterID, Mcu_PatientResumeStatusMcuID, + Mcu_PatientResumeStatusStatus, Mcu_PatientResumeStatusValidated, + Mcu_PatientResumeStatusPublished, Mcu_PatientResumeSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, p_mcu_id, + r.Mcu_ResumeStatus, r.Mcu_ResumeValidation, 'N', NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + LEFT JOIN cpone.mcu_resume r ON r.Mcu_ResumeT_OrderHeaderID = o.T_OrderHeaderID AND r.Mcu_ResumeIsActive = 'Y' + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_PatientResumeStatusStatus = VALUES(Mcu_PatientResumeStatusStatus), + Mcu_PatientResumeStatusValidated = VALUES(Mcu_PatientResumeStatusValidated), + Mcu_PatientResumeSyncedAt = NOW(); + +END $$ + +DELIMITER ; diff --git a/cpone-dashboard/db/migrations/020_fix_patient_demographic_sync.sql b/cpone-dashboard/db/migrations/020_fix_patient_demographic_sync.sql new file mode 100644 index 0000000..35629ed --- /dev/null +++ b/cpone-dashboard/db/migrations/020_fix_patient_demographic_sync.sql @@ -0,0 +1,247 @@ +-- Migration 020: BUG-DASH-280826 +-- Fix sp_generate_dashboard_data step 2 (mcu_patient): +-- ON DUPLICATE KEY UPDATE tidak menyertakan Mcu_PatientNIP, Mcu_PatientGender, +-- Mcu_PatientDOB, Mcu_PatientDepartment, Mcu_PatientDivision, Mcu_PatientPosisi. +-- Akibatnya begitu satu preregister ID pertama kali ke-sync, kolom2 tsb beku +-- selamanya walau datanya dikoreksi di source (cpone.mcu_preregister_patients) — +-- misalnya NIP pasien yang salah ketik lalu dibetulkan di source tidak pernah +-- ikut terupdate di dashboard. +-- +-- Tambahkan kolom2 itu ke ON DUPLICATE KEY UPDATE supaya selalu mengikuti +-- kondisi sumber terkini, konsisten dengan Mcu_PatientName/Mcu_PatientIsActive +-- yang memang sudah di-refresh setiap sync. + +DROP PROCEDURE IF EXISTS sp_generate_dashboard_data; + +DELIMITER $$ + +CREATE PROCEDURE sp_generate_dashboard_data(IN p_mcu_id INT) +BEGIN + + -- ---------------------------------------------------------- + -- 1. mcu_project + -- ---------------------------------------------------------- + INSERT INTO mcu_project ( + Mcu_ProjectMcuID, Mcu_ProjectCorporateID, Mcu_ProjectCorporateName, + Mcu_ProjectNumber, Mcu_ProjectLabel, Mcu_ProjectBranchID, + Mcu_ProjectStartDate, Mcu_ProjectEndDate, Mcu_ProjectIsActive, + Mcu_ProjectTotalParticipant, Mcu_ProjectSyncedAt + ) + SELECT + m.Mgm_McuID, m.Mgm_McuCorporateID, c.CorporateName, + m.Mgm_McuNumber, m.Mgm_McuLabel, m.Mgm_McuM_BranchID, + m.Mgm_McuStartDate, m.Mgm_McuEndDate, m.Mgm_McuIsActive, + COALESCE(pp.total, 0), NOW() + FROM cpone.mgm_mcu m + LEFT JOIN cpone.corporate c ON c.CorporateID = m.Mgm_McuCorporateID + LEFT JOIN ( + SELECT Mcu_PreregisterPatientsMgm_McuID AS mcu_id, COUNT(*) AS total + FROM cpone.mcu_preregister_patients + WHERE Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY Mcu_PreregisterPatientsMgm_McuID + ) pp ON pp.mcu_id = m.Mgm_McuID + WHERE m.Mgm_McuID = p_mcu_id + ON DUPLICATE KEY UPDATE + Mcu_ProjectCorporateName = VALUES(Mcu_ProjectCorporateName), + Mcu_ProjectLabel = VALUES(Mcu_ProjectLabel), + Mcu_ProjectIsActive = VALUES(Mcu_ProjectIsActive), + Mcu_ProjectTotalParticipant = VALUES(Mcu_ProjectTotalParticipant), + Mcu_ProjectSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 2. mcu_patient (with age + pemeriksaan) + -- Semua kolom demografis (NIP, gender, DOB, department, divisi, posisi) + -- ikut di-refresh setiap sync supaya koreksi di source selalu terbawa. + -- ---------------------------------------------------------- + INSERT INTO mcu_patient ( + Mcu_PatientPreregisterID, Mcu_PatientMcuID, Mcu_PatientName, + Mcu_PatientNIP, Mcu_PatientGender, Mcu_PatientDOB, Mcu_PatientAge, + Mcu_PatientDepartment, Mcu_PatientDivision, Mcu_PatientPosisi, + Mcu_PatientPemeriksaan, + Mcu_PatientIsRegistered, Mcu_PatientOrderID, Mcu_PatientIsActive, + Mcu_PatientSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, p.Mcu_PreregisterPatientsMgm_McuID, + p.Mcu_PreregisterPatientsPatientName, p.Mcu_PreregisterPatientsNIP, + p.Mcu_PreregisterPatientsGender, p.Mcu_PreregisterPatientsDOB, + CAST(o.T_OrderHeaderM_PatientAge AS UNSIGNED), + p.Mcu_PreregisterPatientsDepartment, p.Mcu_PreregisterPatientsDivisi, + p.Mcu_PreregisterPatientsPosisi, + p.Mcu_PreregisterPatientsOrders, + p.Mcu_PreregisterPatientsIsRegistered, p.Mcu_PreregisterPatientsT_OrderHeaderID, + p.Mcu_PreregisterPatientsIsActive, NOW() + FROM cpone.mcu_preregister_patients p + LEFT JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + ON DUPLICATE KEY UPDATE + Mcu_PatientName = VALUES(Mcu_PatientName), + Mcu_PatientNIP = VALUES(Mcu_PatientNIP), + Mcu_PatientGender = VALUES(Mcu_PatientGender), + Mcu_PatientDOB = VALUES(Mcu_PatientDOB), + Mcu_PatientAge = VALUES(Mcu_PatientAge), + Mcu_PatientDepartment = VALUES(Mcu_PatientDepartment), + Mcu_PatientDivision = VALUES(Mcu_PatientDivision), + Mcu_PatientPosisi = VALUES(Mcu_PatientPosisi), + Mcu_PatientPemeriksaan = VALUES(Mcu_PatientPemeriksaan), + Mcu_PatientIsRegistered = VALUES(Mcu_PatientIsRegistered), + Mcu_PatientOrderID = VALUES(Mcu_PatientOrderID), + Mcu_PatientIsActive = VALUES(Mcu_PatientIsActive), + Mcu_PatientSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 3. mcu_patient_schedule + -- ---------------------------------------------------------- + INSERT INTO mcu_patient_schedule ( + Mcu_PatientSchedulePreregisterID, Mcu_PatientScheduleDate, + Mcu_PatientScheduleIsActive, Mcu_PatientScheduleSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, DATE(o.T_OrderHeaderDate), 'Y', NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_PatientScheduleSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 4. mcu_checkinout + -- Source: cpone.preregister_checkin_checkout (log checkin/checkout asli + -- dari kiosk/front-desk) — BUKAN ditebak dari event lab. + -- CheckOutDate = '0000-00-00' artinya pasien belum checkout. + -- ---------------------------------------------------------- + INSERT INTO mcu_checkinout ( + Mcu_CheckinoutPreregisterID, Mcu_CheckinoutOrderID, + Mcu_CheckinoutDate, Mcu_CheckinoutInTime, Mcu_CheckinoutOutTime, + Mcu_CheckinoutOutUserID, Mcu_CheckinoutNextDate, Mcu_CheckinoutNote, + Mcu_CheckinoutIsActive, Mcu_CheckinoutSyncedAt, Mcu_CheckinoutMcuID + ) + SELECT + pcc.PreregisterCheckInCheckOutPreregisterID, + pcc.PreregisterCheckInCheckOutT_OrderHeaderID, + pcc.PreregisterCheckInCheckInDate, + pcc.PreregisterCheckInCheckOutInTime, + CASE + WHEN pcc.PreregisterCheckInCheckOutDate = '0000-00-00' THEN NULL + ELSE pcc.PreregisterCheckInCheckOutOutTime + END, + pcc.PreregisterCheckInCheckOutOutUserID, + CASE + WHEN pcc.PreregisterCheckInCheckOutNextPreregisterDate = '0000-00-00' THEN NULL + ELSE pcc.PreregisterCheckInCheckOutNextPreregisterDate + END, + NULLIF(pcc.PreregisterCheckInCheckOutNote, ''), + pcc.PreregisterCheckInCheckOutIsActive, + NOW(), + p_mcu_id + FROM cpone.preregister_checkin_checkout pcc + JOIN cpone.mcu_preregister_patients p + ON p.Mcu_PreregisterPatientsID = pcc.PreregisterCheckInCheckOutPreregisterID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_CheckinoutOrderID = VALUES(Mcu_CheckinoutOrderID), + Mcu_CheckinoutOutTime = VALUES(Mcu_CheckinoutOutTime), + Mcu_CheckinoutOutUserID = VALUES(Mcu_CheckinoutOutUserID), + Mcu_CheckinoutNextDate = VALUES(Mcu_CheckinoutNextDate), + Mcu_CheckinoutNote = VALUES(Mcu_CheckinoutNote), + Mcu_CheckinoutIsActive = VALUES(Mcu_CheckinoutIsActive), + Mcu_CheckinoutSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 5. mcu_station_progress — lab + -- ---------------------------------------------------------- + INSERT INTO mcu_station_progress ( + Mcu_StationProgressOrderID, Mcu_StationProgressPreregisterID, Mcu_StationProgressMcuID, + Mcu_StationProgressStationID, Mcu_StationProgressStationName, Mcu_StationProgressSource, + Mcu_StationProgressCheckinDate, + Mcu_StationProgressSamplingAt, Mcu_StationProgressReceiveAt, Mcu_StationProgressProcessAt, + Mcu_StationProgressDoneAt, Mcu_StationProgressSyncedAt + ) + SELECT + o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, p_mcu_id, + s.T_OrderSampleT_SampleStationID, ss.T_SampleStationName, 'lab', + DATE(o.T_OrderHeaderDate), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleSamplingDate, s.T_OrderSampleSamplingTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleReceiveDate, s.T_OrderSampleReceiveTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(s.T_OrderSampleProcessingDate, s.T_OrderSampleProcessingTime)), '0000-00-00 00:00:00'), + NULL, NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_ordersample s ON s.T_OrderSampleT_OrderHeaderID = o.T_OrderHeaderID AND s.T_OrderSampleIsActive = 'Y' + JOIN cpone.t_samplestation ss ON ss.T_SampleStationID = s.T_OrderSampleT_SampleStationID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, s.T_OrderSampleT_SampleStationID, ss.T_SampleStationName, o.T_OrderHeaderDate + ON DUPLICATE KEY UPDATE + Mcu_StationProgressSamplingAt = VALUES(Mcu_StationProgressSamplingAt), + Mcu_StationProgressReceiveAt = VALUES(Mcu_StationProgressReceiveAt), + Mcu_StationProgressProcessAt = VALUES(Mcu_StationProgressProcessAt), + Mcu_StationProgressSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 6. mcu_station_progress — nonlab + -- ---------------------------------------------------------- + INSERT INTO mcu_station_progress ( + Mcu_StationProgressOrderID, Mcu_StationProgressPreregisterID, Mcu_StationProgressMcuID, + Mcu_StationProgressStationID, Mcu_StationProgressStationName, Mcu_StationProgressSource, + Mcu_StationProgressCheckinDate, + Mcu_StationProgressSamplingAt, Mcu_StationProgressReceiveAt, Mcu_StationProgressProcessAt, + Mcu_StationProgressDoneAt, Mcu_StationProgressSyncedAt + ) + SELECT + o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, p_mcu_id, + ts.T_SamplingSoT_SampleStationID, ss.T_SampleStationName, 'nonlab', + DATE(o.T_OrderHeaderDate), + NULL, NULL, + NULLIF(MAX(TIMESTAMP(ts.T_SamplingSoProcessDate, ts.T_SamplingSoProcessTime)), '0000-00-00 00:00:00'), + NULLIF(MAX(TIMESTAMP(ts.T_SamplingSoDoneDate, ts.T_SamplingSoDoneTime)), '0000-00-00 00:00:00'), + NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + JOIN cpone.t_samplingso ts ON ts.T_SamplingSoT_OrderHeaderID = o.T_OrderHeaderID AND ts.T_SamplingSoIsActive = 'Y' + JOIN cpone.t_samplestation ss ON ss.T_SampleStationID = ts.T_SamplingSoT_SampleStationID + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + GROUP BY o.T_OrderHeaderID, p.Mcu_PreregisterPatientsID, ts.T_SamplingSoT_SampleStationID, ss.T_SampleStationName, o.T_OrderHeaderDate + ON DUPLICATE KEY UPDATE + Mcu_StationProgressProcessAt = VALUES(Mcu_StationProgressProcessAt), + Mcu_StationProgressDoneAt = VALUES(Mcu_StationProgressDoneAt), + Mcu_StationProgressSyncedAt = NOW(); + + + -- ---------------------------------------------------------- + -- 7. mcu_patient_resume_status + -- ---------------------------------------------------------- + INSERT INTO mcu_patient_resume_status ( + Mcu_PatientResumeStatusPreregisterID, Mcu_PatientResumeStatusMcuID, + Mcu_PatientResumeStatusStatus, Mcu_PatientResumeStatusValidated, + Mcu_PatientResumeStatusPublished, Mcu_PatientResumeSyncedAt + ) + SELECT + p.Mcu_PreregisterPatientsID, p_mcu_id, + r.Mcu_ResumeStatus, r.Mcu_ResumeValidation, 'N', NOW() + FROM cpone.mcu_preregister_patients p + JOIN cpone.t_orderheader o ON o.T_OrderHeaderID = p.Mcu_PreregisterPatientsT_OrderHeaderID + LEFT JOIN cpone.mcu_resume r ON r.Mcu_ResumeT_OrderHeaderID = o.T_OrderHeaderID AND r.Mcu_ResumeIsActive = 'Y' + WHERE p.Mcu_PreregisterPatientsMgm_McuID = p_mcu_id + AND p.Mcu_PreregisterPatientsT_OrderHeaderID > 0 + AND p.Mcu_PreregisterPatientsIsActive = 'Y' + ON DUPLICATE KEY UPDATE + Mcu_PatientResumeStatusStatus = VALUES(Mcu_PatientResumeStatusStatus), + Mcu_PatientResumeStatusValidated = VALUES(Mcu_PatientResumeStatusValidated), + Mcu_PatientResumeSyncedAt = NOW(); + +END $$ + +DELIMITER ; diff --git a/cpone-dashboard/menu/arrival/handler.go b/cpone-dashboard/menu/arrival/handler.go index 2e76b99..59f13bc 100644 --- a/cpone-dashboard/menu/arrival/handler.go +++ b/cpone-dashboard/menu/arrival/handler.go @@ -24,13 +24,16 @@ type pageData struct { AvailableDates []string Search string Department string + Paket string Rows []ArrivalRow FilteredRows []ArrivalRow Summary ArrivalSummary Departments []DepartmentStat DepartmentOptions []string + PaketOptions []PaketOption OverviewJSON template.JS DepartmentJSON template.JS + PaketChartJSON template.JS Page int PageSize int HasMore bool @@ -76,15 +79,27 @@ func buildArrivalData(r *http.Request) (pageData, int, error) { return pageData{}, http.StatusInternalServerError, err } summary, deptStats := BuildArrivalStats(rows) - filteredRows := FilterArrivalRows(rows, r.URL.Query().Get("search"), r.URL.Query().Get("dept")) + filteredRows := FilterArrivalRows(rows, r.URL.Query().Get("search"), r.URL.Query().Get("dept"), r.URL.Query().Get("paket")) deptOptions := UniqueDepartments(rows) + paketOptions := UniquePakets(rows) stationMap, err := GetStationProgress(mcuID, selectedDate) if err != nil { return pageData{}, http.StatusInternalServerError, err } for i := range filteredRows { - filteredRows[i].Stations = stationMap[filteredRows[i].PreregisterID] + stations := stationMap[filteredRows[i].PreregisterID] + filteredRows[i].Stations = stations + for _, s := range stations { + switch s.Tone { + case "success": + filteredRows[i].DoneCount++ + case "warning": + filteredRows[i].ProgressCount++ + default: + filteredRows[i].PendingCount++ + } + } } outerDepts := []map[string]any{} @@ -111,6 +126,26 @@ func buildArrivalData(r *http.Request) (pageData, int, error) { "counts": stationCounts, } + paketStats, err := GetPaketStats(mcuID, selectedDate) + if err != nil { + return pageData{}, http.StatusInternalServerError, err + } + paketLabels := make([]string, len(paketStats)) + paketTotal := make([]int, len(paketStats)) + paketDone := make([]int, len(paketStats)) + paketRemaining := make([]int, len(paketStats)) + for i, p := range paketStats { + paketLabels[i] = p.Name + paketTotal[i] = p.Total + paketDone[i] = p.Completed + paketRemaining[i] = p.Total - p.Completed + } + paketChart := map[string]any{ + "labels": paketLabels, + "done": paketDone, + "remaining": paketRemaining, + } + page := parsePage(r.URL.Query().Get("page")) pagedRows, hasMore := PaginateArrivalRows(filteredRows, page, defaultPageSize) visibleCount := page * defaultPageSize @@ -125,13 +160,16 @@ func buildArrivalData(r *http.Request) (pageData, int, error) { AvailableDates: dates, Search: r.URL.Query().Get("search"), Department: r.URL.Query().Get("dept"), + Paket: r.URL.Query().Get("paket"), Rows: rows, FilteredRows: pagedRows, Summary: summary, Departments: deptStats, DepartmentOptions: deptOptions, + PaketOptions: paketOptions, OverviewJSON: toJS(overview), DepartmentJSON: toJS(stationChart), + PaketChartJSON: toJS(paketChart), Page: page, PageSize: defaultPageSize, HasMore: hasMore, diff --git a/cpone-dashboard/menu/arrival/query.go b/cpone-dashboard/menu/arrival/query.go index 7520238..476a6a9 100644 --- a/cpone-dashboard/menu/arrival/query.go +++ b/cpone-dashboard/menu/arrival/query.go @@ -13,6 +13,12 @@ type StationBadge struct { Tone string // "success" | "warning" | "danger" | "neutral" } +type PaketStat struct { + Name string + Total int + Completed int +} + type StationStat struct { Name string Count int @@ -23,10 +29,16 @@ type ArrivalRow struct { NIP string Name string Department string + Pemeriksaan string + PaketCode string + PaketName string InTime string Status string StatusTone string Stations []StationBadge + DoneCount int + ProgressCount int + PendingCount int } type DepartmentStat struct { @@ -83,6 +95,17 @@ func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) { NULLIF(TRIM(mp.Mcu_PatientPosisi), ''), '-' ) AS department_name, + COALESCE(NULLIF(TRIM(mp.Mcu_PatientPemeriksaan), ''), '') AS pemeriksaan, + COALESCE(( + SELECT GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) ORDER BY COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) SEPARATOR ',') + FROM mcu_patient_packet pp + WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID + ), '') AS paket_code, + COALESCE(( + SELECT GROUP_CONCAT(DISTINCT pp.Mcu_PatientPacketName ORDER BY pp.Mcu_PatientPacketName SEPARATOR ', ') + FROM mcu_patient_packet pp + WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID + ), '') AS paket_name, COALESCE(DATE_FORMAT(mc.Mcu_CheckinoutInTime, '%H:%i'), '') AS in_time, CASE WHEN mc.Mcu_CheckinoutOutTime IS NOT NULL THEN 'Performed' @@ -119,7 +142,7 @@ func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) { var result []ArrivalRow for rows.Next() { var r ArrivalRow - if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Department, &r.InTime, &r.Status, &r.StatusTone); err != nil { + if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Department, &r.Pemeriksaan, &r.PaketCode, &r.PaketName, &r.InTime, &r.Status, &r.StatusTone); err != nil { continue } if strings.TrimSpace(r.NIP) == "" { @@ -139,31 +162,59 @@ func GetArrivalRows(mcuID int, date string) ([]ArrivalRow, error) { func GetStationProgress(mcuID int, date string) (map[int][]StationBadge, error) { rows, err := db.DB.Query(` SELECT - sp.Mcu_StationProgressPreregisterID, - sp.Mcu_StationProgressStationName, + rs.preregister_id, + rs.station_name, CASE - WHEN sp.Mcu_StationProgressSource = 'lab' - AND sp.Mcu_StationProgressReceiveAt IS NOT NULL THEN 'success' - WHEN sp.Mcu_StationProgressSource = 'nonlab' - AND sp.Mcu_StationProgressDoneAt IS NOT NULL THEN 'success' - WHEN sp.Mcu_StationProgressProcessAt IS NOT NULL - OR sp.Mcu_StationProgressReceiveAt IS NOT NULL - OR sp.Mcu_StationProgressSamplingAt IS NOT NULL THEN 'warning' + WHEN MAX(CASE + WHEN sp.Mcu_StationProgressSource = 'lab' + AND sp.Mcu_StationProgressReceiveAt IS NOT NULL + AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate THEN 1 + WHEN sp.Mcu_StationProgressSource = 'nonlab' + AND sp.Mcu_StationProgressProcessAt IS NOT NULL + AND DATE(sp.Mcu_StationProgressProcessAt) <= mc.Mcu_CheckinoutDate THEN 1 + WHEN sp.Mcu_StationProgressSource = 'nonlab' + AND sp.Mcu_StationProgressDoneAt IS NOT NULL + AND DATE(sp.Mcu_StationProgressDoneAt) <= mc.Mcu_CheckinoutDate THEN 1 + ELSE 0 + END) = 1 THEN 'success' + WHEN MAX(CASE + WHEN sp.Mcu_StationProgressProcessAt IS NOT NULL + AND DATE(sp.Mcu_StationProgressProcessAt) <= mc.Mcu_CheckinoutDate THEN 1 + WHEN sp.Mcu_StationProgressReceiveAt IS NOT NULL + AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate THEN 1 + WHEN sp.Mcu_StationProgressSamplingAt IS NOT NULL + AND DATE(sp.Mcu_StationProgressSamplingAt) <= mc.Mcu_CheckinoutDate THEN 1 + ELSE 0 + END) = 1 THEN 'warning' ELSE 'neutral' END AS tone - FROM mcu_station_progress sp - WHERE sp.Mcu_StationProgressMcuID = ? - AND sp.Mcu_StationProgressPreregisterID IN ( - SELECT mp.Mcu_PatientPreregisterID - FROM mcu_patient_schedule s - JOIN mcu_patient mp ON mp.Mcu_PatientPreregisterID = s.Mcu_PatientSchedulePreregisterID - WHERE mp.Mcu_PatientMcuID = ? - AND mp.Mcu_PatientIsActive = 'Y' - AND s.Mcu_PatientScheduleDate = ? - AND s.Mcu_PatientScheduleIsActive = 'Y' - ) - ORDER BY sp.Mcu_StationProgressPreregisterID, sp.Mcu_StationProgressStationName - `, mcuID, mcuID, date) + FROM mcu_patient_required_station rs + JOIN mcu_checkinout mc + ON mc.Mcu_CheckinoutPreregisterID = rs.preregister_id + AND mc.Mcu_CheckinoutMcuID = rs.mcu_id + AND mc.Mcu_CheckinoutDate = ? + AND mc.Mcu_CheckinoutIsActive = 'Y' + LEFT JOIN mcu_station_progress sp + ON sp.Mcu_StationProgressPreregisterID = rs.preregister_id + AND sp.Mcu_StationProgressStationID = rs.sample_station_id + AND sp.Mcu_StationProgressMcuID = rs.mcu_id + AND ( + (sp.Mcu_StationProgressCheckinDate IS NOT NULL + AND sp.Mcu_StationProgressCheckinDate <= mc.Mcu_CheckinoutDate) + OR + (sp.Mcu_StationProgressCheckinDate IS NULL + AND ( + (sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= mc.Mcu_CheckinoutDate) + OR (sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= mc.Mcu_CheckinoutDate) + OR (sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate) + OR (sp.Mcu_StationProgressSamplingAt IS NOT NULL AND DATE(sp.Mcu_StationProgressSamplingAt) <= mc.Mcu_CheckinoutDate) + ) + ) + ) + WHERE rs.mcu_id = ? + GROUP BY rs.preregister_id, rs.station_name + ORDER BY rs.preregister_id, rs.station_name + `, date, mcuID) if err != nil { return nil, err } @@ -247,16 +298,20 @@ func BuildStationChart(stationMap map[int][]StationBadge) []StationStat { return stats } -func FilterArrivalRows(rows []ArrivalRow, search, dept string) []ArrivalRow { +func FilterArrivalRows(rows []ArrivalRow, search, dept, paket string) []ArrivalRow { search = strings.ToLower(strings.TrimSpace(search)) dept = strings.TrimSpace(dept) - if search == "" && dept == "" { + paket = strings.TrimSpace(paket) + if search == "" && dept == "" && paket == "" { return rows } out := make([]ArrivalRow, 0, len(rows)) for _, row := range rows { - if dept != "" && dept != "All Departments" && row.Department != dept { + if dept != "" && row.Department != dept { + continue + } + if paket != "" && row.PaketCode != paket { continue } if search != "" { @@ -270,6 +325,33 @@ func FilterArrivalRows(rows []ArrivalRow, search, dept string) []ArrivalRow { return out } +type PaketOption struct { + Code string + Name string +} + +func UniquePakets(rows []ArrivalRow) []PaketOption { + seen := map[string]struct{}{} + var out []PaketOption + for _, row := range rows { + code := strings.TrimSpace(row.PaketCode) + if code == "" { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + name := strings.TrimSpace(row.PaketName) + if name == "" { + name = code + } + out = append(out, PaketOption{Code: code, Name: name}) + } + sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code }) + return out +} + func PaginateArrivalRows(rows []ArrivalRow, page, pageSize int) ([]ArrivalRow, bool) { if page < 1 { page = 1 @@ -306,6 +388,64 @@ func UniqueDepartments(rows []ArrivalRow) []string { return out } +func GetPaketStats(mcuID int, date string) ([]PaketStat, error) { + rows, err := db.DB.Query(` + SELECT + pp.Mcu_PatientPacketName, + COUNT(DISTINCT s.Mcu_PatientSchedulePreregisterID) AS total, + COUNT(DISTINCT CASE + WHEN st.total_required > 0 AND st.total_required = st.total_done + THEN s.Mcu_PatientSchedulePreregisterID + END) AS completed + FROM mcu_patient_packet pp + JOIN mcu_patient_schedule s + ON s.Mcu_PatientSchedulePreregisterID = pp.Mcu_PatientPacketPreregisterID + AND s.Mcu_PatientScheduleDate = ? + AND s.Mcu_PatientScheduleIsActive = 'Y' + JOIN mcu_patient mp + ON mp.Mcu_PatientPreregisterID = s.Mcu_PatientSchedulePreregisterID + AND mp.Mcu_PatientMcuID = ? + AND mp.Mcu_PatientIsActive = 'Y' + LEFT JOIN ( + SELECT + rs.preregister_id, + COUNT(rs.sample_station_id) AS total_required, + SUM(CASE + WHEN sp.Mcu_StationProgressSource = 'lab' + AND sp.Mcu_StationProgressReceiveAt IS NOT NULL THEN 1 + WHEN sp.Mcu_StationProgressSource = 'nonlab' + AND sp.Mcu_StationProgressProcessAt IS NOT NULL THEN 1 + WHEN sp.Mcu_StationProgressSource = 'nonlab' + AND sp.Mcu_StationProgressDoneAt IS NOT NULL THEN 1 + ELSE 0 + END) AS total_done + FROM mcu_patient_required_station rs + LEFT JOIN mcu_station_progress sp + ON sp.Mcu_StationProgressPreregisterID = rs.preregister_id + AND sp.Mcu_StationProgressStationID = rs.sample_station_id + AND sp.Mcu_StationProgressMcuID = rs.mcu_id + WHERE rs.mcu_id = ? + GROUP BY rs.preregister_id + ) st ON st.preregister_id = s.Mcu_PatientSchedulePreregisterID + GROUP BY pp.Mcu_PatientPacketName + ORDER BY total DESC + `, date, mcuID, mcuID) + if err != nil { + return nil, err + } + defer rows.Close() + + var result []PaketStat + for rows.Next() { + var p PaketStat + if err := rows.Scan(&p.Name, &p.Total, &p.Completed); err != nil { + continue + } + result = append(result, p) + } + return result, nil +} + func activeDateOrLatest(dates []string, selected string, fallback string) string { selected = strings.TrimSpace(selected) if selected != "" { diff --git a/cpone-dashboard/menu/dashboard/handler.go b/cpone-dashboard/menu/dashboard/handler.go index c1a3696..3a1150a 100644 --- a/cpone-dashboard/menu/dashboard/handler.go +++ b/cpone-dashboard/menu/dashboard/handler.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "strconv" + "strings" "time" ) @@ -26,8 +27,15 @@ type ArrivalsPartial struct { } type PatientsPartial struct { - Patients []PatientDetail - IsRange bool + Patients []PatientDetail + IsRange bool + Search string + ProgressStatus string + KpiFilter string + Page int + PageSize int + HasMore bool + NextPage int } type PageData struct { @@ -237,12 +245,84 @@ func Patients(w http.ResponseWriter, r *http.Request) { http.Error(w, "query error", http.StatusInternalServerError) return } + + search := strings.TrimSpace(r.URL.Query().Get("search")) + progressStatus := strings.TrimSpace(r.URL.Query().Get("progress_status")) + kpiFilter := strings.TrimSpace(r.URL.Query().Get("kpi_filter")) + + if kpiFilter != "" { + filteredByKPI := make([]PatientDetail, 0, len(patients)) + for _, p := range patients { + switch kpiFilter { + case "invited": + filteredByKPI = append(filteredByKPI, p) + case "total_staff": + if p.InTime != "" { + filteredByKPI = append(filteredByKPI, p) + } + case "inprogress": + if p.InTime != "" && !(p.HasOut && p.ProgressStatus == "done") { + filteredByKPI = append(filteredByKPI, p) + } + case "checkedout": + if p.HasOut && p.ProgressStatus == "done" { + filteredByKPI = append(filteredByKPI, p) + } + default: + filteredByKPI = append(filteredByKPI, p) + } + } + patients = filteredByKPI + } + + if search != "" || progressStatus != "" { + lowerSearch := strings.ToLower(search) + filtered := make([]PatientDetail, 0, len(patients)) + for _, p := range patients { + if progressStatus != "" && p.ProgressStatus != progressStatus { + continue + } + if lowerSearch != "" && !strings.Contains(strings.ToLower(p.Name), lowerSearch) && !strings.Contains(strconv.Itoa(p.ID), lowerSearch) { + continue + } + filtered = append(filtered, p) + } + patients = filtered + } + + page := 1 + if p, err := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("page"))); err == nil && p > 0 { + page = p + } + pageSize := 30 + start := (page - 1) * pageSize + if start > len(patients) { + start = len(patients) + } + end := start + pageSize + if end > len(patients) { + end = len(patients) + } + pagedPatients := patients[start:end] + hasMore := end < len(patients) + data := PatientsPartial{ - Patients: patients, - IsRange: mode != "daily" || dateFrom != dateTo, + Patients: pagedPatients, + IsRange: mode != "daily" || dateFrom != dateTo, + Search: search, + ProgressStatus: progressStatus, + KpiFilter: kpiFilter, + Page: page, + PageSize: pageSize, + HasMore: hasMore, + NextPage: page + 1, } t := parse("templates/dashboard/partials/patients.html") - if err := t.ExecuteTemplate(w, "patients", data); err != nil { + templateName := "patients" + if r.URL.Query().Get("chunk") == "1" { + templateName = "patients-rows" + } + if err := t.ExecuteTemplate(w, templateName, data); err != nil { log.Printf("[dashboard] patients template error: %v", err) } } diff --git a/cpone-dashboard/menu/dashboard/query.go b/cpone-dashboard/menu/dashboard/query.go index 053a8c2..8f395b0 100644 --- a/cpone-dashboard/menu/dashboard/query.go +++ b/cpone-dashboard/menu/dashboard/query.go @@ -93,31 +93,25 @@ func GetKPI(mcuID int, dateFrom, dateTo string) (KPIData, error) { AND Mcu_ParticipantDailyIsActive = 'Y' `, mcuID, dateFrom, dateTo).Scan(&d.InvitedStaff) - // Total staff: semua yang datang (checkin) pada tanggal filter - db.DB.QueryRow(` - SELECT COUNT(DISTINCT Mcu_CheckinoutPreregisterID) FROM mcu_checkinout - WHERE Mcu_CheckinoutMcuID = ? - AND Mcu_CheckinoutDate BETWEEN ? AND ? - AND Mcu_CheckinoutIsActive = 'Y' - `, mcuID, dateFrom, dateTo).Scan(&d.TotalStaff) - - // Checked-in: masih di dalam (belum checkout) - db.DB.QueryRow(` - SELECT COUNT(DISTINCT Mcu_CheckinoutPreregisterID) FROM mcu_checkinout - WHERE Mcu_CheckinoutMcuID = ? - AND Mcu_CheckinoutDate BETWEEN ? AND ? - AND Mcu_CheckinoutOutTime IS NULL - AND Mcu_CheckinoutIsActive = 'Y' - `, mcuID, dateFrom, dateTo).Scan(&d.CheckedIn) - - // Checked-out: sudah selesai - db.DB.QueryRow(` - SELECT COUNT(DISTINCT Mcu_CheckinoutPreregisterID) FROM mcu_checkinout - WHERE Mcu_CheckinoutMcuID = ? - AND Mcu_CheckinoutDate BETWEEN ? AND ? - AND Mcu_CheckinoutOutTime IS NOT NULL - AND Mcu_CheckinoutIsActive = 'Y' - `, mcuID, dateFrom, dateTo).Scan(&d.CheckedOut) + // Total staff / checked-in / checked-out dihitung dari sumber yang sama dengan + // detail pasien (GetAllPatients), supaya KPI card dan modal detail selalu sinkron. + // "Checked Out" hanya terhitung kalau pasien SUDAH checkout DAN semua station + // wajibnya sudah selesai — checkout fisik saja tidak cukup. + patients, err := GetAllPatients(mcuID, dateFrom, dateTo) + if err != nil { + return d, err + } + for _, p := range patients { + if p.InTime == "" { + continue + } + d.TotalStaff++ + if p.HasOut && p.ProgressStatus == "done" { + d.CheckedOut++ + } else { + d.CheckedIn++ + } + } return d, nil } @@ -391,6 +385,7 @@ func GetPeriodTrend(mcuID int, dateFrom, dateTo string, isRange bool) ([]TrendPo type PatientStationStatus struct { Station string Done bool + Progress bool ProcessAt string DoneAt string } @@ -404,6 +399,8 @@ type PatientDetail struct { HasOut bool Stations []PatientStationStatus DoneCount int + ProgressCount int + ProgressStatus string } type patientKey struct { @@ -414,16 +411,22 @@ type patientKey struct { func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error) { rows, err := db.DB.Query(` SELECT - mc.Mcu_CheckinoutPreregisterID, + s.Mcu_PatientSchedulePreregisterID, mp.Mcu_PatientName, - DATE_FORMAT(mc.Mcu_CheckinoutDate, '%Y-%m-%d'), - DATE_FORMAT(mc.Mcu_CheckinoutInTime, '%H:%i'), + DATE_FORMAT(s.Mcu_PatientScheduleDate, '%Y-%m-%d'), + COALESCE(DATE_FORMAT(mc.Mcu_CheckinoutInTime, '%H:%i'), ''), COALESCE(DATE_FORMAT(mc.Mcu_CheckinoutOutTime, '%H:%i'), ''), IF(mc.Mcu_CheckinoutOutTime IS NOT NULL, 1, 0), - rs.station_name, + COALESCE(rs.station_name, ''), IF( - (sp.Mcu_StationProgressSource = 'lab' AND sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= mc.Mcu_CheckinoutDate) - OR (sp.Mcu_StationProgressSource = 'nonlab' AND sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= mc.Mcu_CheckinoutDate), + (sp.Mcu_StationProgressSource = 'lab' AND sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressSource = 'nonlab' AND sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressSource = 'nonlab' AND sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= s.Mcu_PatientScheduleDate), + 1, 0), + IF( + (sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressSamplingAt IS NOT NULL AND DATE(sp.Mcu_StationProgressSamplingAt) <= s.Mcu_PatientScheduleDate), 1, 0), COALESCE(DATE_FORMAT( CASE @@ -435,29 +438,41 @@ func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error) END, '%d/%m/%Y %H:%i'), ''), COALESCE(DATE_FORMAT( - IF(sp.Mcu_StationProgressSource = 'lab', sp.Mcu_StationProgressReceiveAt, sp.Mcu_StationProgressDoneAt), + CASE + WHEN sp.Mcu_StationProgressSource = 'lab' THEN sp.Mcu_StationProgressReceiveAt + WHEN sp.Mcu_StationProgressSource = 'nonlab' THEN COALESCE(sp.Mcu_StationProgressDoneAt, sp.Mcu_StationProgressProcessAt) + ELSE NULL + END, '%d/%m/%Y %H:%i'), '') - FROM mcu_checkinout mc - JOIN mcu_patient mp ON mp.Mcu_PatientPreregisterID = mc.Mcu_CheckinoutPreregisterID - JOIN mcu_patient_required_station rs ON - rs.preregister_id = mc.Mcu_CheckinoutPreregisterID + FROM mcu_patient_schedule s + JOIN mcu_patient mp ON mp.Mcu_PatientPreregisterID = s.Mcu_PatientSchedulePreregisterID + AND mp.Mcu_PatientMcuID = ? + AND mp.Mcu_PatientIsActive = 'Y' + LEFT JOIN mcu_checkinout mc ON + mc.Mcu_CheckinoutPreregisterID = s.Mcu_PatientSchedulePreregisterID + AND mc.Mcu_CheckinoutMcuID = ? + AND mc.Mcu_CheckinoutDate = s.Mcu_PatientScheduleDate + AND mc.Mcu_CheckinoutIsActive = 'Y' + LEFT JOIN mcu_patient_required_station rs ON + rs.preregister_id = s.Mcu_PatientSchedulePreregisterID AND rs.mcu_id = ? LEFT JOIN mcu_station_progress sp ON - sp.Mcu_StationProgressPreregisterID = mc.Mcu_CheckinoutPreregisterID + sp.Mcu_StationProgressPreregisterID = s.Mcu_PatientSchedulePreregisterID AND sp.Mcu_StationProgressStationID = rs.sample_station_id AND sp.Mcu_StationProgressMcuID = ? - AND sp.Mcu_StationProgressCheckinDate = ( - SELECT MAX(sp2.Mcu_StationProgressCheckinDate) - FROM mcu_station_progress sp2 - WHERE sp2.Mcu_StationProgressPreregisterID = mc.Mcu_CheckinoutPreregisterID - AND sp2.Mcu_StationProgressStationID = rs.sample_station_id - AND sp2.Mcu_StationProgressMcuID = ? - AND sp2.Mcu_StationProgressCheckinDate <= mc.Mcu_CheckinoutDate + AND ( + (sp.Mcu_StationProgressCheckinDate IS NOT NULL AND sp.Mcu_StationProgressCheckinDate <= s.Mcu_PatientScheduleDate) + OR + (sp.Mcu_StationProgressCheckinDate IS NULL AND ( + (sp.Mcu_StationProgressDoneAt IS NOT NULL AND DATE(sp.Mcu_StationProgressDoneAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressProcessAt IS NOT NULL AND DATE(sp.Mcu_StationProgressProcessAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressReceiveAt IS NOT NULL AND DATE(sp.Mcu_StationProgressReceiveAt) <= s.Mcu_PatientScheduleDate) + OR (sp.Mcu_StationProgressSamplingAt IS NOT NULL AND DATE(sp.Mcu_StationProgressSamplingAt) <= s.Mcu_PatientScheduleDate) + )) ) - WHERE mc.Mcu_CheckinoutMcuID = ? - AND mc.Mcu_CheckinoutDate BETWEEN ? AND ? - AND mc.Mcu_CheckinoutIsActive = 'Y' - ORDER BY mc.Mcu_CheckinoutDate DESC, mc.Mcu_CheckinoutInTime DESC, rs.station_name + WHERE s.Mcu_PatientScheduleDate BETWEEN ? AND ? + AND s.Mcu_PatientScheduleIsActive = 'Y' + ORDER BY s.Mcu_PatientScheduleDate DESC, mc.Mcu_CheckinoutInTime DESC, mp.Mcu_PatientName ASC, rs.station_name `, mcuID, mcuID, mcuID, mcuID, dateFrom, dateTo) if err != nil { return nil, err @@ -470,8 +485,8 @@ func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error) for rows.Next() { var pid int var name, date, inTime, outTime, stationName, processAt, doneAt string - var hasOutInt, doneInt int - if err := rows.Scan(&pid, &name, &date, &inTime, &outTime, &hasOutInt, &stationName, &doneInt, &processAt, &doneAt); err != nil { + var hasOutInt, doneInt, progressInt int + if err := rows.Scan(&pid, &name, &date, &inTime, &outTime, &hasOutInt, &stationName, &doneInt, &progressInt, &processAt, &doneAt); err != nil { continue } k := patientKey{ID: pid, Date: date} @@ -489,15 +504,33 @@ func GetAllPatients(mcuID int, dateFrom, dateTo string) ([]PatientDetail, error) patients = append(patients, p) idx = len(patients) - 1 } - done := doneInt == 1 - patients[idx].Stations = append(patients[idx].Stations, PatientStationStatus{ - Station: stationName, - Done: done, - ProcessAt: processAt, - DoneAt: doneAt, - }) - if done { - patients[idx].DoneCount++ + if stationName != "" { + done := doneInt == 1 + patients[idx].Stations = append(patients[idx].Stations, PatientStationStatus{ + Station: stationName, + Done: done, + Progress: progressInt == 1, + ProcessAt: processAt, + DoneAt: doneAt, + }) + if done { + patients[idx].DoneCount++ + } + if progressInt == 1 { + patients[idx].ProgressCount++ + } + } + } + + for i := range patients { + totalStations := len(patients[i].Stations) + switch { + case totalStations > 0 && patients[i].DoneCount == totalStations: + patients[i].ProgressStatus = "done" + case patients[i].DoneCount > 0 || patients[i].ProgressCount > 0: + patients[i].ProgressStatus = "partial" + default: + patients[i].ProgressStatus = "not_yet" } } diff --git a/cpone-dashboard/menu/progress/handler.go b/cpone-dashboard/menu/progress/handler.go index f23cca8..a2df4a7 100644 --- a/cpone-dashboard/menu/progress/handler.go +++ b/cpone-dashboard/menu/progress/handler.go @@ -21,6 +21,8 @@ type pageData struct { CurrentProject projects.ProjectItem Search string Status string + Packet string + PacketOptions []PacketOption Rows []ProgressRow FilteredRows []ProgressRow Summary ProgressSummary @@ -64,7 +66,9 @@ func buildProgressData(r *http.Request) (pageData, int, error) { summary := BuildProgressSummary(rows) search := r.URL.Query().Get("search") status := r.URL.Query().Get("status") - filteredRows := FilterProgressRows(rows, search, status) + packet := r.URL.Query().Get("packet") + packetOptions := UniquePackets(rows) + filteredRows := FilterProgressRows(rows, search, status, packet) page := parsePage(r.URL.Query().Get("page")) pagedRows, hasMore := PaginateProgressRows(filteredRows, page, defaultPageSize) visibleCount := page * defaultPageSize @@ -77,6 +81,8 @@ func buildProgressData(r *http.Request) (pageData, int, error) { CurrentProject: project, Search: search, Status: status, + Packet: packet, + PacketOptions: packetOptions, Rows: rows, FilteredRows: pagedRows, Summary: summary, diff --git a/cpone-dashboard/menu/progress/query.go b/cpone-dashboard/menu/progress/query.go index 015a370..aa6f096 100644 --- a/cpone-dashboard/menu/progress/query.go +++ b/cpone-dashboard/menu/progress/query.go @@ -10,6 +10,9 @@ type ProgressRow struct { NIP string Name string Posisi string + PacketCode string + PacketName string + Tests string ResumeStatus string Validated string Published string @@ -33,6 +36,17 @@ func GetProgressRows(mcuID int) ([]ProgressRow, error) { NULLIF(TRIM(mp.Mcu_PatientPosisi), ''), '-' ) AS posisi, + COALESCE(( + SELECT GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) ORDER BY COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) SEPARATOR ',') + FROM mcu_patient_packet pp + WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID + ), '') AS packet_code, + COALESCE(( + SELECT GROUP_CONCAT(DISTINCT pp.Mcu_PatientPacketName ORDER BY pp.Mcu_PatientPacketName SEPARATOR ', ') + FROM mcu_patient_packet pp + WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID + ), '') AS packet_name, + COALESCE(NULLIF(TRIM(mp.Mcu_PatientPemeriksaan), ''), '') AS tests, COALESCE(rs.Mcu_PatientResumeStatusStatus, '') AS resume_status, COALESCE(rs.Mcu_PatientResumeStatusValidated, 'N') AS validated, COALESCE(rs.Mcu_PatientResumeStatusPublished, 'N') AS published @@ -54,7 +68,7 @@ func GetProgressRows(mcuID int) ([]ProgressRow, error) { var result []ProgressRow for rows.Next() { var r ProgressRow - if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Posisi, &r.ResumeStatus, &r.Validated, &r.Published); err != nil { + if err := rows.Scan(&r.PreregisterID, &r.NIP, &r.Name, &r.Posisi, &r.PacketCode, &r.PacketName, &r.Tests, &r.ResumeStatus, &r.Validated, &r.Published); err != nil { continue } result = append(result, r) @@ -75,14 +89,18 @@ func BuildProgressSummary(rows []ProgressRow) ProgressSummary { return s } -func FilterProgressRows(rows []ProgressRow, search, status string) []ProgressRow { +func FilterProgressRows(rows []ProgressRow, search, status, packetCode string) []ProgressRow { search = strings.ToLower(strings.TrimSpace(search)) status = strings.TrimSpace(status) - if search == "" && status == "" { + packetCode = strings.TrimSpace(packetCode) + if search == "" && status == "" && packetCode == "" { return rows } out := make([]ProgressRow, 0, len(rows)) for _, r := range rows { + if packetCode != "" && r.PacketCode != packetCode { + continue + } switch status { case "validated": if r.Validated != "Y" { @@ -102,7 +120,7 @@ func FilterProgressRows(rows []ProgressRow, search, status string) []ProgressRow } } if search != "" { - hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi) + hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi + " " + r.PacketCode + " " + r.PacketName + " " + r.Tests) if !strings.Contains(hay, search) { continue } @@ -112,6 +130,40 @@ func FilterProgressRows(rows []ProgressRow, search, status string) []ProgressRow return out } +type PacketOption struct { + Code string + Name string +} + +func UniquePackets(rows []ProgressRow) []PacketOption { + seen := map[string]struct{}{} + out := []PacketOption{} + for _, r := range rows { + code := strings.TrimSpace(r.PacketCode) + if code == "" { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + name := strings.TrimSpace(r.PacketName) + if name == "" { + name = code + } + out = append(out, PacketOption{Code: code, Name: name}) + } + // Keep deterministic order without new imports. + for i := 0; i < len(out); i++ { + for j := i + 1; j < len(out); j++ { + if out[j].Code < out[i].Code { + out[i], out[j] = out[j], out[i] + } + } + } + return out +} + func Pct(num, total int) int { if total == 0 { return 0 diff --git a/cpone-dashboard/menu/result/handler.go b/cpone-dashboard/menu/result/handler.go index 7c25f0d..df7c82c 100644 --- a/cpone-dashboard/menu/result/handler.go +++ b/cpone-dashboard/menu/result/handler.go @@ -23,6 +23,8 @@ type pageData struct { CurrentProject projects.ProjectItem Search string Filter string + Packet string + PacketOptions []PacketOption Rows []ResultRow FilteredRows []ResultRow Summary ResultSummary @@ -65,7 +67,9 @@ func buildResultData(r *http.Request) (pageData, int, error) { summary := BuildResultSummary(rows) search := r.URL.Query().Get("search") filter := r.URL.Query().Get("filter") - filteredRows := FilterResultRows(rows, search, filter) + packet := r.URL.Query().Get("packet") + packetOptions := UniquePackets(rows) + filteredRows := FilterResultRows(rows, search, filter, packet) page := parsePage(r.URL.Query().Get("page")) pagedRows, hasMore := PaginateResultRows(filteredRows, page, defaultPageSize) visibleCount := page * defaultPageSize @@ -78,6 +82,8 @@ func buildResultData(r *http.Request) (pageData, int, error) { CurrentProject: project, Search: search, Filter: filter, + Packet: packet, + PacketOptions: packetOptions, Rows: rows, FilteredRows: pagedRows, Summary: summary, diff --git a/cpone-dashboard/menu/result/query.go b/cpone-dashboard/menu/result/query.go index c54ac5a..75a608c 100644 --- a/cpone-dashboard/menu/result/query.go +++ b/cpone-dashboard/menu/result/query.go @@ -9,8 +9,12 @@ type ResultRow struct { NIP string Name string Posisi string + PacketCode string + PacketName string + Tests string FileUrl string ReportDate string + ReportVersion string } type ResultSummary struct { @@ -29,13 +33,30 @@ func GetResultRows(mcuID int) ([]ResultRow, error) { NULLIF(TRIM(mp.Mcu_PatientPosisi), ''), '-' ) AS posisi, + COALESCE(( + SELECT GROUP_CONCAT(DISTINCT COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) ORDER BY COALESCE(NULLIF(TRIM(pp.Mcu_PatientPacketCode), ''), pp.Mcu_PatientPacketName) SEPARATOR ',') + FROM mcu_patient_packet pp + WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID + ), '') AS packet_code, + COALESCE(( + SELECT GROUP_CONCAT(DISTINCT pp.Mcu_PatientPacketName ORDER BY pp.Mcu_PatientPacketName SEPARATOR ', ') + FROM mcu_patient_packet pp + WHERE pp.Mcu_PatientPacketPreregisterID = mp.Mcu_PatientPreregisterID + ), '') AS packet_name, + COALESCE(NULLIF(TRIM(mp.Mcu_PatientPemeriksaan), ''), '') AS tests, COALESCE(p.Published_McuDasboardFileUrl, '') AS file_url, CASE WHEN p.Published_McuDasboardFileUrl IS NOT NULL AND p.Published_McuDasboardFileUrl != '' THEN COALESCE(CAST(p.Published_McuDasboardLastUpdated AS CHAR), '') ELSE '' - END AS report_date + END AS report_date, + CASE + WHEN p.Published_McuDasboardFileUrl IS NOT NULL + AND p.Published_McuDasboardFileUrl != '' + THEN COALESCE(CAST(UNIX_TIMESTAMP(p.Published_McuDasboardLastUpdated) AS CHAR), '') + ELSE '' + END AS report_version FROM mcu_patient mp LEFT JOIN published_mcu_dashboard_sync p ON p.Published_McuDasboardT_OrderHeaderID = mp.Mcu_PatientOrderID @@ -53,7 +74,7 @@ func GetResultRows(mcuID int) ([]ResultRow, error) { var result []ResultRow for rows.Next() { var r ResultRow - if err := rows.Scan(&r.NIP, &r.Name, &r.Posisi, &r.FileUrl, &r.ReportDate); err != nil { + if err := rows.Scan(&r.NIP, &r.Name, &r.Posisi, &r.PacketCode, &r.PacketName, &r.Tests, &r.FileUrl, &r.ReportDate, &r.ReportVersion); err != nil { continue } result = append(result, r) @@ -71,14 +92,18 @@ func BuildResultSummary(rows []ResultRow) ResultSummary { return s } -func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow { +func FilterResultRows(rows []ResultRow, search, filter, packetCode string) []ResultRow { search = strings.ToLower(strings.TrimSpace(search)) filter = strings.TrimSpace(filter) - if search == "" && filter == "" { + packetCode = strings.TrimSpace(packetCode) + if search == "" && filter == "" && packetCode == "" { return rows } out := make([]ResultRow, 0, len(rows)) for _, r := range rows { + if packetCode != "" && r.PacketCode != packetCode { + continue + } switch filter { case "has_pdf": if r.FileUrl == "" { @@ -90,7 +115,7 @@ func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow { } } if search != "" { - hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi) + hay := strings.ToLower(r.Name + " " + r.NIP + " " + r.Posisi + " " + r.PacketCode + " " + r.PacketName + " " + r.Tests) if !strings.Contains(hay, search) { continue } @@ -100,6 +125,39 @@ func FilterResultRows(rows []ResultRow, search, filter string) []ResultRow { return out } +type PacketOption struct { + Code string + Name string +} + +func UniquePackets(rows []ResultRow) []PacketOption { + seen := map[string]struct{}{} + out := []PacketOption{} + for _, r := range rows { + code := strings.TrimSpace(r.PacketCode) + if code == "" { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + name := strings.TrimSpace(r.PacketName) + if name == "" { + name = code + } + out = append(out, PacketOption{Code: code, Name: name}) + } + for i := 0; i < len(out); i++ { + for j := i + 1; j < len(out); j++ { + if out[j].Code < out[i].Code { + out[i], out[j] = out[j], out[i] + } + } + } + return out +} + func PaginateResultRows(rows []ResultRow, page, pageSize int) ([]ResultRow, bool) { if page < 1 { page = 1 diff --git a/cpone-dashboard/templates/abnormal/index.html b/cpone-dashboard/templates/abnormal/index.html index 8629fad..2b64ece 100644 --- a/cpone-dashboard/templates/abnormal/index.html +++ b/cpone-dashboard/templates/abnormal/index.html @@ -18,7 +18,7 @@
- Ganti project + Change Project diff --git a/cpone-dashboard/templates/arrival/index.html b/cpone-dashboard/templates/arrival/index.html index 7b5aa2f..2a35286 100644 --- a/cpone-dashboard/templates/arrival/index.html +++ b/cpone-dashboard/templates/arrival/index.html @@ -18,20 +18,21 @@ @@ -74,26 +75,44 @@ +Per Paket Completion
+Total pasien per paket vs sudah menyelesaikan semua station
+