feat: update Dashboard component to utilize new chart color configurations and implement tooltips for donut charts

This commit is contained in:
Yoga Pangestu 2026-07-01 19:16:13 +07:00
parent a7a938e1ff
commit d52f471920
2 changed files with 308 additions and 59 deletions

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { Banknote, Package, Percent, ShoppingCart, TrendingUp, UserCheck } from '@lucide/vue';
import { VisAxis, VisGroupedBar, VisXYContainer } from '@unovis/vue';
import { GroupedBar } from '@unovis/ts';
import { VisAxis, VisGroupedBar, VisTooltip, VisXYContainer } from '@unovis/vue';
import { computed, ref, watch } from 'vue';
import StatCard from '@/components/card/StatCard.vue';
import { Button } from '@/components/ui/button';
@ -171,15 +172,15 @@ type MonthlyRevenueData = { month: string; total: number; net: number; deduction
const revenueChartConfig = {
total: {
label: 'Total',
color: 'var(--chart-1)',
color: '#60a5fa',
},
net: {
label: 'Bersih',
color: 'var(--chart-2)',
color: '#22c55e',
},
deduction: {
label: 'Potongan',
color: 'var(--chart-3)',
color: '#f97316',
},
} satisfies ChartConfig;
@ -197,19 +198,19 @@ type MonthlyExpenseData = { month: string; total: number; purchase: number; expe
const expenseChartConfig = {
total: {
label: 'Total',
color: 'var(--chart-1)',
color: '#60a5fa',
},
purchase: {
label: 'Belanja',
color: 'var(--chart-2)',
color: '#f97316',
},
expense: {
label: 'Pengeluaran',
color: 'var(--chart-3)',
color: '#a855f7',
},
advance: {
label: 'Kasbon',
color: 'var(--chart-4)',
color: '#ef4444',
},
} satisfies ChartConfig;
@ -224,9 +225,11 @@ const expenseTotals = computed(() => ({
const visibleExpenseCharts = computed(() => {
const charts = ['total', 'purchase', 'expense', 'advance'] as const;
if (hasRole('admin-toko')) {
return charts.filter((c) => c !== 'purchase');
}
return charts;
});
@ -236,7 +239,7 @@ type BusyHourData = { hour: string; orders: number };
const busyHoursChartConfig = {
orders: {
label: 'Pesanan',
color: 'var(--chart-1)',
color: '#60a5fa',
},
} satisfies ChartConfig;
@ -256,24 +259,105 @@ type ProductData = { name: string; qty: number };
const supplierChartConfig = {
amount: {
label: 'Total Pembelian',
color: 'var(--chart-1)',
color: '#60a5fa',
},
} satisfies ChartConfig;
const customerChartConfig = {
amount: {
label: 'Total Pesanan',
color: 'var(--chart-2)',
color: '#22c55e',
},
} satisfies ChartConfig;
const productChartConfig = {
qty: {
label: 'Jumlah Terjual',
color: 'var(--chart-3)',
color: '#a855f7',
},
} satisfies ChartConfig;
// Tooltip trigger for bar charts
const barSelector = GroupedBar.selectors.bar;
function revenueTooltip(d: any) {
const item = d as MonthlyRevenueData;
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2 mt-0!">
<p style="margin:0;font-weight:500">${item.month}</p>
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig[activeRevenueChart.value].color}"></span>
<span class="text-muted-foreground">${revenueChartConfig[activeRevenueChart.value].label}</span>
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item[activeRevenueChart.value])}</span>
</p>
</div>`;
}
function expenseTooltip(d: any) {
const item = d as MonthlyExpenseData;
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;font-weight:500">${item.month}</p>
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${expenseChartConfig[activeExpenseChart.value].color}"></span>
<span class="text-muted-foreground">${expenseChartConfig[activeExpenseChart.value].label}</span>
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item[activeExpenseChart.value])}</span>
</p>
</div>`;
}
function busyHoursTooltip(d: any) {
const item = d as BusyHourData;
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;font-weight:500">${item.hour}</p>
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${busyHoursChartConfig.orders.color}"></span>
<span class="text-muted-foreground">Pesanan</span>
<span class="ml-auto font-medium tabular-nums text-foreground">${item.orders}</span>
</p>
</div>`;
}
function supplierTooltip(d: any) {
const item = d as SupplierData;
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;font-weight:500">${item.name}</p>
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${supplierChartConfig.amount.color}"></span>
<span class="text-muted-foreground">Total Pembelian</span>
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.amount)}</span>
</p>
</div>`;
}
function customerTooltip(d: any) {
const item = d as CustomerData;
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;font-weight:500">${item.name}</p>
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${customerChartConfig.amount.color}"></span>
<span class="text-muted-foreground">Total Pesanan</span>
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.amount)}</span>
</p>
</div>`;
}
function productTooltip(d: any) {
const item = d as ProductData;
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;font-weight:500">${item.name}</p>
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${productChartConfig.qty.color}"></span>
<span class="text-muted-foreground">Jumlah Terjual</span>
<span class="ml-auto font-medium tabular-nums text-foreground">${item.qty}</span>
</p>
</div>`;
}
const supplierBarData = computed<SupplierData[]>(() => {
return props.topSuppliers.map((s) => ({
name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name,
@ -363,8 +447,9 @@ watch([startDate, endDate], () => {
<!-- Kehadiran & Kas Toko -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard v-if="can('analysis.attendance') && isManager" title="Kehadiran" :icon="UserCheck" main-label="Total Karyawan"
:main-value="attendance.total_employees" :sub-label="attendance.percentage + '% hadir'" :items="[
<StatCard v-if="can('analysis.attendance') && isManager" title="Kehadiran" :icon="UserCheck"
main-label="Total Karyawan" :main-value="attendance.total_employees"
:sub-label="attendance.percentage + '% hadir'" :items="[
{
label: 'Hadir',
value: attendance.present,
@ -379,8 +464,9 @@ watch([startDate, endDate], () => {
},
]" />
<StatCard v-if="can('analysis.attendance') && !isManager && myAttendance" title="Kehadiran Saya" :icon="UserCheck" main-label="Hari Kerja"
:main-value="myAttendance.total_days" :sub-label="myAttendance.percentage + '% hadir'" :items="[
<StatCard v-if="can('analysis.attendance') && !isManager && myAttendance" title="Kehadiran Saya"
:icon="UserCheck" main-label="Hari Kerja" :main-value="myAttendance.total_days"
:sub-label="myAttendance.percentage + '% hadir'" :items="[
{
label: 'Hadir',
value: myAttendance.present_days,
@ -408,8 +494,8 @@ watch([startDate, endDate], () => {
},
]" :cols="2" />
<StatCard v-if="can('analysis.raw_materials')" title="Bahan Baku" :icon="Package" main-label="Total Stok"
:main-value="rawMaterialStock.total_stock.toLocaleString('id-ID')"
<StatCard v-if="can('analysis.raw_materials')" title="Bahan Baku" :icon="Package"
main-label="Total Stok" :main-value="rawMaterialStock.total_stock.toLocaleString('id-ID')"
:sub-label="'Rp' + formatRupiah(rawMaterialStock.total_value)" :items="[
{
label: 'Yard',
@ -425,8 +511,8 @@ watch([startDate, endDate], () => {
},
]" />
<StatCard v-if="can('analysis.product_stock')" title="Stok Produk" :icon="ShoppingCart" main-label="Total Stok"
:main-value="productStock.total_stock.toLocaleString('id-ID')"
<StatCard v-if="can('analysis.product_stock')" title="Stok Produk" :icon="ShoppingCart"
main-label="Total Stok" :main-value="productStock.total_stock.toLocaleString('id-ID')"
:sub-label="'Rp' + formatRupiah(productStock.total_value) + (productStock.total_reject > 0 ? ' (' + productStock.total_reject + ' reject)' : '')"
:items="[
{
@ -478,6 +564,7 @@ watch([startDate, endDate], () => {
:tick-values="monthlyRevenue.map((_, i) => i)" />
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
<VisTooltip :triggers="{ [barSelector]: revenueTooltip }" class-name="custom-tooltip" />
</VisXYContainer>
</ChartContainer>
<div v-else class="flex h-[300px] items-center justify-center text-muted-foreground">
@ -520,6 +607,7 @@ watch([startDate, endDate], () => {
:tick-values="monthlyExpense.map((_, i) => i)" />
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
<VisTooltip :triggers="{ [barSelector]: expenseTooltip }" class-name="custom-tooltip" />
</VisXYContainer>
</ChartContainer>
<div v-else class="flex h-[300px] items-center justify-center text-muted-foreground">
@ -555,6 +643,7 @@ watch([startDate, endDate], () => {
:tick-values="busyHours.map((_, i) => i)" />
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
:tick-format="(d: number) => Math.round(d).toString()" />
<VisTooltip :triggers="{ [barSelector]: busyHoursTooltip }" class-name="custom-tooltip" />
</VisXYContainer>
</ChartContainer>
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
@ -565,8 +654,8 @@ watch([startDate, endDate], () => {
<!-- Profit Metrics Cards -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<StatCard v-if="can('analysis.profit_orders')" title="Total Order" :icon="ShoppingCart" main-label="Pesanan Selesai"
:main-value="profitMetrics.total_orders.toLocaleString('id-ID')"
<StatCard v-if="can('analysis.profit_orders')" title="Total Order" :icon="ShoppingCart"
main-label="Pesanan Selesai" :main-value="profitMetrics.total_orders.toLocaleString('id-ID')"
:sub-label="'AOV: Rp' + formatRupiah(profitMetrics.aov)" :items="[
{
label: 'Produk Terjual',
@ -578,7 +667,8 @@ watch([startDate, endDate], () => {
},
]" />
<StatCard v-if="can('analysis.profit_gross') || can('analysis.profit_hpp')" title="Laba Kotor" :icon="TrendingUp" main-label="Gross Profit"
<StatCard v-if="can('analysis.profit_gross') || can('analysis.profit_hpp')" title="Laba Kotor"
:icon="TrendingUp" main-label="Gross Profit"
:main-value="'Rp' + formatRupiah(profitMetrics.laba_kotor)"
:sub-label="profitMetrics.laba_kotor >= 0 ? 'Positif' : 'Negatif'" :items="[
{
@ -625,6 +715,8 @@ watch([startDate, endDate], () => {
:tick-values="supplierBarData.map((_, i) => i)" />
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false"
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
<VisTooltip :triggers="{ [barSelector]: supplierTooltip }"
class-name="custom-tooltip" />
</VisXYContainer>
</ChartContainer>
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
@ -651,6 +743,8 @@ watch([startDate, endDate], () => {
:tick-values="customerBarData.map((_, i) => i)" />
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false"
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
<VisTooltip :triggers="{ [barSelector]: customerTooltip }"
class-name="custom-tooltip" />
</VisXYContainer>
</ChartContainer>
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
@ -675,6 +769,7 @@ watch([startDate, endDate], () => {
:tick-format="(d: number) => productBarData[d]?.name ?? ''"
:tick-values="productBarData.map((_, i) => i)" />
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" />
<VisTooltip :triggers="{ [barSelector]: productTooltip }" class-name="custom-tooltip" />
</VisXYContainer>
</ChartContainer>
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
@ -686,3 +781,21 @@ watch([startDate, endDate], () => {
</div>
</AdminLayout>
</template>
<style>
:root {
--vis-tooltip-background-color: transparent !important;
--vis-tooltip-border-color: transparent !important;
--vis-tooltip-padding: 0 !important;
--vis-tooltip-box-shadow: none !important;
--vis-tooltip-border-radius: 0 !important;
}
.custom-tooltip {
background: transparent !important;
border: none !important;
padding: 0 !important;
box-shadow: none !important;
border-radius: 0 !important;
}
</style>

View File

@ -10,7 +10,8 @@ import {
TrendingUp,
UserCheck,
} from '@lucide/vue';
import { VisDonut, VisSingleContainer } from '@unovis/vue';
import { Donut } from '@unovis/ts';
import { VisDonut, VisSingleContainer, VisTooltip } from '@unovis/vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import AttendanceCard from '@/components/card/AttendanceCard.vue';
import StatCard from '@/components/card/StatCard.vue';
@ -25,8 +26,8 @@ import { ChartContainer } from '@/components/ui/chart';
import { useCan } from '@/composables/useCan';
import AdminLayout from '@/layouts/AdminLayout.vue';
import { formatRupiah } from '@/lib/rupiah';
import type { TodayAttendance } from '@/types/attendance';
import AttendanceWebcamModal from '@/pages/admin/hr/attendances/form/AttendanceWebcamModal.vue';
import type { TodayAttendance } from '@/types/attendance';
interface DashboardProps {
attendance: {
@ -152,8 +153,16 @@ const greeting = computed(() => {
return { text: 'Selamat Malam', icon: Moon };
});
// Chart configs
const channelColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
// Chart configs - each segment uses a different color family for clarity
const allChartColors = [
'#60a5fa', // biru
'#f97316', // oranye
'#22c55e', // hijau
'#a855f7', // ungu
'#ef4444', // merah
];
const channelColors = allChartColors;
const channelChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_channel.forEach((item, index) => {
@ -166,7 +175,7 @@ const channelChartConfig = computed(() => {
return config;
});
const paymentColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
const paymentColors = allChartColors;
const paymentChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_payment_type.forEach((item, index) => {
@ -179,7 +188,7 @@ const paymentChartConfig = computed(() => {
return config;
});
const statusColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
const statusColors = allChartColors;
const statusChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_status.forEach((item, index) => {
@ -192,7 +201,7 @@ const statusChartConfig = computed(() => {
return config;
});
const marketingColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
const marketingColors = allChartColors;
const marketingChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_marketing.forEach((item, index) => {
@ -205,37 +214,131 @@ const marketingChartConfig = computed(() => {
return config;
});
// Tooltip triggers for donut charts
const donutSegmentSelector = Donut.selectors.segment;
function channelTooltip(arc: any) {
const d = arc.data as typeof props.orderStats.by_channel[number];
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${channelChartConfig.value[d.channel]?.color ?? channelColors[0]}"></span>
<span class="text-muted-foreground">${d.label}</span>
<span class="ml-auto font-medium tabular-nums text-foreground">${d.count.toLocaleString('id-ID')}</span>
</p>
</div>`;
}
function paymentTooltip(arc: any) {
const d = arc.data as typeof props.orderStats.by_payment_type[number];
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${paymentChartConfig.value[d.payment_type]?.color ?? paymentColors[0]}"></span>
<span class="text-muted-foreground">${d.label}</span>
<span class="ml-auto font-medium tabular-nums text-foreground">${d.count.toLocaleString('id-ID')}</span>
</p>
</div>`;
}
function marketingTooltip(arc: any) {
const d = arc.data as typeof props.orderStats.by_marketing[number];
const idx = props.orderStats.by_marketing.indexOf(d);
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${marketingColors[idx % marketingColors.length]}"></span>
<span class="text-muted-foreground">${d.name}</span>
<span class="ml-auto font-medium tabular-nums text-foreground">${d.count.toLocaleString('id-ID')}</span>
</p>
</div>`;
}
function statusTooltip(arc: any) {
const d = arc.data as typeof props.orderStats.by_status[number];
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
<span class="size-2.5 rounded-full" style="background-color: ${statusChartConfig.value[d.status]?.color ?? statusColors[0]}"></span>
<span class="text-muted-foreground">${d.label}</span>
<span class="ml-auto font-medium tabular-nums text-foreground">${d.count.toLocaleString('id-ID')}</span>
</p>
</div>`;
}
const visibleStatsCount = computed(() => {
let count = 0;
if (can('dashboard.attendance')) count++;
if (can('dashboard.cash')) count++;
if (can('dashboard.revenue')) count++;
if (can('dashboard.expense')) count++;
if (can('dashboard.attendance')) {
count++;
}
if (can('dashboard.cash')) {
count++;
}
if (can('dashboard.revenue')) {
count++;
}
if (can('dashboard.expense')) {
count++;
}
return count;
});
const statsGridClass = computed(() => {
const count = visibleStatsCount.value;
if (count === 1) return 'grid gap-4 grid-cols-1 md:max-w-md';
if (count === 2) return 'grid gap-4 grid-cols-1 md:grid-cols-2';
if (count === 3) return 'grid gap-4 grid-cols-1 md:grid-cols-3';
if (count === 1) {
return 'grid gap-4 grid-cols-1 md:max-w-md';
}
if (count === 2) {
return 'grid gap-4 grid-cols-1 md:grid-cols-2';
}
if (count === 3) {
return 'grid gap-4 grid-cols-1 md:grid-cols-3';
}
return 'grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4';
});
const visibleChartsCount = computed(() => {
let count = 0;
if (can('dashboard.orders_channel')) count++;
if (can('dashboard.orders_payment')) count++;
if (can('dashboard.orders_marketing')) count++;
if (can('dashboard.orders_status')) count++;
if (can('dashboard.orders_channel')) {
count++;
}
if (can('dashboard.orders_payment')) {
count++;
}
if (can('dashboard.orders_marketing')) {
count++;
}
if (can('dashboard.orders_status')) {
count++;
}
return count;
});
const chartsGridClass = computed(() => {
const count = visibleChartsCount.value;
if (count === 1) return 'grid gap-4 grid-cols-1 md:max-w-xl';
if (count === 2) return 'grid gap-4 grid-cols-1 md:grid-cols-2';
if (count === 3) return 'grid gap-4 grid-cols-1 md:grid-cols-3';
if (count === 1) {
return 'grid gap-4 grid-cols-1 md:max-w-xl';
}
if (count === 2) {
return 'grid gap-4 grid-cols-1 md:grid-cols-2';
}
if (count === 3) {
return 'grid gap-4 grid-cols-1 md:grid-cols-3';
}
return 'grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4';
});
</script>
@ -277,13 +380,8 @@ const chartsGridClass = computed(() => {
</CardContent>
</Card>
<AttendanceCard
:today-attendance="todayAttendance"
:is-on-leave="isOnLeave"
:can-check-in="canCheckIn"
@check-in="openCheckInModal"
@check-out="openCheckOutModal"
/>
<AttendanceCard :today-attendance="todayAttendance" :is-on-leave="isOnLeave" :can-check-in="canCheckIn"
@check-in="openCheckInModal" @check-out="openCheckOutModal" />
<div v-if="visibleStatsCount > 0" :class="statsGridClass">
<StatCard v-if="can('dashboard.attendance')" title="Kehadiran" :icon="UserCheck"
@ -323,7 +421,7 @@ const chartsGridClass = computed(() => {
label: 'Bersih',
value: 'Rp' + formatRupiah(
revenueSummary.total_revenue -
revenueSummary.total_deduction,
revenueSummary.total_deduction,
),
},
{
@ -360,11 +458,14 @@ const chartsGridClass = computed(() => {
</CardHeader>
<CardContent>
<div v-if="orderStats.by_channel.length > 0">
<ChartContainer :config="channelChartConfig" class="mx-auto aspect-square max-h-[200px]">
<ChartContainer :config="channelChartConfig"
class="mx-auto aspect-square max-h-[200px] chart-channel">
<VisSingleContainer :data="orderStats.by_channel" :margin="{ top: 10, bottom: 10 }">
<VisDonut :value="(d: (typeof orderStats.by_channel)[number]) => d.count"
:color="(d: (typeof orderStats.by_channel)[number]) => channelChartConfig[d.channel]?.color ?? 'var(--chart-1)'"
:color="(d: (typeof orderStats.by_channel)[number]) => channelChartConfig[d.channel]?.color ?? channelColors[0]"
:arc-width="30" />
<VisTooltip :triggers="{ [donutSegmentSelector]: channelTooltip }"
class-name="custom-tooltip" />
</VisSingleContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
@ -389,12 +490,15 @@ const chartsGridClass = computed(() => {
</CardHeader>
<CardContent>
<div v-if="orderStats.by_payment_type.length > 0">
<ChartContainer :config="paymentChartConfig" class="mx-auto aspect-square max-h-[200px]">
<ChartContainer :config="paymentChartConfig"
class="mx-auto aspect-square max-h-[200px] chart-payment">
<VisSingleContainer :data="orderStats.by_payment_type"
:margin="{ top: 10, bottom: 10 }">
<VisDonut :value="(d: (typeof orderStats.by_payment_type)[number]) => d.count"
:color="(d: (typeof orderStats.by_payment_type)[number]) => paymentChartConfig[d.payment_type]?.color ?? 'var(--chart-1)'"
:color="(d: (typeof orderStats.by_payment_type)[number]) => paymentChartConfig[d.payment_type]?.color ?? paymentColors[0]"
:arc-width="30" />
<VisTooltip :triggers="{ [donutSegmentSelector]: paymentTooltip }"
class-name="custom-tooltip" />
</VisSingleContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
@ -419,11 +523,14 @@ const chartsGridClass = computed(() => {
</CardHeader>
<CardContent>
<div v-if="orderStats.by_marketing.length > 0">
<ChartContainer :config="marketingChartConfig" class="mx-auto aspect-square max-h-[200px]">
<ChartContainer :config="marketingChartConfig"
class="mx-auto aspect-square max-h-[200px] chart-marketing">
<VisSingleContainer :data="orderStats.by_marketing" :margin="{ top: 10, bottom: 10 }">
<VisDonut :value="(d: (typeof orderStats.by_marketing)[number]) => d.count"
:color="(_d: (typeof orderStats.by_marketing)[number], i: number) => marketingColors[i % marketingColors.length]"
:arc-width="30" />
<VisTooltip :triggers="{ [donutSegmentSelector]: marketingTooltip }"
class-name="custom-tooltip" />
</VisSingleContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
@ -448,11 +555,14 @@ const chartsGridClass = computed(() => {
</CardHeader>
<CardContent>
<div v-if="orderStats.by_status.length > 0">
<ChartContainer :config="statusChartConfig" class="mx-auto aspect-square max-h-[200px]">
<ChartContainer :config="statusChartConfig"
class="mx-auto aspect-square max-h-[200px] chart-status">
<VisSingleContainer :data="orderStats.by_status" :margin="{ top: 10, bottom: 10 }">
<VisDonut :value="(d: (typeof orderStats.by_status)[number]) => d.count"
:color="(d: (typeof orderStats.by_status)[number]) => statusChartConfig[d.status]?.color ?? 'var(--chart-1)'"
:color="(d: (typeof orderStats.by_status)[number]) => statusChartConfig[d.status]?.color ?? statusColors[0]"
:arc-width="30" />
<VisTooltip :triggers="{ [donutSegmentSelector]: statusTooltip }"
class-name="custom-tooltip" />
</VisSingleContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
@ -476,3 +586,29 @@ const chartsGridClass = computed(() => {
<AttendanceWebcamModal v-if="canCheckIn" v-model:open="webcamModalOpen" :mode="webcamMode" />
</AdminLayout>
</template>
<style>
:root {
--vis-tooltip-background-color: transparent !important;
--vis-tooltip-border-color: transparent !important;
--vis-tooltip-padding: 0 !important;
--vis-tooltip-box-shadow: none !important;
--vis-tooltip-border-radius: 0 !important;
}
.custom-tooltip {
background: transparent !important;
border: none !important;
padding: 0 !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.chart-channel,
.chart-payment,
.chart-marketing,
.chart-status {
--vis-donut-segment-stroke-width: 2;
--vis-donut-segment-stroke-color: #374151;
}
</style>