feat: Add admin counter queue page with patient and current patient cards.

This commit is contained in:
Fanrouver
2026-02-13 14:59:00 +07:00
parent 67a5514654
commit 95ff83027c
3 changed files with 212 additions and 20 deletions
@@ -86,23 +86,6 @@
<div class="patient-klinik-pembayaran">{{ patient.klinik }} | {{ patient.pembayaran }}</div>
</div>
<!-- NEW: Option to process next even if current is active -->
<div class="next-action-shortcut mt-2" v-if="hasNextQueue">
<v-btn
block
variant="tonal"
color="primary-600"
size="small"
class="text-none font-weight-bold"
@click="$emit('process-next')"
>
<v-icon start size="16">mdi-skip-next</v-icon>
Panggil Berikutnya
</v-btn>
<div class="info-text text-center mt-1" style="font-size: 10px; color: var(--color-neutral-600);">
(Gunakan jika lupa klik Selesai pada pasien ini)
</div>
</div>
<div class="create-queue-buttons mt-4">
<div class="create-buttons-container">
+1 -1
View File
@@ -11,7 +11,7 @@
:class="{
'clickable-card': isClickable,
'fast-track-card': isFastTrack && !isCurrentlyProcessing,
'processing-card': isCurrentlyProcessing
'processing-card': isCurrentlyProcessing || patient.status === 'diproses'
}"
@click="handleCardClick"
v-bind="isClickable ? tooltipProps : {}"
+211 -2
View File
@@ -221,6 +221,78 @@
</v-card>
</v-dialog>
<!-- Confirmation Replace Dialog -->
<v-dialog v-model="showConfirmReplaceDialog" max-width="500px">
<v-card class="dialog-card overflow-hidden rounded-xl">
<!-- Solid Blue Header (Matching Detail Pasien Style) -->
<v-card-title class="bg-primary-600 text-white py-4 px-6 d-flex justify-between align-center">
<div class="d-flex align-center">
<v-icon start icon="mdi-alert-circle-outline" class="mr-2"></v-icon>
<span class="text-h6 font-weight-bold">Konfirmasi Ganti Pasien</span>
</div>
<v-btn icon="mdi-close" variant="text" size="small" @click="showConfirmReplaceDialog = false" class="text-white"></v-btn>
</v-card-title>
<v-card-text class="dialog-content-premium pa-6">
<div class="text-center mb-6 text-uppercase font-weight-bold text-caption text-neutral-600" style="letter-spacing: 1px;">
Ganti Antrean Aktif
</div>
<div class="comparison-container mb-6">
<!-- Current Patient -->
<div class="comparison-item current">
<div class="item-label">DARI (AKTIF)</div>
<div class="item-card">
<div class="item-number text-danger-700">{{ currentProcessingPatient?.noAntrian?.split(" |")[0] || '-' }}</div>
<div class="item-status">Sedang Diproses</div>
</div>
</div>
<div class="comparison-arrow">
<v-icon size="24" color="neutral-600">mdi-chevron-double-right</v-icon>
</div>
<!-- New Patient -->
<div class="comparison-item target">
<div class="item-label">KE (BARU)</div>
<div class="item-card">
<div class="item-number text-primary-600">{{ pendingReplaceItem ? pendingReplaceItem.noAntrian.split(" |")[0] : (nextPatient ? nextPatient.noAntrian.split(" |")[0] : 'Berikutnya') }}</div>
<div class="item-status">Siap Diproses</div>
</div>
</div>
</div>
<div class="confirm-text text-center px-4">
Apakah anda yakin ingin mengganti antrian yang aktif saat ini?
</div>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4 bg-light justify-center">
<v-btn
variant="flat"
color="neutral-200"
@click="showConfirmReplaceDialog = false"
class="px-8 font-weight-bold rounded-lg"
size="large"
>
Batal
</v-btn>
<v-btn
color="primary-600"
@click="handleConfirmReplace"
class="px-8 text-white font-weight-bold ml-4 rounded-lg"
variant="flat"
size="large"
elevation="2"
>
Ya, Ganti Pasien
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Snackbar -->
<AppSnackbar
v-model="snackbar"
@@ -468,6 +540,11 @@ const showKlinikRuangDialog = ref(false);
const klinikRuangSearch = ref("");
const activePanel = ref(null); // Tracks the open Klinik Ruang panel
// Confirmation Replace Dialog
const showConfirmReplaceDialog = ref(false);
const pendingReplaceItem = ref(null);
const pendingReplaceAction = ref(null);
// Watch dialog state to reset search when closed
watch(showKlinikRuangDialog, (newValue) => {
if (!newValue) {
@@ -790,6 +867,11 @@ const handleCall = async (count) => {
};
const handleTableAction = async (item, action) => {
if (action === "proses") {
confirmAndProcess(item, action);
return;
}
await processPatient(item, action);
// Real-time broadcast after process
@@ -805,11 +887,43 @@ const handleTableAction = async (item, action) => {
};
const handleProcessNext = () => {
processNextQueue();
// Real-time broadcast after process next
confirmAndProcess(null, "next");
};
const confirmAndProcess = (item, action) => {
if (currentProcessingPatient.value) {
pendingReplaceItem.value = item;
pendingReplaceAction.value = action;
showConfirmReplaceDialog.value = true;
} else {
executeProcess(item, action);
}
};
const executeProcess = async (item, action) => {
if (action === "next") {
processNextQueue();
} else {
await processPatient(item, action);
// If action is process, auto-call the patient after a short delay
if (action === "proses") {
setTimeout(() => {
handleCallPatient();
}, 300);
}
}
// Real-time broadcast after process
broadcastUpdate();
};
const handleConfirmReplace = () => {
executeProcess(pendingReplaceItem.value, pendingReplaceAction.value);
showConfirmReplaceDialog.value = false;
pendingReplaceItem.value = null;
pendingReplaceAction.value = null;
};
const { sendViaPost } = useWebSocket();
const broadcastUpdate = async (callData = null) => {
@@ -1332,6 +1446,101 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
sans-serif;
}
.dialog-header-blue {
background-color: var(--color-primary-600) !important;
color: white !important;
}
.header-icon-wrapper {
background: rgba(255, 255, 255, 0.2);
padding: 12px;
border-radius: 12px;
backdrop-filter: blur(4px);
}
.header-title {
font-size: 20px;
font-weight: 700;
line-height: 1.2;
}
.header-subtitle {
font-size: 13px;
opacity: 0.9;
font-weight: 500;
}
.dialog-content-premium {
background: var(--color-neutral-100);
}
.comparison-container {
display: flex;
align-items: center;
gap: 12px;
background: var(--color-neutral-200);
padding: 16px;
border-radius: 16px;
border: 1px solid var(--color-neutral-400);
}
.comparison-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.item-label {
font-size: 10px;
font-weight: 800;
color: var(--color-neutral-600);
letter-spacing: 0.05em;
}
.item-card {
background: white;
padding: 12px;
border-radius: 12px;
border: 1px solid var(--color-neutral-400);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
text-align: center;
}
.item-number {
font-size: 24px;
font-weight: 800;
line-height: 1;
margin-bottom: 4px;
&.text-danger-700 {
color: var(--color-danger-700) !important;
}
&.text-primary-600 {
color: var(--color-primary-600) !important;
}
}
.item-status {
font-size: 11px;
color: var(--color-neutral-700);
font-weight: 600;
}
.comparison-arrow {
display: flex;
align-items: center;
justify-content: center;
}
.confirm-text {
font-size: 15px;
color: var(--color-neutral-800);
font-weight: 500;
line-height: 1.5;
}
.headline-5 {
font-size: 18px;
line-height: 24px;