update list pasien dan antrian klinik

This commit is contained in:
bagus-arie05
2025-09-15 15:32:34 +07:00
parent bc5419ca4d
commit 28822718de
3 changed files with 1093 additions and 56 deletions

View File

@@ -2,7 +2,7 @@
<div> <div>
<!-- Filter Section --> <!-- Filter Section -->
<v-row class="mb-4"> <v-row class="mb-4">
<v-col cols="12" class="d-flex align-center flex-wrap"> <v-col cols="12" class="d-flex align-center flex-wrap ml-4">
<div style="width: 200px;" class="mr-4"> <div style="width: 200px;" class="mr-4">
<v-text-field <v-text-field
v-model="filterDate" v-model="filterDate"
@@ -38,7 +38,7 @@
<!-- Table Controls --> <!-- Table Controls -->
<v-row class="mb-3"> <v-row class="mb-3">
<v-col cols="12" md="6" class="d-flex align-center"> <v-col cols="12" md="6" class="d-flex align-center">
<span class="mr-2">Show</span> <span class="mr-2 pa-4">Show</span>
<div style="width: 100px;"> <div style="width: 100px;">
<v-select <v-select
v-model="itemsPerPage" v-model="itemsPerPage"
@@ -51,7 +51,7 @@
<span class="ml-2">entries</span> <span class="ml-2">entries</span>
</v-col> </v-col>
<v-col cols="12" md="6" class="d-flex justify-end"> <v-col cols="12" md="6" class="d-flex justify-end">
<div class="d-flex align-center"> <div class="d-flex align-center pa-4">
<span class="mr-2">Search:</span> <span class="mr-2">Search:</span>
<v-text-field <v-text-field
v-model="search" v-model="search"

View File

@@ -1,25 +1,172 @@
<template> <template>
<div> <div class="pasien-container">
<!-- Page Header --> <!-- Header Section -->
<v-container fluid> <div class="page-header">
<v-row> <div class="header-content">
<v-col cols="12"> <div class="header-left">
<h1 class="text-h4 font-weight-bold mb-6">List Pasien</h1> <div class="header-icon">
</v-col> <v-icon size="32" color="white">mdi-account-group</v-icon>
</v-row> </div>
<div class="header-text">
<h1 class="page-title">List Pasien</h1>
<p class="page-subtitle">Senin, 15 September 2025 - Data Master Pasien</p>
</div>
</div>
<div class="header-right">
<v-chip
color="success"
variant="flat"
class="mr-2"
>
Total {{ pasienData.length }} Pasien
</v-chip>
<v-chip
color="white"
variant="flat"
class="text-primary"
>
Status: Aktif
</v-chip>
</div>
</div>
</div>
<!-- Main Content Card --> <!-- Filter Controls
<v-card elevation="2"> <v-card class="filter-controls-card mb-4" elevation="2">
<v-card-text class="pa-6"> <v-card-text class="py-4">
<TabelListPasien <v-row align="center">
:items="pasienData" <v-col cols="12" md="8">
@search="handleSearch" <div class="d-flex align-center flex-wrap gap-10">
@export-laporan="handleExportLaporan" <span class="text-subtitle-1 font-weight-medium">Filter Cepat:</span>
@export-laporan-per-klinik="handleExportLaporanPerKlinik" <v-btn
/> color="primary"
</v-card-text> variant="flat"
</v-card> size="default"
</v-container> class="px-4"
@click="filterByStatus('Tunggu Daftar')"
>
<v-icon start size="16">mdi-clock-outline</v-icon>
Tunggu Daftar
</v-btn>
<v-btn
color="success"
variant="flat"
size="default"
class="px-4"
@click="filterByStatus('Selesai')"
>
<v-icon start size="16">mdi-check-circle</v-icon>
Selesai
</v-btn>
<v-btn
color="warning"
variant="flat"
size="default"
class="px-4"
@click="filterByKlinik('JKN')"
>
<v-icon start size="16">mdi-card-account-details</v-icon>
JKN
</v-btn>
<v-btn
color="info"
variant="flat"
size="default"
class="px-4"
@click="resetFilter()"
>
<v-icon start size="16">mdi-refresh</v-icon>
Reset
</v-btn>
</div>
</v-col>
<v-col cols="12" md="4">
<div class="d-flex justify-end gap-2">
<v-btn
color="success"
variant="flat"
@click="handleExportLaporan"
:loading="loading"
>
<v-icon start>mdi-file-excel</v-icon>
Export Laporan
</v-btn>
<v-btn
color="primary"
variant="flat"
@click="handleExportLaporanPerKlinik"
:loading="loading"
>
<v-icon start>mdi-hospital-building</v-icon>
Export Per Klinik
</v-btn>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card> -->
<!-- Main Patients Table -->
<v-card class="main-table-card mb-4" elevation="2">
<v-card-title class="d-flex align-center justify-space-between pa-6">
<div class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-table</v-icon>
<span class="text-h6 font-weight-bold">DATA PASIEN</span>
</div>
<v-chip color="info" variant="flat">
{{ filteredData.length }} dari {{ pasienData.length }} pasien
</v-chip>
</v-card-title>
<v-divider></v-divider>
<TabelListPasien
:items="filteredData"
@search="handleSearch"
@export-laporan="handleExportLaporan"
@export-laporan-per-klinik="handleExportLaporanPerKlinik"
/>
</v-card>
Statistics Cards
<v-row class="mb-4">
<v-col cols="12" md="3">
<v-card class="stats-card" elevation="2">
<v-card-text class="text-center pa-4">
<v-icon size="40" color="success" class="mb-2">mdi-account-check</v-icon>
<div class="text-h4 font-weight-bold text-success">{{ getStatsByStatus('Tunggu Daftar') }}</div>
<div class="text-body-2 text-grey-darken-1">Tunggu Daftar</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card class="stats-card" elevation="2">
<v-card-text class="text-center pa-4">
<v-icon size="40" color="info" class="mb-2">mdi-barcode</v-icon>
<div class="text-h4 font-weight-bold text-info">{{ getStatsByStatus('Barcode') }}</div>
<div class="text-body-2 text-grey-darken-1">Barcode</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card class="stats-card" elevation="2">
<v-card-text class="text-center pa-4">
<v-icon size="40" color="warning" class="mb-2">mdi-wifi</v-icon>
<div class="text-h4 font-weight-bold text-warning">{{ getStatsByKeterangan('Online') }}</div>
<div class="text-body-2 text-grey-darken-1">Online</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card class="stats-card" elevation="2">
<v-card-text class="text-center pa-4">
<v-icon size="40" color="error" class="mb-2">mdi-wifi-off</v-icon>
<div class="text-h4 font-weight-bold text-error">{{ getStatsByKeterangan('Offline') }}</div>
<div class="text-body-2 text-grey-darken-1">Offline</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Loading Overlay --> <!-- Loading Overlay -->
<v-overlay v-model="loading" class="align-center justify-center"> <v-overlay v-model="loading" class="align-center justify-center">
@@ -30,7 +177,7 @@
/> />
</v-overlay> </v-overlay>
<!-- Success Snackbar --> <!-- Snackbar -->
<v-snackbar <v-snackbar
v-model="snackbar.show" v-model="snackbar.show"
:color="snackbar.color" :color="snackbar.color"
@@ -40,19 +187,18 @@
{{ snackbar.message }} {{ snackbar.message }}
<template v-slot:actions> <template v-slot:actions>
<v-btn <v-btn
variant="text" icon
@click="snackbar.show = false" @click="snackbar.show = false"
> >
Close <v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
</template> </template>
</v-snackbar> </v-snackbar>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import TabelListPasien from '~/components/TabelListPasien.vue' import TabelListPasien from '~/components/TabelListPasien.vue'
// Meta untuk SEO // Meta untuk SEO
@@ -63,13 +209,14 @@ definePageMeta({
// Reactive states // Reactive states
const loading = ref(false) const loading = ref(false)
const currentFilter = ref('')
const snackbar = ref({ const snackbar = ref({
show: false, show: false,
message: '', message: '',
color: 'success' color: 'success'
}) })
// Sample data (replace with actual API call) // Sample data
const pasienData = ref([ const pasienData = ref([
{ {
tglPeriksa: '27/08/2025', tglPeriksa: '27/08/2025',
@@ -203,17 +350,57 @@ const pasienData = ref([
} }
]) ])
// Computed properties
const filteredData = computed(() => {
if (!currentFilter.value) {
return pasienData.value
}
return pasienData.value.filter(item => {
return item.status === currentFilter.value ||
item.pembayaran === currentFilter.value ||
item.keterangan === currentFilter.value
})
})
// Methods // Methods
const showSnackbar = (message, color = 'success') => {
snackbar.value = {
show: true,
message,
color
}
}
const filterByStatus = (status) => {
currentFilter.value = status
showSnackbar(`Filter diterapkan: ${status}`, 'info')
}
const filterByKlinik = (pembayaran) => {
currentFilter.value = pembayaran
showSnackbar(`Filter diterapkan: ${pembayaran}`, 'info')
}
const resetFilter = () => {
currentFilter.value = ''
showSnackbar('Filter direset', 'success')
}
const getStatsByStatus = (status) => {
return pasienData.value.filter(item => item.status === status).length
}
const getStatsByKeterangan = (keterangan) => {
return pasienData.value.filter(item => item.keterangan === keterangan).length
}
const handleSearch = async (filters) => { const handleSearch = async (filters) => {
loading.value = true loading.value = true
try { try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000))
// Here you would typically call your API with filters
console.log('Search filters:', filters) console.log('Search filters:', filters)
showSnackbar('Data berhasil difilter', 'success') showSnackbar('Data berhasil difilter', 'success')
} catch (error) { } catch (error) {
console.error('Error searching data:', error) console.error('Error searching data:', error)
@@ -227,12 +414,8 @@ const handleExportLaporan = async () => {
loading.value = true loading.value = true
try { try {
// Simulate export process
await new Promise(resolve => setTimeout(resolve, 2000)) await new Promise(resolve => setTimeout(resolve, 2000))
// Here you would typically call your export API
console.log('Exporting laporan pasien...') console.log('Exporting laporan pasien...')
showSnackbar('Laporan pasien berhasil diexport', 'success') showSnackbar('Laporan pasien berhasil diexport', 'success')
} catch (error) { } catch (error) {
console.error('Error exporting laporan:', error) console.error('Error exporting laporan:', error)
@@ -246,12 +429,8 @@ const handleExportLaporanPerKlinik = async () => {
loading.value = true loading.value = true
try { try {
// Simulate export process
await new Promise(resolve => setTimeout(resolve, 2000)) await new Promise(resolve => setTimeout(resolve, 2000))
// Here you would typically call your export API
console.log('Exporting laporan pasien per klinik...') console.log('Exporting laporan pasien per klinik...')
showSnackbar('Laporan pasien per klinik berhasil diexport', 'success') showSnackbar('Laporan pasien per klinik berhasil diexport', 'success')
} catch (error) { } catch (error) {
console.error('Error exporting laporan per klinik:', error) console.error('Error exporting laporan per klinik:', error)
@@ -261,25 +440,11 @@ const handleExportLaporanPerKlinik = async () => {
} }
} }
const showSnackbar = (message, color = 'success') => {
snackbar.value = {
show: true,
message,
color
}
}
const fetchPasienData = async () => { const fetchPasienData = async () => {
loading.value = true loading.value = true
try { try {
// Simulate API call to fetch patient data
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000))
// Here you would typically call your API
// const response = await $fetch('/api/pasien')
// pasienData.value = response.data
console.log('Patient data loaded successfully') console.log('Patient data loaded successfully')
} catch (error) { } catch (error) {
console.error('Error fetching patient data:', error) console.error('Error fetching patient data:', error)
@@ -307,5 +472,164 @@ useHead({
</script> </script>
<style scoped> <style scoped>
/* Custom styles if needed */ .pasien-container {
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.page-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
border-radius: 16px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(25, 118, 210, 0.3);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: white;
}
.header-left {
display: flex;
align-items: center;
}
.header-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 16px;
margin-right: 20px;
backdrop-filter: blur(10px);
}
.page-title {
font-size: 32px;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
}
.header-right {
display: flex;
align-items: center;
}
/* .filter-controls-card, */
.main-table-card,
.stats-card {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.stats-card {
transition: all 0.2s ease;
}
.stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
/* Enhanced table styling */
.main-table-card :deep(.v-data-table th) {
background: #fafbfc;
font-weight: 600;
font-size: 13px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.main-table-card :deep(.v-data-table tbody tr:hover) {
background: #f8fafc !important;
}
.main-table-card :deep(.v-data-table tbody td) {
padding: 12px 16px;
border-bottom: 1px solid #f1f5f9;
}
/* Button styling */
.v-btn {
text-transform: none !important;
font-weight: 500;
}
.v-btn--size-default {
height: 40px;
}
/* Responsive Design */
@media (max-width: 1024px) {
.header-content {
flex-direction: column;
gap: 20px;
text-align: center;
}
.header-left {
flex-direction: column;
gap: 12px;
}
.page-title {
font-size: 28px;
}
}
@media (max-width: 768px) {
.pasien-container {
padding: 16px;
}
.header-content {
padding: 24px 20px;
}
.page-title {
font-size: 24px;
}
.header-icon {
padding: 12px;
}
/* .filter-controls-card .d-flex {
flex-direction: column;
align-items: flex-start;
gap: 16px;
} */
.filter-controls-card .v-col:last-child .d-flex {
justify-content: flex-start;
}
}
@media (max-width: 600px) {
.page-title {
font-size: 20px;
}
.filter-buttons-container {
justify-content: center;
}
.filter-btn {
margin-right: 12px;
margin-bottom: 10px;
}
.header-right .v-chip {
font-size: 12px;
}
}
</style> </style>

View File

@@ -0,0 +1,713 @@
<!-- pages/AntrianKlinik.vue -->
<template>
<div class="antrian-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-clipboard-list</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Antrian Klinik</h1>
<p class="page-subtitle">Informasi Antrian Real-time</p>
</div>
</div>
<div class="header-right">
<div class="time-display">
<v-chip color="white" variant="flat" size="large" class="time-chip">
<v-icon start size="16">mdi-clock-outline</v-icon>
{{ currentTime }}
</v-chip>
</div>
</div>
</div>
</div>
<!-- Controls Section -->
<v-card class="controls-card mb-4" elevation="2">
<v-card-text class="py-3">
<v-row align="center">
<v-col cols="12" md="6">
<div class="d-flex align-center flex-wrap gap-3">
<v-select
v-model="selectedClinic"
:items="clinicOptions"
label="Pilih Klinik"
density="compact"
variant="outlined"
hide-details
clearable
style="min-width: 200px;"
/>
<v-select
v-model="selectedStatus"
:items="statusOptions"
label="Filter Status"
density="compact"
variant="outlined"
hide-details
clearable
style="min-width: 160px;"
/>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="d-flex justify-end align-center flex-wrap gap-2">
<v-btn
variant="outlined"
prepend-icon="mdi-refresh"
@click="refreshData"
:loading="loading"
size="small"
>
Refresh
</v-btn>
<v-btn
:color="autoRefresh ? 'success' : 'grey'"
:variant="autoRefresh ? 'flat' : 'outlined'"
prepend-icon="mdi-autorenew"
@click="toggleAutoRefresh"
size="small"
>
Auto Refresh
</v-btn>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Queue Display -->
<v-card elevation="2" class="main-content-card">
<v-card-title class="d-flex align-center pa-4 bg-grey-lighten-4">
<v-icon class="mr-2">mdi-format-list-numbered</v-icon>
<span>Antrian Aktif - {{ filteredQueues.length }} antrian</span>
</v-card-title>
<v-card-text class="pa-6">
<v-row>
<v-col
v-for="queue in filteredQueues"
:key="queue.id"
cols="12"
sm="6"
md="4"
lg="3"
class="pa-2"
>
<v-card
:class="{
'queue-card': true,
'queue-active': queue.status === 'active',
'queue-waiting': queue.status === 'waiting',
'queue-completed': queue.status === 'completed',
'queue-called': queue.status === 'called'
}"
elevation="3"
>
<v-card-text class="text-center pa-4">
<!-- Status Chip -->
<div class="mb-3">
<v-chip
:color="getStatusColor(queue.status)"
size="small"
variant="flat"
>
{{ getStatusText(queue.status) }}
</v-chip>
</div>
<!-- Queue Number -->
<div class="queue-number-wrapper mb-3">
<div class="queue-number">
{{ queue.number }}
</div>
</div>
<!-- Clinic Info -->
<h3 class="text-h6 font-weight-bold mb-2">
{{ queue.clinicName }}
</h3>
<!-- Patient Info -->
<div class="patient-info mb-3">
<p class="text-body-2 mb-1">
<strong>Pasien:</strong> {{ queue.patientName }}
</p>
<p class="text-caption text-grey-darken-1 mb-1">
<strong>Dokter:</strong> {{ queue.doctorName }}
</p>
<p class="text-caption text-grey-darken-1">
<strong>Estimasi:</strong> {{ queue.estimatedTime }}
</p>
</div>
<!-- Waiting Info -->
<div v-if="queue.status === 'waiting'" class="waiting-info">
<v-chip
size="small"
color="orange"
variant="outlined"
class="mb-2"
>
Sisa {{ queue.remainingQueue }} antrian
</v-chip>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Empty State -->
<div v-if="filteredQueues.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-clipboard-list-outline</v-icon>
<h3 class="text-h6 mt-4 text-grey-darken-1">Tidak ada antrian yang sesuai filter</h3>
<p class="text-body-2 text-grey-darken-1">Coba ubah filter pencarian Anda</p>
</div>
</v-card-text>
</v-card>
<!-- Current Queue Display -->
<v-card class="current-queue-card mt-4" elevation="4" v-if="currentQueue">
<div class="current-queue-header">
<v-icon size="24" class="mr-2">mdi-account-voice</v-icon>
<span class="text-h6">Sedang Dipanggil</span>
</div>
<v-card-text class="pa-6">
<v-row align="center">
<v-col cols="12" md="8">
<div class="d-flex align-center">
<div class="current-number-display mr-4">
{{ currentQueue.number }}
</div>
<div>
<h3 class="text-h5 font-weight-bold mb-1">{{ currentQueue.patientName }}</h3>
<p class="text-body-1 mb-1">{{ currentQueue.clinicName }}</p>
<p class="text-body-2 text-grey-darken-1">Dr. {{ currentQueue.doctorName }}</p>
</div>
</div>
</v-col>
<v-col cols="12" md="4" class="text-right">
<v-chip color="success" size="large" variant="flat">
<v-icon start>mdi-microphone</v-icon>
DIPANGGIL
</v-chip>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Statistics Card -->
<v-card class="stats-card mt-4" elevation="2">
<v-card-title class="d-flex align-center pa-4 bg-info text-white">
<v-icon class="mr-2">mdi-chart-line</v-icon>
Statistik Antrian Hari Ini
</v-card-title>
<v-card-text class="pa-4">
<v-row>
<v-col cols="6" md="3" class="text-center">
<div class="stat-item">
<div class="stat-number text-primary">{{ statistics.total }}</div>
<div class="stat-label">Total Antrian</div>
</div>
</v-col>
<v-col cols="6" md="3" class="text-center">
<div class="stat-item">
<div class="stat-number text-success">{{ statistics.completed }}</div>
<div class="stat-label">Selesai</div>
</div>
</v-col>
<v-col cols="6" md="3" class="text-center">
<div class="stat-item">
<div class="stat-number text-warning">{{ statistics.waiting }}</div>
<div class="stat-label">Menunggu</div>
</div>
</v-col>
<v-col cols="6" md="3" class="text-center">
<div class="stat-item">
<div class="stat-number text-error">{{ statistics.avgWaitTime }}</div>
<div class="stat-label">Rata-rata Tunggu (menit)</div>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Snackbar -->
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="3000"
location="top right"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn icon @click="snackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
definePageMeta({
layout: false,
});
// Reactive data
const loading = ref(false)
const selectedClinic = ref(null)
const selectedStatus = ref(null)
const snackbar = ref(false)
const snackbarText = ref('')
const snackbarColor = ref('success')
const currentTime = ref('')
const autoRefresh = ref(false)
let autoRefreshInterval = null
let timeInterval = null
// Options - removed all filter options
// Queue data
const queues = ref([
{
id: 1,
number: '026',
clinicName: 'ANAK',
patientName: 'Ahmad Fauzi',
doctorName: 'Dr. Sarah Putri',
status: 'waiting',
estimatedTime: '10:30',
remainingQueue: 3
},
{
id: 2,
number: '018',
clinicName: 'JANTUNG',
patientName: 'Siti Aminah',
doctorName: 'Dr. Budi Santoso',
status: 'called',
estimatedTime: '10:15',
remainingQueue: 0
},
{
id: 3,
number: '019',
clinicName: 'MATA',
patientName: 'Rudi Hartono',
doctorName: 'Dr. Maya Sari',
status: 'active',
estimatedTime: '10:20',
remainingQueue: 0
},
{
id: 4,
number: '020',
clinicName: 'GIGI DAN MULUT',
patientName: 'Dewi Lestari',
doctorName: 'Dr. Agus Wijaya',
status: 'waiting',
estimatedTime: '10:40',
remainingQueue: 5
},
{
id: 5,
number: '021',
clinicName: 'IPD',
patientName: 'Bambang Susilo',
doctorName: 'Dr. Rina Handayani',
status: 'waiting',
estimatedTime: '10:50',
remainingQueue: 7
},
{
id: 6,
number: '022',
clinicName: 'THT',
patientName: 'Lisa Permata',
doctorName: 'Dr. Hendra Gunawan',
status: 'waiting',
estimatedTime: '11:00',
remainingQueue: 2
},
{
id: 7,
number: '015',
clinicName: 'BEDAH',
patientName: 'Eko Prasetyo',
doctorName: 'Dr. Diana Sari',
status: 'completed',
estimatedTime: '09:30',
remainingQueue: 0
}
])
// Current queue (being called)
const currentQueue = ref({
number: '018',
clinicName: 'JANTUNG',
patientName: 'Siti Aminah',
doctorName: 'Budi Santoso'
})
// Statistics
const statistics = ref({
total: 45,
completed: 18,
waiting: 22,
avgWaitTime: 25
})
// Computed properties
const filteredQueues = computed(() => {
let filtered = queues.value
if (selectedClinic.value) {
filtered = filtered.filter(queue => queue.clinicName === selectedClinic.value)
}
if (selectedStatus.value) {
const statusMap = {
'Menunggu': 'waiting',
'Dipanggil': 'called',
'Aktif': 'active',
'Selesai': 'completed'
}
const statusFilter = statusMap[selectedStatus.value]
filtered = filtered.filter(queue => queue.status === statusFilter)
}
return filtered
})
// Methods
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const showSnackbar = (text, color = 'success') => {
snackbarText.value = text
snackbarColor.value = color
snackbar.value = true
}
const getStatusColor = (status) => {
const colors = {
'waiting': 'orange',
'called': 'info',
'active': 'success',
'completed': 'grey'
}
return colors[status] || 'grey'
}
const getStatusText = (status) => {
const texts = {
'waiting': 'MENUNGGU',
'called': 'DIPANGGIL',
'active': 'AKTIF',
'completed': 'SELESAI'
}
return texts[status] || 'UNKNOWN'
}
const refreshData = () => {
loading.value = true
setTimeout(() => {
loading.value = false
showSnackbar('Data antrian berhasil diperbarui', 'success')
}, 1000)
}
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
autoRefreshInterval = setInterval(() => {
refreshData()
}, 30000) // Refresh every 30 seconds
showSnackbar('Auto refresh diaktifkan (30 detik)', 'info')
} else {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval)
autoRefreshInterval = null
}
showSnackbar('Auto refresh dinonaktifkan', 'warning')
}
}
// Lifecycle
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
refreshData()
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval)
}
})
</script>
<style scoped>
.antrian-container {
background: #f5f7fa;
min-height: 100vh;
padding: 20px;
}
.page-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
border-radius: 16px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(25, 118, 210, 0.3);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: white;
}
.header-left {
display: flex;
align-items: center;
}
.header-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 16px;
margin-right: 20px;
backdrop-filter: blur(10px);
}
.page-title {
font-size: 32px;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
}
.time-chip {
font-weight: 600;
color: #1976d2 !important;
}
.controls-card,
.main-content-card,
.current-queue-card,
.stats-card {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.queue-card {
border-radius: 16px !important;
height: 280px;
background: white;
transition: all 0.3s ease;
}
.queue-waiting {
border-left: 6px solid #FF9800;
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
}
.queue-called {
border-left: 6px solid #2196F3;
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
animation: pulse 2s infinite;
}
.queue-active {
border-left: 6px solid #4CAF50;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.queue-completed {
border-left: 6px solid #9E9E9E;
opacity: 0.8;
}
.queue-number-wrapper {
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 16px;
}
.queue-number {
background: linear-gradient(135deg, #1976d2, #1565c0);
color: white;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.4);
}
.patient-info {
text-align: left;
}
.current-queue-card {
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
}
.current-queue-header {
background: rgba(255, 255, 255, 0.2);
padding: 16px 24px;
display: flex;
align-items: center;
color: white;
font-weight: 600;
}
.current-number-display {
background: white;
color: #4CAF50;
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
font-weight: bold;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.stats-card .stat-item {
padding: 16px 0;
}
.stat-number {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
@keyframes pulse {
0% {
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
}
50% {
box-shadow: 0 8px 20px rgba(33, 150, 243, 0.6);
}
100% {
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
}
}
/* Responsive Design */
@media (max-width: 1024px) {
.header-content {
flex-direction: column;
gap: 20px;
text-align: center;
}
.header-left {
flex-direction: column;
gap: 12px;
}
.page-title {
font-size: 28px;
}
}
@media (max-width: 768px) {
.antrian-container {
padding: 16px;
}
.header-content {
padding: 24px 20px;
}
.page-title {
font-size: 24px;
}
.queue-card {
height: 260px;
}
.queue-number {
width: 70px;
height: 70px;
font-size: 20px;
}
.current-number-display {
width: 80px;
height: 80px;
font-size: 24px;
}
}
@media (max-width: 600px) {
.queue-card {
height: 300px;
}
.page-title {
font-size: 20px;
}
.stat-number {
font-size: 24px;
}
.queue-number {
width: 90px;
height: 90px;
font-size: 24px;
}
.current-number-display {
width: 100px;
height: 100px;
font-size: 32px;
}
}
</style>