691 lines
20 KiB
Vue
691 lines
20 KiB
Vue
<template>
|
|
<v-container fluid class="pa-8 dashboard-bg">
|
|
|
|
<div class="d-flex justify-space-between align-center mb-10">
|
|
<div>
|
|
<h1 class="text-h3 font-weight-black primary-text">
|
|
Antrean Dashboard
|
|
</h1>
|
|
<p v-if="user" class="text-subtitle-1 secondary-text mt-2">
|
|
Selamat Datang,
|
|
<span class="font-weight-bold primary-accent">
|
|
{{ user.name || user.preferred_username }}
|
|
</span>! 🍊
|
|
</p>
|
|
</div>
|
|
<div class="d-flex align-center">
|
|
|
|
<v-menu offset-y>
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
v-bind="props"
|
|
color="#FF9B1B"
|
|
variant="flat"
|
|
class="text-caption font-weight-bold rounded-lg elevation-2 mr-4 export-btn"
|
|
>
|
|
<v-icon start icon="mdi-download-box-outline"></v-icon>
|
|
Export Data ({{ exportType }})
|
|
<v-icon end>mdi-menu-down</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item
|
|
v-for="(item, index) in exportOptions"
|
|
:key="index"
|
|
@click="handleExport(item.type)"
|
|
>
|
|
<v-list-item-title>
|
|
<v-icon start :icon="item.icon"></v-icon>
|
|
{{ item.title }}
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
|
|
<v-chip color="white" variant="flat" class="pa-4 font-weight-bold text-caption secondary-text rounded-lg date-chip">
|
|
<v-icon start icon="mdi-calendar-check-outline" class="primary-accent"></v-icon>
|
|
{{ currentDate }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<v-row class="mb-10">
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #D17A4A;">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" class="mr-4 primary-accent">mdi-tablet-dashboard</v-icon>
|
|
<div>
|
|
<div class="text-h4 font-weight-black primary-accent">150</div>
|
|
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Antrean Online</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #4A5F7A;">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" class="mr-4 secondary-accent">mdi-account-hard-hat</v-icon>
|
|
<div>
|
|
<div class="text-h4 font-weight-black secondary-accent">100</div>
|
|
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Antrean MJKN</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #D17A4A;">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" class="mr-4 primary-accent">mdi-account-group</v-icon>
|
|
<div>
|
|
<div class="text-h4 font-weight-black primary-accent">500</div>
|
|
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Total Kunjungan</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="pa-6 rounded-xl elevation-3 hover-effect card-style" style="border-left: 4px solid #4A5F7A;">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" class="mr-4 secondary-accent">mdi-timer-sand</v-icon>
|
|
<div>
|
|
<div class="text-h4 font-weight-black secondary-accent">7.5s</div>
|
|
<div class="text-caption text-uppercase font-weight-medium secondary-light mt-1">Avg. Check-in Time</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row class="mb-4">
|
|
<v-col cols="12" md="6">
|
|
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
|
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
|
<span class="text-h6 font-weight-bold primary-text">Registration Trend</span>
|
|
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="#D17A4A" class="text-caption rounded-lg">
|
|
<v-btn size="small" value="day">Day</v-btn>
|
|
</v-btn-toggle>
|
|
</v-card-title>
|
|
<v-card-text class="pa-0">
|
|
<Bar
|
|
:data="queueChartData"
|
|
:options="queueChartOptions"
|
|
style="height: 350px"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
|
<v-card-title class="d-flex justify-space-between align-center px-0 pt-0 mb-4">
|
|
<span class="text-h6 font-weight-bold primary-text">Total Registrasi Perbulan</span>
|
|
<div>
|
|
<v-chip
|
|
:color="activeYear === '2024' ? '#D17A4A' : '#F5F5F5'"
|
|
:text-color="activeYear === '2024' ? 'white' : '#FF9B1B'"
|
|
variant="flat"
|
|
class="mr-2 text-caption font-weight-bold cursor-pointer transition-chip rounded-pill"
|
|
@click="changeYear('2024')"
|
|
>
|
|
2024
|
|
</v-chip>
|
|
<v-chip
|
|
:color="activeYear === '2025' ? '#D17A4A' : '#F5F5F5'"
|
|
:text-color="activeYear === '2025' ? 'white' : '#FF9B1B'"
|
|
variant="flat"
|
|
class="text-caption font-weight-bold cursor-pointer transition-chip rounded-pill"
|
|
@click="changeYear('2025')"
|
|
>
|
|
2025
|
|
</v-chip>
|
|
</div>
|
|
</v-card-title>
|
|
<v-card-text class="pa-0">
|
|
<Bar
|
|
:data="barData"
|
|
:options="barOptions"
|
|
style="height: 350px"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
|
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">
|
|
Realtime Check-in Velocity
|
|
<v-chip size="small" color="success" class="ml-2">
|
|
<v-icon start size="small">mdi-circle</v-icon>
|
|
Live
|
|
</v-chip>
|
|
</v-card-title>
|
|
<v-card-text class="pa-0">
|
|
<Line
|
|
ref="realtimeChart"
|
|
:data="realtimeLineData"
|
|
:options="lineOptions"
|
|
style="height: 350px"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-card class="pa-6 rounded-xl elevation-3 card-style chart-card">
|
|
<v-card-title class="text-h6 font-weight-bold primary-text px-0 pt-0 mb-4">Registrant Type Breakdown</v-card-title>
|
|
<v-card-text class="d-flex justify-center align-center pa-0" style="height: 350px;">
|
|
<Pie
|
|
:data="pieData"
|
|
:options="pieOptions"
|
|
style="max-height: 100%; max-width: 100%;"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-overlay v-model="isLoading" contained class="align-center justify-center dashboard-bg">
|
|
<v-progress-circular indeterminate size="64" color="#D17A4A"></v-progress-circular>
|
|
<p class="mt-4 primary-text">Loading dashboard...</p>
|
|
</v-overlay>
|
|
</v-container>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed, onUnmounted } from 'vue';
|
|
import { Bar, Pie, Line } from 'vue-chartjs';
|
|
import dayjs from 'dayjs';
|
|
import weekday from 'dayjs/plugin/weekday';
|
|
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
|
import 'dayjs/locale/id';
|
|
|
|
dayjs.extend(weekday);
|
|
dayjs.extend(weekOfYear);
|
|
dayjs.locale('id');
|
|
|
|
import {
|
|
Chart as ChartJS,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
BarElement,
|
|
CategoryScale,
|
|
LinearScale,
|
|
ArcElement,
|
|
PointElement,
|
|
LineElement,
|
|
} from 'chart.js';
|
|
|
|
definePageMeta({
|
|
middleware:['auth']
|
|
})
|
|
|
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, PointElement, LineElement);
|
|
|
|
// --- KEYCLOAK/AUTH INTEGRATION ---
|
|
// const { user, isLoading, checkAuth, logout } = useAuth()
|
|
// const navigateTo = (path) => { console.log('Navigating to', path); };
|
|
// Mock user for demo purposes - replace with your actual auth
|
|
const user = ref({ name: 'Admin User', preferred_username: 'admin' });
|
|
const isLoading = ref(false);
|
|
const checkAuth = async () => user.value;
|
|
const navigateTo = (path) => console.log('Navigate to:', path);
|
|
// ---------------------------------
|
|
|
|
// --- EXPORT STATE ---
|
|
const exportType = ref('JSON');
|
|
const exportOptions = ref([
|
|
{ title: 'JSON (.json)', type: 'json', icon: 'mdi-code-json' },
|
|
{ title: 'CSV (.csv)', type: 'csv', icon: 'mdi-file-delimited' },
|
|
{ title: 'Excel (.xlsx) - Server Only', type: 'excel', icon: 'mdi-file-excel' },
|
|
{ title: 'PDF (.pdf) - Placeholder', type: 'pdf', icon: 'mdi-file-pdf' },
|
|
]);
|
|
// --------------------
|
|
|
|
// Dashboard state
|
|
const currentDate = ref('');
|
|
const activeYear = ref('2025');
|
|
const queueTimePeriod = ref('day');
|
|
|
|
// --- MOCK DATA FOR CHARTS ---
|
|
const mockQueueData = ref([
|
|
{ date: '2025-10-13', registrants: 300, attendees: 250 },
|
|
{ date: '2025-10-14', registrants: 450, attendees: 400 },
|
|
{ date: '2025-10-15', registrants: 500, attendees: 420 },
|
|
{ date: '2025-10-16', registrants: 400, attendees: 350 },
|
|
{ date: '2025-10-17', registrants: 550, attendees: 500 },
|
|
{ date: '2025-10-18', registrants: 435, attendees: 380 },
|
|
]);
|
|
|
|
// --- REALTIME CHART LOGIC - FIXED ---
|
|
const realtimeChart = ref(null);
|
|
const maxDataPoints = 10;
|
|
let realtimeInterval = null;
|
|
|
|
const realtimeLineData = ref({
|
|
labels: Array.from({ length: maxDataPoints }, (_, i) =>
|
|
dayjs().subtract((maxDataPoints - 1 - i) * 5, 'second').format('HH:mm:ss')
|
|
),
|
|
datasets: [
|
|
{
|
|
label: 'Check-ins/min',
|
|
backgroundColor: 'rgba(74, 95, 122, 0.15)',
|
|
borderColor: '#5FB7FF',
|
|
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 20) + 40),
|
|
fill: true,
|
|
tension: 0.3,
|
|
borderWidth: 3,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
pointBackgroundColor: '#4A5F7A',
|
|
},
|
|
],
|
|
});
|
|
|
|
const lineOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: { duration: 300 },
|
|
plugins: {
|
|
legend: { display: true },
|
|
title: { display: false }
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
suggestedMax: 80,
|
|
title: { display: true, text: 'Check-ins per Minute' },
|
|
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
|
},
|
|
x: {
|
|
title: { display: true, text: 'Time (HH:MM:SS)' },
|
|
grid: { display: false }
|
|
}
|
|
}
|
|
});
|
|
|
|
const updateRealtimeData = () => {
|
|
const data = realtimeLineData.value.datasets[0].data;
|
|
const labels = realtimeLineData.value.labels;
|
|
|
|
// Remove first data point
|
|
data.shift();
|
|
labels.shift();
|
|
|
|
// Add new data point
|
|
const newDataPoint = Math.floor(Math.random() * 20) + 40;
|
|
const newTimeLabel = dayjs().format('HH:mm:ss');
|
|
|
|
data.push(newDataPoint);
|
|
labels.push(newTimeLabel);
|
|
|
|
console.log('Realtime chart updated:', newTimeLabel, newDataPoint);
|
|
};
|
|
// --- END REALTIME CHART LOGIC ---
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const sessionUser = await checkAuth()
|
|
if (sessionUser) {
|
|
// Set date with Indonesian locale
|
|
currentDate.value = dayjs().format('dddd, DD MMMM YYYY');
|
|
console.log('Current date set:', currentDate.value);
|
|
|
|
// Start realtime updates every 5 seconds
|
|
realtimeInterval = setInterval(updateRealtimeData, 5000);
|
|
console.log('Realtime interval started');
|
|
} else {
|
|
await navigateTo('/LoginPage');
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth check error:', error);
|
|
await navigateTo('/LoginPage');
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (realtimeInterval) {
|
|
clearInterval(realtimeInterval);
|
|
console.log('Realtime interval cleared');
|
|
}
|
|
});
|
|
|
|
const changeYear = (year) => {
|
|
activeYear.value = year;
|
|
};
|
|
|
|
// --- DATA UTILITY FUNCTIONS ---
|
|
const getKpiData = () => {
|
|
return [
|
|
{ name: 'Antrean Online', value: 150 },
|
|
{ name: 'Antrean MJKN', value: 100 },
|
|
{ name: 'Total Kunjungan', value: 500 },
|
|
{ name: 'Avg. Check-in Time', value: '7.5s' },
|
|
];
|
|
};
|
|
|
|
const convertJsonToCsv = (jsonData, fields) => {
|
|
if (!jsonData.length) return '';
|
|
|
|
const header = fields.join(',');
|
|
|
|
const csv = jsonData.map(row =>
|
|
fields.map(fieldName => {
|
|
let value = row[fieldName];
|
|
if (typeof value === 'string') {
|
|
value = `"${value.replace(/"/g, '""')}"`;
|
|
}
|
|
return value;
|
|
}).join(',')
|
|
).join('\n');
|
|
|
|
return header + '\n' + csv;
|
|
};
|
|
// --- END DATA UTILITY FUNCTIONS ---
|
|
|
|
// --- EXPORT FUNCTION HANDLER ---
|
|
const downloadFile = (data, filename, mimeType) => {
|
|
const dataStr = `data:${mimeType};charset=utf-8,` + encodeURIComponent(data);
|
|
const downloadAnchorNode = document.createElement('a');
|
|
downloadAnchorNode.setAttribute("href", dataStr);
|
|
downloadAnchorNode.setAttribute("download", filename);
|
|
document.body.appendChild(downloadAnchorNode);
|
|
downloadAnchorNode.click();
|
|
downloadAnchorNode.remove();
|
|
}
|
|
|
|
const handleExport = (type) => {
|
|
exportType.value = type.toUpperCase();
|
|
const filenamePrefix = "antrean_data_" + dayjs().format('YYYYMMDD');
|
|
|
|
const dataToExport = mockQueueData.value;
|
|
|
|
if (type === 'json') {
|
|
downloadFile(JSON.stringify(dataToExport, null, 2), `${filenamePrefix}.json`, 'application/json');
|
|
alert("Data Antrean berhasil diexport sebagai JSON! ✅");
|
|
|
|
} else if (type === 'csv') {
|
|
const fields = ['date', 'registrants', 'attendees'];
|
|
const csvContent = convertJsonToCsv(dataToExport, fields);
|
|
|
|
const kpiData = getKpiData().map(kpi => `# ${kpi.name}: ${kpi.value}`).join('\n');
|
|
const finalCsvContent = kpiData + '\n' + csvContent;
|
|
|
|
downloadFile(finalCsvContent, `${filenamePrefix}.csv`, 'text/csv');
|
|
alert("Data Antrean berhasil diexport sebagai CSV! 📊");
|
|
|
|
} else if (type === 'excel') {
|
|
alert("Excel (.xlsx) export requires a dedicated server endpoint. Simulating download... ⚠️");
|
|
|
|
} else if (type === 'pdf') {
|
|
alert("PDF (.pdf) export requires client-side libraries (like jsPDF) or a server service. Simulating download... 📝");
|
|
}
|
|
};
|
|
// --- END EXPORT FUNCTION HANDLER ---
|
|
|
|
// --- CHART DATA LOGIC ---
|
|
const processQueueData = (data, period) => {
|
|
const grouped = {};
|
|
const sortedDates = data.map(item => ({
|
|
...item,
|
|
date: dayjs(item.date)
|
|
})).sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
|
|
|
sortedDates.forEach(item => {
|
|
let key;
|
|
let label;
|
|
if (period === 'day') {
|
|
key = item.date.format('YYYY-MM-DD');
|
|
label = item.date.format('ddd, DD/MM');
|
|
} else if (period === 'week') {
|
|
key = item.date.format('YYYY-WW');
|
|
label = `Wk ${item.date.week()}`;
|
|
} else if (period === 'month') {
|
|
key = item.date.format('YYYY-MM');
|
|
label = item.date.format('MMM YYYY');
|
|
}
|
|
if (!grouped[key]) {
|
|
grouped[key] = { label: label, registrants: 0, attendees: 0 };
|
|
}
|
|
grouped[key].registrants += item.registrants;
|
|
grouped[key].attendees += item.attendees;
|
|
});
|
|
|
|
const finalLabels = Object.values(grouped).map(g => g.label);
|
|
const finalRegistrants = Object.values(grouped).map(g => g.registrants);
|
|
const finalAttendees = Object.values(grouped).map(g => g.attendees);
|
|
|
|
return {
|
|
labels: finalLabels,
|
|
datasets: [
|
|
{
|
|
label: `Registrants`,
|
|
backgroundColor: '#FFB95F',
|
|
data: finalRegistrants,
|
|
yAxisID: 'y1',
|
|
type: 'bar',
|
|
borderRadius: 6,
|
|
},
|
|
{
|
|
label: `Attendees`,
|
|
backgroundColor: 'transparent',
|
|
borderColor: '#5FB7FF',
|
|
data: finalAttendees,
|
|
yAxisID: 'y1',
|
|
type: 'line',
|
|
pointRadius: 5,
|
|
pointBackgroundColor: '#4A5F7A',
|
|
tension: 0.4,
|
|
borderWidth: 3,
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
const queueChartData = computed(() => {
|
|
if (!mockQueueData.value.length) return { labels: [], datasets: [] };
|
|
return processQueueData(mockQueueData.value, queueTimePeriod.value);
|
|
});
|
|
|
|
const queueChartOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: true, position: 'top' },
|
|
},
|
|
scales: {
|
|
y1: {
|
|
beginAtZero: true,
|
|
title: { display: true, text: 'Count' },
|
|
grid: { display: true, color: 'rgba(0, 0, 0, 0.05)' }
|
|
},
|
|
x: {
|
|
grid: { display: false }
|
|
}
|
|
}
|
|
});
|
|
|
|
const visitorData2024 = [150, 200, 350, 400, 380, 500, 550, 600, 520, 480, 650, 700];
|
|
const visitorData2025 = [200, 250, 400, 450, 420, 550, 600, 650, 570, 520, 700, 750];
|
|
const conversionRate2025 = [2.5, 3.1, 4.0, 4.5, 4.2, 5.5, 6.0, 5.8, 5.7, 5.2, 6.5, 7.5];
|
|
|
|
const barData = computed(() => {
|
|
const dataForYear = activeYear.value === '2024' ? visitorData2024 : visitorData2025;
|
|
const conversionData = activeYear.value === '2024' ? [2.0, 2.5, 3.5, 3.8, 3.6, 4.5, 5.0, 5.2, 4.8, 4.5, 5.5, 6.0] : conversionRate2025;
|
|
|
|
return {
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
datasets: [
|
|
{
|
|
label: `Total Registrants`,
|
|
backgroundColor: '#FFB95F',
|
|
data: dataForYear,
|
|
yAxisID: 'y1',
|
|
type: 'bar',
|
|
borderRadius: 6,
|
|
},
|
|
{
|
|
label: `Conv. Rate (%)`,
|
|
borderColor: '#5FB7FF',
|
|
backgroundColor: 'transparent',
|
|
data: conversionData,
|
|
yAxisID: 'y2',
|
|
type: 'line',
|
|
pointRadius: 5,
|
|
pointBackgroundColor: '#4A5F7A',
|
|
tension: 0.4,
|
|
borderWidth: 3,
|
|
}
|
|
],
|
|
};
|
|
});
|
|
|
|
const barOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: true, position: 'top' },
|
|
},
|
|
scales: {
|
|
y1: {
|
|
position: 'left',
|
|
beginAtZero: true,
|
|
title: { display: true, text: 'Registrants' },
|
|
grid: { display: false }
|
|
},
|
|
y2: {
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
suggestedMax: 10,
|
|
title: { display: true, text: 'Conv. Rate (%)' },
|
|
grid: { drawOnChartArea: false }
|
|
},
|
|
x: {
|
|
grid: { display: false }
|
|
}
|
|
}
|
|
});
|
|
|
|
const pieData = ref({
|
|
labels: ['Offline Registrants', 'Online Registrants'],
|
|
datasets: [
|
|
{
|
|
backgroundColor: ['#5FB7FF', '#FFB95F'],
|
|
hoverBackgroundColor: ['#1B98FF ', '#FF9B1B'],
|
|
data: [759, 1876],
|
|
borderWidth: 3,
|
|
borderColor: '#ffffff',
|
|
},
|
|
],
|
|
});
|
|
|
|
const pieOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.parsed || 0;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(1);
|
|
return `${label}: ${value} (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Color Palette */
|
|
.dashboard-bg {
|
|
background-color: #FAFAFA;
|
|
}
|
|
|
|
.card-style {
|
|
background-color: #FFFFFF;
|
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
/* Text Colors */
|
|
.primary-text {
|
|
color: #2C3E50 !important;
|
|
}
|
|
|
|
.secondary-text {
|
|
color: #7B8794 !important;
|
|
}
|
|
|
|
.secondary-light {
|
|
color: #7B8794 !important;
|
|
}
|
|
|
|
.primary-accent {
|
|
color: #FF9B1B !important;
|
|
}
|
|
|
|
.secondary-accent {
|
|
color: #1B98FF !important;
|
|
}
|
|
|
|
/* Buttons & Interactive Elements */
|
|
.export-btn {
|
|
transition: all 0.3s ease;
|
|
text-transform: none;
|
|
}
|
|
|
|
.export-btn:hover {
|
|
background-color: #E0916E !important;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.cursor-pointer {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Card Hover Effect - Subtle and Clean */
|
|
.hover-effect {
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
|
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.hover-effect:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08) !important;
|
|
}
|
|
|
|
/* Chip Transition */
|
|
.transition-chip {
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.transition-chip:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
/* Date Chip Styling */
|
|
.date-chip {
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
/* Chart Card Fixed Height */
|
|
.chart-card {
|
|
min-height: 480px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chart-card .v-card-text {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
</style> |