refactor: consolidate hourly chart components and update sales-by-hour data logic

This commit is contained in:
Yoga Pangestu 2026-04-23 19:47:36 +07:00
parent b1eb893cfe
commit a280eb8a64
4 changed files with 69 additions and 146 deletions

View File

@ -17,14 +17,14 @@ class AnalysisController extends Controller
public function __invoke() public function __invoke()
{ {
$stats = [ $stats = [
'total_sales' => $this->getStats(fn() => Order::count()), 'total_sales' => $this->getStats(fn () => Order::count()),
'total_revenue' => $this->getStats(fn() => Order::sum('total')), 'total_revenue' => $this->getStats(fn () => Order::sum('total')),
'total_expenses' => $this->getStats(fn() => Expense::sum('amount')), 'total_expenses' => $this->getStats(fn () => Expense::sum('amount')),
'cogs' => $this->getStats(fn() => Order::sum('hpp')), 'cogs' => $this->getStats(fn () => Order::sum('hpp')),
'total_discount' => $this->getStats(fn() => Order::sum('discount')), 'total_discount' => $this->getStats(fn () => Order::sum('discount')),
'total_purchases' => $this->getStats(fn() => Purchase::sum('total')), 'total_purchases' => $this->getStats(fn () => Purchase::sum('total')),
'products_sold' => $this->getStats(fn() => OrderItem::sum('qty')), 'products_sold' => $this->getStats(fn () => OrderItem::sum('qty')),
'total_customers' => $this->getStats(fn() => Order::distinct('customer_name')->count('customer_name')), 'total_customers' => $this->getStats(fn () => Order::distinct('customer_name')->count('customer_name')),
]; ];
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']); $stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
@ -114,7 +114,7 @@ public function __invoke()
->limit(5) ->limit(5)
->get(); ->get();
$ordersByHourRaw = DB::table('orders') $salesByHourRaw = DB::table('orders')
->select( ->select(
DB::raw('HOUR(created_at) as hour'), DB::raw('HOUR(created_at) as hour'),
DB::raw('COUNT(*) as total') DB::raw('COUNT(*) as total')
@ -122,11 +122,11 @@ public function __invoke()
->groupBy(DB::raw('HOUR(created_at)')) ->groupBy(DB::raw('HOUR(created_at)'))
->get(); ->get();
$ordersByHour = collect(range(0, 23))->map(function ($hour) use ($ordersByHourRaw) { $salesByHour = collect(range(0, 23))->map(function ($hour) use ($salesByHourRaw) {
$found = $ordersByHourRaw->firstWhere('hour', $hour); $found = $salesByHourRaw->firstWhere('hour', $hour);
return [ return [
'hour' => str_pad($hour, 2, '0', STR_PAD_LEFT) . ':00', 'hour' => str_pad($hour, 2, '0', STR_PAD_LEFT).':00',
'total' => $found ? (int) $found->total : 0, 'total' => $found ? (int) $found->total : 0,
]; ];
}); });
@ -134,7 +134,7 @@ public function __invoke()
return Inertia::render('analysis', [ return Inertia::render('analysis', [
'stats' => $stats, 'stats' => $stats,
'salesByMonth' => $salesByMonth, 'salesByMonth' => $salesByMonth,
'ordersByHour' => $ordersByHour, 'salesByHour' => $salesByHour,
'paymentMethods' => $paymentMethods, 'paymentMethods' => $paymentMethods,
'orderStatuses' => $orderStatuses, 'orderStatuses' => $orderStatuses,
'orderChannels' => $orderChannels, 'orderChannels' => $orderChannels,

View File

@ -1,84 +0,0 @@
"use client"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { ChartConfig } from "@/components/ui/chart";
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent
} from "@/components/ui/chart";
export const description = "Menampilkan aktivitas toko berdasarkan pesanan yang masuk per jam."
const chartConfig = {
total: {
label: "Total Pesanan",
color: "var(--chart-1)",
},
} satisfies ChartConfig
export function OrdersByHourChart({ data }: { data: any[] }) {
return (
<Card className="pt-0">
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
<div className="grid flex-1 gap-1">
<CardTitle>Jam Sibuk</CardTitle>
<CardDescription>
Menampilkan aktivitas toko berdasarkan pesanan yang masuk per jam.
</CardDescription>
</div>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillTotal" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-total)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-total)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="hour"
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="total"
type="natural"
fill="url(#fillTotal)"
stroke="var(--color-total)"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}

View File

@ -17,11 +17,9 @@ import {
ChartTooltipContent ChartTooltipContent
} from "@/components/ui/chart" } from "@/components/ui/chart"
import type {ChartConfig} from "@/components/ui/chart"; import type { ChartConfig } from "@/components/ui/chart";
export const description = "Menampilkan aktivitas toko berdasarkan pesanan yang masuk." const defaultChartConfig = {
const chartConfig = {
today: { today: {
label: "Hari Ini", label: "Hari Ini",
color: "var(--chart-1)", color: "var(--chart-1)",
@ -32,48 +30,50 @@ const chartConfig = {
}, },
} satisfies ChartConfig } satisfies ChartConfig
export function SalesByHourChart({ data }: { data: any[] }) { interface SalesByHourChartProps {
data: any[];
config?: ChartConfig;
title?: string;
description?: string;
}
export function SalesByHourChart({
data,
config = defaultChartConfig,
title = "Jam Sibuk",
description = "Menampilkan aktivitas toko berdasarkan pesanan yang masuk per jam."
}: SalesByHourChartProps) {
const keys = Object.keys(config);
return ( return (
<Card className="pt-0"> <Card className="pt-0">
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row"> <CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
<div className="grid flex-1 gap-1"> <div className="grid flex-1 gap-1">
<CardTitle>Jam Sibuk</CardTitle> <CardTitle>{title}</CardTitle>
<CardDescription> <CardDescription>{description}</CardDescription>
Menampilkan aktivitas toko berdasarkan pesanan yang masuk.
</CardDescription>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6"> <CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer <ChartContainer
config={chartConfig} config={config}
className="aspect-auto h-[250px] w-full" className="aspect-auto h-[250px] w-full"
> >
<AreaChart data={data}> <AreaChart data={data}>
<defs> <defs>
<linearGradient id="fillToday" x1="0" y1="0" x2="0" y2="1"> {keys.map((key) => (
<stop <linearGradient key={key} id={`fill${key}`} x1="0" y1="0" x2="0" y2="1">
offset="5%" <stop
stopColor="var(--color-today)" offset="5%"
stopOpacity={0.8} stopColor={`var(--color-${key})`}
/> stopOpacity={0.8}
<stop />
offset="95%" <stop
stopColor="var(--color-today)" offset="95%"
stopOpacity={0.1} stopColor={`var(--color-${key})`}
/> stopOpacity={0.1}
</linearGradient> />
<linearGradient id="fillYesterday" x1="0" y1="0" x2="0" y2="1"> </linearGradient>
<stop ))}
offset="5%"
stopColor="var(--color-yesterday)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-yesterday)"
stopOpacity={0.1}
/>
</linearGradient>
</defs> </defs>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis <XAxis
@ -86,18 +86,17 @@ export function SalesByHourChart({ data }: { data: any[] }) {
cursor={false} cursor={false}
content={<ChartTooltipContent indicator="dot" />} content={<ChartTooltipContent indicator="dot" />}
/> />
<Area {/* Render Areas in reverse order so first key in config is on top */}
dataKey="yesterday" {[...keys].reverse().map((key) => (
type="natural" <Area
fill="url(#fillYesterday)" key={key}
stroke="var(--color-yesterday)" dataKey={key}
/> type="natural"
<Area fill={`url(#fill${key})`}
dataKey="today" stroke={`var(--color-${key})`}
type="natural" isAnimationActive={true}
fill="url(#fillToday)" />
stroke="var(--color-today)" ))}
/>
<ChartLegend content={<ChartLegendContent />} /> <ChartLegend content={<ChartLegendContent />} />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>

View File

@ -4,11 +4,11 @@ import type { AnalysisPageProps } from '@/types';
import { Head, usePage } from '@inertiajs/react'; import { Head, usePage } from '@inertiajs/react';
import { CustomBarChart } from '../components/charts/bar-chart'; import { CustomBarChart } from '../components/charts/bar-chart';
import { CustomPieChart } from '../components/charts/pie-chart'; import { CustomPieChart } from '../components/charts/pie-chart';
import { OrdersByHourChart } from '../components/charts/orders-by-hour-chart'; import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart'; import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
export default function Analisa() { export default function Analisa() {
const { stats, salesByMonth, ordersByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } = const { stats, salesByMonth, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } =
usePage<AnalysisPageProps>().props; usePage<AnalysisPageProps>().props;
return ( return (
@ -51,7 +51,15 @@ export default function Analisa() {
</div> </div>
<RevenueVsPurchasesChart data={salesByMonth} /> <RevenueVsPurchasesChart data={salesByMonth} />
<OrdersByHourChart data={ordersByHour} /> <SalesByHourChart
data={salesByHour}
config={{
total: {
label: "Total Penjualan",
color: "var(--chart-1)",
},
}}
/>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<CustomPieChart <CustomPieChart