feat: add AOV trend chart and top category distribution to analysis dashboard
This commit is contained in:
parent
1b1c374e99
commit
d182401670
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
100
resources/js/components/charts/aov-trend-chart.tsx
Normal file
100
resources/js/components/charts/aov-trend-chart.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -50,6 +50,7 @@ export const CustomPieChart = React.memo(function CustomPieChart({
|
||||
...item,
|
||||
fill: `var(--color-${key})`,
|
||||
name: String(item.name),
|
||||
total: Number(item.total),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -40,5 +40,6 @@ export interface AnalysisPageProps {
|
||||
orderChannels: any[];
|
||||
topProducts: any[];
|
||||
topCustomers: any[];
|
||||
topCategories: any[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user