18 changed files with 6455 additions and 668 deletions

No files matched your search

+347
View File
@@ -0,0 +1,347 @@
<!-- components/TabelData.vue -->
<template>
<v-card-text>
<!-- Title Section -->
<v-row no-gutters class="mb-3" v-if="title">
<v-col cols="12">
<v-card-title
class="text-subtitle-1 font-weight-bold pa-0"
:class="getTitleClass(title)"
>
{{ title }}
</v-card-title>
</v-col>
</v-row>
<!-- Controls Section -->
<v-row no-gutters class="d-flex align-center mb-4">
<v-col cols="12" sm="6" class="d-flex align-center">
<div class="d-flex align-center">
<span>Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="outlined"
hide-details
class="mx-2"
style="width: 80px;"
/>
<span>entries</span>
</div>
</v-col>
<v-col cols="12" sm="6" class="d-flex justify-end align-center">
<div v-if="showSearch" class="d-flex align-center">
<span class="mr-2">Search:</span>
<v-text-field
v-model="search"
hide-details
density="compact"
variant="outlined"
style="width: 200px;"
/>
</div>
</v-col>
</v-row>
<v-data-table
:headers="headers"
:items="paginatedItems"
:search="search"
no-data-text="No data available in table"
hide-default-footer
class="elevation-1"
item-value="no"
>
<!-- Custom slot untuk nomor urut -->
<template v-slot:item.no="{ index }">
{{ (currentPage - 1) * itemsPerPage + index + 1 }}
</template>
<!-- Custom slot untuk jam panggil dengan highlighting -->
<template v-slot:item.jamPanggil="{ item }">
<slot name="item.jamPanggil" :item="item">
<span>{{ item.jamPanggil }}</span>
</slot>
</template>
<!-- Custom slot untuk status -->
<template v-slot:item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
size="small"
text-color="white"
>
{{ item.status }}
</v-chip>
</template>
<!-- Custom slot untuk barcode dengan formatting -->
<template v-slot:item.barcode="{ item }">
<span class="font-mono">{{ item.barcode }}</span>
</template>
<!-- Custom slot untuk no antrian dengan highlighting -->
<template v-slot:item.noAntrian="{ item }">
<div>
<span class="font-weight-medium">{{ item.noAntrian.split(' |')[0] }}</span>
<br>
<small class="text-grey-darken-1">{{ item.noAntrian.split(' |')[1] }}</small>
</div>
</template>
<!-- Custom slot untuk klinik dengan chip styling -->
<template v-slot:item.klinik="{ item }">
<v-chip
size="small"
variant="outlined"
:color="getKlinikColor(item.klinik)"
>
{{ item.klinik }}
</v-chip>
</template>
<!-- Custom slot untuk fast track -->
<template v-slot:item.fastTrack="{ item }">
<v-chip
size="small"
color="info"
variant="tonal"
>
{{ item.fastTrack }}
</v-chip>
</template>
<!-- Custom slot untuk pembayaran -->
<template v-slot:item.pembayaran="{ item }">
<v-chip
size="small"
color="success"
variant="tonal"
>
{{ item.pembayaran }}
</v-chip>
</template>
<!-- Custom slot untuk keterangan -->
<template v-slot:item.keterangan="{ item }">
<span v-if="item.keterangan" class="text-green font-weight-medium">
{{ item.keterangan }}
</span>
<span v-else>-</span>
</template>
<!-- Slot untuk aksi -->
<template v-slot:item.aksi="{ item }">
<slot name="actions" :item="item" />
</template>
<template #no-data>
<div class="text-center pa-4">No data available in table</div>
</template>
</v-data-table>
<!-- Footer Pagination -->
<div class="d-flex justify-space-between align-center mt-4">
<div class="text-body-2 text-grey-darken-1">
Showing {{ currentPageStart }} to {{ currentPageEnd }} of {{ filteredTotal }} entries
</div>
<v-pagination
v-model="currentPage"
:length="totalPages"
:total-visible="7"
/>
</div>
</v-card-text>
</template>
<script setup>
import { ref, computed, watch } from "vue";
const props = defineProps({
headers: {
type: Array,
required: true,
},
items: {
type: Array,
required: true,
},
title: {
type: String,
default: "",
},
showSearch: {
type: Boolean,
default: true,
},
});
const search = ref("");
const itemsPerPage = ref(10);
const currentPage = ref(1);
// Filter items based on search
const filteredItems = computed(() => {
if (!search.value) {
return props.items;
}
const searchLower = search.value.toLowerCase();
return props.items.filter(item => {
return Object.values(item).some(value =>
String(value).toLowerCase().includes(searchLower)
);
});
});
const filteredTotal = computed(() => filteredItems.value.length);
const totalPages = computed(() => Math.ceil(filteredTotal.value / itemsPerPage.value));
// Paginate the filtered items
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value;
const end = start + itemsPerPage.value;
return filteredItems.value.slice(start, end);
});
const currentPageStart = computed(() => {
if (filteredTotal.value === 0) return 0;
return (currentPage.value - 1) * itemsPerPage.value + 1;
});
const currentPageEnd = computed(() => {
const end = currentPage.value * itemsPerPage.value;
return Math.min(end, filteredTotal.value);
});
// Method untuk mendapatkan warna status
const getStatusColor = (status) => {
switch (status) {
case 'Tunggu Daftar':
return 'orange';
case 'Barcode':
return 'blue';
case 'Selesai':
return 'green';
case 'Batal':
return 'red';
case 'Aktif':
return 'success';
case 'Menunggu':
return 'warning';
default:
return 'grey';
}
};
// Method untuk mendapatkan warna klinik
const getKlinikColor = (klinik) => {
switch (klinik) {
case 'KANDUNGAN':
return 'pink';
case 'IPD':
return 'blue';
case 'THT':
return 'orange';
case 'SARAF':
return 'purple';
default:
return 'grey';
}
};
// Method untuk mendapatkan class title
const getTitleClass = (title) => {
if (title.includes('TERLAMBAT')) {
return 'text-warning';
} else if (title.includes('PENDING')) {
return 'text-info';
} else if (title.includes('DI LOKET')) {
return 'text-success';
}
return 'text-primary';
};
// Watch untuk reset halaman ketika items per page berubah
watch(itemsPerPage, () => {
currentPage.value = 1;
});
// Watch untuk reset halaman ketika items berubah
watch(() => props.items, () => {
currentPage.value = 1;
});
// Watch untuk reset halaman ketika search berubah
watch(search, () => {
currentPage.value = 1;
});
</script>
<style scoped>
.text-red {
color: #f44336 !important;
}
.text-warning {
color: #ff9800 !important;
}
.text-info {
color: #2196f3 !important;
}
.text-success {
color: #4caf50 !important;
}
.text-primary {
color: #1976d2 !important;
}
.font-mono {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
/* Table enhancements */
:deep(.v-data-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.v-data-table tbody tr) {
transition: background-color 0.2s ease;
}
:deep(.v-data-table tbody tr:hover) {
background: rgba(25, 118, 210, 0.04) !important;
}
:deep(.v-data-table th) {
font-weight: 600 !important;
background: #fafafa !important;
color: #424242 !important;
}
:deep(.v-data-table td) {
border-bottom: 1px solid #e0e0e0 !important;
}
/* Responsive adjustments */
@media (max-width: 768px) {
:deep(.v-data-table) {
font-size: 0.875rem;
}
.v-pagination {
:deep(.v-pagination__item) {
min-width: 32px;
height: 32px;
}
}
}
</style>
+52
View File
@@ -0,0 +1,52 @@
<template>
<v-data-table
:headers="headers"
:items="items"
hide-default-footer
class="elevation-1"
>
<template v-slot:item.pilih="{ item }">
<v-checkbox
:model-value="isSelected(item.id)"
@change="toggleService(item.id)"
color="primary"
></v-checkbox>
</template>
<template v-slot:item.no="{ item }">
{{ item.no }}
</template>
</v-data-table>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
headers: Array,
items: Array,
selectedItems: Array,
});
const emit = defineEmits(['update:selectedItems']);
const isSelected = (id) => props.selectedItems.includes(id);
const toggleService = (id) => {
const newSelection = isSelected(id)
? props.selectedItems.filter(serviceId => serviceId !== id)
: [...props.selectedItems, id];
emit('update:selectedItems', newSelection);
};
</script>
<style scoped>
.v-data-table {
border-radius: 8px;
}
.v-checkbox {
justify-content: center;
}
</style>
+320
View File
@@ -0,0 +1,320 @@
<template>
<div>
<!-- Filter Section -->
<v-row class="mb-4">
<v-col cols="12" class="d-flex align-center flex-wrap ml-4">
<div style="width: 200px;" class="mr-4">
<v-text-field
v-model="filterDate"
type="date"
label="Tanggal"
density="compact"
hide-details
variant="outlined"
/>
</div>
<div style="width: 150px;" class="mr-4">
<v-select
v-model="filterStatus"
:items="statusOptions"
label="Status"
density="compact"
hide-details
variant="outlined"
/>
</div>
<v-btn color="primary" @click="searchData" class="mr-3">
SEARCH
</v-btn>
<v-btn color="success" variant="outlined" @click="exportLaporan" class="mr-3">
Laporan Pasien
</v-btn>
<v-btn color="info" variant="outlined" @click="exportLaporanPerKlinik">
Laporan Pasien Per Klinik
</v-btn>
</v-col>
</v-row>
<!-- Table Controls -->
<v-row class="mb-3">
<v-col cols="12" md="6" class="d-flex align-center">
<span class="mr-2 pa-4">Show</span>
<div style="width: 100px;">
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
hide-details
variant="outlined"
/>
</div>
<span class="ml-2">entries</span>
</v-col>
<v-col cols="12" md="6" class="d-flex justify-end">
<div class="d-flex align-center pa-4">
<span class="mr-2">Search:</span>
<v-text-field
v-model="search"
density="compact"
hide-details
style="min-width: 200px"
variant="outlined"
/>
</div>
</v-col>
</v-row>
<!-- Data Table -->
<v-data-table
:headers="headers"
:items="paginatedItems"
:search="search"
hide-default-footer
class="elevation-1"
>
<!-- Custom Status Column -->
<template v-slot:item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
size="small"
variant="flat"
>
{{ item.status }}
</v-chip>
</template>
<!-- Custom Keterangan Column -->
<template v-slot:item.keterangan="{ item }">
<span :class="getKeteranganClass(item.keterangan)">
{{ item.keterangan }}
</span>
</template>
<!-- No Data -->
<template #no-data>
<div class="text-center pa-4">
No data available in table
</div>
</template>
</v-data-table>
<!-- Pagination -->
<div class="d-flex justify-space-between align-center pa-4">
<div class="text-body-2 text-grey-darken-1">
Showing {{ currentPageStart }} to {{ currentPageEnd }} of {{ totalFilteredItems }} entries
</div>
<div class="d-flex align-center">
<v-btn
:disabled="currentPage === 1"
@click="previousPage"
variant="text"
size="small"
>
Previous
</v-btn>
<template v-for="page in visiblePages" :key="page">
<v-btn
v-if="page !== '...'"
:color="page === currentPage ? 'primary' : ''"
:variant="page === currentPage ? 'flat' : 'text'"
@click="goToPage(page)"
size="small"
class="mx-1"
min-width="40"
>
{{ page }}
</v-btn>
<span v-else class="mx-1">...</span>
</template>
<v-btn
:disabled="currentPage === totalPages"
@click="nextPage"
variant="text"
size="small"
>
Next
</v-btn>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const props = defineProps({
items: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['search', 'export-laporan', 'export-laporan-per-klinik'])
// Filter states
const filterDate = ref('')
const filterStatus = ref('Semua')
const search = ref('')
// Pagination states
const itemsPerPage = ref(10)
const currentPage = ref(1)
// Status options
const statusOptions = ['Semua', 'Online', 'Offline', 'Tunggu Daftar', 'Barcode']
// Table headers
const headers = [
{ title: 'No', key: 'no', sortable: false, width: '60px' },
{ title: 'Tgl Periksa', key: 'tglPeriksa', sortable: true },
{ title: 'NIK', key: 'nik', sortable: true },
{ title: 'RM', key: 'rm', sortable: true },
{ title: 'Barcode', key: 'barcode', sortable: true },
{ title: 'No Antrian', key: 'noAntrian', sortable: true },
{ title: 'Klinik', key: 'klinik', sortable: true },
{ title: 'First Name Last Name', key: 'fullName', sortable: true },
{ title: 'Shift', key: 'shift', sortable: true },
{ title: 'Pembayaran', key: 'pembayaran', sortable: true },
{ title: 'Keterangan', key: 'keterangan', sortable: true },
{ title: 'Status', key: 'status', sortable: true }
]
// Computed properties
const filteredItems = computed(() => {
let filtered = props.items
// Filter by date
if (filterDate.value) {
filtered = filtered.filter(item =>
item.tglPeriksa === filterDate.value
)
}
// Filter by status
if (filterStatus.value && filterStatus.value !== 'Semua') {
filtered = filtered.filter(item =>
item.status === filterStatus.value
)
}
return filtered
})
const totalFilteredItems = computed(() => filteredItems.value.length)
const totalPages = computed(() => Math.ceil(totalFilteredItems.value / itemsPerPage.value))
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value
const end = start + itemsPerPage.value
return filteredItems.value.slice(start, end).map((item, index) => ({
...item,
no: start + index + 1
}))
})
const currentPageStart = computed(() =>
totalFilteredItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1
)
const currentPageEnd = computed(() =>
Math.min(currentPage.value * itemsPerPage.value, totalFilteredItems.value)
)
const visiblePages = computed(() => {
const pages = []
const total = totalPages.value
const current = currentPage.value
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i)
}
} else {
if (current <= 4) {
for (let i = 1; i <= 5; i++) {
pages.push(i)
}
pages.push('...')
pages.push(total)
} else if (current >= total - 3) {
pages.push(1)
pages.push('...')
for (let i = total - 4; i <= total; i++) {
pages.push(i)
}
} else {
pages.push(1)
pages.push('...')
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i)
}
pages.push('...')
pages.push(total)
}
}
return pages
})
// Methods
const searchData = () => {
currentPage.value = 1
emit('search', {
date: filterDate.value,
status: filterStatus.value
})
}
const exportLaporan = () => {
emit('export-laporan')
}
const exportLaporanPerKlinik = () => {
// Navigate to laporan pasien per klinik page
navigateTo('/laporan-pasien-per-klinik')
}
const getStatusColor = (status) => {
const colorMap = {
'Online': 'success',
'Offline': 'error',
'Tunggu Daftar': 'warning',
'Barcode': 'info'
}
return colorMap[status] || 'default'
}
const getKeteranganClass = (keterangan) => {
return keterangan === 'Online' ? 'text-success' : ''
}
const previousPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const goToPage = (page) => {
currentPage.value = page
}
// Watchers
watch(itemsPerPage, () => {
currentPage.value = 1
})
watch([filterDate, filterStatus], () => {
currentPage.value = 1
})
</script>
<style scoped>
.text-success {
color: rgb(76, 175, 80) !important;
}
</style>
+299
View File
@@ -0,0 +1,299 @@
<template>
<v-app>
<!-- Navigation Drawer -->
<v-navigation-drawer
v-model="drawer"
:rail="rail"
permanent
app
color="white"
width="280"
rail-width="72"
>
<!-- Header Sidebar -->
<div class="pa-4 d-flex align-center" style="height: 64px;">
<v-icon color="#ff9248" size="32">mdi-hospital-building</v-icon>
<v-toolbar-title v-show="!rail" class="ml-3 text-h6">
Antrean RSSA
</v-toolbar-title>
</div>
<v-divider></v-divider>
<!-- Menu Items -->
<v-list density="default" nav class="py-2">
<v-list-subheader v-show="!rail" class="text-caption text-grey">
OVERVIEW
</v-list-subheader>
<template v-for="item in items" :key="item.title">
<!-- Item dengan Children -->
<v-menu
v-if="item.children"
open-on-hover
location="end"
:disabled="!rail"
>
<template v-slot:activator="{ props }">
<v-list-group v-if="!rail" :value="item.title">
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.title"
:active="item.title === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<v-list-item
v-for="child in item.children"
:key="child.title"
:to="child.to"
:title="child.title"
:active="child.to === currentRoute.path"
rounded="lg"
class="mx-2 my-1 pl-12"
>
</v-list-item>
</v-list-group>
<!-- Rail mode menu -->
<v-list-item
v-else
v-bind="props"
:prepend-icon="item.icon"
:active="item.title === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<!-- Submenu untuk rail mode -->
<v-list class="py-2" style="min-width: 200px;">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ item.title }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.title"
:to="child.to"
:title="child.title"
:active="child.to === currentRoute.path"
rounded="lg"
class="mx-2"
>
</v-list-item>
</v-list>
</v-menu>
<!-- Item tanpa Children -->
<v-list-item
v-else
:prepend-icon="item.icon"
:title="!rail ? item.title : ''"
:to="item.to"
:active="item.to === currentRoute.path"
rounded="lg"
class="mx-2 my-1"
>
<template v-slot:append v-if="item.badge && !rail">
<v-chip size="x-small" color="primary">{{ item.badge }}</v-chip>
</template>
</v-list-item>
</template>
<v-divider class="my-4"></v-divider>
<v-list-subheader v-show="!rail" class="text-caption text-grey">
ACCOUNT
</v-list-subheader>
<v-list-item
prepend-icon="mdi-cog-outline"
:title="!rail ? 'Settings' : ''"
to="/settings"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
<v-list-item
prepend-icon="mdi-logout"
:title="!rail ? 'Log out' : ''"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</v-list>
<template v-slot:append>
<!-- Theme Toggle -->
<div class="pa-4 d-flex justify-center align-center">
<v-icon color="primary" size="20">mdi-white-balance-sunny</v-icon>
<v-switch
v-show="!rail"
v-model="darkMode"
hide-details
density="compact"
color="primary"
class="mx-2"
></v-switch>
<v-icon v-show="!rail" size="20">mdi-moon-waning-crescent</v-icon>
</div>
<v-divider></v-divider>
<!-- User Profile -->
<div class="pa-4">
<div class="d-flex align-center">
<v-avatar color="#ff9248" size="40">
<span class="text-white">AS</span>
</v-avatar>
<div v-show="!rail" class="ml-3 flex-grow-1">
<div class="text-subtitle-2 font-weight-bold">Adam Sulfat</div>
<div class="text-caption text-grey">adam@rssa.com</div>
</div>
<v-btn
v-show="!rail"
icon
size="small"
variant="text"
>
<v-icon size="20">mdi-dots-vertical</v-icon>
</v-btn>
</div>
</div>
</template>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<!-- Top Bar untuk Toggle -->
<v-app-bar flat color="transparent" height="64">
<v-btn
icon
@click="rail = !rail"
class="ml-2"
>
<v-icon>mdi-menu</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>mdi-bell-outline</v-icon>
</v-btn>
</v-app-bar>
<v-container fluid class="pa-6">
<slot></slot>
</v-container>
</v-main>
<!-- Footer -->
<v-footer app class="bg-grey-lighten-4">
<v-container fluid class="py-2">
<v-row no-gutters align="center">
<v-col cols="12" md="6">
<span class="text-caption text-grey-darken-2">
RSUD Dr. Saiful Anwar Malang | Jl. Jaksa Agung Suprapto No. 2 Malang | Telp: 0341-362101
</span>
</v-col>
<v-col cols="12" md="6" class="text-right">
<span class="text-caption text-grey-darken-2">
ITIKom Antrian RSSA Ver. 0.2
</span>
</v-col>
</v-row>
</v-container>
</v-footer>
</v-app>
</template>
<script setup>
import { ref, computed } from "vue";
import { useRoute } from 'vue-router';
// State
const drawer = ref(true);
const rail = ref(false);
const darkMode = ref(false);
// Menu Items
const items = ref([
{ title: "Dashboard", icon: "mdi-view-dashboard-outline", to: "/dashboard" },
// { title: "Marketplace", icon: "mdi-storefront-outline", to: "/marketplace" },
// { title: "My Properties", icon: "mdi-home-outline", to: "/properties" },
// { title: "Auctions", icon: "mdi-gavel", to: "/auctions" },
// { title: "Wallet", icon: "mdi-wallet-outline", to: "/wallet" },
// { title: "Favorites", icon: "mdi-heart-outline", to: "/favorites" },
{
title: "Setting",
icon: "mdi-cog-outline",
children: [
{ title: "Hak Akses", to: "/setting/hak-akses" },
{ title: "User Login", to: "/setting/user-login" },
{ title: "Master Loket", to: "/setting/master-loket" },
{ title: "Master Klinik", to: "/setting/master-klinik" },
{ title: "Master Klinik Ruang", to: "/setting/master-klinik-ruang" },
{ title: "Screen", to: "/setting/screen" },
],
},
{ title: "Loket Admin", icon: "mdi-account-supervisor-outline", to: "/loket-admin" },
{ title: "Ranap Admin", icon: "mdi-bed-outline", to: "/ranap-admin" },
{
title: "Anjungan",
icon: "mdi-account-box-multiple-outline",
children: [
{ title: "Anjungan", to: "/anjungan/anjungan" },
{ title: "Klinik Ruang", to: "/anjungan/AntrianKlinik"},
],
},
{ title: "Data Pasien", icon: "mdi-account-multiple-outline", to: "/data-pasien" },
]);
const currentRoute = useRoute();
const currentActiveMenu = computed(() => {
const currentItem = items.value.find(item => item.to === currentRoute.path);
if (currentItem) {
return currentItem.title;
}
for (const item of items.value) {
if (item.children) {
const childItem = item.children.find(child => child.to === currentRoute.path);
if (childItem) {
return item.title;
}
}
}
return '';
});
</script>
<style scoped>
.v-list-item--active {
background-color: #e8f5e9 !important;
color: #2e7d32 !important;
}
.v-list-item--active :deep(.v-list-item__prepend) {
color: #2e7d32 !important;
}
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
.v-navigation-drawer {
border-right: 1px solid #e0e0e0 !important;
}
</style>
+242
View File
@@ -0,0 +1,242 @@
<template>
<v-container fluid class="bg-grey-lighten-4 pa-4">
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-view-dashboard</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Admin Anjungan</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
</div>
</div>
</div>
<v-card flat>
<v-card-text>
<v-row align="center">
<v-col cols="12" md="4">
<v-text-field
label="Barcode"
placeholder="Masukkan Barcode"
outlined
dense
hide-details
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-chip color="#B71C1C" class="text-caption" text-color="white">
Tekan Enter. (Barcode depan nomor selalu ada huruf lain, Ex:
J20073010005 "Hiraukan huruf 'J' nya")
</v-chip>
</v-col>
<v-col cols="12" md="2">
<v-btn block color="#ff9248" style="color: white"
>Pendaftaran Online</v-btn
>
</v-col>
</v-row>
</v-card-text>
<v-divider :thickness="5" color="deep-orange-darken-4"></v-divider>
<TabelData
:headers="lateHeaders"
:items="lateVisitors"
title="DATA PENGUNJUNG TERLAMBAT"
/>
<v-divider :thickness="5" color="deep-orange-darken-4"></v-divider>
<TabelData
:headers="mainHeaders"
:items="mainPatients"
title="DATA PENGUNJUNG"
>
<template v-slot:actions="{ item }">
<div class="d-flex ga-1">
<v-btn
small
color="#ff9248"
class="d-flex flex-row"
variant="flat"
style="color: white"
>PASIEN</v-btn
>
<v-btn
small
color="grey-lighten-3"
class="d-flex flex-row"
variant="flat"
>PENGANTAR</v-btn
>
<v-btn small color="info" class="d-flex flex-row" variant="flat"
>ByPass</v-btn
>
</div>
</template>
</TabelData>
</v-card>
</v-container>
</template>
<script setup lang="ts">
import { ref } from "vue";
import TabelData from "../components/TabelData.vue"; // Pastikan path-nya benar
// Ini adalah data yang akan menjadi "single source of truth"
// untuk tabel Anda. Data ini dikirim sebagai props ke komponen anak.
const mainHeaders = ref([
{ title: "No", value: "no", sortable: false },
{ title: "Tgl Daftar", value: "tglDaftar", sortable: true },
{ title: "RM", value: "rm", sortable: true },
{ title: "Barcode", value: "barcode", sortable: true },
{ title: "No Antrian", value: "noAntrian", sortable: true },
{ title: "No Klinik", value: "noKlinik", sortable: true },
{ title: "Shift", value: "shift", sortable: true },
{ title: "Klinik", value: "klinik", sortable: true },
{ title: "Pembayaran", value: "pembayaran", sortable: true },
{ title: "Masuk", value: "masuk", sortable: true },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const mainPatients = ref([
{
no: 1,
tglDaftar: "12:49",
rm: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
noKlinik: "THT",
barcode: "2321232",
shift: "Shift 1",
klinik: "KANDUNGAN",
pembayaran: "UMUM",
masuk: "TIDAK",
status: "current",
},
{
no: 2,
tglDaftar: "18:23",
rm: "42081123200199",
noAntrian: "UM1001 | Online - 250811100163",
noKlinik: "THT",
barcode: "2321985",
shift: "Shift 1",
klinik: "DALAM",
pembayaran: "UMUM",
masuk: "TIDAK",
status: "current",
},
{
no: 3,
tglDaftar: "02:19",
rm: "15092710084",
noAntrian: "UM1001 | Online - 250811100163",
noKlinik: "THT",
barcode: "2321777",
shift: "Shift 1",
klinik: "ANAK",
pembayaran: "UMUM",
masuk: "TIDAK",
status: "current",
},
{
no: 4,
tglDaftar: "10:09",
rm: "250254310011",
noAntrian: "UM1001 | Online - 250811100163",
noKlinik: "THT",
barcode: "2321298",
shift: "Shift 1",
klinik: "JANTUNG",
pembayaran: "UMUM",
masuk: "TIDAK",
status: "current",
},
]);
const lateHeaders = ref([
{ title: "No", value: "no", sortable: false },
// Tambahkan headers spesifik untuk tabel ini jika berbeda
]);
const lateVisitors = ref([
// Tambahkan data spesifik untuk tabel ini jika ada
]);
// ... Sisa kode lainnya yang tidak terkait dengan tabel ...
const items = ref([
{ title: "Dashboard", icon: "mdi-view-dashboard", to: "/dashboard" },
{
title: "Setting",
icon: "mdi-cog",
children: [
{ title: "Hak Akses", to: "/setting/hak-akses" },
{ title: "User Login", to: "/setting/user-login" },
{ title: "Master Loket", to: "/setting/master-loket" },
{ title: "Master Klinik", to: "/setting/master-klinik" },
{ title: "Master Klinik Ruang", to: "/setting/master-klinik-ruang" },
{ title: "Screen", to: "/setting/screen" },
],
},
{ title: "Loket Admin", icon: "mdi-account-supervisor" },
{ title: "Ranap Admin", icon: "mdi-bed" },
{ title: "Klinik Admin", icon: "mdi-hospital-box" },
{ title: "Klinik Ruang Admin", icon: "mdi-hospital-marker" },
{ title: "Anjungan", icon: "mdi-account-box-multiple", to: "/anjungan" },
{ title: "Fast Track", icon: "mdi-clock-fast" },
{ title: "Data Pasien", icon: "mdi-account-multiple" },
{ title: "Screen", icon: "mdi-monitor" },
{ title: "List Pasien", icon: "mdi-format-list-bulleted" },
]);
</script>
<style scoped>
.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;
}
</style>
+686
View File
@@ -0,0 +1,686 @@
<!-- pages/Anjungan.vue -->
<template>
<div class="anjungan-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-hospital-building</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Anjungan RSSA</h1>
<p class="page-subtitle">Pilih Klinik untuk Pendaftaran</p>
</div>
</div>
<div class="header-right">
<v-chip color="white" variant="flat" class="instruction-chip">
<v-icon start size="16">mdi-information</v-icon>
Hijau: Tersedia | Merah: Tutup/Penuh
</v-chip>
</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="selectedStatus"
:items="statusOptions"
label="Filter Status"
density="compact"
variant="outlined"
hide-details
clearable
style="min-width: 160px;"
/>
<v-text-field
v-model="searchQuery"
label="Cari Klinik"
density="compact"
variant="outlined"
prepend-inner-icon="mdi-magnify"
hide-details
clearable
style="min-width: 200px;"
/>
</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>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card> -->
<!-- Clinic Cards -->
<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-hospital-marker</v-icon>
<span>Daftar Klinik - {{ filteredClinics.length }} dari {{ totalClinics }} klinik</span>
</v-card-title> -->
<v-card-text class="pa-6">
<v-row>
<v-col
v-for="clinic in filteredClinics"
:key="clinic.id"
cols="12"
sm="6"
md="4"
lg="3"
class="pa-2"
>
<v-card
:class="{
'clinic-card': true,
'clinic-available': clinic.available,
'clinic-closed': !clinic.available
}"
:disabled="!clinic.available"
@click="selectClinic(clinic)"
elevation="2"
>
<v-card-text class="text-center pa-4">
<!-- Status Chip -->
<div class="mb-3">
<v-chip
:color="clinic.available ? 'success' : 'error'"
size="small"
variant="flat"
>
{{ clinic.available ? 'TERSEDIA' : 'TUTUP' }}
</v-chip>
</div>
<!-- Icon -->
<div class="clinic-icon-wrapper mb-3">
<v-icon
:icon="clinic.icon"
size="40"
:color="clinic.available ? 'success' : 'error'"
></v-icon>
</div>
<!-- Clinic Name -->
<h3 class="text-h6 font-weight-bold mb-2">
{{ clinic.name }}
</h3>
<!-- Subtitle -->
<p v-if="clinic.subtitle" class="text-caption text-grey-darken-1 mb-2">
{{ clinic.subtitle }}
</p>
<!-- Shift Info -->
<div class="shift-info">
<v-chip
size="small"
:color="clinic.available ? 'info' : 'error'"
variant="outlined"
class="mb-2"
>
{{ clinic.shift }}
</v-chip>
<br>
<span v-if="clinic.schedule" class="text-caption text-grey-darken-1">
{{ clinic.schedule }}
</span>
</div>
<!-- Action Button -->
<div class="mt-3">
<v-btn
v-if="clinic.available"
color="success"
variant="flat"
size="small"
block
>
Pilih Klinik
</v-btn>
<v-btn
v-else
color="error"
variant="outlined"
size="small"
disabled
block
>
Tidak Tersedia
</v-btn>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Empty State -->
<div v-if="filteredClinics.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-hospital-marker-outline</v-icon>
<h3 class="text-h6 mt-4 text-grey-darken-1">Tidak ada klinik 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>
<!-- Selection Dialog -->
<v-dialog v-model="showDialog" max-width="500">
<v-card>
<v-card-title class="d-flex align-center bg-primary text-white">
<v-icon class="mr-2">mdi-check-circle</v-icon>
Konfirmasi Pilihan
</v-card-title>
<v-card-text class="pa-6" v-if="selectedClinic">
<div class="text-center">
<div class="mb-3">
<v-icon :icon="selectedClinic.icon" size="48" color="primary"></v-icon>
</div>
<h3 class="text-h5 font-weight-bold mb-2">{{ selectedClinic.name }}</h3>
<p v-if="selectedClinic.subtitle" class="text-body-1 text-grey-darken-1 mb-3">
{{ selectedClinic.subtitle }}
</p>
<v-divider class="my-4"></v-divider>
<div class="text-left">
<p><strong>Shift:</strong> {{ selectedClinic.shift }}</p>
<p v-if="selectedClinic.schedule"><strong>Jadwal:</strong> {{ selectedClinic.schedule }}</p>
<p><strong>Status:</strong> <span class="text-success">Tersedia</span></p>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer />
<v-btn @click="showDialog = false" variant="text">
Batal
</v-btn>
<v-btn
color="primary"
@click="proceedToRegistration"
variant="flat"
>
Lanjutkan
</v-btn>
</v-card-actions>
</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 @click="snackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
definePageMeta({
layout: false, // Disables the layout for this specific page
});
// Reactive data
const loading = ref(false)
const selectedStatus = ref(null)
const searchQuery = ref('')
const showDialog = ref(false)
const selectedClinic = ref(null)
const snackbar = ref(false)
const snackbarText = ref('')
const snackbarColor = ref('success')
// Options
const statusOptions = ['Tersedia', 'Tutup']
// Clinic data
const clinics = ref([
{
id: 1,
name: 'ANAK',
subtitle: '',
icon: 'mdi-baby-face',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 2,
name: 'ANESTESI',
subtitle: '',
icon: 'mdi-sleep',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 3,
name: 'BEDAH',
subtitle: '',
icon: 'mdi-medical-bag',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 4,
name: 'GERIATRI',
subtitle: '',
icon: 'mdi-account-supervisor',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 5,
name: 'GIGI DAN MULUT',
subtitle: 'GIGI DAN MULUT',
icon: 'mdi-tooth',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 6,
name: 'GIZI',
subtitle: '',
icon: 'mdi-food-apple',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 7,
name: 'HOM',
subtitle: 'HEMATO ONKOLOGI MEDIS',
icon: 'mdi-water',
shift: 'TUTUP',
schedule: '',
available: false,
},
{
id: 8,
name: 'IPD',
subtitle: 'PENYAKIT DALAM',
icon: 'mdi-hospital',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 9,
name: 'JANTUNG',
subtitle: 'CARDIOLOGI',
icon: 'mdi-heart-pulse',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 10,
name: 'JIWA',
subtitle: '',
icon: 'mdi-brain',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 11,
name: 'KANDUNGAN',
subtitle: '',
icon: 'mdi-human-pregnant',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 12,
name: 'KEMOTERAPI',
subtitle: '',
icon: 'mdi-needle',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 13,
name: 'KOMPLEMENTER',
subtitle: 'NYERI',
icon: 'mdi-leaf',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 14,
name: 'KUL KEL',
subtitle: 'KULIT KELAMIN',
icon: 'mdi-hand-back-right',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 15,
name: 'MATA',
subtitle: '',
icon: 'mdi-eye',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 16,
name: 'MCU',
subtitle: '',
icon: 'mdi-clipboard-check',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 17,
name: 'ONKOLOGI',
subtitle: '',
icon: 'mdi-ribbon',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 18,
name: 'PARU',
subtitle: '',
icon: 'mdi-lungs',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 19,
name: 'R. TINDAKAN',
subtitle: 'EMG, ECG, DLL',
icon: 'mdi-monitor-heart-rate',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 20,
name: 'RADIOTERAPI',
subtitle: '',
icon: 'mdi-radioactive',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 21,
name: 'REHAB MEDIK',
subtitle: '',
icon: 'mdi-human-cane',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
},
{
id: 22,
name: 'SARAF',
subtitle: 'NEUROLOGI',
icon: 'mdi-head-cog',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: false,
},
{
id: 23,
name: 'THT',
subtitle: '',
icon: 'mdi-ear-hearing',
shift: 'SHIFT 1',
schedule: 'Mulai Pukul 07:00',
available: true,
}
])
// Computed properties
const filteredClinics = computed(() => {
let filtered = clinics.value
if (selectedStatus.value) {
const isAvailable = selectedStatus.value === 'Tersedia'
filtered = filtered.filter(clinic => clinic.available === isAvailable)
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(clinic =>
clinic.name.toLowerCase().includes(query) ||
clinic.subtitle.toLowerCase().includes(query)
)
}
return filtered
})
const totalClinics = computed(() => clinics.value.length)
// Methods
const showSnackbar = (text, color = 'success') => {
snackbarText.value = text
snackbarColor.value = color
snackbar.value = true
}
const selectClinic = (clinic) => {
if (clinic.available) {
selectedClinic.value = clinic
showDialog.value = true
}
}
const proceedToRegistration = () => {
showSnackbar(`Mengarahkan ke pendaftaran ${selectedClinic.value.name}...`, 'success')
console.log('Proceeding to registration for:', selectedClinic.value.name)
showDialog.value = false
}
const refreshData = () => {
loading.value = true
setTimeout(() => {
loading.value = false
showSnackbar('Status klinik berhasil diperbarui', 'success')
}, 1000)
}
// Lifecycle
onMounted(() => {
refreshData()
})
</script>
<style scoped>
.anjungan-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;
}
.instruction-chip {
font-weight: 500;
color: #1976d2 !important;
}
.controls-card,
.main-content-card {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.clinic-card {
cursor: pointer;
border-radius: 16px !important;
height: 280px;
background: white;
}
.clinic-available {
border-left: 6px solid #4CAF50;
}
.clinic-closed {
opacity: 0.7;
cursor: not-allowed;
border-left: 6px solid #F44336;
background: #fafafa;
}
.clinic-icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 70px;
height: 70px;
background: rgba(76, 175, 80, 0.1);
border-radius: 50%;
margin: 0 auto;
}
.clinic-closed .clinic-icon-wrapper {
background: rgba(244, 67, 54, 0.1);
}
.shift-info {
margin-top: 8px;
}
/* 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) {
.anjungan-container {
padding: 16px;
}
.header-content {
padding: 24px 20px;
}
.page-title {
font-size: 24px;
}
.header-icon {
padding: 12px;
}
.clinic-card {
height: 260px;
}
.clinic-icon-wrapper {
width: 60px;
height: 60px;
}
}
@media (max-width: 600px) {
.clinic-card {
height: 240px;
}
.page-title {
font-size: 20px;
}
}
</style>
+543
View File
@@ -0,0 +1,543 @@
<!-- pages/AntrianKlinik.vue -->
<template>
<div class="tv-display-container">
<!-- Header -->
<div class="tv-header">
<div class="tv-header-left">
<div class="hospital-logo">
<v-icon size="64" color="white">mdi-hospital-box</v-icon>
</div>
<div class="header-text">
<h1 class="hospital-name">ANTRIAN KLINIK</h1>
<p class="display-title">RSUD Dr. Saiful Anwar Provinsi Jawa Timur</p>
</div>
</div>
<div class="tv-header-right">
<div class="current-time">{{ currentTime }}</div>
<div class="current-date">{{ currentDate }}</div>
</div>
</div>
<!-- Current Called Queue - Super Prominent -->
<div v-if="currentCalledQueue" class="hero-queue">
<div class="hero-label">SEDANG DIPANGGIL</div>
<div class="hero-number">{{ currentCalledQueue.noAntrian.split(' |')[0] }}</div>
<div class="hero-clinic">{{ currentCalledQueue.klinik }}</div>
</div>
<!-- Clinic Grid - 3x3 Layout -->
<div class="clinic-grid">
<div
v-for="clinic in displayedClinics"
:key="clinic.name"
class="clinic-card"
>
<div class="clinic-name">{{ clinic.name }}</div>
<div class="queue-display">
<!-- Show current queue number -->
<div v-if="clinic.currentQueue" class="current-queue-large">
<div class="queue-label">SEKARANG</div>
<div class="queue-number-huge">
{{ clinic.currentQueue.noAntrian.split(' |')[0] }}
</div>
</div>
<!-- Show next queues -->
<div v-if="clinic.nextQueues.length > 0" class="next-queues">
<div class="queue-label-small">SELANJUTNYA</div>
<div class="next-queue-numbers">
<span
v-for="(queue, index) in clinic.nextQueues.slice(0, 3)"
:key="queue.no"
class="next-number"
>
{{ queue.noAntrian.split(' |')[0] }}
</span>
</div>
</div>
<!-- Empty state -->
<div v-if="!clinic.currentQueue && clinic.nextQueues.length === 0" class="empty-queue">
<v-icon size="48" color="grey-lighten-2">mdi-checkbox-blank-circle-outline</v-icon>
<div class="empty-text">Tidak Ada Antrian</div>
</div>
</div>
<!-- Queue count badge -->
<div class="queue-count">
<v-icon size="20" class="mr-1">mdi-account-multiple</v-icon>
{{ clinic.totalQueues }}
</div>
</div>
</div>
<!-- Footer Info -->
<div class="tv-footer">
<div class="footer-stats">
<div class="stat-box">
<div class="stat-value">{{ statistics.total }}</div>
<div class="stat-label">Total Antrian</div>
</div>
<div class="stat-box">
<div class="stat-value">{{ statistics.waiting }}</div>
<div class="stat-label">Menunggu</div>
</div>
<div class="stat-box">
<div class="stat-value">{{ statistics.active }}</div>
<div class="stat-label">Sedang Dilayani</div>
</div>
</div>
<div class="footer-message">
<v-icon size="24" class="mr-2">mdi-information</v-icon>
Harap perhatikan nomor antrian Anda di layar
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useQueueStore } from '../stores/queueStore'
definePageMeta({
layout: false,
});
// Store
const queueStore = useQueueStore()
// Reactive data
const currentTime = ref('')
const currentDate = ref('')
let timeInterval = null
let refreshInterval = null
// Computed - Get all klinik patients (stage: klinik)
const klinikPatients = computed(() => {
return queueStore.getPatientsByStage('klinik').value.all
})
// Get clinics with their queues organized
const displayedClinics = computed(() => {
const allKliniks = queueStore.kliniks.slice(0, 9) // Max 9 clinics for 3x3 grid
return allKliniks.map(klinik => {
const queues = klinikPatients.value
.filter(p => p.klinik === klinik.name)
.sort((a, b) => {
// Sort by status priority then time
const statusPriority = {
'di-loket': 1,
'waiting': 2,
'terlambat': 3,
'pending': 4
}
const priorityDiff = (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99)
if (priorityDiff !== 0) return priorityDiff
// Sort by time if same status
const timeA = a.jamPanggil.split(':').map(Number)
const timeB = b.jamPanggil.split(':').map(Number)
return timeA[0] * 60 + timeA[1] - (timeB[0] * 60 + timeB[1])
})
// Get current (first di-loket or first waiting)
const currentQueue = queues.find(q => q.status === 'di-loket') ||
(queues.find(q => q.status === 'waiting') || null)
// Get next queues (excluding current)
const nextQueues = queues.filter(q =>
q.no !== currentQueue?.no &&
(q.status === 'waiting' || q.status === 'di-loket')
)
return {
name: klinik.name,
currentQueue: currentQueue,
nextQueues: nextQueues,
totalQueues: queues.length
}
})
})
// Current queue being called (most recent di-loket)
const currentCalledQueue = computed(() => {
const calledQueues = klinikPatients.value
.filter(p => p.status === 'di-loket')
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
return calledQueues[0] || null
})
// Statistics
const statistics = computed(() => {
const all = klinikPatients.value
return {
total: all.length,
waiting: all.filter(p => p.status === 'waiting').length,
active: all.filter(p => p.status === 'di-loket').length
}
})
// Methods
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
currentDate.value = now.toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
// Lifecycle
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
// Auto refresh every 5 seconds
refreshInterval = setInterval(() => {
// Data will auto-update due to reactive store
}, 5000)
})
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
if (refreshInterval) clearInterval(refreshInterval)
})
</script>
<style scoped>
.tv-display-container {
background: linear-gradient(135deg, #e7e7e7 0%, #FFDCAF 100%);
min-height: 100vh;
padding: 20px;
font-family: 'Roboto', sans-serif;
color: #fff;
overflow: hidden;
}
/* Header */
.tv-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #FAFAFA;
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 20px 40px;
margin-bottom: 20px;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.3);
}
.tv-header-left {
display: flex;
align-items: center;
gap: 20px;
}
.hospital-logo {
background: #1e3c72;
border-radius: 50%;
padding: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.hospital-name {
font-size: 36px;
margin: 0;
letter-spacing: 2px;
color: #1e3c72;
line-height: 1;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.display-title {
font-size: 24px;
font-weight: 600;
margin: 0;
color: #1e3c72;
opacity: 0.9;
}
.tv-header-right {
text-align: right;
}
.current-time {
font-size: 48px;
font-weight: 700;
line-height: 1;
color: #1e3c72;
text-shadow: 2px 2px 4px rgba(124, 124, 124, 0.3);
}
.current-date {
font-size: 18px;
opacity: 0.9;
margin-top: 5px;
}
/* Hero Queue - Sedang Dipanggil */
.hero-queue {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
border-radius: 24px;
padding: 30px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 12px 48px rgba(255, 107, 107, 0.4);
animation: pulse-glow 2s infinite;
}
.hero-label {
font-size: 32px;
font-weight: 700;
letter-spacing: 3px;
margin-bottom: 10px;
text-transform: uppercase;
}
.hero-number {
font-size: 120px;
font-weight: 900;
line-height: 1;
margin: 10px 0;
text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.3);
letter-spacing: 4px;
}
.hero-clinic {
font-size: 40px;
font-weight: 600;
margin-top: 10px;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 12px 48px rgba(255, 107, 107, 0.4);
}
50% {
box-shadow: 0 16px 64px rgba(255, 107, 107, 0.6);
}
}
/* Clinic Grid - 3x3 */
.clinic-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.clinic-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
position: relative;
min-height: 200px;
display: flex;
flex-direction: column;
}
.clinic-name {
font-size: 28px;
font-weight: 800;
color: #1e3c72;
text-align: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 3px solid #2a5298;
text-transform: uppercase;
letter-spacing: 1px;
}
.queue-display {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* Current Queue Display */
.current-queue-large {
text-align: center;
margin-bottom: 10px;
}
.queue-label {
font-size: 18px;
font-weight: 600;
color: #4caf50;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.queue-number-huge {
font-size: 72px;
font-weight: 900;
color: #1e3c72;
line-height: 1;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
/* Next Queues */
.next-queues {
text-align: center;
}
.queue-label-small {
font-size: 14px;
font-weight: 600;
color: #666;
margin-bottom: 5px;
text-transform: uppercase;
}
.next-queue-numbers {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.next-number {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 12px;
font-size: 24px;
font-weight: 700;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
/* Empty State */
.empty-queue {
text-align: center;
opacity: 0.5;
}
.empty-text {
font-size: 18px;
color: #999;
margin-top: 10px;
font-weight: 500;
}
/* Queue Count Badge */
.queue-count {
position: absolute;
top: 15px;
right: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Footer */
.tv-footer {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.footer-stats {
display: flex;
gap: 40px;
}
.stat-box {
text-align: center;
}
.stat-value {
font-size: 42px;
font-weight: 900;
line-height: 1;
color:#1e3c72;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.stat-label {
font-size: 16px;
opacity: 0.9;
margin-top: 5px;
color:#1e3c72;
font-weight: 500;
}
.footer-message {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
opacity: 0.95;
color:#1e3c72;
}
/* Responsive for different TV sizes */
@media (max-width: 1920px) {
.hero-number {
font-size: 100px;
}
.queue-number-huge {
font-size: 64px;
}
}
@media (max-width: 1366px) {
.clinic-grid {
gap: 10px;
}
.clinic-card {
padding: 15px;
min-height: 180px;
}
.clinic-name {
font-size: 24px;
}
.queue-number-huge {
font-size: 56px;
}
.hero-number {
font-size: 90px;
}
}
/* Animation for smooth transitions */
.clinic-card {
transition: all 0.3s ease;
}
.queue-number-huge,
.hero-number {
transition: all 0.3s ease;
}
</style>
+1106
View File
File diff suppressed because it is too large Load diff
+635
View File
@@ -0,0 +1,635 @@
<template>
<div class="pasien-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-account-group</v-icon>
</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>
<!-- Filter Controls
<v-card class="filter-controls-card mb-4" elevation="2">
<v-card-text class="py-4">
<v-row align="center">
<v-col cols="12" md="8">
<div class="d-flex align-center flex-wrap gap-10">
<span class="text-subtitle-1 font-weight-medium">Filter Cepat:</span>
<v-btn
color="primary"
variant="flat"
size="default"
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 -->
<v-overlay v-model="loading" class="align-center justify-center">
<v-progress-circular
color="primary"
indeterminate
size="64"
/>
</v-overlay>
<!-- Snackbar -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="top right"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
icon
@click="snackbar.show = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import TabelListPasien from '~/components/TabelListPasien.vue'
// Meta untuk SEO
definePageMeta({
title: 'List Pasien',
layout: 'default'
})
// Reactive states
const loading = ref(false)
const currentFilter = ref('')
const snackbar = ref({
show: false,
message: '',
color: 'success'
})
// Sample data
const pasienData = ref([
{
tglPeriksa: '27/08/2025',
nik: '3507264104730004',
rm: '11412584',
barcode: '250627100001',
noAntrian: 'HQ1001',
klinik: 'HOM',
fullName: 'Binti Almatul',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Online',
status: 'Tunggu Daftar'
},
{
tglPeriksa: '27/08/2025',
nik: '3504096063630001',
rm: '',
barcode: '250627100002',
noAntrian: 'QB1001',
klinik: 'KANDUNGAN',
fullName: 'maret kumalal',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Online',
status: 'Tunggu Daftar'
},
{
tglPeriksa: '27/08/2025',
nik: '3507114102250002',
rm: '11555560',
barcode: '250627100003',
noAntrian: 'QB1002',
klinik: 'KANDUNGAN',
fullName: 'ayu rafti lelu amanda',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Online',
status: 'Tunggu Daftar'
},
{
tglPeriksa: '27/08/2025',
nik: '3508185040150002',
rm: '11333655',
barcode: '250627100004',
noAntrian: 'AN1001',
klinik: 'ANAK',
fullName: 'Erin Wahyuni',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Online',
status: 'Tunggu Daftar'
},
{
tglPeriksa: '27/08/2025',
nik: '3515085040110004',
rm: '11585554',
barcode: '250627100005',
noAntrian: 'IP1001',
klinik: 'IPD',
fullName: 'Yohana Karina Pusplta Sari',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Online',
status: 'Tunggu Daftar'
},
{
tglPeriksa: '27/08/2025',
nik: '3506246105750002',
rm: '11527608',
barcode: '250627100006',
noAntrian: 'IP1001',
klinik: 'IPD',
fullName: 'Elok Suharsti',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Online',
status: 'Tunggu Daftar'
},
{
tglPeriksa: '27/08/2025',
nik: '',
rm: '',
barcode: '250627100007',
noAntrian: 'HQ1002',
klinik: 'HOM',
fullName: '',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Offline',
status: 'Barcode'
},
{
tglPeriksa: '27/08/2025',
nik: '',
rm: '',
barcode: '250627100008',
noAntrian: 'IP1002',
klinik: 'IPD',
fullName: '',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Offline',
status: 'Barcode'
},
{
tglPeriksa: '27/08/2025',
nik: '',
rm: '',
barcode: '250627100009',
noAntrian: 'IP1001',
klinik: 'IPD',
fullName: '',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Offline',
status: 'Barcode'
},
{
tglPeriksa: '27/08/2025',
nik: '',
rm: '',
barcode: '250627100010',
noAntrian: 'IP1001',
klinik: 'IPD',
fullName: '',
shift: 'Shift 1',
pembayaran: 'JKN',
keterangan: 'Offline',
status: 'Barcode'
}
])
// 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
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) => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Search filters:', filters)
showSnackbar('Data berhasil difilter', 'success')
} catch (error) {
console.error('Error searching data:', error)
showSnackbar('Gagal memfilter data', 'error')
} finally {
loading.value = false
}
}
const handleExportLaporan = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 2000))
console.log('Exporting laporan pasien...')
showSnackbar('Laporan pasien berhasil diexport', 'success')
} catch (error) {
console.error('Error exporting laporan:', error)
showSnackbar('Gagal export laporan pasien', 'error')
} finally {
loading.value = false
}
}
const handleExportLaporanPerKlinik = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 2000))
console.log('Exporting laporan pasien per klinik...')
showSnackbar('Laporan pasien per klinik berhasil diexport', 'success')
} catch (error) {
console.error('Error exporting laporan per klinik:', error)
showSnackbar('Gagal export laporan pasien per klinik', 'error')
} finally {
loading.value = false
}
}
const fetchPasienData = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Patient data loaded successfully')
} catch (error) {
console.error('Error fetching patient data:', error)
showSnackbar('Gagal memuat data pasien', 'error')
} finally {
loading.value = false
}
}
// Lifecycle
onMounted(() => {
fetchPasienData()
})
// Head untuk SEO
useHead({
title: 'List Pasien - Antrean RSSA',
meta: [
{
name: 'description',
content: 'Daftar master data seluruh pasien rumah sakit'
}
]
})
</script>
<style scoped>
.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>
+754
View File
@@ -0,0 +1,754 @@
<!-- pages/LoketAdmin.vue -->
<template>
<div class="loket-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-view-dashboard</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Loket Admin</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
</div>
<div class="header-right">
<v-chip color="success" variant="flat" class="mr-2">
Total {{ totalPasien }} Pasien
</v-chip>
<v-chip color="white" variant="flat" class="text-primary">
Max 150 Pasien
</v-chip>
</div>
</div>
</div>
<div>
<v-card class="next-patient-card mb-4" elevation="2">
<v-card-text class="text-center pa-6">
<v-chip color="success" variant="flat" class="mb-3" size="large">
<v-icon start>mdi-account-arrow-right</v-icon>
PASIEN SELANJUTNYA
</v-chip>
<div class="text-h4 font-weight-bold mb-2 text-success">
{{ nextPatient ? nextPatient.noAntrian.split(" |")[0] : "UM1004" }}
</div>
<div class="text-body-1 mb-4 text-grey-darken-1">
Klik tombol hijau untuk memanggil
</div>
<v-btn
color="success"
variant="flat"
size="large"
class="px-8"
@click="callNext"
:disabled="!nextPatient"
>
<v-icon start>mdi-microphone</v-icon>
PANGGIL NEXT
</v-btn>
</v-card-text>
</v-card>
</div>
<!-- Combined Control Section -->
<v-row class="mb-4">
<!-- Left Side: Call Controls and Current Processing -->
<v-col cols="12" lg="6">
<!-- Current Patient Processing Card -->
<v-card
v-if="currentProcessingPatient"
class="current-processing-card"
elevation="2"
>
<v-card-text class="pa-4">
<div class="d-flex align-center justify-space-between">
<div class="patient-info">
<v-chip color="primary" variant="flat" class="mb-2">
<v-icon start>mdi-account-clock</v-icon>
SEDANG DIPROSES
</v-chip>
<div class="text-h5 font-weight-bold mb-1">
{{ currentProcessingPatient.noAntrian.split(" |")[0] }}
</div>
<div class="text-subtitle-1 text-grey-darken-1">
{{ currentProcessingPatient.barcode }} |
{{ currentProcessingPatient.klinik }}
</div>
</div>
<div class="action-buttons">
<v-btn
color="success"
variant="flat"
class="mr-2"
@click="processPatient(currentProcessingPatient, 'check-in')"
>
<v-icon start>mdi-check-circle</v-icon>
Check In
</v-btn>
<v-btn
color="warning"
variant="flat"
class="mr-2"
@click="processPatient(currentProcessingPatient, 'terlambat')"
>
<v-icon start>mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn
color="error"
variant="flat"
@click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Pending
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Right Side: Patient Queue Info -->
<v-col cols="12" lg="6">
<!-- Next Patient Card -->
<!-- Quota Info Card -->
<v-card class="quota-info-card" elevation="2">
<v-card-text class="pa-4">
<div class="text-center">
<div class="text-h6 font-weight-medium mb-3">
Panggil Antrean Anjungan
</div>
<v-row class="mb-3">
<v-col cols="6" class="text-center">
<div class="text-caption text-grey-darken-1">Kuota</div>
<div class="text-h4 font-weight-bold">150</div>
</v-col>
<v-col cols="6" class="text-center">
<div class="text-caption text-grey-darken-1">Tersedia</div>
<div class="text-h4 font-weight-bold text-success">
{{ 150 - quotaUsed }}
</div>
</v-col>
</v-row>
<div class="text-body-2 text-grey-darken-1 mb-2">
Total Quota Terpakai: {{ quotaUsed }}
</div>
<v-progress-linear
:model-value="(quotaUsed / 150) * 100"
color="success"
height="8"
rounded
></v-progress-linear>
</div>
</v-card-text>
</v-card>
<v-card-text class="call-controls-card align-center py-4">
<div class="text-subtitle-1 font-weight-medium"
>Panggil Pasien:</div>
<div class="d-flex align-center flex-wrap mb-3">
<v-btn
color="success"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(1)"
>
<span class="text-h6 font-weight-bold">1</span>
</v-btn>
<v-btn
color="info"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(5)"
>
<span class="text-h6 font-weight-bold">5</span>
</v-btn>
<v-btn
color="warning"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(10)"
>
<span class="text-h6 font-weight-bold">10</span>
</v-btn>
<v-btn
color="error"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(20)"
>
<span class="text-h6 font-weight-bold">20</span>
</v-btn>
</div>
</v-card-text>
</v-col>
</v-row>
<!-- Di Loket Patients Table -->
<v-card class="main-table-card mb-4" elevation="2">
<TabelData
:headers="diLoketHeaders"
:items="diLoketPatients"
title="DATA PASIEN - DI LOKET"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="info"
variant="flat"
@click="processPatient(item, 'aktifkan')"
>
Aktifkan
</v-btn>
<v-btn
size="small"
color="success"
variant="flat"
@click="processPatient(item, 'proses')"
>
Proses
</v-btn>
</div>
</template>
<template #item.jamPanggil="{ item }">
<span :class="getRowClass(item)">{{ item.jamPanggil }}</span>
</template>
</TabelData>
</v-card>
<!-- Terlambat Patients Table -->
<v-card
class="late-table-card mb-4"
elevation="2"
v-if="terlambatPatients.length > 0"
>
<TabelData
:headers="terlambatHeaders"
:items="terlambatPatients"
title="INFO PASIEN LAPOR TERLAMBAT"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="success"
variant="flat"
@click="processPatient(item, 'aktifkan')"
>
Aktifkan
</v-btn>
</div>
</template>
</TabelData>
</v-card>
<!-- Pending Patients Table -->
<v-card
class="pending-table-card mb-4"
elevation="2"
v-if="pendingPatients.length > 0"
>
<TabelData
:headers="pendingHeaders"
:items="pendingPatients"
title="INFO PASIEN PENDING"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="success"
variant="flat"
@click="processPatient(item, 'proses')"
>
Proses
</v-btn>
</div>
</template>
</TabelData>
</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 } from "vue";
import TabelData from "../components/TabelData.vue";
// Reactive data
const snackbar = ref(false);
const snackbarText = ref("");
const snackbarColor = ref("success");
const quotaUsed = ref(5);
const currentProcessingPatient = ref(null);
// Base patient data - semua pasien yang belum dipanggil
const allPatients = ref([
{
no: 1,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
shift: "Shift 1",
klinik: "KANDUNGAN",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting", // waiting, di-loket, terlambat, pending, processed
},
{
no: 2,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1002 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
{
no: 3,
jamPanggil: "09:30",
barcode: "250811100200",
noAntrian: "UM1003 | Online - 250811100200",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
{
no: 4,
jamPanggil: "14:15",
barcode: "250811100210",
noAntrian: "UM1004 | Online - 250811100210",
shift: "Shift 1",
klinik: "THT",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
},
...Array.from({ length: 16 }, (_, i) => ({
no: i + 5,
jamPanggil: `${String(Math.floor(Math.random() * 12) + 1).padStart(2, "0")}:${String(Math.floor(Math.random() * 60)).padStart(2, "0")}`,
barcode: `25081110${String(i + 300).padStart(4, "0")}`,
noAntrian: `UM100${i + 5} | Online - 25081110${String(i + 300).padStart(4, "0")}`,
shift: "Shift 1",
klinik: ["KANDUNGAN", "IPD", "THT", "SARAF"][Math.floor(Math.random() * 4)],
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting",
})),
]);
// Computed properties for different status tables
const diLoketPatients = computed(() =>
allPatients.value.filter((patient) => patient.status === "di-loket")
);
const terlambatPatients = computed(() =>
allPatients.value.filter((patient) => patient.status === "terlambat")
);
const pendingPatients = computed(() =>
allPatients.value.filter((patient) => patient.status === "pending")
);
const nextPatient = computed(() => {
return allPatients.value.find((patient) => patient.status === "waiting");
});
const totalPasien = computed(() => allPatients.value.length);
// Headers for different tables
const diLoketHeaders = ref([
{ title: "No", value: "no", sortable: false, width: "60px" },
{ title: "Jam Panggil", value: "jamPanggil", sortable: true, width: "100px" },
{ title: "Barcode", value: "barcode", sortable: true, width: "140px" },
{ title: "No Antrian", value: "noAntrian", sortable: true, width: "200px" },
{ title: "Shift", value: "shift", sortable: true, width: "80px" },
{ title: "Klinik", value: "klinik", sortable: true, width: "120px" },
{ title: "Fast Track", value: "fastTrack", sortable: true, width: "100px" },
{ title: "Pembayaran", value: "pembayaran", sortable: true, width: "100px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "200px" },
]);
const terlambatHeaders = ref([
{ title: "No", value: "no", sortable: false, width: "60px" },
{ title: "Barcode", value: "barcode", sortable: true, width: "140px" },
{ title: "No Antrian", value: "noAntrian", sortable: true, width: "200px" },
{ title: "Shift", value: "shift", sortable: true, width: "80px" },
{ title: "Klinik", value: "klinik", sortable: true, width: "120px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "100px" },
]);
const pendingHeaders = ref([
{ title: "#", value: "no", sortable: false, width: "60px" },
{ title: "Barcode", value: "barcode", sortable: true, width: "140px" },
{ title: "No Antrian", value: "noAntrian", sortable: true, width: "200px" },
{ title: "Shift", value: "shift", sortable: true, width: "80px" },
{ title: "Klinik", value: "klinik", sortable: true, width: "120px" },
{ title: "Fast Track", value: "fastTrack", sortable: true, width: "100px" },
{ title: "Pembayaran", value: "pembayaran", sortable: true, width: "100px" },
{ title: "Aksi", value: "aksi", sortable: false, width: "100px" },
]);
// Methods
const showSnackbar = (text, color = "success") => {
snackbarText.value = text;
snackbarColor.value = color;
snackbar.value = true;
};
const callMultiplePatients = (count) => {
const waitingPatients = allPatients.value.filter(
(patient) => patient.status === "waiting"
);
const patientsToCall = waitingPatients.slice(0, count);
if (patientsToCall.length === 0) {
showSnackbar("Tidak ada pasien yang menunggu", "warning");
return;
}
// Check quota
if (quotaUsed.value + patientsToCall.length > 150) {
showSnackbar("Quota tidak mencukupi", "error");
return;
}
// Move patients to "di-loket" status
patientsToCall.forEach((patient) => {
patient.status = "di-loket";
});
quotaUsed.value += patientsToCall.length;
showSnackbar(`Memanggil ${patientsToCall.length} pasien ke loket`, "success");
};
const callNext = () => {
if (!nextPatient.value) {
showSnackbar("Tidak ada pasien selanjutnya", "warning");
return;
}
if (quotaUsed.value >= 150) {
showSnackbar("Quota sudah penuh", "error");
return;
}
// Move next patient to processing
nextPatient.value.status = "di-loket";
currentProcessingPatient.value = nextPatient.value;
quotaUsed.value++;
showSnackbar(
`Memanggil pasien ${nextPatient.value.noAntrian.split(" |")[0]}`,
"success"
);
};
const processPatient = (patient, action) => {
const patientCode = patient.noAntrian.split(" |")[0];
switch (action) {
case "check-in":
patient.status = "processed";
if (currentProcessingPatient.value?.no === patient.no) {
currentProcessingPatient.value = null;
}
showSnackbar(`Pasien ${patientCode} berhasil check in`, "success");
break;
case "terlambat":
patient.status = "terlambat";
if (currentProcessingPatient.value?.no === patient.no) {
currentProcessingPatient.value = null;
}
showSnackbar(`Pasien ${patientCode} ditandai terlambat`, "warning");
break;
case "pending":
patient.status = "pending";
if (currentProcessingPatient.value?.no === patient.no) {
currentProcessingPatient.value = null;
}
showSnackbar(`Pasien ${patientCode} di-pending`, "info");
break;
case "aktifkan":
if (patient.status === "terlambat") {
patient.status = "di-loket";
showSnackbar(`Pasien ${patientCode} diaktifkan kembali`, "success");
}
break;
case "proses":
currentProcessingPatient.value = patient;
showSnackbar(`Memproses pasien ${patientCode}`, "info");
break;
}
};
const getRowClass = (item) => {
if (item.status === "current") {
return "text-success font-weight-bold";
}
return "";
};
</script>
<style scoped>
.loket-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;
}
.call-controls-card,
.next-patient-card,
.current-processing-card,
.quota-info-card,
.main-table-card,
.late-table-card,
.pending-table-card {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.next-patient-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 2px solid rgba(76, 175, 80, 0.2);
}
.current-processing-card {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 2px solid rgba(255, 152, 0, 0.2);
}
.quota-info-card {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border: 2px solid rgba(33, 150, 243, 0.2);
}
.patient-info .text-h5 {
color: #1976d2;
}
.action-buttons {
display: flex;
align-items: center;
}
/* Enhanced table styling */
.main-table-card :deep(.v-data-table th),
.late-table-card :deep(.v-data-table th),
.pending-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),
.late-table-card :deep(.v-data-table tbody tr:hover),
.pending-table-card :deep(.v-data-table tbody tr:hover) {
background: #f8fafc !important;
}
.main-table-card :deep(.v-data-table tbody td),
.late-table-card :deep(.v-data-table tbody td),
.pending-table-card :deep(.v-data-table tbody td) {
padding: 12px 16px;
border-bottom: 1px solid #f1f5f9;
}
/* Button styling */
.v-btn {
text-transform: none !important;
}
.v-btn--size-small {
height: 32px;
padding: 0 12px;
}
/* Success text color */
.text-success {
color: #4caf50 !important;
}
/* 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;
}
.current-processing-card .d-flex {
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.action-buttons {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.action-buttons .v-btn {
flex: 1;
min-width: 120px;
}
/* Stack layout vertically on medium screens */
.call-controls-card .d-flex {
justify-content: center;
}
}
@media (max-width: 768px) {
.loket-container {
padding: 16px;
}
.header-content {
padding: 24px 20px;
}
.page-title {
font-size: 24px;
}
.header-icon {
padding: 12px;
}
.call-controls-card .d-flex {
flex-direction: column;
align-items: center;
gap: 16px;
}
.call-controls-card .v-btn {
min-width: 80px;
}
/* Stack all control buttons vertically on mobile */
.call-controls-card .d-flex.flex-wrap {
flex-direction: column;
align-items: stretch;
}
.call-controls-card .v-btn {
width: 100%;
margin: 4px 0;
}
.action-buttons {
flex-direction: column;
width: 100%;
}
.action-buttons .v-btn {
width: 100%;
margin: 4px 0;
}
}
@media (max-width: 600px) {
.page-title {
font-size: 20px;
}
.next-patient-card .text-h4 {
font-size: 1.75rem !important;
}
.current-processing-card .patient-info .text-h5 {
font-size: 1.25rem !important;
}
.quota-info-card .text-h4 {
font-size: 1.5rem !important;
}
}
</style>
-447
View File
@@ -1,447 +0,0 @@
<template>
<v-app>
<v-layout>
<!-- App Bar Header -->
<v-app-bar app color="green darken-1" dark>
<v-app-bar-nav-icon @click="rail = !rail"></v-app-bar-nav-icon>
<v-toolbar-title class="ml-2">Antrean RSSA</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>mdi-account-circle</v-icon>
</v-btn>
<span class="mr-2">Rajal Bayu Nogroho</span>
</v-app-bar>
<!-- Navigation Drawer -->
<v-navigation-drawer v-model="drawer" :rail="rail" permanent app>
<v-list density="compact" nav>
<template v-for="item in items" :key="item.title">
<v-menu
v-if="item.children"
open-on-hover
location="end"
:nudge-right="8"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.title"
:value="item.title"
:class="{ 'v-list-item--active': item.title === currentPage }"
>
</v-list-item>
</template>
<v-list>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{
item.title
}}</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.title"
:to="child.to"
link
>
<v-list-item-title>{{ child.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-list-item
v-else
:prepend-icon="item.icon"
:title="item.title"
:to="item.to"
:value="item.title"
:class="{ 'v-list-item--active': item.title === currentPage }"
link
></v-list-item>
</template>
</v-list>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<v-container fluid class="pa-4">
<!-- Header Stats -->
<div class="d-flex justify-space-between align-center mb-4">
<div class="d-flex align-center">
<span class="text-h6 mr-4">Total 0</span>
<span class="text-body-2">Max 150 Pasien</span>
</div>
<div class="d-flex align-center">
<span class="mr-4">Dashboard</span>
<span class="mr-4">Loket 24 | Senin, 11 Agustus 2025</span>
<span class="mr-4">11 Agustus 2025 - Pelayanan</span>
</div>
</div>
<!-- Status Cards -->
<v-card class="pa-5 mb-5" color="white" flat></v-card>
<v-row align="center">
<v-col cols="1
2" md="1">
<v-card color="green" dark class="text-center">
<v-card-text>
<div class="text-h4">1</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="1">
<v-card color="blue" dark class="text-center">
<v-card-text>
<div class="text-h4">1</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="1">
<v-card color="orange" dark class="text-center">
<v-card-text>
<div class="text-h4">4</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="1">
<v-card color="red" dark class="text-center">
<v-card-text>
<div class="text-h4">30</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card>
<!-- Next Patient Card -->
<v-col cols="12" md="4">
<v-card color="green" dark class="mb-4">
<v-card-text class="text-center">
<div class="text-h4 mb-2">NEXT</div>
<div class="text-h6 mb-1">Pasien : UM1001</div>
<div class="text-body-2">
Klik untuk memanggil pasien selanjutnya
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Main Data Table -->
<v-card class="mb-4">
<v-card-title class="d-flex justify-space-between align-center">
<span>Data Pasien</span>
<div class="d-flex align-center">
<span class="mr-2">Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="outlined"
style="max-width: 80px"
class="mr-4"
></v-select>
<span class="mr-2">entries</span>
<span class="mr-4">Search:</span>
<v-text-field
v-model="search"
density="compact"
variant="outlined"
style="max-width: 200px"
hide-details
></v-text-field>
</div>
</v-card-title>
<v-data-table
:headers="mainHeaders"
:items="mainPatients"
:search="search"
:items-per-page="itemsPerPage"
class="elevation-1"
>
<template v-slot:item.aksi="{ item }">
<div class="d-flex ga-1">
<v-btn size="small" color="success" variant="flat"
>Panggil</v-btn
>
<v-btn size="small" color="info" variant="flat">Cancel</v-btn>
<v-btn size="small" color="primary" variant="flat"
>Selesai</v-btn
>
</div>
</template>
<template v-slot:item.jamPanggil="{ item }">
<span :class="getRowClass(item)">{{ item.jamPanggil }}</span>
</template>
</v-data-table>
</v-card>
<!-- Total Quota Used -->
<v-card color="cyan" dark class="mb-4">
<v-card-text class="text-center">
<div class="text-h6">Total Quota Terpakai 5</div>
</v-card-text>
</v-card>
<!-- Late Patients Table -->
<v-card class="mb-4">
<v-card-title class="d-flex justify-space-between align-center">
<span>Info Pasien Lapor Terlambat</span>
<div class="d-flex align-center">
<span class="mr-2 text-caption text-orange"
>KETERANGAN: PASIEN MASUK PADA TANGGAL</span
>
<span class="mr-2">Show</span>
<v-select
v-model="lateItemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="outlined"
style="max-width: 80px"
class="mr-4"
></v-select>
<span class="mr-2">entries</span>
<span class="mr-4">Search:</span>
<v-text-field
v-model="lateSearch"
density="compact"
variant="outlined"
style="max-width: 200px"
hide-details
></v-text-field>
</div>
</v-card-title>
<v-data-table
:headers="lateHeaders"
:items="latePatients"
:search="lateSearch"
:items-per-page="lateItemsPerPage"
class="elevation-1"
>
<template v-slot:no-data>
<div class="text-center pa-4">No data available in table</div>
</template>
</v-data-table>
</v-card>
<!-- Clinic Entry Patients Table -->
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span>Info Pasien Masuk Klinik</span>
<div class="d-flex align-center">
<span class="mr-2">Show</span>
<v-select
v-model="clinicItemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="outlined"
style="max-width: 80px"
class="mr-4"
></v-select>
<span class="mr-2">entries</span>
<span class="mr-4">Search:</span>
<v-text-field
v-model="clinicSearch"
density="compact"
variant="outlined"
style="max-width: 200px"
hide-details
></v-text-field>
</div>
</v-card-title>
<v-data-table
:headers="clinicHeaders"
:items="clinicPatients"
:search="clinicSearch"
:items-per-page="clinicItemsPerPage"
class="elevation-1"
>
<template v-slot:no-data>
<div class="text-center pa-4">No data available in table</div>
</template>
</v-data-table>
</v-card>
</v-container>
</v-main>
</v-layout>
</v-app>
</template>
<script setup>
import { ref, computed } from "vue";
// Reactive data
const drawer = ref(true);
const rail = ref(false);
const search = ref("");
const lateSearch = ref("");
const clinicSearch = ref("");
const itemsPerPage = ref(10);
const lateItemsPerPage = ref(10);
const clinicItemsPerPage = ref(10);
const currentPage = ref("Loket Admin");
// Navigation items
const items = ref([
{ title: "Dashboard", icon: "mdi-view-dashboard", to: "/dashboard" },
{
title: "Setting",
icon: "mdi-cog",
children: [
{ title: "Hak Akses", to: "/setting/hak-akses" },
{ title: "User Login", to: "/setting/user-login" },
{ title: "Master Loket", to: "/setting/master-loket" },
{ title: "Master Klinik", to: "/setting/master-klinik" },
{ title: "Master Klinik Ruang", to: "/setting/master-klinik-ruang" },
{ title: "Screen", to: "/setting/screen" },
],
},
{ title: "Loket Admin", icon: "mdi-account-supervisor" },
{ title: "Ranap Admin", icon: "mdi-bed" },
{ title: "Klinik Admin", icon: "mdi-hospital-box" },
{ title: "Klinik Ruang Admin", icon: "mdi-hospital-marker" },
{ title: "Anjungan", icon: "mdi-account-box-multiple", to: "/anjungan" },
{ title: "Fast Track", icon: "mdi-clock-fast" },
{ title: "Data Pasien", icon: "mdi-account-multiple" },
{ title: "Screen", icon: "mdi-monitor" },
{ title: "List Pasien", icon: "mdi-format-list-bulleted" },
]);
// Table headers
const mainHeaders = ref([
{ title: "No", value: "no", sortable: false },
{ title: "Jam Panggil", value: "jamPanggil" },
{ title: "Barcode", value: "barcode" },
{ title: "No Antrian", value: "noAntrian" },
{ title: "Shift", value: "shift" },
{ title: "Klinik", value: "klinik" },
{ title: "Fast Track", value: "fastTrack" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Panggil", value: "panggil" },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const lateHeaders = ref([
{ title: "No", value: "no", sortable: false },
{ title: "Barcode", value: "barcode" },
{ title: "No Antrian", value: "noAntrian" },
{ title: "Shift", value: "shift" },
{ title: "Klinik", value: "klinik" },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const clinicHeaders = ref([
{ title: "#", value: "no", sortable: false },
{ title: "Barcode", value: "barcode" },
{ title: "No Antrian", value: "noAntrian" },
{ title: "No RM", value: "noRM" },
{ title: "Shift", value: "shift" },
{ title: "Klinik", value: "klinik" },
{ title: "Fast Track", value: "fastTrack" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Aksi", value: "aksi", sortable: false },
]);
// Sample data
const mainPatients = ref([
{
no: 1,
jamPanggil: "12:49",
barcode: "250811100163",
noAntrian: "UM1001 | Online - 250811100163",
shift: "Shift 1",
klinik: "KANDUNGAN",
fastTrack: "UMUM",
pembayaran: "UMUM",
panggil: "Panggil",
status: "current",
},
{
no: 2,
jamPanggil: "10:52",
barcode: "250811100155",
noAntrian: "UM1002 | Online - 250811100155",
shift: "Shift 1",
klinik: "IPD",
fastTrack: "UMUM",
pembayaran: "UMUM",
panggil: "Cancel",
status: "normal",
},
{
no: 3,
jamPanggil: "07:35",
barcode: "250811100355",
noAntrian: "UM1005 | Online - 250811100355",
shift: "Shift 1",
klinik: "THT",
fastTrack: "UMUM",
pembayaran: "UMUM",
panggil: "Panggil",
status: "normal",
},
{
no: 4,
jamPanggil: "08:05",
barcode: "250811100355",
noAntrian: "UM1006 | Online - 250811100355",
shift: "Shift 1",
klinik: "THT",
fastTrack: "UMUM",
pembayaran: "UMUM",
panggil: "Panggil",
status: "normal",
},
{
no: 5,
jamPanggil: "12:43",
barcode: "250811100402",
noAntrian: "UM1004 | Online - 250811100402",
shift: "Shift 1",
klinik: "SARAF",
fastTrack: "UMUM",
pembayaran: "UMUM",
panggil: "Panggil",
status: "normal",
},
]);
const latePatients = ref([]);
const clinicPatients = ref([]);
// Methods
const getRowClass = (item) => {
if (item.status === "current") {
return "text-green font-weight-bold";
}
return "";
};
</script>
<style scoped>
.v-list-item--active {
background-color: rgba(25, 118, 210, 0.12);
color: #1976d2;
}
.text-green {
color: #4caf50 !important;
}
/* Custom scrollbar */
:deep(.v-data-table) {
font-size: 14px;
}
:deep(.v-data-table__wrapper) {
max-height: 400px;
overflow-y: auto;
}
/* Row highlighting */
:deep(.v-data-table tbody tr:nth-child(1)) {
background-color: #fff3cd !important;
}
</style>
+113
View File
@@ -0,0 +1,113 @@
<template>
<v-container>
<v-card>
<v-card-title>Edit Loket</v-card-title>
<v-card-text>
<v-form @submit.prevent="simpanLoket">
<v-text-field label="Nama Loket" v-model="loket.namaLoket"></v-text-field>
<v-text-field label="Kuota Bangku" v-model="loket.kuota" type="number"></v-text-field>
<v-select
label="Status Pelayanan"
:items="['RAWAT JALAN', 'RAWAT INAP']"
v-model="loket.statusPelayanan"
></v-select>
<v-select
label="Pembayaran"
:items="['JKN', 'UMUM']"
v-model="loket.pembayaran"
></v-select>
<v-select
label="Keterangan"
:items="['ONLINE', 'MANUAL']"
v-model="loket.keterangan"
></v-select>
<div class="my-4">
<h3 class="text-h6">Pelayanan</h3>
<TabelLayanan
:headers="serviceHeaders"
:items="availableServices"
v-model:selected-items="loket.pelayanan"
/>
</div>
<v-btn color="success" type="submit" class="mr-2">Simpan</v-btn>
<v-btn color="secondary" @click="cancelEdit">Batal</v-btn>
</v-form>
</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import TabelLayanan from '../../components/TabelLayanan.vue';
const route = useRoute();
const router = useRouter();
// Data dummy loket yang sama seperti di Master-Loket.vue
const loketData = ref([
{ id: 1, no: 1, namaLoket: 'Loket 1', kuota: 500, pelayanan: ['RADIOTERAPI', 'REHAB MEDIK', 'TINDAKAN'], pembayaran: 'JKN', keterangan: 'ONLINE' },
{ id: 2, no: 2, namaLoket: 'Loket 2', kuota: 666, pelayanan: ['JIWA', 'SARAF'], pembayaran: 'JKN', keterangan: 'ONLINE' },
{ id: 3, no: 3, namaLoket: 'Loket 3', kuota: 666, pelayanan: ['ANESTESI', 'JANTUNG'], pembayaran: 'JKN', keterangan: 'ONLINE' },
{ id: 4, no: 4, namaLoket: 'Loket 4', kuota: 3676, pelayanan: ['KULIT KELAMIN', 'PARU'], pembayaran: 'JKN', keterangan: 'ONLINE' },
]);
const loket = ref({
id: null,
namaLoket: '',
kuota: 0,
statusPelayanan: '',
pembayaran: '',
keterangan: '',
pelayanan: [],
});
const serviceHeaders = ref([
{ title: '#', value: 'no', sortable: false },
{ title: 'Kode', value: 'id' },
{ title: 'Klinik', value: 'nama' },
{ title: 'Pilih', value: 'pilih', sortable: false },
]);
const availableServices = ref([
{ no: 1, id: 'AN', nama: 'ANAK' },
{ no: 2, id: 'AS', nama: 'ANESTESI' },
{ no: 3, id: 'BD', nama: 'BEDAH' },
{ no: 4, id: 'GR', nama: 'GERIATRI' },
{ no: 5, id: 'GI', nama: 'GIGI DAN MULUT' },
{ no: 6, id: 'GZ', nama: 'GIZI' },
{ no: 7, id: 'HO', nama: 'HOM' },
{ no: 8, id: 'IP', nama: 'IPD' },
]);
onMounted(() => {
// Cari loket yang sesuai dengan ID di URL
const selectedLoket = loketData.value.find(loket => loket.id === parseInt(route.params.id));
if (selectedLoket) {
// Jika data ditemukan, salin ke objek loket
loket.value = { ...selectedLoket };
// Konversi string pelayanan menjadi array untuk checkbox
if (typeof loket.value.pelayanan === 'string') {
loket.value.pelayanan = loket.value.pelayanan.split(', ').map(s => s.trim());
}
}
});
const simpanLoket = () => {
// Dalam aplikasi nyata, ini adalah tempat untuk memanggil API update data
// Untuk simulasi, kita akan kembali ke halaman master
router.back();
};
const cancelEdit = () => {
router.back();
};
</script>
+156
View File
@@ -0,0 +1,156 @@
<template>
<v-container>
<v-card>
<!-- <v-card-title class="d-flex justify-space-between align-center">
<span>Master Loket</span>
<v-btn color=#ff9248 @click="tambahLoket" style="color:white;">Tambah Baru</v-btn>
</v-card-title> -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-view-dashboard</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Master Loket</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
</div>
</div>
</div>
<TabelData
:headers="loketHeaders"
:items="loketData"
title="Master Loket"
>
<template #actions="{ item }">
<v-btn
small
color="#ff9248"
@click="editLoket(item)"
class="mr-2"
style="color: white"
>Edit</v-btn
>
<v-btn small color="grey-lighten-4" @click="deleteLoket(item)"
>Delete</v-btn
>
</template>
</TabelData>
</v-card>
</v-container>
</template>
<script setup>
import { ref } from "vue";
import TabelData from "../../components/TabelData.vue";
import { useRouter } from "vue-router";
const router = useRouter();
const loketHeaders = ref([
{ title: "No", value: "no" },
{ title: "Nama Loket", value: "namaLoket" },
{ title: "Kuota", value: "kuota" },
{ title: "Pelayanan", value: "pelayanan" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Keterangan", value: "keterangan" },
{ title: "Aksi", value: "aksi", sortable: false }, // 'value' harus 'actions'
]);
// Master-Loket.vue
const loketData = ref([
{
id: 1,
no: 1,
namaLoket: "Loket 1",
kuota: 500,
pelayanan: ["RADIOTERAPI", "REHAB MEDIK", "TINDAKAN"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
{
id: 2,
no: 2,
namaLoket: "Loket 2",
kuota: 666,
pelayanan: ["JIWA", "SARAF"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
{
id: 3,
no: 3,
namaLoket: "Loket 3",
kuota: 666,
pelayanan: ["ANESTESI", "JANTUNG"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
{
id: 4,
no: 4,
namaLoket: "Loket 4",
kuota: 3676,
pelayanan: ["KULIT KELAMIN", "PARU"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
]);
const editLoket = (item) => {
router.push({ path: `/Setting/Edit-Loket/${item.id}` });
};
const deleteLoket = (item) => {
const index = loketData.value.findIndex((loket) => loket.id === item.id);
if (index !== -1) {
loketData.value.splice(index, 1);
}
};
const tambahLoket = () => {
router.push({ path: "/Setting/Tambah-Loket" });
};
</script>
<style scoped>
.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;
}
</style>
+204
View File
@@ -0,0 +1,204 @@
<template>
<div class="screen-edit">
<!-- Header -->
<div class="d-flex align-center mb-4">
<v-btn icon="mdi-arrow-left" @click="goBack" class="mr-2"></v-btn>
<h2>Edit Screen</h2>
</div>
<!-- Screen Selection Cards -->
<div class="screen-cards mb-6">
<v-row>
<v-col cols="12" md="4" v-for="screen in screens" :key="screen.id">
<v-card
:class="['screen-card', { active: selectedScreen?.id === screen.id }]"
@click="selectScreen(screen)"
elevation="2"
>
<v-card-title class="text-center">
{{ screen.nama }}
</v-card-title>
</v-card>
</v-col>
</v-row>
</div>
<!-- Selected Screen Display -->
<div v-if="selectedScreen" class="mb-4">
<v-chip color="primary" size="large">
Selected: {{ selectedScreen.nama }}
</v-chip>
</div>
<!-- Clinic Selection Table -->
<TabelLayanan
v-if="selectedScreen"
:headers="computedHeaders"
:items="klinikItems"
:selectedItems="selectedKlinik"
@update:selectedItems="updateSelectedKlinik"
/>
<!-- Action Buttons -->
<div class="d-flex justify-end gap-4 mt-6">
<v-btn variant="outlined" @click="cancel">
Cancel
</v-btn>
<v-btn color="warning" @click="submit" :disabled="!selectedScreen">
Submit
</v-btn>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import TabelLayanan from '~/components/TabelLayanan.vue';
// Get route parameter
const route = useRoute();
const screenId = route.params.id;
// Data
const selectedScreen = ref(null);
const selectedKlinik = ref([]);
const screens = ref([
{ id: 1, nama: 'Screen 1' },
{ id: 2, nama: 'Screen 2' },
{ id: 3, nama: 'Screen 3' }
]);
const klinikItems = ref([
{ id: 1, no: 1, nama_klinik: 'ANAK' },
{ id: 2, no: 2, nama_klinik: 'ANESTESI' },
{ id: 3, no: 3, nama_klinik: 'BEDAH' },
{ id: 4, no: 4, nama_klinik: 'GIGI DAN MULUT' },
{ id: 5, no: 5, nama_klinik: 'GERIATRI' },
{ id: 6, no: 6, nama_klinik: 'GIZI' },
{ id: 7, no: 7, nama_klinik: 'IPD' },
{ id: 8, no: 8, nama_klinik: 'JANTUNG' },
{ id: 9, no: 9, nama_klinik: 'JIWA' },
{ id: 10, no: 10, nama_klinik: 'KUL KEL' },
{ id: 11, no: 11, nama_klinik: 'KOMPLEMENTER' },
{ id: 12, no: 12, nama_klinik: 'MATA' },
{ id: 13, no: 13, nama_klinik: 'SARAF' },
{ id: 14, no: 14, nama_klinik: 'KANDUNGAN' },
{ id: 15, no: 15, nama_klinik: 'ONKOLOGI' },
{ id: 16, no: 16, nama_klinik: 'PARU' },
{ id: 17, no: 17, nama_klinik: 'RADIOTERAPI' },
{ id: 18, no: 18, nama_klinik: 'REHAB MEDIK' },
{ id: 19, no: 19, nama_klinik: 'THT' },
{ id: 20, no: 20, nama_klinik: 'MCU' },
{ id: 21, no: 21, nama_klinik: 'KEMOTERAPI' },
{ id: 22, no: 22, nama_klinik: 'R. TINDAKAN' },
{ id: 23, no: 23, nama_klinik: 'HOM' }
]);
// Computed headers based on selected screen
const computedHeaders = computed(() => {
return [
{ title: 'No', key: 'no', sortable: false, width: '80px' },
{ title: 'Nama Klinik', key: 'nama_klinik', sortable: true },
{ title: 'Pilih', key: 'pilih', sortable: false, width: '100px' }
];
});
// Methods
const selectScreen = (screen) => {
selectedScreen.value = screen;
loadScreenData(screen.id);
};
const loadScreenData = (screenId) => {
// Simulate loading existing selections for the screen
switch(parseInt(screenId)) {
case 1:
selectedKlinik.value = [1, 2, 3, 4, 5, 6, 7, 8]; // ANAK, ANESTESI, etc.
break;
case 2:
selectedKlinik.value = [9, 10, 11, 12, 13, 14, 15, 16]; // JIWA, KUL KEL, etc.
break;
case 3:
selectedKlinik.value = [17, 18, 19, 20, 21, 22, 23]; // RADIOTERAPI, REHAB MEDIK, etc.
break;
default:
selectedKlinik.value = [];
}
};
const updateSelectedKlinik = (newSelection) => {
selectedKlinik.value = newSelection;
};
const submit = () => {
if (!selectedScreen.value) {
alert('Please select a screen first');
return;
}
// Simulate API call to save the configuration
const data = {
screenId: selectedScreen.value.id,
selectedKlinik: selectedKlinik.value
};
console.log('Saving configuration:', data);
alert('Configuration saved successfully!');
goBack();
};
const cancel = () => {
goBack();
};
const goBack = () => {
navigateTo('/setting/screen');
};
// Lifecycle
onMounted(() => {
// Auto-select screen based on route parameter
if (screenId) {
const screen = screens.value.find(s => s.id == screenId);
if (screen) {
selectScreen(screen);
}
}
});
</script>
<style scoped>
.screen-edit {
padding: 20px;
}
.screen-cards {
margin-bottom: 2rem;
}
.screen-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.screen-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.screen-card.active {
border-color: #1976d2;
background-color: #d4d4d4;
}
.screen-card .v-card-title {
font-weight: 600;
padding: 20px;
}
.gap-4 {
gap: 16px;
}
</style>
+253
View File
@@ -0,0 +1,253 @@
<template>
<div class="screen-list">
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-monitor</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Screen</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
</div>
<div class="header-right">
<!-- <v-chip color="success" variant="flat" class="mr-2">
Total {{ totalPasien }} Pasien
</v-chip> -->
<v-chip color="white" variant="flat" class="text-primary" to="/Setting/screen/edit/1">
Edit Screen
</v-chip>
</div>
</div>
</div>
<!-- <div class="d-flex justify-space-between align-center mb-4">
<h2>Screen</h2>
<div class="d-flex gap-2">
<v-btn color="primary" variant="outlined" prepend-icon="mdi-eye">
View
</v-btn>
<v-btn color="warning" prepend-icon="mdi-pencil">
Edit
</v-btn>
</div>
</div> -->
<!-- Controls -->
<div class="d-flex justify-space-between align-center mb-4">
<div class="d-flex align-center gap-2">
<span>Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50, 100]"
density="compact"
variant="outlined"
style="width: 80px"
></v-select>
<span>entries</span>
</div>
<div class="d-flex align-center gap-2">
<span>Search:</span>
<v-text-field
v-model="search"
density="compact"
variant="outlined"
hide-details
style="width: 200px"
></v-text-field>
</div>
</div>
<!-- Table -->
<v-data-table
:headers="headers"
:items="screenItems"
:items-per-page="itemsPerPage"
:search="search"
class="elevation-1"
>
<template v-slot:item.no="{ index }">
{{ index + 1 }}
</template>
<template v-slot:item.klinik="{ item }">
<div class="klinik-tags">
<v-chip
v-for="klinik in item.klinik"
:key="klinik"
size="small"
class="ma-1"
color="red"
text-color="white"
>
{{ klinik }}
</v-chip>
</div>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
icon="mdi-pencil"
size="small"
color="#ff9248"
@click="editScreen(item)"
style="color: white"
></v-btn>
</template>
</v-data-table>
<!-- Footer -->
<div class="d-flex justify-space-between align-center mt-4">
<div>
Showing {{ currentPageStart }} to {{ currentPageEnd }} of
{{ totalItems }} entries
</div>
<v-pagination
v-model="currentPage"
:length="totalPages"
:total-visible="5"
></v-pagination>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
// Data
const search = ref("");
const itemsPerPage = ref(10);
const currentPage = ref(1);
const headers = [
{ title: "No", key: "no", sortable: false, width: "60px" },
{ title: "Nama Screen", key: "nama_screen", sortable: true },
{ title: "Klinik", key: "klinik", sortable: false },
{ title: "Actions", key: "actions", sortable: false, width: "100px" },
];
const screenItems = ref([
{
id: 1,
nama_screen: "Layar Screen 1",
klinik: [
"ANAK",
"ANESTESI",
"BEDAH",
"GIGI DAN MULUT",
"GERIATRI",
"GIZI",
"IPD",
"JANTUNG",
],
},
{
id: 2,
nama_screen: "Layar Screen 2",
klinik: [
"JIWA",
"KUL KEL",
"KOMPLEMENTER",
"MATA",
"SARAF",
"KANDUNGAN",
"ONKOLOGI",
"PARU",
],
},
{
id: 3,
nama_screen: "Layar Screen 3",
klinik: [
"RADIOTERAPI",
"REHAB MEDIK",
"THT",
"MCU",
"KEMOTERAPI",
"R. TINDAKAN",
"HOM",
],
},
]);
// Computed
const totalItems = computed(() => screenItems.value.length);
const totalPages = computed(() =>
Math.ceil(totalItems.value / itemsPerPage.value)
);
const currentPageStart = computed(
() => (currentPage.value - 1) * itemsPerPage.value + 1
);
const currentPageEnd = computed(() =>
Math.min(currentPage.value * itemsPerPage.value, totalItems.value)
);
// Methods
const editScreen = (item) => {
navigateTo(`/setting/screen/edit/${item.id}`);
};
</script>
<style scoped>
.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;
}
.screen-list {
padding: 20px;
}
.klinik-tags {
max-width: 600px;
}
.gap-2 {
gap: 8px;
}
</style>
+308
View File
@@ -0,0 +1,308 @@
<!-- page edit id data pasien -->
<template>
<div class="edit-pasien">
<!-- Header -->
<div class="d-flex align-center mb-4">
<v-btn icon="mdi-arrow-left" @click="goBack" class="mr-2"></v-btn>
<h2>Edit Pasien</h2>
</div>
<!-- Form -->
<v-card class="pa-6" elevation="2">
<v-form ref="form" v-model="valid">
<v-row>
<!-- Tanggal Daftar -->
<v-col cols="12" md="6">
<v-text-field
v-model="formData.tanggal_daftar"
label="Tanggal Daftar"
variant="outlined"
readonly
density="compact"
></v-text-field>
</v-col>
<!-- Tanggal Periksa -->
<v-col cols="12" md="6">
<v-text-field
v-model="formData.tanggal_periksa"
label="Tanggal Periksa"
variant="outlined"
type="date"
density="compact"
:rules="[rules.required]"
></v-text-field>
</v-col>
<!-- No Barcode -->
<v-col cols="12" md="6">
<v-text-field
v-model="formData.no_barcode"
label="No Barcode"
variant="outlined"
readonly
density="compact"
></v-text-field>
</v-col>
<!-- No Antrian -->
<v-col cols="12" md="6">
<v-text-field
v-model="formData.no_antrian"
label="No Antrian"
variant="outlined"
readonly
density="compact"
></v-text-field>
</v-col>
<!-- No Klinik -->
<v-col cols="12" md="6">
<v-text-field
v-model="formData.no_klinik"
label="No Klinik"
variant="outlined"
placeholder="Belum Mendapatkan Antrian Klinik"
density="compact"
></v-text-field>
</v-col>
<!-- No Rekammedik -->
<v-col cols="12" md="6">
<v-text-field
v-model="formData.no_rekammedik"
label="No Rekammedik"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<!-- Klinik -->
<v-col cols="12" md="6">
<v-select
v-model="formData.klinik"
label="Klinik"
:items="klinikOptions"
variant="outlined"
density="compact"
:rules="[rules.required]"
></v-select>
</v-col>
<!-- Shift -->
<v-col cols="12" md="6">
<v-select
v-model="formData.shift"
label="Shift"
:items="shiftOptions"
variant="outlined"
density="compact"
:rules="[rules.required]"
></v-select>
</v-col>
<!-- Keterangan -->
<v-col cols="12">
<v-text-field
v-model="formData.keterangan"
label="Keterangan"
variant="outlined"
density="compact"
readonly
>
<template v-slot:append-inner>
<span class="text-red font-weight-bold">
{{ formData.keterangan }}
</span>
</template>
</v-text-field>
</v-col>
<!-- Pembayaran -->
<v-col cols="12" md="6">
<v-select
v-model="formData.pembayaran"
label="Pembayaran"
:items="pembayaranOptions"
variant="outlined"
density="compact"
:rules="[rules.required]"
></v-select>
</v-col>
</v-row>
<!-- Action Buttons -->
<div class="d-flex justify-end gap-4 mt-6">
<v-btn variant="outlined" @click="cancel">
Cancel
</v-btn>
<v-btn
color="warning"
@click="submit"
:disabled="!valid"
:loading="loading"
>
Submit
</v-btn>
</div>
</v-form>
</v-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const route = useRoute();
const pasienId = route.params.id;
// Form data
const valid = ref(false);
const loading = ref(false);
const form = ref(null);
const formData = ref({
tanggal_daftar: '',
tanggal_periksa: '',
no_barcode: '',
no_antrian: '',
no_klinik: '',
no_rekammedik: '',
klinik: '',
shift: '',
keterangan: '',
pembayaran: ''
});
// Options
const klinikOptions = [
'HOM',
'KANDUNGAN',
'ANAK',
'IPD',
'JIWA',
'KUL KEL',
'KOMPLEMENTER',
'MATA',
'SARAF',
'ONKOLOGI',
'PARU',
'RADIOTERAPI',
'REHAB MEDIK',
'THT',
'MCU',
'KEMOTERAPI',
'R. TINDAKAN',
'ANESTESI',
'BEDAH',
'GIGI DAN MULUT',
'GERIATRI',
'GIZI',
'JANTUNG'
];
const shiftOptions = [
'Shift 1 = Mulai Pukul 07:00',
'Shift 2 = Mulai Pukul 13:00',
'Shift 3 = Mulai Pukul 19:00'
];
const pembayaranOptions = [
'JKN',
'UMUM',
'ASURANSI',
'KARYAWAN'
];
// Validation rules
const rules = {
required: value => !!value || 'Field ini wajib diisi'
};
// Mock data for editing
const mockPasienData = {
1: {
tanggal_daftar: '2025-08-13 00:00:03',
tanggal_periksa: '2025-08-27',
no_barcode: '25027100007',
no_antrian: 'IP1001',
no_klinik: 'Belum Mendapatkan Antrian Klinik',
no_rekammedik: '11555500',
klinik: 'IPD',
shift: 'Shift 1 = Mulai Pukul 07:00',
keterangan: 'PENDAFTARAN ONLINE',
pembayaran: 'JKN'
},
2: {
tanggal_daftar: '2025-07-24 13:50:01',
tanggal_periksa: '2025-08-27',
no_barcode: '25027100002',
no_antrian: 'OB1001',
no_klinik: '',
no_rekammedik: '',
klinik: 'KANDUNGAN',
shift: 'Shift 1 = Mulai Pukul 07:00',
keterangan: '',
pembayaran: 'JKN'
}
};
// Methods
const loadPasienData = () => {
const data = mockPasienData[pasienId];
if (data) {
formData.value = { ...data };
}
};
const submit = async () => {
if (!valid.value) return;
loading.value = true;
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Updating pasien data:', formData.value);
// Show success message (you can use a toast/snackbar here)
alert('Data pasien berhasil diperbarui!');
// Navigate back to list
goBack();
} catch (error) {
console.error('Error updating pasien:', error);
alert('Gagal memperbarui data pasien!');
} finally {
loading.value = false;
}
};
const cancel = () => {
goBack();
};
const goBack = () => {
navigateTo('/data-pasien');
};
// Lifecycle
onMounted(() => {
loadPasienData();
});
</script>
<style scoped>
.edit-pasien {
padding: 20px;
}
.gap-4 {
gap: 16px;
}
.text-red {
color: #d32f2f;
}
</style>
+437
View File
@@ -0,0 +1,437 @@
<!-- pages/data-pasien.vue -->
<template>
<div class="data-pasien-container">
<div class="data-pasien">
<!-- Header -->
<!-- <div class="d-flex justify-space-between align-center mb-4">
<h2>Data Pasien</h2>
<div class="d-flex gap-2">
<v-btn
color="#ff9248"
prepend-icon="mdi-plus"
@click="addPatient"
style="color: white"
>
Tambah Pasien
</v-btn>
</div>
</div> -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-view-dashboard</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Data Pasien</h1>
</div>
</div>
<div class="header-right">
<v-btn
color="#ff9248"
prepend-icon="mdi-plus"
@click="addPatient"
style="color: white"
>
Tambah Pasien
</v-btn>
</div>
</div>
</div>
<!-- Menggunakan komponen TabelData -->
<TabelData
:headers="headers"
:items="pasienItems"
title="Daftar Data Pasien"
:show-search="true"
>
<template #actions="{ item }">
<div class="d-flex gap-1">
<v-btn
size="small"
color="#ff9248"
variant="flat"
@click="viewPasien(item)"
style="color: white"
>VIEW</v-btn
>
<v-btn
size="small"
color="grey-lighten-4"
variant="flat"
@click="editPasien(item)"
>EDIT</v-btn
>
</div>
</template>
</TabelData>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import TabelData from "@/components/TabelData.vue";
// Headers untuk tabel
const headers = [
{ title: "No", key: "no", sortable: false, width: "60px" },
{ title: "Tgl Daftar", key: "tgl_daftar", sortable: true, width: "140px" },
{ title: "No Barcode", key: "no_barcode", sortable: true, width: "120px" },
{ title: "No Antrian", key: "no_antrian", sortable: true, width: "100px" },
{ title: "No Klinik", key: "no_klinik", sortable: true, width: "100px" },
{ title: "RM", key: "rm", sortable: true, width: "100px" },
{ title: "Klinik", key: "klinik", sortable: true, width: "120px" },
{ title: "Shift", key: "shift", sortable: true, width: "80px" },
{ title: "Ket", key: "keterangan", sortable: false, width: "150px" },
{ title: "Pembayaran", key: "pembayaran", sortable: true, width: "100px" },
{ title: "Status", key: "status", sortable: true, width: "120px" },
{ title: "Aksi", key: "aksi", sortable: false, width: "100px" },
];
// Data pasien dengan informasi lengkap untuk edit
const pasienItems = ref([
{
id: 1,
no: 1,
tgl_daftar: "2025-07-15 13:47:33",
no_barcode: "25027100001",
no_antrian: "HO1001",
no_klinik: "",
rm: "",
klinik: "HOM",
shift: "Shift 1",
keterangan: "",
pembayaran: "JKN",
status: "Tunggu Daftar",
// Data tambahan untuk form edit
editData: {
tanggal_daftar: "2025-07-15 13:47:33",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100001",
no_antrian: "HO1001",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "",
klinik: "HOM",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "",
pembayaran: "JKN",
},
},
{
id: 2,
no: 2,
tgl_daftar: "2025-07-24 13:50:01",
no_barcode: "25027100002",
no_antrian: "OB1001",
no_klinik: "",
rm: "",
klinik: "KANDUNGAN",
shift: "Shift 1",
keterangan: "",
pembayaran: "JKN",
status: "Barcode",
editData: {
tanggal_daftar: "2025-07-24 13:50:01",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100002",
no_antrian: "OB1001",
no_klinik: "",
no_rekammedik: "",
klinik: "KANDUNGAN",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "",
pembayaran: "JKN",
},
},
{
id: 3,
no: 3,
tgl_daftar: "2025-07-24 13:50:37",
no_barcode: "25027100003",
no_antrian: "OB1002",
no_klinik: "",
rm: "",
klinik: "KANDUNGAN",
shift: "Shift 1",
keterangan: "",
pembayaran: "JKN",
status: "Barcode",
editData: {
tanggal_daftar: "2025-07-24 13:50:37",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100003",
no_antrian: "OB1002",
no_klinik: "",
no_rekammedik: "",
klinik: "KANDUNGAN",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "",
pembayaran: "JKN",
},
},
{
id: 4,
no: 4,
tgl_daftar: "2025-07-28 08:18:20",
no_barcode: "25027100004",
no_antrian: "AN1001",
no_klinik: "",
rm: "",
klinik: "ANAK",
shift: "Shift 1",
keterangan: "",
pembayaran: "JKN",
status: "Barcode",
editData: {
tanggal_daftar: "2025-07-28 08:18:20",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100004",
no_antrian: "AN1001",
no_klinik: "",
no_rekammedik: "",
klinik: "ANAK",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "",
pembayaran: "JKN",
},
},
{
id: 5,
no: 5,
tgl_daftar: "2025-08-13 00:00:02",
no_barcode: "25027100005",
no_antrian: "HO1002",
no_klinik: "",
rm: "11412684",
klinik: "HOM",
shift: "Shift 1",
keterangan: "Online 25#27100005",
pembayaran: "JKN",
status: "Tunggu Daftar",
editData: {
tanggal_daftar: "2025-08-13 00:00:02",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100005",
no_antrian: "HO1002",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "11412684",
klinik: "HOM",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "Online 25#27100005",
pembayaran: "JKN",
},
},
{
id: 6,
no: 6,
tgl_daftar: "2025-08-13 00:00:03",
no_barcode: "25027100006",
no_antrian: "HO1003",
no_klinik: "",
rm: "",
klinik: "HOM",
shift: "Shift 1",
keterangan: "Online 25#27100006",
pembayaran: "JKN",
status: "Tunggu Daftar",
editData: {
tanggal_daftar: "2025-08-13 00:00:03",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100006",
no_antrian: "HO1003",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "",
klinik: "HOM",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "Online 25#27100006",
pembayaran: "JKN",
},
},
{
id: 7,
no: 7,
tgl_daftar: "2025-08-13 00:00:03",
no_barcode: "25027100007",
no_antrian: "IP1001",
no_klinik: "",
rm: "11555500",
klinik: "IPD",
shift: "Shift 1",
keterangan: "Online 25#27100007",
pembayaran: "JKN",
status: "Tunggu Daftar",
editData: {
tanggal_daftar: "2025-08-13 00:00:03",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100007",
no_antrian: "IP1001",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "11555500",
klinik: "IPD",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "PENDAFTARAN ONLINE",
pembayaran: "JKN",
},
},
{
id: 8,
no: 8,
tgl_daftar: "2025-08-13 00:00:04",
no_barcode: "25027100008",
no_antrian: "IP1001",
no_klinik: "",
rm: "11333855",
klinik: "IPD",
shift: "Shift 1",
keterangan: "Online 25#27100008",
pembayaran: "JKN",
status: "Tunggu Daftar",
editData: {
tanggal_daftar: "2025-08-13 00:00:04",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100008",
no_antrian: "IP1001",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "11333855",
klinik: "IPD",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "Online 25#27100008",
pembayaran: "JKN",
},
},
{
id: 9,
no: 9,
tgl_daftar: "2025-08-13 00:00:04",
no_barcode: "25027100009",
no_antrian: "IP1001",
no_klinik: "",
rm: "11565554",
klinik: "IPD",
shift: "Shift 1",
keterangan: "Online 25#27100009",
pembayaran: "JKN",
status: "Tunggu Daftar",
editData: {
tanggal_daftar: "2025-08-13 00:00:04",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100009",
no_antrian: "IP1001",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "11565554",
klinik: "IPD",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "Online 25#27100009",
pembayaran: "JKN",
},
},
{
id: 10,
no: 10,
tgl_daftar: "2025-08-13 00:00:04",
no_barcode: "25027100010",
no_antrian: "IP1001",
no_klinik: "",
rm: "11627608",
klinik: "IPD",
shift: "Shift 1",
keterangan: "Online 25#27100010",
pembayaran: "JKN",
status: "Tunggu Daftar",
editData: {
tanggal_daftar: "2025-08-13 00:00:04",
tanggal_periksa: "2025-08-27",
no_barcode: "25027100010",
no_antrian: "IP1001",
no_klinik: "Belum Mendapatkan Antrian Klinik",
no_rekammedik: "11627608",
klinik: "IPD",
shift: "Shift 1 = Mulai Pukul 07:00",
keterangan: "Online 25#27100010",
pembayaran: "JKN",
},
},
]);
// Methods
const addPatient = () => {
// Navigate to add patient page
navigateTo("/data-pasien/add");
};
const viewPasien = (item) => {
// Implement view functionality
console.log("View pasien:", item);
// You can navigate to a view page or open a modal
// navigateTo(`/data-pasien/view/${item.id}`);
};
const editPasien = (item) => {
// Navigate to edit page
navigateTo(`/data-pasien/edit/${item.id}`);
};
// Provide data globally untuk diakses oleh halaman edit
provide("pasienData", pasienItems);
</script>
<style scoped>
.data-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: rgb(255, 255, 255);
}
.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);
}
.header-right {
display: flex;
align-items: center;
}
.data-pasien {
padding: 20px;
}
.gap-1 {
gap: 4px;
}
.gap-2 {
gap: 8px;
}
</style>
-221
View File
@@ -1,221 +0,0 @@
<template>
<v-app>
<v-layout>
<v-app-bar app color="green darken-1" dark>
<v-app-bar-nav-icon @click="rail = !rail"></v-app-bar-nav-icon>
<v-toolbar-title class="ml-2">Antrean RSSA</v-toolbar-title>
</v-app-bar>
<v-navigation-drawer v-model="drawer" :rail="rail" permanent app>
<!-- /* <v-list-item>
<v-list-item-title class="text-h6">
RSSA
</v-list-item-title>
</v-list-item>
-->
<v-list density="compact" nav>
<template v-for="item in items" :key="item.title">
<v-menu
v-if="item.children"
open-on-hover
location="end"
:nudge-right="8"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.title"
:value="item.title"
>
</v-list-item>
</template>
<v-list>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{
item.title
}}</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.title"
:to="child.to"
link
>
<v-list-item-title>{{ child.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-list-item
v-else
:prepend-icon="item.icon"
:title="item.title"
:to="item.to"
:value="item.title"
link
></v-list-item>
</template>
</v-list>
</v-navigation-drawer>
<v-main>
<v-container fluid>
<p>Admin Anjungan</p>
<v-card class="pa-5 mb-5" color="white" flat>
<v-row align="center">
<v-col cols="12" md="4">
<v-text-field
label="Barcode"
placeholder="Masukkan Barcode"
outlined
dense
hide-details
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-chip color="#B71C1C" class="text-caption">
Tekan Enter. (Barcode depan nomor selalu ada huruf lain, Ex:
J20073010005 "Hiraukan huruf 'J' nya")
</v-chip>
</v-col>
<v-col cols="12" md="2">
<v-btn block color="info">Pendaftaran Online</v-btn>
</v-col>
</v-row>
</v-card>
<v-divider class="my-5"></v-divider>
<v-card class="mb-5">
<v-toolbar flat color="transparent" dense>
<v-toolbar-title
class="text-subtitle-1 font-weight-bold red--text"
>DATA PENGUNJUNG TERLAMBAT</v-toolbar-title
>
<v-spacer></v-spacer>
<v-text-field
v-model="searchLate"
append-icon="mdi-magnify"
label="Search"
single-line
hide-details
dense
class="mr-2"
></v-text-field>
<v-select
:items="[10, 25, 50, 100]"
label="Show"
dense
single-line
hide-details
class="shrink"
></v-select>
</v-toolbar>
<v-card-text>
<v-data-table
:headers="lateHeaders"
:items="lateVisitors"
:search="searchLate"
no-data-text="No data available in table"
hide-default-footer
class="elevation-1"
></v-data-table>
<div class="d-flex justify-end pt-2">
<v-pagination
v-model="page"
:length="10"
:total-visible="5"
></v-pagination>
</div>
</v-card-text>
</v-card>
<v-divider class="my-5"></v-divider>
<v-card>
<v-toolbar flat color="transparent" dense>
<v-toolbar-title
class="text-subtitle-1 font-weight-bold red--text"
>DATA PENGUNJUNG</v-toolbar-title
>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="Search"
single-line
hide-details
dense
class="mr-2"
></v-text-field>
<v-select
:items="[10, 25, 50, 100]"
label="Show"
dense
single-line
hide-details
class="shrink"
></v-select>
</v-toolbar>
<v-card-text>
<v-data-table
:headers="headers"
:items="visitors"
:search="search"
no-data-text="No data available in table"
class="elevation-1"
:footer-props="{
'items-per-page-options': [10, 25, 50, 100],
'show-current-page': true,
}"
>
<template v-slot:item.aksi="{ item }">
<div class="d-flex flex-column">
<v-btn small color="success" class="my-1">Tiket</v-btn>
<v-btn small color="primary" class="my-1"
>Tiket Pengantar</v-btn
>
<v-btn small color="warning" class="my-1">ByPass</v-btn>
</div>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</v-main>
</v-layout>
</v-app>
</template>
<script setup>
import { ref } from "vue";
const drawer = ref(true); // Nilai awal true agar sidebar terlihat
const rail = ref(true); // Nilai awal true agar sidebar dimulai dalam mode rail
// Struktur data yang memisahkan menu dengan dan tanpa submenu
const items = ref([
{ title: "Dashboard", icon: "mdi-view-dashboard", to: "/dashboard" },
{
title: "Setting",
icon: "mdi-cog",
children: [
{ title: "Hak Akses", to: "/setting/hak-akses" },
{ title: "User Login", to: "/setting/user-login" },
{ title: "Master Loket", to: "/setting/master-loket" },
{ title: "Master Klinik", to: "/setting/master-klinik" },
{ title: "Master Klinik Ruang", to: "/setting/master-klinik-ruang" },
{ title: "Screen", to: "/setting/screen" },
],
},
{ title: "Loket Admin", icon: "mdi-account-supervisor" },
{ title: "Ranap Admin", icon: "mdi-bed" },
{ title: "Klinik Admin", icon: "mdi-hospital-box" },
{ title: "Klinik Ruang Admin", icon: "mdi-hospital-marker" },
{ title: "Anjungan", icon: "mdi-account-box-multiple", to: "/anjungan" },
]);
</script>