1001 lines
36 KiB
Vue
1001 lines
36 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, onMounted } from 'vue';
|
|
import { Icon } from '@iconify/vue';
|
|
// @ts-ignore: module has incompatible/undiscoverable typings in package exports
|
|
import VueApexCharts from "vue3-apexcharts";
|
|
import ModalPendaftaran from '@/components/pendaftaran/ModalPendaftaranV2.vue';
|
|
import api from '@/services/api';
|
|
import { useAuth } from '~/composables/useAuth';
|
|
import { useUserMenuStore } from '~/store/userMenu';
|
|
|
|
// Check user access to dashboard
|
|
const { user } = useAuth();
|
|
const menuStore = useUserMenuStore();
|
|
|
|
// Check if user has access to dashboard menu item
|
|
const hasDashboardAccess = computed(() => {
|
|
if (!menuStore.hasMenu) return false;
|
|
|
|
// Extract all eligible paths from menu
|
|
const eligiblePaths: string[] = [];
|
|
menuStore.menuItems.forEach((group) => {
|
|
if (group.children) {
|
|
group.children.forEach((item) => {
|
|
if (item.to) {
|
|
eligiblePaths.push(item.to);
|
|
}
|
|
if (item.children) {
|
|
item.children.forEach((subItem) => {
|
|
if (subItem.to) {
|
|
eligiblePaths.push(subItem.to);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Check if /dashboard is in eligible paths
|
|
return eligiblePaths.includes('/dashboard');
|
|
});
|
|
|
|
// Filter bulan
|
|
const selectedMonth = ref(new Date().getMonth());
|
|
const selectedYear = ref(new Date().getFullYear());
|
|
|
|
const months = [
|
|
{ value: 0, text: 'Januari' },
|
|
{ value: 1, text: 'Februari' },
|
|
{ value: 2, text: 'Maret' },
|
|
{ value: 3, text: 'April' },
|
|
{ value: 4, text: 'Mei' },
|
|
{ value: 5, text: 'Juni' },
|
|
{ value: 6, text: 'Juli' },
|
|
{ value: 7, text: 'Agustus' },
|
|
{ value: 8, text: 'September' },
|
|
{ value: 9, text: 'Oktober' },
|
|
{ value: 10, text: 'November' },
|
|
{ value: 11, text: 'Desember' }
|
|
];
|
|
const yearNow = new Date().getFullYear();
|
|
const years = ref([
|
|
yearNow -3, yearNow - 2, yearNow -1,yearNow
|
|
]);
|
|
|
|
// Modal state
|
|
const showModal = ref(false);
|
|
|
|
// Loading state
|
|
const loadingStatusAntrian = ref(false);
|
|
const loadingKategoriAntrian = ref(false);
|
|
const loadingAntrianPerHari = ref(false);
|
|
const loadingAntrianPerSpesialis = ref(false);
|
|
const loadingAntrianPerSubspesialis = ref(false);
|
|
|
|
// Data status antrian dari API
|
|
const statusAntrianData = ref<Array<{ id: number; jumlah: number; statust: string }>>([]);
|
|
const kategoriAntrianData = ref<Array<{ id_kategori: number; kategori: string; jumlah: number }>>([]);
|
|
const antrianPerHariData = ref<Array<{ TanggalDaftar: string; Belum: number; Selesai: number; Tunda: number; Batal: number }>>([]);
|
|
|
|
// Fetch data perbandingan status antrian
|
|
const fetchStatusAntrian = async () => {
|
|
loadingStatusAntrian.value = true;
|
|
try {
|
|
const response = await api.get('/dashboard/perbandingan-status-antrian', {
|
|
params: {
|
|
year: selectedYear.value,
|
|
month: selectedMonth.value + 1 // API expects 1-12, not 0-11
|
|
}
|
|
});
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
statusAntrianData.value = response.data.data;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching status antrian:', error);
|
|
statusAntrianData.value = [];
|
|
} finally {
|
|
loadingStatusAntrian.value = false;
|
|
}
|
|
};
|
|
|
|
// Fetch data antrian per kategori
|
|
const fetchKategoriAntrian = async () => {
|
|
loadingKategoriAntrian.value = true;
|
|
try {
|
|
const response = await api.get('/dashboard/perbandingan-kategori-antrian', {
|
|
params: {
|
|
year: selectedYear.value,
|
|
month: selectedMonth.value + 1 // API expects 1-12, not 0-11
|
|
}
|
|
});
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
kategoriAntrianData.value = response.data.data;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching kategori antrian:', error);
|
|
kategoriAntrianData.value = [];
|
|
} finally {
|
|
loadingKategoriAntrian.value = false;
|
|
}
|
|
};
|
|
|
|
// Fetch data antrian per hari
|
|
const fetchAntrianPerHari = async () => {
|
|
loadingAntrianPerHari.value = true;
|
|
try {
|
|
const response = await api.get('/dashboard/antrian-per-hari', {
|
|
params: {
|
|
year: selectedYear.value,
|
|
month: selectedMonth.value + 1 // API expects 1-12, not 0-11
|
|
}
|
|
});
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
antrianPerHariData.value = response.data.data;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching antrian per hari:', error);
|
|
antrianPerHariData.value = [];
|
|
} finally {
|
|
loadingAntrianPerHari.value = false;
|
|
}
|
|
};
|
|
|
|
// Fetch data antrian per spesialis
|
|
const fetchAntrianPerSpesialis = async () => {
|
|
loadingAntrianPerSpesialis.value = true;
|
|
try {
|
|
const response = await api.get('/dashboard/table-antrian-per-spesialis', {
|
|
params: {
|
|
year: selectedYear.value,
|
|
month: selectedMonth.value + 1 // API expects 1-12, not 0-11
|
|
}
|
|
});
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
// Transform API data to match table headers (lowercase keys)
|
|
antrianPerSpesialis.value = response.data.data.map((item: any) => ({
|
|
spesialis: item.Spesialis,
|
|
total: item.Total,
|
|
belum: item.Belum,
|
|
selesai: item.Selesai,
|
|
tunda: item.Tunda,
|
|
batal: item.Batal
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching antrian per spesialis:', error);
|
|
antrianPerSpesialis.value = [];
|
|
} finally {
|
|
loadingAntrianPerSpesialis.value = false;
|
|
}
|
|
};
|
|
|
|
// Fetch data antrian per subspesialis
|
|
const fetchAntrianPerSubspesialis = async () => {
|
|
loadingAntrianPerSubspesialis.value = true;
|
|
try {
|
|
const response = await api.get('/dashboard/table-antrian-per-subspesialis', {
|
|
params: {
|
|
year: selectedYear.value,
|
|
month: selectedMonth.value + 1 // API expects 1-12, not 0-11
|
|
}
|
|
});
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
// Transform API data to match table headers (lowercase keys)
|
|
antrianPerSubspesialis.value = response.data.data.map((item: any) => ({
|
|
spesialis: item.Spesialis,
|
|
subspesialis: item.SubSpesialis,
|
|
total: item.Total,
|
|
belum: item.Belum,
|
|
selesai: item.Selesai,
|
|
tunda: item.Tunda,
|
|
batal: item.Batal
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching antrian per subspesialis:', error);
|
|
antrianPerSubspesialis.value = [];
|
|
} finally {
|
|
loadingAntrianPerSubspesialis.value = false;
|
|
}
|
|
};
|
|
|
|
// Computed properties untuk total masing-masing status
|
|
const totalBelum = computed(() => {
|
|
const item = statusAntrianData.value.find(d => d.statust === 'Belum');
|
|
return item?.jumlah || 0;
|
|
});
|
|
|
|
const totalSelesai = computed(() => {
|
|
const item = statusAntrianData.value.find(d => d.statust === 'Selesai');
|
|
return item?.jumlah || 0;
|
|
});
|
|
|
|
const totalTunda = computed(() => {
|
|
const item = statusAntrianData.value.find(d => d.statust === 'Tunda');
|
|
return item?.jumlah || 0;
|
|
});
|
|
|
|
const totalBatal = computed(() => {
|
|
const item = statusAntrianData.value.find(d => d.statust === 'Batal');
|
|
return item?.jumlah || 0;
|
|
});
|
|
|
|
const totalAll = computed(() => {
|
|
return totalBelum.value + totalSelesai.value + totalTunda.value + totalBatal.value;
|
|
});
|
|
|
|
// Buka modal pendaftaran
|
|
const openModal = () => {
|
|
showModal.value = true;
|
|
};
|
|
|
|
const handleModalSuccess = () => {
|
|
// Refresh data dashboard setelah pendaftaran berhasil
|
|
fetchStatusAntrian();
|
|
fetchKategoriAntrian();
|
|
fetchAntrianPerHari();
|
|
fetchAntrianPerSpesialis();
|
|
fetchAntrianPerSubspesialis();
|
|
console.log('Pendaftaran berhasil');
|
|
};
|
|
|
|
// Data dummy untuk antrian operasi berdasarkan status
|
|
// Dihapus karena sudah menggunakan API
|
|
|
|
// Data untuk Pie Chart - Status Antrian
|
|
const pieChartOptions = ref({
|
|
chart: {
|
|
type: 'pie',
|
|
height: 350
|
|
},
|
|
labels: ['Belum', 'Selesai', 'Tunda', 'Batal'],
|
|
colors: ['#4C71CF', '#2FA4A9', '#F06D02', '#EF4444'],
|
|
legend: {
|
|
position: 'bottom'
|
|
},
|
|
dataLabels: {
|
|
enabled: true,
|
|
formatter: function (val: number) {
|
|
return Math.round(val) + '%'
|
|
}
|
|
},
|
|
responsive: [{
|
|
breakpoint: 480,
|
|
options: {
|
|
chart: {
|
|
width: 300
|
|
},
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}]
|
|
});
|
|
|
|
const pieChartSeries = computed(() => {
|
|
return [
|
|
totalBelum.value,
|
|
totalSelesai.value,
|
|
totalTunda.value,
|
|
totalBatal.value
|
|
];
|
|
});
|
|
|
|
// Data untuk Pie Chart - Kategori Antrian (computed untuk dynamic labels)
|
|
const pieKategoriChartOptions = computed(() => ({
|
|
chart: {
|
|
type: 'pie',
|
|
height: 350
|
|
},
|
|
labels: kategoriAntrianData.value.map(item => item.kategori.split('-')[1]?.trim() ),
|
|
colors: ['#4C71CF', '#2FA4A9', '#F06D02', '#EF4444', '#9C27B0', '#E91E63', '#00BCD4', '#4CAF50'],
|
|
legend: {
|
|
position: 'bottom'
|
|
},
|
|
dataLabels: {
|
|
enabled: true,
|
|
formatter: function (val: number) {
|
|
return Math.round(val) + '%'
|
|
}
|
|
},
|
|
responsive: [{
|
|
breakpoint: 480,
|
|
options: {
|
|
chart: {
|
|
width: 300
|
|
},
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}]
|
|
}));
|
|
|
|
const pieKategoriChartSeries = computed(() => {
|
|
return kategoriAntrianData.value.map(item => item.jumlah);
|
|
});
|
|
|
|
// Data untuk Column Chart - Antrian Per Hari dalam 1 Bulan
|
|
const lineChartOptions = computed(() => {
|
|
const monthName = months[selectedMonth.value].text.substring(0, 3);
|
|
|
|
// Get categories from API data or generate default
|
|
const categories = antrianPerHariData.value.length > 0
|
|
? antrianPerHariData.value.map(item => {
|
|
const date = new Date(item.TanggalDaftar);
|
|
return `${date.getDate()} ${monthName}`;
|
|
})
|
|
: [];
|
|
|
|
return {
|
|
chart: {
|
|
type: 'bar',
|
|
height: 350,
|
|
stacked: true,
|
|
toolbar: {
|
|
show: true
|
|
},
|
|
zoom: {
|
|
enabled: true
|
|
}
|
|
},
|
|
colors: ['#4C71CF', '#2FA4A9', '#F06D02', '#EF4444'],
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: false,
|
|
columnWidth: '55%',
|
|
borderRadius: 4
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
stroke: {
|
|
show: true,
|
|
width: 2,
|
|
colors: ['transparent']
|
|
},
|
|
xaxis: {
|
|
categories: categories,
|
|
title: {
|
|
text: 'Tanggal'
|
|
}
|
|
},
|
|
yaxis: {
|
|
title: {
|
|
text: 'Jumlah Antrian'
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'left'
|
|
},
|
|
fill: {
|
|
opacity: 1
|
|
},
|
|
tooltip: {
|
|
shared: true,
|
|
intersect: false,
|
|
y: {
|
|
formatter: function (val: number) {
|
|
return val + ' antrian'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
const lineChartSeries = computed(() => {
|
|
// If no data from API, return empty series
|
|
if (antrianPerHariData.value.length === 0) {
|
|
return [
|
|
{ name: 'Belum', data: [] },
|
|
{ name: 'Selesai', data: [] },
|
|
{ name: 'Tunda', data: [] },
|
|
{ name: 'Batal', data: [] }
|
|
];
|
|
}
|
|
|
|
// Map data from API
|
|
return [
|
|
{
|
|
name: 'Belum',
|
|
data: antrianPerHariData.value.map(item => item.Belum)
|
|
},
|
|
{
|
|
name: 'Selesai',
|
|
data: antrianPerHariData.value.map(item => item.Selesai)
|
|
},
|
|
{
|
|
name: 'Tunda',
|
|
data: antrianPerHariData.value.map(item => item.Tunda)
|
|
},
|
|
{
|
|
name: 'Batal',
|
|
data: antrianPerHariData.value.map(item => item.Batal)
|
|
}
|
|
];
|
|
});
|
|
|
|
// Data antrian per spesialis - populated from API
|
|
const antrianPerSpesialis = ref<Array<{ spesialis: string; total: number; belum: number; selesai: number; tunda: number; batal: number }>>([]);
|
|
|
|
// Data antrian per subspesialis - populated from API
|
|
const antrianPerSubspesialis = ref<Array<{ spesialis: string; subspesialis: string; total: number; belum: number; selesai: number; tunda: number; batal: number }>>([]);
|
|
|
|
// Chart options untuk Antrian Per Spesialis
|
|
const spesialisChartOptions = computed(() => ({
|
|
chart: {
|
|
type: 'bar',
|
|
height: 400,
|
|
stacked: true,
|
|
toolbar: {
|
|
show: true
|
|
}
|
|
},
|
|
colors: ['#4C71CF', '#2FA4A9', '#F06D02', '#EF4444'],
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: true,
|
|
borderRadius: 6,
|
|
dataLabels: {
|
|
total: {
|
|
enabled: true,
|
|
style: {
|
|
fontSize: '12px',
|
|
fontWeight: 600
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
stroke: {
|
|
show: true,
|
|
width: 1,
|
|
colors: ['#fff']
|
|
},
|
|
xaxis: {
|
|
categories: antrianPerSpesialis.value.map(item => item.spesialis),
|
|
title: {
|
|
text: 'Jumlah Antrian'
|
|
}
|
|
},
|
|
yaxis: {
|
|
title: {
|
|
text: 'Spesialis'
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'left',
|
|
offsetX: 40
|
|
},
|
|
fill: {
|
|
opacity: 1
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: function (val: number) {
|
|
return val + ' antrian'
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
const spesialisChartSeries = computed(() => [
|
|
{
|
|
name: 'Belum',
|
|
data: antrianPerSpesialis.value.map(item => item.belum)
|
|
},
|
|
{
|
|
name: 'Selesai',
|
|
data: antrianPerSpesialis.value.map(item => item.selesai)
|
|
},
|
|
{
|
|
name: 'Tunda',
|
|
data: antrianPerSpesialis.value.map(item => item.tunda)
|
|
},
|
|
{
|
|
name: 'Batal',
|
|
data: antrianPerSpesialis.value.map(item => item.batal)
|
|
}
|
|
]);
|
|
|
|
// Chart options untuk Antrian Per Subspesialis
|
|
const subspesialisChartOptions = computed(() => ({
|
|
chart: {
|
|
type: 'bar',
|
|
height: 400,
|
|
stacked: true,
|
|
toolbar: {
|
|
show: true
|
|
}
|
|
},
|
|
colors: ['#4C71CF', '#2FA4A9', '#F06D02', '#EF4444'],
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: true,
|
|
borderRadius: 6,
|
|
dataLabels: {
|
|
total: {
|
|
enabled: true,
|
|
style: {
|
|
fontSize: '11px',
|
|
fontWeight: 600
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
stroke: {
|
|
show: true,
|
|
width: 1,
|
|
colors: ['#fff']
|
|
},
|
|
xaxis: {
|
|
categories: antrianPerSubspesialis.value.map(item => `${item.subspesialis} (${item.spesialis})`),
|
|
title: {
|
|
text: 'Jumlah Antrian'
|
|
}
|
|
},
|
|
yaxis: {
|
|
title: {
|
|
text: 'Subspesialis'
|
|
},
|
|
labels: {
|
|
style: {
|
|
fontSize: '11px'
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'left',
|
|
offsetX: 40
|
|
},
|
|
fill: {
|
|
opacity: 1
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: function (val: number) {
|
|
return val + ' antrian'
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
const subspesialisChartSeries = computed(() => [
|
|
{
|
|
name: 'Belum',
|
|
data: antrianPerSubspesialis.value.map(item => item.belum)
|
|
},
|
|
{
|
|
name: 'Selesai',
|
|
data: antrianPerSubspesialis.value.map(item => item.selesai)
|
|
},
|
|
{
|
|
name: 'Tunda',
|
|
data: antrianPerSubspesialis.value.map(item => item.tunda)
|
|
},
|
|
{
|
|
name: 'Batal',
|
|
data: antrianPerSubspesialis.value.map(item => item.batal)
|
|
}
|
|
]);
|
|
|
|
// Watch untuk refetch data saat bulan atau tahun berubah
|
|
watch([selectedMonth, selectedYear], () => {
|
|
if (hasDashboardAccess.value) {
|
|
fetchStatusAntrian();
|
|
fetchKategoriAntrian();
|
|
fetchAntrianPerHari();
|
|
fetchAntrianPerSpesialis();
|
|
fetchAntrianPerSubspesialis();
|
|
}
|
|
});
|
|
|
|
// Watch for dashboard access changes (when menu loads)
|
|
watch(hasDashboardAccess, (hasAccess) => {
|
|
if (hasAccess) {
|
|
fetchStatusAntrian();
|
|
fetchKategoriAntrian();
|
|
fetchAntrianPerHari();
|
|
fetchAntrianPerSpesialis();
|
|
fetchAntrianPerSubspesialis();
|
|
}
|
|
});
|
|
|
|
// Fetch data saat component mounted
|
|
onMounted(() => {
|
|
if (hasDashboardAccess.value) {
|
|
fetchStatusAntrian();
|
|
fetchKategoriAntrian();
|
|
fetchAntrianPerHari();
|
|
fetchAntrianPerSpesialis();
|
|
fetchAntrianPerSubspesialis();
|
|
}
|
|
});
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
pageTitle: 'Dashboard',
|
|
breadcrumbs: [{ text: 'Dashboard' }]
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Loading State - Menu Belum Ready -->
|
|
<v-row v-if="!menuStore.menuFetched">
|
|
<div> </div>
|
|
</v-row>
|
|
|
|
<!-- Welcome Card - Limited Access (Setelah Menu Ready) -->
|
|
<v-row v-else-if="!hasDashboardAccess">
|
|
<!-- Main Welcome Card -->
|
|
<v-col cols="12">
|
|
<v-card elevation="10" class="overflow-hidden">
|
|
<v-card-item class="pa-6">
|
|
<div class="d-flex align-center ga-6 flex-wrap">
|
|
<!-- Left Side - Avatar -->
|
|
<div class="flex-shrink-0">
|
|
<v-avatar size="140" class="elevation-1 bg-lightprimary">
|
|
<Icon icon="solar:user-circle-bold" height="100" class="text-primary" />
|
|
</v-avatar>
|
|
</div>
|
|
|
|
<!-- Right Side - Content -->
|
|
<div class="flex-grow-1">
|
|
|
|
<h1 class="text-h3 font-weight-bold text-primary mb-2">
|
|
👋 Selamat Datang, {{ user?.name || 'User' }}!
|
|
</h1>
|
|
<p class="text-h6 text-medium-emphasis mb-1">
|
|
Sistem Informasi Antrean Operasi RSSA
|
|
</p>
|
|
<div class="d-flex align-center ga-2 text-medium-emphasis mb-3">
|
|
<Icon icon="solar:calendar-bold" height="18" class="text-primary" />
|
|
<span class="text-body-1">
|
|
{{ new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) }}
|
|
</span>
|
|
</div>
|
|
<div class="d-flex align-center ga-2 flex-wrap">
|
|
<v-chip size="small" color="success" variant="flat" class="font-weight-bold px-3">
|
|
<Icon icon="solar:shield-check-bold" height="16" class="text-white mr-1" />
|
|
Terautentikasi
|
|
</v-chip>
|
|
<v-chip v-if="user?.client_roles && user.client_roles.length > 0"
|
|
size="small"
|
|
color="primary"
|
|
variant="outlined"
|
|
class="font-weight-medium px-3">
|
|
<Icon icon="solar:user-id-bold" height="16" class="text-primary mr-1" />
|
|
{{ user.client_roles.join(', ') }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</v-card-item>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Full Dashboard - With Access -->
|
|
<template v-else>
|
|
<v-row class="mb-4">
|
|
<v-col cols="12">
|
|
</v-col>
|
|
<v-col cols="12">
|
|
<v-card elevation="0" class="bg-transparent">
|
|
<v-card-item class="pa-0">
|
|
<div class="d-flex align-center justify-space-between flex-wrap ga-3">
|
|
<div class="d-flex align-center ga-3 flex-wrap">
|
|
<div class="d-flex align-center ga-2">
|
|
<v-select
|
|
v-model="selectedMonth"
|
|
:items="months"
|
|
item-title="text"
|
|
item-value="value"
|
|
density="compact"
|
|
variant="outlined"
|
|
hide-details
|
|
style="min-width: 140px;"
|
|
prepend-inner-icon="mdi-calendar-month"
|
|
></v-select>
|
|
<v-select
|
|
v-model="selectedYear"
|
|
:items="years"
|
|
density="compact"
|
|
variant="outlined"
|
|
hide-details
|
|
style="min-width: 100px;"
|
|
></v-select>
|
|
</div>
|
|
</div>
|
|
<v-btn
|
|
color="primary"
|
|
size="large"
|
|
elevation="2"
|
|
@click="openModal"
|
|
>
|
|
<Icon icon="solar:add-circle-bold" height="20" class="mr-2" />
|
|
Pendaftaran Operasi Baru
|
|
</v-btn>
|
|
</div>
|
|
</v-card-item>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<!-- Status Cards -->
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card elevation="10" class="bg-primary" style="background: linear-gradient(135deg, #708DD9 0%, #4C71CF 100%);">
|
|
<v-card-item>
|
|
<div v-if="loadingStatusAntrian" class="d-flex ga-3 align-center">
|
|
<v-skeleton-loader type="avatar" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<div class="flex-grow-1">
|
|
<v-skeleton-loader type="text" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<v-skeleton-loader type="heading" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
</div>
|
|
</div>
|
|
<div v-else class="d-flex ga-3 align-center">
|
|
<v-avatar size="56" class="rounded-md" style="background: rgba(255,255,255,0.2);">
|
|
<Icon icon="solar:hourglass-line-outline" class="text-white" height="28" />
|
|
</v-avatar>
|
|
<div>
|
|
<h6 class="text-subtitle-1 text-white opacity-80 mb-1">Belum</h6>
|
|
<h3 class="text-h2 font-weight-bold text-white">{{ totalBelum }}</h3>
|
|
</div>
|
|
</div>
|
|
</v-card-item>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card elevation="10" class="bg-success" style="background: linear-gradient(135deg, #59B6BA 0%, #2FA4A9 100%);">
|
|
<v-card-item>
|
|
<div v-if="loadingStatusAntrian" class="d-flex ga-3 align-center">
|
|
<v-skeleton-loader type="avatar" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<div class="flex-grow-1">
|
|
<v-skeleton-loader type="text" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<v-skeleton-loader type="heading" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
</div>
|
|
</div>
|
|
<div v-else class="d-flex ga-3 align-center">
|
|
<v-avatar size="56" class="rounded-md" style="background: rgba(255,255,255,0.2);">
|
|
<Icon icon="solar:check-circle-outline" class="text-white" height="28" />
|
|
</v-avatar>
|
|
<div>
|
|
<h6 class="text-subtitle-1 text-white opacity-80 mb-1">Selesai</h6>
|
|
<h3 class="text-h2 font-weight-bold text-white">{{ totalSelesai }}</h3>
|
|
</div>
|
|
</div>
|
|
</v-card-item>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card elevation="10" class="bg-warning" style="background: linear-gradient(135deg, #F38A35 0%, #F06D02 100%);">
|
|
<v-card-item>
|
|
<div v-if="loadingStatusAntrian" class="d-flex ga-3 align-center">
|
|
<v-skeleton-loader type="avatar" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<div class="flex-grow-1">
|
|
<v-skeleton-loader type="text" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<v-skeleton-loader type="heading" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
</div>
|
|
</div>
|
|
<div v-else class="d-flex ga-3 align-center">
|
|
<v-avatar size="56" class="rounded-md" style="background: rgba(255,255,255,0.2);">
|
|
<Icon icon="solar:pause-circle-outline" class="text-white" height="28" />
|
|
</v-avatar>
|
|
<div>
|
|
<h6 class="text-subtitle-1 text-white opacity-80 mb-1">Tunda</h6>
|
|
<h3 class="text-h2 font-weight-bold text-white">{{ totalTunda }}</h3>
|
|
</div>
|
|
</div>
|
|
</v-card-item>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card elevation="10" class="bg-error" style="background: linear-gradient(135deg, #F26969 0%, #EF4444 100%);">
|
|
<v-card-item>
|
|
<div v-if="loadingStatusAntrian" class="d-flex ga-3 align-center">
|
|
<v-skeleton-loader type="avatar" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<div class="flex-grow-1">
|
|
<v-skeleton-loader type="text" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
<v-skeleton-loader type="heading" class="bg-transparent white-skeleton"></v-skeleton-loader>
|
|
</div>
|
|
</div>
|
|
<div v-else class="d-flex ga-3 align-center">
|
|
<v-avatar size="56" class="rounded-md" style="background: rgba(255,255,255,0.2);">
|
|
<Icon icon="solar:close-circle-outline" class="text-white" height="28" />
|
|
</v-avatar>
|
|
<div>
|
|
<h6 class="text-subtitle-1 text-white opacity-80 mb-1">Batal</h6>
|
|
<h3 class="text-h2 font-weight-bold text-white">{{ totalBatal }}</h3>
|
|
</div>
|
|
</div>
|
|
</v-card-item>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Pie Chart - Status Antrian -->
|
|
<v-col cols="12" lg="6">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<div class="d-flex align-center justify-space-between mb-4">
|
|
<h5 class="text-h5 font-weight-bold">Perbandingan Status Antrean</h5>
|
|
<v-avatar size="40" class="rounded-md bg-lightprimary">
|
|
<Icon icon="solar:pie-chart-2-outline" class="text-primary" height="22" />
|
|
</v-avatar>
|
|
</div>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<div v-if="loadingStatusAntrian" class="d-flex justify-center align-center" style="min-height: 350px;">
|
|
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
|
</div>
|
|
<ClientOnly v-else>
|
|
<VueApexCharts
|
|
:key="`pie-status-${selectedMonth}-${selectedYear}`"
|
|
type="donut"
|
|
height="350"
|
|
:options="pieChartOptions"
|
|
:series="pieChartSeries"
|
|
></VueApexCharts>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Pie Chart - Kategori Antrian -->
|
|
<v-col cols="12" lg="6">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<div class="d-flex align-center justify-space-between mb-4">
|
|
<h5 class="text-h5 font-weight-bold">Perbandingan Kategori Antrean</h5>
|
|
<v-avatar size="40" class="rounded-md bg-lightsecondary">
|
|
<Icon icon="solar:pie-chart-2-outline" class="text-secondary" height="22" />
|
|
</v-avatar>
|
|
</div>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<div v-if="loadingKategoriAntrian" class="d-flex justify-center align-center" style="min-height: 350px;">
|
|
<v-progress-circular indeterminate color="secondary" size="64"></v-progress-circular>
|
|
</div>
|
|
<ClientOnly v-else>
|
|
<VueApexCharts
|
|
:key="`pie-kategori-${selectedMonth}-${selectedYear}`"
|
|
type="donut"
|
|
height="350"
|
|
:options="pieKategoriChartOptions"
|
|
:series="pieKategoriChartSeries"
|
|
></VueApexCharts>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Line Chart - Antrian Per Hari -->
|
|
<v-col cols="12" lg="12">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<div class="d-flex align-center justify-space-between mb-4">
|
|
<h5 class="text-h5 font-weight-bold">Antrean Per Hari ({{ months[selectedMonth].text }} {{ selectedYear }})</h5>
|
|
<v-avatar size="40" class="rounded-md bg-lightsecondary">
|
|
<Icon icon="solar:chart-outline" class="text-secondary" height="22" />
|
|
</v-avatar>
|
|
</div>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<div v-if="loadingAntrianPerHari" class="d-flex justify-center align-center" style="min-height: 350px;">
|
|
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
|
</div>
|
|
<ClientOnly v-else>
|
|
<VueApexCharts
|
|
:key="`column-${selectedMonth}-${selectedYear}`"
|
|
type="bar"
|
|
height="350"
|
|
:options="lineChartOptions"
|
|
:series="lineChartSeries"
|
|
></VueApexCharts>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Chart Antrian Per Spesialis -->
|
|
<v-col cols="12" lg="6">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<div class="d-flex align-center justify-space-between mb-4">
|
|
<h5 class="text-h5 font-weight-bold">Antrean Per Spesialis</h5>
|
|
<v-avatar size="40" class="rounded-md bg-lightprimary">
|
|
<Icon icon="solar:chart-square-bold" class="text-primary" height="22" />
|
|
</v-avatar>
|
|
</div>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<div v-if="loadingAntrianPerSpesialis" class="d-flex justify-center align-center" style="min-height: 400px;">
|
|
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
|
</div>
|
|
<div v-else-if="antrianPerSpesialis.length === 0" class="d-flex justify-center align-center" style="min-height: 400px;">
|
|
<div class="text-center">
|
|
<Icon icon="solar:chart-square-outline" height="64" class="text-medium-emphasis mb-4" />
|
|
<p class="text-body-1 text-medium-emphasis">Tidak ada data antrian</p>
|
|
</div>
|
|
</div>
|
|
<ClientOnly v-else>
|
|
<VueApexCharts
|
|
:key="`spesialis-${selectedMonth}-${selectedYear}`"
|
|
type="bar"
|
|
height="400"
|
|
:options="spesialisChartOptions"
|
|
:series="spesialisChartSeries"
|
|
></VueApexCharts>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Chart Antrian Per Subspesialis -->
|
|
<v-col cols="12" lg="6">
|
|
<v-card elevation="10">
|
|
<v-card-item>
|
|
<div class="d-flex align-center justify-space-between mb-4">
|
|
<h5 class="text-h5 font-weight-bold">Antrean Per Subspesialis</h5>
|
|
<v-avatar size="40" class="rounded-md bg-lightsecondary">
|
|
<Icon icon="solar:chart-square-bold" class="text-secondary" height="22" />
|
|
</v-avatar>
|
|
</div>
|
|
</v-card-item>
|
|
<v-divider></v-divider>
|
|
<v-card-text>
|
|
<div v-if="loadingAntrianPerSubspesialis" class="d-flex justify-center align-center" style="min-height: 400px;">
|
|
<v-progress-circular indeterminate color="secondary" size="64"></v-progress-circular>
|
|
</div>
|
|
<div v-else-if="antrianPerSubspesialis.length === 0" class="d-flex justify-center align-center" style="min-height: 400px;">
|
|
<div class="text-center">
|
|
<Icon icon="solar:chart-square-outline" height="64" class="text-medium-emphasis mb-4" />
|
|
<p class="text-body-1 text-medium-emphasis">Tidak ada data antrian</p>
|
|
</div>
|
|
</div>
|
|
<ClientOnly v-else>
|
|
<VueApexCharts
|
|
:key="`subspesialis-${selectedMonth}-${selectedYear}`"
|
|
type="bar"
|
|
height="400"
|
|
:options="subspesialisChartOptions"
|
|
:series="subspesialisChartSeries"
|
|
></VueApexCharts>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</template>
|
|
|
|
<!-- Modal Pendaftaran -->
|
|
<ModalPendaftaran
|
|
v-model="showModal"
|
|
mode="create"
|
|
@success="handleModalSuccess"
|
|
/>
|
|
</template>
|