From 5b3b12e6c2337dd178df92a75daa1a4e7b621e48 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 22 Apr 2026 19:07:15 +0700 Subject: [PATCH] feat: implement dashboard statistics controller and update UI with interactive metrics cards --- app/Http/Controllers/DashboardController.php | 97 +++++++++++++ resources/js/pages/dashboard.tsx | 142 ++++++++++++++++--- routes/web.php | 4 +- 3 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/DashboardController.php diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..2a8c2a9 --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,97 @@ + $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')), + ]; + + // 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']); + + return Inertia::render('dashboard', [ + 'stats' => $stats, + ]); + } + + private function getStats($today, $yesterday, $callback) + { + $todayVal = $callback($today); + $yesterdayVal = $callback($yesterday); + + return [ + 'value' => $todayVal, + 'yesterday' => $yesterdayVal, + 'change' => $this->calculatePercentageChange($todayVal, $yesterdayVal), + ]; + } + + private function calculatePercentageChange($current, $previous) + { + if ($previous == 0) { + return $current > 0 ? 100 : 0; + } + + return round((($current - $previous) / $previous) * 100, 2); + } + + private function calculateAov($revenue, $sales) + { + $todayVal = $sales['value'] > 0 ? $revenue['value'] / $sales['value'] : 0; + $yesterdayVal = $sales['yesterday'] > 0 ? $revenue['yesterday'] / $sales['yesterday'] : 0; + + return [ + 'value' => $todayVal, + 'yesterday' => $yesterdayVal, + 'change' => $this->calculatePercentageChange($todayVal, $yesterdayVal), + ]; + } + + private function calculateDiff($a, $b) + { + $todayVal = $a['value'] - $b['value']; + $yesterdayVal = $a['yesterday'] - $b['yesterday']; + + return [ + 'value' => $todayVal, + 'yesterday' => $yesterdayVal, + 'change' => $this->calculatePercentageChange($todayVal, $yesterdayVal), + ]; + } + + private function calculateMargin($profit, $revenue) + { + $todayVal = $revenue['value'] > 0 ? ($profit['value'] / $revenue['value']) * 100 : 0; + $yesterdayVal = $revenue['yesterday'] > 0 ? ($profit['yesterday'] / $revenue['yesterday']) * 100 : 0; + + return [ + 'value' => round($todayVal, 2), + 'yesterday' => round($yesterdayVal, 2), + 'change' => $this->calculatePercentageChange($todayVal, $yesterdayVal), + ]; + } +} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 25f2205..a1e277d 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,12 +1,42 @@ 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 { Sun, Moon, Cloud, Trees, CloudRain, Stars, Bird } from 'lucide-react'; +import { + Sun, Moon, Cloud, Trees, CloudRain, 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 } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +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; +} interface PageProps { auth: Auth; + stats: DashboardStats; [key: string]: any; } @@ -49,7 +79,7 @@ const DynamicScene = ({ hour }: { hour: number }) => { }; export default function Dashboard() { - const { auth } = usePage().props; + const { auth, stats } = usePage().props; const [time, setTime] = useState(new Date()); useEffect(() => { @@ -120,10 +150,76 @@ export default function Dashboard() { year: 'numeric', }); + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); + }; + + const formatNumber = (value: number) => { + return new Intl.NumberFormat('id-ID').format(value); + }; + + const StatCard = ({ + title, + stat, + icon: Icon, + isCurrency = true, + isPercentage = false, + className + }: { + title: string; + stat: StatItem; + icon: any; + isCurrency?: boolean; + isPercentage?: boolean; + className?: string; + }) => { + const isPositive = stat.change >= 0; + + return ( + + +
+ {title} +
+ + {isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))} + + + + {isPositive ? : } + {Math.abs(stat.change)}% + + +
+ +
+ {isPositive ? ( + + Meningkat + + ) : ( + + Menurun + + )} + vs kemarin +
+
+ Kemarin: {isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))} +
+
+
+ ); + }; + return ( <> -
+
{/* Welcome Section */}
{/* Decorative Elements */} @@ -152,19 +248,33 @@ 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: "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 }, + ].map((item, index) => ( +
+ +
+ ))}
diff --git a/routes/web.php b/routes/web.php index 9ef362a..85fbc49 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,8 +7,10 @@ 'canRegister' => Features::enabled(Features::registration()), ])->name('home'); +use App\Http\Controllers\DashboardController; + Route::middleware(['auth', 'verified'])->group(function () { - Route::inertia('dashboard', 'dashboard')->name('dashboard'); + Route::get('dashboard', DashboardController::class)->name('dashboard'); }); require __DIR__.'/settings.php';