Files
web-antrean/pages/AdminKlinik.vue
T
2026-01-27 15:21:42 +07:00

644 lines
17 KiB
Vue

<template>
<div>
<!-- Compact Header -->
<PageHeader
icon="mdi-hospital-building"
title="Admin Klinik"
:subtitle="currentDate"
:show-add-button="false"
theme="primary"
/>
<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">
<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"
@call="handleCallPatient"
/>
<!-- Queue Actions Card -->
<QueueActionsCard
class="mt-3"
:total-quota="150"
:used-quota="quotaUsed"
:menunggu-count="menungguCount"
: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="warning"
:disabled="!currentProcessingPatient"
@click="openKlinikRuangDialog()"
>
<v-icon start>mdi-door</v-icon>
Buat Antrean Ruang
</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">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="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"
: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" persistent 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-list">
<div
v-for="ruang in klinikRuang.ruangList"
:key="ruang.nomorRuang"
class="ruang-item"
@click="buatAntreanKlinikRuang(klinikRuang, ruang)"
>
<v-icon color="warning" size="20">mdi-door</v-icon>
<div class="ruang-info">
<span class="body-3 text-semibold">Ruang {{ ruang.nomorRuang }} - {{ ruang.namaRuang }}</span>
<span class="caption-2 text-muted">Screen: {{ ruang.nomorScreen }}</span>
</div>
<v-icon size="20" color="grey">mdi-chevron-right</v-icon>
</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"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { useQueue } from "@/composables/useQueue";
import { useQueueStore } from "@/stores/queueStore";
import { useMasterStore } from "@/stores/masterStore";
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 masterStore = useMasterStore();
const queueStore = useQueueStore();
const {
snackbar,
snackbarText,
snackbarColor,
showKlinikDialog,
showPenunjangDialog,
showChangeKlinikDialog,
klinikSearch,
penunjangSearch,
changeKlinikSearch,
currentProcessingPatient,
diLoketPatients,
terlambatPatients,
pendingPatients,
nextPatient,
menungguCount,
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);
// Dialog Klinik Ruang
const showKlinikRuangDialog = ref(false);
const klinikRuangSearch = ref("");
// Get klinik ruang data from masterStore
const klinikRuangList = computed(() => {
return masterStore.ruangData || [];
});
const filteredKlinikRuang = computed(() => {
if (!klinikRuangSearch.value) return klinikRuangList.value;
return klinikRuangList.value.filter(
(k) =>
k.namaKlinik.toLowerCase().includes(klinikRuangSearch.value.toLowerCase()) ||
k.kodeKlinik.toLowerCase().includes(klinikRuangSearch.value.toLowerCase())
);
});
// 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();
};
const handleCallPatient = () => {
// TODO: Integrate text-to-speech library here
// Example: speak(`Nomor antrian ${currentProcessingPatient.value?.noAntrian.split(" |")[0]}, silakan menuju ke loket`)
if (currentProcessingPatient.value) {
console.log('Calling patient:', currentProcessingPatient.value);
// Placeholder for text-to-speech integration
}
};
const openKlinikRuangDialog = () => {
showKlinikRuangDialog.value = true;
};
const closeKlinikRuangDialog = () => {
showKlinikRuangDialog.value = false;
klinikRuangSearch.value = "";
};
const buatAntreanKlinikRuang = (klinikRuang, ruang) => {
if (!currentProcessingPatient.value) return;
const result = queueStore.createAntreanKlinikRuang(
klinikRuang,
ruang,
currentProcessingPatient.value,
"klinik"
);
snackbarText.value = result.message;
snackbarColor.value = result.success ? "success" : "error";
snackbar.value = true;
closeKlinikRuangDialog();
};
</script>
<style scoped lang="scss">
.loket-container {
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;
}
.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;
}
.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);
}
/* Dialog Klinik Ruang Styles */
.dialog-card {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.dialog-header {
background: linear-gradient(135deg, var(--color-warning-600) 0%, var(--color-primary-700) 100%);
color: var(--color-neutral-100);
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-header-warning {
background: linear-gradient(135deg, var(--color-warning-600) 0%, var(--color-primary-700) 100%);
}
.headline-4 {
font-size: 20px;
line-height: 28px;
font-weight: 600;
margin: 0;
}
.headline-5 {
font-size: 18px;
line-height: 24px;
font-weight: 600;
margin: 0;
color: var(--color-neutral-900);
}
.btn-close {
color: var(--color-neutral-100) !important;
}
.dialog-content {
padding: 24px !important;
background: var(--color-neutral-300);
}
.patient-card {
background: var(--color-neutral-100);
border-radius: 12px;
padding: 20px;
border: 1px solid var(--color-neutral-400);
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.caption-2 {
font-size: 12px;
line-height: 16px;
font-weight: 400;
}
.body-2 {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.body-3 {
font-size: 14px;
line-height: 20px;
font-weight: 400;
}
.text-semibold {
font-weight: 600 !important;
}
.text-muted {
color: var(--color-neutral-700);
}
.expansion-panels {
background: transparent;
}
.expansion-panel {
background: var(--color-neutral-100);
border: 1px solid var(--color-neutral-400);
border-radius: 12px !important;
margin-bottom: 12px;
overflow: hidden;
}
.expansion-panel::before {
box-shadow: none !important;
}
.expansion-title {
background: var(--color-neutral-100);
min-height: 60px;
}
.expansion-header {
display: flex;
align-items: center;
}
.ruang-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.ruang-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-neutral-300);
border: 1px solid var(--color-neutral-400);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.ruang-item:hover {
border-color: var(--color-warning-600);
background: rgba(255, 185, 95, 0.15);
}
.ruang-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
@media (max-width: 960px) {
.loket-container {
padding: 12px;
}
.sticky-wrapper {
position: relative;
top: 0;
max-height: none;
}
.filters {
flex-direction: column;
align-items: stretch;
}
.search-field {
max-width: 100%;
min-width: 100%;
}
}
</style>