Files
web-antrean/stores/queueStore.js
T
2026-01-06 14:51:28 +07:00

703 lines
22 KiB
JavaScript

// stores/queueStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useClinicStore } from './clinicStore';
import { usePenunjangStore } from './penunjangStore';
export const useQueueStore = defineStore('queue', () => {
const clinicStore = useClinicStore();
const penunjangStore = usePenunjangStore();
// Seed data for easy reset during dev
const seedPatients = [
{
no: 1,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
shift: "Shift 1",
klinik: "KANDUNGAN",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 2,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1002 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 3,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1003 | Online - 250811100200",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 4,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1004 | Online - 250811100210",
shift: "Shift 1",
klinik: "THT",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 5,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1005 | Online - 250811100163",
shift: "Shift 2",
klinik: "KANDUNGAN",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 6,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1006 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 7,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1007 | Online - 250811100200",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 8,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1008 | Online - 250811100210",
shift: "Shift 1",
klinik: "THT",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 9,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1009 | Online - 250811100163",
shift: "Shift 2",
klinik: "KANDUNGAN",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 10,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1010 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 11,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1011 | Online - 250811100200",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "TIDAK",
pembayaran: "UMUM",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
{
no: 12,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1012 | Online - 250811100210",
shift: "Shift 2",
klinik: "THT",
fastTrack: "YA", // Fast Track Patient
pembayaran: "BPJS",
status: "waiting",
processStage: "loket",
createdAt: new Date().toISOString(),
},
];
const cloneSeed = () => seedPatients.map(p => ({ ...p }));
// Initialize state - will be automatically hydrated from localStorage by pinia-plugin-persistedstate
// If localStorage has data, it will override these defaults
const allPatients = ref(cloneSeed());
const quotaUsed = ref(5);
const currentProcessingPatient = ref({
loket: null,
klinik: null,
penunjang: null,
});
// Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore
const kliniks = computed(() => {
const baseList = typeof clinicStore.getClinicsForDropdown === 'function'
? clinicStore.getClinicsForDropdown()
: [];
// Bentuk objek disesuaikan dengan yang dipakai di useQueue (id, name, kode)
return baseList.map((c) => ({
id: c.id,
name: c.name,
kode: c.kode,
icon: c.icon,
available: c.available,
}));
});
// Penunjang data - reference dari penunjangStore (single source of truth)
// Menggunakan computed untuk reactive reference
const penunjangs = computed(() => {
// Get penunjang list from penunjangStore dan map ke format yang diharapkan
const penunjangList = penunjangStore.penunjangList || [];
return penunjangList.map(p => ({
id: p.id,
name: p.nama || p.name, // Support both nama and name for backward compatibility
kode: p.kode
}));
});
// Computed - Filter berdasarkan process stage dan status
const getPatientsByStage = (stage) => {
return computed(() => {
const patients = allPatients.value.filter(p => p.processStage === stage);
// Debug log
console.log(`getPatientsByStage(${stage}):`, patients.length, 'patients');
if (patients.length > 0) {
console.log('Sample patient properties:', Object.keys(patients[0]));
console.log('Sample fastTrack values:', patients.map(p => p.fastTrack));
}
return {
all: patients,
waiting: patients.filter(p => p.status === 'waiting'),
diLoket: patients.filter(p => p.status === 'di-loket'),
terlambat: patients.filter(p => p.status === 'terlambat'),
pending: patients.filter(p => p.status === 'pending'),
};
});
};
// Total pasien per stage
const getTotalPasienByStage = (stage) => {
return computed(() =>
allPatients.value.filter(p => p.processStage === stage).length
);
};
const totalPasien = computed(() => allPatients.value.length);
const resetPatients = () => {
allPatients.value = cloneSeed();
quotaUsed.value = 5;
currentProcessingPatient.value = { loket: null, klinik: null, penunjang: null };
};
// Actions
const callNext = (adminType = 'loket') => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
'penunjang': 'penunjang'
};
const targetStage = stageMap[adminType];
const nextPatient = allPatients.value.find(p =>
p.status === 'waiting' && p.processStage === targetStage
);
if (!nextPatient) {
return { success: false, message: "Tidak ada pasien selanjutnya" };
}
if (quotaUsed.value >= 150) {
return { success: false, message: "Quota sudah penuh" };
}
// Update dengan cara yang Vue reactive
const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
if (index !== -1) {
allPatients.value[index] = { ...allPatients.value[index], status: "di-loket" };
}
quotaUsed.value++;
return {
success: true,
message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]}`,
};
};
const callMultiplePatients = (count, adminType = 'loket') => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
'penunjang': 'penunjang'
};
const targetStage = stageMap[adminType];
const waitingList = allPatients.value.filter(p =>
p.status === 'waiting' && p.processStage === targetStage
);
const patientsToCall = waitingList.slice(0, count);
if (patientsToCall.length === 0) {
return { success: false, message: "Tidak ada pasien yang menunggu" };
}
if (quotaUsed.value + patientsToCall.length > 150) {
return { success: false, message: "Quota tidak mencukupi" };
}
// Update dengan cara yang Vue reactive
patientsToCall.forEach((patient) => {
const index = allPatients.value.findIndex(p => p.no === patient.no);
if (index !== -1) {
allPatients.value[index] = { ...allPatients.value[index], status: "di-loket" };
}
});
quotaUsed.value += patientsToCall.length;
return {
success: true,
message: `Memanggil ${patientsToCall.length} pasien ke loket`,
};
};
const processPatient = (patient, action, adminType = 'loket') => {
const patientCode = patient.noAntrian.split(" |")[0];
let message = "";
// Find patient index for reactive update
const patientIndex = allPatients.value.findIndex(p => p.no === patient.no);
if (patientIndex === -1) {
return { success: false, message: "Pasien tidak ditemukan" };
}
switch (action) {
case "check-in":
// Jika check-in di loket, pindahkan ke klinik dengan status di-loket
if (adminType === 'loket') {
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "di-loket",
processStage: "klinik"
};
message = `Pasien ${patientCode} berhasil check in dan masuk ke Tabel Loket Klinik`;
}
// Jika check-in di klinik, selesai
else if (adminType === 'klinik') {
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "processed"
};
message = `Pasien ${patientCode} berhasil check in di Klinik`;
}
// Jika check-in di penunjang, selesai
else if (adminType === 'penunjang') {
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "processed"
};
message = `Pasien ${patientCode} berhasil check in di Penunjang`;
}
// Clear current processing di admin yang melakukan check-in
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = null;
}
break;
case "terlambat":
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "terlambat"
};
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = null;
}
message = `Pasien ${patientCode} ditandai terlambat`;
break;
case "pending":
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "pending"
};
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = null;
}
message = `Pasien ${patientCode} di-pending`;
break;
case "aktifkan":
const currentStatus = allPatients.value[patientIndex].status;
if (currentStatus === "terlambat" || currentStatus === "pending") {
// PERBAIKAN: Update dengan cara yang Vue reactive
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "di-loket"
};
message = `Pasien ${patientCode} diaktifkan kembali dan masuk ke tabel Di Loket`;
} else {
message = `Pasien ${patientCode} tidak dapat diaktifkan (status: ${currentStatus})`;
}
break;
case "proses":
// Ambil data terbaru dari array
currentProcessingPatient.value[adminType] = allPatients.value[patientIndex];
message = `Memproses pasien ${patientCode}`;
break;
}
return { success: true, message };
};
const processNextQueue = (adminType = 'loket') => {
const stageMap = {
'loket': 'loket',
'klinik': 'klinik',
'penunjang': 'penunjang'
};
const targetStage = stageMap[adminType];
const nextPatient = allPatients.value.find(p =>
p.status === 'di-loket' && p.processStage === targetStage
);
if (!nextPatient) {
return { success: false, message: "Tidak ada pasien di loket yang dapat diproses" };
}
// Set sebagai current processing patient
currentProcessingPatient.value[adminType] = nextPatient;
return {
success: true,
message: `Memproses pasien ${nextPatient.noAntrian.split(" |")[0]}`,
};
};
const createAntreanKlinik = (klinik, patient = null, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
const newPatient = {
no: newNo,
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
timestamp.getMinutes()
).padStart(2, "0")}`,
barcode: barcode,
noAntrian: `KL${String(newNo).padStart(4, "0")} | Klinik - ${barcode}`,
shift: "Shift 1",
klinik: klinik.name,
fastTrack: "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
status: "di-loket",
processStage: "klinik",
createdAt: timestamp.toISOString(),
referencePatient: patient ? patient.noAntrian : null,
};
allPatients.value.push(newPatient);
return {
success: true,
message: `Antrean klinik ${klinik.name} berhasil dibuat dan masuk ke Tabel Loket Klinik`,
patient: newPatient,
};
};
const createAntreanPenunjang = (penunjang, patient = null, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
const newPatient = {
no: newNo,
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
timestamp.getMinutes()
).padStart(2, "0")}`,
barcode: barcode,
noAntrian: `PN${String(newNo).padStart(4, "0")} | Penunjang - ${barcode}`,
shift: "Shift 1",
klinik: penunjang.name,
fastTrack: "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
status: "di-loket",
processStage: "penunjang",
createdAt: timestamp.toISOString(),
referencePatient: patient ? patient.noAntrian : null,
};
allPatients.value.push(newPatient);
return {
success: true,
message: `Antrean penunjang ${penunjang.name} berhasil dibuat dan masuk ke Tabel Loket Penunjang`,
patient: newPatient,
};
};
const changeKlinik = (patient, newKlinik, adminType = 'loket') => {
const patientIndex = allPatients.value.findIndex((p) => p.no === patient.no);
if (patientIndex !== -1) {
// Update dengan cara yang Vue reactive
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
klinik: newKlinik.name
};
// Update current processing if it's the same patient
if (currentProcessingPatient.value[adminType]?.no === patient.no) {
currentProcessingPatient.value[adminType] = {
...currentProcessingPatient.value[adminType],
klinik: newKlinik.name
};
}
return {
success: true,
message: `Klinik berhasil diubah ke ${newKlinik.name}`,
};
}
return { success: false, message: "Pasien tidak ditemukan" };
};
const getCurrentProcessing = (adminType) => {
return computed(() => currentProcessingPatient.value[adminType]);
};
const setCurrentProcessing = (patient, adminType) => {
currentProcessingPatient.value[adminType] = patient;
};
// Register patient from Anjungan (onsite registration)
const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null) => {
const newNo = allPatients.value.length > 0
? Math.max(...allPatients.value.map(p => p.no)) + 1
: 1;
const timestamp = new Date();
const visitDateTime = visitDate ? new Date(visitDate) : timestamp;
const jamPanggil = visitDate
? `${String(visitDateTime.getHours()).padStart(2, "0")}:${String(visitDateTime.getMinutes()).padStart(2, "0")}`
: `${String(timestamp.getHours()).padStart(2, "0")}:${String(timestamp.getMinutes()).padStart(2, "0")}`;
// Generate barcode (simulasi - bisa diganti dengan API call)
const barcode = `250811${String(Date.now()).slice(-6)}${String(newNo).padStart(3, "0")}`;
// Format nomor antrean: UMXXXX | Onsite - barcode
const noAntrian = `UM${String(newNo).padStart(4, "0")} | Onsite - ${barcode}`;
// Determine status based on visit type
const status = visitType === 'SEKARANG' ? 'waiting' : 'pending';
const newPatient = {
no: newNo,
jamPanggil: jamPanggil,
barcode: barcode,
noAntrian: noAntrian,
shift: shift,
klinik: clinic.name || clinic,
fastTrack: "TIDAK", // Default, bisa diubah nanti
pembayaran: paymentType,
status: status,
processStage: "loket",
createdAt: timestamp.toISOString(),
registrationType: 'onsite',
visitType: visitType,
visitDate: visitDate || timestamp.toISOString().substring(0, 10),
namaDokter: namaDokter || null, // Nama dokter hanya untuk pasien Eksekutif
};
allPatients.value.push(newPatient);
return {
success: true,
message: `Pendaftaran ${clinic.name || clinic} untuk kunjungan ${visitType === 'SEKARANG' ? 'HARI INI' : visitDate} dengan pembayaran ${paymentType} berhasil diproses.`,
patient: newPatient,
};
};
// Check-in patient (update status from waiting to di-loket)
const checkInPatient = (patientIdOrBarcode) => {
console.log('🔍 checkInPatient called with:', patientIdOrBarcode);
console.log('📊 Total patients in store:', allPatients.value.length);
console.log('📋 First few patients:', allPatients.value.slice(0, 3).map(p => ({
no: p.no,
noAntrian: p.noAntrian,
barcode: p.barcode,
status: p.status
})));
// Clean input - remove whitespace and convert to uppercase for comparison
const cleanInput = String(patientIdOrBarcode).trim().toUpperCase();
// Try to find by multiple criteria:
// 1. Exact barcode match
// 2. Parse as number and match with no
// 3. Extract number from input (e.g., "UM0014" -> "0014" or "14") and match with noAntrian
// 4. Check if noAntrian includes the input
const patientIndex = allPatients.value.findIndex(p => {
// Exact barcode match
if (p.barcode === cleanInput || p.barcode === patientIdOrBarcode) {
console.log('✅ Found by barcode:', p.barcode);
return true;
}
// Try parsing as number
const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(patientIdOrBarcode);
if (!isNaN(parsedNo) && p.no === parsedNo) {
console.log('✅ Found by no:', p.no);
return true;
}
// Check if noAntrian includes the input (case insensitive)
const noAntrianUpper = (p.noAntrian || '').toUpperCase();
if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(patientIdOrBarcode)) {
console.log('✅ Found by noAntrian:', p.noAntrian);
return true;
}
// Try to extract number from noAntrian (e.g., "UM0014 | Onsite - ..." -> "0014")
const noAntrianNumber = noAntrianUpper.match(/([A-Z]+)(\d+)/);
if (noAntrianNumber) {
const extractedNumber = noAntrianNumber[2];
const inputNumber = cleanInput.replace(/[^0-9]/g, '');
if (inputNumber && extractedNumber.includes(inputNumber) || inputNumber.includes(extractedNumber)) {
console.log('✅ Found by extracted number from noAntrian');
return true;
}
}
return false;
});
if (patientIndex === -1) {
console.log('❌ Patient not found. Searched for:', cleanInput);
console.log('📋 Available noAntrian values:', allPatients.value.map(p => p.noAntrian));
return { success: false, message: "Pasien tidak ditemukan" };
}
const patient = allPatients.value[patientIndex];
console.log('✅ Patient found:', patient);
// Only allow check-in if status is waiting or pending
if (patient.status !== 'waiting' && patient.status !== 'pending') {
console.log('⚠️ Patient status is not waiting/pending:', patient.status);
return {
success: false,
message: `Pasien tidak dapat check-in. Status saat ini: ${patient.status}`
};
}
// Update status to di-loket
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "di-loket",
};
console.log('✅ Check-in successful, patient status updated to di-loket');
return {
success: true,
message: `Check-in berhasil. Pasien ${patient.noAntrian.split(" |")[0]} siap diproses di loket.`,
patient: allPatients.value[patientIndex],
};
};
// State persistence is handled automatically by pinia-plugin-persistedstate
// No need for manual watch or save logic
return {
// State
allPatients,
quotaUsed,
currentProcessingPatient,
kliniks,
penunjangs,
// Computed
totalPasien,
// Actions
callNext,
callMultiplePatients,
processPatient,
createAntreanKlinik,
createAntreanPenunjang,
changeKlinik,
processNextQueue,
getPatientsByStage,
getTotalPasienByStage,
getCurrentProcessing,
setCurrentProcessing,
registerPatientFromAnjungan,
checkInPatient,
};
}, {
persist: {
key: 'queue-store-state',
storage: typeof window !== 'undefined' ? localStorage : undefined,
paths: ['allPatients', 'quotaUsed', 'currentProcessingPatient'],
},
});