744 lines
20 KiB
Vue
744 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="dashboard-title">
|
|
Antrean Dashboard
|
|
</h1>
|
|
<p v-if="user" class="dashboard-subtitle">
|
|
Selamat Datang,
|
|
<span class="user-name">
|
|
{{ 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="primary-600"
|
|
variant="flat"
|
|
class="export-btn mr-4"
|
|
>
|
|
<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="date-chip">
|
|
<v-icon start icon="mdi-calendar-check-outline" color="primary-600"></v-icon>
|
|
{{ currentDate }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<v-row class="mb-10">
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="stat-card stat-card-primary">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" color="primary-600" class="mr-4">mdi-tablet-dashboard</v-icon>
|
|
<div>
|
|
<div class="stat-value">150</div>
|
|
<div class="stat-label">Total Antrean Online</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="stat-card stat-card-secondary">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" color="secondary-600" class="mr-4">mdi-account-hard-hat</v-icon>
|
|
<div>
|
|
<div class="stat-value">100</div>
|
|
<div class="stat-label">Total Antrean MJKN</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="stat-card stat-card-primary">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" color="primary-600" class="mr-4">mdi-account-group</v-icon>
|
|
<div>
|
|
<div class="stat-value">500</div>
|
|
<div class="stat-label">Total Kunjungan</div>
|
|
</div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" sm="6" md="3">
|
|
<v-card class="stat-card stat-card-secondary">
|
|
<div class="d-flex align-center">
|
|
<v-icon size="48" color="secondary-600" class="mr-4">mdi-timer-sand</v-icon>
|
|
<div>
|
|
<div class="stat-value">7.5s</div>
|
|
<div class="stat-label">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="chart-card">
|
|
<v-card-title class="chart-header">
|
|
<span class="chart-title">Registration Trend</span>
|
|
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="primary-600" class="period-toggle">
|
|
<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="chart-card">
|
|
<v-card-title class="chart-header">
|
|
<span class="chart-title">Total Registrasi Perbulan</span>
|
|
<div>
|
|
<v-chip
|
|
:color="activeYear === '2024' ? 'primary-600' : 'neutral-300'"
|
|
:text-color="activeYear === '2024' ? 'white' : 'neutral-700'"
|
|
variant="flat"
|
|
class="year-chip mr-2"
|
|
@click="changeYear('2024')"
|
|
>
|
|
2024
|
|
</v-chip>
|
|
<v-chip
|
|
:color="activeYear === '2025' ? 'primary-600' : 'neutral-300'"
|
|
:text-color="activeYear === '2025' ? 'white' : 'neutral-700'"
|
|
variant="flat"
|
|
class="year-chip"
|
|
@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="chart-card">
|
|
<v-card-title class="chart-header">
|
|
<span class="chart-title">Realtime Check-in Velocity</span>
|
|
<v-chip size="small" color="success-600" class="live-chip">
|
|
<v-icon start size="small">mdi-circle</v-icon>
|
|
Live
|
|
</v-chip>
|
|
</v-card-title>
|
|
<v-card-text class="pa-0">
|
|
<Line
|
|
:data="realtimeLineData"
|
|
:options="lineOptions"
|
|
style="height: 350px"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-card class="chart-card">
|
|
<v-card-title class="chart-header">
|
|
<span class="chart-title">Registrant Type Breakdown</span>
|
|
</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">
|
|
<v-progress-circular indeterminate size="64" color="primary-600"></v-progress-circular>
|
|
<p class="loading-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 { useAuth } from '~/composables/useAuth';
|
|
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);
|
|
|
|
const user = ref(null);
|
|
const isLoading = ref(false);
|
|
const { checkAuth } = useAuth();
|
|
|
|
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' },
|
|
]);
|
|
|
|
const currentDate = ref('');
|
|
const activeYear = ref('2025');
|
|
const queueTimePeriod = ref('day');
|
|
|
|
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 },
|
|
]);
|
|
|
|
// --- SIMPLIFIED REALTIME CHART (NO AUTO-UPDATE) ---
|
|
const maxDataPoints = 10;
|
|
|
|
const realtimeLineData = ref({
|
|
labels: Array.from({ length: maxDataPoints }, (_, i) =>
|
|
dayjs().subtract((maxDataPoints - 1 - i) * 30, 'second').format('HH:mm:ss')
|
|
),
|
|
datasets: [
|
|
{
|
|
label: 'Check-ins/min',
|
|
backgroundColor: 'rgba(6, 99, 199, 0.1)',
|
|
borderColor: '#0671E0',
|
|
data: Array.from({ length: maxDataPoints }, () => Math.floor(Math.random() * 20) + 40),
|
|
fill: true,
|
|
tension: 0.3,
|
|
borderWidth: 3,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
pointBackgroundColor: '#0663C7',
|
|
},
|
|
],
|
|
});
|
|
|
|
const lineOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: { duration: 0 },
|
|
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 }
|
|
}
|
|
}
|
|
});
|
|
// --- END SIMPLIFIED REALTIME CHART ---
|
|
|
|
// Single onMounted
|
|
onMounted(async () => {
|
|
console.log('📊 Dashboard mounted');
|
|
try {
|
|
const sessionUser = await checkAuth();
|
|
if (sessionUser) {
|
|
user.value = sessionUser;
|
|
currentDate.value = dayjs().format('dddd, DD MMMM YYYY');
|
|
console.log('✅ Dashboard loaded successfully');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Auth check error:', error);
|
|
}
|
|
});
|
|
|
|
// Clean onUnmounted
|
|
onUnmounted(() => {
|
|
console.log('🧹 Dashboard unmounting');
|
|
});
|
|
|
|
const changeYear = (year) => {
|
|
activeYear.value = year;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
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... 📝");
|
|
}
|
|
};
|
|
|
|
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: '#0671E0',
|
|
data: finalAttendees,
|
|
yAxisID: 'y1',
|
|
type: 'line',
|
|
pointRadius: 5,
|
|
pointBackgroundColor: '#0663C7',
|
|
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: '#0671E0',
|
|
backgroundColor: 'transparent',
|
|
data: conversionData,
|
|
yAxisID: 'y2',
|
|
type: 'line',
|
|
pointRadius: 5,
|
|
pointBackgroundColor: '#0663C7',
|
|
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: ['#0671E0', '#FFB95F'],
|
|
hoverBackgroundColor: ['#0053AD', '#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 lang="scss">
|
|
/* Background */
|
|
.dashboard-bg {
|
|
background: var(--color-neutral-300);
|
|
}
|
|
|
|
/* Typography */
|
|
.dashboard-title {
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
color: var(--color-neutral-900);
|
|
margin: 0;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: var(--color-neutral-700);
|
|
margin: 8px 0 0 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 700;
|
|
color: var(--color-primary-600);
|
|
}
|
|
|
|
/* Export Button */
|
|
.export-btn {
|
|
text-transform: none;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
letter-spacing: 0;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(255, 155, 27, 0.2);
|
|
}
|
|
|
|
/* Date Chip */
|
|
.date-chip {
|
|
padding: 12px 16px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
border: 1px solid var(--color-neutral-500);
|
|
}
|
|
|
|
/* Stat Cards */
|
|
.stat-card {
|
|
background: var(--color-neutral-100);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
border: 2px solid transparent;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.stat-card-primary {
|
|
border-left-color: var(--color-primary-600);
|
|
}
|
|
|
|
.stat-card-secondary {
|
|
border-left-color: var(--color-secondary-600);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 36px;
|
|
font-weight: 900;
|
|
color: var(--color-neutral-900);
|
|
line-height: 1;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--color-neutral-700);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Chart Cards */
|
|
.chart-card {
|
|
background: var(--color-neutral-100);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
border: 1px solid var(--color-neutral-500);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
min-height: 480px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chart-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 0 16px 0;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--color-neutral-900);
|
|
}
|
|
|
|
.period-toggle {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.year-chip {
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
border-radius: 20px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.year-chip:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.live-chip {
|
|
font-weight: 600;
|
|
color: var(--color-neutral-100);
|
|
}
|
|
|
|
.chart-card .v-card-text {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Loading */
|
|
.loading-text {
|
|
margin-top: 16px;
|
|
color: var(--color-neutral-900);
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 960px) {
|
|
.dashboard-title {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.d-flex.justify-space-between {
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
align-items: flex-start !important;
|
|
}
|
|
|
|
.d-flex.align-center .mr-4 {
|
|
margin-right: 0 !important;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 11px;
|
|
}
|
|
}
|
|
</style> |