Files
web-antrean/pages/Dashboard_OLD.vue
T
2026-01-21 08:48:08 +07:00

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>