@@ -502,6 +519,8 @@ export const useThermalPrint = () => {
noAntrian: patient.noAntrian || '',
barcode: patient.barcode || '',
klinik: patient.klinik || '',
+ ruang: patient.ruang || undefined,
+ nomorRuang: patient.nomorRuang || undefined,
shift: patient.shift || 'Shift 1',
pembayaran: patient.pembayaran || '',
tanggal: tanggalKunjungan,
diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue
index 9f485cb..a1b1872 100644
--- a/pages/AdminKlinikRuang/[kodeKlinik].vue
+++ b/pages/AdminKlinikRuang/[kodeKlinik].vue
@@ -9,31 +9,6 @@
theme="warning"
/>
-
-
-
- GENERATE TIKET
-
-
-
-
-
-
- Tekan Enter. (Apabila barcode depan nomor ada huruf lain, Ex: J200730100005 "Hiraukan huruf 'j' nya")
-
-
-
-
-
-
SEDANG DIPROSES
- {{ getCurrentProcessingForRoom(ruang)?.noAntrian.split(" |")[0] }}
+ {{ getCurrentProcessingForRoom(ruang)?.noAntrian?.split(" |")[0] || '-' }}
-
-
+
+
- DAFTAR PASIEN PERLU GENERATE TIKET
+ DAFTAR PASIEN
- {{ getPendingPatientsForRoom(ruang).length }}
+ {{ getAllPatientsForRoom(ruang).length }}
-
+
-
- {{ patient.noAntrian.split(" |")[0] }}
+
+ {{ patient.noAntrian?.split(" |")[0] || patient.barcode || '-' }}
-
+
+
+
+ mdi-check-circle
+ Pemeriksaan Awal
+
+
+
+ mdi-check-circle
+ Tindakan
+
+
+
+ mdi-pause-circle
+ Pending
+
+
+
Pindah Ruang
mdi-play
Proses
-
- mdi-ticket
- Generate
-
-
-
-
-
-
-
-
-
Pasien: {{ scannedPatient?.noAntrian?.split(" |")[0] }}
-
{{ scannedPatient?.barcode }}
-
-
-
-
-
-
-
-
- Batal
-
- Buat Antrean
-
-
-
-
-
@@ -403,14 +356,9 @@ const currentDate = ref(
})
);
-const barcodeInput = ref('');
const snackbar = ref(false);
const snackbarText = ref('');
const snackbarColor = ref('success');
-const showRuangDialog = ref(false);
-const scannedPatient = ref(null);
-const selectedRuang = ref(null);
-const selectedTipeLayanan = ref('Pemeriksaan Awal');
const showDetailDialog = ref(false);
const selectedPatientDetail = ref(null);
const pendingPage = ref({});
@@ -424,105 +372,29 @@ const showSnackbar = (message, color = 'success') => {
snackbar.value = true;
};
-const handleScanBarcode = () => {
- if (!barcodeInput.value.trim()) {
- showSnackbar('Masukkan barcode/nomor antrian', 'error');
- return;
- }
-
- // Find patient from queueStore
- const cleanInput = String(barcodeInput.value).trim().toUpperCase();
- const numericInput = cleanInput.replace(/^[A-Z]+/, '');
-
- const patient = queueStore.allPatients.find(p => {
- if (p.barcode === cleanInput || p.barcode === numericInput) return true;
- const noAntrianUpper = (p.noAntrian || '').toUpperCase();
- if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(numericInput)) return true;
- return false;
- });
-
- if (!patient) {
- showSnackbar('Pasien tidak ditemukan', 'error');
- barcodeInput.value = '';
- return;
- }
-
- // Check if patient already has antrean klinik ruang
- const existingAntrean = queueStore.allPatients.find(p =>
- p.referencePatient === patient.noAntrian &&
- p.kodeKlinik === klinikData.value?.kodeKlinik &&
- p.processStage === 'klinik-ruang'
- );
-
- if (existingAntrean) {
- showSnackbar(`Pasien sudah memiliki antrean di ${existingAntrean.ruang}`, 'warning');
- barcodeInput.value = '';
- return;
- }
-
- scannedPatient.value = patient;
- selectedRuang.value = null;
- selectedTipeLayanan.value = 'Pemeriksaan Awal';
- showRuangDialog.value = true;
- barcodeInput.value = '';
-};
-
-const confirmCreateAntrean = () => {
- if (!selectedRuang.value || !selectedTipeLayanan.value || !scannedPatient.value) {
- showSnackbar('Pilih ruang dan tipe layanan', 'error');
- return;
- }
-
- const klinikRuang = {
- kodeKlinik: klinikData.value.kodeKlinik,
- namaKlinik: klinikData.value.namaKlinik
- };
-
- const result = queueStore.scanAndCreateAntreanKlinikRuang(
- scannedPatient.value.barcode,
- klinikRuang,
- selectedRuang.value,
- selectedTipeLayanan.value
- );
-
- if (result.success && result.patient) {
- // Auto proses pasien yang baru digenerate tiket
- const processResult = queueStore.processPatientKlinikRuang(
- result.patient,
- 'proses',
- klinikData.value.kodeKlinik,
- selectedRuang.value.nomorRuang
- );
- showSnackbar(processResult.message, processResult.success ? 'success' : 'error');
- } else {
- showSnackbar(result.message, result.success ? 'success' : 'error');
- }
-
- showRuangDialog.value = false;
- scannedPatient.value = null;
-};
-
-// Get pending patients (belum generate tiket - dari AdminLoket/AdminKlinik)
-// Juga include pasien yang sudah digenerate tiket tapi status pending
-const getPendingPatientsForRoom = (ruang) => {
+// Get all patients for room (hanya pasien dari processStage 'klinik-ruang')
+const getAllPatientsForRoom = (ruang) => {
return queueStore.allPatients
.filter(p =>
p.kodeKlinik === klinikData.value?.kodeKlinik &&
p.nomorRuang === ruang.nomorRuang &&
p.ruang === ruang.namaRuang &&
- (
- // Belum generate tiket (belum punya tipeLayanan)
- (!p.tipeLayanan && (p.processStage === 'klinik' || p.processStage === 'klinik-ruang') &&
- (p.status === 'waiting' || p.status === 'di-loket' || p.status === 'terlambat')) ||
- // Sudah generate tiket tapi status pending
- (p.tipeLayanan && p.processStage === 'klinik-ruang' && p.status === 'pending')
- )
+ p.processStage === 'klinik-ruang' &&
+ // Exclude pasien yang sedang diproses (current processing)
+ p.no !== getCurrentProcessingForRoom(ruang)?.no &&
+ // Include semua pasien dengan status yang relevan
+ (p.status === 'waiting' || p.status === 'di-loket' || p.status === 'terlambat' || p.status === 'pending')
)
.sort((a, b) => {
- // Prioritaskan yang belum digenerate (pending status di akhir)
- if (!a.tipeLayanan && b.tipeLayanan) return -1;
- if (a.tipeLayanan && !b.tipeLayanan) return 1;
+ // Prioritaskan yang sudah digenerate/diproses & pending di atas
+ const aHasTiket = !!a.tipeLayanan;
+ const bHasTiket = !!b.tipeLayanan;
+ // Jika satu sudah punya tiket dan yang lain belum, yang sudah tiket di atas
+ if (aHasTiket && !bHasTiket) return -1;
+ if (!aHasTiket && bHasTiket) return 1;
+
+ // Jika keduanya sudah punya tiket atau keduanya belum, sort by status
const statusPriority = {
'di-loket': 1,
'waiting': 2,
@@ -532,12 +404,18 @@ const getPendingPatientsForRoom = (ruang) => {
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
if (priorityDiff !== 0) return priorityDiff;
+ // Sort by time
const timeA = a.jamPanggil?.split(':').map(Number) || [0, 0];
const timeB = b.jamPanggil?.split(':').map(Number) || [0, 0];
return timeA[0] * 60 + timeA[1] - (timeB[0] * 60 + timeB[1]);
});
};
+// Get pending patients (untuk backward compatibility jika masih digunakan)
+const getPendingPatientsForRoom = (ruang) => {
+ return getAllPatientsForRoom(ruang).filter(p => !p.tipeLayanan || p.status === 'pending');
+};
+
const getCurrentProcessingForRoom = (ruang) => {
@@ -610,7 +488,7 @@ const handleGenerateTicket = (patient, currentRuang) => {
}
} else {
// Pasien belum punya ruang, isi field dan trigger scan (akan muncul dialog)
- barcodeInput.value = patient.noAntrian.split(" |")[0] || patient.barcode;
+ barcodeInput.value = patient.noAntrian?.split(" |")[0] || patient.barcode || '';
handleScanBarcode();
}
};
@@ -638,16 +516,17 @@ const confirmPindahRuang = () => {
// Jika pasien sudah punya antrean klinik ruang (sudah digenerate tiket), update antreannya juga
if (patient.processStage === 'klinik-ruang' && patient.tipeLayanan) {
- // Update antrean yang sudah ada
- queueStore.allPatients[patientIndex] = {
- ...queueStore.allPatients[patientIndex],
- ruang: selectedRuangBaru.value.namaRuang,
- nomorRuang: selectedRuangBaru.value.nomorRuang,
- nomorScreen: selectedRuangBaru.value.nomorScreen,
- // Update noAntrian untuk reflect ruang baru
- noAntrian: `${patient.noAntrian.split(" |")[0]} | ${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} - ${patient.tipeLayanan}`,
- noAntrianRuang: `${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} | ${patient.noAntrian.split(" |")[0]}`
- };
+ // Update antrean yang sudah ada
+ const baseNoAntrian = patient.noAntrian?.split(" |")[0] || patient.barcode || '';
+ queueStore.allPatients[patientIndex] = {
+ ...queueStore.allPatients[patientIndex],
+ ruang: selectedRuangBaru.value.namaRuang,
+ nomorRuang: selectedRuangBaru.value.nomorRuang,
+ nomorScreen: selectedRuangBaru.value.nomorScreen,
+ // Update noAntrian untuk reflect ruang baru
+ noAntrian: `${baseNoAntrian} | ${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} - ${patient.tipeLayanan}`,
+ noAntrianRuang: `${klinikData.value.namaKlinik} - ${selectedRuangBaru.value.namaRuang} | ${baseNoAntrian}`
+ };
// Clear current processing dari ruang lama jika ada
const oldRuang = ruangList.value.find(r => r.nomorRuang === patient.nomorRuang);
@@ -668,7 +547,7 @@ const confirmPindahRuang = () => {
}
showSnackbar(
- `Pasien ${selectedPatientForPindah.value.noAntrian.split(" |")[0]} berhasil dipindah ke ${selectedRuangBaru.value.namaRuang}`,
+ `Pasien ${selectedPatientForPindah.value.noAntrian?.split(" |")[0] || selectedPatientForPindah.value.barcode || '-'} berhasil dipindah ke ${selectedRuangBaru.value.namaRuang}`,
'success'
);
@@ -679,27 +558,43 @@ const confirmPindahRuang = () => {
const handleCallPatientByTipe = (ruang, tipeLayanan) => {
const current = getCurrentProcessingForRoom(ruang);
- if (!current) return;
+ if (!current || !current.no) {
+ showSnackbar('Tidak ada pasien yang sedang diproses', 'error');
+ return;
+ }
// Update tipeLayanan pasien untuk tracking panggilan terakhir
const patientIndex = queueStore.allPatients.findIndex(p => p.no === current.no);
- if (patientIndex !== -1) {
- // Update status to di-loket untuk ditampilkan di anjungan
- // Simpan tipeLayanan yang dipanggil untuk display di anjungan
- queueStore.allPatients[patientIndex] = {
- ...queueStore.allPatients[patientIndex],
- status: 'di-loket',
- tipeLayanan: tipeLayanan, // Update tipeLayanan untuk display di anjungan
- lastCalledAt: new Date().toISOString(),
- lastCalledTipeLayanan: tipeLayanan // Track tipe layanan terakhir yang dipanggil
- };
-
- // Update current processing (tetap 1 pasien, tidak dipisah per tipe)
- const key = `klinik-ruang-${klinikData.value.kodeKlinik}-${ruang.nomorRuang}`;
- queueStore.currentProcessingPatient[key] = queueStore.allPatients[patientIndex];
+ if (patientIndex === -1) {
+ showSnackbar('Pasien tidak ditemukan', 'error');
+ return;
}
- showSnackbar(`Memanggil pasien ${current.noAntrian.split(" |")[0]} untuk ${tipeLayanan}`, 'success');
+ // Update tracking panggilan berdasarkan tipeLayanan
+ const updateData = {
+ ...queueStore.allPatients[patientIndex],
+ status: 'di-loket',
+ lastCalledAt: new Date().toISOString(),
+ lastCalledTipeLayanan: tipeLayanan
+ };
+
+ // Set penanda panggilan sesuai tipeLayanan
+ if (tipeLayanan === 'Pemeriksaan Awal') {
+ updateData.calledPemeriksaanAwal = true;
+ } else if (tipeLayanan === 'Tindakan') {
+ updateData.calledTindakan = true;
+ }
+
+ queueStore.allPatients[patientIndex] = updateData;
+
+ // Update current processing (tetap 1 pasien, tidak dipisah per tipe)
+ const key = `klinik-ruang-${klinikData.value.kodeKlinik}-${ruang.nomorRuang}`;
+ queueStore.currentProcessingPatient[key] = queueStore.allPatients[patientIndex];
+
+ const patientCode = queueStore.allPatients[patientIndex].noAntrian?.split(" |")[0] ||
+ queueStore.allPatients[patientIndex].barcode ||
+ '-';
+ showSnackbar(`Memanggil pasien ${patientCode} untuk ${tipeLayanan}`, 'success');
};
const showPatientDetail = (patient) => {
@@ -758,6 +653,25 @@ const handlePatientAction = (ruang, action) => {
showSnackbar(result.message, result.success ? 'success' : 'error');
};
+// Handle proses pasien (untuk pasien yang belum diproses)
+const handleProcessPatient = (ruang, patient) => {
+ const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no);
+ if (patientIndex === -1) {
+ showSnackbar('Pasien tidak ditemukan', 'error');
+ return;
+ }
+
+ // Set sebagai current processing
+ const result = queueStore.processPatientKlinikRuang(
+ queueStore.allPatients[patientIndex],
+ 'proses',
+ klinikData.value.kodeKlinik,
+ ruang.nomorRuang
+ );
+
+ showSnackbar(result.message, result.success ? 'success' : 'error');
+};
+
const handleProcessPendingPatient = (ruang, patient) => {
// Proses kembali pasien yang dipending
const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no);
@@ -766,10 +680,10 @@ const handleProcessPendingPatient = (ruang, patient) => {
return;
}
- // Update status pasien dari pending menjadi di-loket
+ // Update status pasien dari pending menjadi waiting
queueStore.allPatients[patientIndex] = {
...queueStore.allPatients[patientIndex],
- status: 'di-loket'
+ status: 'waiting'
};
// Set sebagai current processing
@@ -783,12 +697,23 @@ const handleProcessPendingPatient = (ruang, patient) => {
showSnackbar(result.message, result.success ? 'success' : 'error');
};
-const paginatedPendingPatients = (ruang) => {
- const allPending = getPendingPatientsForRoom(ruang);
+const paginatedAllPatients = (ruang) => {
+ const allPatients = getAllPatientsForRoom(ruang);
const page = pendingPage.value[ruang.nomorRuang] || 1;
const startIndex = (page - 1) * 10;
const endIndex = startIndex + 10;
- return allPending.slice(startIndex, endIndex);
+ return allPatients.slice(startIndex, endIndex);
+};
+
+// Helper untuk menentukan class card berdasarkan status pasien
+const getPatientCardClass = (patient) => {
+ const baseClass = 'patient-queue-item';
+ // Pasien yang sudah digenerate/diproses & pending: warna berbeda
+ if (patient.tipeLayanan || patient.status === 'pending') {
+ return `${baseClass} patient-queue-item-processed`;
+ }
+ // Pasien yang perlu digenerate: warna default
+ return `${baseClass} patient-queue-item-pending`;
};
@@ -981,7 +906,7 @@ onMounted(() => {
color: var(--color-neutral-700);
}
-.pending-section {
+.patient-list-section {
background: var(--color-warning-50);
border: 1px solid var(--color-warning-200);
border-radius: 8px;
@@ -989,7 +914,7 @@ onMounted(() => {
margin-bottom: 16px;
}
-.pending-queue-list {
+.patient-queue-list {
display: flex;
flex-direction: column;
gap: 6px;
@@ -997,35 +922,59 @@ onMounted(() => {
overflow-y: auto;
}
-.pending-queue-item {
+.patient-queue-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
- background: var(--color-neutral-100);
- border: 1px solid var(--color-neutral-300);
border-radius: 6px;
transition: all 0.2s ease;
+ gap: 8px;
}
-.pending-queue-item:hover {
+/* Pasien yang sudah digenerate/diproses & pending */
+.patient-queue-item-processed {
+ background: var(--color-primary-50);
+ border: 1px solid var(--color-primary-200);
+}
+
+.patient-queue-item-processed:hover {
+ background: var(--color-primary-100);
+ border-color: var(--color-primary-400);
+}
+
+/* Pasien yang perlu digenerate */
+.patient-queue-item-pending {
+ background: var(--color-neutral-100);
+ border: 1px solid var(--color-neutral-300);
+}
+
+.patient-queue-item-pending:hover {
background: var(--color-warning-100);
border-color: var(--color-warning-400);
}
-.pending-queue-number {
+.patient-queue-number {
font-size: 14px;
font-weight: 700;
color: var(--color-neutral-900);
+ min-width: 80px;
}
-.pending-queue-actions {
+.patient-queue-info {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ flex: 1;
+}
+
+.patient-queue-actions {
display: flex;
gap: 8px;
align-items: center;
}
-.pending-queue-actions .v-btn {
+.patient-queue-actions .v-btn {
text-transform: none;
font-weight: 600;
}
diff --git a/pages/AdminKlinikRuang/index.vue b/pages/AdminKlinikRuang/index.vue
index db5bee7..9ad9706 100644
--- a/pages/AdminKlinikRuang/index.vue
+++ b/pages/AdminKlinikRuang/index.vue
@@ -58,7 +58,7 @@ const klinikRuangList = computed(() => {
});
const navigateToKlinik = (kodeKlinik) => {
- router.push(`/admin-klinik-ruang/${kodeKlinik}`);
+ router.push(`/adminklinikruang/${kodeKlinik}`);
};
diff --git a/pages/AdminLoket.vue b/pages/AdminLoket.vue
index e05abd4..7ca2cf5 100644
--- a/pages/AdminLoket.vue
+++ b/pages/AdminLoket.vue
@@ -255,9 +255,11 @@ import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
import PatientDataTable from "@/components/features/queue/TabelPatientData.vue";
import SelectionDialog from "@/components/common/SelectionDialog.vue";
import AppSnackbar from "@/components/common/AppSnackbar.vue";
+import { useThermalPrint } from "@/composables/useThermalPrint";
const masterStore = useMasterStore();
const queueStore = useQueueStore();
+const { printTicketFromPatient } = useThermalPrint();
const {
snackbar,
@@ -446,7 +448,7 @@ const closeKlinikRuangDialog = () => {
klinikRuangSearch.value = "";
};
-const buatAntreanKlinikRuang = (klinikRuang, ruang) => {
+const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
// Pastikan currentProcessingPatient valid, jika tidak, coba dapatkan dari store
let patient = currentProcessingPatient.value;
if (!patient) {
@@ -491,6 +493,20 @@ const buatAntreanKlinikRuang = (klinikRuang, ruang) => {
"loket"
);
+ if (result.success && result.patient) {
+ // Print ticket dengan nomor antrian baru
+ try {
+ await printTicketFromPatient(result.patient);
+ } catch (error) {
+ console.error('Error printing ticket:', error);
+ snackbarText.value = `${result.message}. Gagal print tiket.`;
+ snackbarColor.value = "warning";
+ snackbar.value = true;
+ closeKlinikRuangDialog();
+ return;
+ }
+ }
+
snackbarText.value = result.message;
snackbarColor.value = result.success ? "success" : "error";
snackbar.value = true;
diff --git a/stores/queueStore.js b/stores/queueStore.js
index 03d24ee..b24869c 100644
--- a/stores/queueStore.js
+++ b/stores/queueStore.js
@@ -570,32 +570,61 @@ export const useQueueStore = defineStore('queue', () => {
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
+ // 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: `KR${String(newNo).padStart(4, "0")} | ${klinikRuang.namaKlinik} - Ruang ${ruang.nomorRuang} - ${barcode}`,
- shift: "Shift 1",
+ 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: "TIDAK",
+ fastTrack: patient ? (patient.fastTrack || "TIDAK") : "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
status: "waiting",
- processStage: "klinik",
+ 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 dan akan ditampilkan di layar antrian`,
+ message: `Antrean ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang} berhasil dibuat: ${newNoAntrian}`,
patient: newPatient,
};
};