feat: add AOV trend chart and top category distribution to analysis dashboard

This commit is contained in:
Yoga Pangestu 2026-04-23 20:29:11 +07:00
parent 1b1c374e99
commit d182401670
6 changed files with 135 additions and 38 deletions

View File

@ -196,6 +196,17 @@ public function __invoke()
];
});
$topCategories = DB::table('order_items')
->join('products', 'order_items.product_id', '=', 'products.id')
->join('category_product', 'products.id', '=', 'category_product.product_id')
->join('categories', 'category_product.category_id', '=', 'categories.id')
->select('categories.name', DB::raw('SUM(order_items.qty) as total'))
->whereNull('order_items.deleted_at')
->groupBy('categories.id', 'categories.name')
->orderByDesc('total')
->limit(5)
->get();
return Inertia::render('analysis', [
'stats' => $stats,
'salesByMonth' => $salesByMonth,
@ -207,6 +218,7 @@ public function __invoke()
'orderChannels' => $orderChannels,
'topProducts' => $topProducts,
'topCustomers' => $topCustomers,
'topCategories' => $topCategories,
]);
}

View File

@ -0,0 +1,100 @@
"use client"
import { TrendingUp } from "lucide-react"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import { formatCurrency, formatShortCurrency } from "@/lib/formatters"
export const description = "Tren rata-rata nilai pesanan (AOV) per bulan."
const chartConfig = {
aov: {
label: "AOV",
color: "var(--chart-1)",
},
} satisfies ChartConfig
export function AovTrendChart({ data }: { data: any[] }) {
return (
<Card>
<CardHeader>
<CardTitle>Tren AOV</CardTitle>
<CardDescription>Rata-rata nilai belanja per pesanan</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="aspect-auto h-[300px] w-full">
<LineChart
accessibilityLayer
data={data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<YAxis
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={formatShortCurrency}
width={60}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideLabel
formatter={(value) => [
formatCurrency(Number(value)),
chartConfig.aov.label
]}
/>
}
/>
<Line
dataKey="aov"
type="natural"
stroke="var(--color-aov)"
strokeWidth={2}
dot={{
fill: "var(--color-aov)",
}}
activeDot={{
r: 6,
}}
/>
</LineChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 leading-none font-medium">
Menampilkan tren kualitas belanja pelanggan <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Berdasarkan data operasional Januari - Desember
</div>
</CardFooter>
</Card>
)
}

View File

@ -50,6 +50,7 @@ export const CustomPieChart = React.memo(function CustomPieChart({
...item,
fill: `var(--color-${key})`,
name: String(item.name),
total: Number(item.total),
}
})

View File

@ -26,10 +26,6 @@ const chartConfig = {
label: "Jumlah Transaksi",
color: "var(--chart-1)",
},
aov: {
label: "AOV",
color: "var(--chart-2)",
},
} satisfies ChartConfig
export function TransactionVolumeChart({ data }: { data: any[] }) {
@ -65,29 +61,7 @@ export function TransactionVolumeChart({ data }: { data: any[] }) {
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
formatter={(value, name, item) => {
if (name === "count") {
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-[var(--color-count)]" />
<span className="text-muted-foreground">Transaksi:</span>
<span className="font-bold">{value}</span>
</div>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-[var(--color-aov)]" />
<span className="text-muted-foreground">AOV:</span>
<span className="font-bold">{formatCurrency(item.payload.aov)}</span>
</div>
</div>
)
}
return null;
}}
/>
}
content={<ChartTooltipContent hideLabel />}
/>
<Line
dataKey="count"

View File

@ -6,11 +6,12 @@ import { CustomBarChart } from '../components/charts/bar-chart';
import { CustomPieChart } from '../components/charts/pie-chart';
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart';
import { AovTrendChart } from '../components/charts/aov-trend-chart';
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
export default function Analisa() {
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } =
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers, topCategories } =
usePage<AnalysisPageProps>().props;
return (
@ -62,18 +63,20 @@ export default function Analisa() {
{/* Activity & Volume Grid */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<TransactionVolumeChart data={transactionVolume} />
<SalesByHourChart
data={salesByHour}
config={{
total: {
label: "Total Penjualan",
color: "var(--chart-1)",
},
}}
/>
<AovTrendChart data={transactionVolume} />
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<SalesByHourChart
data={salesByHour}
config={{
total: {
label: "Total Penjualan",
color: "var(--chart-1)",
},
}}
/>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 @4xl/main:grid-cols-4">
<CustomPieChart
title="Metode Pembayaran"
description="Distribusi pesanan berdasarkan metode pembayaran."
@ -92,6 +95,12 @@ export default function Analisa() {
data={orderChannels}
colorOffset={240}
/>
<CustomPieChart
title="Top 5 Kategori"
description="Distribusi penjualan berdasarkan kategori produk."
data={topCategories}
colorOffset={60}
/>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">

View File

@ -40,5 +40,6 @@ export interface AnalysisPageProps {
orderChannels: any[];
topProducts: any[];
topCustomers: any[];
topCategories: any[];
[key: string]: any;
}