// 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 increment barcode counter setelah barcode digunakan const incrementBarcodeCounter = () => { if (typeof window === 'undefined' || typeof localStorage === 'undefined') return; const now = new Date(); const year = String(now.getFullYear()).slice(-2); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const datePrefix = `${year}${month}${day}`; const STORAGE_KEY = `barcode_counter_${datePrefix}`; const LAST_DATE_KEY = 'barcode_last_date'; const storedCounter = localStorage.getItem(STORAGE_KEY); if (storedCounter) { const currentCounter = parseInt(storedCounter, 10) || 1; localStorage.setItem(STORAGE_KEY, String(currentCounter + 1)); localStorage.setItem(LAST_DATE_KEY, datePrefix); } }; // Helper function untuk generate barcode dengan format: YYMMDD + 5 digit sequential // Format: YY (tahun 2 digit terakhir) + MM (bulan 2 digit) + DD (tanggal 2 digit) + XXXXX (5 digit sequential) // Contoh: 26011400001, 26011400002, dst // Counter akan reset setiap ganti tanggal (mulai dari 00001 lagi) // IMPORTANT: Memastikan barcode selalu UNIQUE dan sequential per tanggal // 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 const datePrefix = `${year}${month}${day}`; // YYMMDD // Check if localStorage is available (browser environment) if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') { // Key untuk localStorage berdasarkan tanggal const STORAGE_KEY = `barcode_counter_${datePrefix}`; const LAST_DATE_KEY = 'barcode_last_date'; // Cek apakah tanggal sudah berubah (reset counter) const lastDate = localStorage.getItem(LAST_DATE_KEY); const currentDate = datePrefix; let counter = 1; // Default mulai dari 1 if (lastDate === currentDate) { // Tanggal sama, lanjutkan counter dari localStorage const storedCounter = localStorage.getItem(STORAGE_KEY); if (storedCounter) { counter = parseInt(storedCounter, 10) || 1; } } else { // Tanggal berbeda, reset counter ke 1 counter = 1; // Hapus counter lama untuk tanggal sebelumnya (cleanup) if (lastDate) { localStorage.removeItem(`barcode_counter_${lastDate}`); } } // Generate barcode dengan counter saat ini let barcode = `${datePrefix}${String(counter).padStart(5, '0')}`; // Cek apakah barcode sudah ada di existingBarcodes atau allPatientsRef 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; }; let attempts = 0; const maxAttempts = 1000; // Maksimal 1000 pasien per hari // Cek uniqueness dan increment jika duplikat while (checkExists(barcode) && attempts < maxAttempts) { counter++; barcode = `${datePrefix}${String(counter).padStart(5, '0')}`; attempts++; } // IMPORTANT: JANGAN tulis counter ke localStorage di sini! // Counter hanya di-increment dan di-save setelah barcode benar-benar digunakan // Ini mencegah counter naik meskipun barcode tidak digunakan (misalnya saat refresh) // Counter akan di-increment oleh incrementBarcodeCounter() setelah pasien dibuat // Hanya update LAST_DATE_KEY untuk tracking tanggal localStorage.setItem(LAST_DATE_KEY, currentDate); return barcode; } else { // Fallback untuk SSR atau environment tanpa localStorage // Gunakan counter berdasarkan existing barcodes atau allPatientsRef const existingCount = existingBarcodes.filter(b => b && b.startsWith(datePrefix)).length; 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'); return `${datePrefix}${counterCode}`; } }; // 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 // NOTE: Seed data menggunakan barcode statis untuk menghindari increment counter saat refresh // Counter hanya di-increment ketika pasien baru benar-benar dibuat dari Anjungan // Gunakan barcode statis untuk seed data (tidak memanggil generateBarcode) // Format: YYMMDD + 5 digit sequential dimulai dari 00001 // Ini mencegah counter naik setiap kali halaman di-refresh const getSeedBarcode = (index) => { if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') { const now = new Date(); const year = String(now.getFullYear()).slice(-2); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const datePrefix = `${year}${month}${day}`; // Gunakan barcode statis berdasarkan index (1-16) // Format: YYMMDD + 5 digit (contoh: 26011500001, 26011500002, dst) return `${datePrefix}${String(index).padStart(5, '0')}`; } // Fallback untuk SSR const now = new Date(); const year = String(now.getFullYear()).slice(-2); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const datePrefix = `${year}${month}${day}`; return `${datePrefix}${String(index).padStart(5, '0')}`; }; const seedBarcode1 = getSeedBarcode(1); const seedBarcode2 = getSeedBarcode(2); const seedBarcode3 = getSeedBarcode(3); const seedBarcode4 = getSeedBarcode(4); const seedBarcode5 = getSeedBarcode(5); const seedBarcode6 = getSeedBarcode(6); const seedBarcode7 = getSeedBarcode(7); const seedBarcode8 = getSeedBarcode(8); const seedBarcode9 = getSeedBarcode(9); const seedBarcode10 = getSeedBarcode(10); const seedBarcode11 = getSeedBarcode(11); const seedBarcode12 = getSeedBarcode(12); // Barcode untuk pasien Eksekutif const seedBarcodeE1 = getSeedBarcode(13); const seedBarcodeE2 = getSeedBarcode(14); const seedBarcodeE3 = getSeedBarcode(15); const seedBarcodeE4 = getSeedBarcode(16); // IMPORTANT: Set counter ke nilai yang sesuai dengan jumlah seed data // Ini mencegah counter naik tidak terkendali saat seed data di-generate // Counter akan di-set ke jumlah seed data (16) + 1 untuk next barcode // Hanya set jika counter belum ada atau lebih kecil dari jumlah seed data if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') { const now = new Date(); const year = String(now.getFullYear()).slice(-2); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const datePrefix = `${year}${month}${day}`; const STORAGE_KEY = `barcode_counter_${datePrefix}`; const LAST_DATE_KEY = 'barcode_last_date'; const storedCounter = localStorage.getItem(STORAGE_KEY); const seedDataCount = 16; // Jumlah seed data // Hanya set counter jika belum ada atau lebih kecil dari jumlah seed data // Jangan overwrite counter yang sudah lebih besar (berarti sudah ada pasien baru) if (!storedCounter || parseInt(storedCounter, 10) < seedDataCount) { localStorage.setItem(STORAGE_KEY, String(seedDataCount + 1)); localStorage.setItem(LAST_DATE_KEY, datePrefix); } } const seedPatients = [ { no: 1, jamPanggil: "12:49", barcode: seedBarcode1, // Barcode unik untuk setiap pasien noAntrian: `F-RA001 | Online - ${seedBarcode1}`, // Counter 1: Fast Track BPJS shift: "Shift 1", klinik: "KANDUNGAN", kodeKlinik: "KD", fastTrack: "YA", // Fast Track Patient pembayaran: "BPJS", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000001", penanggungJawab: "Dr. Ahmad Wijaya", // Fast Track data alasanFastTrack: "Pasien prioritas", // Fast Track data }, { no: 2, jamPanggil: "10:52", barcode: seedBarcode2, noAntrian: `RA002 | Online - ${seedBarcode2}`, // Counter 2: Non-fast track UMUM shift: "Shift 1", klinik: "IPD", kodeKlinik: "IP", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000002", penanggungJawab: null, alasanFastTrack: null, }, { no: 3, jamPanggil: "09:30", barcode: seedBarcode3, noAntrian: `F-RA003 | Online - ${seedBarcode3}`, // Counter 3: Fast Track BPJS shift: "Shift 1", klinik: "SARAF", kodeKlinik: "SR", fastTrack: "YA", // Fast Track Patient pembayaran: "BPJS", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000003", penanggungJawab: "Dr. Budi Santoso", // Fast Track data alasanFastTrack: "Kondisi darurat", // Fast Track data }, { no: 4, jamPanggil: "14:15", barcode: seedBarcode4, noAntrian: `RA004 | Online - ${seedBarcode4}`, // Counter 4: Non-fast track UMUM shift: "Shift 1", klinik: "THT", kodeKlinik: "TH", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000004", penanggungJawab: null, alasanFastTrack: null, }, { no: 5, jamPanggil: "12:49", barcode: seedBarcode5, noAntrian: `RA005 | Online - ${seedBarcode5}`, // Counter 5: Non-fast track UMUM shift: "Shift 2", klinik: "KANDUNGAN", kodeKlinik: "KD", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000005", penanggungJawab: null, alasanFastTrack: null, }, { no: 6, jamPanggil: "10:52", barcode: seedBarcode6, noAntrian: `F-RA006 | Online - ${seedBarcode6}`, // Counter 6: Fast Track BPJS shift: "Shift 1", klinik: "IPD", kodeKlinik: "IP", fastTrack: "YA", // Fast Track Patient pembayaran: "BPJS", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000006", penanggungJawab: "Dr. Citra Dewi", // Fast Track data alasanFastTrack: "Rujukan darurat", // Fast Track data }, { no: 7, jamPanggil: "09:30", barcode: seedBarcode7, noAntrian: `RA007 | Online - ${seedBarcode7}`, // Counter 7: Non-fast track UMUM shift: "Shift 1", klinik: "SARAF", kodeKlinik: "SR", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000007", penanggungJawab: null, alasanFastTrack: null, }, { no: 8, jamPanggil: "14:15", barcode: seedBarcode8, noAntrian: `RA008 | Online - ${seedBarcode8}`, // Counter 8: Non-fast track UMUM shift: "Shift 1", klinik: "THT", kodeKlinik: "TH", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000008", penanggungJawab: null, alasanFastTrack: null, }, { no: 9, jamPanggil: "12:49", barcode: seedBarcode9, noAntrian: `F-RA009 | Online - ${seedBarcode9}`, // Counter 9: Fast Track BPJS shift: "Shift 2", klinik: "KANDUNGAN", kodeKlinik: "KD", fastTrack: "YA", // Fast Track Patient pembayaran: "BPJS", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000009", penanggungJawab: "Dr. Dedi Kurniawan", // Fast Track data alasanFastTrack: "Pasien VIP", // Fast Track data }, { no: 10, jamPanggil: "10:52", barcode: seedBarcode10, noAntrian: `RA010 | Online - ${seedBarcode10}`, // Counter 10: Non-fast track UMUM shift: "Shift 1", klinik: "IPD", kodeKlinik: "IP", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000010", penanggungJawab: null, alasanFastTrack: null, }, { no: 11, jamPanggil: "09:30", barcode: seedBarcode11, noAntrian: `RA011 | Online - ${seedBarcode11}`, // Counter 11: Non-fast track UMUM shift: "Shift 1", klinik: "SARAF", kodeKlinik: "SR", fastTrack: "TIDAK", pembayaran: "UMUM", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000011", penanggungJawab: null, alasanFastTrack: null, }, { no: 12, jamPanggil: "14:15", barcode: seedBarcode12, noAntrian: `F-RA012 | Online - ${seedBarcode12}`, // Counter 12: Fast Track BPJS shift: "Shift 2", klinik: "THT", kodeKlinik: "TH", fastTrack: "YA", // Fast Track Patient pembayaran: "BPJS", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: null, noRM: "RM-000012", penanggungJawab: "Dr. Eka Putri", // Fast Track data alasanFastTrack: "Kondisi kritis", // Fast Track data }, { no: 13, jamPanggil: "11:20", barcode: seedBarcodeE1, // Barcode unik untuk pasien Eksekutif noAntrian: `EA013 | Online - ${seedBarcodeE1}`, // Counter 13: Non-fast track Eksekutif shift: "Shift 1", klinik: "KANDUNGAN", kodeKlinik: "KD", fastTrack: "TIDAK", pembayaran: "Eksekutif", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Ahmad Wijaya, Sp.OG", noRM: "RM-000013", penanggungJawab: null, alasanFastTrack: null, }, { no: 14, jamPanggil: "13:45", barcode: seedBarcodeE2, noAntrian: `EA014 | Online - ${seedBarcodeE2}`, // Counter 14: Non-fast track Eksekutif shift: "Shift 1", klinik: "IPD", kodeKlinik: "IP", fastTrack: "TIDAK", pembayaran: "Eksekutif", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Budi Santoso, Sp.PD", noRM: "RM-000014", penanggungJawab: null, alasanFastTrack: null, }, { no: 15, jamPanggil: "15:10", barcode: seedBarcodeE3, noAntrian: `F-EA015 | Online - ${seedBarcodeE3}`, // Counter 15: Fast Track Eksekutif shift: "Shift 2", klinik: "SARAF", kodeKlinik: "SR", fastTrack: "YA", // Fast Track Patient pembayaran: "Eksekutif", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Citra Dewi, Sp.S", noRM: "RM-000015", penanggungJawab: "Dr. Citra Dewi", // Fast Track data alasanFastTrack: "Pasien Eksekutif prioritas", // Fast Track data }, { no: 16, jamPanggil: "16:30", barcode: seedBarcodeE4, noAntrian: `EA016 | Online - ${seedBarcodeE4}`, // Counter 16: Non-fast track Eksekutif shift: "Shift 1", klinik: "THT", fastTrack: "TIDAK", pembayaran: "Eksekutif", status: "waiting", processStage: "loket", createdAt: new Date().toISOString(), registrationType: 'online', visitType: 'SEKARANG', visitDate: new Date().toISOString().substring(0, 10), namaDokter: "Dr. Dedi Kurniawan, Sp.THT", noRM: "RM-000016", penanggungJawab: null, alasanFastTrack: null, }, ]; const cloneSeed = () => seedPatients.map(p => ({ ...p })); // Initialize counters from seed data to ensure numbering continues correctly // Counter SHARED untuk semua payment group dan semua jenis pasien // Calculate max queue number from seeds or existing patients const syncCountersWithState = () => { if (typeof window === 'undefined') return; const today = new Date().toISOString().split('T')[0]; const STORAGE_KEY_BARCODE = `barcode_counter_${today.replace(/-/g, '').substring(2)}`; // Determine the source of patients (prefer current state, fallback to seeds) const sourcePatients = allPatients.value.length > 0 ? allPatients.value : seedPatients; // Find max barcode counter let maxBarcodeCounter = 0; sourcePatients.forEach(patient => { const barcode = patient.barcode || ''; if (barcode.length > 5) { const counterStr = barcode.substring(barcode.length - 5); const counter = parseInt(counterStr, 10); if (!isNaN(counter) && counter > maxBarcodeCounter) { maxBarcodeCounter = counter; } } }); // Initialize barcode counter if not exists or smaller const existingBarcode = localStorage.getItem(STORAGE_KEY_BARCODE); if (!existingBarcode || parseInt(existingBarcode, 10) < maxBarcodeCounter) { localStorage.setItem(STORAGE_KEY_BARCODE, maxBarcodeCounter.toString()); } // Existing queue counter logic... const counterKeyQueue = `queue_counter_loket_shared_${today}`; // Calculate max queue number from seeds dynamically const maxQueueCounter = sourcePatients.length > 0 ? Math.max(...sourcePatients.map(p => p.no)) : 0; const existingQueue = localStorage.getItem(counterKeyQueue); // Always update if current max in memory is larger than stored if (!existingQueue || parseInt(existingQueue) < maxQueueCounter) { localStorage.setItem(counterKeyQueue, maxQueueCounter.toString()); } }; // Initial state - START EMPTY to avoid clashing with hydration const allPatients = ref([]); const quotaUsed = ref(5); // State - currentProcessingPatient is now an object mapping 'stage-id' to patient // For example: { 'loket-1': patient, 'loket-2': patient, 'klinik-3': patient } const currentProcessingPatient = ref({}); // Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore const kliniks = ref(clinicStore.clinics || []); // Penunjang data - reference dari penunjangStore const penunjangs = ref(penunjangStore.penunjangs || []); /** * Ensures initial data exists. * Only seeds if the store is empty (not hydrated from storage) */ const ensureInitialData = () => { if (allPatients.value.length === 0) { console.log('🌱 Seeding queueStore with initial data...'); allPatients.value = cloneSeed(); } // ALWAYS sync counters with state (hydrated or seeded) to ensure next number is correct syncCountersWithState(); }; // CROSS-TAB SYNC: Listen for storage events to update store across tabs if (typeof window !== 'undefined') { window.addEventListener('storage', (event) => { if (event.key === 'queue-store-state') { console.log('🔄 queue-store-state changed in another tab, re-hydrating...'); try { const newState = JSON.parse(event.newValue); if (newState) { if (newState.allPatients) allPatients.value = newState.allPatients; if (newState.quotaUsed !== undefined) quotaUsed.value = newState.quotaUsed; if (newState.currentProcessingPatient) currentProcessingPatient.value = newState.currentProcessingPatient; } } catch (e) { console.error('Error hydrating from storage event:', e); } } }); } // 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'), // Pasien yang sudah dipanggil, menunggu check-in 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 }; syncCountersWithState(); // Re-initialize counters after reset }; // Action: Ambil Antrean Masuk dari Anjungan (Reservoir -> Waiting Room) // "fungsi ini juga sesuaikan dengan idloket dan klinik id" // "hanya memanggil tiket dari loket lain harus tiket menunggu yang sesuai id loket dan id kliniknya" const callNext = (adminType = 'loket', specificId = null) => { const stageMap = { 'loket': 'loket', 'klinik': 'klinik', 'penunjang': 'penunjang' }; const targetStage = stageMap[adminType]; const targetId = specificId; // Filter list by stage AND relevance to this specific loket/clinic/penunjang const eligiblePatients = allPatients.value.filter(p => { // 1. Stage Check if (p.processStage !== targetStage) return false; // 2. Relevance Check based on adminType if (targetId) { if (adminType === 'loket') { const thisLoket = loketStore.getLoketById(parseInt(targetId)); // Enforce clinic mapping (pelayanan) if (thisLoket && thisLoket.pelayanan && Array.isArray(thisLoket.pelayanan)) { if (!thisLoket.pelayanan.includes(p.kodeKlinik)) return false; } // Enforce assignment if present if (p.loketId && String(p.loketId) !== String(targetId)) return false; } else if (adminType === 'klinik') { // Clinic ID match if (String(p.kodeKlinik) !== String(targetId) && String(p.klinik) !== String(targetId)) return false; } else if (adminType === 'penunjang') { // Penunjang match if (String(p.klinik) !== String(targetId) && String(p.kodeKlinik) !== String(targetId)) return false; } } return true; }); // PRIORITAS: Hanya ambil pasien dengan status 'menunggu' (Antrean Baru dari Anjungan) // "bukan untuk memanggil pasien tapi tiket baru dari anjungan yang statusnya menunggu" const nextPatient = eligiblePatients.find(p => p.status === 'menunggu'); if (!nextPatient) { return { success: false, message: `Tidak ada antrean baru yang sesuai untuk ${adminType} ${targetId || ''}` }; } // Hitung kuota yang tersedia (khusus loket ini) const diLoketCount = allPatients.value.filter(p => p.status === 'di-loket' && p.processStage === targetStage && (targetId && adminType === 'loket' ? String(p.loketId) === String(targetId) : true) ).length; const availableQuota = 150 - diLoketCount; if (availableQuota <= 0) { return { success: false, message: "Kuota sudah penuh" }; } // Update status menjadi 'waiting' (Masuk ke antrean aktif) 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", // Assign to this specific loket if it was unassigned loketId: (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId, lastCalledAt: callTimestamp }; } return { success: true, message: `Berhasil mengambil antrean ${nextPatient.noAntrian.split(" |")[0]} ke daftar tunggu`, }; }; const callMultiplePatients = (count, adminType = 'loket', specificId = null) => { const stageMap = { 'loket': 'loket', 'klinik': 'klinik', 'penunjang': 'penunjang' }; const targetStage = stageMap[adminType]; const targetId = specificId; const eligiblePatients = allPatients.value.filter(p => { if (p.processStage !== targetStage) return false; if (targetId) { if (adminType === 'loket') { const thisLoket = loketStore.getLoketById(parseInt(targetId)); if (thisLoket && thisLoket.pelayanan && Array.isArray(thisLoket.pelayanan)) { if (!thisLoket.pelayanan.includes(p.kodeKlinik)) return false; } if (p.loketId && String(p.loketId) !== String(targetId)) return false; } else if (adminType === 'klinik' || adminType === 'penunjang') { if (String(p.kodeKlinik) !== String(targetId) && String(p.klinik) !== String(targetId)) return false; } } return true; }); // Hanya ambil pasien status 'menunggu' const menungguList = eligiblePatients.filter(p => p.status === 'menunggu'); if (menungguList.length === 0) { return { success: false, message: `Tidak ada antrean baru yang sesuai untuk ${adminType} ${targetId || ''}` }; } // Hitung kuota yang tersedia const diLoketCount = allPatients.value.filter(p => p.status === 'di-loket' && p.processStage === targetStage && (targetId && adminType === 'loket' ? String(p.loketId) === String(targetId) : true) ).length; const availableQuota = 150 - diLoketCount; const maxCallable = Math.min(count, menungguList.length, availableQuota); if (maxCallable <= 0) { if (availableQuota <= 0) return { success: false, message: "Kuota sudah penuh" }; return { success: false, message: "Tidak ada antrean yang bisa diambil" }; } const patientsToCall = menungguList.slice(0, maxCallable); 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", loketId: (adminType === 'loket' && targetId) ? targetId : allPatients.value[index].loketId, lastCalledAt: callTimestamp }; } }); return { success: true, message: `Berhasil mengambil ${patientsToCall.length} antrean ke daftar tunggu`, }; }; const processPatient = (patient, action, adminType = 'loket', specificId = null) => { const storageKey = specificId ? `${adminType}-${specificId}` : adminType; 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", calledByAdmin: false // Reset flag saat pindah stage }; 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[storageKey]?.no === patient.no) { currentProcessingPatient.value[storageKey] = null; } break; case "terlambat": allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "terlambat", calledByAdmin: false // Reset flag }; if (currentProcessingPatient.value[storageKey]?.no === patient.no) { currentProcessingPatient.value[storageKey] = null; } message = `Pasien ${patientCode} ditandai terlambat`; break; case "pending": allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "pending", calledByAdmin: false // Reset flag }; if (currentProcessingPatient.value[storageKey]?.no === patient.no) { currentProcessingPatient.value[storageKey] = 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 pasien memiliki status "pending" atau "terlambat", ubah menjadi "di-loket" // agar pasien masuk ke kategori diLoketPatients dan ditampilkan sebagai "diproses" const currentStatus = patient.status; const shouldUpdateStatus = currentStatus === "pending" || currentStatus === "terlambat"; // 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 dan status (jika perlu) const updatedPatient = { ...patient, loket: patient.loket || currentLoket, loketId: patient.loketId || currentLoketId, // Ubah status menjadi "di-loket" jika sebelumnya "pending" atau "terlambat" ...(shouldUpdateStatus && { status: "di-loket" }) }; allPatients.value[patientIndex] = updatedPatient; // Set currentProcessingPatient with isolated key currentProcessingPatient.value[storageKey] = updatedPatient; } else { // Untuk adminType selain loket, update status jika perlu if (shouldUpdateStatus) { const updatedPatient = { ...patient, status: "di-loket" }; allPatients.value[patientIndex] = updatedPatient; currentProcessingPatient.value[storageKey] = updatedPatient; } else { currentProcessingPatient.value[storageKey] = patient; } } message = `Memproses pasien ${patientCode}`; break; } } return { success: true, message }; }; const callProcessingPatient = (adminType = 'loket', specificId = null) => { // Panggil pasien yang sedang diproses untuk ditampilkan di layar anjungan const storageKey = specificId ? `${adminType}-${specificId}` : adminType; const processingPatient = currentProcessingPatient.value[storageKey]; if (!processingPatient) { return { success: false, message: "Tidak ada pasien yang sedang diproses" }; } // Update flag calledByAdmin const patientIndex = allPatients.value.findIndex(p => p.no === processingPatient.no); if (patientIndex !== -1) { allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], calledByAdmin: true, lastCalledAt: new Date().toISOString() }; // Update currentProcessingPatient with the new data currentProcessingPatient.value[storageKey] = allPatients.value[patientIndex]; return { success: true, message: `Memanggil pasien ${processingPatient.noAntrian.split(" |")[0]}` }; } return { success: false, message: "Gagal memanggil pasien" }; }; 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", noRM: patient ? (patient.noRM || `RM-SCAN-${barcode.slice(-6)}`) : `RM-SCAN-${barcode.slice(-6)}`, kodeKlinik: klinik.kode || klinik.id, // Ensure kodeKlinik is saved status: "di-loket", processStage: "klinik", createdAt: timestamp.toISOString(), referencePatient: patient ? patient.noAntrian : null, loketId: null, // Initial check }; // Auto-assign Loket ID based on Clinic Mapping // "menyesuaikan loketnya berdasar loket id tergantung dari create tiket itu di klinik" if (newPatient.kodeKlinik) { const allLokets = loketStore.lokets || []; const targetLoket = allLokets.find(l => l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik) ); if (targetLoket) { newPatient.loketId = targetLoket.id; newPatient.loket = targetLoket.namaLoket; console.log(`✅ Auto-assigned Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`); } else { console.log(`⚠️ No specific Loket found for Clinic ${newPatient.kodeKlinik}, defaulting to general pool.`); } } allPatients.value.push(newPatient); // Increment counter setelah barcode digunakan if (!patient) { incrementBarcodeCounter(); } 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", noRM: patient ? patient.noRM : `RM-SCAN-${barcode.slice(-6)}`, status: "di-loket", processStage: "penunjang", createdAt: timestamp.toISOString(), referencePatient: patient ? patient.noAntrian : null, }; allPatients.value.push(newPatient); // Increment counter setelah barcode digunakan if (!patient) { incrementBarcodeCounter(); } 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", noRM: patient ? patient.noRM : null, 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, calledByAdmin: false, // Flag untuk tracking apakah sudah dipanggil oleh admin }; allPatients.value.push(newPatient); // Increment counter setelah barcode digunakan if (!patient) { incrementBarcodeCounter(); } return { success: true, message: `Antrean ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang} berhasil dibuat: ${newNoAntrian}`, patient: newPatient, }; }; // Pindah pasien ke klinik ruang lain dengan nomor antrian tetap const pindahKlinikRuang = (patient, targetKlinikRuang, targetRuang) => { const patientIndex = allPatients.value.findIndex(p => p.no === patient.no); if (patientIndex === -1) { return { success: false, message: "Pasien tidak ditemukan" }; } const currentPatient = allPatients.value[patientIndex]; // Jika pasien sudah punya antrean klinik ruang, update antreannya if (currentPatient.processStage === 'klinik-ruang' && currentPatient.tipeLayanan) { const baseNoAntrian = currentPatient.noAntrian?.split(" |")[0] || currentPatient.barcode || ''; // Update dengan klinik dan ruang baru, tapi nomor antrian tetap sama allPatients.value[patientIndex] = { ...currentPatient, klinik: targetKlinikRuang.namaKlinik, ruang: targetRuang.namaRuang, kodeKlinik: targetKlinikRuang.kodeKlinik, nomorRuang: targetRuang.nomorRuang, nomorScreen: targetRuang.nomorScreen, // Nomor antrian tetap sama noAntrian: `${baseNoAntrian} | ${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} - ${currentPatient.tipeLayanan}`, noAntrianRuang: `${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} | ${baseNoAntrian}` }; // Clear current processing dari ruang lama jika ada const oldKey = `klinik-ruang-${currentPatient.kodeKlinik}-${currentPatient.nomorRuang}`; if (currentProcessingPatient.value[oldKey]?.no === currentPatient.no) { currentProcessingPatient.value[oldKey] = null; } } else { // Pasien belum digenerate tiket, hanya update ruang allPatients.value[patientIndex] = { ...currentPatient, klinik: targetKlinikRuang.namaKlinik, ruang: targetRuang.namaRuang, kodeKlinik: targetKlinikRuang.kodeKlinik, nomorRuang: targetRuang.nomorRuang, nomorScreen: targetRuang.nomorScreen }; } return { success: true, message: `Pasien berhasil dipindah ke ${targetKlinikRuang.namaKlinik} Ruang ${targetRuang.nomorRuang}`, patient: allPatients.value[patientIndex] }; }; // Konsultasi: pindah pasien ke klinik ruang lain dengan nomor antrian baru (prefix K- atau KF-) const konsultasiKlinikRuang = (patient, targetKlinikRuang, targetRuang, tipeLayanan = 'Pemeriksaan Awal') => { const patientIndex = allPatients.value.findIndex(p => p.no === patient.no); if (patientIndex === -1) { return { success: false, message: "Pasien tidak ditemukan" }; } const sourcePatient = allPatients.value[patientIndex]; // Cek apakah sudah ada antrean konsultasi di ruang tujuan const existingKonsultasi = allPatients.value.find(p => p.referencePatient === sourcePatient.noAntrian && p.kodeKlinik === targetKlinikRuang.kodeKlinik && p.nomorRuang === targetRuang.nomorRuang && p.processStage === 'klinik-ruang' && (p.noAntrian?.startsWith('K-') || p.noAntrian?.startsWith('KF-')) ); if (existingKonsultasi) { return { success: false, message: `Pasien sudah memiliki antrean konsultasi di ${targetKlinikRuang.namaKlinik} Ruang ${targetRuang.nomorRuang}` }; } // Generate queue number untuk konsultasi const roomQueues = allPatients.value.filter(p => p.kodeKlinik === targetKlinikRuang.kodeKlinik && p.nomorRuang === targetRuang.nomorRuang && p.tipeLayanan === tipeLayanan && p.processStage === 'klinik-ruang' && (p.noAntrian?.startsWith('K-') || p.noAntrian?.startsWith('KF-')) ); const queueNumber = roomQueues.length + 1; const prefix = tipeLayanan === 'Pemeriksaan Awal' ? 'PA' : 'TD'; // Tentukan prefix konsultasi: KF- untuk Fast Track, K- untuk normal const isFastTrack = sourcePatient.fastTrack === "YA"; const konsultasiPrefix = isFastTrack ? 'KF-' : 'K-'; const queueNumberStr = String(queueNumber).padStart(3, "0"); const newNoAntrian = `${konsultasiPrefix}${prefix}${queueNumberStr}`; 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: `${newNoAntrian} | ${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} - ${tipeLayanan}`, noAntrianRuang: `${targetKlinikRuang.namaKlinik} - ${targetRuang.namaRuang} | ${newNoAntrian}`, shift: sourcePatient.shift || "Shift 1", klinik: targetKlinikRuang.namaKlinik, ruang: targetRuang.namaRuang, kodeKlinik: targetKlinikRuang.kodeKlinik, nomorRuang: targetRuang.nomorRuang, nomorScreen: targetRuang.nomorScreen, tipeLayanan: tipeLayanan, fastTrack: sourcePatient.fastTrack || "TIDAK", pembayaran: sourcePatient.pembayaran || "UMUM", status: "waiting", processStage: "klinik-ruang", createdAt: sourcePatient.createdAt || timestamp.toISOString(), // Gunakan createdAt dari pasien awal referencePatient: sourcePatient.noAntrian, sourcePatientNo: sourcePatient.no, isKonsultasi: true, // Flag untuk konsultasi }; allPatients.value.push(newPatient); return { success: true, message: `Antrean konsultasi berhasil dibuat: ${newNoAntrian} untuk ${targetKlinikRuang.namaKlinik} Ruang ${targetRuang.nomorRuang}`, 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' && !p.noAntrian?.startsWith('K-') && // Exclude konsultasi !p.noAntrian?.startsWith('KF-') // Exclude konsultasi Fast Track ); const queueNumber = roomQueues.length + 1; const prefix = tipeLayanan === 'Pemeriksaan Awal' ? 'PA' : 'TD'; // Tentukan prefix untuk Fast Track: F- untuk Fast Track const isFastTrack = sourcePatient.fastTrack === "YA"; const fastTrackPrefix = isFastTrack ? 'F-' : ''; const queueNumberStr = String(queueNumber).padStart(3, "0"); const newNoAntrian = `${fastTrackPrefix}${prefix}${queueNumberStr}`; 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: `${newNoAntrian} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang} - ${tipeLayanan}`, noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${newNoAntrian}`, 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: sourcePatient.createdAt || timestamp.toISOString(), // Gunakan createdAt dari pasien awal 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: waiting=1, di-loket=2 const statusPriority = { 'waiting': 1, 'di-loket': 2 }; const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99); if (priorityDiff !== 0) return priorityDiff; 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` }; } const nextPatient = patients[0]; const index = allPatients.value.findIndex(p => p.no === nextPatient.no); if (index !== -1) { allPatients.value[index] = { ...allPatients.value[index], status: "di-loket", lastCalledAt: new Date().toISOString() }; } return { success: true, message: `Memanggil ${nextPatient.noAntrian.split(" |")[0]}`, patient: allPatients.value[index] }; }; // 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 specificId = `${kodeKlinik}-${nomorRuang}`; const storageKey = `klinik-ruang-${specificId}`; const pCode = patient.noAntrian.split(" |")[0]; let message = ""; switch (action) { case "proses": currentProcessingPatient.value[storageKey] = allPatients.value[patientIndex]; message = `Memproses ${pCode}`; break; case "selesai": allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "processed" }; currentProcessingPatient.value[storageKey] = null; message = `Pasien ${pCode} selesai diproses`; break; case "terlambat": allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "terlambat" }; currentProcessingPatient.value[storageKey] = null; message = `Pasien ${pCode} ditandai terlambat`; break; case "pending": allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "pending" }; currentProcessingPatient.value[storageKey] = null; message = `Pasien ${pCode} di-pending`; break; } return { success: true, message }; }; const changeKlinik = (patient, newKlinik, adminType = 'loket', specificId = null) => { const patientIndex = allPatients.value.findIndex(p => p.no === patient.no); if (patientIndex === -1) return { success: false, message: "Pasien tidak ditemukan" }; allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], klinik: newKlinik.name, kodeKlinik: newKlinik.kode }; const key = specificId ? `${adminType}-${specificId}` : adminType; if (currentProcessingPatient.value[key]?.no === patient.no) { currentProcessingPatient.value[key] = { ...currentProcessingPatient.value[key], klinik: newKlinik.name, kodeKlinik: newKlinik.kode }; } return { success: true, message: `Klinik berhasil diubah ke ${newKlinik.name}` }; }; const getCurrentProcessing = (adminType, id = null) => { const key = id ? `${adminType}-${id}` : adminType; return computed(() => currentProcessingPatient.value[key] || null); }; const setCurrentProcessing = (patient, adminType, id = null) => { const key = id ? `${adminType}-${id}` : adminType; currentProcessingPatient.value[key] = patient; }; const generateQueueNumber = (clinic, paymentType, isEksekutif = false) => { const serviceType = isEksekutif ? 'E' : 'R'; const today = new Date().toISOString().split('T')[0]; // FIXED: Use the same key as initializeCountersFromSeed const counterKey = `queue_counter_loket_shared_${today}`; let counter = 0; if (typeof window !== 'undefined') { const stored = localStorage.getItem(counterKey); counter = stored ? parseInt(stored, 10) : 0; } counter = counter + 1; const loketIndex = Math.floor((counter - 1) / 999); if (loketIndex > 13) { counter = 1; if (typeof window !== 'undefined') localStorage.setItem(counterKey, '1'); return `${serviceType}A001`; } const loketLetter = String.fromCharCode(65 + loketIndex); const numberInLoket = ((counter - 1) % 999) + 1; const numberPart = String(numberInLoket).padStart(3, '0'); if (typeof window !== 'undefined') localStorage.setItem(counterKey, counter.toString()); return `${serviceType}${loketLetter}${numberPart}`; }; // Register patient from Anjungan (onsite registration) const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null, isFastTrack = false, fastTrackData = null, loketId = null, loket = null) => { // 1. Validasi keberadaan (prevent duplicates) // Gunakan date today untuk check-in sync const timestamp = new Date(); // Generate barcode FIRST to check for existence const barcode = generateBarcode([], allPatients); // Check EXACT barcode match prevent duplicate submission const duplicate = allPatients.value.find(p => p.barcode === barcode); if (duplicate) { return { success: false, message: "Pasien dengan barcode ini sudah terdaftar untuk hari ini.", patient: duplicate }; } const newNo = allPatients.value.length > 0 ? Math.max(...allPatients.value.map(p => p.no)) + 1 : 1; 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")}`; // 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 const queueNumber = generateQueueNumber(clinic, paymentType, isEksekutif); const finalQueueNumber = isFastTrack ? `F-${queueNumber}` : queueNumber; const noAntrian = `${finalQueueNumber} | Onsite - ${barcode}`; const status = 'menunggu'; const newPatient = { no: newNo, jamPanggil: jamPanggil, barcode: barcode, noAntrian: noAntrian, shift: shift, klinik: clinic.name || clinic, kodeKlinik: clinic.kode || null, // Essential for filtering fastTrack: isFastTrack ? "YA" : "TIDAK", pembayaran: paymentType, noRM: `RM-${barcode.slice(-6)}`, status: status, processStage: "loket", createdAt: timestamp.toISOString(), registrationType: 'onsite', visitType: visitType, visitDate: visitDate || timestamp.toISOString().substring(0, 10), namaDokter: namaDokter || null, loketId: loketId, // Optional loketId loket: loket, // Optional loket name calledByAdmin: false, penanggungJawab: (isFastTrack && fastTrackData) ? fastTrackData.penanggungJawab : null, alasanFastTrack: (isFastTrack && fastTrackData) ? fastTrackData.alasanFastTrack : null, }; // Auto-assign Loket ID if not provided, based on Clinic Mapping // fulfill requirement: "adjust based on loket id depending on creation" if (!newPatient.loketId && newPatient.kodeKlinik) { const allLokets = loketStore.lokets || []; const targetLoket = allLokets.find(l => l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik) ); if (targetLoket) { newPatient.loketId = targetLoket.id; if (!newPatient.loket) newPatient.loket = targetLoket.namaLoket; console.log(`✅ Auto-assigned Onsite Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`); } } allPatients.value.push(newPatient); incrementBarcodeCounter(); return { success: true, message: `Pendaftaran ${clinic.name || clinic} berhasil diproses.`, patient: newPatient, }; }; // Check-in patient (update status from waiting to di-loket) // IMPORTANT: Hanya menggunakan EXACT barcode match untuk menghindari false positive // Format barcode: YYMMDD + 5 digit (contoh: 26011500001) // Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive const checkInPatient = (patientIdOrBarcode) => { console.log('🔍 checkInPatient called with:', patientIdOrBarcode); console.log('📊 Total patients in store:', allPatients.value.length); // Clean input - remove whitespace and normalize const cleanInput = String(patientIdOrBarcode).trim(); // PRIORITAS: Hanya cari dengan EXACT barcode match (case-insensitive, whitespace-insensitive) // Format barcode: YYMMDD + 5 digit (contoh: 26011500001) // Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive const patientIndex = allPatients.value.findIndex(p => { // Normalize barcode untuk comparison const patientBarcode = String(p.barcode || '').trim(); // EXACT barcode match (case-insensitive, whitespace-insensitive) // Ini adalah satu-satunya cara yang aman untuk match pasien if (patientBarcode === cleanInput || patientBarcode.toLowerCase() === cleanInput.toLowerCase()) { console.log('✅ Found by exact barcode match:', patientBarcode, '===', cleanInput); return true; } return false; }); if (patientIndex === -1) { console.log('❌ Patient not found. Searched for barcode:', cleanInput); console.log('📋 Available barcodes (first 10):', allPatients.value.slice(0, 10).map(p => ({ no: p.no, barcode: p.barcode, noAntrian: p.noAntrian?.split(' |')[0] }))); return { success: false, message: `Pasien dengan barcode ${cleanInput} tidak ditemukan. Pastikan barcode benar (format: YYMMDD + 5 digit, contoh: 26011500001).` }; } // 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], }; }; // FIX: Explicit implementation of processNextQueue to prevent cross-loket leakage const processNextQueueCorrected = (adminType = 'loket', specificId = null) => { // 1. Determine target stage and key const stageMap = { 'loket': 'loket', 'klinik': 'klinik', 'penunjang': 'penunjang' }; const targetStage = stageMap[adminType]; const key = specificId ? `${adminType}-${specificId}` : adminType; console.log(`🚀 [processNextQueue] Processing for ${key} (Stage: ${targetStage})`); // 2. Find next patient (Status: 'menunggu' -> 'waiting') // Prioritaskan yang assigned ke loket ini (loketId) jika ada match let nextPatient = null; if (adminType === 'loket' && specificId) { // Cari yang spesifik untuk loket ini dulu nextPatient = allPatients.value.find(p => p.status === 'menunggu' && p.processStage === targetStage && String(p.loketId) === String(specificId) ); } // Jika tidak ada yang spesifik, cari yang umum (menunggu & loketId null/match) if (!nextPatient) { nextPatient = allPatients.value.find(p => p.status === 'menunggu' && p.processStage === targetStage && (adminType !== 'loket' || !p.loketId || String(p.loketId) === String(specificId)) ); } // Fallback: Check 'waiting' status if needed (though usually 'menunggu' is for calling) if (!nextPatient) { nextPatient = allPatients.value.find(p => p.status === 'waiting' && p.processStage === targetStage && (adminType !== 'loket' || String(p.loketId) === String(specificId)) ); } if (!nextPatient) { return { success: false, message: "Tidak ada antrean yang menunggu untuk diproses." }; } // 3. Update Patient Status (di-loket) const index = allPatients.value.findIndex(p => p.no === nextPatient.no); if (index !== -1) { const updatedPatient = { ...allPatients.value[index], status: 'di-loket', loketId: specificId || allPatients.value[index].loketId, // Ensure loketId is set lastCalledAt: new Date().toISOString() }; allPatients.value[index] = updatedPatient; // 4. Set Current Processing ISOLATED by key currentProcessingPatient.value[key] = updatedPatient; return { success: true, message: `Memproses ${nextPatient.noAntrian.split(" |")[0]}`, patient: updatedPatient }; } return { success: false, message: "Gagal memproses antrean." }; }; return { // State allPatients, resetPatients, ensureInitialData, quotaUsed, currentProcessingPatient, kliniks, penunjangs, // Computed totalPasien, // Actions callNext, callMultiplePatients, processPatient, callProcessingPatient, createAntreanKlinik, createAntreanPenunjang, createAntreanKlinikRuang, scanAndCreateAntreanKlinikRuang, getPatientsByKlinikRuang, callNextKlinikRuang, processPatientKlinikRuang, changeKlinik, pindahKlinikRuang, konsultasiKlinikRuang, processNextQueue: processNextQueueCorrected, getPatientsByStage, getTotalPasienByStage, getCurrentProcessing, setCurrentProcessing, registerPatientFromAnjungan, checkInPatient, generateQueueNumber, generateBarcode, incrementBarcodeCounter, syncCountersWithState, }; }, { 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; }, }, });