feat: add order distribution charts for payment methods, status, and channels to dashboard
This commit is contained in:
parent
7ee44c8fd9
commit
f7288db5cb
@ -2,6 +2,9 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PaymentMethod;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
@ -63,9 +66,48 @@ public function __invoke()
|
||||
];
|
||||
});
|
||||
|
||||
$paymentMethods = DB::table('orders')
|
||||
->select('payment_method as name', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('payment_method')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
if ($item->name) {
|
||||
$item->name = PaymentMethod::tryFrom($item->name)?->label() ?? $item->name;
|
||||
}
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
$orderStatuses = DB::table('orders')
|
||||
->select('order_status as name', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('order_status')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
if ($item->name) {
|
||||
$item->name = OrderStatus::tryFrom($item->name)?->label() ?? $item->name;
|
||||
}
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
$orderChannels = DB::table('orders')
|
||||
->select('order_channel as name', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('order_channel')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
if ($item->name) {
|
||||
$item->name = OrderChannel::tryFrom($item->name)?->label() ?? $item->name;
|
||||
}
|
||||
|
||||
return $item;
|
||||
});
|
||||
|
||||
return Inertia::render('dashboard', [
|
||||
'stats' => $stats,
|
||||
'salesByHour' => $salesByHour,
|
||||
'paymentMethods' => $paymentMethods,
|
||||
'orderStatuses' => $orderStatuses,
|
||||
'orderChannels' => $orderChannels,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
80
resources/js/components/charts/pie-chart.tsx
Normal file
80
resources/js/components/charts/pie-chart.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import { Pie, PieChart } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/components/ui/chart"
|
||||
|
||||
export const CustomPieChart = React.memo(function CustomPieChart({
|
||||
title,
|
||||
description,
|
||||
data,
|
||||
colorOffset = 0,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
data: any[]
|
||||
colorOffset?: number
|
||||
}) {
|
||||
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, '_')
|
||||
|
||||
// Generate distinct HSL color based on index and chart-level offset
|
||||
// 137.5 is the golden angle, which distributes colors nicely
|
||||
const hue = Math.floor((index * 137.5 + colorOffset) % 360)
|
||||
|
||||
cfg[key] = {
|
||||
label: String(item.name),
|
||||
color: `hsl(${hue}, 70%, 50%)`,
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
fill: `var(--color-${key})`,
|
||||
name: String(item.name),
|
||||
}
|
||||
})
|
||||
return { config: cfg, chartData: formattedData }
|
||||
}, [data, colorOffset])
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer
|
||||
config={config}
|
||||
className="mx-auto aspect-square max-h-[250px] pb-0 [&_.recharts-pie-label-text]:fill-foreground [&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none"
|
||||
>
|
||||
<PieChart style={{ outline: "none" }}>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="total"
|
||||
nameKey="name"
|
||||
label
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
@ -5,9 +5,10 @@ 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 { CustomPieChart } from '../components/charts/pie-chart';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { auth, stats, salesByHour } = usePage<DashboardPageProps & { salesByHour: any[] }>().props;
|
||||
const { auth, stats, salesByHour, paymentMethods, orderStatuses, orderChannels } = usePage<DashboardPageProps & { salesByHour: any[], paymentMethods: any[], orderStatuses: any[], orderChannels: any[] }>().props;
|
||||
const [time, setTime] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
@ -101,6 +102,27 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
<SalesByHourChart data={salesByHour} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<CustomPieChart
|
||||
title="Metode Pembayaran"
|
||||
description="Distribusi pesanan berdasarkan metode pembayaran."
|
||||
data={paymentMethods}
|
||||
colorOffset={0}
|
||||
/>
|
||||
<CustomPieChart
|
||||
title="Status Pesanan"
|
||||
description="Distribusi pesanan berdasarkan status."
|
||||
data={orderStatuses}
|
||||
colorOffset={120}
|
||||
/>
|
||||
<CustomPieChart
|
||||
title="Channel Pesanan"
|
||||
description="Distribusi pesanan berdasarkan channel/platform."
|
||||
data={orderChannels}
|
||||
colorOffset={240}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user