feat: add Top Products and Top Customers charts to the dashboard using a new BarChart component

This commit is contained in:
Yoga Pangestu 2026-04-23 13:28:02 +07:00
parent f7288db5cb
commit ed7918e858
3 changed files with 155 additions and 1 deletions

View File

@ -102,12 +102,33 @@ public function __invoke()
return $item;
});
$topProducts = DB::table('order_items')
->join('products', 'order_items.product_id', '=', 'products.id')
->join('orders', 'order_items.order_id', '=', 'orders.id')
->select('products.name as name', DB::raw('SUM(order_items.qty) as total'))
->whereDate('orders.created_at', $today)
->groupBy('products.name')
->orderByDesc('total')
->limit(5)
->get();
$topCustomers = DB::table('orders')
->select('customer_name as name', DB::raw('SUM(total) as total'))
->whereDate('created_at', $today)
->whereNotNull('customer_name')
->groupBy('customer_name')
->orderByDesc('total')
->limit(5)
->get();
return Inertia::render('dashboard', [
'stats' => $stats,
'salesByHour' => $salesByHour,
'paymentMethods' => $paymentMethods,
'orderStatuses' => $orderStatuses,
'orderChannels' => $orderChannels,
'topProducts' => $topProducts,
'topCustomers' => $topCustomers,
]);
}

View File

@ -0,0 +1,116 @@
"use client"
import React, { useMemo } from "react"
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
export const CustomBarChart = React.memo(function CustomBarChart({
title,
description,
data,
colorOffset = 0,
isCurrency = false
}: {
title: string
description: string
data: any[]
colorOffset?: number
isCurrency?: boolean
}) {
const { config, chartData } = useMemo(() => {
const cfg: ChartConfig = {
total: { label: "Total" },
}
const formattedData = data.map((item, index) => {
const key = String(item.name).toLowerCase().replace(/[^a-z0-9]/g, '_')
const hue = Math.floor((index * 137.5 + colorOffset) % 360)
cfg[key] = {
label: String(item.name),
color: `hsl(${hue}, 60%, 65%)`,
}
return {
...item,
fill: `var(--color-${key})`,
name: String(item.name),
total: Number(item.total)
}
})
return { config: cfg, chartData: formattedData }
}, [data, colorOffset])
const formatValue = (val: any) => {
if (!isCurrency) return val;
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(val);
};
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={config} className="[&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none">
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
margin={{
right: isCurrency ? 90 : 32, // More space if currency
left: 0
}}
style={{ outline: "none" }}
>
<CartesianGrid horizontal={false} vertical={false} />
<YAxis
dataKey="name"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
hide
/>
<XAxis dataKey="total" type="number" hide />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" valueFormatter={formatValue} />}
/>
<Bar dataKey="total" radius={4}>
<LabelList
dataKey="name"
position="insideLeft"
offset={8}
className="fill-white drop-shadow-md"
fontSize={12}
/>
<LabelList
dataKey="total"
position="right"
offset={8}
className="fill-foreground"
fontSize={12}
formatter={formatValue}
/>
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
)
})

View File

@ -5,10 +5,11 @@ import { DashboardPageProps } from '@/types';
import { StatCard } from '@/components/cards/stat-card';
import { WelcomeCard } from '@/components/cards/welcome-card';
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
import { CustomBarChart } from '../components/charts/bar-chart';
import { CustomPieChart } from '../components/charts/pie-chart';
export default function Dashboard() {
const { auth, stats, salesByHour, paymentMethods, orderStatuses, orderChannels } = usePage<DashboardPageProps & { salesByHour: any[], paymentMethods: any[], orderStatuses: any[], orderChannels: any[] }>().props;
const { auth, stats, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } = usePage<DashboardPageProps & { salesByHour: any[], paymentMethods: any[], orderStatuses: any[], orderChannels: any[], topProducts: any[], topCustomers: any[] }>().props;
const [time, setTime] = useState(new Date());
useEffect(() => {
@ -123,6 +124,22 @@ export default function Dashboard() {
colorOffset={240}
/>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<CustomBarChart
title="Top 5 Produk Terlaris"
description="Berdasarkan jumlah produk yang terjual."
data={topProducts}
colorOffset={45}
/>
<CustomBarChart
title="Top 5 Pelanggan Setia"
description="Berdasarkan total nominal belanja."
data={topCustomers}
colorOffset={180}
isCurrency={true}
/>
</div>
</div>
</>
);