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:
@@ -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
@@ -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})`);
|
||||
|
||||
Reference in New Issue
Block a user