feat: add transaction volume and AOV chart to analysis dashboard

This commit is contained in:
Yoga Pangestu 2026-04-23 20:06:40 +07:00
parent 51f2e65bd2
commit 4d417fa08b
4 changed files with 146 additions and 3 deletions

View File

@ -137,12 +137,13 @@ public function __invoke()
];
});
// Revenue & Profit per month
// Revenue & Profit & Volume per month
$ordersByMonth = DB::table('orders')
->select(
DB::raw('MONTH(created_at) as month'),
DB::raw('SUM(total) as revenue'),
DB::raw('SUM(hpp) as cogs')
DB::raw('SUM(hpp) as cogs'),
DB::raw('COUNT(*) as count')
)
->groupBy(DB::raw('MONTH(created_at)'))
->get();
@ -182,10 +183,24 @@ public function __invoke()
];
});
$transactionVolume = collect(range(1, 12))->map(function ($month) use ($ordersByMonth, $monthNames) {
$found = $ordersByMonth->firstWhere('month', $month);
$count = $found ? (int) $found->count : 0;
$revenue = $found ? (float) $found->revenue : 0;
$aov = $count > 0 ? $revenue / $count : 0;
return [
'month' => $monthNames[$month - 1],
'count' => $count,
'aov' => $aov,
];
});
return Inertia::render('analysis', [
'stats' => $stats,
'salesByMonth' => $salesByMonth,
'revenueVsProfit' => $revenueVsProfit,
'transactionVolume' => $transactionVolume,
'salesByHour' => $salesByHour,
'paymentMethods' => $paymentMethods,
'orderStatuses' => $orderStatuses,

View File

@ -0,0 +1,124 @@
"use client"
import { GitCommitVertical, TrendingUp } from "lucide-react"
import { CartesianGrid, Line, LineChart, XAxis } 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 } from "@/lib/formatters"
export const description = "Menampilkan volume transaksi per bulan."
const chartConfig = {
count: {
label: "Jumlah Transaksi",
color: "var(--chart-1)",
},
aov: {
label: "AOV",
color: "var(--chart-2)",
},
} satisfies ChartConfig
export function TransactionVolumeChart({ data }: { data: any[] }) {
return (
<Card>
<CardHeader>
<CardTitle>Volume Transaksi</CardTitle>
<CardDescription>Total pesanan masuk per bulan</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)}
/>
<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;
}}
/>
}
/>
<Line
dataKey="count"
type="natural"
stroke="var(--color-count)"
strokeWidth={2}
dot={({ cx, cy, payload }) => {
if (cx == null || cy == null) {
return null
}
const r = 24
return (
<GitCommitVertical
key={payload.month}
x={cx - r / 2}
y={cy - r / 2}
width={r}
height={r}
fill="hsl(var(--background))"
stroke="var(--color-count)"
/>
)
}}
/>
</LineChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex gap-2 leading-none font-medium">
Menampilkan tren aktivitas transaksi <TrendingUp className="h-4 w-4" />
</div>
<div className="leading-none text-muted-foreground">
Berdasarkan total pesanan dari Januari - Desember
</div>
</CardFooter>
</Card>
)
}

View File

@ -5,11 +5,12 @@ import { Head, usePage } from '@inertiajs/react';
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 { 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, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } =
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } =
usePage<AnalysisPageProps>().props;
return (
@ -56,6 +57,8 @@ export default function Analisa() {
<RevenueVsProfitChart data={revenueVsProfit} />
<TransactionVolumeChart data={transactionVolume} />
<SalesByHourChart
data={salesByHour}
config={{

View File

@ -33,6 +33,7 @@ export interface AnalysisPageProps {
stats: DashboardStats;
salesByMonth: any[];
revenueVsProfit: any[];
transactionVolume: any[];
salesByHour: any[];
paymentMethods: any[];
orderStatuses: any[];