perubahan tiket dan alur antrean

This commit is contained in:
Fanrouver
2026-01-12 11:52:45 +07:00
parent 6e7160c935
commit 5c142273aa
13 changed files with 997 additions and 393 deletions

No files matched your search

+3 -3
View File
@@ -35,15 +35,15 @@ KEYCLOAK_ISSUER="https://auth.rssa.top/realms/sandbox"
# AUTH_ORIGIN="http://localhost:3000"
# AUTH_ORIGIN="http://10.10.150.175:3001"
# AUTH_ORIGIN="http://0.0.0.0:3000"
# AUTH_ORIGIN="https://antrean.dev.rssa.id/
# AUTH_ORIGIN="https://antrean.rssa.id/
# AUTH_ORIGIN="https://antrean.dev.rssa.id"
# AUTH_ORIGIN="https://antrean.rssa.id"
#nuxt config.ts dev server host and port
# HOST="localhost"
# PORT=3000
# HOST="http://10.10.150.175:3000"
# PORT=3000
# HOST="http://0.0.0.0:3000"
# HOST="0.0.0.0"
# PORT=3000
# HOST="https://antrean.dev.rssa.id/"
# PORT=3000
@@ -22,7 +22,7 @@
:rules="[v => !!v || 'Nama loket harus diisi']"
hide-details="auto"
class="mb-3 input-field"
placeholder="Loket 1"
placeholder="Loket A"
/>
<v-text-field
+67 -35
View File
@@ -43,31 +43,36 @@ export const useThermalPrint = () => {
/**
* Format date untuk tampilan
* Format: senin, 12 Jan 2026
*/
const formatDate = (dateString: string): string => {
try {
const date = dateString ? new Date(dateString) : new Date();
let date = dateString ? new Date(dateString) : new Date();
if (isNaN(date.getTime())) {
return new Date().toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
date = new Date();
}
return date.toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Array nama hari dalam bahasa Indonesia (lowercase)
const days = ['minggu', 'senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu'];
// Array nama bulan singkat (title case)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const dayName = days[date.getDay()];
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear(); // Tahun 4 digit
return `${dayName}, ${day} ${month} ${year}`;
} catch (error) {
return new Date().toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Fallback jika error
const date = new Date();
const days = ['minggu', 'senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu'];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const dayName = days[date.getDay()];
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
return `${dayName}, ${day} ${month} ${year}`;
}
};
@@ -203,7 +208,8 @@ export const useThermalPrint = () => {
}
.qr-code-section {
margin: 3mm 0;
margin-top: -1.5mm;
margin-bottom: 1mm;
text-align: center;
}
@@ -211,8 +217,8 @@ export const useThermalPrint = () => {
width: 50mm;
height: 50mm;
margin: 0 auto;
border: 1px solid #000;
padding: 2mm;
border: none;
padding: 0.5mm;
background: white;
}
@@ -225,7 +231,7 @@ export const useThermalPrint = () => {
.barcode-number {
font-size: 10px;
font-weight: bold;
margin-top: 1mm;
margin-top: -1mm;
word-break: break-all;
}
@@ -243,30 +249,43 @@ export const useThermalPrint = () => {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 4mm;
padding: 0;
}
.info-label {
.info-label-text {
font-weight: bold;
font-size: 10px;
flex: 0 0 38%;
text-align: left;
text-transform: uppercase;
word-break: keep-all;
}
.info-colon {
font-weight: bold;
font-size: 10px;
margin-left: auto;
padding-right: 2mm;
}
.info-label-wrapper {
display: flex;
width: 22mm;
flex-shrink: 0;
align-items: flex-start;
gap: 1mm;
}
.info-value {
font-size: 10px;
font-weight: bold;
flex: 1;
text-align: right;
word-break: break-word;
}
.no-antrian {
font-size: 18px;
font-size: 28px;
font-weight: bold;
margin: 2mm 0;
margin: 0.5mm 0;
letter-spacing: 2px;
text-transform: uppercase;
}
@@ -274,8 +293,11 @@ export const useThermalPrint = () => {
.klinik-name {
font-size: 14px;
font-weight: bold;
margin: 2mm 0;
margin-top: 0.5mm;
margin-bottom: 0;
padding-bottom: 0;
text-transform: uppercase;
overflow: visible;
}
.footer {
@@ -373,28 +395,38 @@ export const useThermalPrint = () => {
<div class="info-section">
<div class="info-row">
<span class="info-label">Tanggal:</span>
<div class="info-label-wrapper">
<span class="info-label-text">Tanggal</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.tanggal}</span>
</div>
<div class="info-row">
<span class="info-label">Waktu:</span>
<div class="info-label-wrapper">
<span class="info-label-text">Waktu</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.waktu}</span>
</div>
<div class="info-row">
<span class="info-label">Shift:</span>
<div class="info-label-wrapper">
<span class="info-label-text">Shift</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.shift}</span>
</div>
<div class="info-row">
<span class="info-label">Pembayaran:</span>
<div class="info-label-wrapper">
<span class="info-label-text">Pembayaran</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.pembayaran}</span>
</div>
${data.namaDokter ? `
<div class="info-row">
<span class="info-label">Dokter:</span>
<div class="info-label-wrapper">
<span class="info-label-text">Dokter</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.namaDokter}</span>
</div>
` : ''}
+133 -62
View File
@@ -487,13 +487,13 @@
Cetak Nomor Antrian
</v-card-title>
<v-divider />
<v-card-text class="pa-6" v-if="lastRegisteredPatient">
<div class="dialog-content">
<v-card-text class="pa-6">
<div v-if="lastRegisteredPatient" class="dialog-content">
<div class="text-center mb-4">
<v-icon size="64" color="primary-600" class="mb-3">mdi-ticket-confirmation</v-icon>
<h3 class="dialog-clinic-name mb-2">Tiket Berhasil Dibuat</h3>
<p class="text-body-2 text-grey mb-4">
Nomor antrean Anda: <strong>{{ lastRegisteredPatient.noAntrian.split(" |")[0] }}</strong>
Nomor antrean Anda: <strong>{{ lastRegisteredPatient.noAntrian?.split(" |")[0] || lastRegisteredPatient.noAntrian || 'N/A' }}</strong>
</p>
</div>
@@ -502,19 +502,19 @@
<div class="dialog-info">
<div class="info-row mb-3">
<span class="info-label">Klinik:</span>
<span class="info-value">{{ lastRegisteredPatient.klinik }}</span>
<span class="info-value">{{ lastRegisteredPatient.klinik || 'N/A' }}</span>
</div>
<div class="info-row mb-3">
<span class="info-label">Shift:</span>
<span class="info-value">{{ lastRegisteredPatient.shift }}</span>
<span class="info-value">{{ lastRegisteredPatient.shift || 'N/A' }}</span>
</div>
<div class="info-row mb-3">
<span class="info-label">Pembayaran:</span>
<span class="info-value">{{ lastRegisteredPatient.pembayaran }}</span>
<span class="info-value">{{ lastRegisteredPatient.pembayaran || 'N/A' }}</span>
</div>
<div class="info-row">
<span class="info-label">Barcode:</span>
<span class="info-value">{{ lastRegisteredPatient.barcode }}</span>
<span class="info-value">{{ lastRegisteredPatient.barcode || 'N/A' }}</span>
</div>
</div>
@@ -532,6 +532,10 @@
</div>
</v-alert>
</div>
<div v-else class="text-center py-6">
<v-progress-circular indeterminate color="primary-600" class="mb-3"></v-progress-circular>
<p class="text-body-2 text-grey">Memuat data tiket...</p>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
@@ -580,7 +584,7 @@
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from "vue";
import { ref, computed, onMounted, nextTick, watch } from "vue";
import { useRoute } from "#app";
import { useAnjunganStore } from "@/stores/anjunganStore";
import { useClinicStore } from "@/stores/clinicStore";
@@ -597,6 +601,16 @@ const clinicStore = useClinicStore();
const queueStore = useQueueStore();
const { printTicketFromPatient, isPrinting } = useThermalPrint();
// Validasi bahwa queueStore ter-load dengan benar
onMounted(() => {
console.log('🔍 QueueStore initialized:', queueStore);
console.log('🔍 registerPatientFromAnjungan available:', typeof queueStore.registerPatientFromAnjungan);
if (typeof queueStore.registerPatientFromAnjungan !== 'function') {
console.error('❌ registerPatientFromAnjungan is not a function!');
console.error('Available methods:', Object.keys(queueStore));
}
});
const anjunganId = computed(() => {
const raw = route.params.id;
const val = Array.isArray(raw) ? raw[0] : raw;
@@ -1031,33 +1045,57 @@ const confirmDoctorSelection = () => {
};
const registerPatient = async (visitType, paymentType, namaDokter) => {
// Register patient to queueStore
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
visitType,
null,
'Shift 1',
namaDokter
);
if (result.success) {
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
const message = `Pendaftaran ${selectedClinic.value.name} untuk kunjungan HARI INI dengan pembayaran ${paymentType}${doctorInfo} berhasil diproses. Nomor antrean: ${result.patient.noAntrian.split(" |")[0]}`;
showSnackbar(message, "success");
try {
// Validasi bahwa fungsi registerPatientFromAnjungan ada
if (!queueStore) {
console.error('❌ queueStore is not initialized');
showSnackbar('Error', 'Store tidak ter-initialize. Silakan refresh halaman.', 'error');
return;
}
// Simpan data pasien untuk print
lastRegisteredPatient.value = result.patient;
if (typeof queueStore.registerPatientFromAnjungan !== 'function') {
console.error('❌ queueStore.registerPatientFromAnjungan is not a function');
console.error('queueStore:', queueStore);
console.error('Available methods:', Object.keys(queueStore || {}));
showSnackbar('Error', 'Fungsi registrasi tidak tersedia. Silakan refresh halaman.', 'error');
return;
}
// Tampilkan dialog print
showPrintDialog.value = true;
} else {
showSnackbar(result.message || "Gagal melakukan pendaftaran", "error");
// Register patient to queueStore
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
visitType,
null,
'Shift 1',
namaDokter
);
if (result && result.success && result.patient) {
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
const message = `Pendaftaran ${selectedClinic.value.name} untuk kunjungan HARI INI dengan pembayaran ${paymentType}${doctorInfo} berhasil diproses. Nomor antrean: ${result.patient.noAntrian?.split(" |")[0] || result.patient.noAntrian || 'N/A'}`;
showSnackbar(message, "success");
// Simpan data pasien untuk print
lastRegisteredPatient.value = result.patient;
console.log('📋 Patient data saved for print:', lastRegisteredPatient.value);
// Tampilkan dialog print - gunakan nextTick untuk memastikan reactive update
await nextTick();
showPrintDialog.value = true;
console.log('✅ Print dialog opened, showPrintDialog:', showPrintDialog.value, 'patient:', lastRegisteredPatient.value);
} else {
console.error('❌ Registration failed or patient data missing:', result);
showSnackbar(result?.message || "Gagal melakukan pendaftaran", "error");
}
} catch (error) {
console.error('❌ Error in registerPatient:', error);
showSnackbar('Terjadi kesalahan saat melakukan pendaftaran. Silakan coba lagi.', 'error');
} finally {
selectedClinic.value = null;
selectedDoctor.value = null;
pendingPaymentType.value = null;
}
selectedClinic.value = null;
selectedDoctor.value = null;
pendingPaymentType.value = null;
};
const submitBooking = async () => {
@@ -1078,36 +1116,60 @@ const submitBooking = async () => {
showBookingFormDialog.value = false;
// Register patient to queueStore for future visit
const paymentType = bookingForm.value.payment === 'BPJS' ? 'BPJS' : 'UMUM';
const namaDokter = isEksekutif.value ? bookingForm.value.doctor : null;
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
'JADWAL_LAIN',
bookingForm.value.date,
bookingForm.value.shift,
namaDokter
);
if (result.success) {
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
const message = `Jadwal kunjungan ${selectedClinic.value.name} pada ${bookingForm.value.date} - ${bookingForm.value.shift} (${paymentType})${doctorInfo} berhasil disimpan. Nomor antrean: ${result.patient.noAntrian.split(" |")[0]}`;
showSnackbar(message, "success");
try {
// Register patient to queueStore for future visit
const paymentType = bookingForm.value.payment === 'BPJS' ? 'BPJS' : 'UMUM';
const namaDokter = isEksekutif.value ? bookingForm.value.doctor : null;
// Simpan data pasien untuk print
lastRegisteredPatient.value = result.patient;
// Validasi bahwa fungsi registerPatientFromAnjungan ada
if (!queueStore) {
console.error('❌ queueStore is not initialized');
showSnackbar('Error', 'Store tidak ter-initialize. Silakan refresh halaman.', 'error');
return;
}
// Tampilkan dialog print
showPrintDialog.value = true;
} else {
showSnackbar(result.message || "Gagal menyimpan jadwal", "error");
if (typeof queueStore.registerPatientFromAnjungan !== 'function') {
console.error('❌ queueStore.registerPatientFromAnjungan is not a function');
console.error('queueStore:', queueStore);
console.error('Available methods:', Object.keys(queueStore || {}));
showSnackbar('Error', 'Fungsi registrasi tidak tersedia. Silakan refresh halaman.', 'error');
return;
}
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
paymentType,
'JADWAL_LAIN',
bookingForm.value.date,
bookingForm.value.shift,
namaDokter
);
if (result && result.success && result.patient) {
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
const message = `Jadwal kunjungan ${selectedClinic.value.name} pada ${bookingForm.value.date} - ${bookingForm.value.shift} (${paymentType})${doctorInfo} berhasil disimpan. Nomor antrean: ${result.patient.noAntrian?.split(" |")[0] || result.patient.noAntrian || 'N/A'}`;
showSnackbar(message, "success");
// Simpan data pasien untuk print
lastRegisteredPatient.value = result.patient;
console.log('📋 Patient data saved for print (booking):', lastRegisteredPatient.value);
// Tampilkan dialog print - gunakan nextTick untuk memastikan reactive update
await nextTick();
showPrintDialog.value = true;
console.log('✅ Print dialog opened (booking), showPrintDialog:', showPrintDialog.value, 'patient:', lastRegisteredPatient.value);
} else {
console.error('❌ Booking failed or patient data missing:', result);
showSnackbar(result?.message || "Gagal menyimpan jadwal", "error");
}
} catch (error) {
console.error('❌ Error in submitBooking:', error);
showSnackbar('Terjadi kesalahan saat menyimpan jadwal. Silakan coba lagi.', 'error');
} finally {
selectedClinic.value = null;
selectedDoctor.value = null;
bookingForm.value.doctor = null;
}
selectedClinic.value = null;
selectedDoctor.value = null;
bookingForm.value.doctor = null;
};
const handlePrintTicket = async () => {
@@ -1133,9 +1195,18 @@ const backToList = () => {
navigateTo("/anjungan/anjungan");
};
const navigateToCheckIn = () => {
navigateTo("/check-in-pasien/check-in");
};
// Watch showPrintDialog untuk debugging
watch(showPrintDialog, (newVal) => {
console.log('🔔 showPrintDialog changed:', newVal);
if (newVal) {
console.log('📋 lastRegisteredPatient:', lastRegisteredPatient.value);
}
});
// Watch lastRegisteredPatient untuk debugging
watch(lastRegisteredPatient, (newVal) => {
console.log('🔔 lastRegisteredPatient changed:', newVal);
}, { deep: true });
</script>
<style scoped lang="scss">
+1 -1
View File
@@ -25,7 +25,7 @@
<div class="queue-grid-container">
<!-- Loket Header -->
<div class="loket-header">
<h2 class="loket-title">{{ loketData?.namaLoket || 'LOKET 1' }}</h2>
<h2 class="loket-title">{{ loketData?.namaLoket || 'LOKET A' }}</h2>
</div>
<!-- Patient Cards Grid (like AdminLoket) -->
+8 -1
View File
@@ -22,7 +22,7 @@
<v-icon size="32" color="primary">mdi-counter</v-icon>
<div class="loket-info">
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
<p class="loket-details">No. {{ loket.no }}</p>
<p class="loket-details">No. {{ numberToLetter(loket.no) }}</p>
</div>
</div>
@@ -100,6 +100,13 @@ definePageMeta({
const masterStore = useMasterStore();
const route = useRoute();
// Helper function: Convert nomor loket ke huruf (1 -> A, 2 -> B, dst)
const numberToLetter = (num) => {
if (num < 1) return 'A';
const charCode = 64 + num;
return String.fromCharCode(Math.min(charCode, 90));
};
// Get all lokets
// Untuk saat ini, hanya menampilkan loket 1
// Nanti setiap loket akan menampilkan data yang berbeda
+1 -1
View File
@@ -159,7 +159,7 @@ const allLokets = computed(() => {
const getDefaultLoket = () => {
const allLoketsData = loketStore.loketData?.value || loketStore.loketData || []
const loket1 = allLoketsData.find(l => l.id === 1 || l.no === 1)
return loket1 ? loket1.namaLoket : 'Loket 1'
return loket1 ? loket1.namaLoket : 'Loket A'
}
// Display lokets with their queues
+8 -1
View File
@@ -22,7 +22,7 @@
<v-icon size="32" color="primary-600">mdi-view-dashboard</v-icon>
<div class="loket-info">
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
<p class="loket-details">No. {{ loket.no }}</p>
<p class="loket-details">No. {{ numberToLetter(loket.no) }}</p>
</div>
</div>
@@ -100,6 +100,13 @@ definePageMeta({
const loketStore = useLoketStore();
const route = useRoute();
// Helper function: Convert nomor loket ke huruf (1 -> A, 2 -> B, dst)
const numberToLetter = (num) => {
if (num < 1) return 'A';
const charCode = 64 + num;
return String.fromCharCode(Math.min(charCode, 90));
};
// Get all lokets
const allLokets = computed(() => {
return loketStore.loketData?.value || loketStore.loketData || [];
File diff suppressed because it is too large Load diff
+1 -1
View File
@@ -102,7 +102,7 @@
:rules="[v => !!v || 'Nama loket harus diisi']"
hide-details="auto"
class="mb-3 input-field"
placeholder="Loket 1"
placeholder="Loket A"
/>
<v-text-field
+12 -1
View File
@@ -91,11 +91,22 @@ export default defineEventHandler(async (event: H3Event) => {
})
}
// Helper function untuk generate barcode dengan format: YYMMDD + 5 digit random
const generateBarcode = () => {
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 randomCode = String(Math.floor(Math.random() * 100000)).padStart(5, '0'); // 5 digit random
return `${year}${month}${day}${randomCode}`;
};
// Generate patient data
const barcode = generateBarcode();
const newPatient: Patient = {
no: mockDB.length + 1,
jamPanggil: new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }),
barcode: `250811${String(Date.now()).slice(-6)}${String(mockDB.length + 1).padStart(3, '0')}`,
barcode: barcode,
noAntrian: `UM${String(mockDB.length + 1).padStart(4, '0')} | Onsite - ${barcode}`,
shift: body.shift || 'Shift 1',
klinik: body.clinic,
+24 -4
View File
@@ -3,6 +3,14 @@ import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useClinicStore } from './clinicStore';
// Helper function: Convert nomor loket ke huruf (1 -> A, 2 -> B, dst)
const numberToLetter = (num) => {
if (num < 1) return 'A';
// 1 -> A (65), 2 -> B (66), ..., 26 -> Z (90)
const charCode = 64 + num; // 64 = '@', 65 = 'A'
return String.fromCharCode(Math.min(charCode, 90)); // Maksimal Z (90)
};
export const useLoketStore = defineStore('loket', () => {
const clinicStore = useClinicStore();
@@ -11,7 +19,7 @@ export const useLoketStore = defineStore('loket', () => {
{
id: 1,
no: 1,
namaLoket: "Loket 1",
namaLoket: "Loket A",
kuota: 500,
pelayanan: ["RT", "RM", "TD"],
pembayaran: "JKN",
@@ -21,7 +29,7 @@ export const useLoketStore = defineStore('loket', () => {
{
id: 2,
no: 2,
namaLoket: "Loket 2",
namaLoket: "Loket B",
kuota: 666,
pelayanan: ["JW", "SR"],
pembayaran: "JKN",
@@ -31,7 +39,7 @@ export const useLoketStore = defineStore('loket', () => {
{
id: 3,
no: 3,
namaLoket: "Loket 3",
namaLoket: "Loket C",
kuota: 666,
pelayanan: ["AS", "JT"],
pembayaran: "JKN",
@@ -41,7 +49,7 @@ export const useLoketStore = defineStore('loket', () => {
{
id: 4,
no: 4,
namaLoket: "Loket 4",
namaLoket: "Loket D",
kuota: 3676,
pelayanan: ["KK", "PR"],
pembayaran: "JKN",
@@ -65,10 +73,18 @@ export const useLoketStore = defineStore('loket', () => {
const newId = Math.max(...loketData.value.map(l => l.id), 0) + 1;
const newNo = loketData.value.length + 1;
// Jika namaLoket tidak disediakan atau masih menggunakan angka, ubah ke huruf
let namaLoket = loketPayload.namaLoket;
if (!namaLoket || namaLoket.match(/Loket \d+/)) {
const letter = numberToLetter(newNo);
namaLoket = `Loket ${letter}`;
}
const newLoket = {
id: newId,
no: newNo,
...loketPayload,
namaLoket: namaLoket,
};
loketData.value.push(newLoket);
@@ -104,6 +120,9 @@ export const useLoketStore = defineStore('loket', () => {
return loketData.value.find(l => l.id === id);
};
// Helper function untuk convert nomor ke huruf (export untuk digunakan di komponen)
const getLoketLetter = (num) => numberToLetter(num);
return {
// State
loketData,
@@ -114,6 +133,7 @@ export const useLoketStore = defineStore('loket', () => {
updateLoket,
deleteLoket,
getLoketById,
getLoketLetter,
};
}, {
persist: {
+176 -51
View File
@@ -10,19 +10,93 @@ export const useQueueStore = defineStore('queue', () => {
const penunjangStore = usePenunjangStore();
const loketStore = useLoketStore();
// Helper function untuk mendapatkan loket default (Loket 1)
// 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 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: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
barcode: seedBarcode1, // Barcode unik untuk setiap pasien
noAntrian: `UM1001 | Online - ${seedBarcode1}`,
shift: "Shift 1",
klinik: "KANDUNGAN",
fastTrack: "YA", // Fast Track Patient
@@ -34,8 +108,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 2,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1002 | Online - 250811100155",
barcode: seedBarcode2,
noAntrian: `UM1002 | Online - ${seedBarcode2}`,
shift: "Shift 1",
klinik: "IPD",
fastTrack: "TIDAK",
@@ -47,8 +121,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 3,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1003 | Online - 250811100200",
barcode: seedBarcode3,
noAntrian: `UM1003 | Online - ${seedBarcode3}`,
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "YA", // Fast Track Patient
@@ -60,8 +134,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 4,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1004 | Online - 250811100210",
barcode: seedBarcode4,
noAntrian: `UM1004 | Online - ${seedBarcode4}`,
shift: "Shift 1",
klinik: "THT",
fastTrack: "TIDAK",
@@ -73,8 +147,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 5,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1005 | Online - 250811100163",
barcode: seedBarcode5,
noAntrian: `UM1005 | Online - ${seedBarcode5}`,
shift: "Shift 2",
klinik: "KANDUNGAN",
fastTrack: "TIDAK",
@@ -86,8 +160,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 6,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1006 | Online - 250811100155",
barcode: seedBarcode6,
noAntrian: `UM1006 | Online - ${seedBarcode6}`,
shift: "Shift 1",
klinik: "IPD",
fastTrack: "YA", // Fast Track Patient
@@ -99,8 +173,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 7,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1007 | Online - 250811100200",
barcode: seedBarcode7,
noAntrian: `UM1007 | Online - ${seedBarcode7}`,
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "TIDAK",
@@ -112,8 +186,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 8,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1008 | Online - 250811100210",
barcode: seedBarcode8,
noAntrian: `UM1008 | Online - ${seedBarcode8}`,
shift: "Shift 1",
klinik: "THT",
fastTrack: "TIDAK",
@@ -125,8 +199,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 9,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1009 | Online - 250811100163",
barcode: seedBarcode9,
noAntrian: `UM1009 | Online - ${seedBarcode9}`,
shift: "Shift 2",
klinik: "KANDUNGAN",
fastTrack: "YA", // Fast Track Patient
@@ -138,8 +212,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 10,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1010 | Online - 250811100155",
barcode: seedBarcode10,
noAntrian: `UM1010 | Online - ${seedBarcode10}`,
shift: "Shift 1",
klinik: "IPD",
fastTrack: "TIDAK",
@@ -151,8 +225,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 11,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1011 | Online - 250811100200",
barcode: seedBarcode11,
noAntrian: `UM1011 | Online - ${seedBarcode11}`,
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "TIDAK",
@@ -164,8 +238,8 @@ export const useQueueStore = defineStore('queue', () => {
{
no: 12,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1012 | Online - 250811100210",
barcode: seedBarcode12,
noAntrian: `UM1012 | Online - ${seedBarcode12}`,
shift: "Shift 2",
klinik: "THT",
fastTrack: "YA", // Fast Track Patient
@@ -506,7 +580,7 @@ export const useQueueStore = defineStore('queue', () => {
const createAntreanKlinik = (klinik, patient = null, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
const barcode = patient ? patient.barcode : generateBarcode([], allPatients);
const newPatient = {
no: newNo,
@@ -537,7 +611,7 @@ export const useQueueStore = defineStore('queue', () => {
const createAntreanPenunjang = (penunjang, patient = null, adminType = 'loket') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
const barcode = patient ? patient.barcode : generateBarcode([], allPatients);
const newPatient = {
no: newNo,
@@ -568,7 +642,7 @@ export const useQueueStore = defineStore('queue', () => {
const createAntreanKlinikRuang = (klinikRuang, ruang, patient = null, adminType = 'klinik') => {
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const barcode = patient ? patient.barcode : `250811${String(timestamp.getTime()).slice(-6)}`;
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)
@@ -883,21 +957,23 @@ export const useQueueStore = defineStore('queue', () => {
// Helper function untuk generate nomor antrean baru
// Format: 2 huruf (kode klinik) + 3 digit (001-999)
// Jika pembayaran BPJS: gunakan kode klinik dari master
// Jika pembayaran UMUM/JKMM/SPM/DLL: gunakan "UM" sebagai prefix
const generateQueueNumber = (clinic, paymentType) => {
// Tentukan prefix berdasarkan jenis pembayaran
let prefix = 'UM'; // Default untuk UMUM/JKMM/SPM/DLL
// 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';
if (paymentType === 'BPJS') {
// Gunakan kode klinik dari master (2 huruf)
const clinicCode = clinic?.kode || clinic?.code || 'UM';
prefix = clinicCode.length >= 2 ? clinicCode.substring(0, 2).toUpperCase() : 'UM';
}
// Tentukan payment group untuk counter terpisah (UMUM atau JKN/BPJS)
const paymentGroup = paymentType === 'BPJS' || paymentType === 'JKN' ? 'JKN' : 'UMUM';
// Get counter untuk klinik ini per hari
// Get counter untuk payment group ini per hari
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const counterKey = `queue_counter_${prefix}_${today}`;
const counterKey = `queue_counter_loket_${paymentGroup}_${today}`;
// Get current counter dari localStorage
let counter = 0;
@@ -906,16 +982,44 @@ export const useQueueStore = defineStore('queue', () => {
counter = stored ? parseInt(stored, 10) : 0;
}
// Increment counter (max 999)
counter = Math.min(counter + 1, 999);
// 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: prefix (2 huruf) + 3 digit
return `${prefix}${String(counter).padStart(3, '0')}`;
// Format: R/E + Loket + 3 digit
// Contoh: RA001, RA002, ..., RA999, RB001, RB002, ...
return `${serviceType}${loketLetter}${numberPart}`;
};
// Register patient from Anjungan (onsite registration)
@@ -930,11 +1034,15 @@ export const useQueueStore = defineStore('queue', () => {
? `${String(visitDateTime.getHours()).padStart(2, "0")}:${String(visitDateTime.getMinutes()).padStart(2, "0")}`
: `${String(timestamp.getHours()).padStart(2, "0")}:${String(timestamp.getMinutes()).padStart(2, "0")}`;
// Generate barcode (simulasi - bisa diganti dengan API call)
const barcode = `250811${String(Date.now()).slice(-6)}${String(newNo).padStart(3, "0")}`;
// Generate barcode dengan format: YYMMDD + 5 digit random
const barcode = generateBarcode([], allPatients);
// Generate nomor antrean dengan format baru: 2 huruf + 3 digit
const queueNumber = generateQueueNumber(clinic, paymentType);
// 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)
@@ -1035,8 +1143,16 @@ export const useQueueStore = defineStore('queue', () => {
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:', patient);
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)
@@ -1048,6 +1164,15 @@ export const useQueueStore = defineStore('queue', () => {
};
}
// 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 {
@@ -1056,7 +1181,7 @@ export const useQueueStore = defineStore('queue', () => {
};
}
// Update status to di-loket
// Update status to di-loket using the patientIndex
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "di-loket",