feat: add Top Products and Top Customers charts to the dashboard using a new BarChart component
This commit is contained in:
parent
f7288db5cb
commit
ed7918e858
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
116
resources/js/components/charts/bar-chart.tsx
Normal file
116
resources/js/components/charts/bar-chart.tsx
Normal 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>
|
||||
)
|
||||
})
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user