From 0cce4ee73de74954d98d91c74474ea866852f784 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 9 Aug 2026 16:30:16 +0700 Subject: [PATCH] feat: enhance StatCard component with description prop and update chart tooltips for better localization - Added optional description prop to StatCard for additional context. - Updated ChartTooltipContent to format numbers according to Indonesian locale. - Refactored revenue and expense charts in the Analysis page to use new chart configurations and improved tooltip content. - Introduced DashboardPieChart component for displaying order statistics by channel, payment type, marketing, and status. - Adjusted revenue summary calculations in the Dashboard page to reflect changes in data structure. --- app/Http/Controllers/AnalysisController.php | 4 +- app/Services/AnalysisService.php | 284 ++++++---- app/Services/DashboardService.php | 16 +- chart.tsx | 221 ++++++++ resources/js/components/card/stat-card.tsx | 4 +- resources/js/components/ui/chart.tsx | 6 +- resources/js/pages/admin/analysis/index.tsx | 588 ++++++++++++++------ resources/js/pages/dashboard.tsx | 7 +- 8 files changed, 861 insertions(+), 269 deletions(-) create mode 100644 chart.tsx diff --git a/app/Http/Controllers/AnalysisController.php b/app/Http/Controllers/AnalysisController.php index cbb1fdd..365b60e 100644 --- a/app/Http/Controllers/AnalysisController.php +++ b/app/Http/Controllers/AnalysisController.php @@ -27,7 +27,7 @@ public function index(Request $request): Response $attendance = $this->service->getAttendanceStats($startDate, $endDate); $myAttendance = $this->service->getMyAttendance($user, $startDate, $endDate); - $cashOverview = $this->service->getCashOverview(); + $cashOverview = $this->service->getCashOverview($startDate, $endDate); $rawMaterialStock = $this->service->getRawMaterialStock(); $productStock = $this->service->getProductStock(); $revenueSummary = $this->service->getRevenueSummary($startDate, $endDate); @@ -42,6 +42,7 @@ public function index(Request $request): Response $topCustomers = $this->service->getTopCustomers($startDate, $endDate); $topProducts = $this->service->getTopProducts($startDate, $endDate); $marketingSales = $this->service->getMarketingSales($startDate, $endDate); + $orderStats = $this->service->getOrderStats($startDate, $endDate); return Inertia::render('admin/analysis/index', [ 'filters' => [ @@ -66,6 +67,7 @@ public function index(Request $request): Response 'topCustomers' => $topCustomers, 'topProducts' => $topProducts, 'marketingSales' => $marketingSales, + 'orderStats' => $orderStats, ]); } } diff --git a/app/Services/AnalysisService.php b/app/Services/AnalysisService.php index 8914951..3e99a0f 100644 --- a/app/Services/AnalysisService.php +++ b/app/Services/AnalysisService.php @@ -2,7 +2,10 @@ namespace App\Services; +use App\Enums\CashTransactionType; +use App\Enums\OrderChannel; use App\Enums\OrderStatus; +use App\Enums\PaymentType; use App\Enums\PriceType; use App\Enums\RawMaterialUnit; use App\Models\Attendance; @@ -14,7 +17,9 @@ use App\Models\Order; use App\Models\ProductVariant; use App\Models\Purchase; +use App\Models\PurchaseItem; use App\Models\RawMaterialPrice; +use App\Models\RestockItem; use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Facades\DB; @@ -26,40 +31,42 @@ public function getAttendanceStats(?string $startDate, ?string $endDate): array $start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth(); $end = $endDate ? Carbon::parse($endDate) : Carbon::now(); - $workingDays = 0; - $current = $start->copy(); + $employees = Employee::whereHas('user', fn ($q) => $q + ->where('is_active', true) + ->whereHas('roles', fn ($r) => $r + ->whereHas('permissions', fn ($p) => $p + ->where('name', 'attendances.create') + ) + ) + ) + ->where('join_date', '<=', $end) + ->where(function ($q) use ($start) { + $q->whereNull('resign_date')->orWhere('resign_date', '>=', $start); + }) + ->get(); - while ($current->lte($end)) { - if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) { - $workingDays++; - } - $current->addDay(); - } + $employeeIds = $employees->pluck('id'); + $employeeCount = $employeeIds->count(); - $totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count(); + $present = Attendance::whereIn('employee_id', $employeeIds) + ->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()]) + ->distinct('employee_id') + ->count('employee_id'); - $present = Attendance::whereBetween('attendance_date', [$start, $end])->count(); - - $leaveDays = LeaveRequest::approved() + $onLeave = LeaveRequest::approved() ->where('start_date', '<=', $end) ->where('end_date', '>=', $start) - ->get() - ->reduce(function ($carry, $leave) use ($start, $end) { - $leaveStart = max($leave->start_date->timestamp, $start->timestamp); - $leaveEnd = min($leave->end_date->timestamp, $end->timestamp); - $days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1; + ->whereIn('employee_id', $employeeIds) + ->count(); - return $carry + max(0, $days); - }, 0); - - $absent = max(0, $workingDays - $present - $leaveDays); + $absent = max(0, $employeeCount - $present - $onLeave); return [ - 'total_employees' => $totalEmployees, + 'total_employees' => $employeeCount, 'present' => $present, 'absent' => $absent, - 'on_leave' => $leaveDays, - 'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0, + 'on_leave' => $onLeave, + 'percentage' => $employeeCount > 0 ? round(($present / $employeeCount) * 100) : 0, ]; } @@ -112,7 +119,7 @@ public function getMyAttendance(User $user, ?string $startDate, ?string $endDate ]; } - public function getCashOverview(): array + public function getCashOverview(?string $startDate, ?string $endDate): array { $cashAccount = CashAccount::first(); @@ -126,32 +133,37 @@ public function getCashOverview(): array } $transactions = $cashAccount->cashTransactions(); + $this->applyDateFilter($transactions, $startDate, $endDate, 'cash_transactions.created_at'); return [ 'total_balance' => $cashAccount->balance, 'total_transactions' => (clone $transactions)->count(), - 'total_deposit' => (int) (clone $transactions)->where('type', 'deposit')->sum('amount'), - 'total_withdrawal' => (int) (clone $transactions)->where('type', 'withdrawal')->sum('amount'), + 'total_deposit' => (int) (clone $transactions)->where('type', CashTransactionType::DEPOSIT)->sum('amount'), + 'total_withdrawal' => (int) (clone $transactions)->where('type', CashTransactionType::WITHDRAWAL)->sum('amount'), ]; } public function getRawMaterialStock(): array { - $prices = RawMaterialPrice::select('stock', 'price', 'raw_material_id') - ->with('rawMaterial:id,unit') + $query = PurchaseItem::query() + ->join('purchases', 'purchase_items.purchase_id', '=', 'purchases.id') + ->leftJoin('raw_material_prices', 'purchase_items.raw_material_price_id', '=', 'raw_material_prices.id') + ->leftJoin('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id'); + + $items = $query->select('purchase_items.*', 'raw_materials.unit') ->get(); - $totalStock = $prices->sum('stock'); - $totalValue = $prices->sum(fn ($p) => $p->stock * $p->price); + $totalQty = $items->sum('quantity'); + $totalValue = $items->sum('subtotal'); $byUnit = [ - 'yard' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'), - 'meter' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'), - 'kilogram' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'), + 'yard' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::YARD)->sum('quantity'), + 'meter' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::METER)->sum('quantity'), + 'kilogram' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::KG)->sum('quantity'), ]; return [ - 'total_stock' => $totalStock, + 'total_stock' => $totalQty, 'total_value' => $totalValue, 'by_unit' => $byUnit, ]; @@ -159,53 +171,45 @@ public function getRawMaterialStock(): array public function getProductStock(): array { - $variants = ProductVariant::select('id', 'product_id', 'stock', 'reject_stock', 'retail_stock') - ->with(['product:id,name', 'productPrices' => fn ($q) => $q->where('type', PriceType::CAPITAL)]) + $query = RestockItem::query() + ->join('restocks', 'restock_items.restock_id', '=', 'restocks.id'); + + $items = $query->select('restock_items.*', 'restocks.stock_type') ->get(); - $totalStock = $variants->sum('stock'); - $totalReject = $variants->sum('reject_stock'); - $totalRetail = $variants->sum('retail_stock'); + $totalQty = $items->sum('quantity'); + $totalValue = $items->sum('subtotal'); - $totalValue = $variants->sum(function ($v) { - $capitalPrice = $v->productPrices->first()?->price ?? 0; - - return $v->stock * $capitalPrice; - }); + $byType = $items->groupBy(fn ($i) => $i->stock_type ?? 'unknown') + ->map(fn ($group) => $group->sum('quantity')) + ->toArray(); return [ - 'total_stock' => $totalStock, - 'total_reject' => $totalReject, - 'total_retail' => $totalRetail, + 'total_stock' => $totalQty, 'total_value' => $totalValue, - 'total_products' => ProductVariant::distinct('product_id')->count('product_id'), - 'total_variants' => ProductVariant::count(), - 'total_categories' => DB::table('product_categories')->distinct('category_id')->count('category_id'), + 'by_type' => $byType, ]; } public function getRevenueSummary(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); $stats = (clone $query) ->selectRaw('COUNT(*) as total_orders') ->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(discount), 0) as total_discount') - ->selectRaw('COALESCE(SUM(discount), 0) as total_deduction') - ->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net') - ->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse") - ->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail") + ->selectRaw('COALESCE(SUM(nego_price), 0) as total_deduction') + ->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs') ->first(); return [ 'total_revenue' => (int) $stats->total_revenue, 'total_discount' => (int) $stats->total_discount, 'total_deduction' => (int) $stats->total_deduction, - 'net' => (int) $stats->net, - 'net_warehouse' => (int) $stats->net_warehouse, - 'net_retail' => (int) $stats->net_retail, + 'cogs' => (int) $stats->total_cogs, + 'net' => (int) $stats->total_revenue - (int) $stats->total_cogs, 'total_orders' => (int) $stats->total_orders, 'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0, ]; @@ -214,19 +218,26 @@ public function getRevenueSummary(?string $startDate, ?string $endDate): array public function getMonthlyRevenue(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as total') - ->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net') - ->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse") - ->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail") - ->selectRaw('COALESCE(SUM(discount), 0) as deduction') + ->selectRaw('COALESCE(SUM(total_amount) - SUM(cogs), 0) as net') + ->selectRaw('COALESCE(SUM(discount), 0) as discount') + ->selectRaw('COALESCE(SUM(nego_price), 0) as deduction') + ->selectRaw('COALESCE(SUM(cogs), 0) as cogs') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')")) ->get() - ->map(fn ($item) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction'])); + ->map(fn ($item) => [ + 'month' => $item->month, + 'total' => (int) $item->total, + 'net' => (int) $item->net, + 'discount' => (int) $item->discount, + 'deduction' => (int) $item->deduction, + 'cogs' => (int) $item->cogs, + ]); return $monthly->values()->toArray(); } @@ -234,17 +245,22 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate): array public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") - ->selectRaw("SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END) as store") - ->selectRaw("SUM(CASE WHEN channel = 'shopee' THEN total_amount ELSE 0 END) as shopee") - ->selectRaw("SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END) as tiktok") + ->selectRaw("COALESCE(SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END), 0) as store") + ->selectRaw("COALESCE(SUM(CASE WHEN channel = 'shopee' THEN total_amount ELSE 0 END), 0) as shopee") + ->selectRaw("COALESCE(SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END), 0) as tiktok") ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')")) ->get() - ->map(fn ($item) => $item->only(['month', 'store', 'shopee', 'tiktok'])); + ->map(fn ($item) => [ + 'month' => $item->month, + 'store' => (int) $item->store, + 'shopee' => (int) $item->shopee, + 'tiktok' => (int) $item->tiktok, + ]); return $monthly->values()->toArray(); } @@ -252,7 +268,7 @@ public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate) public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); $data = (clone $query) ->select('payment_type') @@ -271,30 +287,30 @@ public function getRevenueByPaymentType(?string $startDate, ?string $endDate): a public function getExpenseSummary(?string $startDate, ?string $endDate): array { $expenseQuery = Expense::query(); - $this->applyDateFilter($expenseQuery, $startDate, $endDate); + $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); $advanceQuery = EmployeeAdvance::where('status', 'paid'); - $this->applyDateFilter($advanceQuery, $startDate, $endDate); + $this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at'); $purchaseQuery = Purchase::query(); - $this->applyDateFilter($purchaseQuery, $startDate, $endDate); + $this->applyDateFilter($purchaseQuery, $startDate, $endDate, 'purchases.created_at'); $expenseTotal = (clone $expenseQuery)->sum('amount'); $advanceTotal = (clone $advanceQuery)->sum('amount'); $purchaseTotal = (clone $purchaseQuery)->sum('total'); return [ - 'total' => $expenseTotal + $advanceTotal + $purchaseTotal, - 'purchase_total' => $purchaseTotal, - 'expense_total' => $expenseTotal, - 'advance_total' => $advanceTotal, + 'total' => (int) ($expenseTotal + $advanceTotal + $purchaseTotal), + 'purchase_total' => (int) $purchaseTotal, + 'expense_total' => (int) $expenseTotal, + 'advance_total' => (int) $advanceTotal, ]; } public function getMonthlyExpense(?string $startDate, ?string $endDate): array { $expenseMonthly = Expense::query(); - $this->applyDateFilter($expenseMonthly, $startDate, $endDate); + $this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at'); $expenseByMonth = (clone $expenseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") @@ -304,7 +320,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array ->keyBy('month'); $purchaseMonthly = Purchase::query(); - $this->applyDateFilter($purchaseMonthly, $startDate, $endDate); + $this->applyDateFilter($purchaseMonthly, $startDate, $endDate, 'purchases.created_at'); $purchaseByMonth = (clone $purchaseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") @@ -314,7 +330,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array ->keyBy('month'); $advanceMonthly = EmployeeAdvance::where('status', 'paid'); - $this->applyDateFilter($advanceMonthly, $startDate, $endDate); + $this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at'); $advanceByMonth = (clone $advanceMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") @@ -345,7 +361,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array public function getBusyHours(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); $hours = range(0, 23); $hourCounts = (clone $query) @@ -366,7 +382,7 @@ public function getBusyHours(?string $startDate, ?string $endDate): array public function getProfitMetrics(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); $stats = (clone $query) ->selectRaw('COUNT(*) as total_orders') @@ -399,7 +415,7 @@ public function getProfitMetrics(?string $startDate, ?string $endDate): array public function getTopSuppliers(?string $startDate, ?string $endDate): array { $query = Purchase::query(); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'purchases.created_at'); return (clone $query)->toBase() ->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id') @@ -416,7 +432,7 @@ public function getTopSuppliers(?string $startDate, ?string $endDate): array public function getTopCustomers(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id'); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); return (clone $query)->toBase() ->join('customers', 'orders.customer_id', '=', 'customers.id') @@ -433,7 +449,7 @@ public function getTopCustomers(?string $startDate, ?string $endDate): array public function getTopProducts(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); return (clone $query)->toBase() ->join('order_items', 'orders.id', '=', 'order_items.order_id') @@ -453,23 +469,101 @@ public function getMarketingSales(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED) ->whereNotNull('orders.marketing_id'); - $this->applyDateFilter($query, $startDate, $endDate); + $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); - return (clone $query)->toBase() + $orders = (clone $query) ->join('users', 'orders.marketing_id', '=', 'users.id') ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') - ->leftJoin('order_items', 'orders.id', '=', 'order_items.order_id') - ->select('user_profiles.full_name as marketing_name') + ->select('orders.marketing_id', 'user_profiles.full_name as marketing_name') ->selectRaw('COUNT(DISTINCT orders.id) as total_orders') - ->selectRaw('COALESCE(SUM(order_items.quantity), 0) as total_products_sold') ->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(orders.subtotal), 0) as total_subtotal') ->selectRaw('COALESCE(SUM(orders.discount), 0) as total_discount') - ->selectRaw('ROUND(COALESCE(SUM(orders.total_amount), 0) / COUNT(DISTINCT orders.id)) as avg_order') - ->groupBy('user_profiles.full_name') - ->orderByDesc('total_revenue') + ->groupBy('orders.marketing_id', 'user_profiles.full_name') + ->get(); + + $productCounts = (clone $query) + ->join('order_items', 'orders.id', '=', 'order_items.order_id') + ->selectRaw('orders.marketing_id, SUM(order_items.quantity) as total_qty') + ->groupBy('orders.marketing_id') + ->pluck('total_qty', 'marketing_id'); + + return $orders->map(function ($item) use ($productCounts) { + $totalOrders = (int) $item->total_orders; + $totalRevenue = (int) $item->total_revenue; + + return [ + 'marketing_name' => $item->marketing_name, + 'total_orders' => $totalOrders, + 'total_products_sold' => (int) ($productCounts[$item->marketing_id] ?? 0), + 'total_revenue' => $totalRevenue, + 'total_subtotal' => (int) $item->total_subtotal, + 'total_discount' => (int) $item->total_discount, + 'avg_order' => $totalOrders > 0 ? (int) ($totalRevenue / $totalOrders) : 0, + ]; + })->toArray(); + } + + public function getOrderStats(?string $startDate, ?string $endDate): array + { + $baseQuery = Order::query(); + $this->applyDateFilter($baseQuery, $startDate, $endDate, 'orders.created_at'); + + $byChannel = collect(OrderChannel::values())->map(function ($channel) use ($baseQuery) { + $count = (clone $baseQuery)->where('channel', $channel)->count(); + $label = OrderChannel::from($channel)->label(); + + return [ + 'channel' => $channel, + 'label' => $label, + 'count' => $count, + 'total' => (int) (clone $baseQuery)->where('channel', $channel)->sum('total_amount'), + ]; + }); + + $byPaymentType = collect(PaymentType::values())->map(function ($paymentType) use ($baseQuery) { + $count = (clone $baseQuery)->where('payment_type', $paymentType)->count(); + $label = PaymentType::from($paymentType)->label(); + + return [ + 'payment_type' => $paymentType, + 'label' => $label, + 'count' => $count, + 'total' => (int) (clone $baseQuery)->where('payment_type', $paymentType)->sum('total_amount'), + ]; + }); + + $byMarketing = (clone $baseQuery) + ->whereNotNull('marketing_id') + ->select('marketing_id') + ->selectRaw('COUNT(*) as count') + ->selectRaw('COALESCE(SUM(total_amount), 0) as total') + ->groupBy('marketing_id') + ->with('marketing:id') ->get() - ->toArray(); + ->map(fn($item) => [ + 'name' => $item->marketing?->userProfile->full_name ?? '-', + 'count' => $item->count, + 'total' => (int) $item->total, + ]); + + $byStatus = collect(OrderStatus::values())->map(function ($status) use ($baseQuery) { + $count = (clone $baseQuery)->where('status', $status)->count(); + $label = OrderStatus::from($status)->label(); + + return [ + 'status' => $status, + 'label' => $label, + 'count' => $count, + ]; + }); + + return [ + 'by_channel' => $byChannel, + 'by_payment_type' => $byPaymentType, + 'by_marketing' => $byMarketing, + 'by_status' => $byStatus, + ]; } private function applyDateFilter($query, ?string $startDate, ?string $endDate, string $dateColumn = 'created_at'): void diff --git a/app/Services/DashboardService.php b/app/Services/DashboardService.php index 9409792..a9e857b 100644 --- a/app/Services/DashboardService.php +++ b/app/Services/DashboardService.php @@ -90,26 +90,16 @@ public function getRevenueSummary(): array ->selectRaw('COUNT(*) as total_orders') ->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(discount), 0) as total_discount') + ->selectRaw('COALESCE(SUM(nego_price), 0) as total_deduction') ->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs') ->first(); - $marketplaceFees = (clone $baseQuery) - ->whereNotNull('marketplace_settings_snapshot') - ->selectRaw("COALESCE(SUM(JSON_EXTRACT(marketplace_settings_snapshot, '$.total_fee_amount')), 0) as total") - ->first() - ->total; - - $negoDiff = (clone $baseQuery) - ->whereNotNull('nego_price') - ->selectRaw('COALESCE(SUM(nego_price), 0) as total') - ->first() - ->total; - return [ 'total_revenue' => (int) $stats->total_revenue, 'total_discount' => (int) $stats->total_discount, 'total_cogs' => (int) $stats->total_cogs, - 'total_deduction' => (int) $marketplaceFees + (int) $negoDiff, + 'total_deduction' => (int) $stats->total_deduction, + 'net' => (int) $stats->total_revenue - (int) $stats->total_cogs, 'total_orders' => (int) $stats->total_orders, ]; } diff --git a/chart.tsx b/chart.tsx new file mode 100644 index 0000000..b177162 --- /dev/null +++ b/chart.tsx @@ -0,0 +1,221 @@ +"use client" + +import * as React from "react" +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart" + +export const description = "An interactive bar chart" + +const chartData = [ + { date: "2024-04-01", desktop: 222, mobile: 150 }, + { date: "2024-04-02", desktop: 97, mobile: 180 }, + { date: "2024-04-03", desktop: 167, mobile: 120 }, + { date: "2024-04-04", desktop: 242, mobile: 260 }, + { date: "2024-04-05", desktop: 373, mobile: 290 }, + { date: "2024-04-06", desktop: 301, mobile: 340 }, + { date: "2024-04-07", desktop: 245, mobile: 180 }, + { date: "2024-04-08", desktop: 409, mobile: 320 }, + { date: "2024-04-09", desktop: 59, mobile: 110 }, + { date: "2024-04-10", desktop: 261, mobile: 190 }, + { date: "2024-04-11", desktop: 327, mobile: 350 }, + { date: "2024-04-12", desktop: 292, mobile: 210 }, + { date: "2024-04-13", desktop: 342, mobile: 380 }, + { date: "2024-04-14", desktop: 137, mobile: 220 }, + { date: "2024-04-15", desktop: 120, mobile: 170 }, + { date: "2024-04-16", desktop: 138, mobile: 190 }, + { date: "2024-04-17", desktop: 446, mobile: 360 }, + { date: "2024-04-18", desktop: 364, mobile: 410 }, + { date: "2024-04-19", desktop: 243, mobile: 180 }, + { date: "2024-04-20", desktop: 89, mobile: 150 }, + { date: "2024-04-21", desktop: 137, mobile: 200 }, + { date: "2024-04-22", desktop: 224, mobile: 170 }, + { date: "2024-04-23", desktop: 138, mobile: 230 }, + { date: "2024-04-24", desktop: 387, mobile: 290 }, + { date: "2024-04-25", desktop: 215, mobile: 250 }, + { date: "2024-04-26", desktop: 75, mobile: 130 }, + { date: "2024-04-27", desktop: 383, mobile: 420 }, + { date: "2024-04-28", desktop: 122, mobile: 180 }, + { date: "2024-04-29", desktop: 315, mobile: 240 }, + { date: "2024-04-30", desktop: 454, mobile: 380 }, + { date: "2024-05-01", desktop: 165, mobile: 220 }, + { date: "2024-05-02", desktop: 293, mobile: 310 }, + { date: "2024-05-03", desktop: 247, mobile: 190 }, + { date: "2024-05-04", desktop: 385, mobile: 420 }, + { date: "2024-05-05", desktop: 481, mobile: 390 }, + { date: "2024-05-06", desktop: 498, mobile: 520 }, + { date: "2024-05-07", desktop: 388, mobile: 300 }, + { date: "2024-05-08", desktop: 149, mobile: 210 }, + { date: "2024-05-09", desktop: 227, mobile: 180 }, + { date: "2024-05-10", desktop: 293, mobile: 330 }, + { date: "2024-05-11", desktop: 335, mobile: 270 }, + { date: "2024-05-12", desktop: 197, mobile: 240 }, + { date: "2024-05-13", desktop: 197, mobile: 160 }, + { date: "2024-05-14", desktop: 448, mobile: 490 }, + { date: "2024-05-15", desktop: 473, mobile: 380 }, + { date: "2024-05-16", desktop: 338, mobile: 400 }, + { date: "2024-05-17", desktop: 499, mobile: 420 }, + { date: "2024-05-18", desktop: 315, mobile: 350 }, + { date: "2024-05-19", desktop: 235, mobile: 180 }, + { date: "2024-05-20", desktop: 177, mobile: 230 }, + { date: "2024-05-21", desktop: 82, mobile: 140 }, + { date: "2024-05-22", desktop: 81, mobile: 120 }, + { date: "2024-05-23", desktop: 252, mobile: 290 }, + { date: "2024-05-24", desktop: 294, mobile: 220 }, + { date: "2024-05-25", desktop: 201, mobile: 250 }, + { date: "2024-05-26", desktop: 213, mobile: 170 }, + { date: "2024-05-27", desktop: 420, mobile: 460 }, + { date: "2024-05-28", desktop: 233, mobile: 190 }, + { date: "2024-05-29", desktop: 78, mobile: 130 }, + { date: "2024-05-30", desktop: 340, mobile: 280 }, + { date: "2024-05-31", desktop: 178, mobile: 230 }, + { date: "2024-06-01", desktop: 178, mobile: 200 }, + { date: "2024-06-02", desktop: 470, mobile: 410 }, + { date: "2024-06-03", desktop: 103, mobile: 160 }, + { date: "2024-06-04", desktop: 439, mobile: 380 }, + { date: "2024-06-05", desktop: 88, mobile: 140 }, + { date: "2024-06-06", desktop: 294, mobile: 250 }, + { date: "2024-06-07", desktop: 323, mobile: 370 }, + { date: "2024-06-08", desktop: 385, mobile: 320 }, + { date: "2024-06-09", desktop: 438, mobile: 480 }, + { date: "2024-06-10", desktop: 155, mobile: 200 }, + { date: "2024-06-11", desktop: 92, mobile: 150 }, + { date: "2024-06-12", desktop: 492, mobile: 420 }, + { date: "2024-06-13", desktop: 81, mobile: 130 }, + { date: "2024-06-14", desktop: 426, mobile: 380 }, + { date: "2024-06-15", desktop: 307, mobile: 350 }, + { date: "2024-06-16", desktop: 371, mobile: 310 }, + { date: "2024-06-17", desktop: 475, mobile: 520 }, + { date: "2024-06-18", desktop: 107, mobile: 170 }, + { date: "2024-06-19", desktop: 341, mobile: 290 }, + { date: "2024-06-20", desktop: 408, mobile: 450 }, + { date: "2024-06-21", desktop: 169, mobile: 210 }, + { date: "2024-06-22", desktop: 317, mobile: 270 }, + { date: "2024-06-23", desktop: 480, mobile: 530 }, + { date: "2024-06-24", desktop: 132, mobile: 180 }, + { date: "2024-06-25", desktop: 141, mobile: 190 }, + { date: "2024-06-26", desktop: 434, mobile: 380 }, + { date: "2024-06-27", desktop: 448, mobile: 490 }, + { date: "2024-06-28", desktop: 149, mobile: 200 }, + { date: "2024-06-29", desktop: 103, mobile: 160 }, + { date: "2024-06-30", desktop: 446, mobile: 400 }, +] + +const chartConfig = { + views: { + label: "Page Views", + }, + desktop: { + label: "Desktop", + color: "var(--chart-2)", + }, + mobile: { + label: "Mobile", + color: "var(--chart-1)", + }, +} satisfies ChartConfig + +export function ChartBarInteractive() { + const [activeChart, setActiveChart] = + React.useState("desktop") + + const total = React.useMemo( + () => ({ + desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0), + mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0), + }), + [] + ) + + return ( + + +
+ Bar Chart - Interactive + + Showing total visitors for the last 3 months + +
+
+ {["desktop", "mobile"].map((key) => { + const chart = key as keyof typeof chartConfig + return ( + + ) + })} +
+
+ + + + + { + const date = new Date(value) + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + }} + /> + { + return new Date(value).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + }} + /> + } + /> + + + + +
+ ) +} diff --git a/resources/js/components/card/stat-card.tsx b/resources/js/components/card/stat-card.tsx index 4192646..85e0b46 100644 --- a/resources/js/components/card/stat-card.tsx +++ b/resources/js/components/card/stat-card.tsx @@ -13,11 +13,12 @@ type StatCardProps = { mainLabel?: string; mainValue: string | number; subLabel?: string; + description?: string; items?: StatItem[]; cols?: 2 | 3 | 4; }; -export function StatCard({ title, icon: Icon, mainLabel, mainValue, subLabel, items = [], cols = 3 }: StatCardProps) { +export function StatCard({ title, icon: Icon, mainLabel, mainValue, subLabel, description, items = [], cols = 3 }: StatCardProps) { return ( @@ -28,6 +29,7 @@ export function StatCard({ title, icon: Icon, mainLabel, mainValue, subLabel, it {mainLabel &&

{mainLabel}

}

{mainValue}

{subLabel &&

{subLabel}

} + {description &&

{description}

} {items.length > 0 && (
{items.map((item, index) => ( diff --git a/resources/js/components/ui/chart.tsx b/resources/js/components/ui/chart.tsx index 6947c2e..c7e0d39 100644 --- a/resources/js/components/ui/chart.tsx +++ b/resources/js/components/ui/chart.tsx @@ -253,8 +253,10 @@ function ChartTooltipContent({ {item.value != null && ( {typeof item.value === "number" - ? item.value.toLocaleString() - : String(item.value)} + ? item.value.toLocaleString('id-ID') + : typeof item.value === "string" && !isNaN(Number(item.value)) + ? Number(item.value).toLocaleString('id-ID') + : String(item.value ?? '')} )}
diff --git a/resources/js/pages/admin/analysis/index.tsx b/resources/js/pages/admin/analysis/index.tsx index 83b3677..ef5a4a3 100644 --- a/resources/js/pages/admin/analysis/index.tsx +++ b/resources/js/pages/admin/analysis/index.tsx @@ -1,7 +1,8 @@ import { StatCard } from '@/components/card/stat-card'; import { DatePicker } from '@/components/inputs'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; import { Select, SelectContent, @@ -11,13 +12,12 @@ import { SelectValue, } from '@/components/ui/select'; import { useCan } from '@/hooks/use-can'; -import { formatRupiah, formatRupiahShort } from '@/lib/rupiah'; +import { formatRupiah } from '@/lib/rupiah'; import { Head, router } from '@inertiajs/react'; import { Banknote, Package, ShoppingCart, - TrendingUp, UserCheck, } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -31,7 +31,6 @@ import { ResponsiveContainer, Tooltip, XAxis, - YAxis, } from 'recharts'; type Filters = { @@ -73,20 +72,19 @@ type AnalysisProps = { }; productStock: { total_stock: number; - total_reject: number; - total_retail: number; total_value: number; - total_products: number; - total_variants: number; - total_categories: number; + by_type: { + good?: number; + reject?: number; + retail?: number; + }; }; revenueSummary: { total_revenue: number; total_discount: number; total_deduction: number; + cogs: number; net: number; - net_warehouse: number; - net_retail: number; total_orders: number; avg_order: number; }; @@ -94,9 +92,9 @@ type AnalysisProps = { month: string; total: number; net: number; - net_warehouse: number; - net_retail: number; + discount: number; deduction: number; + cogs: number; }>; monthlyRevenueByChannel: Array<{ month: string; @@ -160,22 +158,77 @@ type AnalysisProps = { total_discount: number; avg_order: number; }>; + orderStats: { + by_channel: Array<{ + channel: string; + label: string; + count: number; + total: number; + }>; + by_payment_type: Array<{ + payment_type: string; + label: string; + count: number; + total: number; + }>; + by_marketing: Array<{ + name: string; + count: number; + total: number; + }>; + by_status: Array<{ + status: string; + label: string; + count: number; + }>; + }; }; -const REVENUE_COLORS: Record = { - total: '#60a5fa', - net: '#22c55e', - net_warehouse: '#10b981', - net_retail: '#06b6d4', - deduction: '#f97316', -}; +const revenueChartConfig = { + total: { + label: 'Total', + color: 'var(--chart-1)', + }, + net: { + label: 'Bersih', + color: 'var(--chart-2)', + }, + deduction: { + label: 'Potongan', + color: 'var(--chart-3)', + }, + discount: { + label: 'Diskon', + color: 'var(--chart-4)', + }, + cogs: { + label: 'HPP', + color: 'var(--chart-5)', + }, +} satisfies ChartConfig; -const EXPENSE_COLORS: Record = { - total: '#60a5fa', - purchase: '#f97316', - expense: '#a855f7', - advance: '#ef4444', -}; +const revenueKeys = ['total', 'net', 'deduction', 'discount', 'cogs'] as const; + +const expenseChartConfig = { + total: { + label: 'Total', + color: 'var(--chart-1)', + }, + purchase: { + label: 'Belanja', + color: 'var(--chart-2)', + }, + expense: { + label: 'Pengeluaran', + color: 'var(--chart-3)', + }, + advance: { + label: 'Kasbon', + color: 'var(--chart-4)', + }, +} satisfies ChartConfig; + +const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const; const CHANNEL_COLORS: Record = { store: '#22c55e', @@ -190,6 +243,88 @@ const PAYMENT_COLORS: Record = { marketplace: '#f97316', }; +const PIE_COLORS = [ + 'var(--chart-1)', + 'var(--chart-2)', + 'var(--chart-3)', + 'var(--chart-4)', + 'var(--chart-5)', +]; + +type PieChartItem = { + name: string; + count: number; +}; + +type DashboardPieChartProps = { + title: string; + data: PieChartItem[]; + dataKey: string; + nameKey: string; +}; + +function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { + const hasData = data.length > 0 && data.some((d) => d.count > 0); + + const chartConfig = useMemo(() => { + const config: ChartConfig = {}; + data.forEach((item, index) => { + config[item.name] = { + label: item.name, + color: PIE_COLORS[index % PIE_COLORS.length], + }; + }); + return config; + }, [data]); + + const chartData = useMemo(() => { + return data.map((item) => ({ + ...item, + fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length], + })); + }, [data]); + + return ( + + + {title} + + + {hasData ? ( + + + + } + /> + + } + className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center" + /> + + + ) : ( +
+ Belum ada data +
+ )} +
+
+ ); +} + type TooltipProps = { active?: boolean; payload?: Array<{ @@ -264,11 +399,14 @@ export default function Analysis({ topCustomers, topProducts, marketingSales, + orderStats, }: AnalysisProps) { const { can, hasAnyRole, hasRole } = useCan(); const [startDate, setStartDate] = useState(initialFilters.start_date ?? ''); const [endDate, setEndDate] = useState(initialFilters.end_date ?? ''); const [selectedPreset, setSelectedPreset] = useState(''); + const [activeRevenueKey, setActiveRevenueKey] = useState('total'); + const [activeExpenseKey, setActiveExpenseKey] = useState('total'); const hasActiveFilters = !!startDate || !!endDate; @@ -348,26 +486,24 @@ export default function Analysis({ }, [busyHours]); const visibleExpenseCharts = useMemo(() => { - const charts = ['total', 'purchase', 'expense', 'advance'] as const; - if (hasAnyRole(['owner', 'developer'])) { - return charts; + return expenseKeys; } - return charts.filter((c) => c !== 'purchase'); + return expenseKeys.filter((c) => c !== 'purchase'); }, [hasAnyRole]); const sectionOrder = useMemo(() => { if (hasAnyRole(['owner', 'developer'])) { - return { statCards: 1, revenue: 5, revenueByChannel: 6, expense: 7, profitGross: 8, totalOrder: 9, marketingSales: 10, topSuppliers: 11, topProducts: 12, topCustomers: 13, busyHours: 14 }; + return { statCards: 1, revenue: 4, revenueByChannel: 5, orderPie: 6, expense: 7, totalOrder: 8, marketingSales: 9, topSuppliers: 10, topProducts: 11, topCustomers: 12, busyHours: 13 }; } if (hasAnyRole(['admin_toko', 'direktur'])) { - return { statCards: 1, revenue: 4, revenueByChannel: 5, expense: 6, profitGross: 7, totalOrder: 8, marketingSales: 9, topProducts: 10, topCustomers: 11, busyHours: 12 }; + return { statCards: 1, revenue: 4, revenueByChannel: 5, orderPie: 6, expense: 7, totalOrder: 8, marketingSales: 9, topProducts: 10, topCustomers: 11, busyHours: 12 }; } if (hasRole('marketing')) { - return { statCards: 1, revenue: 2, revenueByChannel: 3, totalOrder: 4, topProducts: 5, topCustomers: 6 }; + return { statCards: 1, revenue: 2, revenueByChannel: 3, orderPie: 4, totalOrder: 5, topProducts: 6, topCustomers: 7 }; } return {}; @@ -412,22 +548,7 @@ export default function Analysis({ -
- {can('analysis.attendance') && isManager && ( - - )} - +
{can('analysis.attendance') && !isManager && myAttendance && ( )}
{can('analysis.revenue') && ( - + -
+
Pendapatan
- {Object.entries(REVENUE_COLORS).filter(([key]) => { - if (hasAnyRole(['owner', 'developer'])) { - return true; - } - - if (hasRole('cashier')) { - return key === 'total' || key === 'deduction'; - } - - return key === 'total' || key === 'net' || key === 'deduction'; - }).map(([key]) => ( -
- {key === 'net_warehouse' ? 'Total Gudang' : key === 'net_retail' ? 'Total Ecer' : key === 'total' ? 'Total' : key === 'net' ? 'Bersih' : 'Potongan'} - Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'net' ? revenueSummary.net : key === 'net_warehouse' ? revenueSummary.net_warehouse : key === 'net_retail' ? revenueSummary.net_retail : revenueSummary.total_deduction)} -
- ))} + {revenueKeys.filter((key) => { + if (hasAnyRole(['owner', 'developer'])) return true; + if (hasRole('cashier')) return key === 'total' || key === 'discount' || key === 'deduction' || key === 'cogs'; + return key === 'total' || key === 'discount' || key === 'deduction' || key === 'net' || key === 'cogs'; + }).map((key) => { + const value = key === 'total' ? revenueSummary.total_revenue : key === 'discount' ? revenueSummary.total_discount : key === 'net' ? revenueSummary.net : key === 'cogs' ? revenueSummary.cogs : revenueSummary.total_deduction; + return ( + + ); + })}
{monthlyRevenue.length > 0 ? ( - - + + - `Rp${formatRupiahShort(v)}`} /> - } /> - - + ( + <> +
+ {revenueChartConfig[activeRevenueKey]?.label ?? name} + + Rp{Number(value).toLocaleString('id-ID')} + + + )} + /> + } + /> + - + ) : (
Belum ada data pendapatan
)} @@ -618,35 +765,107 @@ export default function Analysis({
)} + {can('analysis.revenue') && ( +
+ ({ + name: item.label, + count: item.count, + }))} + dataKey="count" + nameKey="name" + /> + + ({ + name: item.label, + count: item.count, + }))} + dataKey="count" + nameKey="name" + /> + + ({ + name: item.name, + count: item.count, + }))} + dataKey="count" + nameKey="name" + /> + + ({ + name: item.label, + count: item.count, + }))} + dataKey="count" + nameKey="name" + /> +
+ )} + {can('analysis.expense') && ( - + -
+
Pengeluaran
- {visibleExpenseCharts.map((chart) => ( -
- {chart === 'total' ? 'Total' : chart === 'purchase' ? 'Belanja' : chart === 'expense' ? 'Pengeluaran' : 'Kasbon'} - Rp{formatRupiah(chart === 'total' ? expenseSummary.total : chart === 'purchase' ? expenseSummary.purchase_total : chart === 'expense' ? expenseSummary.expense_total : expenseSummary.advance_total)} -
- ))} + {visibleExpenseCharts.map((key) => { + const value = key === 'total' ? expenseSummary.total : key === 'purchase' ? expenseSummary.purchase_total : key === 'expense' ? expenseSummary.expense_total : expenseSummary.advance_total; + return ( + + ); + })}
{monthlyExpense.length > 0 ? ( - - + + - `Rp${formatRupiahShort(v)}`} /> - } /> - - - - + ( + <> +
+ {expenseChartConfig[activeExpenseKey]?.label ?? name} + + Rp{Number(value).toLocaleString('id-ID')} + + + )} + /> + } + /> + - + ) : (
Belum ada data pengeluaran
)} @@ -654,24 +873,6 @@ export default function Analysis({ )} - {(can('analysis.profit_gross') || can('analysis.profit_hpp')) && ( -
- -
- )} - {can('analysis.profit_orders') && (
Penjualan Marketing - Rekap penjualan per marketing {marketingSales.length > 0 ? ( @@ -735,19 +935,37 @@ export default function Analysis({ Top 5 Supplier - Berdasarkan total harga pembelian - + {topSuppliers.length > 0 ? ( - - ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))}> + + ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))} margin={{ left: 12, right: 12 }}> - `Rp${formatRupiahShort(v)}`} /> - } /> - + ( + <> +
+ Total Pembelian + + Rp{Number(value).toLocaleString('id-ID')} + + + )} + /> + } + /> + - + ) : (
Belum ada data supplier
)} @@ -759,19 +977,37 @@ export default function Analysis({ Top 5 Produk - Berdasarkan jumlah terjual - + {topProducts.length > 0 ? ( - - ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))}> + + ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))} margin={{ left: 12, right: 12 }}> - - } /> - + ( + <> +
+ Jumlah Terjual + + {Number(value).toLocaleString('id-ID')} + + + )} + /> + } + /> + - + ) : (
Belum ada data produk
)} @@ -783,19 +1019,37 @@ export default function Analysis({ Top 5 Pelanggan - Berdasarkan total nilai pesanan - + {topCustomers.length > 0 ? ( - - ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))}> + + ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))} margin={{ left: 12, right: 12 }}> - `Rp${formatRupiahShort(v)}`} /> - } /> - + ( + <> +
+ Total Pesanan + + Rp{Number(value).toLocaleString('id-ID')} + + + )} + /> + } + /> + - + ) : (
Belum ada data pelanggan
)} @@ -804,30 +1058,52 @@ export default function Analysis({ )} {can('analysis.busy_hours') && ( - - -
-
- Jam Sibuk Toko + + +
+ Jam Sibuk Toko +
+
+
+ Jam Tersibuk + {peakHour.hour}
-
-

Jam Tersibuk

-

{peakHour.hour}

-

{peakHour.orders} pesanan

+
+ Pesanan + {peakHour.orders.toLocaleString('id-ID')}
- + {busyHours.length > 0 ? ( - - + + - Math.round(v).toString()} /> - } /> - + ( + <> +
+ Pesanan + + {Number(value).toLocaleString('id-ID')} + + + )} + /> + } + /> + - + ) : (
Belum ada data pesanan
)} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index de6144f..f1c1782 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -343,7 +343,7 @@ export default function Dashboard({ items={[ { label: 'Bersih', - value: `Rp${formatRupiah(revenueSummary.total_revenue - revenueSummary.total_cogs - revenueSummary.total_deduction)}`, + value: `Rp${formatRupiah(revenueSummary.total_revenue - revenueSummary.total_cogs)}`, }, { label: 'Potongan', @@ -353,7 +353,12 @@ export default function Dashboard({ label: 'Diskon', value: `Rp${formatRupiah(revenueSummary.total_discount)}`, }, + { + label: 'HPP', + value: `Rp${formatRupiah(revenueSummary.total_cogs)}`, + }, ]} + cols={2} /> )}