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;
|
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', [
|
return Inertia::render('dashboard', [
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
'salesByHour' => $salesByHour,
|
'salesByHour' => $salesByHour,
|
||||||
'paymentMethods' => $paymentMethods,
|
'paymentMethods' => $paymentMethods,
|
||||||
'orderStatuses' => $orderStatuses,
|
'orderStatuses' => $orderStatuses,
|
||||||
'orderChannels' => $orderChannels,
|
'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 { StatCard } from '@/components/cards/stat-card';
|
||||||
import { WelcomeCard } from '@/components/cards/welcome-card';
|
import { WelcomeCard } from '@/components/cards/welcome-card';
|
||||||
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
||||||
|
import { CustomBarChart } from '../components/charts/bar-chart';
|
||||||
import { CustomPieChart } from '../components/charts/pie-chart';
|
import { CustomPieChart } from '../components/charts/pie-chart';
|
||||||
|
|
||||||
export default function Dashboard() {
|
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());
|
const [time, setTime] = useState(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -123,6 +124,22 @@ export default function Dashboard() {
|
|||||||
colorOffset={240}
|
colorOffset={240}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user