1998 lines
46 KiB
Vue
1998 lines
46 KiB
Vue
<template>
|
|
<div class="dashboard-wrapper">
|
|
<!-- Hero Header Section with Animated Background -->
|
|
<div class="dashboard-hero">
|
|
<div class="animated-bg">
|
|
<div class="gradient-orb orb-1"></div>
|
|
<div class="gradient-orb orb-2"></div>
|
|
<div class="gradient-orb orb-3"></div>
|
|
</div>
|
|
<v-container fluid class="pa-5 hero-container">
|
|
<div class="d-flex justify-space-between align-center flex-wrap">
|
|
<div class="hero-content">
|
|
<div class="welcome-badge animate-fade-in">
|
|
<v-icon size="18" class="mr-2 wave-animation">mdi-hand-wave</v-icon>
|
|
<span class="badge-text">Selamat Datang Kembali</span>
|
|
</div>
|
|
<h1 class="dashboard-title animate-slide-up">
|
|
<span class="title-gradient">Antrean</span> Dashboard
|
|
</h1>
|
|
<p v-if="user" class="dashboard-subtitle animate-slide-up">
|
|
Halo,
|
|
<span class="user-name">
|
|
{{ user.name || user.preferred_username }}
|
|
</span>
|
|
<span class="greeting-emoji">✨</span>
|
|
</p>
|
|
</div>
|
|
<div class="hero-actions animate-fade-in">
|
|
<v-menu offset-y>
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
v-bind="props"
|
|
color="white"
|
|
variant="flat"
|
|
class="export-btn glass-effect"
|
|
size="large"
|
|
>
|
|
<v-icon start icon="mdi-download-box-outline"></v-icon>
|
|
Export
|
|
<v-icon end>mdi-menu-down</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list class="export-menu">
|
|
<v-list-item
|
|
v-for="(item, index) in exportOptions"
|
|
:key="index"
|
|
@click="handleExport(item.type)"
|
|
class="export-item"
|
|
>
|
|
<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-card class="date-card glass-effect" elevation="0">
|
|
<div class="date-icon-wrapper">
|
|
<v-icon icon="mdi-calendar-today" size="20"></v-icon>
|
|
</div>
|
|
<div class="date-content">
|
|
<span class="date-label">Today</span>
|
|
<span class="date-text">{{ currentDate }}</span>
|
|
</div>
|
|
</v-card>
|
|
</div>
|
|
</div>
|
|
</v-container>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<v-container fluid class="pa-3 dashboard-bg">
|
|
|
|
<!-- Compact Stats Cards Section -->
|
|
<v-row class="mb-2 stats-row">
|
|
<v-col cols="12" sm="6" lg="3" v-for="(stat, index) in stats" :key="index">
|
|
<v-card
|
|
class="stat-card innovative-card"
|
|
:class="`stat-card-${stat.color}`"
|
|
elevation="0"
|
|
:style="{ animationDelay: `${index * 0.1}s` }"
|
|
>
|
|
<!-- Animated Background Pattern -->
|
|
<div class="card-pattern">
|
|
<div class="pattern-dot"></div>
|
|
<div class="pattern-dot"></div>
|
|
<div class="pattern-dot"></div>
|
|
</div>
|
|
|
|
<!-- Card Content -->
|
|
<div class="stat-card-content">
|
|
<div class="stat-header">
|
|
<div class="stat-icon-wrapper modern-icon" :class="`icon-${stat.color}`">
|
|
<v-icon :size="32" :color="stat.iconColor">{{ stat.icon }}</v-icon>
|
|
<div class="icon-glow"></div>
|
|
</div>
|
|
<div v-if="stat.change" class="stat-badge" :class="stat.changeType">
|
|
<v-icon size="12">{{ stat.changeIcon }}</v-icon>
|
|
<span>{{ stat.change }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-body">
|
|
<div class="stat-value-wrapper">
|
|
<div class="stat-value modern-value">{{ stat.value }}</div>
|
|
<div class="value-sparkline"></div>
|
|
</div>
|
|
<div class="stat-label modern-label">{{ stat.label }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hover Effect Gradient -->
|
|
<div class="hover-gradient"></div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="stat-progress">
|
|
<div class="progress-bar" :class="`progress-${stat.color}`"></div>
|
|
</div>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Charts Section -->
|
|
<v-row class="mb-2">
|
|
<v-col cols="12" md="6">
|
|
<v-card class="chart-card modern-card" elevation="0">
|
|
<div class="chart-header-modern">
|
|
<div class="chart-header-left">
|
|
<div class="chart-icon-wrapper chart-icon-primary">
|
|
<v-icon size="20" color="primary-600">mdi-chart-line</v-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="chart-title">Registration Trend</h3>
|
|
<p class="chart-subtitle">Tren pendaftaran harian</p>
|
|
</div>
|
|
</div>
|
|
<v-btn-toggle v-model="queueTimePeriod" mandatory variant="outlined" color="primary-600" class="period-toggle" density="compact">
|
|
<v-btn size="small" value="day">Hari</v-btn>
|
|
</v-btn-toggle>
|
|
</div>
|
|
<v-card-text class="pa-3 chart-container">
|
|
<ClientOnly>
|
|
<Bar
|
|
v-if="queueChartData.labels && queueChartData.labels.length > 0"
|
|
:data="queueChartData"
|
|
:options="queueChartOptions"
|
|
/>
|
|
<template #fallback>
|
|
<div class="chart-loading">Loading chart...</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-card class="chart-card modern-card" elevation="0">
|
|
<div class="chart-header-modern">
|
|
<div class="chart-header-left">
|
|
<div class="chart-icon-wrapper chart-icon-secondary">
|
|
<v-icon size="20" color="secondary-600">mdi-chart-bar</v-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="chart-title">Total Registrasi Perbulan</h3>
|
|
<p class="chart-subtitle">Data registrasi bulanan</p>
|
|
</div>
|
|
</div>
|
|
<div class="year-selector">
|
|
<v-chip
|
|
:color="activeYear === '2024' ? 'secondary-600' : 'neutral-300'"
|
|
:text-color="activeYear === '2024' ? 'white' : 'neutral-700'"
|
|
variant="flat"
|
|
class="year-chip mr-2"
|
|
@click="changeYear('2024')"
|
|
size="small"
|
|
>
|
|
2024
|
|
</v-chip>
|
|
<v-chip
|
|
:color="activeYear === '2025' ? 'secondary-600' : 'neutral-300'"
|
|
:text-color="activeYear === '2025' ? 'white' : 'neutral-700'"
|
|
variant="flat"
|
|
class="year-chip"
|
|
@click="changeYear('2025')"
|
|
size="small"
|
|
>
|
|
2025
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
<v-card-text class="pa-3 chart-container">
|
|
<ClientOnly>
|
|
<Bar
|
|
v-if="barData.labels && barData.labels.length > 0"
|
|
:data="barData"
|
|
:options="barOptions"
|
|
/>
|
|
<template #fallback>
|
|
<div class="chart-loading">Loading chart...</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-card class="chart-card modern-card" elevation="0">
|
|
<div class="chart-header-modern">
|
|
<div class="chart-header-left">
|
|
<div class="chart-icon-wrapper chart-icon-success">
|
|
<v-icon size="20" color="success-600">mdi-pulse</v-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="chart-title">Realtime Check-in Velocity</h3>
|
|
<p class="chart-subtitle">Kecepatan check-in realtime</p>
|
|
</div>
|
|
</div>
|
|
<v-chip size="small" color="success-600" class="live-chip" variant="flat">
|
|
<v-icon start size="small" class="pulse-icon">mdi-circle</v-icon>
|
|
Live
|
|
</v-chip>
|
|
</div>
|
|
<v-card-text class="pa-3 chart-container">
|
|
<ClientOnly>
|
|
<Line
|
|
v-if="realtimeLineData.labels && realtimeLineData.labels.length > 0"
|
|
:data="realtimeLineData"
|
|
:options="lineOptions"
|
|
/>
|
|
<template #fallback>
|
|
<div class="chart-loading">Loading chart...</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-card class="chart-card modern-card" elevation="0">
|
|
<div class="chart-header-modern">
|
|
<div class="chart-header-left">
|
|
<div class="chart-icon-wrapper chart-icon-primary">
|
|
<v-icon size="20" color="primary-600">mdi-chart-pie</v-icon>
|
|
</div>
|
|
<div>
|
|
<h3 class="chart-title">Registrant Type Breakdown</h3>
|
|
<p class="chart-subtitle">Distribusi tipe registrasi</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<v-card-text class="d-flex justify-center align-center pa-3 chart-container">
|
|
<ClientOnly>
|
|
<Pie
|
|
v-if="pieData.labels && pieData.labels.length > 0"
|
|
:data="pieData"
|
|
:options="pieOptions"
|
|
/>
|
|
<template #fallback>
|
|
<div class="chart-loading">Loading chart...</div>
|
|
</template>
|
|
</ClientOnly>
|
|
</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="secondary-600"></v-progress-circular>
|
|
<p class="loading-text">Loading dashboard...</p>
|
|
</v-overlay>
|
|
</v-container>
|
|
</div>
|
|
</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';
|
|
|
|
import {
|
|
Chart as ChartJS,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
BarElement,
|
|
CategoryScale,
|
|
LinearScale,
|
|
ArcElement,
|
|
PointElement,
|
|
LineElement,
|
|
} from 'chart.js';
|
|
|
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement, PointElement, LineElement);
|
|
|
|
dayjs.extend(weekday);
|
|
dayjs.extend(weekOfYear);
|
|
dayjs.locale('id');
|
|
|
|
definePageMeta({
|
|
middleware:['auth']
|
|
})
|
|
|
|
const user = ref(null);
|
|
const isLoading = ref(false);
|
|
const { checkAuth } = useAuth();
|
|
|
|
// Color palette from assets/scss/_colors.scss
|
|
const colors = {
|
|
primary: {
|
|
700: '#3556AE',
|
|
600: '#3A61C9',
|
|
500: '#567EE7',
|
|
400: '#7898EC',
|
|
300: '#9AB2F1',
|
|
},
|
|
secondary: {
|
|
700: '#E65A0D',
|
|
600: '#F16F29',
|
|
500: '#FF8441',
|
|
400: '#FF9D67',
|
|
300: '#FFB58D',
|
|
},
|
|
success: {
|
|
600: '#33A484',
|
|
},
|
|
neutral: {
|
|
100: '#FFFFFF',
|
|
300: '#F5F7FA',
|
|
500: '#CDD4DC',
|
|
700: '#717171',
|
|
900: '#212121',
|
|
},
|
|
};
|
|
|
|
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');
|
|
|
|
// New Data: Monthly Visit Trend (Area Chart)
|
|
const visitTrendData = ref({
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
datasets: [
|
|
{
|
|
label: 'Pasien Umum',
|
|
data: [850, 920, 880, 1050, 1120, 1080, 1200, 1150, 1080, 1190, 1250, 1300],
|
|
backgroundColor: 'rgba(86, 126, 231, 0.2)',
|
|
borderColor: colors.primary[500],
|
|
borderWidth: 3,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
pointBackgroundColor: colors.primary[600],
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
},
|
|
{
|
|
label: 'Pasien BPJS',
|
|
data: [1200, 1350, 1280, 1480, 1550, 1620, 1700, 1650, 1590, 1720, 1800, 1850],
|
|
backgroundColor: 'rgba(255, 132, 65, 0.2)',
|
|
borderColor: colors.secondary[500],
|
|
borderWidth: 3,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
pointBackgroundColor: colors.secondary[600],
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
}
|
|
]
|
|
});
|
|
|
|
// New Stats Data - Updated Metrics
|
|
const calculateStats = () => {
|
|
return [
|
|
{
|
|
label: 'Pasien Hari Ini',
|
|
value: '324',
|
|
icon: 'mdi-account-heart',
|
|
iconSize: 24,
|
|
iconColor: 'white',
|
|
color: 'primary',
|
|
change: '+18.2%',
|
|
changeType: 'positive',
|
|
changeIcon: 'mdi-trending-up'
|
|
},
|
|
{
|
|
label: 'Antrean Aktif',
|
|
value: '47',
|
|
icon: 'mdi-clock-fast',
|
|
iconSize: 24,
|
|
iconColor: 'white',
|
|
color: 'secondary',
|
|
change: '+5',
|
|
changeType: 'positive',
|
|
changeIcon: 'mdi-arrow-up'
|
|
},
|
|
{
|
|
label: 'Rata-rata Tunggu',
|
|
value: '12 min',
|
|
icon: 'mdi-timer-outline',
|
|
iconSize: 24,
|
|
iconColor: 'white',
|
|
color: 'success',
|
|
change: '-3 min',
|
|
changeType: 'positive',
|
|
changeIcon: 'mdi-trending-down'
|
|
},
|
|
{
|
|
label: 'Tingkat Kehadiran',
|
|
value: '89.5%',
|
|
icon: 'mdi-check-circle',
|
|
iconSize: 24,
|
|
iconColor: 'white',
|
|
color: 'primary',
|
|
change: '+2.3%',
|
|
changeType: 'positive',
|
|
changeIcon: 'mdi-trending-up'
|
|
}
|
|
];
|
|
};
|
|
|
|
const stats = ref(calculateStats());
|
|
|
|
// --- REALTIME CHECK-IN VELOCITY CHART - Direct data ---
|
|
const realtimeLineData = ref({
|
|
labels: ['13:00:00', '13:00:30', '13:01:00', '13:01:30', '13:02:00', '13:02:30', '13:03:00', '13:03:30', '13:04:00', '13:04:30', '13:05:00', '13:05:30', '13:06:00', '13:06:30', '13:07:00'],
|
|
datasets: [
|
|
{
|
|
label: 'Check-ins/min',
|
|
backgroundColor: 'rgba(255, 132, 65, 0.15)',
|
|
borderColor: colors.secondary[500],
|
|
data: [42, 48, 45, 52, 50, 47, 55, 53, 49, 51, 48, 54, 52, 50, 46],
|
|
fill: true,
|
|
tension: 0.4,
|
|
borderWidth: 3,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 8,
|
|
pointBackgroundColor: colors.secondary[600],
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
},
|
|
],
|
|
});
|
|
|
|
const lineOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: {
|
|
duration: 0,
|
|
easing: 'easeInOutQuart'
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 15,
|
|
font: {
|
|
size: 12,
|
|
weight: 600
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
padding: 12,
|
|
titleFont: { size: 13, weight: 600 },
|
|
bodyFont: { size: 12 },
|
|
cornerRadius: 8,
|
|
displayColors: true
|
|
},
|
|
title: { display: false }
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
suggestedMax: 80,
|
|
title: {
|
|
display: true,
|
|
text: 'Check-ins per Minute',
|
|
font: { size: 12, weight: 600 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
},
|
|
grid: {
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Time (HH:MM:SS)',
|
|
font: { size: 12, weight: 600 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
},
|
|
grid: { display: false },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
// --- 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');
|
|
|
|
// Recalculate stats after data is refreshed
|
|
stats.value = calculateStats();
|
|
|
|
// Log data for debugging
|
|
console.log('📊 Queue Chart Data:', queueChartData.value);
|
|
console.log('📊 Bar Chart Data:', barData.value);
|
|
console.log('📊 Realtime Line Data:', realtimeLineData.value);
|
|
console.log('📊 Pie Data:', pieData.value);
|
|
console.log('✅ Dashboard loaded successfully with dummy data');
|
|
}
|
|
} 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... 📝");
|
|
}
|
|
};
|
|
|
|
// Direct queue chart data - simpler approach
|
|
const queueChartData = ref({
|
|
labels: ['Min, 14/01', 'Sel, 15/01', 'Rab, 16/01', 'Kam, 17/01', 'Jum, 18/01', 'Sab, 19/01', 'Min, 20/01'],
|
|
datasets: [
|
|
{
|
|
label: 'Registrants',
|
|
backgroundColor: colors.primary[300],
|
|
borderColor: colors.primary[500],
|
|
data: [320, 450, 380, 520, 410, 490, 560],
|
|
borderRadius: 8,
|
|
borderSkipped: false,
|
|
borderWidth: 2,
|
|
},
|
|
{
|
|
label: 'Attendees',
|
|
backgroundColor: 'transparent',
|
|
borderColor: colors.secondary[500],
|
|
data: [280, 400, 340, 480, 370, 450, 520],
|
|
type: 'line',
|
|
pointRadius: 6,
|
|
pointHoverRadius: 8,
|
|
pointBackgroundColor: colors.secondary[600],
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
tension: 0.4,
|
|
borderWidth: 3,
|
|
fill: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const queueChartOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 15,
|
|
font: {
|
|
size: 12,
|
|
weight: 600
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
padding: 12,
|
|
titleFont: { size: 13, weight: 600 },
|
|
bodyFont: { size: 12 },
|
|
cornerRadius: 8,
|
|
displayColors: true
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Count',
|
|
font: { size: 12, weight: 600 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
},
|
|
grid: {
|
|
display: true,
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Monthly registration data - more realistic progression
|
|
const visitorData2024 = [
|
|
180, 220, 280, 320, 350, 420,
|
|
480, 520, 490, 450, 580, 650
|
|
];
|
|
|
|
const visitorData2025 = [
|
|
220, 280, 380, 450, 420, 580,
|
|
650, 720, 680, 620, 780, 850
|
|
];
|
|
|
|
// Conversion rate data (percentage)
|
|
const conversionRate2024 = [2.2, 2.8, 3.5, 3.9, 3.7, 4.6, 5.2, 5.5, 5.1, 4.8, 6.0, 6.8];
|
|
const conversionRate2025 = [2.8, 3.5, 4.2, 4.8, 4.5, 5.8, 6.5, 6.2, 6.0, 5.5, 7.2, 8.0];
|
|
|
|
// Direct bar chart data - simpler approach
|
|
const barData = computed(() => {
|
|
const dataForYear = activeYear.value === '2024' ? visitorData2024 : visitorData2025;
|
|
const conversionData = activeYear.value === '2024' ? conversionRate2024 : conversionRate2025;
|
|
|
|
return {
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
|
|
datasets: [
|
|
{
|
|
label: 'Total Registrants',
|
|
backgroundColor: colors.primary[300],
|
|
borderColor: colors.primary[500],
|
|
data: dataForYear,
|
|
yAxisID: 'y1',
|
|
type: 'bar',
|
|
borderRadius: 8,
|
|
borderSkipped: false,
|
|
borderWidth: 2,
|
|
},
|
|
{
|
|
label: 'Conv. Rate (%)',
|
|
borderColor: colors.secondary[500],
|
|
backgroundColor: 'transparent',
|
|
data: conversionData,
|
|
yAxisID: 'y2',
|
|
type: 'line',
|
|
pointRadius: 6,
|
|
pointHoverRadius: 8,
|
|
pointBackgroundColor: colors.secondary[600],
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
tension: 0.4,
|
|
borderWidth: 3,
|
|
fill: false,
|
|
}
|
|
],
|
|
};
|
|
});
|
|
|
|
const barOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 15,
|
|
font: {
|
|
size: 12,
|
|
weight: 600
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
padding: 12,
|
|
titleFont: { size: 13, weight: 600 },
|
|
bodyFont: { size: 12 },
|
|
cornerRadius: 8,
|
|
displayColors: true
|
|
}
|
|
},
|
|
scales: {
|
|
y1: {
|
|
position: 'left',
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Registrants',
|
|
font: { size: 12, weight: 600 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
},
|
|
grid: {
|
|
display: false,
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
},
|
|
y2: {
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
suggestedMax: 10,
|
|
title: {
|
|
display: true,
|
|
text: 'Conv. Rate (%)',
|
|
font: { size: 12, weight: 600 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
color: 'rgba(0, 0, 0, 0.6)'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Direct pie chart data - simpler approach
|
|
const pieData = ref({
|
|
labels: ['Offline Registrants', 'Online Registrants'],
|
|
datasets: [
|
|
{
|
|
label: 'Registrant Type',
|
|
backgroundColor: [
|
|
colors.secondary[500], // Orange for offline
|
|
colors.primary[400] // Blue for online
|
|
],
|
|
hoverBackgroundColor: [
|
|
colors.secondary[700],
|
|
colors.primary[600]
|
|
],
|
|
data: [1245, 2834], // ~30% offline, ~70% online
|
|
borderWidth: 3,
|
|
borderColor: colors.neutral[100],
|
|
hoverBorderWidth: 4,
|
|
},
|
|
],
|
|
});
|
|
|
|
const pieOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'bottom',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 20,
|
|
font: {
|
|
size: 12,
|
|
weight: 600
|
|
},
|
|
color: 'rgba(0, 0, 0, 0.8)'
|
|
}
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
padding: 12,
|
|
titleFont: { size: 13, weight: 600 },
|
|
bodyFont: { size: 12 },
|
|
cornerRadius: 8,
|
|
displayColors: true,
|
|
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">
|
|
@use '~/assets/scss/_colors.scss' as colorVars;
|
|
|
|
/* Dashboard Wrapper */
|
|
.dashboard-wrapper {
|
|
min-height: 100vh;
|
|
background: var(--color-neutral-300);
|
|
}
|
|
|
|
/* Hero Header Section - Compact Design */
|
|
.dashboard-hero {
|
|
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 50%, var(--color-secondary-600) 100%);
|
|
box-shadow: 0 8px 32px rgba(58, 97, 201, 0.25);
|
|
position: relative;
|
|
overflow: hidden;
|
|
min-height: 160px;
|
|
|
|
/* Animated Background Orbs */
|
|
.animated-bg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.gradient-orb {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(80px);
|
|
opacity: 0.4;
|
|
animation: float 20s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-1 {
|
|
width: 300px;
|
|
height: 300px;
|
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, transparent 70%);
|
|
top: -150px;
|
|
left: -80px;
|
|
animation-delay: 0s;
|
|
}
|
|
|
|
.orb-2 {
|
|
width: 250px;
|
|
height: 250px;
|
|
background: radial-gradient(circle, rgba(255, 132, 65, 0.3) 0%, transparent 70%);
|
|
top: 50%;
|
|
right: -100px;
|
|
animation-delay: 7s;
|
|
}
|
|
|
|
.orb-3 {
|
|
width: 200px;
|
|
height: 200px;
|
|
background: radial-gradient(circle, rgba(86, 126, 231, 0.2) 0%, transparent 70%);
|
|
bottom: -80px;
|
|
left: 30%;
|
|
animation-delay: 14s;
|
|
}
|
|
|
|
.hero-container {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% {
|
|
transform: translate(0, 0) scale(1);
|
|
}
|
|
33% {
|
|
transform: translate(30px, -30px) scale(1.1);
|
|
}
|
|
66% {
|
|
transform: translate(-20px, 20px) scale(0.9);
|
|
}
|
|
}
|
|
|
|
/* Hero Content Styling */
|
|
.welcome-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
background: rgba(255, 255, 255, 0.15);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
padding: 4px 12px;
|
|
border-radius: 50px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: white;
|
|
margin-bottom: 8px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
|
|
.wave-animation {
|
|
animation: wave 2s ease-in-out infinite;
|
|
}
|
|
}
|
|
|
|
@keyframes wave {
|
|
0%, 100% { transform: rotate(0deg); }
|
|
25% { transform: rotate(20deg); }
|
|
75% { transform: rotate(-20deg); }
|
|
}
|
|
|
|
.hero-content {
|
|
color: white;
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 32px;
|
|
font-weight: 900;
|
|
color: white;
|
|
margin: 0 0 6px 0;
|
|
line-height: 1.2;
|
|
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
|
|
|
.title-gradient {
|
|
background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: rgba(255, 255, 255, 0.95);
|
|
margin: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 700;
|
|
color: var(--color-secondary-200);
|
|
text-shadow: 0 2px 8px rgba(255, 181, 141, 0.3);
|
|
}
|
|
|
|
.greeting-emoji {
|
|
margin-left: 6px;
|
|
font-size: 18px;
|
|
display: inline-block;
|
|
animation: sparkle 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes sparkle {
|
|
0%, 100% { transform: scale(1) rotate(0deg); opacity: 1; }
|
|
50% { transform: scale(1.2) rotate(180deg); opacity: 0.8; }
|
|
}
|
|
|
|
.quick-stats {
|
|
display: none; // Hidden untuk compact mode
|
|
}
|
|
|
|
.hero-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Glass Effect Buttons */
|
|
.glass-effect {
|
|
background: rgba(255, 255, 255, 0.15) !important;
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
&:hover {
|
|
background: rgba(255, 255, 255, 0.25) !important;
|
|
transform: translateY(-4px) scale(1.02);
|
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
|
|
}
|
|
}
|
|
|
|
.export-btn {
|
|
text-transform: none;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
letter-spacing: 0.3px;
|
|
border-radius: 12px;
|
|
padding: 8px 16px !important;
|
|
color: white !important;
|
|
}
|
|
|
|
.export-menu {
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.export-item {
|
|
transition: all 0.3s ease;
|
|
|
|
&:hover {
|
|
background: linear-gradient(135deg, var(--color-primary-50) 0%, var(--color-secondary-50) 100%);
|
|
transform: translateX(8px);
|
|
}
|
|
}
|
|
|
|
.date-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 14px;
|
|
background: rgba(255, 255, 255, 0.15);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
|
|
.date-icon-wrapper {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 8px;
|
|
color: white;
|
|
}
|
|
|
|
.date-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
}
|
|
|
|
.date-label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.date-text {
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
color: white;
|
|
}
|
|
}
|
|
|
|
/* Compact Background */
|
|
.dashboard-bg {
|
|
background: var(--color-neutral-300);
|
|
padding-top: 12px !important;
|
|
padding-bottom: 12px !important;
|
|
min-height: auto;
|
|
position: relative;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-image:
|
|
radial-gradient(circle at 20% 50%, rgba(58, 97, 201, 0.02) 0%, transparent 50%),
|
|
radial-gradient(circle at 80% 80%, rgba(241, 111, 41, 0.02) 0%, transparent 50%);
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
/* Compact Stats Cards */
|
|
.stats-row {
|
|
margin-top: -40px;
|
|
position: relative;
|
|
z-index: 3;
|
|
|
|
:deep(.v-col) {
|
|
padding: 6px !important;
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.innovative-card {
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%);
|
|
backdrop-filter: blur(20px);
|
|
border-radius: 24px;
|
|
padding: 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
box-shadow:
|
|
0 10px 40px rgba(0, 0, 0, 0.08),
|
|
0 0 0 1px rgba(255, 255, 255, 0.5) inset;
|
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
position: relative;
|
|
overflow: hidden;
|
|
animation: fadeInUp 0.8s ease-out both;
|
|
|
|
&:hover {
|
|
transform: translateY(-12px) scale(1.02);
|
|
box-shadow:
|
|
0 20px 60px rgba(0, 0, 0, 0.15),
|
|
0 0 0 1px rgba(255, 255, 255, 0.8) inset;
|
|
|
|
.icon-glow {
|
|
opacity: 0.6;
|
|
transform: scale(1.5);
|
|
}
|
|
|
|
.hover-gradient {
|
|
opacity: 1;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Animated Background Pattern */
|
|
.card-pattern {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 150px;
|
|
height: 150px;
|
|
opacity: 0.05;
|
|
pointer-events: none;
|
|
|
|
.pattern-dot {
|
|
position: absolute;
|
|
width: 8px;
|
|
height: 8px;
|
|
background: currentColor;
|
|
border-radius: 50%;
|
|
animation: pulse-dot 3s ease-in-out infinite;
|
|
|
|
&:nth-child(1) {
|
|
top: 20px;
|
|
right: 30px;
|
|
animation-delay: 0s;
|
|
}
|
|
|
|
&:nth-child(2) {
|
|
top: 50px;
|
|
right: 60px;
|
|
animation-delay: 1s;
|
|
}
|
|
|
|
&:nth-child(3) {
|
|
top: 80px;
|
|
right: 20px;
|
|
animation-delay: 2s;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes pulse-dot {
|
|
0%, 100% {
|
|
transform: scale(1);
|
|
opacity: 0.5;
|
|
}
|
|
50% {
|
|
transform: scale(1.5);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* Compact Stat Card Layout */
|
|
.stat-card-content {
|
|
padding: 16px;
|
|
position: relative;
|
|
z-index: 2;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
min-height: 130px;
|
|
height: 100%;
|
|
flex: 1;
|
|
}
|
|
|
|
.stat-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.modern-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
.icon-glow {
|
|
position: absolute;
|
|
inset: -6px;
|
|
background: currentColor;
|
|
border-radius: 16px;
|
|
opacity: 0;
|
|
filter: blur(16px);
|
|
transition: all 0.4s ease;
|
|
}
|
|
|
|
:deep(.v-icon) {
|
|
font-size: 24px !important;
|
|
}
|
|
}
|
|
|
|
.stat-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding: 4px 8px;
|
|
border-radius: 50px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.3px;
|
|
|
|
&.positive {
|
|
background: var(--color-success-100);
|
|
color: var(--color-success-700);
|
|
box-shadow: 0 2px 8px rgba(51, 164, 132, 0.2);
|
|
}
|
|
}
|
|
|
|
.stat-body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.stat-value-wrapper {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.modern-value {
|
|
font-size: 28px;
|
|
font-weight: 900;
|
|
color: var(--color-neutral-900);
|
|
line-height: 1;
|
|
font-family: 'Inter', sans-serif;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.modern-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--color-neutral-700);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.icon-primary {
|
|
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-primary-600) 100%);
|
|
color: white;
|
|
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.3);
|
|
}
|
|
|
|
.icon-secondary {
|
|
background: linear-gradient(135deg, var(--color-secondary-500) 0%, var(--color-secondary-600) 100%);
|
|
color: white;
|
|
box-shadow: 0 4px 16px rgba(241, 111, 41, 0.3);
|
|
}
|
|
|
|
.icon-success {
|
|
background: linear-gradient(135deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
|
|
color: white;
|
|
box-shadow: 0 4px 16px rgba(51, 164, 132, 0.3);
|
|
}
|
|
|
|
/* Hover Gradient Effect */
|
|
.hover-gradient {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, transparent 0%, rgba(102, 126, 234, 0.05) 100%);
|
|
opacity: 0;
|
|
transition: opacity 0.4s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Animated Progress Bar */
|
|
.stat-progress {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: rgba(0, 0, 0, 0.05);
|
|
overflow: hidden;
|
|
border-radius: 0 0 24px 24px;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 100%;
|
|
width: 0;
|
|
transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
&.progress-primary {
|
|
background: linear-gradient(90deg, var(--color-primary-500) 0%, var(--color-primary-600) 100%);
|
|
}
|
|
|
|
&.progress-secondary {
|
|
background: linear-gradient(90deg, var(--color-secondary-500) 0%, var(--color-secondary-600) 100%);
|
|
}
|
|
|
|
&.progress-success {
|
|
background: linear-gradient(90deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
|
|
}
|
|
}
|
|
|
|
.stat-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
color: var(--color-neutral-900);
|
|
line-height: 1;
|
|
margin-bottom: 4px;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--color-neutral-700);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
line-height: 1.4;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-change {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
margin-top: 4px;
|
|
|
|
&.positive {
|
|
color: var(--color-success-600);
|
|
background: var(--color-success-100);
|
|
}
|
|
|
|
&.negative {
|
|
color: var(--color-danger-600);
|
|
background: var(--color-danger-100);
|
|
}
|
|
}
|
|
|
|
.stat-card-decoration {
|
|
position: absolute;
|
|
bottom: 0;
|
|
right: 0;
|
|
width: 120px;
|
|
height: 120px;
|
|
opacity: 0.1;
|
|
background: radial-gradient(circle, currentColor 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.stat-card-primary .stat-card-decoration {
|
|
color: var(--color-primary-600);
|
|
}
|
|
|
|
.stat-card-secondary .stat-card-decoration {
|
|
color: var(--color-secondary-600);
|
|
}
|
|
|
|
.stat-card-success .stat-card-decoration {
|
|
color: var(--color-success-600);
|
|
}
|
|
|
|
/* Compact Chart Cards */
|
|
.v-row:not(.stats-row) {
|
|
:deep(.v-col) {
|
|
padding: 6px !important;
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.modern-card {
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%);
|
|
backdrop-filter: blur(20px);
|
|
border-radius: 24px;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
box-shadow:
|
|
0 10px 40px rgba(0, 0, 0, 0.08),
|
|
0 0 0 1px rgba(255, 255, 255, 0.5) inset;
|
|
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
overflow: hidden;
|
|
position: relative;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: linear-gradient(90deg, var(--color-primary-500) 0%, var(--color-primary-600) 50%, var(--color-secondary-500) 100%);
|
|
opacity: 0;
|
|
transition: opacity 0.4s ease;
|
|
}
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(58, 97, 201, 0.05) 0%, transparent 70%);
|
|
opacity: 0;
|
|
transition: opacity 0.4s ease;
|
|
}
|
|
|
|
&:hover {
|
|
box-shadow:
|
|
0 20px 60px rgba(0, 0, 0, 0.15),
|
|
0 0 0 1px rgba(255, 255, 255, 0.8) inset;
|
|
border-color: rgba(255, 255, 255, 0.5);
|
|
transform: translateY(-4px);
|
|
|
|
&::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
&::after {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.chart-card {
|
|
height: 320px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chart-header-modern {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 14px 18px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.5) 0%, rgba(245, 247, 250, 0.3) 100%);
|
|
position: relative;
|
|
z-index: 1;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px;
|
|
left: 18px;
|
|
right: 18px;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, var(--color-primary-500) 0%, var(--color-primary-600) 50%, var(--color-secondary-500) 100%);
|
|
opacity: 0.5;
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
|
|
.chart-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.chart-icon-wrapper {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
position: relative;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -3px;
|
|
border-radius: 14px;
|
|
background: inherit;
|
|
opacity: 0;
|
|
filter: blur(10px);
|
|
transition: opacity 0.4s ease;
|
|
}
|
|
|
|
:deep(.v-icon) {
|
|
font-size: 18px !important;
|
|
}
|
|
}
|
|
|
|
.chart-header-modern:hover .chart-icon-wrapper {
|
|
transform: scale(1.08) rotate(5deg);
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
|
|
|
|
&::before {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.chart-icon-primary {
|
|
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-primary-600) 100%);
|
|
color: white;
|
|
}
|
|
|
|
.chart-icon-secondary {
|
|
background: linear-gradient(135deg, var(--color-secondary-500) 0%, var(--color-secondary-600) 100%);
|
|
color: white;
|
|
}
|
|
|
|
.chart-icon-success {
|
|
background: linear-gradient(135deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
|
|
color: white;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: var(--color-neutral-900);
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
|
|
.chart-subtitle {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--color-neutral-600);
|
|
margin: 3px 0 0 0;
|
|
opacity: 0.8;
|
|
letter-spacing: 0.2px;
|
|
}
|
|
|
|
.period-toggle {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
border-radius: 10px;
|
|
border: 1.5px solid var(--color-primary-200);
|
|
background: var(--color-primary-50);
|
|
overflow: hidden;
|
|
|
|
:deep(.v-btn) {
|
|
transition: all 0.3s ease;
|
|
padding: 4px 10px !important;
|
|
|
|
&.v-btn--active {
|
|
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-primary-600) 100%);
|
|
color: white;
|
|
box-shadow: 0 2px 8px rgba(58, 97, 201, 0.25);
|
|
}
|
|
}
|
|
}
|
|
|
|
.year-selector {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.year-chip {
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
border-radius: 50px;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
padding: 6px 12px;
|
|
border: 1.5px solid transparent;
|
|
|
|
&:hover {
|
|
transform: translateY(-1px) scale(1.03);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
}
|
|
|
|
.live-chip {
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
background: linear-gradient(135deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
|
|
box-shadow: 0 2px 10px rgba(51, 164, 132, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
padding: 5px 12px !important;
|
|
border-radius: 50px !important;
|
|
}
|
|
|
|
.pulse-icon {
|
|
animation: pulse-glow 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse-glow {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
|
|
}
|
|
50% {
|
|
opacity: 0.7;
|
|
transform: scale(0.9);
|
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
|
|
}
|
|
}
|
|
|
|
.chart-card .v-card-text {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--color-neutral-100);
|
|
}
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 12px;
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, rgba(245, 247, 250, 0.1) 100%);
|
|
border-radius: 0 0 24px 24px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chart-wrapper {
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
max-height: 100%;
|
|
|
|
canvas {
|
|
max-height: 100% !important;
|
|
}
|
|
}
|
|
|
|
.pie-chart {
|
|
max-width: 400px;
|
|
max-height: 400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Enhanced Chart styling */
|
|
:deep(canvas) {
|
|
border-radius: 12px;
|
|
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.05));
|
|
}
|
|
|
|
:deep(.chartjs-render-monitor) {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
/* Animate charts on hover */
|
|
.chart-card:hover :deep(canvas) {
|
|
transform: scale(1.01);
|
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
/* Enhanced chart card animations */
|
|
.chart-card {
|
|
animation: fadeInUp 0.5s ease-out;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Smooth transitions for chart interactions */
|
|
:deep(.chartjs-tooltip) {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
/* Better legend styling */
|
|
:deep(.chartjs-legend) {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.chart-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 300px;
|
|
gap: 16px;
|
|
|
|
&::before {
|
|
content: '';
|
|
width: 48px;
|
|
height: 48px;
|
|
border: 4px solid rgba(102, 126, 234, 0.1);
|
|
border-top-color: #667eea;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
&::after {
|
|
content: 'Loading chart data...';
|
|
color: var(--color-neutral-600);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Responsive chart container */
|
|
@media (max-width: 960px) {
|
|
.chart-card {
|
|
height: 280px;
|
|
}
|
|
|
|
.chart-loading {
|
|
height: 200px;
|
|
}
|
|
}
|
|
|
|
/* Loading */
|
|
.loading-text {
|
|
margin-top: 16px;
|
|
color: var(--color-neutral-900);
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* Animations for Creative Elements */
|
|
.animate-fade-in {
|
|
animation: fadeIn 0.8s ease-out;
|
|
}
|
|
|
|
.animate-slide-up {
|
|
animation: slideUp 0.8s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Compact Responsive */
|
|
@media (max-width: 960px) {
|
|
.dashboard-hero {
|
|
min-height: 140px;
|
|
|
|
.hero-container {
|
|
padding: 16px 12px !important;
|
|
}
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.hero-actions {
|
|
width: 100%;
|
|
margin-top: 12px;
|
|
|
|
.export-btn {
|
|
font-size: 12px;
|
|
padding: 6px 12px !important;
|
|
}
|
|
|
|
.date-card {
|
|
font-size: 11px;
|
|
}
|
|
}
|
|
|
|
.stats-row {
|
|
margin-top: -30px;
|
|
|
|
:deep(.v-col) {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.innovative-card {
|
|
height: 100%;
|
|
}
|
|
|
|
.stat-card-content {
|
|
padding: 14px;
|
|
min-height: 120px;
|
|
}
|
|
|
|
.modern-value {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.modern-label {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.modern-icon {
|
|
width: 42px;
|
|
height: 42px;
|
|
}
|
|
|
|
.chart-header-modern {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
padding: 12px 14px;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chart-subtitle {
|
|
font-size: 10px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.dashboard-bg {
|
|
padding: 8px !important;
|
|
}
|
|
|
|
.stats-row,
|
|
.v-row:not(.stats-row) {
|
|
:deep(.v-col) {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
.innovative-card,
|
|
.modern-card {
|
|
height: 100%;
|
|
}
|
|
|
|
.stat-card-content {
|
|
padding: 12px;
|
|
min-height: 110px;
|
|
}
|
|
|
|
.modern-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
|
|
.chart-card {
|
|
height: 260px;
|
|
}
|
|
}
|
|
</style> |