699 lines
18 KiB
Vue
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> |