feat: update Dashboard component to utilize new chart color configurations and implement tooltips for donut charts
This commit is contained in:
parent
a7a938e1ff
commit
d52f471920
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user