Merge branch 'Antrean-Code' of https://git.rssa.top/arie.bagus.2905/Web-Antrean into Antrean-Code

This commit is contained in:
Fanrouver
2026-01-08 12:47:36 +07:00
10 changed files with 2373 additions and 59 deletions
+13 -2
View File
@@ -51,8 +51,8 @@ defineProps({
},
theme: {
type: String,
default: 'primary', // 'primary', 'secondary', 'success', or 'accent'
validator: (value) => ['primary', 'secondary', 'success', 'accent'].includes(value)
default: 'primary', // 'primary', 'secondary', 'success', 'accent', or 'warning'
validator: (value) => ['primary', 'secondary', 'success', 'accent', 'warning'].includes(value)
}
});
@@ -69,6 +69,8 @@ $success-600: #009262;
$success-700: #1B6E53;
$accent-600: #8B5CF6;
$accent-700: #7C3AED;
$warning-600: #FF9800;
$warning-700: #F57C00;
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
$font-weight-regular: 400;
$font-weight-semibold: 600;
@@ -97,6 +99,11 @@ $font-weight-semibold: 600;
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
}
.page-header-warning {
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);
box-shadow: 0 4px 16px rgba(255, 152, 0, 0.3);
}
.header-content {
display: flex;
align-items: center;
@@ -159,4 +166,8 @@ $font-weight-semibold: 600;
.add-btn-accent {
color: $accent-600 !important;
}
.add-btn-warning {
color: $warning-600 !important;
}
</style>
+16
View File
@@ -49,6 +49,22 @@
</v-chip>
</div>
<div v-if="patient.tipeLayanan" class="info-row">
<span class="info-label">Tipe Layanan:</span>
<v-chip
size="small"
:color="patient.tipeLayanan === 'Pemeriksaan Awal' ? 'primary' : 'secondary'"
class="text-white"
>
{{ patient.tipeLayanan }}
</v-chip>
</div>
<div v-if="patient.ruang" class="info-row">
<span class="info-label">Ruang:</span>
<span class="info-value">{{ patient.ruang }}</span>
</div>
<div class="info-row">
<span class="info-label">Pembayaran:</span>
<span class="info-value">{{ patient.pembayaran }}</span>
+522
View File
@@ -0,0 +1,522 @@
import { ref } from 'vue';
import QRCode from 'qrcode';
export interface ThermalPrintData {
noAntrian: string;
barcode: string;
klinik: string;
shift: string;
pembayaran: string;
tanggal: string;
waktu: string;
namaDokter?: string;
status?: 'ALLOWED' | 'NOT_ALLOWED';
}
export const useThermalPrint = () => {
const isPrinting = ref(false);
/**
* Generate QR Code data URL
*/
const generateQRCode = async (data: string): Promise<string> => {
try {
const qrDataUrl = await QRCode.toDataURL(data, {
errorCorrectionLevel: 'M',
type: 'image/png',
quality: 1,
margin: 2,
width: 200, // Ukuran QR Code untuk thermal printer 80mm
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
return qrDataUrl;
} catch (error) {
console.error('Error generating QR Code:', error);
throw error;
}
};
/**
* Format date untuk tampilan
*/
const formatDate = (dateString: string): string => {
try {
const 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'
});
}
return date.toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
} catch (error) {
return new Date().toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
};
/**
* Format time untuk tampilan
*/
const formatTime = (dateString: string): string => {
try {
const date = dateString ? new Date(dateString) : new Date();
if (isNaN(date.getTime())) {
return new Date().toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit'
});
}
return date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
return new Date().toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit'
});
}
};
/**
* Generate HTML untuk thermal print
*/
const generatePrintHTML = async (data: ThermalPrintData): Promise<string> => {
// QR Code data: barcode|status (ALLOWED atau NOT_ALLOWED)
const qrData = `${data.barcode}|${data.status || 'NOT_ALLOWED'}`;
const qrCodeImage = await generateQRCode(qrData);
// Format nomor antrian (hilangkan bagian "| Onsite - barcode")
const noAntrianDisplay = data.noAntrian.split(' |')[0];
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nomor Antrian - ${noAntrianDisplay}</title>
<style>
@page {
size: 80mm auto;
margin: 0;
padding: 0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 80mm;
max-width: 80mm;
margin: 0;
padding: 2mm 4mm 6mm 4mm;
font-family: 'Courier New', monospace;
font-size: 12px;
font-weight: bold;
line-height: 1.2;
background: white;
color: black;
}
.ticket-container {
width: 100%;
text-align: center;
border: 2px dashed #000;
padding: 2mm 4mm 4mm 4mm;
background: white;
margin-bottom: 0;
}
.header {
margin-bottom: 3mm;
border-bottom: 2px solid #000;
padding-bottom: 2mm;
margin-top: 0;
}
.hospital-name {
font-size: 14px;
font-weight: bold;
margin-bottom: 1mm;
text-transform: uppercase;
letter-spacing: 1px;
}
.hospital-address {
font-size: 10px;
font-weight: bold;
margin-bottom: 1mm;
}
.ticket-title {
font-size: 12px;
font-weight: bold;
margin-top: 2mm;
text-transform: uppercase;
}
.qr-code-section {
margin: 3mm 0;
text-align: center;
}
.qr-code {
width: 50mm;
height: 50mm;
margin: 0 auto;
border: 1px solid #000;
padding: 2mm;
background: white;
}
.qr-code img {
width: 100%;
height: 100%;
display: block;
}
.barcode-number {
font-size: 10px;
font-weight: bold;
margin-top: 1mm;
word-break: break-all;
}
.info-section {
margin-top: 3mm;
text-align: left;
border-top: 1px dashed #000;
padding-top: 2mm;
padding-left: 3mm;
padding-right: 3mm;
}
.info-row {
margin-bottom: 1.5mm;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 4mm;
padding: 0;
}
.info-label {
font-weight: bold;
font-size: 10px;
flex: 0 0 38%;
text-transform: uppercase;
word-break: keep-all;
}
.info-value {
font-size: 10px;
font-weight: bold;
flex: 1;
text-align: right;
word-break: break-word;
}
.no-antrian {
font-size: 18px;
font-weight: bold;
margin: 2mm 0;
letter-spacing: 2px;
text-transform: uppercase;
}
.klinik-name {
font-size: 14px;
font-weight: bold;
margin: 2mm 0;
text-transform: uppercase;
}
.footer {
margin-top: 3mm;
padding-top: 2mm;
padding-bottom: 0;
margin-bottom: 6mm;
border-top: 2px solid #000;
font-size: 9px;
font-weight: bold;
text-align: center;
}
.footer-text {
margin-bottom: 1mm;
font-weight: bold;
}
.footer-text:last-child {
margin-bottom: 0;
}
/* Feed lines untuk auto cutter (space setelah footer) */
.cut-section {
margin-top: 0;
padding-top: 0;
height: 0;
display: block;
page-break-after: always;
visibility: hidden;
}
@media print {
body {
margin: 0;
padding: 2mm 4mm 6mm 4mm;
}
.ticket-container {
border: none;
padding: 2mm 4mm 4mm 4mm;
margin-bottom: 0;
margin-top: 0;
}
/* Feed setelah print untuk auto cutter */
.ticket-container::after {
content: '';
display: block;
height: 10mm;
min-height: 10mm;
page-break-after: always;
}
@page {
size: 80mm auto;
margin: 2mm 0 6mm 0;
/* Margin top kecil, margin bottom besar untuk feed auto cutter */
}
/* Feed tambahan di akhir untuk memastikan kertas cukup untuk dipotong */
.cut-section {
height: 0 !important;
min-height: 0 !important;
display: block !important;
page-break-after: always !important;
visibility: hidden !important;
}
/* Pastikan tidak ada page break di tengah content */
.ticket-container > * {
page-break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="ticket-container">
<div class="header">
<div class="hospital-name">RSUD DR. SAIFUL ANWAR</div>
<div class="hospital-address">Jl. Jaksa Agung Suprapto No.2, Malang</div>
<div class="ticket-title">Tiket Antrian</div>
</div>
<div class="no-antrian">${noAntrianDisplay}</div>
<div class="klinik-name">${data.klinik}</div>
<div class="qr-code-section">
<div class="qr-code">
<img src="${qrCodeImage}" alt="QR Code" />
</div>
<div class="barcode-number">${data.barcode}</div>
</div>
<div class="info-section">
<div class="info-row">
<span class="info-label">Tanggal:</span>
<span class="info-value">${data.tanggal}</span>
</div>
<div class="info-row">
<span class="info-label">Waktu:</span>
<span class="info-value">${data.waktu}</span>
</div>
<div class="info-row">
<span class="info-label">Shift:</span>
<span class="info-value">${data.shift}</span>
</div>
<div class="info-row">
<span class="info-label">Pembayaran:</span>
<span class="info-value">${data.pembayaran}</span>
</div>
${data.namaDokter ? `
<div class="info-row">
<span class="info-label">Dokter:</span>
<span class="info-value">${data.namaDokter}</span>
</div>
` : ''}
</div>
<div class="footer">
<div class="footer-text">Tunjukkan tiket ini saat check-in</div>
<div class="footer-text">QR Code digunakan untuk check-in pasien</div>
<div class="footer-text">Terima kasih</div>
</div>
<!-- Feed lines untuk auto cutter (space setelah footer untuk cutter) -->
<div class="cut-section"></div>
<!-- ESC/POS Auto Cut Command Comment (untuk printer driver yang support) -->
<!-- ESC/POS Commands for EPSON M352A: -->
<!-- ESC i = Full Cut (0x1B 0x69) -->
<!-- ESC m = Partial Cut (0x1B 0x6D) -->
<!-- GS V 0 = Full Cut (0x1D 0x56 0x00) -->
<!-- GS V 1 = Partial Cut (0x1D 0x56 0x01) -->
</div>
</body>
</html>
`;
return html;
};
/**
* Print thermal ticket
*/
const printTicket = async (data: ThermalPrintData): Promise<boolean> => {
try {
isPrinting.value = true;
// Generate HTML
const html = await generatePrintHTML(data);
// Create print window
const printWindow = window.open('', '_blank', 'width=300,height=400');
if (!printWindow) {
throw new Error('Tidak dapat membuka window print. Pastikan pop-up diizinkan.');
}
printWindow.document.write(html);
printWindow.document.close();
// Wait for images to load
await new Promise((resolve) => {
printWindow.onload = () => {
setTimeout(() => {
printWindow.focus();
// Add event listener untuk afterprint (auto cut trigger)
const handleAfterPrint = () => {
console.log('Print completed. Auto cutter should activate.');
console.log('Note: For EPSON M352A, ensure printer settings have auto cut enabled.');
console.log('ESC/POS Cut Command: ESC i (0x1B 0x69) for full cut');
};
printWindow.addEventListener('afterprint', handleAfterPrint);
// Print ke printer default
// Catatan: Window.print() akan menggunakan printer default yang sudah di-set di sistem
// Jika user ingin mengubah printer, mereka perlu mengubah default printer di Windows/OS Settings terlebih dahulu
printWindow.print();
// Note: Untuk printer thermal EPSON M352A, auto cutter akan otomatis aktif jika:
// 1. Driver printer sudah dikonfigurasi untuk auto cut
// 2. Printer settings di Windows/OS sudah enable auto cut
// 3. CSS feed lines di atas sudah cukup untuk trigger auto cut
//
// ESC/POS Commands yang didukung:
// - ESC i (0x1B 0x69): Full Cut
// - ESC m (0x1B 0x6D): Partial Cut
// - GS V 0 (0x1D 0x56 0x00): Full Cut
// - GS V 1 (0x1D 0x56 0x01): Partial Cut
// Close window after print (optional)
// Delay lebih lama untuk memastikan print dan cut selesai
setTimeout(() => {
printWindow.removeEventListener('afterprint', handleAfterPrint);
printWindow.close();
}, 1500);
resolve(true);
}, 500);
};
});
return true;
} catch (error) {
console.error('Error printing ticket:', error);
throw error;
} finally {
isPrinting.value = false;
}
};
/**
* Print ticket dengan data dari queueStore patient object
*/
const printTicketFromPatient = async (patient: any): Promise<boolean> => {
// Determine tanggal kunjungan
let tanggalKunjungan: string;
if (patient.visitDate) {
// Jika ada visitDate (untuk jadwal lain), gunakan itu
tanggalKunjungan = formatDate(patient.visitDate);
} else if (patient.visitType === 'SEKARANG' || !patient.visitType) {
// Jika kunjungan hari ini
tanggalKunjungan = formatDate(new Date().toISOString());
} else {
// Fallback ke tanggal sekarang
tanggalKunjungan = formatDate(new Date().toISOString());
}
// Determine waktu
let waktu: string;
if (patient.createdAt) {
waktu = formatTime(patient.createdAt);
} else if (patient.jamPanggil) {
// Gunakan jamPanggil jika ada
waktu = patient.jamPanggil;
} else {
waktu = formatTime(new Date().toISOString());
}
// Determine status - pasien baru dari anjungan biasanya status 'menunggu' (NOT_ALLOWED)
// Hanya setelah dipanggil oleh admin loket, status menjadi 'waiting' (ALLOWED)
const status: 'ALLOWED' | 'NOT_ALLOWED' = patient.status === 'waiting' ? 'ALLOWED' : 'NOT_ALLOWED';
const printData: ThermalPrintData = {
noAntrian: patient.noAntrian || '',
barcode: patient.barcode || '',
klinik: patient.klinik || '',
shift: patient.shift || 'Shift 1',
pembayaran: patient.pembayaran || '',
tanggal: tanggalKunjungan,
waktu: waktu,
namaDokter: patient.namaDokter || undefined,
status: status
};
return await printTicket(printData);
};
return {
isPrinting,
printTicket,
printTicketFromPatient,
generateQRCode
};
};
+97
View File
@@ -0,0 +1,97 @@
# Panduan Alur Pemrosesan Pasien - Admin Klinik Ruang
## Overview
Halaman Admin Klinik Ruang digunakan untuk mengelola antrian pasien di setiap ruang klinik. Pasien yang sudah dibuat antrean klinik ruang dari Admin Loket atau Admin Klinik akan muncul di halaman ini untuk di-generate tiket baru.
## Alur Pemrosesan Pasien
### 1. Daftar Pasien Perlu Generate Tiket
- Pasien yang sudah dibuat antrean klinik ruang dari **Admin Loket** atau **Admin Klinik** akan muncul di bagian "DAFTAR PASIEN PERLU GENERATE TIKET"
- Pasien dikelompokkan berdasarkan **ruang** yang telah dipilih sebelumnya oleh admin
- Hanya menampilkan **nomor antrian** untuk menghemat tempat
- Klik ikon **mata (👁️)** untuk melihat detail lengkap pasien
- Klik tombol **"Generate"** untuk mengisi field generate tiket dengan nomor antrian pasien tersebut
### 2. Generate Tiket Baru
- Setelah klik tombol "Generate", nomor antrian pasien akan otomatis terisi di field "Masukan Barcode"
- Atau scan barcode/nomor antrian pasien secara manual di field "Masukan Barcode" lalu tekan **Enter**
- Sistem akan menampilkan dialog untuk memilih:
- **Ruang**: Pilih ruang tujuan (jika ada beberapa ruang)
- **Tipe Layanan**: Pilih antara "Pemeriksaan Awal" atau "Tindakan"
- Setelah konfirmasi, sistem akan generate nomor antrian baru:
- **PA001, PA002, ...** untuk Pemeriksaan Awal
- **TD001, TD002, ...** untuk Tindakan
- Nomor antrian baru akan di-generate otomatis per ruang dan tipe layanan
### 3. Proses Antrian
- Setelah generate tiket, pasien akan muncul di antrian sesuai **tipe layanan** (Pemeriksaan Awal atau Tindakan)
- Klik **"Proses Antrian Berikutnya"** untuk memproses antrian pertama yang menunggu
- Atau klik langsung pada **card pasien** untuk memproses pasien tertentu
### 4. Panggil & Proses Pasien
- Pasien yang sedang diproses akan muncul di bagian **"SEDANG DIPROSES"**
- Admin dapat:
- **Pilih Tipe Layanan**: Ubah antara "Pemeriksaan Awal" atau "Tindakan" dengan klik tombol
- **Panggil**: Klik tombol "Panggil" untuk memanggil pasien (dapat dipanggil berkali-kali)
- **Selesai**: Tandai pasien selesai diproses
- **Terlambat**: Tandai pasien terlambat
- **Pending**: Tandai pasien pending
### 5. Panggil Antrian Berikutnya
- Untuk setiap tipe layanan (Pemeriksaan Awal/Tindakan), admin dapat memanggil antrian berikutnya
- Klik tombol **"Panggil"** di header section setiap tipe layanan
- Pasien dapat dipanggil **berkali-kali** tanpa batas
- Pasien yang dipanggil akan muncul di layar **Anjungan Klinik Ruang**
## Tips & Catatan Penting
### Tips:
- ✅ Pasien yang sudah dibuat antrean klinik ruang akan **otomatis dikelompokkan** berdasarkan ruang yang dipilih
- ✅ Nomor antrian baru (PA/TD) akan di-generate **otomatis per ruang dan tipe layanan**
- ✅ Setiap pasien dapat dipanggil **berkali-kali tanpa batas**
- ✅ Informasi pasien yang dipanggil akan muncul di **layar Anjungan Klinik Ruang**
- ✅ Gunakan tombol **"Generate"** untuk mempercepat proses generate tiket
### Catatan:
- Pasien yang sudah di-generate tiket akan pindah dari "Daftar Pasien Perlu Generate Tiket" ke antrian sesuai tipe layanan
- Status pasien dapat berubah: waiting → di-loket → processed
- Setiap ruang memiliki antrian terpisah untuk Pemeriksaan Awal dan Tindakan
## Struktur Data Pasien
### Pasien Belum Generate Tiket:
- `processStage`: "klinik" atau "klinik-ruang"
- `tipeLayanan`: null (belum ada)
- `ruang`: nama ruang yang dipilih
- `nomorRuang`: nomor ruang
- `status`: "waiting", "di-loket", "terlambat", atau "pending"
### Pasien Sudah Generate Tiket:
- `processStage`: "klinik-ruang"
- `tipeLayanan`: "Pemeriksaan Awal" atau "Tindakan"
- `noAntrian`: format "PA001" atau "TD001" + info klinik dan ruang
- `status`: "waiting", "di-loket", "terlambat", atau "pending"
## Fitur Utama
1. **Scan Barcode**: Auto-detect barcode dengan mengabaikan huruf di depan (jika ada)
2. **Generate Tiket**: Generate nomor antrian baru per ruang dan tipe layanan
3. **Panggil Pasien**: Panggil pasien berkali-kali tanpa batas
4. **Lihat Detail**: Lihat detail lengkap pasien dengan klik ikon mata
5. **Kelola Antrian**: Proses, selesai, terlambat, atau pending pasien
6. **Pisah Tipe Layanan**: Antrian terpisah untuk Pemeriksaan Awal dan Tindakan
## Troubleshooting
**Q: Pasien tidak muncul di daftar?**
A: Pastikan pasien sudah dibuat antrean klinik ruang dari Admin Loket/Admin Klinik dan ruang yang dipilih sesuai.
**Q: Nomor antrian tidak ter-generate?**
A: Pastikan sudah memilih ruang dan tipe layanan di dialog, lalu klik "Buat Antrean".
**Q: Pasien tidak bisa dipanggil?**
A: Pastikan pasien sudah di-generate tiket dan status-nya "waiting" atau "di-loket".
**Q: Pasien muncul di ruang yang salah?**
A: Pastikan ruang yang dipilih saat create antrean klinik ruang sesuai dengan ruang yang diinginkan.
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
<template>
<div class="admin-klinik-ruang-index">
<PageHeader
icon="mdi-door-open"
title="Admin Klinik Ruang"
subtitle="Pilih Klinik untuk mengelola antrian ruang"
:show-add-button="false"
theme="warning"
/>
<v-row class="mt-4">
<v-col
v-for="klinikRuang in klinikRuangList"
:key="klinikRuang.id"
cols="12"
sm="6"
md="4"
lg="3"
>
<v-card
class="klinik-card"
elevation="2"
@click="navigateToKlinik(klinikRuang.kodeKlinik)"
>
<v-card-text class="text-center pa-6">
<v-icon size="64" color="warning" class="mb-4">mdi-hospital-building</v-icon>
<div class="klinik-name">{{ klinikRuang.namaKlinik }}</div>
<v-chip size="small" color="warning" class="mt-2">
{{ klinikRuang.kodeKlinik }}
</v-chip>
<div class="ruang-count mt-3">
<v-icon size="16" class="mr-1">mdi-door</v-icon>
{{ klinikRuang.ruangList.length }} Ruang
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<div v-if="klinikRuangList.length === 0" class="empty-state">
<v-icon size="64" color="grey-lighten-2">mdi-hospital-building-outline</v-icon>
<div class="empty-text">Tidak ada klinik ruang yang tersedia</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useMasterStore } from '@/stores/masterStore';
import PageHeader from '@/components/common/PageHeader.vue';
const router = useRouter();
const masterStore = useMasterStore();
const klinikRuangList = computed(() => {
return masterStore.ruangData || [];
});
const navigateToKlinik = (kodeKlinik) => {
router.push(`/admin-klinik-ruang/${kodeKlinik}`);
};
</script>
<style scoped lang="scss">
.admin-klinik-ruang-index {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 16px;
}
.klinik-card {
border-radius: 12px;
border: 1px solid var(--color-neutral-500);
background: var(--color-neutral-100);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
border-color: var(--color-warning-600);
}
}
.klinik-name {
font-size: 18px;
font-weight: 700;
color: var(--color-neutral-900);
margin-bottom: 8px;
}
.ruang-count {
font-size: 13px;
color: var(--color-neutral-700);
display: flex;
align-items: center;
justify-content: center;
}
.empty-state {
text-align: center;
padding: 64px 16px;
}
.empty-text {
font-size: 16px;
color: var(--color-neutral-600);
margin-top: 16px;
}
</style>
+146 -2
View File
@@ -474,6 +474,90 @@
</v-card>
</v-dialog>
<!-- Print Dialog -->
<v-dialog
v-model="showPrintDialog"
max-width="500"
persistent
@click:outside="skipPrint"
>
<v-card class="dialog-card">
<v-card-title class="dialog-header">
<v-icon class="mr-2">mdi-printer</v-icon>
Cetak Nomor Antrian
</v-card-title>
<v-divider />
<v-card-text class="pa-6" v-if="lastRegisteredPatient">
<div 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>
</p>
</div>
<v-divider class="my-4" />
<div class="dialog-info">
<div class="info-row mb-3">
<span class="info-label">Klinik:</span>
<span class="info-value">{{ lastRegisteredPatient.klinik }}</span>
</div>
<div class="info-row mb-3">
<span class="info-label">Shift:</span>
<span class="info-value">{{ lastRegisteredPatient.shift }}</span>
</div>
<div class="info-row mb-3">
<span class="info-label">Pembayaran:</span>
<span class="info-value">{{ lastRegisteredPatient.pembayaran }}</span>
</div>
<div class="info-row">
<span class="info-label">Barcode:</span>
<span class="info-value">{{ lastRegisteredPatient.barcode }}</span>
</div>
</div>
<v-alert
type="info"
variant="tonal"
class="mt-4"
density="compact"
>
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-body-2">
Cetak tiket ini untuk digunakan saat check-in. QR Code pada tiket dapat di-scan untuk proses check-in.
</div>
</v-alert>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
variant="outlined"
color="neutral-600"
@click="skipPrint"
:disabled="isPrinting"
>
Lewati
</v-btn>
<v-btn
color="primary-600"
class="text-white"
variant="flat"
@click="handlePrintTicket"
:loading="isPrinting"
:disabled="isPrinting"
prepend-icon="mdi-printer"
>
{{ isPrinting ? 'Mencetak...' : 'Cetak Tiket' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
@@ -501,6 +585,7 @@ import { useRoute } from "#app";
import { useAnjunganStore } from "@/stores/anjunganStore";
import { useClinicStore } from "@/stores/clinicStore";
import { useQueueStore } from "@/stores/queueStore";
import { useThermalPrint } from "@/composables/useThermalPrint";
definePageMeta({
layout: false,
@@ -510,6 +595,7 @@ const route = useRoute();
const anjunganStore = useAnjunganStore();
const clinicStore = useClinicStore();
const queueStore = useQueueStore();
const { printTicketFromPatient, isPrinting } = useThermalPrint();
const anjunganId = computed(() => {
const raw = route.params.id;
@@ -786,6 +872,8 @@ const showVisitTypeDialog = ref(false);
const showPaymentTypeDialog = ref(false);
const showBookingFormDialog = ref(false);
const showDoctorSelectionDialog = ref(false);
const showPrintDialog = ref(false);
const lastRegisteredPatient = ref(null);
const selectedClinic = ref(null);
const selectedDoctor = ref(null);
const pendingPaymentType = ref(null);
@@ -942,7 +1030,7 @@ const confirmDoctorSelection = () => {
registerPatient('SEKARANG', pendingPaymentType.value, selectedDoctor.value);
};
const registerPatient = (visitType, paymentType, namaDokter) => {
const registerPatient = async (visitType, paymentType, namaDokter) => {
// Register patient to queueStore
const result = queueStore.registerPatientFromAnjungan(
selectedClinic.value,
@@ -957,6 +1045,12 @@ const registerPatient = (visitType, paymentType, namaDokter) => {
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");
// Simpan data pasien untuk print
lastRegisteredPatient.value = result.patient;
// Tampilkan dialog print
showPrintDialog.value = true;
} else {
showSnackbar(result.message || "Gagal melakukan pendaftaran", "error");
}
@@ -966,7 +1060,7 @@ const registerPatient = (visitType, paymentType, namaDokter) => {
pendingPaymentType.value = null;
};
const submitBooking = () => {
const submitBooking = async () => {
if (
!bookingForm.value.date ||
!bookingForm.value.shift ||
@@ -1001,6 +1095,12 @@ const submitBooking = () => {
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");
// Simpan data pasien untuk print
lastRegisteredPatient.value = result.patient;
// Tampilkan dialog print
showPrintDialog.value = true;
} else {
showSnackbar(result.message || "Gagal menyimpan jadwal", "error");
}
@@ -1010,6 +1110,25 @@ const submitBooking = () => {
bookingForm.value.doctor = null;
};
const handlePrintTicket = async () => {
if (!lastRegisteredPatient.value) return;
try {
await printTicketFromPatient(lastRegisteredPatient.value);
showSnackbar("Tiket berhasil dicetak", "success");
showPrintDialog.value = false;
lastRegisteredPatient.value = null;
} catch (error) {
console.error("Error printing ticket:", error);
showSnackbar("Gagal mencetak tiket. Silakan coba lagi.", "error");
}
};
const skipPrint = () => {
showPrintDialog.value = false;
lastRegisteredPatient.value = null;
};
const backToList = () => {
navigateTo("/anjungan/anjungan");
};
@@ -1310,6 +1429,31 @@ const navigateToCheckIn = () => {
}
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--color-neutral-200);
&:last-child {
border-bottom: none;
}
}
.info-label {
font-weight: 600;
color: var(--color-neutral-700);
flex: 0 0 40%;
}
.info-value {
color: var(--color-neutral-900);
flex: 1;
text-align: right;
word-break: break-word;
}
.doctor-list-section {
margin-bottom: 16px;
@@ -49,30 +49,65 @@
<!-- Queue Content -->
<div class="clinic-content">
<!-- Current Queue -->
<div v-if="ruang.currentQueue" class="current-section">
<div class="current-label">SEKARANG</div>
<div class="current-number">
{{ ruang.currentQueue.noAntrian.split(' |')[0] }}
</div>
</div>
<!-- Next Queues -->
<div v-if="ruang.nextQueues.length > 0" class="next-section">
<div class="next-label">SELANJUTNYA</div>
<div class="next-numbers">
<div
v-for="queue in ruang.nextQueues.slice(0, 3)"
:key="queue.no"
class="next-item"
>
{{ queue.noAntrian.split(' |')[0] }}
<!-- Pemeriksaan Awal Section -->
<div class="tipe-layanan-section">
<div class="tipe-label">PEMERIKSAAN AWAL</div>
<div v-if="ruang.pemeriksaanAwal.currentQueue" class="current-section">
<div class="current-label">SEKARANG</div>
<div class="current-number">
{{ ruang.pemeriksaanAwal.currentQueue.noAntrian.split(' |')[0] }}
</div>
</div>
<div v-if="ruang.pemeriksaanAwal.nextQueues.length > 0" class="next-section">
<div class="next-label">SELANJUTNYA</div>
<div class="next-numbers">
<div
v-for="queue in ruang.pemeriksaanAwal.nextQueues"
:key="queue.no"
class="next-item"
>
{{ queue.noAntrian.split(' |')[0] }}
</div>
</div>
</div>
<div v-if="!ruang.pemeriksaanAwal.currentQueue && ruang.pemeriksaanAwal.nextQueues.length === 0" class="empty-state-small">
<v-icon size="24" color="grey-lighten-3">mdi-clock-outline</v-icon>
<p class="empty-text-small">Tidak Ada Antrian</p>
</div>
</div>
<!-- Empty State -->
<div v-if="!ruang.currentQueue && ruang.nextQueues.length === 0" class="empty-state">
<!-- Divider -->
<v-divider class="my-3" />
<!-- Tindakan Section -->
<div class="tipe-layanan-section">
<div class="tipe-label">TINDAKAN</div>
<div v-if="ruang.tindakan.currentQueue" class="current-section">
<div class="current-label">SEKARANG</div>
<div class="current-number">
{{ ruang.tindakan.currentQueue.noAntrian.split(' |')[0] }}
</div>
</div>
<div v-if="ruang.tindakan.nextQueues.length > 0" class="next-section">
<div class="next-label">SELANJUTNYA</div>
<div class="next-numbers">
<div
v-for="queue in ruang.tindakan.nextQueues"
:key="queue.no"
class="next-item"
>
{{ queue.noAntrian.split(' |')[0] }}
</div>
</div>
</div>
<div v-if="!ruang.tindakan.currentQueue && ruang.tindakan.nextQueues.length === 0" class="empty-state-small">
<v-icon size="24" color="grey-lighten-3">mdi-clock-outline</v-icon>
<p class="empty-text-small">Tidak Ada Antrian</p>
</div>
</div>
<!-- Empty State for entire room -->
<div v-if="ruang.totalQueues === 0" class="empty-state">
<v-icon size="48" color="grey-lighten-3">mdi-clock-outline</v-icon>
<p class="empty-text">Tidak Ada Antrian</p>
</div>
@@ -181,7 +216,11 @@ const currentDate = ref('')
let timeInterval = null
const klinikPatients = computed(() => {
return queueStore.getPatientsByStage('klinik').value.all
// Get patients from klinik-ruang stage (created from AdminKlinikRuang)
return queueStore.allPatients.filter(p =>
p.processStage === 'klinik-ruang' &&
p.kodeKlinik === kodeKlinik.value
)
})
// Get ruang list for this specific klinik
@@ -221,8 +260,9 @@ const displayedRuang = computed(() => {
}
return ruangListForKlinik.value.map(ruang => {
const queues = klinikPatients.value
.filter(p => p.klinik === ruang.namaKlinik && (p.ruang === ruang.namaRuang || !p.ruang))
// Get queues for this specific room
const allQueues = klinikPatients.value
.filter(p => p.nomorRuang === ruang.nomorRuang)
.sort((a, b) => {
const statusPriority = {
'di-loket': 1,
@@ -234,18 +274,28 @@ const displayedRuang = computed(() => {
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99)
if (priorityDiff !== 0) return priorityDiff
const timeA = a.jamPanggil.split(':').map(Number)
const timeB = b.jamPanggil.split(':').map(Number)
return timeA[0] * 60 + timeA[1] - (timeB[0] * 60 + timeB[1])
// Sort by queue number
const numA = parseInt(a.noAntrian.match(/\d+/)?.[0] || '999')
const numB = parseInt(b.noAntrian.match(/\d+/)?.[0] || '999')
return numA - numB
})
const currentQueue = queues.find(q => q.status === 'di-loket') ||
(queues.find(q => q.status === 'waiting') || null)
const nextQueues = queues.filter(q =>
q.no !== currentQueue?.no &&
(q.status === 'waiting' || q.status === 'di-loket')
)
// Split by tipeLayanan
const pemeriksaanAwalQueues = allQueues.filter(q => q.tipeLayanan === 'Pemeriksaan Awal')
const tindakanQueues = allQueues.filter(q => q.tipeLayanan === 'Tindakan')
// Get current and next queues for each tipeLayanan
const getCurrentAndNext = (queues) => {
const current = queues.find(q => q.status === 'di-loket') || null
const next = queues.filter(q =>
q.no !== current?.no &&
(q.status === 'waiting' || q.status === 'di-loket')
).slice(0, 3) // Show max 3 next queues
return { current, next }
}
const pemeriksaanAwal = getCurrentAndNext(pemeriksaanAwalQueues)
const tindakan = getCurrentAndNext(tindakanQueues)
return {
kodeKlinik: ruang.kodeKlinik,
@@ -253,9 +303,17 @@ const displayedRuang = computed(() => {
nomorRuang: ruang.nomorRuang,
namaRuang: ruang.namaRuang,
nomorScreen: ruang.nomorScreen,
currentQueue: currentQueue ? { ...currentQueue, ruang: ruang.namaRuang } : null,
nextQueues: nextQueues,
totalQueues: queues.length
pemeriksaanAwal: {
currentQueue: pemeriksaanAwal.current,
nextQueues: pemeriksaanAwal.next,
totalQueues: pemeriksaanAwalQueues.length
},
tindakan: {
currentQueue: tindakan.current,
nextQueues: tindakan.next,
totalQueues: tindakanQueues.length
},
totalQueues: allQueues.length
}
})
})
@@ -263,16 +321,16 @@ const displayedRuang = computed(() => {
const currentCalledQueue = computed(() => {
if (!kodeKlinik.value || !klinikData.value) return null
// Get most recently called patient (status di-loket)
const calledQueues = klinikPatients.value
.filter(p => p.status === 'di-loket' && p.klinik === klinikData.value.namaKlinik)
.filter(p => p.status === 'di-loket')
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
if (calledQueues.length > 0) {
const patient = calledQueues[0]
const ruang = ruangListForKlinik.value.find(r => r.namaKlinik === patient.klinik)
return {
...patient,
ruang: ruang ? ruang.namaRuang : ruangListForKlinik.value[0]?.namaRuang || 'Ruang 1'
ruang: patient.ruang || patient.namaRuang || 'Ruang 1'
}
}
@@ -284,7 +342,7 @@ const statistics = computed(() => {
return { total: 0, waiting: 0, active: 0 }
}
const all = klinikPatients.value.filter(p => p.klinik === klinikData.value.namaKlinik)
const all = klinikPatients.value
return {
total: all.length,
waiting: all.filter(p => p.status === 'waiting').length,
@@ -516,6 +574,31 @@ onUnmounted(() => {
background: var(--color-success-100);
}
.tipe-layanan-section {
margin-bottom: 8px;
}
.tipe-label {
font-size: 12px;
font-weight: 700;
color: var(--color-success-700);
margin-bottom: 8px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.empty-state-small {
text-align: center;
padding: 16px 0;
}
.empty-text-small {
font-size: 12px;
color: var(--color-neutral-600);
margin-top: 8px;
font-weight: 500;
}
.current-section {
text-align: center;
margin-bottom: 12px;
+14 -15
View File
@@ -25,11 +25,12 @@ const defaultNavItems: NavItem[] = [
},
{ id: 4, name: "Admin Loket", icon: "mdi-account-supervisor-outline", path: "/AdminLoket" },
{ id: 5, name: "Admin Klinik", icon: "mdi-text-box-plus-outline", path: "/AdminKlinik" },
{ id: 6, name: "Admin Penunjang", icon: "mdi-plus-box-outline", path: "/AdminPenunjang" },
{ id: 7, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },
{ id: 8, name: "Monitoring Pasien", icon: "mdi-account-group-outline", path: "/MonitoringPasien/monitoringPasien" },
{ id: 6, name: "Admin Klinik Ruang", icon: "mdi-door-open", path: "/AdminKlinikRuang" },
{ id: 7, name: "Admin Penunjang", icon: "mdi-plus-box-outline", path: "/AdminPenunjang" },
{ id: 8, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },
{ id: 9, name: "Monitoring Pasien", icon: "mdi-account-group-outline", path: "/MonitoringPasien/monitoringPasien" },
{
id: 9,
id: 10,
name: "Layar Informasi",
icon: "mdi-monitor-multiple",
path: "",
@@ -38,25 +39,23 @@ const defaultNavItems: NavItem[] = [
{ id: 11, name: "Klinik", path: "/anjungan/AntrianKlinik", icon: "mdi-circle-small" },
{ id: 12, name: "Klinik Ruang", path: "/anjungan/AntrianKlinikRuang", icon: "mdi-circle-small"},
{ id: 13, name: "Penunjang", path: "/anjungan/AntrianPenunjang", icon: "mdi-circle-small"},
{id: 14, name: "Loket", path: "/anjungan/AntrianLoket", icon: "mdi-circle-small"},
{id: 14, name: "Antrean Masuk", path: "/anjungan/AntreanMasuk", icon: "mdi-circle-small"},
],
},
{
id: 14,
id: 15,
name: "Master Data",
icon: "mdi-cog-outline",
path: "",
children: [
{ id: 15, name: "Hak Akses", path: "/setting/HakAkses", icon: "mdi-circle-small" },
{ id: 16, name: "User Login", path: "/setting/UserLogin", icon: "mdi-circle-small" },
{ id: 17, name: "Master Anjungan", path: "/setting/masteranjungan", icon: "mdi-circle-small" },
{ id: 18, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
{ id: 19, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
{ id: 20, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
{ id: 21, name: "Master Penunjang", path: "/setting/masterpenunjang", icon: "mdi-circle-small" },
{ id: 22, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
{ id: 16, name: "Hak Akses", path: "/setting/HakAkses", icon: "mdi-circle-small" },
{ id: 17, name: "User Login", path: "/setting/UserLogin", icon: "mdi-circle-small" },
{ id: 18, name: "Master Anjungan", path: "/setting/masteranjungan", icon: "mdi-circle-small" },
{ id: 19, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
{ id: 20, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
{ id: 21, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
{ id: 22, name: "Master Penunjang", path: "/setting/masterpenunjang", icon: "mdi-circle-small" },
{ id: 23, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
],
},
];
+244
View File
@@ -600,6 +600,246 @@ export const useQueueStore = defineStore('queue', () => {
};
};
// Scan barcode dan generate antrean klinik ruang baru
const scanAndCreateAntreanKlinikRuang = (barcodeInput, klinikRuang, ruang, tipeLayanan = 'Pemeriksaan Awal') => {
// Clean input - remove whitespace and handle prefix letters
const cleanInput = String(barcodeInput).trim().toUpperCase();
// Remove leading letters if any (e.g., "J200730100005" -> "200730100005")
const numericInput = cleanInput.replace(/^[A-Z]+/, '');
// Find patient by barcode or noAntrian
const sourcePatient = allPatients.value.find(p => {
// Exact barcode match
if (p.barcode === cleanInput || p.barcode === numericInput) return true;
// Check if noAntrian includes the input
const noAntrianUpper = (p.noAntrian || '').toUpperCase();
if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(numericInput)) return true;
// Try extracting number from noAntrian
const noAntrianNumber = noAntrianUpper.match(/([A-Z]+)(\d+)/);
if (noAntrianNumber) {
const extractedNumber = noAntrianNumber[2];
if (extractedNumber.includes(numericInput) || numericInput.includes(extractedNumber)) return true;
}
return false;
});
if (!sourcePatient) {
return { success: false, message: "Pasien tidak ditemukan. Pastikan barcode/nomor antrian benar." };
}
// Check if patient already has antrean klinik ruang for this room
const existingAntrean = allPatients.value.find(p =>
p.referencePatient === sourcePatient.noAntrian &&
p.kodeKlinik === klinikRuang.kodeKlinik &&
p.nomorRuang === ruang.nomorRuang &&
p.processStage === 'klinik-ruang'
);
if (existingAntrean) {
return {
success: false,
message: `Pasien sudah memiliki antrean di ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang}`
};
}
// Generate queue number for this specific room and tipeLayanan
const roomQueues = allPatients.value.filter(p =>
p.kodeKlinik === klinikRuang.kodeKlinik &&
p.nomorRuang === ruang.nomorRuang &&
p.tipeLayanan === tipeLayanan &&
p.processStage === 'klinik-ruang'
);
const queueNumber = roomQueues.length + 1;
const prefix = tipeLayanan === 'Pemeriksaan Awal' ? 'PA' : 'TD';
const newNo = allPatients.value.length + 1;
const timestamp = new Date();
const newPatient = {
no: newNo,
jamPanggil: `${String(timestamp.getHours()).padStart(2, "0")}:${String(
timestamp.getMinutes()
).padStart(2, "0")}`,
barcode: sourcePatient.barcode,
noAntrian: `${prefix}${String(queueNumber).padStart(3, "0")} | ${klinikRuang.namaKlinik} - ${ruang.namaRuang} - ${tipeLayanan}`,
noAntrianRuang: `${klinikRuang.namaKlinik} - ${ruang.namaRuang} | ${prefix}${String(queueNumber).padStart(3, "0")}`,
shift: sourcePatient.shift || "Shift 1",
klinik: klinikRuang.namaKlinik,
ruang: ruang.namaRuang,
kodeKlinik: klinikRuang.kodeKlinik,
nomorRuang: ruang.nomorRuang,
nomorScreen: ruang.nomorScreen,
tipeLayanan: tipeLayanan,
fastTrack: sourcePatient.fastTrack || "TIDAK",
pembayaran: sourcePatient.pembayaran || "UMUM",
status: "waiting",
processStage: "klinik-ruang",
createdAt: timestamp.toISOString(),
referencePatient: sourcePatient.noAntrian,
sourcePatientNo: sourcePatient.no,
};
allPatients.value.push(newPatient);
return {
success: true,
message: `Antrean ${tipeLayanan} berhasil dibuat: ${newPatient.noAntrian.split(" |")[0]} untuk ${klinikRuang.namaKlinik} Ruang ${ruang.nomorRuang}`,
patient: newPatient,
};
};
// Get patients by klinik and ruang
const getPatientsByKlinikRuang = (kodeKlinik, nomorRuang, tipeLayanan = null) => {
return computed(() => {
let patients = allPatients.value.filter(p =>
p.kodeKlinik === kodeKlinik &&
p.nomorRuang === nomorRuang &&
p.processStage === 'klinik-ruang'
);
if (tipeLayanan) {
patients = patients.filter(p => p.tipeLayanan === tipeLayanan);
}
return {
all: patients,
waiting: patients.filter(p => p.status === 'waiting'),
diLoket: patients.filter(p => p.status === 'di-loket'),
terlambat: patients.filter(p => p.status === 'terlambat'),
pending: patients.filter(p => p.status === 'pending'),
};
});
};
// Call next patient for a specific room and tipeLayanan
const callNextKlinikRuang = (kodeKlinik, nomorRuang, tipeLayanan, allowMultiple = false) => {
const patients = allPatients.value.filter(p =>
p.kodeKlinik === kodeKlinik &&
p.nomorRuang === nomorRuang &&
p.tipeLayanan === tipeLayanan &&
p.processStage === 'klinik-ruang' &&
(p.status === 'waiting' || (allowMultiple && p.status === 'di-loket'))
).sort((a, b) => {
// Sort by status priority first
const statusPriority = {
'waiting': 1,
'di-loket': 2
};
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
if (priorityDiff !== 0) return priorityDiff;
// Then sort by queue number (extract from noAntrian)
const numA = parseInt(a.noAntrian.match(/\d+/)?.[0] || '999');
const numB = parseInt(b.noAntrian.match(/\d+/)?.[0] || '999');
return numA - numB;
});
if (patients.length === 0) {
return { success: false, message: `Tidak ada antrian ${tipeLayanan} yang menunggu di ruang ini` };
}
const nextPatient = patients[0];
const patientIndex = allPatients.value.findIndex(p => p.no === nextPatient.no);
if (patientIndex !== -1) {
// If allowMultiple, we can call even if already di-loket (just update timestamp)
// Otherwise, only update if status is waiting
if (allowMultiple || nextPatient.status === 'waiting') {
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "di-loket",
lastCalledAt: new Date().toISOString() // Track last call time
};
}
}
return {
success: true,
message: `Memanggil pasien ${nextPatient.noAntrian.split(" |")[0]} untuk ${tipeLayanan}`,
patient: allPatients.value[patientIndex],
};
};
// Process patient in klinik ruang (set as current processing)
const processPatientKlinikRuang = (patient, action, kodeKlinik, nomorRuang) => {
const patientIndex = allPatients.value.findIndex(p => p.no === patient.no);
if (patientIndex === -1) {
return { success: false, message: "Pasien tidak ditemukan" };
}
const patientCode = patient.noAntrian.split(" |")[0];
const key = `klinik-ruang-${kodeKlinik}-${nomorRuang}`;
let message = "";
switch (action) {
case "proses":
// Set as current processing for this room
if (!currentProcessingPatient.value[key]) {
// Use Vue's reactive assignment
currentProcessingPatient.value = {
...currentProcessingPatient.value,
[key]: {}
};
}
// Update nested property reactively
currentProcessingPatient.value[key] = {
...currentProcessingPatient.value[key],
[patient.tipeLayanan]: allPatients.value[patientIndex]
};
message = `Memproses pasien ${patientCode}`;
break;
case "selesai":
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "processed"
};
// Clear current processing
if (currentProcessingPatient.value[key]) {
currentProcessingPatient.value[key] = {
...currentProcessingPatient.value[key],
[patient.tipeLayanan]: null
};
}
message = `Pasien ${patientCode} selesai diproses`;
break;
case "terlambat":
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "terlambat"
};
if (currentProcessingPatient.value[key]) {
currentProcessingPatient.value[key] = {
...currentProcessingPatient.value[key],
[patient.tipeLayanan]: null
};
}
message = `Pasien ${patientCode} ditandai terlambat`;
break;
case "pending":
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: "pending"
};
if (currentProcessingPatient.value[key]) {
currentProcessingPatient.value[key] = {
...currentProcessingPatient.value[key],
[patient.tipeLayanan]: null
};
}
message = `Pasien ${patientCode} di-pending`;
break;
}
return { success: true, message };
};
const changeKlinik = (patient, newKlinik, adminType = 'loket') => {
const patientIndex = allPatients.value.findIndex((p) => p.no === patient.no);
@@ -801,6 +1041,10 @@ export const useQueueStore = defineStore('queue', () => {
createAntreanKlinik,
createAntreanPenunjang,
createAntreanKlinikRuang,
scanAndCreateAntreanKlinikRuang,
getPatientsByKlinikRuang,
callNextKlinikRuang,
processPatientKlinikRuang,
changeKlinik,
processNextQueue,
getPatientsByStage,