feat: implement AdminKlinikRuang page for managing patient queues, including processing, calling, filtering, and a global search, alongside a new queueStore.

This commit is contained in:
Fanrouver
2026-02-13 14:26:49 +07:00
parent 4df13cbe70
commit 67a5514654
2 changed files with 134 additions and 87 deletions
+29 -23
View File
@@ -1062,53 +1062,55 @@ const getFilteredAndSortedPatientsForRoom = (ruang) => {
}
}
// Default sorting - berdasarkan createdAt dari nomor tiket awal
// Default sorting
return patients.sort((a, b) => {
// Prioritaskan yang sudah digenerate/diproses & pending di atas
const aHasTiket = !!a.tipeLayanan;
const bHasTiket = !!b.tipeLayanan;
// 1. Prioritaskan Fast Track (Selalu paling atas di status yang sama)
const aFT = (a.fastTrack ?? "").toString().trim().toUpperCase() === "YA";
const bFT = (b.fastTrack ?? "").toString().trim().toUpperCase() === "YA";
if (aFT && !bFT) return -1;
if (!aFT && bFT) return 1;
// 2. Prioritaskan yang sudah diproses (punya tipeLayanan)
const aHasTiket = !!a.tipeLayanan || a.status === 'pemeriksaan';
const bHasTiket = !!b.tipeLayanan || b.status === 'pemeriksaan';
// Jika satu sudah punya tiket dan yang lain belum, yang sudah tiket di atas
if (aHasTiket && !bHasTiket) return -1;
if (!aHasTiket && bHasTiket) return 1;
// Jika keduanya sudah punya tiket atau keduanya belum, sort by status
// 3. Sort berdasarkan status priority
const statusPriority = {
'di-loket': 1,
'waiting': 2,
'terlambat': 3,
'pending': 4
'pemeriksaan': 1,
'di-loket': 2,
'menunggu': 3,
'waiting': 3, // Fallback for local/legacy data
'terlambat': 4,
'pending': 5
};
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
if (priorityDiff !== 0) return priorityDiff;
// Sort by createdAt dari nomor tiket awal (referencePatient)
// Jika ada referencePatient, cari createdAt dari pasien awal
// 4. Sort by createdAt dari nomor tiket awal (referencePatient)
let createdAtA = a.createdAt;
let createdAtB = b.createdAt;
if (a.referencePatient) {
const sourceA = queueStore.allPatients.find(p => p.noAntrian === a.referencePatient);
if (sourceA?.createdAt) {
createdAtA = sourceA.createdAt;
}
if (sourceA?.createdAt) createdAtA = sourceA.createdAt;
}
if (b.referencePatient) {
const sourceB = queueStore.allPatients.find(p => p.noAntrian === b.referencePatient);
if (sourceB?.createdAt) {
createdAtB = sourceB.createdAt;
}
if (sourceB?.createdAt) createdAtB = sourceB.createdAt;
}
// Sort by createdAt (yang lebih awal di atas)
if (createdAtA && createdAtB) {
const dateA = new Date(createdAtA).getTime();
const dateB = new Date(createdAtB).getTime();
if (dateA !== dateB) return dateA - dateB;
}
// Fallback: Sort by time
// 5. Fallback: Sort by time
const timeA = a.jamPanggil?.split(':').map(Number) || [0, 0];
const timeB = b.jamPanggil?.split(':').map(Number) || [0, 0];
return timeA[0] * 60 + timeA[1] - (timeB[0] * 60 + timeB[1]);
@@ -1636,9 +1638,11 @@ const broadcastUpdate = async () => {
const getStatusColor = (status) => {
const colors = {
'di-loket': 'success-600',
'pemeriksaan': 'success-600',
'di-loket': 'warning-600',
'menunggu': 'primary-600',
'waiting': 'primary-600',
'terlambat': 'primary-600',
'terlambat': 'secondary-600',
'pending': 'danger-600'
};
return colors[status] || 'grey';
@@ -1647,9 +1651,11 @@ const getStatusColor = (status) => {
const getStatusLabel = (status) => {
const labels = {
'di-loket': 'Di Loket',
'menunggu': 'Menunggu',
'waiting': 'Menunggu',
'terlambat': 'Terlambat',
'pending': 'Pending'
'pending': 'Pending',
'pemeriksaan': 'Pemeriksaan'
};
return labels[status] || status;
};
@@ -1671,7 +1677,7 @@ const handleCallPatientByTipe = async (ruang, tipeLayanan) => {
const updateData = {
...queueStore.allPatients[patientIndex],
status: 'di-loket',
status: 'pemeriksaan',
tipeLayanan: tipeLayanan,
lastCalledAt: new Date().toISOString(),
lastCalledTipeLayanan: tipeLayanan
+105 -64
View File
@@ -131,8 +131,18 @@ export const useQueueStore = defineStore('queue', () => {
const latestStatus = visitStatuses[visitStatuses.length - 1];
let patientStatus = 'di-loket';
if (latestStatus?.desc) {
const desc = latestStatus.desc.toLowerCase();
// Map status from database ID if available
if (latestStatus?.fk_ref_visit_status_id) {
const statusId = String(latestStatus.fk_ref_visit_status_id);
if (PATIENT_STATUS_MAP[statusId]) {
patientStatus = PATIENT_STATUS_MAP[statusId];
}
}
// Fallback to description search
if (patientStatus === 'di-loket' && (latestStatus?.visit_status_description || latestStatus?.desc)) {
const desc = (latestStatus.visit_status_description || latestStatus.desc).toLowerCase();
if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) {
patientStatus = 'pemeriksaan';
}
@@ -155,7 +165,7 @@ export const useQueueStore = defineStore('queue', () => {
createdAt: visit.registration_datetime || new Date().toISOString(),
visitType: visit.visit_type_name || 'ONSITE',
noRM: visit.norm || '',
fastTrack: 'TIDAK',
fastTrack: (service.ticket && String(service.ticket).startsWith('F-')) ? "YA" : "TIDAK",
registrationType: 'api',
visitId: visit.visit_id || visit.id,
visitCode: visit.visit_code,
@@ -202,32 +212,44 @@ export const useQueueStore = defineStore('queue', () => {
const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`);
const apiP = newPatientMap.get(key);
if (apiP) {
// If we have local updates, preserve them
const hasLocalUpdates = (
p.status !== 'di-loket' ||
p.calledPemeriksaanAwal ||
p.calledTindakan ||
p.lastCalledAt
);
if (hasLocalUpdates) {
if (apiP) {
newPatientMap.delete(key); // Mark as handled
return {
// Preserve fastTrack and F- prefix if existing patient has it
const isFastTrack = (p.fastTrack === "YA" || (p.noAntrian && p.noAntrian.startsWith('F-')));
let finalNoAntrian = apiP.noAntrian;
if (isFastTrack && !finalNoAntrian.startsWith('F-')) {
finalNoAntrian = `F-${finalNoAntrian}`;
}
const mergedP = {
...apiP,
status: p.status,
calledPemeriksaanAwal: p.calledPemeriksaanAwal,
calledTindakan: p.calledTindakan,
tipeLayanan: p.tipeLayanan,
lastCalledAt: p.lastCalledAt,
lastCalledTipeLayanan: p.lastCalledTipeLayanan
fastTrack: isFastTrack ? "YA" : apiP.fastTrack,
noAntrian: finalNoAntrian
};
} else {
newPatientMap.delete(key); // Handled
return apiP;
// If we have local updates, preserve them
const hasLocalUpdates = (
p.status !== 'di-loket' ||
p.calledPemeriksaanAwal ||
p.calledTindakan ||
p.lastCalledAt
);
if (hasLocalUpdates) {
return {
...mergedP,
status: p.status,
calledPemeriksaanAwal: p.calledPemeriksaanAwal,
calledTindakan: p.calledTindakan,
tipeLayanan: p.tipeLayanan,
lastCalledAt: p.lastCalledAt,
lastCalledTipeLayanan: p.lastCalledTipeLayanan
};
} else {
return mergedP;
}
}
}
// If it was an API patient that disappeared from the list, it's either stale or from another room
if (p.registrationType === 'api') return null;
@@ -413,22 +435,45 @@ export const useQueueStore = defineStore('queue', () => {
* idvisit 5, 6 = di-loket (PS CHECK-IN, TP LOKET)
*/
const PATIENT_STATUS_MAP = {
1: 'menunggu',
2: 'menunggu',
3: 'anjungan',
4: 'anjungan',
5: 'di-loket',
6: 'di-loket',
28: 'pending', // PE PENDAFTARAN
29: 'terlambat', // TR PENDAFTARAN
"1": 'menunggu',
"2": 'menunggu',
"3": 'anjungan',
"4": 'anjungan',
"5": 'di-loket',
"6": 'di-loket',
"28": 'pending', // PE PENDAFTARAN
"29": 'terlambat' // TR PENDAFTARAN
// Stage: ANJUNGAN (1-5, 25-27)
1: 'menunggu', // CT ANJUNGAN
2: 'menunggu', // RT ANJUNGAN
3: 'anjungan', // PG ANJUNGAN
4: 'anjungan', // RT CHECK-IN
5: 'di-loket', // PS CHECK-IN
25: 'menunggu', // BT ANJUNGAN
26: 'pending', // PE ANJUNGAN
27: 'terlambat', // TE ANJUNGAN
// Stage: LOKET (6-9, 28-29)
6: 'di-loket', // RT PENDAFTARAN
7: 'anjungan', // PG PENDAFTARAN
8: 'di-loket', // PO PENDAFTARAN
9: 'di-loket', // PS PENDAFTARAN
28: 'pending', // PE PENDAFTARAN
29: 'terlambat', // TR PENDAFTARAN
// Stage: VERIFIKASI (10-13, 30-31)
10: 'di-loket',
11: 'anjungan',
12: 'di-loket',
13: 'di-loket',
30: 'pending',
31: 'terlambat',
// Stage: PELAYANAN (14-19, 32-33)
14: 'pemeriksaan', // CT PEMERIKSAAN
15: 'pemeriksaan', // RT PEMERIKSAAN
16: 'pemeriksaan', // PG PEMERIKSAAN AWAL
17: 'pemeriksaan', // PG PEMERIKSAAN
18: 'pemeriksaan', // PO PEMERIKSAAN
19: 'pemeriksaan', // PS PEMERIKSAAN
32: 'pending', // PE PEMERIKSAAN
33: 'terlambat', // TR PEMERIKSAAN
// String versions for robustness
"1": 'menunggu', "2": 'menunggu', "3": 'anjungan', "4": 'anjungan', "5": 'di-loket',
"6": 'di-loket', "14": 'pemeriksaan', "15": 'pemeriksaan', "28": 'pending', "29": 'terlambat'
};
/**
@@ -517,7 +562,7 @@ export const useQueueStore = defineStore('queue', () => {
shift: apiPatient.shift ? `Shift ${apiPatient.shift}` : '',
klinik: apiPatient.klinik || '',
kodeKlinik: apiPatient.klinik || '', // Will be mapped later
fastTrack: "TIDAK",
fastTrack: (apiPatient.ticket && String(apiPatient.ticket).startsWith('F-')) ? "YA" : "TIDAK",
pembayaran: apiPatient.pembayaran || '',
status: status,
processStage: 'loket',
@@ -678,7 +723,7 @@ export const useQueueStore = defineStore('queue', () => {
}
});
// 3. Update new patients with preserved status
// 3. Update new patients with preserved status and fasttrack
patientsWithLoketId.forEach(p => {
const key = p.idtiket ? `id-${p.idtiket}` : `bc-${p.barcode}`;
if (preservedStatuses.has(key)) {
@@ -725,25 +770,11 @@ export const useQueueStore = defineStore('queue', () => {
return true;
}
// CRITICAL FIX: Protect terlambat and pending status from being overwritten
// API might have lag in updating these statuses, so keep local version
if ((p.status === 'terlambat' || p.status === 'pending') && newPatientMap.has(key)) {
console.log(`🛡️ [queueStore] Protecting ${p.status} patient ${p.noAntrian} from API overwrite`);
newPatientMap.delete(key);
return true;
}
// Remove if it's being replaced by new API batch
if (newPatientMap.has(key)) return false;
// Also check if barcode matches any new API patient
if (p.barcode) {
const barcodeMatches = patientsWithLoketId.some(newP => newP.barcode === p.barcode);
if (barcodeMatches) return false;
}
// Remove if it's a stale 'api' patient for this loket
if (p.registrationType === 'api' && String(p.loketId) === String(loketId)) return false;
if (p.registrationType === 'api' && String(p.loketId) === String(loketId)) {
// But only if it's NOT in the new batch (stale)
if (!newPatientMap.has(key)) return false;
}
return true;
});
@@ -767,15 +798,25 @@ export const useQueueStore = defineStore('queue', () => {
const existingPrio = statusPriority[existing.status] || 0;
const newPrio = statusPriority[newPatient.status] || 0;
// Only update if new status has higher priority (e.g., di-loket > terlambat)
// Only update if new status has higher or equal priority
// This prevents API data from overwriting LocalStorage terlambat/pending status
if (newPrio > existingPrio) {
if (newPrio >= existingPrio) {
console.log(`🔄 [queueStore] Updating patient ${newPatient.barcode} from ${existing.status} to ${newPatient.status}`);
// Preserve fastTrack and F- prefix if existing patient has it
const isFastTrack = (existing.fastTrack === "YA" || (existing.noAntrian && existing.noAntrian.startsWith('F-')));
let finalNoAntrian = newPatient.noAntrian;
if (isFastTrack && !finalNoAntrian.startsWith('F-')) {
finalNoAntrian = `F-${finalNoAntrian}`;
}
allPatients.value[existingIndex] = {
...newPatient,
// Preserve some local state
calledByAdmin: existing.calledByAdmin,
lastCalledAt: existing.lastCalledAt
lastCalledAt: existing.lastCalledAt,
fastTrack: isFastTrack ? "YA" : newPatient.fastTrack,
noAntrian: finalNoAntrian
};
} else {
console.log(`🛡️ [queueStore] Protecting patient ${existing.barcode} with status ${existing.status} from API overwrite (${newPatient.status})`);