From 558f44b7c221e1a2d499bc6844ada6f4155a0881 Mon Sep 17 00:00:00 2001 From: "sas.fajri" Date: Fri, 28 Aug 2026 16:09:04 +0700 Subject: [PATCH] BUG-DASH-280826 - perbaiki sumber data checkout dan definisi Checked Out di dashboard sp_generate_dashboard_data menebak Mcu_CheckinoutOutTime dari MAX() timestamp event lab (sample receive/sampling done), bukan dari checkout fisik pasien, sehingga pasien yang belum checkout bisa tampil "Checked Out" walau stationnya belum selesai. Ganti sumbernya ke cpone.preregister_checkin_checkout (log checkin/checkout asli) dan hentikan pemaksaan Mcu_CheckinoutID = order_id yang bentrok untuk project multi-hari. KPI "Checked Out" di GetKPI juga diselaraskan dengan detail pasien: sekarang hitung dari GetAllPatients dan mensyaratkan semua station wajib sudah selesai, bukan cuma checkout fisik saja, supaya angka KPI dan modal detail selalu sinkron. --- .../migrations/019_fix_checkinout_source.sql | 246 ++++++++++++++++++ cpone-dashboard/menu/dashboard/handler.go | 4 +- cpone-dashboard/menu/dashboard/query.go | 44 ++-- 3 files changed, 267 insertions(+), 27 deletions(-) create mode 100644 cpone-dashboard/db/migrations/019_fix_checkinout_source.sql 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/menu/dashboard/handler.go b/cpone-dashboard/menu/dashboard/handler.go index 09f236f..3a1150a 100644 --- a/cpone-dashboard/menu/dashboard/handler.go +++ b/cpone-dashboard/menu/dashboard/handler.go @@ -261,11 +261,11 @@ func Patients(w http.ResponseWriter, r *http.Request) { filteredByKPI = append(filteredByKPI, p) } case "inprogress": - if p.InTime != "" && !p.HasOut { + if p.InTime != "" && !(p.HasOut && p.ProgressStatus == "done") { filteredByKPI = append(filteredByKPI, p) } case "checkedout": - if p.HasOut { + if p.HasOut && p.ProgressStatus == "done" { filteredByKPI = append(filteredByKPI, p) } default: diff --git a/cpone-dashboard/menu/dashboard/query.go b/cpone-dashboard/menu/dashboard/query.go index c4636b2..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 }