Files
web-antrean/pages/AdminKlinik.vue
T
2026-01-05 09:26:14 +07:00

419 lines
11 KiB
Vue

<template>
<div class="loket-container">
<!-- Compact Header -->
<PageHeader
icon="mdi-hospital-building"
title="Admin Klinik"
:subtitle="currentDate"
:show-add-button="false"
theme="secondary"
/>
<!-- 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"
theme="secondary"
: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">
<!-- Status Filter -->
<v-chip-group v-model="selectedStatus" mandatory class="status-filter">
<v-chip value="all" :class="{ 'active-chip': selectedStatus === 'all' }">
Semua ({{ allPatients.length }})
</v-chip>
<v-chip value="diloket" :class="{ 'active-chip': selectedStatus === 'diloket' }">
Di Klinik ({{ (diLoketPatients || []).length }})
</v-chip>
<v-chip value="terlambat" :class="{ 'active-chip': selectedStatus === 'terlambat' }">
Terlambat ({{ (terlambatPatients || []).length }})
</v-chip>
<v-chip value="pending" :class="{ 'active-chip': selectedStatus === 'pending' }">
Pending ({{ (pendingPatients || []).length }})
</v-chip>
</v-chip-group>
<!-- Fast Track Filter -->
<v-select
v-model="selectedFastTrack"
:items="fastTrackOptions"
label="Filter Fast Track"
density="compact"
hide-details
clearable
class="fast-track-filter"
variant="outlined"
>
<template #prepend-inner>
<v-icon size="20">mdi-flash</v-icon>
</template>
</v-select>
<!-- 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="allPatients"
v-model:selected-status="selectedStatus"
v-model:search-query="searchQuery"
v-model:selected-fast-track="selectedFastTrack"
:di-loket-count="(diLoketPatients || []).length"
:terlambat-count="(terlambatPatients || []).length"
:pending-count="(pendingPatients || []).length"
:status-labels="statusLabels"
:show-diproses="false"
@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,
processNextQueue,
selectKlinik,
selectPenunjang,
openPenunjangDialog,
changeKlinik,
} = useQueue("klinik");
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 = allPatients.value
.map((p) => (p.fastTrack ?? "").toString().trim().toUpperCase())
.filter((v) => v.length > 0);
const uniqueTracks = [...new Set(normalizedValues)];
return uniqueTracks.sort();
});
// Custom status labels for Klinik
const statusLabels = {
all: 'Semua',
diloket: 'Di Klinik',
terlambat: 'Terlambat',
pending: 'Pending'
};
// Combine all patients dengan status
const allPatients = computed(() => {
try {
const diLoketList = diLoketPatients.value || [];
const terlambatList = terlambatPatients.value || [];
const pendingList = pendingPatients.value || [];
const diLoket = diLoketList.map(p => ({ ...p, status: 'diloket' }));
const terlambat = terlambatList.map(p => ({ ...p, status: 'terlambat' }));
const pending = pendingList.map(p => ({ ...p, status: 'pending' }));
return [...diLoket, ...terlambat, ...pending];
} catch (error) {
console.error('Error in allPatients computed:', error);
return [];
}
});
// Next queue info untuk CurrentPatientCard (Admin Klinik)
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 .create-btn {
text-transform: none;
font-weight: 600;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.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-secondary-600);
color: var(--color-neutral-100);
border-color: var(--color-secondary-600);
}
.fast-track-filter {
min-width: 180px;
max-width: 220px;
}
.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,
.fast-track-filter {
max-width: 100%;
min-width: 100%;
}
}
</style>