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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\OrderChannel;
|
||||||
|
use App\Enums\OrderStatus;
|
||||||
|
use App\Enums\PaymentMethod;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
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', [
|
return Inertia::render('dashboard', [
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
'salesByHour' => $salesByHour,
|
'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 { 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 { CustomPieChart } from '../components/charts/pie-chart';
|
||||||
|
|
||||||
export default function Dashboard() {
|
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());
|
const [time, setTime] = useState(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -101,6 +102,27 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SalesByHourChart data={salesByHour} />
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user