From 5f8718af69ebfcdaf51a813083eef2dc22ccbb43 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 22 Apr 2026 23:21:04 +0700 Subject: [PATCH] refactor: modularize dashboard types and streamline UI components for improved stats display --- app/Http/Controllers/DashboardController.php | 114 ++------ resources/js/pages/dashboard.tsx | 262 ++----------------- resources/js/types/dashboard.ts | 28 ++ resources/js/types/index.ts | 2 +- 4 files changed, 70 insertions(+), 336 deletions(-) create mode 100644 resources/js/types/dashboard.ts diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 37cb121..d020473 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -2,9 +2,6 @@ namespace App\Http\Controllers; -use App\Enums\OrderChannel; -use App\Enums\OrderStatus; -use App\Enums\PaymentMethod; use App\Models\Expense; use App\Models\Order; use App\Models\OrderItem; @@ -20,114 +17,49 @@ public function __invoke() $yesterday = Carbon::yesterday(); $stats = [ - 'total_penjualan' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->count()), - 'total_pendapatan' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('total')), - 'total_pengeluaran' => $this->getStats($today, $yesterday, fn ($date) => Expense::whereDate('created_at', $date)->sum('amount')), - 'hpp' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('hpp')), - 'total_diskon' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('discount')), - 'total_pembelian' => $this->getStats($today, $yesterday, fn ($date) => Purchase::whereDate('created_at', $date)->sum('total')), - 'produk_terjual' => $this->getStats($today, $yesterday, fn ($date) => OrderItem::whereHas('order', fn ($q) => $q->whereDate('created_at', $date))->sum('qty')), - 'total_pelanggan' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->distinct('customer_name')->count('customer_name')), + 'total_sales' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->count()), + 'total_revenue' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('total')), + 'total_expenses' => $this->getStats($today, $yesterday, fn ($date) => Expense::whereDate('created_at', $date)->sum('amount')), + 'cogs' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('hpp')), + 'total_discount' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('discount')), + 'total_purchases' => $this->getStats($today, $yesterday, fn ($date) => Purchase::whereDate('created_at', $date)->sum('total')), + 'products_sold' => $this->getStats($today, $yesterday, fn ($date) => OrderItem::whereHas('order', fn ($q) => $q->whereDate('created_at', $date))->sum('qty')), + 'total_customers' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->distinct('customer_name')->count('customer_name')), ]; - // Complex stats - $stats['aov'] = $this->calculateAov($stats['total_pendapatan'], $stats['total_penjualan']); - $stats['laba_kotor'] = $this->calculateDiff($stats['total_pendapatan'], $stats['hpp']); - $stats['laba_bersih'] = $this->calculateDiff($stats['laba_kotor'], $stats['total_pengeluaran']); - $stats['profit_margin'] = $this->calculateMargin($stats['laba_bersih'], $stats['total_pendapatan']); - - // Chart Data - $stats['charts'] = [ - 'jam_sibuk' => $this->getJamSibuk($today, $yesterday), - 'order_by_payment' => $this->getOrderByEnum($today, 'payment_method', PaymentMethod::class), - 'order_by_status' => $this->getOrderByEnum($today, 'order_status', OrderStatus::class), - 'order_by_channel' => $this->getOrderByEnum($today, 'order_channel', OrderChannel::class), - 'top_products' => $this->getTopProducts($today), - ]; + $stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']); + $stats['gross_profit'] = $this->calculateDiff($stats['total_revenue'], $stats['cogs']); + $stats['net_profit'] = $this->calculateDiff($stats['gross_profit'], $stats['total_expenses']); + $stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']); return Inertia::render('dashboard', [ 'stats' => $stats, ]); } - private function getJamSibuk($today, $yesterday) - { - $todayData = Order::whereDate('created_at', $today) - ->selectRaw('HOUR(created_at) as hour, count(*) as count') - ->groupBy('hour') - ->pluck('count', 'hour') - ->toArray(); - - $yesterdayData = Order::whereDate('created_at', $yesterday) - ->selectRaw('HOUR(created_at) as hour, count(*) as count') - ->groupBy('hour') - ->pluck('count', 'hour') - ->toArray(); - - $chartData = []; - for ($i = 0; $i < 24; $i++) { - $chartData[] = [ - 'hour' => sprintf('%02d:00', $i), - 'today' => $todayData[$i] ?? 0, - 'yesterday' => $yesterdayData[$i] ?? 0, - ]; - } - - return $chartData; - } - - private function getOrderByEnum($date, $column, $enumClass) - { - $data = Order::whereDate('created_at', $date) - ->selectRaw("$column, count(*) as count") - ->groupBy($column) - ->get(); - - return $data->map(function ($item) use ($column, $enumClass) { - $enumValue = $item->$column; - $label = $enumValue instanceof $enumClass ? $enumValue->label() : $enumValue; - - return [ - 'name' => $label, - 'value' => $item->count, - ]; - }); - } - - private function getTopProducts($date) - { - return OrderItem::whereHas('order', fn ($q) => $q->whereDate('created_at', $date)) - ->with('product:id,name') - ->selectRaw('product_id, sum(qty) as total_qty') - ->groupBy('product_id') - ->orderByDesc('total_qty') - ->limit(5) - ->get() - ->map(fn ($item) => [ - 'name' => $item->product->name ?? 'Unknown', - 'qty' => (int) $item->total_qty, - ]); - } - - private function getStats($today, $yesterday, $callback) + private function getStats($today, $yesterday, $callback): array { $todayVal = $callback($today); $yesterdayVal = $callback($yesterday); + $diff = $todayVal - $yesterdayVal; + $change = $this->calculatePercentageChange($todayVal, $yesterdayVal); + return [ 'value' => $todayVal, 'yesterday' => $yesterdayVal, - 'change' => $this->calculatePercentageChange($todayVal, $yesterdayVal), + 'diff' => $diff, + 'change' => $change, ]; } - private function calculatePercentageChange($current, $previous) + private function calculatePercentageChange($current, $previous): ?string { if ($previous == 0) { - return $current > 0 ? 100 : 0; + return $current == 0 ? 0 : null; } - return round((($current - $previous) / $previous) * 100, 2); + return round((($current - $previous) / abs($previous)) * 100, 2); } private function calculateAov($revenue, $sales) @@ -142,7 +74,7 @@ private function calculateAov($revenue, $sales) ]; } - private function calculateDiff($a, $b) + private function calculateDiff($a, $b): array { $todayVal = $a['value'] - $b['value']; $yesterdayVal = $a['yesterday'] - $b['yesterday']; @@ -154,7 +86,7 @@ private function calculateDiff($a, $b) ]; } - private function calculateMargin($profit, $revenue) + private function calculateMargin($profit, $revenue): array { $todayVal = $revenue['value'] > 0 ? ($profit['value'] / $revenue['value']) * 100 : 0; $yesterdayVal = $revenue['yesterday'] > 0 ? ($profit['yesterday'] / $revenue['yesterday']) * 100 : 0; diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 4274025..c3e571f 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,60 +1,16 @@ import { Head, usePage } from '@inertiajs/react'; import { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; -import { PlaceholderPattern } from '@/components/ui/placeholder-pattern'; import { dashboard } from '@/routes'; -import { Auth } from '@/types'; +import { StatItem, DashboardPageProps } from '@/types'; import { - Sun, Moon, Cloud, Trees, CloudRain, Stars, Bird, + Sun, Moon, Cloud, Trees, Stars, Bird, TrendingUp, TrendingDown, ShoppingBag, DollarSign, Wallet, Receipt, Percent, BarChart3, CreditCard, ArrowUpRight, ArrowDownRight, Package, Users, ShoppingCart } from 'lucide-react'; -import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter, CardContent } from '@/components/ui/card'; +import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { - BarChart, Bar, XAxis, YAxis, CartesianGrid, - ResponsiveContainer, AreaChart, Area, - PieChart, Pie, Cell, LabelList, LineChart, Line, Sector, Label -} from 'recharts'; -import { - ChartContainer, ChartTooltip, ChartTooltipContent, - ChartLegend, ChartLegendContent, type ChartConfig -} from '@/components/ui/chart'; - -interface StatItem { - value: number; - yesterday: number; - change: number; -} - -interface DashboardStats { - total_penjualan: StatItem; - total_pendapatan: StatItem; - total_pengeluaran: StatItem; - hpp: StatItem; - aov: StatItem; - profit_margin: StatItem; - total_diskon: StatItem; - laba_kotor: StatItem; - laba_bersih: StatItem; - total_pembelian: StatItem; - produk_terjual: StatItem; - total_pelanggan: StatItem; - charts: { - jam_sibuk: Array<{ hour: string; today: number; yesterday: number }>; - order_by_payment: Array<{ name: string; value: number }>; - order_by_status: Array<{ name: string; value: number }>; - order_by_channel: Array<{ name: string; value: number }>; - top_products: Array<{ name: string; qty: number }>; - }; -} - -interface PageProps { - auth: Auth; - stats: DashboardStats; - [key: string]: any; -} const DynamicScene = ({ hour }: { hour: number }) => { if (hour >= 5 && hour < 11) { @@ -95,7 +51,7 @@ const DynamicScene = ({ hour }: { hour: number }) => { }; export default function Dashboard() { - const { auth, stats } = usePage().props; + const { auth, stats } = usePage().props; const [time, setTime] = useState(new Date()); useEffect(() => { @@ -205,25 +161,15 @@ export default function Dashboard() { {isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))} - + {isPositive ? : } - {Math.abs(stat.change)}% + {stat.change === null + ? '∞' + : `${Math.abs(stat.change)}%`} -
- {isPositive ? ( - - Meningkat - - ) : ( - - Menurun - - )} - vs kemarin -
Kemarin: {isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))}
@@ -267,18 +213,18 @@ export default function Dashboard() { {/* Stats Grid */}
{[ - { title: "Total Penjualan", stat: stats.total_penjualan, icon: ShoppingBag, isCurrency: false }, - { title: "Total Pendapatan", stat: stats.total_pendapatan, icon: DollarSign }, - { title: "Total Pengeluaran", stat: stats.total_pengeluaran, icon: Wallet }, - { title: "HPP (Modal)", stat: stats.hpp, icon: Receipt }, + { title: "Total Penjualan", stat: stats.total_sales, icon: ShoppingBag, isCurrency: false }, + { title: "Total Pendapatan", stat: stats.total_revenue, icon: DollarSign }, + { title: "Total Pengeluaran", stat: stats.total_expenses, icon: Wallet }, + { title: "HPP (Modal)", stat: stats.cogs, icon: Receipt }, { title: "AOV (Avg Order Value)", stat: stats.aov, icon: CreditCard }, { title: "Profit Margin", stat: stats.profit_margin, icon: Percent, isPercentage: true }, - { title: "Total Diskon", stat: stats.total_diskon, icon: Percent }, - { title: "Laba Kotor", stat: stats.laba_kotor, icon: BarChart3 }, - { title: "Laba Bersih", stat: stats.laba_bersih, icon: TrendingUp }, - { title: "Total Pembelian", stat: stats.total_pembelian, icon: ShoppingCart }, - { title: "Produk Terjual", stat: stats.produk_terjual, icon: Package, isCurrency: false }, - { title: "Total Pelanggan", stat: stats.total_pelanggan, icon: Users, isCurrency: false }, + { title: "Total Diskon", stat: stats.total_discount, icon: Percent }, + { title: "Laba Kotor", stat: stats.gross_profit, icon: BarChart3 }, + { title: "Laba Bersih", stat: stats.net_profit, icon: TrendingUp }, + { title: "Total Pembelian", stat: stats.total_purchases, icon: ShoppingCart }, + { title: "Produk Terjual", stat: stats.products_sold, icon: Package, isCurrency: false }, + { title: "Total Pelanggan", stat: stats.total_customers, icon: Users, isCurrency: false }, ].map((item, index) => (
))}
- - {/* Charts Section */} -
- {/* Jam Sibuk - Multiple Line Chart Pattern */} - - - Jam Sibuk (Pesanan per Jam) - Perbandingan aktivitas hari ini vs kemarin - - - - - - - } /> - - - } /> - - - - -
- - Menampilkan data pesanan per jam untuk mengidentifikasi waktu operasional tersibuk. -
-
-
- - {/* Order Distribution - Pie Chart Label List Pattern */} - - - Metode Pembayaran - Distribusi transaksi hari ini - - - [ - item.name.toLowerCase().replace(/[^a-z0-h]/g, ''), - { label: item.name, color: `var(--chart-${(i % 5) + 1})` } - ]))} - className="mx-auto aspect-square max-h-[300px] [&_.recharts-text]:fill-foreground" - > - - } /> - ({ - ...item, - fill: `var(--color-${item.name.toLowerCase().replace(/[^a-z0-h]/g, '')})` - }))} - dataKey="value" - nameKey="name" - > - - - - - - -
- Distribusi pembayaran real-time -
-
-
- - {/* Order Channel - Donut Active Pattern */} - - - Saluran Pesanan - Sumber pesanan masuk - - - [ - item.name.toLowerCase().replace(/[^a-z0-h]/g, ''), - { label: item.name, color: `var(--chart-${((i + 2) % 5) + 1})` } - ]))} - className="mx-auto aspect-square max-h-[300px]" - > - - } /> - ({ - ...item, - fill: `var(--color-${item.name.toLowerCase().replace(/[^a-z0-h]/g, '')})` - }))} - dataKey="value" - nameKey="name" - innerRadius={60} - strokeWidth={5} - activeShape={({ outerRadius = 0, ...props }: any) => ( - - )} - /> - - - - -
- Performa saluran penjualan -
-
-
- - {/* Top Products - Multiple Bar Pattern (Adapted) */} - - - Top 5 Produk Terlaris - Berdasarkan kuantitas yang terjual hari ini - - - - - - value.length > 15 ? value.slice(0, 15) + '...' : value} - /> - } /> - - - - - -
); diff --git a/resources/js/types/dashboard.ts b/resources/js/types/dashboard.ts new file mode 100644 index 0000000..9c1428d --- /dev/null +++ b/resources/js/types/dashboard.ts @@ -0,0 +1,28 @@ +import { Auth } from './auth'; + +export interface StatItem { + value: number; + yesterday: number; + change: number; +} + +export interface DashboardStats { + total_sales: StatItem; + total_revenue: StatItem; + total_expenses: StatItem; + cogs: StatItem; + aov: StatItem; + profit_margin: StatItem; + total_discount: StatItem; + gross_profit: StatItem; + net_profit: StatItem; + total_purchases: StatItem; + products_sold: StatItem; + total_customers: StatItem; +} + +export interface DashboardPageProps { + auth: Auth; + stats: DashboardStats; + [key: string]: any; +} diff --git a/resources/js/types/index.ts b/resources/js/types/index.ts index 2e23a54..6f249fb 100644 --- a/resources/js/types/index.ts +++ b/resources/js/types/index.ts @@ -9,4 +9,4 @@ export type * from './purchase'; export type * from './general-setting'; export type * from './activity'; export type * from './system-log'; - +export type * from './dashboard';