update api loket admin
This commit is contained in:
No files matched your search
@@ -2,209 +2,222 @@
|
||||
<div>
|
||||
<!-- Compact Header -->
|
||||
<PageHeader
|
||||
icon="mdi-view-dashboard"
|
||||
title="Admin Loket"
|
||||
icon="mdi-desktop-mac"
|
||||
:title="`Admin ${loketName}`"
|
||||
:subtitle="currentDate"
|
||||
:show-add-button="false"
|
||||
theme="primary"
|
||||
/>
|
||||
>
|
||||
<template #actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
@click="router.push('/adminloket')"
|
||||
class="mr-2"
|
||||
>
|
||||
Kembali
|
||||
</v-btn>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="loket-container">
|
||||
<!-- Main Content Grid -->
|
||||
<v-row class="content-grid" dense>
|
||||
<!-- Left Column: Current Patient & Queue Actions -->
|
||||
<v-col cols="12" md="5" class="sticky-column">
|
||||
<div class="sticky-wrapper">
|
||||
<!-- Current Patient Card -->
|
||||
<CurrentPatientCard
|
||||
:patient="currentProcessingPatient"
|
||||
:has-next-queue="(diLoketPatients || []).length > 0"
|
||||
:next-queue-info="nextQueueInfo"
|
||||
@action="handlePatientAction"
|
||||
@change-klinik="showChangeKlinikDialog = true"
|
||||
@process-next="handleProcessNext"
|
||||
@call="handleCallPatient"
|
||||
@open-klinik-ruang="openKlinikRuangDialog"
|
||||
@open-penunjang="openPenunjangDialog"
|
||||
/>
|
||||
<!-- Left Column: Current Patient & Queue Actions -->
|
||||
<v-col cols="12" md="5" class="sticky-column">
|
||||
<div class="sticky-wrapper">
|
||||
<!-- Current Patient Card -->
|
||||
<CurrentPatientCard
|
||||
:patient="currentProcessingPatient"
|
||||
:has-next-queue="(diLoketPatients || []).length > 0"
|
||||
:next-queue-info="nextQueueInfo"
|
||||
@action="handlePatientAction"
|
||||
@change-klinik="showChangeKlinikDialog = true"
|
||||
@process-next="handleProcessNext"
|
||||
@call="handleCallPatient"
|
||||
@open-klinik-ruang="openKlinikRuangDialog"
|
||||
@open-penunjang="openPenunjangDialog"
|
||||
/>
|
||||
|
||||
<!-- Queue Actions Card -->
|
||||
<QueueActionsCard
|
||||
class="mt-3"
|
||||
:total-quota="150"
|
||||
:used-quota="quotaUsed"
|
||||
:menunggu-count="menungguCount"
|
||||
:has-next="!!nextPatient"
|
||||
@call="handleCall"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<!-- Queue Actions Card -->
|
||||
<QueueActionsCard
|
||||
class="mt-3"
|
||||
:total-quota="150"
|
||||
:used-quota="quotaUsed"
|
||||
:menunggu-count="menungguCount"
|
||||
:has-next="!!nextPatient"
|
||||
@call="handleCall"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Column: Patient Table -->
|
||||
<v-col cols="12" md="7">
|
||||
<v-card class="patient-data-container" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Header with Filters -->
|
||||
<div class="data-header mb-4">
|
||||
<div class="section-label">CARI PASIEN</div>
|
||||
|
||||
<div class="filters">
|
||||
<!-- Search Field -->
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Cari barcode, nomor antrian..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="search-field"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
<!-- Right Column: Patient Table -->
|
||||
<v-col cols="12" md="7">
|
||||
<v-card class="patient-data-container" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Header with Filters -->
|
||||
<div class="data-header mb-4">
|
||||
<div class="section-label">CARI PASIEN</div>
|
||||
|
||||
<div class="filters">
|
||||
<!-- Search Field -->
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Cari barcode, nomor antrian..."
|
||||
density="compact"
|
||||
hide-details
|
||||
class="search-field"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PatientDataTable
|
||||
:items="allPatientsForStage"
|
||||
v-model:selected-status="selectedStatus"
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:selected-fast-track="selectedFastTrack"
|
||||
:di-loket-count="diLoketCount"
|
||||
:diproses-count="currentProcessingPatient ? 1 : 0"
|
||||
:waiting-count="waitingCount"
|
||||
:terlambat-count="(terlambatPatients || []).length"
|
||||
:pending-count="(pendingPatients || []).length"
|
||||
:show-diproses="false"
|
||||
:fast-track-options="fastTrackOptions"
|
||||
@action="handleTableAction"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Klinik Dialog -->
|
||||
<SelectionDialog
|
||||
v-model="showKlinikDialog"
|
||||
title="Pilih Klinik"
|
||||
:items="filteredKliniks"
|
||||
v-model:search-query="klinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="selectKlinik"
|
||||
/>
|
||||
|
||||
<!-- Penunjang Dialog -->
|
||||
<SelectionDialog
|
||||
v-model="showPenunjangDialog"
|
||||
title="Pilih Penunjang"
|
||||
:items="filteredPenunjangs"
|
||||
v-model:search-query="penunjangSearch"
|
||||
search-placeholder="Cari penunjang..."
|
||||
@select="selectPenunjang"
|
||||
/>
|
||||
|
||||
<!-- Change Klinik Dialog -->
|
||||
<SelectionDialog
|
||||
v-model="showChangeKlinikDialog"
|
||||
title="Ubah Klinik"
|
||||
:items="filteredChangeKliniks"
|
||||
v-model:search-query="changeKlinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="changeKlinik"
|
||||
/>
|
||||
|
||||
<!-- Dialog Klinik Ruang -->
|
||||
<v-dialog v-model="showKlinikRuangDialog" max-width="900px" scrollable>
|
||||
<v-card class="dialog-card">
|
||||
<v-card-title class="dialog-header dialog-header-warning">
|
||||
<span class="headline-4">Pilih Klinik Ruang</span>
|
||||
<v-btn icon variant="text" size="small" class="btn-close" @click="closeKlinikRuangDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text class="dialog-content">
|
||||
<div v-if="currentProcessingPatient" class="patient-card mb-4">
|
||||
<h4 class="headline-5 mb-3">Informasi Pasien</h4>
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<div class="detail-item">
|
||||
<span class="caption-2 text-muted">Barcode</span>
|
||||
<span class="body-2 text-semibold">{{ currentProcessingPatient.barcode }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="detail-item">
|
||||
<span class="caption-2 text-muted">No. Antrian</span>
|
||||
<span class="body-2 text-semibold">{{ currentProcessingPatient.noAntrian }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<PatientDataTable
|
||||
:items="allPatientsForStage"
|
||||
v-model:selected-status="selectedStatus"
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:selected-fast-track="selectedFastTrack"
|
||||
:di-loket-count="diLoketCount"
|
||||
:diproses-count="currentProcessingPatient ? 1 : 0"
|
||||
:waiting-count="waitingCount"
|
||||
:terlambat-count="(terlambatPatients || []).length"
|
||||
:pending-count="(pendingPatients || []).length"
|
||||
:show-diproses="false"
|
||||
:fast-track-options="fastTrackOptions"
|
||||
@action="handleTableAction"
|
||||
<v-text-field
|
||||
v-model="klinikRuangSearch"
|
||||
placeholder="Cari Klinik Ruang..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-expansion-panels class="expansion-panels">
|
||||
<v-expansion-panel
|
||||
v-for="klinikRuang in filteredKlinikRuang"
|
||||
:key="klinikRuang.id"
|
||||
class="expansion-panel"
|
||||
>
|
||||
<v-expansion-panel-title class="expansion-title">
|
||||
<div class="expansion-header">
|
||||
<v-chip size="small" color="warning" class="mr-2">
|
||||
{{ klinikRuang.kodeKlinik }}
|
||||
</v-chip>
|
||||
<span class="body-2 text-semibold">{{ klinikRuang.namaKlinik }}</span>
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-chip size="x-small" variant="outlined">
|
||||
{{ klinikRuang.ruangList.length }} Ruang
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="ruang-card-grid">
|
||||
<div
|
||||
v-for="ruang in klinikRuang.ruangList"
|
||||
:key="ruang.nomorRuang"
|
||||
class="ruang-card-item"
|
||||
@click="buatAntreanKlinikRuang(klinikRuang, ruang)"
|
||||
>
|
||||
<v-icon color="warning" size="24" class="mb-2">mdi-door</v-icon>
|
||||
<div class="ruang-card-name">Ruang {{ ruang.nomorRuang }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Klinik Dialog -->
|
||||
<SelectionDialog
|
||||
v-model="showKlinikDialog"
|
||||
title="Pilih Klinik"
|
||||
:items="filteredKliniks"
|
||||
v-model:search-query="klinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="selectKlinik"
|
||||
/>
|
||||
|
||||
<!-- Penunjang Dialog -->
|
||||
<SelectionDialog
|
||||
v-model="showPenunjangDialog"
|
||||
title="Pilih Penunjang"
|
||||
:items="filteredPenunjangs"
|
||||
v-model:search-query="penunjangSearch"
|
||||
search-placeholder="Cari penunjang..."
|
||||
@select="selectPenunjang"
|
||||
/>
|
||||
|
||||
<!-- Change Klinik Dialog -->
|
||||
<SelectionDialog
|
||||
v-model="showChangeKlinikDialog"
|
||||
title="Ubah Klinik"
|
||||
:items="filteredChangeKliniks"
|
||||
v-model:search-query="changeKlinikSearch"
|
||||
search-placeholder="Cari klinik..."
|
||||
@select="changeKlinik"
|
||||
/>
|
||||
|
||||
<!-- Dialog Klinik Ruang -->
|
||||
<v-dialog v-model="showKlinikRuangDialog" max-width="900px" scrollable>
|
||||
<v-card class="dialog-card">
|
||||
<v-card-title class="dialog-header dialog-header-warning">
|
||||
<span class="headline-4">Pilih Klinik Ruang</span>
|
||||
<v-btn icon variant="text" size="small" class="btn-close" @click="closeKlinikRuangDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text class="dialog-content">
|
||||
<div v-if="currentProcessingPatient" class="patient-card mb-4">
|
||||
<h4 class="headline-5 mb-3">Informasi Pasien</h4>
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<div class="detail-item">
|
||||
<span class="caption-2 text-muted">Barcode</span>
|
||||
<span class="body-2 text-semibold">{{ currentProcessingPatient.barcode }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="detail-item">
|
||||
<span class="caption-2 text-muted">No. Antrian</span>
|
||||
<span class="body-2 text-semibold">{{ currentProcessingPatient.noAntrian }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="klinikRuangSearch"
|
||||
placeholder="Cari Klinik Ruang..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-expansion-panels class="expansion-panels">
|
||||
<v-expansion-panel
|
||||
v-for="klinikRuang in filteredKlinikRuang"
|
||||
:key="klinikRuang.id"
|
||||
class="expansion-panel"
|
||||
>
|
||||
<v-expansion-panel-title class="expansion-title">
|
||||
<div class="expansion-header">
|
||||
<v-chip size="small" color="warning" class="mr-2">
|
||||
{{ klinikRuang.kodeKlinik }}
|
||||
</v-chip>
|
||||
<span class="body-2 text-semibold">{{ klinikRuang.namaKlinik }}</span>
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-chip size="x-small" variant="outlined">
|
||||
{{ klinikRuang.ruangList.length }} Ruang
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="ruang-card-grid">
|
||||
<div
|
||||
v-for="ruang in klinikRuang.ruangList"
|
||||
:key="ruang.nomorRuang"
|
||||
class="ruang-card-item"
|
||||
@click="buatAntreanKlinikRuang(klinikRuang, ruang)"
|
||||
>
|
||||
<v-icon color="warning" size="24" class="mb-2">mdi-door</v-icon>
|
||||
<div class="ruang-card-name">Ruang {{ ruang.nomorRuang }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<AppSnackbar
|
||||
v-model="snackbar"
|
||||
:message="snackbarText"
|
||||
:color="snackbarColor"
|
||||
/>
|
||||
<!-- Snackbar -->
|
||||
<AppSnackbar
|
||||
v-model="snackbar"
|
||||
:message="snackbarText"
|
||||
:color="snackbarColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useQueue } from "@/composables/useQueue";
|
||||
import { useQueueStore } from "@/stores/queueStore";
|
||||
import { useMasterStore } from "@/stores/masterStore";
|
||||
import { useLoketStore } from "@/stores/loketStore";
|
||||
import PageHeader from "@/components/common/PageHeader.vue";
|
||||
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
|
||||
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
|
||||
@@ -213,11 +226,23 @@ import SelectionDialog from "@/components/common/SelectionDialog.vue";
|
||||
import AppSnackbar from "@/components/common/AppSnackbar.vue";
|
||||
import { useThermalPrint } from "@/composables/useThermalPrint";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const masterStore = useMasterStore();
|
||||
const queueStore = useQueueStore();
|
||||
const loketStore = useLoketStore();
|
||||
const { printTicketFromPatient } = useThermalPrint();
|
||||
|
||||
const {
|
||||
// Broadcast Channel
|
||||
let broadcastChannel = null;
|
||||
|
||||
const loketId = computed(() => route.params.id);
|
||||
const loketName = computed(() => {
|
||||
const loket = loketStore.getLoketById(parseInt(loketId.value));
|
||||
return loket ? loket.namaLoket : 'Loket';
|
||||
});
|
||||
|
||||
const {
|
||||
snackbar,
|
||||
snackbarText,
|
||||
snackbarColor,
|
||||
@@ -296,72 +321,46 @@ const fastTrackOptions = computed(() => {
|
||||
});
|
||||
|
||||
// Combine all patients with status - PRESERVE ALL PROPERTIES
|
||||
// Hanya tampilkan pasien yang sudah dipanggil (bukan status "menunggu")
|
||||
const allPatientsForStage = computed(() => {
|
||||
const currentPatientNo = currentProcessingPatient.value?.no;
|
||||
|
||||
// Pasien yang sudah di loket (sudah check-in)
|
||||
const diLoket = (diLoketPatients.value || []).map((p) => ({
|
||||
...p, // Spread all properties first
|
||||
// If this patient is currently being processed, mark as "diproses", otherwise "diloket"
|
||||
...p,
|
||||
status: p.no === currentPatientNo ? "diproses" : "diloket",
|
||||
}));
|
||||
|
||||
// Pasien terlambat
|
||||
const terlambat = (terlambatPatients.value || []).map((p) => ({
|
||||
...p,
|
||||
status: "terlambat",
|
||||
}));
|
||||
|
||||
// Pasien pending
|
||||
const pending = (pendingPatients.value || []).map((p) => ({
|
||||
...p,
|
||||
status: "pending",
|
||||
}));
|
||||
|
||||
// Pasien dengan status "waiting" (sudah dipanggil tapi belum check-in)
|
||||
// Ini adalah kategori baru "Menunggu" di tabel Admin Loket
|
||||
const waiting = (waitingPatients.value || []).map((p) => ({
|
||||
...p,
|
||||
status: "waiting", // Tampilkan sebagai "waiting" atau "menunggu" di UI
|
||||
status: "waiting",
|
||||
}));
|
||||
|
||||
// Jangan tampilkan pasien dengan status "menunggu" (belum dipanggil) di tabel
|
||||
// Hanya tampilkan pasien yang sudah dipanggil oleh admin loket
|
||||
const combined = [...diLoket, ...waiting, ...terlambat, ...pending];
|
||||
|
||||
// Debug: check if fastTrack property exists
|
||||
console.log("📊 AdminLoket - allPatientsForStage:", combined.length);
|
||||
if (combined.length > 0) {
|
||||
console.log("📊 First patient:", combined[0]);
|
||||
console.log("📊 First patient has fastTrack?", "fastTrack" in combined[0]);
|
||||
console.log("📊 First patient fastTrack value:", combined[0].fastTrack);
|
||||
console.log(
|
||||
"📊 All fastTrack values:",
|
||||
combined.map((p) => p.fastTrack)
|
||||
);
|
||||
}
|
||||
|
||||
return combined;
|
||||
});
|
||||
|
||||
// Count diLoket patients (excluding currently processing)
|
||||
const diLoketCount = computed(() => {
|
||||
const currentPatientNo = currentProcessingPatient.value?.no;
|
||||
return (diLoketPatients.value || []).filter(p => p.no !== currentPatientNo).length;
|
||||
});
|
||||
|
||||
// Count menunggu patients (belum dipanggil)
|
||||
const menungguCount = computed(() => {
|
||||
return (menungguPatients.value || []).length;
|
||||
});
|
||||
|
||||
// Count waiting patients (sudah dipanggil tapi belum check-in)
|
||||
const waitingCount = computed(() => {
|
||||
return (waitingPatients.value || []).length;
|
||||
});
|
||||
|
||||
// Next queue info for CurrentPatientCard
|
||||
const nextQueueInfo = computed(() => {
|
||||
const currentPatientNo = currentProcessingPatient.value?.no;
|
||||
const nextPatient = (diLoketPatients.value || []).find(p => p.no !== currentPatientNo) || (diLoketPatients.value || [])[0];
|
||||
@@ -380,6 +379,11 @@ const handlePatientAction = (action) => {
|
||||
const handleCall = (count) => {
|
||||
if (count === 1) {
|
||||
callNext();
|
||||
// Broadcast the call event after calling next
|
||||
// Note: callNext updates the store, so we can pick up the new patient from there
|
||||
// But we probably want to wait a tick or ensure store is updated.
|
||||
// For simplicity, handleCallPatient below sends the explicit broadcast.
|
||||
// Here we might just be moving status from 'menunggu' to 'waiting'.
|
||||
} else {
|
||||
callMultiplePatients(count);
|
||||
}
|
||||
@@ -394,13 +398,22 @@ const handleProcessNext = () => {
|
||||
};
|
||||
|
||||
const handleCallPatient = () => {
|
||||
// Panggil pasien yang sedang diproses
|
||||
if (currentProcessingPatient.value) {
|
||||
const result = queueStore.callProcessingPatient('loket');
|
||||
if (result.success) {
|
||||
snackbarText.value = result.message;
|
||||
snackbarColor.value = "success";
|
||||
snackbar.value = true;
|
||||
|
||||
// BROADCAST CALL EVENT
|
||||
if (broadcastChannel) {
|
||||
broadcastChannel.postMessage({
|
||||
type: 'CALL_PATIENT',
|
||||
patient: JSON.parse(JSON.stringify(currentProcessingPatient.value)),
|
||||
loketId: loketId.value,
|
||||
loketName: loketName.value
|
||||
});
|
||||
}
|
||||
} else {
|
||||
snackbarText.value = result.message;
|
||||
snackbarColor.value = "error";
|
||||
@@ -421,19 +434,24 @@ const updateStickyOffset = () => {
|
||||
if (header) {
|
||||
const headerHeight = header.offsetHeight;
|
||||
const containerPadding = 16;
|
||||
stickyTopOffset.value = headerHeight + containerPadding + 8; // header + padding + minimal space
|
||||
stickyTopOffset.value = headerHeight + containerPadding + 8;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateStickyOffset();
|
||||
window.addEventListener('resize', updateStickyOffset);
|
||||
// Use nextTick to ensure DOM is fully rendered
|
||||
setTimeout(updateStickyOffset, 100);
|
||||
|
||||
// Initialize Broadcast Channel
|
||||
broadcastChannel = new BroadcastChannel('antrian-loket-channel');
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateStickyOffset);
|
||||
if (broadcastChannel) {
|
||||
broadcastChannel.close();
|
||||
}
|
||||
});
|
||||
|
||||
const closeKlinikRuangDialog = () => {
|
||||
@@ -442,33 +460,30 @@ const closeKlinikRuangDialog = () => {
|
||||
};
|
||||
|
||||
const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
|
||||
// Pastikan currentProcessingPatient valid, jika tidak, coba dapatkan dari store
|
||||
let patient = currentProcessingPatient.value;
|
||||
// ... (keeping existing logic to find patient)
|
||||
if (!patient) {
|
||||
try {
|
||||
// Coba dapatkan pasien yang sedang diproses dari store
|
||||
const processingPatient = queueStore.currentProcessingPatient?.loket;
|
||||
if (processingPatient) {
|
||||
// Dapatkan data terbaru dari allPatients langsung (bukan dari getPatientsByStage yang sudah difilter)
|
||||
// allPatients adalah ref yang di-export dari Pinia store, bisa diakses langsung atau dengan .value
|
||||
let allPatients = [];
|
||||
if (queueStore.allPatients) {
|
||||
// Jika allPatients adalah ref (punya .value)
|
||||
allPatients = Array.isArray(queueStore.allPatients)
|
||||
? queueStore.allPatients
|
||||
: (queueStore.allPatients.value || []);
|
||||
// ... fallback logic as in original file
|
||||
// simplified for brevity in this manual copy, but we should keep original robustness
|
||||
try {
|
||||
const processingPatient = queueStore.currentProcessingPatient?.loket;
|
||||
if (processingPatient) {
|
||||
let allPatients = [];
|
||||
if (queueStore.allPatients) {
|
||||
allPatients = Array.isArray(queueStore.allPatients)
|
||||
? queueStore.allPatients
|
||||
: (queueStore.allPatients.value || []);
|
||||
}
|
||||
const latestPatient = allPatients.find(
|
||||
p => (p && p.no === processingPatient.no) ||
|
||||
(p && p.barcode && p.barcode === processingPatient.barcode) ||
|
||||
(p && p.noAntrian && p.noAntrian === processingPatient.noAntrian)
|
||||
);
|
||||
patient = latestPatient || processingPatient;
|
||||
}
|
||||
|
||||
const latestPatient = allPatients.find(
|
||||
p => (p && p.no === processingPatient.no) ||
|
||||
(p && p.barcode && p.barcode === processingPatient.barcode) ||
|
||||
(p && p.noAntrian && p.noAntrian === processingPatient.noAntrian)
|
||||
);
|
||||
patient = latestPatient || processingPatient;
|
||||
} catch (error) {
|
||||
console.error('Error getting patient from store:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting patient from store:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!patient) {
|
||||
@@ -487,7 +502,6 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
|
||||
);
|
||||
|
||||
if (result.success && result.patient) {
|
||||
// Print ticket dengan nomor antrian baru
|
||||
try {
|
||||
await printTicketFromPatient(result.patient);
|
||||
} catch (error) {
|
||||
@@ -715,6 +729,7 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-neutral-700);
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.expansion-panels {
|
||||
@@ -798,30 +813,4 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
|
||||
line-height: 14px;
|
||||
color: var(--color-neutral-600);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.loket-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sticky-wrapper {
|
||||
position: relative;
|
||||
top: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader
|
||||
icon="mdi-counter"
|
||||
title="Admin Loket"
|
||||
subtitle="Pilih Loket untuk mengelola antrian"
|
||||
:show-add-button="false"
|
||||
theme="primary"
|
||||
/>
|
||||
|
||||
<div class="admin-loket-index">
|
||||
<v-row class="mt-4">
|
||||
<v-col
|
||||
v-for="loket in loketList"
|
||||
:key="loket.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<v-card
|
||||
class="loket-card"
|
||||
elevation="2"
|
||||
@click="navigateToLoket(loket.id)"
|
||||
>
|
||||
<v-card-text class="text-center pa-6">
|
||||
<v-icon size="64" color="primary" class="mb-4">mdi-desktop-mac</v-icon>
|
||||
<div class="loket-name">{{ loket.namaLoket }}</div>
|
||||
<v-chip size="small" :color="loket.loketAktif ? 'success' : 'grey'" class="mt-2">
|
||||
{{ loket.loketAktif ? 'Aktif' : 'Non-Aktif' }}
|
||||
</v-chip>
|
||||
<div class="loket-info mt-3">
|
||||
<v-icon size="16" class="mr-1">mdi-account-group</v-icon>
|
||||
{{ loket.pembayaran }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div v-if="loketList.length === 0" class="empty-state">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-desktop-mac-dashboard</v-icon>
|
||||
<div class="empty-text">Tidak ada data loket yang tersedia</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useLoketStore } from '@/stores/loketStore';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const loketStore = useLoketStore();
|
||||
|
||||
// Ensure data is loaded
|
||||
onMounted(() => {
|
||||
if (loketStore.fetchLoketFromAPI) {
|
||||
loketStore.fetchLoketFromAPI();
|
||||
}
|
||||
});
|
||||
|
||||
const loketList = computed(() => {
|
||||
return loketStore.loketData || [];
|
||||
});
|
||||
|
||||
const navigateToLoket = (id) => {
|
||||
router.push(`/adminloket/${id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-loket-index {
|
||||
background: var(--color-neutral-300);
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Remove rounded edges from PageHeader component */
|
||||
:deep(.page-header) {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.loket-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-primary-600);
|
||||
}
|
||||
}
|
||||
|
||||
.loket-name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--color-neutral-900);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.loket-info {
|
||||
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>
|
||||
@@ -201,6 +201,8 @@ const clinicStore = useClinicStore()
|
||||
const currentTime = ref('')
|
||||
const currentDate = ref('')
|
||||
let timeInterval = null
|
||||
let broadcastChannel = null
|
||||
const broadcastedPatient = ref(null)
|
||||
|
||||
// Get loket ID from route
|
||||
const loketId = computed(() => {
|
||||
@@ -665,6 +667,11 @@ const isInTTSWindow = (queue) => {
|
||||
// Current called queue - tiket yang sedang diproses di admin loket
|
||||
// Nomor antrian menjadi "dipanggil" jika diproses pada AdminLoket DAN sudah dipanggil oleh admin
|
||||
const currentCalledQueue = computed(() => {
|
||||
// Prioritas 0: Broadcasted patient (Real-time from BroadcastChannel)
|
||||
if (broadcastedPatient.value) {
|
||||
return broadcastedPatient.value
|
||||
}
|
||||
|
||||
// Prioritas 1: Pasien yang sedang diproses di admin loket (currentProcessingPatient)
|
||||
// Hanya tampilkan jika sudah dipanggil eksplisit oleh admin (calledByAdmin === true)
|
||||
if (currentProcessingPatient.value) {
|
||||
@@ -926,11 +933,71 @@ onMounted(() => {
|
||||
|
||||
// Langsung start timer tanpa menunggu fetch
|
||||
updateTime()
|
||||
// Init BroadcastChannel
|
||||
try {
|
||||
broadcastChannel = new BroadcastChannel('antrian-loket-channel')
|
||||
broadcastChannel.onmessage = (event) => {
|
||||
console.log('Anjungan received broadcast:', event.data)
|
||||
if (event.data && event.data.type === 'CALL_PATIENT') {
|
||||
const { patient, loketId: senderLoketId } = event.data
|
||||
|
||||
// Cek apakah event ini untuk loket ini
|
||||
// Gunakan loose equality untuk handle string vs number
|
||||
if (loketId.value) {
|
||||
if (String(loketId.value) != String(senderLoketId)) {
|
||||
console.log('Skipping broadcast for different loket:', senderLoketId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Update local state untuk prioritas tampilan hero
|
||||
broadcastedPatient.value = patient
|
||||
|
||||
// PENTING: Update Store agar list antrian juga berubah
|
||||
// 1. Update currentProcessingPatient
|
||||
if (!queueStore.currentProcessingPatient) {
|
||||
queueStore.currentProcessingPatient = {}
|
||||
}
|
||||
queueStore.currentProcessingPatient.loket = patient
|
||||
|
||||
// 2. Update status patient di allPatients list jika ada
|
||||
if (queueStore.allPatients && Array.isArray(queueStore.allPatients)) {
|
||||
const idx = queueStore.allPatients.findIndex(p =>
|
||||
p.no === patient.no ||
|
||||
p.noAntrian === patient.noAntrian
|
||||
)
|
||||
|
||||
if (idx !== -1) {
|
||||
// Update existing patient data
|
||||
// Kita update status menjadi 'di-loket' dan lastCalledAt
|
||||
const updatedPatient = {
|
||||
...queueStore.allPatients[idx],
|
||||
...patient,
|
||||
status: 'di-loket', // Pastikan status sinkron
|
||||
lastCalledAt: patient.lastCalledAt || new Date().toISOString()
|
||||
}
|
||||
queueStore.allPatients[idx] = updatedPatient
|
||||
} else {
|
||||
// Opsional: Jika pasien tidak ditemukan di list (misal data awal kosong), tambahkan?
|
||||
// Sebaiknya jangan sembarang nambah, tapi untuk robustnes UI boleh saja
|
||||
// queueStore.allPatients.push(patient)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Store updated from broadcast')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('BroadcastChannel error:', e)
|
||||
}
|
||||
|
||||
// Start TTS checker
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
if (broadcastChannel) broadcastChannel.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user