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()
{
$stats = [
'total_sales' => $this->getStats(fn() => Order::count()),
'total_revenue' => $this->getStats(fn() => Order::sum('total')),
'total_expenses' => $this->getStats(fn() => Expense::sum('amount')),
'cogs' => $this->getStats(fn() => Order::sum('hpp')),
'total_discount' => $this->getStats(fn() => Order::sum('discount')),
'total_purchases' => $this->getStats(fn() => Purchase::sum('total')),
'products_sold' => $this->getStats(fn() => OrderItem::sum('qty')),
'total_customers' => $this->getStats(fn() => Order::distinct('customer_name')->count('customer_name')),
'total_sales' => $this->getStats(fn () => Order::count()),
'total_revenue' => $this->getStats(fn () => Order::sum('total')),
'total_expenses' => $this->getStats(fn () => Expense::sum('amount')),
'cogs' => $this->getStats(fn () => Order::sum('hpp')),
'total_discount' => $this->getStats(fn () => Order::sum('discount')),
'total_purchases' => $this->getStats(fn () => Purchase::sum('total')),
'products_sold' => $this->getStats(fn () => OrderItem::sum('qty')),
'total_customers' => $this->getStats(fn () => Order::distinct('customer_name')->count('customer_name')),
];
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
@ -114,7 +114,7 @@ public function __invoke()
->limit(5)
->get();
$ordersByHourRaw = DB::table('orders')
$salesByHourRaw = DB::table('orders')
->select(
DB::raw('HOUR(created_at) as hour'),
DB::raw('COUNT(*) as total')
@ -122,11 +122,11 @@ public function __invoke()
->groupBy(DB::raw('HOUR(created_at)'))
->get();
$ordersByHour = collect(range(0, 23))->map(function ($hour) use ($ordersByHourRaw) {
$found = $ordersByHourRaw->firstWhere('hour', $hour);
$salesByHour = collect(range(0, 23))->map(function ($hour) use ($salesByHourRaw) {
$found = $salesByHourRaw->firstWhere('hour', $hour);
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,
];
});
@ -134,7 +134,7 @@ public function __invoke()
return Inertia::render('analysis', [
'stats' => $stats,
'salesByMonth' => $salesByMonth,
'ordersByHour' => $ordersByHour,
'salesByHour' => $salesByHour,
'paymentMethods' => $paymentMethods,
'orderStatuses' => $orderStatuses,
'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
} 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 chartConfig = {
const defaultChartConfig = {
today: {
label: "Hari Ini",
color: "var(--chart-1)",
@ -32,48 +30,50 @@ const 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 (
<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.
</CardDescription>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</div>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
config={config}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillToday" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-today)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-today)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillYesterday" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-yesterday)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-yesterday)"
stopOpacity={0.1}
/>
</linearGradient>
{keys.map((key) => (
<linearGradient key={key} id={`fill${key}`} x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
<CartesianGrid vertical={false} />
<XAxis
@ -86,18 +86,17 @@ export function SalesByHourChart({ data }: { data: any[] }) {
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="yesterday"
type="natural"
fill="url(#fillYesterday)"
stroke="var(--color-yesterday)"
/>
<Area
dataKey="today"
type="natural"
fill="url(#fillToday)"
stroke="var(--color-today)"
/>
{/* Render Areas in reverse order so first key in config is on top */}
{[...keys].reverse().map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill${key})`}
stroke={`var(--color-${key})`}
isAnimationActive={true}
/>
))}
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>

View File

@ -4,11 +4,11 @@ import type { AnalysisPageProps } from '@/types';
import { Head, usePage } from '@inertiajs/react';
import { CustomBarChart } from '../components/charts/bar-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';
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;
return (
@ -51,7 +51,15 @@ export default function Analisa() {
</div>
<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">
<CustomPieChart