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', [
|
return Inertia::render('analysis', [
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
'salesByMonth' => $salesByMonth,
|
'salesByMonth' => $salesByMonth,
|
||||||
@ -207,6 +218,7 @@ public function __invoke()
|
|||||||
'orderChannels' => $orderChannels,
|
'orderChannels' => $orderChannels,
|
||||||
'topProducts' => $topProducts,
|
'topProducts' => $topProducts,
|
||||||
'topCustomers' => $topCustomers,
|
'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,
|
...item,
|
||||||
fill: `var(--color-${key})`,
|
fill: `var(--color-${key})`,
|
||||||
name: String(item.name),
|
name: String(item.name),
|
||||||
|
total: Number(item.total),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -26,10 +26,6 @@ const chartConfig = {
|
|||||||
label: "Jumlah Transaksi",
|
label: "Jumlah Transaksi",
|
||||||
color: "var(--chart-1)",
|
color: "var(--chart-1)",
|
||||||
},
|
},
|
||||||
aov: {
|
|
||||||
label: "AOV",
|
|
||||||
color: "var(--chart-2)",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig
|
} satisfies ChartConfig
|
||||||
|
|
||||||
export function TransactionVolumeChart({ data }: { data: any[] }) {
|
export function TransactionVolumeChart({ data }: { data: any[] }) {
|
||||||
@ -65,29 +61,7 @@ export function TransactionVolumeChart({ data }: { data: any[] }) {
|
|||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
cursor={false}
|
cursor={false}
|
||||||
content={
|
content={<ChartTooltipContent hideLabel />}
|
||||||
<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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Line
|
<Line
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
|
|||||||
@ -6,11 +6,12 @@ import { CustomBarChart } from '../components/charts/bar-chart';
|
|||||||
import { CustomPieChart } from '../components/charts/pie-chart';
|
import { CustomPieChart } from '../components/charts/pie-chart';
|
||||||
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
|
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
|
||||||
import { TransactionVolumeChart } from '../components/charts/transaction-volume-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 { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
|
||||||
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
||||||
|
|
||||||
export default function Analisa() {
|
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;
|
usePage<AnalysisPageProps>().props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -62,18 +63,20 @@ export default function Analisa() {
|
|||||||
{/* Activity & Volume Grid */}
|
{/* Activity & Volume Grid */}
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<TransactionVolumeChart data={transactionVolume} />
|
<TransactionVolumeChart data={transactionVolume} />
|
||||||
<SalesByHourChart
|
<AovTrendChart data={transactionVolume} />
|
||||||
data={salesByHour}
|
|
||||||
config={{
|
|
||||||
total: {
|
|
||||||
label: "Total Penjualan",
|
|
||||||
color: "var(--chart-1)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</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
|
<CustomPieChart
|
||||||
title="Metode Pembayaran"
|
title="Metode Pembayaran"
|
||||||
description="Distribusi pesanan berdasarkan metode pembayaran."
|
description="Distribusi pesanan berdasarkan metode pembayaran."
|
||||||
@ -92,6 +95,12 @@ export default function Analisa() {
|
|||||||
data={orderChannels}
|
data={orderChannels}
|
||||||
colorOffset={240}
|
colorOffset={240}
|
||||||
/>
|
/>
|
||||||
|
<CustomPieChart
|
||||||
|
title="Top 5 Kategori"
|
||||||
|
description="Distribusi penjualan berdasarkan kategori produk."
|
||||||
|
data={topCategories}
|
||||||
|
colorOffset={60}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
|||||||
@ -40,5 +40,6 @@ export interface AnalysisPageProps {
|
|||||||
orderChannels: any[];
|
orderChannels: any[];
|
||||||
topProducts: any[];
|
topProducts: any[];
|
||||||
topCustomers: any[];
|
topCustomers: any[];
|
||||||
|
topCategories: any[];
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user