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

699 lines
18 KiB
Vue

<!-- pages/AdminPenunjang.vue -->
<template>
<div class="loket-container">
<!-- Compact Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<v-icon size="28" color="white">mdi-clipboard-pulse</v-icon>
<div class="header-text">
<h1 class="page-title">Admin Penunjang</h1>
<p class="page-subtitle">{{ currentDate }}</p>
</div>
</div>
</div>
</div>
<!-- Main Content Grid -->
<v-row class="content-grid" dense>
<!-- Left Column: Current Patient -->
<v-col cols="12" md="5">
<div class="sticky-wrapper">
<v-card class="current-patient-card" elevation="0">
<v-card-text class="pa-4">
<div class="section-label mb-3">SEDANG DIPROSES</div>
<div v-if="currentProcessingPatient" class="patient-details">
<div class="patient-number mb-2">{{ currentProcessingPatient.noAntrian.split(" |")[0] }}</div>
<div class="patient-info-text mb-3">
<div>{{ currentProcessingPatient.barcode }}</div>
<div>{{ currentProcessingPatient.klinik }} | {{ currentProcessingPatient.pembayaran }}</div>
</div>
<div class="action-grid">
<v-btn color="#10b981" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'check-in')">
<v-icon start size="20">mdi-check</v-icon>
Selesai
</v-btn>
<v-btn color="#f59e0b" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'terlambat')">
<v-icon start size="20">mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn color="#ef4444" variant="flat" block size="large" @click="processPatient(currentProcessingPatient, 'pending')">
<v-icon start size="20">mdi-pause</v-icon>
Pending
</v-btn>
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showChangeKlinikDialog = true">
<v-icon start size="20">mdi-swap-horizontal</v-icon>
Ubah Ruang
</v-btn>
</div>
</div>
<div v-else class="empty-state">
<v-icon size="48" color="grey-lighten-2">mdi-account-off-outline</v-icon>
<div class="empty-text">Tidak ada pasien yang diproses</div>
</div>
</v-card-text>
</v-card>
<!-- Queue Actions -->
<v-card class="queue-actions-card mt-3" elevation="0">
<v-card-text class="pa-4">
<div class="section-label mb-3">PANGGIL ANTREAN</div>
<div class="quota-info mb-3">
<div class="quota-item">
<span class="quota-label">Kuota</span>
<span class="quota-value">150</span>
</div>
<div class="quota-item">
<span class="quota-label">Tersedia</span>
<span class="quota-value quota-available">{{ 150 - quotaUsed }}</span>
</div>
<div class="quota-item full-width">
<v-progress-linear :model-value="(quotaUsed / 150) * 100" color="#10b981" height="6" rounded class="mt-1"></v-progress-linear>
<span class="quota-used">Terpakai: {{ quotaUsed }}</span>
</div>
</div>
<div class="call-buttons">
<v-btn color="#10b981" variant="outlined" @click="callNext" :disabled="!nextPatient">1</v-btn>
<v-btn color="#3b82f6" variant="outlined" @click="callMultiplePatients(5)">5</v-btn>
<v-btn color="#f59e0b" variant="outlined" @click="callMultiplePatients(10)">10</v-btn>
<v-btn color="#ef4444" variant="outlined" @click="callMultiplePatients(20)">20</v-btn>
</div>
</v-card-text>
</v-card>
<!-- Create Queue Button -->
<div class="create-buttons mt-3">
<v-btn color="#3b82f6" variant="flat" block size="large" @click="showPenunjangDialog = true">
<v-icon start size="20">mdi-clipboard-pulse</v-icon>
Buat Antrean Penunjang
</v-btn>
</div>
</div>
</v-col>
<!-- Right Column: Patient Table -->
<v-col cols="12" md="7">
<v-card class="patient-table-card" elevation="0">
<v-card-text class="pa-4">
<!-- Table Header with Filters -->
<div class="table-header mb-4">
<div class="section-label">DATA PASIEN</div>
<div class="filters">
<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 Penunjang ({{ (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>
<v-text-field
v-model="searchQuery"
placeholder="Cari barcode, nomor antrian..."
density="compact"
hide-details
class="search-field"
prepend-inner-icon="mdi-magnify"
></v-text-field>
</div>
</div>
<!-- Data Table -->
<v-data-table
:headers="tableHeaders"
:items="filteredPatients"
:search="searchQuery"
class="patient-table"
:items-per-page="10"
density="comfortable"
>
<template #item.noAntrian="{ item }">
<div class="queue-number">{{ item.noAntrian.split(" |")[0] }}</div>
</template>
<template #item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
size="small"
class="status-chip"
>
{{ getStatusLabel(item.status) }}
</v-chip>
</template>
<template #item.klinik="{ item }">
<v-chip size="small" variant="outlined" class="klinik-chip">
{{ item.klinik }}
</v-chip>
</template>
<template #item.aksi="{ item }">
<div class="action-buttons-table">
<v-btn
v-if="item.status === 'diloket'"
size="small"
color="#10b981"
variant="flat"
@click="processPatient(item, 'proses')"
>
Proses
</v-btn>
<v-btn
v-else-if="item.status === 'terlambat'"
size="small"
color="#10b981"
variant="flat"
@click="processPatient(item, 'aktifkan')"
>
Aktifkan
</v-btn>
<v-btn
v-else-if="item.status === 'pending'"
size="small"
color="#10b981"
variant="flat"
@click="processPatient(item, 'proses')"
>
Proses
</v-btn>
</div>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Penunjang Dialog -->
<v-dialog v-model="showPenunjangDialog" max-width="800px">
<v-card>
<v-card-title class="dialog-header">
<v-btn icon size="small" @click="showPenunjangDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<span>Pilih Ruang Penunjang</span>
</v-card-title>
<v-card-text class="pa-4">
<v-text-field
v-model="penunjangSearch"
placeholder="Cari penunjang..."
density="compact"
hide-details
class="mb-4"
prepend-inner-icon="mdi-magnify"
></v-text-field>
<v-row dense>
<v-col v-for="penunjang in filteredPenunjangs" :key="penunjang.id" cols="6" sm="4">
<v-card class="penunjang-card" @click="selectPenunjang(penunjang)" elevation="0">
<v-card-text class="text-center pa-3">
<div class="penunjang-name">{{ penunjang.name }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Change Ruang Dialog -->
<v-dialog v-model="showChangeKlinikDialog" max-width="800px">
<v-card>
<v-card-title class="dialog-header">
<v-btn icon size="small" @click="showChangeKlinikDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<span>Ubah Ruang Penunjang</span>
</v-card-title>
<v-card-text class="pa-4">
<v-text-field
v-model="changeKlinikSearch"
placeholder="Cari ruang..."
density="compact"
hide-details
class="mb-4"
prepend-inner-icon="mdi-magnify"
></v-text-field>
<v-row dense>
<v-col v-for="klinik in filteredChangeKliniks" :key="klinik.id" cols="6" sm="4">
<v-card class="klinik-card" @click="changeKlinik(klinik)" elevation="0">
<v-card-text class="text-center pa-3">
<div class="klinik-name">{{ klinik.name }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar" :color="snackbarColor" :timeout="3000" location="top right">
{{ snackbarText }}
<template v-slot:actions>
<v-btn icon size="small" @click="snackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { useQueue } from "../composables/useQueue";
const {
snackbar,
snackbarText,
snackbarColor,
showPenunjangDialog,
showChangeKlinikDialog,
penunjangSearch,
changeKlinikSearch,
currentProcessingPatient,
diLoketPatients,
terlambatPatients,
pendingPatients,
nextPatient,
quotaUsed,
filteredPenunjangs,
filteredChangeKliniks,
callNext,
callMultiplePatients,
processPatient,
selectPenunjang,
changeKlinik,
} = useQueue("penunjang");
const currentDate = ref(
new Date().toLocaleDateString("id-ID", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
})
);
const selectedStatus = ref("all");
const searchQuery = ref("");
// Combine all patients with status - Fixed to prevent infinite loop
const allPatients = computed(() => {
try {
// Safely access reactive values
const diLoketList = diLoketPatients.value || [];
const terlambatList = terlambatPatients.value || [];
const pendingList = pendingPatients.value || [];
// Map with status
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 [];
}
});
// Filter patients based on selected status
const filteredPatients = computed(() => {
try {
const all = allPatients.value;
if (selectedStatus.value === "all") return all;
return all.filter(p => p.status === selectedStatus.value);
} catch (error) {
console.error('Error in filteredPatients computed:', error);
return [];
}
});
const tableHeaders = ref([
{ title: "No", value: "no", width: "60px", sortable: false },
{ title: "Jam Panggil", value: "jamPanggil", width: "100px" },
{ title: "Barcode", value: "barcode", width: "130px" },
{ title: "No Antrian", value: "noAntrian", width: "140px" },
{ title: "Klinik", value: "klinik", width: "100px" },
{ title: "Fast Track", value: "fastTrack", width: "100px" },
{ title: "Pembayaran", value: "pembayaran", width: "100px" },
{ title: "Status", value: "status", width: "100px" },
{ title: "Aksi", value: "aksi", width: "100px", sortable: false },
]);
const getStatusColor = (status) => {
const colors = {
diloket: "#3b82f6",
terlambat: "#f59e0b",
pending: "#ef4444"
};
return colors[status] || "#6b7280";
};
const getStatusLabel = (status) => {
const labels = {
diloket: "Di Penunjang",
terlambat: "Terlambat",
pending: "Pending"
};
return labels[status] || status;
};
</script>
<style scoped>
.loket-container {
background: #f8fafc;
min-height: 100vh;
padding: 16px;
}
.page-header {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.header-content {
padding: 20px 24px;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-text {
color: white;
}
.page-title {
font-size: 24px;
font-weight: 700;
margin: 0;
line-height: 1.2;
}
.page-subtitle {
font-size: 14px;
margin: 4px 0 0 0;
opacity: 0.9;
}
.content-grid {
margin: 0 -8px;
}
.content-grid > .v-col {
padding: 8px;
}
.sticky-wrapper {
position: sticky;
top: 16px;
align-self: flex-start;
max-height: calc(100vh - 32px);
overflow-y: auto;
}
.current-patient-card,
.queue-actions-card,
.patient-table-card {
border-radius: 12px;
border: 1px solid #e2e8f0;
background: white;
}
.section-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
color: #64748b;
text-transform: uppercase;
}
.patient-details {
background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%);
border-radius: 8px;
padding: 16px;
}
.patient-number {
font-size: 28px;
font-weight: 800;
color: #1e293b;
line-height: 1;
}
.patient-info-text {
font-size: 13px;
color: #475569;
line-height: 1.5;
}
.action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.empty-state {
text-align: center;
padding: 32px 16px;
}
.empty-text {
font-size: 13px;
color: #94a3b8;
margin-top: 8px;
}
.quota-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.quota-item {
background: #f1f5f9;
border-radius: 8px;
padding: 12px;
text-align: center;
}
.quota-item.full-width {
grid-column: 1 / -1;
}
.quota-label {
display: block;
font-size: 11px;
color: #64748b;
margin-bottom: 4px;
font-weight: 600;
}
.quota-value {
display: block;
font-size: 24px;
font-weight: 800;
color: #1e293b;
}
.quota-available {
color: #10b981;
}
.quota-used {
display: block;
font-size: 11px;
color: #64748b;
margin-top: 4px;
}
.call-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.call-buttons .v-btn {
font-size: 16px;
font-weight: 700;
height: 44px;
}
.create-buttons .v-btn {
text-transform: none;
font-weight: 600;
}
.table-header {
display: flex;
flex-direction: column;
gap: 12px;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.status-filter {
flex: 1;
}
.status-filter .v-chip {
font-size: 12px;
font-weight: 600;
height: 32px;
border: 1px solid #e2e8f0;
background: white;
color: #64748b;
}
.status-filter .v-chip.active-chip {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.search-field {
max-width: 250px;
}
:deep(.patient-table) {
border-radius: 8px;
}
:deep(.patient-table .v-data-table__thead) {
background: #f8fafc;
}
:deep(.patient-table th) {
font-size: 11px !important;
font-weight: 700 !important;
color: #64748b !important;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 8px !important;
}
:deep(.patient-table td) {
font-size: 13px !important;
padding: 10px 8px !important;
border-bottom: 1px solid #f1f5f9 !important;
}
:deep(.patient-table tbody tr:hover) {
background: #f8fafc !important;
}
.queue-number {
font-weight: 700;
color: #1e293b;
}
.status-chip {
font-weight: 600;
font-size: 11px;
}
.klinik-chip {
font-size: 11px;
font-weight: 600;
}
.action-buttons-table .v-btn {
text-transform: none;
font-weight: 600;
font-size: 12px;
height: 32px;
}
.dialog-header {
background: #f8fafc;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 700;
}
.klinik-card,
.penunjang-card {
cursor: pointer;
transition: all 0.2s;
border: 2px solid #e2e8f0;
background: white;
}
.klinik-card:hover,
.penunjang-card:hover {
border-color: #8b5cf6;
background: #f5f3ff;
}
.klinik-name,
.penunjang-name {
font-size: 13px;
font-weight: 700;
color: #1e293b;
}
.v-btn {
text-transform: none !important;
letter-spacing: 0 !important;
}
@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%;
}
.action-grid {
grid-template-columns: 1fr;
}
.call-buttons {
grid-template-columns: repeat(2, 1fr);
}
}
</style>