394 lines
10 KiB
Vue
394 lines
10 KiB
Vue
<template>
|
|
<div class="loket-container">
|
|
<!-- Compact Header -->
|
|
<PageHeader
|
|
icon="mdi-view-dashboard"
|
|
title="Admin Loket"
|
|
:subtitle="currentDate"
|
|
:show-add-button="false"
|
|
theme="primary"
|
|
/>
|
|
|
|
<!-- Main Content Grid -->
|
|
<v-row class="content-grid" dense>
|
|
<!-- Left Column: Current Patient & Queue Actions -->
|
|
<v-col cols="12" md="5">
|
|
<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"
|
|
/>
|
|
|
|
<!-- Queue Actions Card -->
|
|
<QueueActionsCard
|
|
class="mt-3"
|
|
:total-quota="150"
|
|
:used-quota="quotaUsed"
|
|
:has-next="!!nextPatient"
|
|
@call="handleCall"
|
|
/>
|
|
|
|
<!-- Create Queue Buttons -->
|
|
<div class="create-buttons mt-3">
|
|
<v-row no-gutters>
|
|
<v-col cols="6" class="pr-2">
|
|
<v-btn
|
|
block
|
|
class="py-6"
|
|
color="primary-600"
|
|
:disabled="!currentProcessingPatient"
|
|
@click="showKlinikDialog = true"
|
|
>
|
|
<v-icon start>mdi-hospital-building</v-icon>
|
|
Buat Antrean Klinik
|
|
</v-btn>
|
|
</v-col>
|
|
|
|
<v-col cols="6" class="pl-2">
|
|
<v-btn
|
|
block
|
|
class="py-6 text-white"
|
|
color="secondary-600"
|
|
:disabled="!currentProcessingPatient"
|
|
@click="openPenunjangDialog()"
|
|
>
|
|
<v-icon start>mdi-clipboard-pulse</v-icon>
|
|
Buat Antrean Penunjang
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
</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">DATA 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"
|
|
: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"
|
|
/>
|
|
|
|
<!-- Snackbar -->
|
|
<AppSnackbar
|
|
v-model="snackbar"
|
|
:message="snackbarText"
|
|
:color="snackbarColor"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from "vue";
|
|
import { useQueue } from "@/composables/useQueue";
|
|
import PageHeader from "@/components/common/PageHeader.vue";
|
|
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
|
|
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
|
|
import PatientDataTable from "@/components/features/queue/TabelPatientData.vue";
|
|
import SelectionDialog from "@/components/common/SelectionDialog.vue";
|
|
import AppSnackbar from "@/components/common/AppSnackbar.vue";
|
|
|
|
const {
|
|
snackbar,
|
|
snackbarText,
|
|
snackbarColor,
|
|
showKlinikDialog,
|
|
showPenunjangDialog,
|
|
showChangeKlinikDialog,
|
|
klinikSearch,
|
|
penunjangSearch,
|
|
changeKlinikSearch,
|
|
currentProcessingPatient,
|
|
diLoketPatients,
|
|
terlambatPatients,
|
|
pendingPatients,
|
|
nextPatient,
|
|
quotaUsed,
|
|
filteredKliniks,
|
|
filteredPenunjangs,
|
|
filteredChangeKliniks,
|
|
callNext,
|
|
callMultiplePatients,
|
|
processPatient,
|
|
selectKlinik,
|
|
selectPenunjang,
|
|
openPenunjangDialog,
|
|
changeKlinik,
|
|
processNextQueue,
|
|
} = useQueue("loket");
|
|
|
|
const currentDate = ref(
|
|
new Date().toLocaleDateString("id-ID", {
|
|
weekday: "long",
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
})
|
|
);
|
|
|
|
const selectedStatus = ref("all");
|
|
const searchQuery = ref("");
|
|
const selectedFastTrack = ref(null);
|
|
|
|
// Fast Track options from all patients
|
|
const fastTrackOptions = computed(() => {
|
|
const normalizedValues = allPatientsForStage.value
|
|
.map((p) => (p.fastTrack ?? "").toString().trim().toUpperCase())
|
|
.filter((v) => v.length > 0);
|
|
const uniqueTracks = [...new Set(normalizedValues)];
|
|
return uniqueTracks.sort();
|
|
});
|
|
|
|
// Combine all patients with status - PRESERVE ALL PROPERTIES
|
|
const allPatientsForStage = computed(() => {
|
|
const currentPatientNo = currentProcessingPatient.value?.no;
|
|
|
|
const diLoket = (diLoketPatients.value || []).map((p) => ({
|
|
...p, // Spread all properties first
|
|
// If this patient is currently being processed, mark as "diproses", otherwise "diloket"
|
|
status: p.no === currentPatientNo ? "diproses" : "diloket",
|
|
}));
|
|
const terlambat = (terlambatPatients.value || []).map((p) => ({
|
|
...p,
|
|
status: "terlambat",
|
|
}));
|
|
const pending = (pendingPatients.value || []).map((p) => ({
|
|
...p,
|
|
status: "pending",
|
|
}));
|
|
|
|
// Do not show waiting patients in the table until called
|
|
const combined = [...diLoket, ...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;
|
|
});
|
|
|
|
// 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];
|
|
if (nextPatient) {
|
|
return `Antrian berikutnya: ${nextPatient.noAntrian.split(" |")[0]}`;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const handlePatientAction = (action) => {
|
|
if (currentProcessingPatient.value) {
|
|
processPatient(currentProcessingPatient.value, action);
|
|
}
|
|
};
|
|
|
|
const handleCall = (count) => {
|
|
if (count === 1) {
|
|
callNext();
|
|
} else {
|
|
callMultiplePatients(count);
|
|
}
|
|
};
|
|
|
|
const handleTableAction = (item, action) => {
|
|
processPatient(item, action);
|
|
};
|
|
|
|
const handleProcessNext = () => {
|
|
processNextQueue();
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.loket-container {
|
|
background: var(--color-neutral-300);
|
|
min-height: 100vh;
|
|
padding: 16px;
|
|
}
|
|
|
|
.content-grid {
|
|
margin: 0 -8px;
|
|
}
|
|
|
|
.content-grid > .v-col {
|
|
padding: 8px;
|
|
}
|
|
|
|
.create-buttons .v-btn {
|
|
text-transform: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sticky-wrapper {
|
|
position: sticky;
|
|
top: 16px;
|
|
align-self: flex-start;
|
|
max-height: calc(100vh - 32px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.filters {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.status-filter {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
}
|
|
|
|
.status-filter .v-chip {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
height: 32px;
|
|
border: 1px solid var(--color-neutral-500);
|
|
background: var(--color-neutral-100);
|
|
color: var(--color-neutral-600);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.status-filter .v-chip:hover {
|
|
background: var(--color-neutral-300);
|
|
}
|
|
|
|
.status-filter .v-chip.active-chip {
|
|
background: var(--color-primary-600);
|
|
color: var(--color-neutral-100);
|
|
border-color: var(--color-primary-600);
|
|
}
|
|
|
|
.search-field {
|
|
max-width: 300px;
|
|
min-width: 250px;
|
|
}
|
|
|
|
.section-label {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
color: var(--color-neutral-600);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.data-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.patient-data-container {
|
|
border-radius: 12px;
|
|
border: 1px solid var(--color-neutral-500);
|
|
background: var(--color-neutral-100);
|
|
}
|
|
|
|
@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>
|