diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 2a8c2a9..4caf931 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -33,11 +33,78 @@ public function __invoke() $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', \App\Enums\PaymentMethod::class), + 'order_by_status' => $this->getOrderByEnum($today, 'order_status', \App\Enums\OrderStatus::class), + 'order_by_channel' => $this->getOrderByEnum($today, 'order_channel', \App\Enums\OrderChannel::class), + 'top_products' => $this->getTopProducts($today), + ]; + 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) { $todayVal = $callback($today); diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index a1e277d..4274025 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -10,8 +10,17 @@ import { 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 { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter, CardContent } 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; @@ -32,6 +41,13 @@ interface DashboardStats { 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 { @@ -276,6 +292,178 @@ export default function Dashboard() { ))} + + {/* 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} + /> + } /> + + + + + +
);