From bb17bf6ad449c25a127fde7f5c9d35ac4ec84d5d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 16 Jul 2026 10:38:28 +0700 Subject: [PATCH] Refactor HomepageService to remove caching and simplify pageData method; disable SSR in Inertia config; improve service worker registration in app.ts; fix URL parameters in Analysis.vue router call. --- app/Services/System/AnalysisService.php | 1227 +++++++++++------------ app/Services/System/HomepageService.php | 47 +- config/inertia.php | 2 +- resources/js/app.ts | 34 +- resources/js/pages/admin/Analysis.vue | 6 +- 5 files changed, 640 insertions(+), 676 deletions(-) diff --git a/app/Services/System/AnalysisService.php b/app/Services/System/AnalysisService.php index 2130619..b3add16 100644 --- a/app/Services/System/AnalysisService.php +++ b/app/Services/System/AnalysisService.php @@ -20,762 +20,729 @@ use App\Models\Purchase; use App\Models\RawMaterialPrice; use App\Models\User; -use App\Services\Concerns\CachesQuery; use Carbon\Carbon; use Illuminate\Support\Facades\DB; class AnalysisService { - use CachesQuery; - public function getAttendance(): array { - return $this->cacheRemember('analysis:get_attendance', 900, function () { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $employeeQuery = Employee::query(); - $attendanceQuery = Attendance::query()->where('attendance_date', Carbon::today()); - $leaveRequestQuery = LeaveRequest::query() - ->approved() - ->where('start_date', '<=', Carbon::today()) - ->where('end_date', '>=', Carbon::today()); + $employeeQuery = Employee::query(); + $attendanceQuery = Attendance::query()->where('attendance_date', Carbon::today()); + $leaveRequestQuery = LeaveRequest::query() + ->approved() + ->where('start_date', '<=', Carbon::today()) + ->where('end_date', '>=', Carbon::today()); - if (! $isSuper) { - $employeeId = $user?->employee?->id; - $employeeQuery->where('id', $employeeId); - $attendanceQuery->where('employee_id', $employeeId); - $leaveRequestQuery->where('employee_id', $employeeId); - } + if (! $isSuper) { + $employeeId = $user?->employee?->id; + $employeeQuery->where('id', $employeeId); + $attendanceQuery->where('employee_id', $employeeId); + $leaveRequestQuery->where('employee_id', $employeeId); + } - $totalEmployees = $employeeQuery->count(); - $present = $attendanceQuery->distinct('employee_id')->count('employee_id'); - $onLeave = $leaveRequestQuery->distinct('employee_id')->count('employee_id'); - $absent = max(0, $totalEmployees - $present - $onLeave); + $totalEmployees = $employeeQuery->count(); + $present = $attendanceQuery->distinct('employee_id')->count('employee_id'); + $onLeave = $leaveRequestQuery->distinct('employee_id')->count('employee_id'); + $absent = max(0, $totalEmployees - $present - $onLeave); - return [ - 'total_employees' => $totalEmployees, - 'percentage' => $totalEmployees > 0 ? round($present / $totalEmployees * 100) : 0, - 'present' => $present, - 'absent' => $absent, - 'on_leave' => $onLeave, - ]; - }); + return [ + 'total_employees' => $totalEmployees, + 'percentage' => $totalEmployees > 0 ? round($present / $totalEmployees * 100) : 0, + 'present' => $present, + 'absent' => $absent, + 'on_leave' => $onLeave, + ]; } public function getMyAttendance(User $user, ?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_my_attendance', 900, function () use ($user, $startDate, $endDate) { - $employee = $user->employee; - - if ($employee === null) { - return [ - 'total_days' => 0, - 'present_days' => 0, - 'absent_days' => 0, - 'leave_days' => 0, - 'percentage' => 0, - ]; - } - - $start = $startDate ?? ($employee->join_date ? Carbon::parse($employee->join_date) : Carbon::today()->startOfMonth()); - $end = $endDate ?? Carbon::today(); - - $totalDays = 0; - $presentDays = 0; - $current = $start->copy()->startOfDay(); - - while ($current->lte($end)) { - if ($current->isWeekday()) { - $totalDays++; - - $hasAttendance = Attendance::query() - ->where('employee_id', $employee->id) - ->whereDate('attendance_date', $current) - ->exists(); - - if ($hasAttendance) { - $presentDays++; - } - } - - $current->addDay(); - } - - $leaveDays = LeaveRequest::query() - ->approved() - ->where('employee_id', $employee->id) - ->where('start_date', '<=', $end->toDateString()) - ->where('end_date', '>=', $start->toDateString()) - ->get() - ->sum(fn ($leave) => max( - 0, - min($leave->end_date, $end->toDateString()) - - max($leave->start_date, $start->toDateString()) - ) / 86400 + 1); - - $leaveDays = (int) $leaveDays; - $absentDays = max(0, $totalDays - $presentDays - $leaveDays); - $percentage = $totalDays > 0 ? round(($presentDays / $totalDays) * 100) : 0; + $employee = $user->employee; + if ($employee === null) { return [ - 'total_days' => $totalDays, - 'present_days' => $presentDays, - 'absent_days' => $absentDays, - 'leave_days' => $leaveDays, - 'percentage' => $percentage, + 'total_days' => 0, + 'present_days' => 0, + 'absent_days' => 0, + 'leave_days' => 0, + 'percentage' => 0, ]; - }); + } + + $start = $startDate ?? ($employee->join_date ? Carbon::parse($employee->join_date) : Carbon::today()->startOfMonth()); + $end = $endDate ?? Carbon::today(); + + $totalDays = 0; + $presentDays = 0; + $current = $start->copy()->startOfDay(); + + while ($current->lte($end)) { + if ($current->isWeekday()) { + $totalDays++; + + $hasAttendance = Attendance::query() + ->where('employee_id', $employee->id) + ->whereDate('attendance_date', $current) + ->exists(); + + if ($hasAttendance) { + $presentDays++; + } + } + + $current->addDay(); + } + + $leaveDays = LeaveRequest::query() + ->approved() + ->where('employee_id', $employee->id) + ->where('start_date', '<=', $end->toDateString()) + ->where('end_date', '>=', $start->toDateString()) + ->get() + ->sum(fn ($leave) => max( + 0, + min($leave->end_date, $end->toDateString()) + - max($leave->start_date, $start->toDateString()) + ) / 86400 + 1); + + $leaveDays = (int) $leaveDays; + $absentDays = max(0, $totalDays - $presentDays - $leaveDays); + $percentage = $totalDays > 0 ? round(($presentDays / $totalDays) * 100) : 0; + + return [ + 'total_days' => $totalDays, + 'present_days' => $presentDays, + 'absent_days' => $absentDays, + 'leave_days' => $leaveDays, + 'percentage' => $percentage, + ]; } public function getCashOverview(): array { - return $this->cacheRemember('analysis:get_cash_overview', 900, function () { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $totalBalanceQuery = CashAccount::query(); - $transactionQuery = CashTransaction::query()->whereDate('created_at', Carbon::today()); + $totalBalanceQuery = CashAccount::query(); + $transactionQuery = CashTransaction::query()->whereDate('created_at', Carbon::today()); - if (! $isSuper) { - $transactionQuery->where('created_by_id', $user->id); - $totalBalanceQuery->whereHas('transactions', fn ($q) => $q->where('created_by_id', $user->id)); - } + if (! $isSuper) { + $transactionQuery->where('created_by_id', $user->id); + $totalBalanceQuery->whereHas('transactions', fn ($q) => $q->where('created_by_id', $user->id)); + } - $totalBalance = $totalBalanceQuery->sum('balance'); - $summary = $transactionQuery - ->selectRaw(" - COUNT(*) as total_transactions, - COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as total_deposit, - COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawal - ") - ->first(); + $totalBalance = $totalBalanceQuery->sum('balance'); + $summary = $transactionQuery + ->selectRaw(" + COUNT(*) as total_transactions, + COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as total_deposit, + COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawal + ") + ->first(); - return [ - 'total_balance' => (int) $totalBalance, - 'total_transactions' => (int) ($summary->total_transactions ?? 0), - 'total_deposit' => (int) ($summary->total_deposit ?? 0), - 'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0), - ]; - }); + return [ + 'total_balance' => (int) $totalBalance, + 'total_transactions' => (int) ($summary->total_transactions ?? 0), + 'total_deposit' => (int) ($summary->total_deposit ?? 0), + 'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0), + ]; } public function getTopSuppliers(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_top_suppliers', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - return Purchase::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) - ->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id') - ->selectRaw('suppliers.name, SUM(purchases.total) as total_amount, COUNT(purchases.id) as purchase_count') - ->groupBy('suppliers.id', 'suppliers.name') - ->orderByDesc('total_amount') - ->limit(5) - ->get() - ->map(fn ($item) => [ - 'name' => $item->name, - 'total_amount' => (int) $item->total_amount, - 'purchase_count' => (int) $item->purchase_count, - ]) - ->toArray(); - }); + return Purchase::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) + ->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id') + ->selectRaw('suppliers.name, SUM(purchases.total) as total_amount, COUNT(purchases.id) as purchase_count') + ->groupBy('suppliers.id', 'suppliers.name') + ->orderByDesc('total_amount') + ->limit(5) + ->get() + ->map(fn ($item) => [ + 'name' => $item->name, + 'total_amount' => (int) $item->total_amount, + 'purchase_count' => (int) $item->purchase_count, + ]) + ->toArray(); } public function getTopCustomers(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_top_customers', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; - return Order::query() - ->completed() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->join('customers', 'orders.customer_id', '=', 'customers.id') - ->selectRaw('customers.name, SUM(orders.total_amount) as total_amount, COUNT(orders.id) as order_count') - ->groupBy('customers.id', 'customers.name') - ->orderByDesc('total_amount') - ->limit(5) - ->get() - ->map(fn ($item) => [ - 'name' => $item->name, - 'total_amount' => (int) $item->total_amount, - 'order_count' => (int) $item->order_count, - ]) - ->toArray(); - }); + return Order::query() + ->completed() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->join('customers', 'orders.customer_id', '=', 'customers.id') + ->selectRaw('customers.name, SUM(orders.total_amount) as total_amount, COUNT(orders.id) as order_count') + ->groupBy('customers.id', 'customers.name') + ->orderByDesc('total_amount') + ->limit(5) + ->get() + ->map(fn ($item) => [ + 'name' => $item->name, + 'total_amount' => (int) $item->total_amount, + 'order_count' => (int) $item->order_count, + ]) + ->toArray(); } public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_revenue_summary', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; - $revenueSummary = Order::query() - ->completed() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, COUNT(*) as total_orders, AVG(total_amount) as avg_order') - ->first(); + $revenueSummary = Order::query() + ->completed() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, COUNT(*) as total_orders, AVG(total_amount) as avg_order') + ->first(); - $totalMarketplaceFees = Order::query() - ->completed() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) - ->whereNotNull('marketplace_settings_snapshot') - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->get() - ->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); + $totalMarketplaceFees = Order::query() + ->completed() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) + ->whereNotNull('marketplace_settings_snapshot') + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->get() + ->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); - $totalCostPrice = OrderItem::query() - ->join('orders', 'order_items.order_id', '=', 'orders.id') - ->leftJoin('product_prices', function ($join) { - $join->on('order_items.product_variant_id', '=', 'product_prices.variant_id') - ->where('product_prices.type', PriceType::HARGA_MODAL->value); - }) - ->where('orders.status', OrderStatus::COMPLETED) - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp') - ->value('total_hpp'); + $totalCostPrice = OrderItem::query() + ->join('orders', 'order_items.order_id', '=', 'orders.id') + ->leftJoin('product_prices', function ($join) { + $join->on('order_items.product_variant_id', '=', 'product_prices.variant_id') + ->where('product_prices.type', PriceType::HARGA_MODAL->value); + }) + ->where('orders.status', OrderStatus::COMPLETED) + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp') + ->value('total_hpp'); - return [ - 'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0), - 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), - 'total_marketplace_fees' => $totalMarketplaceFees, - 'total_cost_price' => (int) ($totalCostPrice ?? 0), - 'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, - 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), - 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), - ]; - }); + return [ + 'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0), + 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), + 'total_marketplace_fees' => $totalMarketplaceFees, + 'total_cost_price' => (int) ($totalCostPrice ?? 0), + 'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, + 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), + 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), + ]; } public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_monthly_revenue', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; - $query = Order::query()->completed() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)); - $feeQuery = Order::query()->completed() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) - ->whereNotNull('marketplace_settings_snapshot'); + $query = Order::query()->completed() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)); + $feeQuery = Order::query()->completed() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) + ->whereNotNull('marketplace_settings_snapshot'); - if ($startDate && $endDate) { - $query->whereBetween('orders.created_at', [$startDate, $endDate]); - $feeQuery->whereBetween('orders.created_at', [$startDate, $endDate]); + if ($startDate && $endDate) { + $query->whereBetween('orders.created_at', [$startDate, $endDate]); + $feeQuery->whereBetween('orders.created_at', [$startDate, $endDate]); + } + + $monthlyData = $query + ->selectRaw(" + DATE_FORMAT(orders.created_at, '%Y-%m') as month_key, + SUM(total_amount) as total_revenue, + SUM(discount) as total_discount + ") + ->groupBy('month_key') + ->orderBy('month_key') + ->get(); + + $monthlyFees = $feeQuery + ->selectRaw("DATE_FORMAT(orders.created_at, '%Y-%m') as month_key") + ->get() + ->groupBy('month_key') + ->map(fn ($orders) => $orders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0))); + + $monthlyItemsData = OrderItem::query() + ->join('orders', 'order_items.order_id', '=', 'orders.id') + ->leftJoin('product_prices', function ($join) { + $join->on('order_items.product_variant_id', '=', 'product_prices.variant_id') + ->where('product_prices.type', PriceType::HARGA_MODAL->value); + }) + ->where('orders.status', OrderStatus::COMPLETED) + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw(" + DATE_FORMAT(orders.created_at, '%Y-%m') as month_key, + order_items.stock_quality, + SUM(order_items.subtotal) as subtotal, + COALESCE(SUM(order_items.quantity * product_prices.price), 0) as hpp + ") + ->groupBy('month_key', 'order_items.stock_quality') + ->get() + ->groupBy('month_key'); + + if ($monthlyData->isEmpty()) { + return []; + } + + $start = $startDate ?? Carbon::parse($monthlyData->first()->month_key.'-01'); + $end = $endDate ?? Carbon::parse($monthlyData->last()->month_key.'-01')->endOfMonth(); + + $result = []; + $current = $start->copy()->startOfMonth(); + while ($current->lte($end)) { + $key = $current->format('Y-m'); + $monthLabel = $current->locale('id')->translatedFormat('M Y'); + + $revenue = $monthlyData->firstWhere('month_key', $key); + $fees = $monthlyFees->get($key, 0); + $discount = (int) ($revenue->total_discount ?? 0); + $deduction = $discount + $fees; + + $monthItems = $monthlyItemsData->get($key, collect()); + $warehouseItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'good' + ); + $retailItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'retail' + ); + $rejectItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'reject' + ); + + $warehouseSubtotal = (int) ($warehouseItem?->subtotal ?? 0); + $warehouseHpp = (int) ($warehouseItem?->hpp ?? 0); + $retailSubtotal = (int) ($retailItem?->subtotal ?? 0); + $retailHpp = (int) ($retailItem?->hpp ?? 0); + $rejectSubtotal = (int) ($rejectItem?->subtotal ?? 0); + $rejectHpp = (int) ($rejectItem?->hpp ?? 0); + + $totalItemsSubtotal = $warehouseSubtotal + $retailSubtotal + $rejectSubtotal; + $hpp = $warehouseHpp + $retailHpp + $rejectHpp; + + $warehouseDeduction = 0; + $retailDeduction = 0; + if ($totalItemsSubtotal > 0) { + $warehouseDeduction = ($warehouseSubtotal / $totalItemsSubtotal) * $deduction; + $retailDeduction = ($retailSubtotal / $totalItemsSubtotal) * $deduction; } - $monthlyData = $query - ->selectRaw(" - DATE_FORMAT(orders.created_at, '%Y-%m') as month_key, - SUM(total_amount) as total_revenue, - SUM(discount) as total_discount - ") - ->groupBy('month_key') - ->orderBy('month_key') - ->get(); + $netWarehouse = $warehouseSubtotal - $warehouseDeduction - $warehouseHpp; + $netRetail = $retailSubtotal - $retailDeduction - $retailHpp; - $monthlyFees = $feeQuery - ->selectRaw("DATE_FORMAT(orders.created_at, '%Y-%m') as month_key") - ->get() - ->groupBy('month_key') - ->map(fn ($orders) => $orders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0))); + $result[] = [ + 'month' => $monthLabel, + 'total' => (int) ($revenue->total_revenue ?? 0), + 'net' => (int) ($revenue->total_revenue ?? 0) - $deduction - $hpp, + 'net_warehouse' => (int) round($netWarehouse), + 'net_retail' => (int) round($netRetail), + 'deduction' => $deduction, + ]; - $monthlyItemsData = OrderItem::query() - ->join('orders', 'order_items.order_id', '=', 'orders.id') - ->leftJoin('product_prices', function ($join) { - $join->on('order_items.product_variant_id', '=', 'product_prices.variant_id') - ->where('product_prices.type', PriceType::HARGA_MODAL->value); - }) - ->where('orders.status', OrderStatus::COMPLETED) - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw(" - DATE_FORMAT(orders.created_at, '%Y-%m') as month_key, - order_items.stock_quality, - SUM(order_items.subtotal) as subtotal, - COALESCE(SUM(order_items.quantity * product_prices.price), 0) as hpp - ") - ->groupBy('month_key', 'order_items.stock_quality') - ->get() - ->groupBy('month_key'); + $current->addMonth(); + } - if ($monthlyData->isEmpty()) { - return []; - } - - $start = $startDate ?? Carbon::parse($monthlyData->first()->month_key.'-01'); - $end = $endDate ?? Carbon::parse($monthlyData->last()->month_key.'-01')->endOfMonth(); - - $result = []; - $current = $start->copy()->startOfMonth(); - while ($current->lte($end)) { - $key = $current->format('Y-m'); - $monthLabel = $current->locale('id')->translatedFormat('M Y'); - - $revenue = $monthlyData->firstWhere('month_key', $key); - $fees = $monthlyFees->get($key, 0); - $discount = (int) ($revenue->total_discount ?? 0); - $deduction = $discount + $fees; - - $monthItems = $monthlyItemsData->get($key, collect()); - $warehouseItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'good' - ); - $retailItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'retail' - ); - $rejectItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'reject' - ); - - $warehouseSubtotal = (int) ($warehouseItem?->subtotal ?? 0); - $warehouseHpp = (int) ($warehouseItem?->hpp ?? 0); - $retailSubtotal = (int) ($retailItem?->subtotal ?? 0); - $retailHpp = (int) ($retailItem?->hpp ?? 0); - $rejectSubtotal = (int) ($rejectItem?->subtotal ?? 0); - $rejectHpp = (int) ($rejectItem?->hpp ?? 0); - - $totalItemsSubtotal = $warehouseSubtotal + $retailSubtotal + $rejectSubtotal; - $hpp = $warehouseHpp + $retailHpp + $rejectHpp; - - $warehouseDeduction = 0; - $retailDeduction = 0; - if ($totalItemsSubtotal > 0) { - $warehouseDeduction = ($warehouseSubtotal / $totalItemsSubtotal) * $deduction; - $retailDeduction = ($retailSubtotal / $totalItemsSubtotal) * $deduction; - } - - $netWarehouse = $warehouseSubtotal - $warehouseDeduction - $warehouseHpp; - $netRetail = $retailSubtotal - $retailDeduction - $retailHpp; - - $result[] = [ - 'month' => $monthLabel, - 'total' => (int) ($revenue->total_revenue ?? 0), - 'net' => (int) ($revenue->total_revenue ?? 0) - $deduction - $hpp, - 'net_warehouse' => (int) round($netWarehouse), - 'net_retail' => (int) round($netRetail), - 'deduction' => $deduction, - ]; - - $current->addMonth(); - } - - return $result; - }); + return $result; } public function getExpenseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_expense_summary', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $purchase = Purchase::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) - ->selectRaw('COALESCE(SUM(total), 0) as total') - ->first(); + $purchase = Purchase::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) + ->selectRaw('COALESCE(SUM(total), 0) as total') + ->first(); - $expenses = Expense::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id)) - ->selectRaw('COALESCE(SUM(amount), 0) as total') - ->first(); + $expenses = Expense::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id)) + ->selectRaw('COALESCE(SUM(amount), 0) as total') + ->first(); - $employeeAdvance = EmployeeAdvance::query() - ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id)) - ->selectRaw('COALESCE(SUM(amount - paid_amount), 0) as total') - ->first(); + $employeeAdvance = EmployeeAdvance::query() + ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id)) + ->selectRaw('COALESCE(SUM(amount - paid_amount), 0) as total') + ->first(); - $purchaseTotal = (int) ($purchase->total ?? 0); - if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) { - $purchaseTotal = 0; - } - $expenseTotal = (int) ($expenses->total ?? 0); - $advanceTotal = (int) ($employeeAdvance->total ?? 0); + $purchaseTotal = (int) ($purchase->total ?? 0); + if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) { + $purchaseTotal = 0; + } + $expenseTotal = (int) ($expenses->total ?? 0); + $advanceTotal = (int) ($employeeAdvance->total ?? 0); - return [ - 'total' => $purchaseTotal + $expenseTotal + $advanceTotal, - 'purchase_total' => $purchaseTotal, - 'expense_total' => $expenseTotal, - 'advance_total' => $advanceTotal, - ]; - }); + return [ + 'total' => $purchaseTotal + $expenseTotal + $advanceTotal, + 'purchase_total' => $purchaseTotal, + 'expense_total' => $expenseTotal, + 'advance_total' => $advanceTotal, + ]; } public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_monthly_expense', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $purchases = Purchase::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) - ->selectRaw("DATE_FORMAT(purchases.created_at, '%Y-%m') as month_key, COALESCE(SUM(total), 0) as total") - ->groupBy('month_key') - ->orderBy('month_key') - ->get(); + $purchases = Purchase::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) + ->selectRaw("DATE_FORMAT(purchases.created_at, '%Y-%m') as month_key, COALESCE(SUM(total), 0) as total") + ->groupBy('month_key') + ->orderBy('month_key') + ->get(); - $expenses = Expense::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id)) - ->selectRaw("DATE_FORMAT(expenses.created_at, '%Y-%m') as month_key, COALESCE(SUM(amount), 0) as total") - ->groupBy('month_key') - ->orderBy('month_key') - ->get(); + $expenses = Expense::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id)) + ->selectRaw("DATE_FORMAT(expenses.created_at, '%Y-%m') as month_key, COALESCE(SUM(amount), 0) as total") + ->groupBy('month_key') + ->orderBy('month_key') + ->get(); - $advances = EmployeeAdvance::query() - ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id)) - ->selectRaw("DATE_FORMAT(employee_advances.created_at, '%Y-%m') as month_key, COALESCE(SUM(amount - paid_amount), 0) as total") - ->groupBy('month_key') - ->orderBy('month_key') - ->get(); + $advances = EmployeeAdvance::query() + ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id)) + ->selectRaw("DATE_FORMAT(employee_advances.created_at, '%Y-%m') as month_key, COALESCE(SUM(amount - paid_amount), 0) as total") + ->groupBy('month_key') + ->orderBy('month_key') + ->get(); - $allMonths = collect() - ->merge($purchases->pluck('month_key')) - ->merge($expenses->pluck('month_key')) - ->merge($advances->pluck('month_key')) - ->unique() - ->sort() - ->values(); + $allMonths = collect() + ->merge($purchases->pluck('month_key')) + ->merge($expenses->pluck('month_key')) + ->merge($advances->pluck('month_key')) + ->unique() + ->sort() + ->values(); - if ($allMonths->isEmpty()) { - return []; + if ($allMonths->isEmpty()) { + return []; + } + + $start = $startDate ?? Carbon::parse($allMonths->first().'-01'); + $end = $endDate ?? Carbon::parse($allMonths->last().'-01')->endOfMonth(); + + $result = []; + $current = $start->copy()->startOfMonth(); + while ($current->lte($end)) { + $key = $current->format('Y-m'); + $monthLabel = $current->locale('id')->translatedFormat('M Y'); + + $purchaseAmount = (int) ($purchases->firstWhere('month_key', $key)->total ?? 0); + if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) { + $purchaseAmount = 0; } + $expenseAmount = (int) ($expenses->firstWhere('month_key', $key)->total ?? 0); + $advanceAmount = (int) ($advances->firstWhere('month_key', $key)->total ?? 0); - $start = $startDate ?? Carbon::parse($allMonths->first().'-01'); - $end = $endDate ?? Carbon::parse($allMonths->last().'-01')->endOfMonth(); + $result[] = [ + 'month' => $monthLabel, + 'total' => $purchaseAmount + $expenseAmount + $advanceAmount, + 'purchase' => $purchaseAmount, + 'expense' => $expenseAmount, + 'advance' => $advanceAmount, + ]; - $result = []; - $current = $start->copy()->startOfMonth(); - while ($current->lte($end)) { - $key = $current->format('Y-m'); - $monthLabel = $current->locale('id')->translatedFormat('M Y'); + $current->addMonth(); + } - $purchaseAmount = (int) ($purchases->firstWhere('month_key', $key)->total ?? 0); - if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) { - $purchaseAmount = 0; - } - $expenseAmount = (int) ($expenses->firstWhere('month_key', $key)->total ?? 0); - $advanceAmount = (int) ($advances->firstWhere('month_key', $key)->total ?? 0); - - $result[] = [ - 'month' => $monthLabel, - 'total' => $purchaseAmount + $expenseAmount + $advanceAmount, - 'purchase' => $purchaseAmount, - 'expense' => $expenseAmount, - 'advance' => $advanceAmount, - ]; - - $current->addMonth(); - } - - return $result; - }); + return $result; } public function getProfitMetrics(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_profit_metrics', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; - $orderQuery = Order::query()->completed() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)); - if ($startDate && $endDate) { - $orderQuery->whereBetween('orders.created_at', [$startDate, $endDate]); - } + $orderQuery = Order::query()->completed() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)); + if ($startDate && $endDate) { + $orderQuery->whereBetween('orders.created_at', [$startDate, $endDate]); + } - $revenueData = (clone $orderQuery) - ->selectRaw(' - COUNT(*) as total_orders, - SUM(total_amount) as total_revenue, - SUM(discount) as total_discount, - SUM(subtotal) as total_subtotal - ') - ->first(); + $revenueData = (clone $orderQuery) + ->selectRaw(' + COUNT(*) as total_orders, + SUM(total_amount) as total_revenue, + SUM(discount) as total_discount, + SUM(subtotal) as total_subtotal + ') + ->first(); - $totalRevenue = (int) ($revenueData->total_revenue ?? 0); - $totalDiscount = (int) ($revenueData->total_discount ?? 0); - $totalSubtotal = (int) ($revenueData->total_subtotal ?? 0); - $totalOrders = (int) ($revenueData->total_orders ?? 0); + $totalRevenue = (int) ($revenueData->total_revenue ?? 0); + $totalDiscount = (int) ($revenueData->total_discount ?? 0); + $totalSubtotal = (int) ($revenueData->total_subtotal ?? 0); + $totalOrders = (int) ($revenueData->total_orders ?? 0); - $marketplaceFees = (clone $orderQuery) - ->whereNotNull('marketplace_settings_snapshot') - ->get() - ->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); + $marketplaceFees = (clone $orderQuery) + ->whereNotNull('marketplace_settings_snapshot') + ->get() + ->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); - $itemsData = OrderItem::query() - ->join('orders', 'order_items.order_id', '=', 'orders.id') - ->where('orders.status', OrderStatus::COMPLETED) - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw(' - SUM(order_items.quantity) as total_qty, - COUNT(order_items.id) as total_items - ') - ->first(); + $itemsData = OrderItem::query() + ->join('orders', 'order_items.order_id', '=', 'orders.id') + ->where('orders.status', OrderStatus::COMPLETED) + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw(' + SUM(order_items.quantity) as total_qty, + COUNT(order_items.id) as total_items + ') + ->first(); - $totalQty = (int) ($itemsData->total_qty ?? 0); - $totalItems = (int) ($itemsData->total_items ?? 0); + $totalQty = (int) ($itemsData->total_qty ?? 0); + $totalItems = (int) ($itemsData->total_items ?? 0); - // HPP from product_prices with type = 'harga_modal' - $hpp = OrderItem::query() - ->join('orders', 'order_items.order_id', '=', 'orders.id') - ->leftJoin('product_prices', function ($join) { - $join->on('order_items.product_variant_id', '=', 'product_prices.variant_id') - ->where('product_prices.type', PriceType::HARGA_MODAL->value); - }) - ->where('orders.status', OrderStatus::COMPLETED) - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp') - ->value('total_hpp'); + // HPP from product_prices with type = 'harga_modal' + $hpp = OrderItem::query() + ->join('orders', 'order_items.order_id', '=', 'orders.id') + ->leftJoin('product_prices', function ($join) { + $join->on('order_items.product_variant_id', '=', 'product_prices.variant_id') + ->where('product_prices.type', PriceType::HARGA_MODAL->value); + }) + ->where('orders.status', OrderStatus::COMPLETED) + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw('COALESCE(SUM(order_items.quantity * product_prices.price), 0) as total_hpp') + ->value('total_hpp'); - $totalHpp = (int) ($hpp ?? 0); + $totalHpp = (int) ($hpp ?? 0); - // Expenses - $purchaseTotal = Purchase::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) - ->sum('total'); + // Expenses + $purchaseTotal = Purchase::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('purchases.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('purchases.created_by_id', $user->id)) + ->sum('total'); - if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) { - $purchaseTotal = 0; - } + if (! $user?->hasAnyRole([Role::DEVELOPER->value, Role::OWNER->value])) { + $purchaseTotal = 0; + } - $expenseTotal = Expense::query() - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id)) - ->sum('amount'); + $expenseTotal = Expense::query() + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('expenses.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('expenses.created_by_id', $user->id)) + ->sum('amount'); - $advanceTotal = EmployeeAdvance::query() - ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate])) - ->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id)) - ->sum(DB::raw('amount - paid_amount')); + $advanceTotal = EmployeeAdvance::query() + ->whereIn('status', [EmployeeAdvanceStatus::APPROVED, EmployeeAdvanceStatus::PARTIALLY_PAID]) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('employee_advances.created_at', [$startDate, $endDate])) + ->when(! $isSuper, fn ($q) => $q->where('employee_id', $user->employee?->id)) + ->sum(DB::raw('amount - paid_amount')); - $totalExpenses = (int) $purchaseTotal + (int) $expenseTotal + (int) $advanceTotal; + $totalExpenses = (int) $purchaseTotal + (int) $expenseTotal + (int) $advanceTotal; - $grossProfit = $totalRevenue - $totalHpp - $totalDiscount - $marketplaceFees; - $netProfit = $grossProfit - (int) $expenseTotal - (int) $advanceTotal; - $profitMargin = $totalRevenue > 0 ? round(($netProfit / $totalRevenue) * 100, 1) : 0; - $aov = $totalOrders > 0 ? (int) round($totalRevenue / $totalOrders) : 0; - $itemsPerTransaction = $totalOrders > 0 ? round($totalItems / $totalOrders, 1) : 0; + $grossProfit = $totalRevenue - $totalHpp - $totalDiscount - $marketplaceFees; + $netProfit = $grossProfit - (int) $expenseTotal - (int) $advanceTotal; + $profitMargin = $totalRevenue > 0 ? round(($netProfit / $totalRevenue) * 100, 1) : 0; + $aov = $totalOrders > 0 ? (int) round($totalRevenue / $totalOrders) : 0; + $itemsPerTransaction = $totalOrders > 0 ? round($totalItems / $totalOrders, 1) : 0; - return [ - 'total_orders' => $totalOrders, - 'total_products_sold' => $totalQty, - 'hpp' => $totalHpp, - 'gross_profit' => $grossProfit, - 'net_profit' => $netProfit, - 'profit_margin' => $profitMargin, - 'aov' => $aov, - 'items_per_transaction' => $itemsPerTransaction, - ]; - }); + return [ + 'total_orders' => $totalOrders, + 'total_products_sold' => $totalQty, + 'hpp' => $totalHpp, + 'gross_profit' => $grossProfit, + 'net_profit' => $netProfit, + 'profit_margin' => $profitMargin, + 'aov' => $aov, + 'items_per_transaction' => $itemsPerTransaction, + ]; } public function getRawMaterialStock(): array { - return $this->cacheRemember('analysis:get_raw_material_stock', 900, function () { - $prices = RawMaterialPrice::query() - ->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id') - ->selectRaw(' - raw_materials.unit, - SUM(raw_material_prices.stock) as total_stock, - SUM(raw_material_prices.stock * raw_material_prices.price) as total_value - ') - ->groupBy('raw_materials.unit') - ->get() - ->keyBy('unit'); + $prices = RawMaterialPrice::query() + ->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id') + ->selectRaw(' + raw_materials.unit, + SUM(raw_material_prices.stock) as total_stock, + SUM(raw_material_prices.stock * raw_material_prices.price) as total_value + ') + ->groupBy('raw_materials.unit') + ->get() + ->keyBy('unit'); - $totalStock = (float) $prices->sum('total_stock'); - $totalValue = (int) $prices->sum('total_value'); + $totalStock = (float) $prices->sum('total_stock'); + $totalValue = (int) $prices->sum('total_value'); - return [ - 'total_stock' => round($totalStock, 2), - 'total_value' => $totalValue, - 'by_unit' => [ - 'yard' => round((float) ($prices->get('yard')->total_stock ?? 0), 2), - 'meter' => round((float) ($prices->get('meter')->total_stock ?? 0), 2), - 'kilogram' => round((float) ($prices->get('kilogram')->total_stock ?? 0), 2), - ], - ]; - }); + return [ + 'total_stock' => round($totalStock, 2), + 'total_value' => $totalValue, + 'by_unit' => [ + 'yard' => round((float) ($prices->get('yard')->total_stock ?? 0), 2), + 'meter' => round((float) ($prices->get('meter')->total_stock ?? 0), 2), + 'kilogram' => round((float) ($prices->get('kilogram')->total_stock ?? 0), 2), + ], + ]; } public function getProductStock(): array { - return $this->cacheRemember('analysis:get_product_stock', 900, function () { - $variants = ProductVariant::query() - ->join('products', 'product_variants.product_id', '=', 'products.id') - ->selectRaw(' - SUM(product_variants.stock) as total_stock, - SUM(product_variants.reject_stock) as total_reject, - SUM(product_variants.retail_stock) as total_retail, - COUNT(product_variants.id) as total_variants, - COUNT(DISTINCT products.id) as total_products - ') - ->first(); + $variants = ProductVariant::query() + ->join('products', 'product_variants.product_id', '=', 'products.id') + ->selectRaw(' + SUM(product_variants.stock) as total_stock, + SUM(product_variants.reject_stock) as total_reject, + SUM(product_variants.retail_stock) as total_retail, + COUNT(product_variants.id) as total_variants, + COUNT(DISTINCT products.id) as total_products + ') + ->first(); - $totalValue = \DB::table('product_prices') - ->join('product_variants', 'product_prices.variant_id', '=', 'product_variants.id') - ->where('product_prices.type', PriceType::HARGA_MODAL->value) - ->whereNull('product_variants.deleted_at') - ->whereNull('product_prices.deleted_at') - ->selectRaw('SUM(product_variants.stock * product_prices.price) as total') - ->value('total'); + $totalValue = \DB::table('product_prices') + ->join('product_variants', 'product_prices.variant_id', '=', 'product_variants.id') + ->where('product_prices.type', PriceType::HARGA_MODAL->value) + ->whereNull('product_variants.deleted_at') + ->whereNull('product_prices.deleted_at') + ->selectRaw('SUM(product_variants.stock * product_prices.price) as total') + ->value('total'); - $totalCategories = \DB::table('product_categories') - ->distinct('category_id') - ->count('category_id'); + $totalCategories = \DB::table('product_categories') + ->distinct('category_id') + ->count('category_id'); - return [ - 'total_stock' => (int) ($variants->total_stock ?? 0), - 'total_reject' => (int) ($variants->total_reject ?? 0), - 'total_retail' => (int) ($variants->total_retail ?? 0), - 'total_value' => (int) ($totalValue ?? 0), - 'total_products' => (int) ($variants->total_products ?? 0), - 'total_variants' => (int) ($variants->total_variants ?? 0), - 'total_categories' => (int) $totalCategories, - ]; - }); + return [ + 'total_stock' => (int) ($variants->total_stock ?? 0), + 'total_reject' => (int) ($variants->total_reject ?? 0), + 'total_retail' => (int) ($variants->total_retail ?? 0), + 'total_value' => (int) ($totalValue ?? 0), + 'total_products' => (int) ($variants->total_products ?? 0), + 'total_variants' => (int) ($variants->total_variants ?? 0), + 'total_categories' => (int) $totalCategories, + ]; } public function getBusyHours(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_busy_hours', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; - $hourlyData = Order::query() - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw('HOUR(orders.created_at) as hour, COUNT(*) as order_count') - ->groupBy('hour') - ->orderBy('hour') - ->get() - ->keyBy('hour'); + $hourlyData = Order::query() + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw('HOUR(orders.created_at) as hour, COUNT(*) as order_count') + ->groupBy('hour') + ->orderBy('hour') + ->get() + ->keyBy('hour'); - $result = []; - for ($h = 0; $h < 24; $h++) { - $result[] = [ - 'hour' => str_pad($h, 2, '0', STR_PAD_LEFT).':00', - 'orders' => (int) ($hourlyData->get($h)->order_count ?? 0), - ]; - } + $result = []; + for ($h = 0; $h < 24; $h++) { + $result[] = [ + 'hour' => str_pad($h, 2, '0', STR_PAD_LEFT).':00', + 'orders' => (int) ($hourlyData->get($h)->order_count ?? 0), + ]; + } - return $result; - }); + return $result; } public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_top_products', 900, function () use ($startDate, $endDate) { - /** @var User|null $user */ - $user = auth()->user(); - $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; - $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; + /** @var User|null $user */ + $user = auth()->user(); + $isSuper = $user?->hasAnyRole(['developer', 'owner', 'direktur']) ?? false; + $isMarketing = $user?->hasAnyRole(['marketing-offline', 'marketing-online']) ?? false; - return OrderItem::query() - ->join('orders', 'order_items.order_id', '=', 'orders.id') - ->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id') - ->join('products', 'product_variants.product_id', '=', 'products.id') - ->where('orders.status', OrderStatus::COMPLETED) - ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) - ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, SUM(order_items.quantity) as total_qty, SUM(order_items.subtotal) as total_revenue") - ->groupBy('order_items.product_variant_id', 'products.name', 'product_variants.name') - ->orderByDesc('total_qty') - ->limit(5) - ->get() - ->map(fn ($item) => [ - 'name' => $item->full_name, - 'total_qty' => (int) $item->total_qty, - 'total_revenue' => (int) $item->total_revenue, - ]) - ->toArray(); - }); + return OrderItem::query() + ->join('orders', 'order_items.order_id', '=', 'orders.id') + ->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id') + ->join('products', 'product_variants.product_id', '=', 'products.id') + ->where('orders.status', OrderStatus::COMPLETED) + ->when(! $isSuper && $isMarketing, fn ($q) => $q->where('orders.marketing_id', $user->id)) + ->when(! $isSuper && ! $isMarketing, fn ($q) => $q->where('orders.created_by_id', $user->id)) + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, SUM(order_items.quantity) as total_qty, SUM(order_items.subtotal) as total_revenue") + ->groupBy('order_items.product_variant_id', 'products.name', 'product_variants.name') + ->orderByDesc('total_qty') + ->limit(5) + ->get() + ->map(fn ($item) => [ + 'name' => $item->full_name, + 'total_qty' => (int) $item->total_qty, + 'total_revenue' => (int) $item->total_revenue, + ]) + ->toArray(); } public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate = null): array { - return $this->cacheRemember('analysis:get_marketing_sales', 900, function () use ($startDate, $endDate) { - $qtySubquery = DB::table('order_items') - ->select('order_id', DB::raw('SUM(quantity) as total_qty')) - ->whereNull('deleted_at') - ->groupBy('order_id'); + $qtySubquery = DB::table('order_items') + ->select('order_id', DB::raw('SUM(quantity) as total_qty')) + ->whereNull('deleted_at') + ->groupBy('order_id'); - return Order::query() - ->completed() - ->whereNotNull('marketing_id') - ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) - ->join('users', 'orders.marketing_id', '=', 'users.id') - ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') - ->leftJoinSub($qtySubquery, 'order_qtys', function ($join) { - $join->on('orders.id', '=', 'order_qtys.order_id'); - }) - ->selectRaw(' - users.id as marketing_id, - COALESCE(user_profiles.full_name, users.username) as marketing_name, - COUNT(orders.id) as total_orders, - SUM(orders.total_amount) as total_revenue, - SUM(orders.subtotal) as total_subtotal, - SUM(orders.discount) as total_discount, - AVG(orders.total_amount) as avg_order, - SUM(COALESCE(order_qtys.total_qty, 0)) as total_products_sold - ') - ->groupBy('users.id', 'user_profiles.full_name', 'users.username') - ->orderByDesc('total_revenue') - ->get() - ->map(fn ($item) => [ - 'marketing_name' => $item->marketing_name, - 'total_orders' => (int) $item->total_orders, - 'total_products_sold' => (int) $item->total_products_sold, - 'total_revenue' => (int) $item->total_revenue, - 'total_subtotal' => (int) $item->total_subtotal, - 'total_discount' => (int) $item->total_discount, - 'avg_order' => (int) $item->avg_order, - ]) - ->toArray(); - }); + return Order::query() + ->completed() + ->whereNotNull('marketing_id') + ->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate])) + ->join('users', 'orders.marketing_id', '=', 'users.id') + ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') + ->leftJoinSub($qtySubquery, 'order_qtys', function ($join) { + $join->on('orders.id', '=', 'order_qtys.order_id'); + }) + ->selectRaw(' + users.id as marketing_id, + COALESCE(user_profiles.full_name, users.username) as marketing_name, + COUNT(orders.id) as total_orders, + SUM(orders.total_amount) as total_revenue, + SUM(orders.subtotal) as total_subtotal, + SUM(orders.discount) as total_discount, + AVG(orders.total_amount) as avg_order, + SUM(COALESCE(order_qtys.total_qty, 0)) as total_products_sold + ') + ->groupBy('users.id', 'user_profiles.full_name', 'users.username') + ->orderByDesc('total_revenue') + ->get() + ->map(fn ($item) => [ + 'marketing_name' => $item->marketing_name, + 'total_orders' => (int) $item->total_orders, + 'total_products_sold' => (int) $item->total_products_sold, + 'total_revenue' => (int) $item->total_revenue, + 'total_subtotal' => (int) $item->total_subtotal, + 'total_discount' => (int) $item->total_discount, + 'avg_order' => (int) $item->avg_order, + ]) + ->toArray(); } public function isManager(?User $user): bool diff --git a/app/Services/System/HomepageService.php b/app/Services/System/HomepageService.php index 7457d40..4e6269d 100644 --- a/app/Services/System/HomepageService.php +++ b/app/Services/System/HomepageService.php @@ -6,7 +6,6 @@ use App\Models\Category; use App\Models\Product; use App\Models\SystemConfiguration; -use App\Services\Concerns\CachesQuery; use App\Services\Manage\CuttingResultPriceResolver; use App\Services\System\Setting\HomepageSettingService; use App\Settings\SocialMediaSettings; @@ -15,8 +14,6 @@ class HomepageService { - use CachesQuery; - public function __construct( private readonly CuttingResultPriceResolver $cuttingResultPriceResolver, private readonly HomepageSettingService $homepageSettingService, @@ -24,32 +21,30 @@ public function __construct( public function pageData(): array { - return $this->cacheRemember('homepage:page_data', 900, function () { - $categories = Category::getActiveWithProducts(); - $products = $this->getProducts(); + $categories = Category::getActiveWithProducts(); + $products = $this->getProducts(); - $configuration = SystemConfiguration::instance(); - $logo = MediaPresenter::first($configuration, 'logo'); - $logoUrl = $logo['url'] ?? null; + $configuration = SystemConfiguration::instance(); + $logo = MediaPresenter::first($configuration, 'logo'); + $logoUrl = $logo['url'] ?? null; - $settings = app(SystemSettings::class); - $socialSettings = app(SocialMediaSettings::class); + $settings = app(SystemSettings::class); + $socialSettings = app(SocialMediaSettings::class); - return [ - 'categories' => $categories, - 'products' => $products, - 'appName' => $settings->app_name ?? 'DST Collection', - 'aboutApp' => $settings->about_app ?? '', - 'contactEmail' => $settings->email ?? '', - 'contactPhone' => $settings->phone ?? '', - 'contactAddress' => $settings->address ?? '', - 'logoUrl' => $logoUrl, - 'instagramUrl' => $socialSettings->instagram_url ?? null, - 'facebookUrl' => $socialSettings->facebook_url ?? null, - 'tiktokUrl' => $socialSettings->tiktok_url ?? null, - 'homepage' => $this->homepageSettingService->homepageData(), - ]; - }); + return [ + 'categories' => $categories, + 'products' => $products, + 'appName' => $settings->app_name ?? 'DST Collection', + 'aboutApp' => $settings->about_app ?? '', + 'contactEmail' => $settings->email ?? '', + 'contactPhone' => $settings->phone ?? '', + 'contactAddress' => $settings->address ?? '', + 'logoUrl' => $logoUrl, + 'instagramUrl' => $socialSettings->instagram_url ?? null, + 'facebookUrl' => $socialSettings->facebook_url ?? null, + 'tiktokUrl' => $socialSettings->tiktok_url ?? null, + 'homepage' => $this->homepageSettingService->homepageData(), + ]; } private function getProducts(): array diff --git a/config/inertia.php b/config/inertia.php index 70f6b9b..8902391 100644 --- a/config/inertia.php +++ b/config/inertia.php @@ -16,7 +16,7 @@ */ 'ssr' => [ - 'enabled' => true, + 'enabled' => false, 'url' => 'http://127.0.0.1:13714', // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), diff --git a/resources/js/app.ts b/resources/js/app.ts index 1a0bbfd..a11d7a6 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -5,24 +5,26 @@ import { Toaster } from '@/components/ui/sonner'; import { useFlashToast } from '@/composables/useFlashToast'; import { restoreConnection } from '@/lib/thermal-printer/stable-transport'; -if ('serviceWorker' in navigator) { - navigator.serviceWorker - .register('/sw.js', { scope: '/' }) - .then((reg) => { - console.log('[SW] Registered:', reg.scope); - }) - .catch((err) => { - console.error('[SW] Registration failed:', err); - }); +if (typeof window !== 'undefined') { + if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/sw.js', { scope: '/' }) + .then((reg) => { + console.log('[SW] Registered:', reg.scope); + }) + .catch((err) => { + console.error('[SW] Registration failed:', err); + }); + } + + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + (window as any).deferredPwaPrompt = e; + }); + + void restoreConnection(); } -window.addEventListener('beforeinstallprompt', (e) => { - e.preventDefault(); - (window as any).deferredPwaPrompt = e; -}); - -void restoreConnection(); - const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; createInertiaApp({ diff --git a/resources/js/pages/admin/Analysis.vue b/resources/js/pages/admin/Analysis.vue index e335f68..5f4241c 100644 --- a/resources/js/pages/admin/Analysis.vue +++ b/resources/js/pages/admin/Analysis.vue @@ -529,11 +529,11 @@ const productBarData = computed(() => { function applyFilters() { router.get( - admin.analysis.url({ + admin.analysis.url(), + { start_date: startDate.value, end_date: endDate.value, - }), - {}, + }, { preserveState: true, preserveScroll: true,