1333 lines
48 KiB
JavaScript
1333 lines
48 KiB
JavaScript
// stores/queueStore.js
|
|
import { defineStore } from 'pinia';
|
|
import { ref, computed } from 'vue';
|
|
import { useClinicStore } from './clinicStore';
|
|
import { usePenunjangStore } from './penunjangStore';
|
|
import { useLoketStore } from './loketStore';
|
|
|
|
export const useQueueStore = defineStore('queue', () => {
|
|
const clinicStore = useClinicStore();
|
|
const penunjangStore = usePenunjangStore();
|
|
const loketStore = useLoketStore();
|
|
|
|
// Helper function untuk mendapatkan loket default (Loket A)
|
|
const getDefaultLoket = () => {
|
|
const allLokets = loketStore.loketData?.value || loketStore.loketData || [];
|
|
const loket1 = allLokets.find(l => l.id === 1 || l.no === 1);
|
|
return loket1 ? loket1.namaLoket : 'Loket A';
|
|
};
|
|
|
|
// Helper function untuk generate barcode dengan format: YYMMDD + 5 digit random
|
|
// Format: YY (tahun 2 digit) + MM (bulan 2 digit) + DD (tanggal 2 digit) + XXXXX (5 digit random)
|
|
// Contoh: 25011212345 (12 Januari 2025 dengan random 5 digit 12345)
|
|
// IMPORTANT: Memastikan barcode selalu UNIQUE meskipun reset harian
|
|
// Menggunakan timestamp milisecond + random untuk memastikan uniqueness
|
|
// NOTE: allPatientsRef adalah optional parameter untuk menghindari TDZ error saat seed initialization
|
|
const generateBarcode = (existingBarcodes = [], allPatientsRef = null) => {
|
|
const now = new Date();
|
|
const year = String(now.getFullYear()).slice(-2); // 2 digit tahun terakhir
|
|
const month = String(now.getMonth() + 1).padStart(2, '0'); // 2 digit bulan
|
|
const day = String(now.getDate()).padStart(2, '0'); // 2 digit tanggal
|
|
|
|
// Generate 5 digit code dari timestamp milisecond untuk memastikan uniqueness
|
|
// Ambil 5 digit terakhir dari timestamp + random untuk menghindari duplikasi
|
|
const timestamp = Date.now();
|
|
const randomOffset = Math.floor(Math.random() * 1000); // 0-999
|
|
const uniqueCode = String((timestamp % 100000) + randomOffset).slice(-5).padStart(5, '0');
|
|
|
|
// Pastikan barcode unique dengan mengecek di existingBarcodes atau allPatientsRef
|
|
let barcode = `${year}${month}${day}${uniqueCode}`;
|
|
let attempts = 0;
|
|
const maxAttempts = 100;
|
|
|
|
// Cek apakah barcode sudah ada
|
|
// NOTE: allPatientsRef adalah optional untuk menghindari TDZ error saat seed initialization
|
|
const checkExists = (b) => {
|
|
if (existingBarcodes.includes(b)) return true;
|
|
// Hanya cek allPatientsRef jika diberikan (tidak null/undefined)
|
|
if (allPatientsRef && allPatientsRef.value && allPatientsRef.value.some(p => p.barcode === b)) return true;
|
|
return false;
|
|
};
|
|
|
|
// Generate ulang jika duplikat
|
|
while (checkExists(barcode) && attempts < maxAttempts) {
|
|
const newTimestamp = Date.now();
|
|
const newRandomOffset = Math.floor(Math.random() * 1000);
|
|
const newUniqueCode = String((newTimestamp % 100000) + newRandomOffset).slice(-5).padStart(5, '0');
|
|
barcode = `${year}${month}${day}${newUniqueCode}`;
|
|
attempts++;
|
|
}
|
|
|
|
// Jika masih duplikat setelah maxAttempts, tambahkan counter berdasarkan existing barcodes
|
|
if (attempts >= maxAttempts) {
|
|
const datePrefix = `${year}${month}${day}`;
|
|
const existingCount = existingBarcodes.filter(b => b && b.startsWith(datePrefix)).length;
|
|
// Hanya cek allPatientsRef jika diberikan (tidak null/undefined)
|
|
const allCount = allPatientsRef && allPatientsRef.value
|
|
? allPatientsRef.value.filter(p => p.barcode && p.barcode.startsWith(datePrefix)).length
|
|
: 0;
|
|
const counter = Math.max(existingCount, allCount) + 1;
|
|
const counterCode = String(counter).padStart(5, '0');
|
|
barcode = `${datePrefix}${counterCode}`;
|
|
}
|
|
|
|
return barcode;
|
|
};
|
|
// Seed data for easy reset during dev
|
|
// IMPORTANT: Setiap pasien HARUS memiliki barcode UNIK untuk menghindari konflik
|
|
// Format barcode menggunakan generateBarcode() untuk konsistensi dengan format baru
|
|
// Generate barcode dengan memastikan uniqueness menggunakan existingBarcodes array
|
|
const seedBarcodes = [];
|
|
const seedBarcode1 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode1);
|
|
const seedBarcode2 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode2);
|
|
const seedBarcode3 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode3);
|
|
const seedBarcode4 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode4);
|
|
const seedBarcode5 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode5);
|
|
const seedBarcode6 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode6);
|
|
const seedBarcode7 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode7);
|
|
const seedBarcode8 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode8);
|
|
const seedBarcode9 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode9);
|
|
const seedBarcode10 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode10);
|
|
const seedBarcode11 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode11);
|
|
const seedBarcode12 = generateBarcode(seedBarcodes); seedBarcodes.push(seedBarcode12);
|
|
|
|
const seedPatients = [
|
|
{
|
|
no: 1,
|
|
jamPanggil: "12:49",
|
|
barcode: seedBarcode1, // Barcode unik untuk setiap pasien
|
|
noAntrian: `UM1001 | Online - ${seedBarcode1}`,
|
|
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: seedBarcode2,
|
|
noAntrian: `UM1002 | Online - ${seedBarcode2}`,
|
|
shift: "Shift 1",
|
|
klinik: "IPD",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 3,
|
|
jamPanggil: "09:30",
|
|
barcode: seedBarcode3,
|
|
noAntrian: `UM1003 | Online - ${seedBarcode3}`,
|
|
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: seedBarcode4,
|
|
noAntrian: `UM1004 | Online - ${seedBarcode4}`,
|
|
shift: "Shift 1",
|
|
klinik: "THT",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 5,
|
|
jamPanggil: "12:49",
|
|
barcode: seedBarcode5,
|
|
noAntrian: `UM1005 | Online - ${seedBarcode5}`,
|
|
shift: "Shift 2",
|
|
klinik: "KANDUNGAN",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 6,
|
|
jamPanggil: "10:52",
|
|
barcode: seedBarcode6,
|
|
noAntrian: `UM1006 | Online - ${seedBarcode6}`,
|
|
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: seedBarcode7,
|
|
noAntrian: `UM1007 | Online - ${seedBarcode7}`,
|
|
shift: "Shift 1",
|
|
klinik: "SARAF",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 8,
|
|
jamPanggil: "14:15",
|
|
barcode: seedBarcode8,
|
|
noAntrian: `UM1008 | Online - ${seedBarcode8}`,
|
|
shift: "Shift 1",
|
|
klinik: "THT",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 9,
|
|
jamPanggil: "12:49",
|
|
barcode: seedBarcode9,
|
|
noAntrian: `UM1009 | Online - ${seedBarcode9}`,
|
|
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: seedBarcode10,
|
|
noAntrian: `UM1010 | Online - ${seedBarcode10}`,
|
|
shift: "Shift 1",
|
|
klinik: "IPD",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 11,
|
|
jamPanggil: "09:30",
|
|
barcode: seedBarcode11,
|
|
noAntrian: `UM1011 | Online - ${seedBarcode11}`,
|
|
shift: "Shift 1",
|
|
klinik: "SARAF",
|
|
fastTrack: "TIDAK",
|
|
pembayaran: "UMUM",
|
|
status: "waiting",
|
|
processStage: "loket",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
no: 12,
|
|
jamPanggil: "14:15",
|
|
barcode: seedBarcode12,
|
|
noAntrian: `UM1012 | Online - ${seedBarcode12}`,
|
|
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'),
|
|
menunggu: patients.filter(p => p.status === 'menunggu'), // Pasien yang belum dipanggil
|
|
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];
|
|
// Prioritaskan pasien dengan status 'menunggu' (yang belum dipanggil)
|
|
const nextPatient = allPatients.value.find(p =>
|
|
p.status === 'menunggu' && p.processStage === targetStage
|
|
) || allPatients.value.find(p =>
|
|
p.status === 'waiting' && p.processStage === targetStage
|
|
);
|
|
|
|
if (!nextPatient) {
|
|
return { success: false, message: "Tidak ada pasien selanjutnya" };
|
|
}
|
|
|
|
// Hitung kuota yang tersedia
|
|
const menungguCount = allPatients.value.filter(p =>
|
|
p.status === 'menunggu' && p.processStage === targetStage
|
|
).length;
|
|
const diLoketCount = allPatients.value.filter(p =>
|
|
p.status === 'di-loket' && p.processStage === targetStage
|
|
).length;
|
|
const availableQuota = 150 - diLoketCount;
|
|
|
|
if (availableQuota <= 0) {
|
|
return { success: false, message: "Kuota sudah penuh" };
|
|
}
|
|
|
|
if (menungguCount === 0) {
|
|
return { success: false, message: "Tidak ada pasien yang menunggu untuk dipanggil" };
|
|
}
|
|
|
|
// Untuk adminType 'loket', tambahkan delay 5 detik sebelum status berubah
|
|
// Ini untuk simulasi multiple loket memanggil bersamaan
|
|
if (adminType === 'loket') {
|
|
// Set status pending call dengan timestamp
|
|
const pendingCallTimestamp = new Date().toISOString();
|
|
const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
|
|
if (index !== -1) {
|
|
allPatients.value[index] = {
|
|
...allPatients.value[index],
|
|
status: "pending-call", // Status sementara sebelum delay
|
|
pendingCallAt: pendingCallTimestamp
|
|
};
|
|
}
|
|
|
|
// Set timeout 5 detik untuk mengubah status menjadi 'waiting'
|
|
setTimeout(() => {
|
|
const patientIndex = allPatients.value.findIndex(p => p.no === nextPatient.no);
|
|
if (patientIndex !== -1 && allPatients.value[patientIndex].status === 'pending-call') {
|
|
const callTimestamp = new Date().toISOString();
|
|
allPatients.value[patientIndex] = {
|
|
...allPatients.value[patientIndex],
|
|
status: "waiting",
|
|
lastCalledAt: callTimestamp, // Track waktu panggilan untuk multiple calls
|
|
pendingCallAt: undefined // Clear pending call timestamp
|
|
};
|
|
}
|
|
}, 5000); // Delay 5 detik
|
|
|
|
return {
|
|
success: true,
|
|
message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]} (akan dipanggil dalam 5 detik)`,
|
|
};
|
|
} else {
|
|
// Untuk adminType selain loket, langsung update status
|
|
const callTimestamp = new Date().toISOString();
|
|
const index = allPatients.value.findIndex(p => p.no === nextPatient.no);
|
|
if (index !== -1) {
|
|
allPatients.value[index] = {
|
|
...allPatients.value[index],
|
|
status: "waiting",
|
|
lastCalledAt: callTimestamp // Track waktu panggilan untuk multiple calls
|
|
};
|
|
}
|
|
|
|
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];
|
|
// Prioritaskan pasien dengan status 'menunggu' (yang belum dipanggil)
|
|
const menungguList = allPatients.value.filter(p =>
|
|
p.status === 'menunggu' && p.processStage === targetStage
|
|
);
|
|
const waitingList = allPatients.value.filter(p =>
|
|
p.status === 'waiting' && p.processStage === targetStage
|
|
);
|
|
|
|
// Gabungkan: menunggu dulu, baru waiting
|
|
const combinedList = [...menungguList, ...waitingList];
|
|
|
|
if (combinedList.length === 0) {
|
|
return { success: false, message: "Tidak ada pasien yang menunggu" };
|
|
}
|
|
|
|
// Hitung kuota yang tersedia
|
|
const diLoketCount = allPatients.value.filter(p =>
|
|
p.status === 'di-loket' && p.processStage === targetStage
|
|
).length;
|
|
const availableQuota = 150 - diLoketCount;
|
|
|
|
// Kuota yang bisa dipanggil = min(count yang diminta, jumlah pasien menunggu, kuota tersedia)
|
|
const maxCallable = Math.min(count, menungguList.length, availableQuota);
|
|
|
|
if (maxCallable <= 0) {
|
|
if (menungguList.length === 0) {
|
|
return { success: false, message: "Tidak ada pasien yang menunggu untuk dipanggil" };
|
|
}
|
|
if (availableQuota <= 0) {
|
|
return { success: false, message: "Kuota sudah penuh. Tidak bisa memanggil pasien lagi" };
|
|
}
|
|
}
|
|
|
|
const patientsToCall = combinedList.slice(0, maxCallable);
|
|
|
|
// Untuk adminType 'loket', tambahkan delay 5 detik sebelum status berubah
|
|
// Ini untuk simulasi multiple loket memanggil bersamaan
|
|
if (adminType === 'loket') {
|
|
// Set status pending call dengan timestamp
|
|
const pendingCallTimestamp = new Date().toISOString();
|
|
patientsToCall.forEach((patient) => {
|
|
const index = allPatients.value.findIndex(p => p.no === patient.no);
|
|
if (index !== -1) {
|
|
allPatients.value[index] = {
|
|
...allPatients.value[index],
|
|
status: "pending-call", // Status sementara sebelum delay
|
|
pendingCallAt: pendingCallTimestamp
|
|
};
|
|
}
|
|
});
|
|
|
|
// Set timeout 5 detik untuk mengubah status menjadi 'waiting'
|
|
setTimeout(() => {
|
|
const callTimestamp = new Date().toISOString();
|
|
patientsToCall.forEach((patient) => {
|
|
const patientIndex = allPatients.value.findIndex(p => p.no === patient.no);
|
|
if (patientIndex !== -1 && allPatients.value[patientIndex].status === 'pending-call') {
|
|
allPatients.value[patientIndex] = {
|
|
...allPatients.value[patientIndex],
|
|
status: "waiting",
|
|
lastCalledAt: callTimestamp, // Track waktu panggilan untuk multiple calls
|
|
pendingCallAt: undefined // Clear pending call timestamp
|
|
};
|
|
}
|
|
});
|
|
}, 5000); // Delay 5 detik
|
|
|
|
return {
|
|
success: true,
|
|
message: `Memanggil ${patientsToCall.length} pasien ke loket (akan dipanggil dalam 5 detik)`,
|
|
};
|
|
} else {
|
|
// Untuk adminType selain loket, langsung update status
|
|
const callTimestamp = new Date().toISOString();
|
|
patientsToCall.forEach((patient) => {
|
|
const index = allPatients.value.findIndex(p => p.no === patient.no);
|
|
if (index !== -1) {
|
|
allPatients.value[index] = {
|
|
...allPatients.value[index],
|
|
status: "waiting",
|
|
lastCalledAt: callTimestamp // Track waktu panggilan untuk multiple calls
|
|
};
|
|
}
|
|
});
|
|
|
|
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
|
|
const patient = allPatients.value[patientIndex];
|
|
|
|
// Jika adminType adalah 'loket', pastikan ada loket assignment
|
|
if (adminType === 'loket') {
|
|
// Pastikan antrian yang diproses memiliki loket assignment
|
|
const currentLoket = patient.loket || getDefaultLoket();
|
|
const currentLoketId = patient.loketId || 1;
|
|
|
|
// Update patient dengan loket assignment (jika belum ada)
|
|
if (!patient.loket || !patient.loketId) {
|
|
const updatedPatient = {
|
|
...patient,
|
|
loket: currentLoket,
|
|
loketId: currentLoketId
|
|
};
|
|
allPatients.value[patientIndex] = updatedPatient;
|
|
// Set currentProcessingPatient dengan loket assignment
|
|
currentProcessingPatient.value[adminType] = updatedPatient;
|
|
} else {
|
|
// Jika sudah ada loket assignment, langsung set currentProcessingPatient
|
|
currentProcessingPatient.value[adminType] = patient;
|
|
}
|
|
} else {
|
|
// Untuk adminType selain loket, langsung set currentProcessingPatient
|
|
currentProcessingPatient.value[adminType] = patient;
|
|
}
|
|
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 : generateBarcode([], allPatients);
|
|
|
|
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 : generateBarcode([], allPatients);
|
|
|
|
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 createAntreanKlinikRuang = (klinikRuang, ruang, patient = null, adminType = 'klinik') => {
|
|
const newNo = allPatients.value.length + 1;
|
|
const timestamp = new Date();
|
|
const barcode = patient ? patient.barcode : generateBarcode([], allPatients);
|
|
|
|
// Generate nomor antrian baru dengan format: [huruf pertama poli + urutan abjad ruang + nomor antrian ruang]
|
|
// Contoh: "Anak" ruang 1 = "AA001" (A dari Anak, A dari ruang 1, 001 nomor antrian)
|
|
|
|
// 1. Ambil huruf pertama dari nama klinik/poli
|
|
const firstLetter = klinikRuang.namaKlinik.charAt(0).toUpperCase();
|
|
|
|
// 2. Konversi nomor ruang ke abjad (1 = A, 2 = B, 3 = C, dst)
|
|
const ruangNumber = parseInt(ruang.nomorRuang) || 1;
|
|
const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A', 66 = 'B', dst
|
|
|
|
// 3. Hitung nomor antrian ruang (dimulai dari 1, maksimal 3 digit)
|
|
const roomQueues = allPatients.value.filter(p =>
|
|
p.kodeKlinik === klinikRuang.kodeKlinik &&
|
|
p.nomorRuang === ruang.nomorRuang &&
|
|
p.processStage === 'klinik-ruang'
|
|
);
|
|
const queueNumber = roomQueues.length + 1;
|
|
const queueNumberStr = String(queueNumber).padStart(3, "0");
|
|
|
|
// 4. Format nomor antrian: AA001, AB002, dst
|
|
const newNoAntrian = `${firstLetter}${ruangLetter}${queueNumberStr}`;
|
|
|
|
const newPatient = {
|
|
no: newNo,
|
|
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
|
|
timestamp.getMinutes()
|
|
).padStart(2, "0")}`,
|
|
barcode: barcode,
|
|
noAntrian: `${newNoAntrian} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang}`,
|
|
noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${newNoAntrian}`,
|
|
shift: patient ? (patient.shift || "Shift 1") : "Shift 1",
|
|
klinik: klinikRuang.namaKlinik,
|
|
ruang: ruang.namaRuang,
|
|
kodeKlinik: klinikRuang.kodeKlinik,
|
|
nomorRuang: ruang.nomorRuang,
|
|
nomorScreen: ruang.nomorScreen,
|
|
fastTrack: patient ? (patient.fastTrack || "TIDAK") : "TIDAK",
|
|
pembayaran: patient ? patient.pembayaran : "UMUM",
|
|
status: "waiting",
|
|
processStage: "klinik-ruang", // Set ke klinik-ruang langsung
|
|
createdAt: timestamp.toISOString(),
|
|
referencePatient: patient ? patient.noAntrian : null,
|
|
sourcePatientNo: patient ? patient.no : null,
|
|
// Tracking panggilan
|
|
calledPemeriksaanAwal: false,
|
|
calledTindakan: false,
|
|
lastCalledAt: null,
|
|
lastCalledTipeLayanan: null,
|
|
};
|
|
|
|
allPatients.value.push(newPatient);
|
|
|
|
return {
|
|
success: true,
|
|
message: `Antrean ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang} berhasil dibuat: ${newNoAntrian}`,
|
|
patient: newPatient,
|
|
};
|
|
};
|
|
|
|
// Scan barcode dan generate antrean klinik ruang baru
|
|
const scanAndCreateAntreanKlinikRuang = (barcodeInput, klinikRuang, ruang, tipeLayanan = 'Pemeriksaan Awal') => {
|
|
// Clean input - remove whitespace and handle prefix letters
|
|
const cleanInput = String(barcodeInput).trim().toUpperCase();
|
|
// Remove leading letters if any (e.g., "J200730100005" -> "200730100005")
|
|
const numericInput = cleanInput.replace(/^[A-Z]+/, '');
|
|
|
|
// Find patient by barcode or noAntrian
|
|
const sourcePatient = allPatients.value.find(p => {
|
|
// Exact barcode match
|
|
if (p.barcode === cleanInput || p.barcode === numericInput) return true;
|
|
|
|
// Check if noAntrian includes the input
|
|
const noAntrianUpper = (p.noAntrian || '').toUpperCase();
|
|
if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(numericInput)) return true;
|
|
|
|
// Try extracting number from noAntrian
|
|
const noAntrianNumber = noAntrianUpper.match(/([A-Z]+)(\d+)/);
|
|
if (noAntrianNumber) {
|
|
const extractedNumber = noAntrianNumber[2];
|
|
if (extractedNumber.includes(numericInput) || numericInput.includes(extractedNumber)) return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
if (!sourcePatient) {
|
|
return { success: false, message: "Pasien tidak ditemukan. Pastikan barcode/nomor antrian benar." };
|
|
}
|
|
|
|
// Check if patient already has antrean klinik ruang for this room
|
|
const existingAntrean = allPatients.value.find(p =>
|
|
p.referencePatient === sourcePatient.noAntrian &&
|
|
p.kodeKlinik === klinikRuang.kodeKlinik &&
|
|
p.nomorRuang === ruang.nomorRuang &&
|
|
p.processStage === 'klinik-ruang'
|
|
);
|
|
|
|
if (existingAntrean) {
|
|
return {
|
|
success: false,
|
|
message: `Pasien sudah memiliki antrean di ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang}`
|
|
};
|
|
}
|
|
|
|
// Generate queue number for this specific room and tipeLayanan
|
|
const roomQueues = allPatients.value.filter(p =>
|
|
p.kodeKlinik === klinikRuang.kodeKlinik &&
|
|
p.nomorRuang === ruang.nomorRuang &&
|
|
p.tipeLayanan === tipeLayanan &&
|
|
p.processStage === 'klinik-ruang'
|
|
);
|
|
|
|
const queueNumber = roomQueues.length + 1;
|
|
const prefix = tipeLayanan === 'Pemeriksaan Awal' ? 'PA' : 'TD';
|
|
const newNo = allPatients.value.length + 1;
|
|
const timestamp = new Date();
|
|
|
|
const newPatient = {
|
|
no: newNo,
|
|
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
|
|
timestamp.getMinutes()
|
|
).padStart(2, "0")}`,
|
|
barcode: sourcePatient.barcode,
|
|
noAntrian: `${prefix}${String(queueNumber).padStart(3, "0")} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang} - ${tipeLayanan}`,
|
|
noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${prefix}${String(queueNumber).padStart(3, "0")}`,
|
|
shift: sourcePatient.shift || "Shift 1",
|
|
klinik: klinikRuang.namaKlinik,
|
|
ruang: ruang.namaRuang,
|
|
kodeKlinik: klinikRuang.kodeKlinik,
|
|
nomorRuang: ruang.nomorRuang,
|
|
nomorScreen: ruang.nomorScreen,
|
|
tipeLayanan: tipeLayanan,
|
|
fastTrack: sourcePatient.fastTrack || "TIDAK",
|
|
pembayaran: sourcePatient.pembayaran || "UMUM",
|
|
status: "waiting",
|
|
processStage: "klinik-ruang",
|
|
createdAt: timestamp.toISOString(),
|
|
referencePatient: sourcePatient.noAntrian,
|
|
sourcePatientNo: sourcePatient.no,
|
|
};
|
|
|
|
allPatients.value.push(newPatient);
|
|
|
|
return {
|
|
success: true,
|
|
message: `Antrean ${tipeLayanan} berhasil dibuat: ${newPatient.noAntrian.split(" |")[0]} untuk ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang}`,
|
|
patient: newPatient,
|
|
};
|
|
};
|
|
|
|
// Get patients by klinik and ruang
|
|
const getPatientsByKlinikRuang = (kodeKlinik, nomorRuang, tipeLayanan = null) => {
|
|
return computed(() => {
|
|
let patients = allPatients.value.filter(p =>
|
|
p.kodeKlinik === kodeKlinik &&
|
|
p.nomorRuang === nomorRuang &&
|
|
p.processStage === 'klinik-ruang'
|
|
);
|
|
|
|
if (tipeLayanan) {
|
|
patients = patients.filter(p => p.tipeLayanan === tipeLayanan);
|
|
}
|
|
|
|
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'),
|
|
};
|
|
});
|
|
};
|
|
|
|
// Call next patient for a specific room and tipeLayanan
|
|
const callNextKlinikRuang = (kodeKlinik, nomorRuang, tipeLayanan, allowMultiple = false) => {
|
|
const patients = allPatients.value.filter(p =>
|
|
p.kodeKlinik === kodeKlinik &&
|
|
p.nomorRuang === nomorRuang &&
|
|
p.tipeLayanan === tipeLayanan &&
|
|
p.processStage === 'klinik-ruang' &&
|
|
(p.status === 'waiting' || (allowMultiple && p.status === 'di-loket'))
|
|
).sort((a, b) => {
|
|
// Sort by status priority first
|
|
const statusPriority = {
|
|
'waiting': 1,
|
|
'di-loket': 2
|
|
};
|
|
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
|
|
if (priorityDiff !== 0) return priorityDiff;
|
|
|
|
// Then sort by queue number (extract from noAntrian)
|
|
const numA = parseInt(a.noAntrian.match(/\d+/)?.[0] || '999');
|
|
const numB = parseInt(b.noAntrian.match(/\d+/)?.[0] || '999');
|
|
return numA - numB;
|
|
});
|
|
|
|
if (patients.length === 0) {
|
|
return { success: false, message: `Tidak ada antrian ${tipeLayanan} yang menunggu di ruang ini` };
|
|
}
|
|
|
|
const nextPatient = patients[0];
|
|
const patientIndex = allPatients.value.findIndex(p => p.no === nextPatient.no);
|
|
|
|
if (patientIndex !== -1) {
|
|
// If allowMultiple, we can call even if already di-loket (just update timestamp)
|
|
// Otherwise, only update if status is waiting
|
|
if (allowMultiple || nextPatient.status === 'waiting') {
|
|
allPatients.value[patientIndex] = {
|
|
...allPatients.value[patientIndex],
|
|
status: "di-loket",
|
|
lastCalledAt: new Date().toISOString() // Track last call time
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]} untuk ${tipeLayanan}`,
|
|
patient: allPatients.value[patientIndex],
|
|
};
|
|
};
|
|
|
|
// Process patient in klinik ruang (set as current processing)
|
|
const processPatientKlinikRuang = (patient, action, kodeKlinik, nomorRuang) => {
|
|
const patientIndex = allPatients.value.findIndex(p => p.no === patient.no);
|
|
|
|
if (patientIndex === -1) {
|
|
return { success: false, message: "Pasien tidak ditemukan" };
|
|
}
|
|
|
|
const patientCode = patient.noAntrian.split(" |")[0];
|
|
const key = `klinik-ruang-${kodeKlinik}-${nomorRuang}`;
|
|
let message = "";
|
|
|
|
switch (action) {
|
|
case "proses":
|
|
// Set as current processing for this room (1 pasien, tidak dipisah per tipe layanan)
|
|
currentProcessingPatient.value = {
|
|
...currentProcessingPatient.value,
|
|
[key]: allPatients.value[patientIndex]
|
|
};
|
|
message = `Memproses pasien ${patientCode}`;
|
|
break;
|
|
|
|
case "selesai":
|
|
allPatients.value[patientIndex] = {
|
|
...allPatients.value[patientIndex],
|
|
status: "processed"
|
|
};
|
|
// Clear current processing
|
|
currentProcessingPatient.value[key] = null;
|
|
message = `Pasien ${patientCode} selesai diproses`;
|
|
break;
|
|
|
|
case "terlambat":
|
|
allPatients.value[patientIndex] = {
|
|
...allPatients.value[patientIndex],
|
|
status: "terlambat"
|
|
};
|
|
currentProcessingPatient.value[key] = null;
|
|
message = `Pasien ${patientCode} ditandai terlambat`;
|
|
break;
|
|
|
|
case "pending":
|
|
allPatients.value[patientIndex] = {
|
|
...allPatients.value[patientIndex],
|
|
status: "pending"
|
|
};
|
|
currentProcessingPatient.value[key] = null;
|
|
message = `Pasien ${patientCode} di-pending`;
|
|
break;
|
|
}
|
|
|
|
return { success: true, message };
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
// Helper function untuk generate nomor antrean baru
|
|
// Format: 2 huruf (kode klinik) + 3 digit (001-999)
|
|
// Generate nomor antrian dengan format baru untuk anjungan: R/E + Loket (A-N) + 3 digit
|
|
// Format: R/E (jenis pelayanan Reguler/Eksekutif) + A-N (nomor loket) + 3 digit angka
|
|
// Contoh: RA001, EA001, RB001, RB002
|
|
// - R = Reguler (untuk pasien Reguler/BPJS)
|
|
// - E = Eksekutif (untuk pasien Eksekutif/Grand Pavilion)
|
|
// Urutan terpisah untuk UMUM dan JKN/BPJS
|
|
// Setiap loket menampung maksimal 999 nomor (001-999)
|
|
const generateQueueNumber = (clinic, paymentType, isEksekutif = false) => {
|
|
// Tentukan prefix jenis pelayanan: R untuk Reguler, E untuk Eksekutif (Grand Pavilion)
|
|
const serviceType = isEksekutif ? 'E' : 'R';
|
|
|
|
// Tentukan payment group untuk counter terpisah (UMUM atau JKN/BPJS)
|
|
const paymentGroup = paymentType === 'BPJS' || paymentType === 'JKN' ? 'JKN' : 'UMUM';
|
|
|
|
// Get counter untuk payment group ini per hari
|
|
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
const counterKey = `queue_counter_loket_${paymentGroup}_${today}`;
|
|
|
|
// Get current counter dari localStorage
|
|
let counter = 0;
|
|
if (typeof window !== 'undefined') {
|
|
const stored = localStorage.getItem(counterKey);
|
|
counter = stored ? parseInt(stored, 10) : 0;
|
|
}
|
|
|
|
// Increment counter
|
|
counter = counter + 1;
|
|
|
|
// Tentukan loket berdasarkan counter
|
|
// Loket A untuk 1-999, B untuk 1000-1998, C untuk 2000-2997, dst sampai N (14 loket)
|
|
// Setiap loket menampung maksimal 999 nomor (001-999)
|
|
// Loket index: 0=A, 1=B, 2=C, ..., 13=N
|
|
const loketIndex = Math.floor((counter - 1) / 999); // 0 untuk A, 1 untuk B, dst
|
|
|
|
// Jika loket melebihi N (14 loket, index 13), wrap kembali ke A
|
|
// Maksimal 14 loket (A-N), jadi maksimal counter per hari = 14 * 999 = 13986
|
|
// Jika melebihi, reset counter ke 1
|
|
if (loketIndex > 13) {
|
|
counter = 1;
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(counterKey, '1');
|
|
}
|
|
// Format: R/E + Loket A + 001
|
|
return `${serviceType}A001`;
|
|
}
|
|
|
|
// Loket letter (A-N)
|
|
const loketLetter = String.fromCharCode(65 + loketIndex); // 65 = 'A', 66 = 'B', dst
|
|
|
|
// Nomor dalam loket (1-999, di-display sebagai 001-999)
|
|
// counter 1-999 → loket A, nomor 001-999
|
|
// counter 1000-1998 → loket B, nomor 001-999
|
|
const numberInLoket = ((counter - 1) % 999) + 1;
|
|
const numberPart = String(numberInLoket).padStart(3, '0');
|
|
|
|
// Save counter back to localStorage
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(counterKey, counter.toString());
|
|
}
|
|
|
|
// Format: R/E + Loket + 3 digit
|
|
// Contoh: RA001, RA002, ..., RA999, RB001, RB002, ...
|
|
return `${serviceType}${loketLetter}${numberPart}`;
|
|
};
|
|
|
|
// 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 dengan format: YYMMDD + 5 digit random
|
|
const barcode = generateBarcode([], allPatients);
|
|
|
|
// Tentukan apakah pasien Eksekutif/Grand Pavilion (jika ada namaDokter, berarti Eksekutif)
|
|
const isEksekutif = namaDokter !== null && namaDokter !== undefined && namaDokter !== '';
|
|
|
|
// Generate nomor antrean dengan format baru: R/E + Loket (A-N) + 3 digit
|
|
// Format: RA001 (Reguler), EA001 (Eksekutif/Grand Pavilion), RB001, RB002, dll
|
|
const queueNumber = generateQueueNumber(clinic, paymentType, isEksekutif);
|
|
const noAntrian = `${queueNumber} | Onsite - ${barcode}`;
|
|
|
|
// Status awal untuk pasien dari anjungan adalah "menunggu" (belum dipanggil)
|
|
// Hanya setelah dipanggil oleh admin loket, status berubah menjadi "waiting" (bisa check-in)
|
|
const status = 'menunggu';
|
|
|
|
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 normalize
|
|
const cleanInput = String(patientIdOrBarcode).trim();
|
|
const cleanInputUpper = cleanInput.toUpperCase();
|
|
|
|
// Try to find by multiple criteria:
|
|
// 1. Exact barcode match (case-insensitive, whitespace-insensitive)
|
|
// 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 => {
|
|
// Normalize barcode untuk comparison
|
|
const patientBarcode = String(p.barcode || '').trim();
|
|
const patientBarcodeUpper = patientBarcode.toUpperCase();
|
|
|
|
// Exact barcode match (case-insensitive, whitespace-insensitive)
|
|
if (patientBarcode === cleanInput ||
|
|
patientBarcodeUpper === cleanInputUpper ||
|
|
patientBarcode === patientIdOrBarcode) {
|
|
console.log('✅ Found by barcode:', patientBarcode, '===', cleanInput);
|
|
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" };
|
|
}
|
|
|
|
// IMPORTANT: Get fresh patient data from array to avoid stale data
|
|
// Use the patientIndex we found earlier, but get fresh data from array
|
|
const patient = allPatients.value[patientIndex];
|
|
console.log('✅ Patient found (fresh):', {
|
|
no: patient.no,
|
|
barcode: patient.barcode,
|
|
noAntrian: patient.noAntrian,
|
|
status: patient.status,
|
|
processStage: patient.processStage
|
|
});
|
|
|
|
// Only allow check-in if status is waiting (sudah dipanggil) or pending
|
|
// Pasien dengan status "menunggu" belum bisa check-in (belum dipanggil)
|
|
if (patient.status === 'menunggu') {
|
|
console.log('⚠️ Patient status is menunggu (belum dipanggil):', patient.status);
|
|
return {
|
|
success: false,
|
|
message: `Pasien belum dipanggil oleh admin loket. Mohon menunggu hingga nomor antrean Anda dipanggil.`
|
|
};
|
|
}
|
|
|
|
// Check if already checked in
|
|
if (patient.status === 'di-loket') {
|
|
console.log('⚠️ Patient already checked in:', patient.status);
|
|
return {
|
|
success: false,
|
|
message: `Pasien sudah melakukan check-in sebelumnya. Status saat ini: ${patient.status}`
|
|
};
|
|
}
|
|
|
|
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 using the patientIndex
|
|
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,
|
|
createAntreanKlinikRuang,
|
|
scanAndCreateAntreanKlinikRuang,
|
|
getPatientsByKlinikRuang,
|
|
callNextKlinikRuang,
|
|
processPatientKlinikRuang,
|
|
changeKlinik,
|
|
processNextQueue,
|
|
getPatientsByStage,
|
|
getTotalPasienByStage,
|
|
getCurrentProcessing,
|
|
setCurrentProcessing,
|
|
registerPatientFromAnjungan,
|
|
checkInPatient,
|
|
generateQueueNumber,
|
|
};
|
|
}, {
|
|
persist: {
|
|
key: 'queue-store-state',
|
|
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
|
paths: ['allPatients', 'quotaUsed', 'currentProcessingPatient'],
|
|
serializer: {
|
|
deserialize: JSON.parse,
|
|
serialize: JSON.stringify,
|
|
},
|
|
restore: (value) => {
|
|
// Ensure allPatients is always an array
|
|
if (value && value.allPatients && !Array.isArray(value.allPatients)) {
|
|
value.allPatients = [];
|
|
}
|
|
return value;
|
|
},
|
|
},
|
|
}); |