startOfMonth(); $end = $endDate ? Carbon::parse($endDate) : Carbon::now(); if ($user && $this->isMarketingUser($user)) { $employee = $user->employee; if (! $employee) { return [ 'total_employees' => 0, 'present' => 0, 'absent' => 0, 'on_leave' => 0, 'percentage' => 0, ]; } $isWithinRange = $employee->join_date <= $end && (is_null($employee->resign_date) || $employee->resign_date >= $start); if (! $isWithinRange) { return [ 'total_employees' => 0, 'present' => 0, 'absent' => 0, 'on_leave' => 0, 'percentage' => 0, ]; } $present = Attendance::where('employee_id', $employee->id) ->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()]) ->distinct('employee_id') ->count('employee_id'); $onLeave = LeaveRequest::approved() ->where('start_date', '<=', $end) ->where('end_date', '>=', $start) ->where('employee_id', $employee->id) ->count(); $workingDays = $this->countWorkingDays($start, $end); $absent = max(0, $workingDays - $present - $onLeave); return [ 'total_employees' => 1, 'present' => $present, 'absent' => $absent, 'on_leave' => $onLeave, 'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0, ]; } $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(); $employeeIds = $employees->pluck('id'); $employeeCount = $employeeIds->count(); $present = Attendance::whereIn('employee_id', $employeeIds) ->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()]) ->distinct('employee_id') ->count('employee_id'); $onLeave = LeaveRequest::approved() ->where('start_date', '<=', $end) ->where('end_date', '>=', $start) ->whereIn('employee_id', $employeeIds) ->count(); $absent = max(0, $employeeCount - $present - $onLeave); return [ 'total_employees' => $employeeCount, 'present' => $present, 'absent' => $absent, 'on_leave' => $onLeave, 'percentage' => $employeeCount > 0 ? round(($present / $employeeCount) * 100) : 0, ]; } public function getMyAttendance(User $user, ?string $startDate, ?string $endDate): ?array { $employee = $user->employee; if (! $employee) { return null; } $start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth(); $end = $endDate ? Carbon::parse($endDate) : Carbon::now(); $workingDays = 0; $current = $start->copy(); while ($current->lte($end)) { if ($current->dayOfWeek !== Carbon::SUNDAY) { $workingDays++; } $current->addDay(); } $present = Attendance::where('employee_id', $employee->id) ->whereBetween('attendance_date', [$start, $end]) ->count(); $leaveDays = LeaveRequest::approved() ->where('employee_id', $employee->id) ->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; return $carry + max(0, $days); }, 0); $absent = max(0, $workingDays - $present - $leaveDays); return [ 'total_days' => $workingDays, 'present_days' => $present, 'absent_days' => $absent, 'leave_days' => $leaveDays, 'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0, ]; } public function getCashOverview(?string $startDate, ?string $endDate): array { $cashAccount = CashAccount::first(); if (! $cashAccount) { return [ 'total_balance' => 0, 'total_transactions' => 0, 'total_deposit' => 0, 'total_withdrawal' => 0, ]; } $transactions = $cashAccount->cashTransactions(); $this->applyDateFilter($transactions, $startDate, $endDate, 'cash_transactions.created_at'); $totalDeposit = (clone $transactions)->where('type', CashTransactionType::DEPOSIT)->sum('amount'); $totalWithdrawal = (clone $transactions)->where('type', CashTransactionType::WITHDRAWAL)->sum('amount'); return [ 'total_balance' => (int) $cashAccount->balance, 'total_transactions' => (clone $transactions)->count(), 'total_deposit' => (int) $totalDeposit, 'total_withdrawal' => (int) $totalWithdrawal, ]; } public function getRawMaterialStock(): array { $items = RawMaterialPrice::query() ->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id') ->select('raw_material_prices.stock', 'raw_material_prices.price', 'raw_materials.unit') ->get(); $totalQty = $items->sum('stock'); $totalPrice = $items->sum(fn ($i) => $i->stock * $i->price); $byUnit = [ 'yard' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::YARD->value)->sum('stock'), 'meter' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::METER->value)->sum('stock'), 'kilogram' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::KG->value)->sum('stock'), ]; return [ 'total_stock' => $totalQty, 'total_price' => $totalPrice, 'by_unit' => $byUnit, ]; } public function getProductStock(): array { $variants = ProductVariant::query() ->leftJoin('product_prices', function ($join) { $join->on('product_variants.id', '=', 'product_prices.variant_id') ->where('product_prices.type', '=', PriceType::RETAIL->value); }) ->select('product_variants.stock', 'product_variants.reject_stock', 'product_variants.retail_stock', 'product_prices.price') ->get(); $totalStock = $variants->sum('stock'); $totalRejectStock = $variants->sum('reject_stock'); $totalRetailStock = $variants->sum('retail_stock'); $totalPrice = $variants->sum(fn ($v) => ($v->stock + $v->reject_stock + $v->retail_stock) * ($v->price ?? 0)); return [ 'total_stock' => $totalStock + $totalRejectStock + $totalRetailStock, 'total_price' => $totalPrice, 'by_type' => [ 'stock' => $totalStock, 'reject_stock' => $totalRejectStock, 'retail_stock' => $totalRetailStock, ], ]; } public function getRevenueByStockType(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $stockQualitySubquery = OrderItem::select('order_id') ->selectRaw('MIN(stock_quality) as stock_quality') ->groupBy('order_id'); $monthly = (clone $query) ->toBase() ->joinSub($stockQualitySubquery, 'oi', fn ($join) => $join->on('orders.id', '=', 'oi.order_id')) ->selectRaw("DATE_FORMAT(orders.created_at, '%b %Y') as month") ->selectRaw('oi.stock_quality') ->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue') ->groupBy(DB::raw("DATE_FORMAT(orders.created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(orders.created_at, '%b %Y')"), 'oi.stock_quality') ->orderBy(DB::raw("DATE_FORMAT(orders.created_at, '%Y-%m')")) ->get(); $allMonths = []; $monthlyData = []; foreach ($monthly as $row) { $month = $row->month; if (! array_key_exists($month, $allMonths)) { $allMonths[$month] = $month; $monthlyData[$month] = ['month' => $month, 'good' => 0, 'reject' => 0, 'retail' => 0]; } $monthlyData[$month][$row->stock_quality] = (int) $row->total_revenue; } $result = []; foreach ($allMonths as $month => $_) { $result[] = $monthlyData[$month]; } $totals = (clone $query) ->toBase() ->joinSub($stockQualitySubquery, 'oi', fn ($join) => $join->on('orders.id', '=', 'oi.order_id')) ->selectRaw('oi.stock_quality') ->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue') ->groupBy('oi.stock_quality') ->get() ->keyBy('stock_quality'); return [ 'monthly' => $result, 'totals' => [ 'good' => (int) ($totals['good']->total_revenue ?? 0), 'reject' => (int) ($totals['reject']->total_revenue ?? 0), 'retail' => (int) ($totals['retail']->total_revenue ?? 0), ], ]; } public function getRevenueSummary(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $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(nego_price), 0) as total_deduction') ->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs') ->first(); $payrollQuery = Payroll::where('status', PayrollStatus::PAID); $this->applyDateFilter($payrollQuery, $startDate, $endDate, 'paid_at'); $payrollTotal = (int) $payrollQuery->sum('total_amount'); $expenseQuery = Expense::query(); $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); $expenseTotal = (int) $expenseQuery->sum('amount'); $totalRevenue = (int) $stats->total_revenue; $totalCogs = (int) $stats->total_cogs; $gross = $totalRevenue - $totalCogs; $net = $gross - $payrollTotal - $expenseTotal; return [ 'total_revenue' => $totalRevenue, 'total_discount' => (int) $stats->total_discount, 'total_deduction' => (int) $stats->total_deduction, 'cogs' => $totalCogs, 'gross' => $gross, 'net' => $net, 'payroll_total' => $payrollTotal, 'expense_total' => $expenseTotal, 'total_orders' => (int) $stats->total_orders, 'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0, ]; } public function getMonthlyRevenue(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->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() ->keyBy('month'); $payrollMonthly = Payroll::where('status', PayrollStatus::PAID); $this->applyDateFilter($payrollMonthly, $startDate, $endDate, 'paid_at'); $payrollByMonth = (clone $payrollMonthly)->toBase() ->selectRaw("DATE_FORMAT(paid_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as payroll') ->groupBy(DB::raw("DATE_FORMAT(paid_at, '%Y-%m')"), DB::raw("DATE_FORMAT(paid_at, '%b %Y')")) ->get() ->keyBy('month'); $expenseMonthly = Expense::query(); $this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at'); $expenseByMonth = (clone $expenseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as expense') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); $allMonths = []; foreach ([$monthly, $payrollByMonth, $expenseByMonth] as $data) { foreach ($data as $month => $row) { if (! array_key_exists($month, $allMonths)) { $allMonths[$month] = $month; } } } $result = []; foreach ($allMonths as $month => $_) { $total = (int) ($monthly[$month]->total ?? 0); $discount = (int) ($monthly[$month]->discount ?? 0); $deduction = (int) ($monthly[$month]->deduction ?? 0); $cogs = (int) ($monthly[$month]->cogs ?? 0); $payroll = (int) ($payrollByMonth[$month]->payroll ?? 0); $expense = (int) ($expenseByMonth[$month]->expense ?? 0); $gross = $total - $cogs; $net = $gross - $payroll - $expense; $result[] = [ 'month' => $month, 'total' => $total, 'gross' => $gross, 'net' => $net, 'discount' => $discount, 'deduction' => $deduction, 'cogs' => $cogs, 'payroll' => $payroll, 'expense' => $expense, ]; } return $result; } public function getMonthlyRetailRevenue(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $retailOrderIds = OrderItem::where('stock_quality', 'retail') ->pluck('order_id') ->unique(); $query->whereIn('orders.id', $retailOrderIds); $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->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() ->keyBy('month'); $summary = (clone $query) ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->selectRaw('COALESCE(SUM(discount), 0) as discount') ->selectRaw('COALESCE(SUM(nego_price), 0) as deduction') ->selectRaw('COALESCE(SUM(cogs), 0) as cogs') ->first(); $gross = (int) $summary->total - (int) $summary->cogs; $result = []; foreach ($monthly as $month => $row) { $total = (int) $row->total; $discount = (int) $row->discount; $deduction = (int) $row->deduction; $cogs = (int) $row->cogs; $itemGross = $total - $cogs; $result[] = [ 'month' => $month, 'total' => $total, 'gross' => $itemGross, 'discount' => $discount, 'deduction' => $deduction, 'cogs' => $cogs, ]; } return [ 'monthly' => $result, 'summary' => [ 'total' => (int) $summary->total, 'discount' => (int) $summary->discount, 'deduction' => (int) $summary->deduction, 'cogs' => (int) $summary->cogs, 'gross' => $gross, ], ]; } public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->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) => [ 'month' => $item->month, 'store' => (int) $item->store, 'shopee' => (int) $item->shopee, 'tiktok' => (int) $item->tiktok, ]); return $monthly->values()->toArray(); } public function getRevenueByPaymentType(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $data = (clone $query) ->select('payment_type') ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->groupBy('payment_type') ->get() ->map(fn ($item) => [ 'payment_type' => $item->payment_type, 'label' => $item->payment_type->label(), 'total' => (int) $item->total, ]); return $data->toArray(); } public function getExpenseSummary(?string $startDate, ?string $endDate, ?User $user = null): array { if ($user && $this->isMarketingUser($user)) { $employee = $user->employee; $expenseQuery = Expense::where('created_by_id', $user->id); $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); $advanceQuery = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID); if ($employee) { $advanceQuery->where('employee_id', $employee->id); } else { $advanceQuery->whereRaw('0 = 1'); } $this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at'); $expenseTotal = (clone $expenseQuery)->sum('amount'); $advanceTotal = (clone $advanceQuery)->sum('amount'); return [ 'total' => (int) ($expenseTotal + $advanceTotal), 'purchase_total' => 0, 'expense_total' => (int) $expenseTotal, 'advance_total' => (int) $advanceTotal, ]; } $expenseQuery = Expense::query(); $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); if ($user && $this->isCashierUser($user)) { $expenseQuery->whereIn('created_by_id', $this->getCashierUserIds()); } $advanceQuery = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID); $this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at'); $expenseTotal = (clone $expenseQuery)->sum('amount'); $advanceTotal = (clone $advanceQuery)->sum('amount'); $includePurchase = $user && $this->isPurchaseVisible($user); if ($includePurchase) { $purchaseQuery = Purchase::query(); $this->applyDateFilter($purchaseQuery, $startDate, $endDate, 'purchases.created_at'); $purchaseTotal = (clone $purchaseQuery)->sum('total'); } else { $purchaseTotal = 0; } return [ 'total' => (int) ($expenseTotal + $advanceTotal + $purchaseTotal), 'purchase_total' => (int) $purchaseTotal, 'expense_total' => (int) $expenseTotal, 'advance_total' => (int) $advanceTotal, ]; } public function getMonthlyExpense(?string $startDate, ?string $endDate, ?User $user = null): array { if ($user && $this->isMarketingUser($user)) { $employee = $user->employee; $expenseMonthly = Expense::where('created_by_id', $user->id); $this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at'); $expenseByMonth = (clone $expenseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as expense') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); $advanceMonthly = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID); if ($employee) { $advanceMonthly->where('employee_id', $employee->id); } else { $advanceMonthly->whereRaw('0 = 1'); } $this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at'); $advanceByMonth = (clone $advanceMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as advance') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); $allMonths = []; foreach ([$expenseByMonth, $advanceByMonth] as $data) { foreach ($data as $month => $row) { if (! array_key_exists($month, $allMonths)) { $allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0]; } } } foreach ($allMonths as $month => &$row) { $row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0); $row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0); $row['total'] = $row['expense'] + $row['advance']; } return array_values($allMonths); } $expenseMonthly = Expense::query(); $this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at'); if ($user && $this->isCashierUser($user)) { $expenseMonthly->whereIn('created_by_id', $this->getCashierUserIds()); } $expenseByMonth = (clone $expenseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as expense') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); $advanceMonthly = EmployeeAdvance::where('status', EmployeeAdvanceStatus::REPAID); $this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at'); $advanceByMonth = (clone $advanceMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(amount), 0) as advance') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); $includePurchase = $user && $this->isPurchaseVisible($user); if ($includePurchase) { $purchaseMonthly = Purchase::query(); $this->applyDateFilter($purchaseMonthly, $startDate, $endDate, 'purchases.created_at'); $purchaseByMonth = (clone $purchaseMonthly)->toBase() ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total), 0) as purchase') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->get() ->keyBy('month'); } else { $purchaseByMonth = collect(); } $allMonths = []; foreach ([$expenseByMonth, $purchaseByMonth, $advanceByMonth] as $data) { foreach ($data as $month => $row) { if (! array_key_exists($month, $allMonths)) { $allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0]; } } } foreach ($allMonths as $month => &$row) { $row['purchase'] = (int) ($purchaseByMonth[$month]->purchase ?? 0); $row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0); $row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0); $row['total'] = $row['expense'] + $row['advance'] + $row['purchase']; } return array_values($allMonths); } public function getBusyHours(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $hours = range(0, 23); $hourCounts = (clone $query) ->selectRaw('HOUR(created_at) as hour') ->selectRaw('COUNT(*) as orders') ->groupBy(DB::raw('HOUR(created_at)')) ->pluck('orders', 'hour') ->toArray(); return array_map(function ($h) use ($hourCounts) { return [ 'hour' => sprintf('%02d:00', $h), 'orders' => (int) ($hourCounts[$h] ?? 0), ]; }, $hours); } public function getProfitMetrics(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $stats = (clone $query) ->selectRaw('COUNT(*) as total_orders') ->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(cogs), 0) as hpp') ->first(); $totalProductsSold = (clone $query) ->join('order_items', 'orders.id', '=', 'order_items.order_id') ->sum('order_items.quantity'); $payrollQuery = Payroll::where('status', PayrollStatus::PAID); $this->applyDateFilter($payrollQuery, $startDate, $endDate, 'paid_at'); $payrollTotal = (int) $payrollQuery->sum('total_amount'); $expenseQuery = Expense::query(); $this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at'); $expenseTotal = (int) $expenseQuery->sum('amount'); $grossProfit = $stats->total_revenue - $stats->hpp; $netProfit = $grossProfit - $payrollTotal - $expenseTotal; $profitMargin = $stats->total_revenue > 0 ? round(($netProfit / $stats->total_revenue) * 100, 1) : 0; $aov = $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0; $itemsPerTransaction = $stats->total_orders > 0 ? round($totalProductsSold / $stats->total_orders, 1) : 0; return [ 'total_orders' => (int) $stats->total_orders, 'total_products_sold' => (int) $totalProductsSold, 'hpp' => (int) $stats->hpp, 'gross_profit' => (int) $grossProfit, 'net_profit' => (int) $netProfit, 'profit_margin' => $profitMargin, 'aov' => $aov, 'items_per_transaction' => $itemsPerTransaction, 'payroll_total' => $payrollTotal, 'expense_total' => $expenseTotal, ]; } public function getTopSuppliers(?string $startDate, ?string $endDate): array { $query = Purchase::query(); $this->applyDateFilter($query, $startDate, $endDate, 'purchases.created_at'); return (clone $query)->toBase() ->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id') ->select('suppliers.name') ->selectRaw('COALESCE(SUM(purchases.total), 0) as total_amount') ->selectRaw('COUNT(*) as purchase_count') ->groupBy('suppliers.name') ->orderByDesc('total_amount') ->limit(5) ->get() ->toArray(); } public function getTopCustomers(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id'); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } return (clone $query)->toBase() ->join('customers', 'orders.customer_id', '=', 'customers.id') ->select('customers.name') ->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_amount') ->selectRaw('COUNT(*) as order_count') ->groupBy('customers.name') ->orderByDesc('total_amount') ->limit(5) ->get() ->toArray(); } public function getTopProducts(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } return (clone $query)->toBase() ->join('order_items', 'orders.id', '=', 'order_items.order_id') ->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id') ->join('products', 'product_variants.product_id', '=', 'products.id') ->select('products.name') ->selectRaw('SUM(order_items.quantity) as total_qty') ->selectRaw('COALESCE(SUM(order_items.subtotal), 0) as total_revenue') ->groupBy('products.name') ->orderByDesc('total_qty') ->limit(5) ->get() ->toArray(); } public function getRevenueTrend(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } return (clone $query) ->join('order_items', 'orders.id', '=', 'order_items.order_id') ->selectRaw('DATE(orders.created_at) as date') ->selectRaw('SUM(order_items.quantity) as qty') ->groupBy(DB::raw('DATE(orders.created_at)')) ->orderBy(DB::raw('DATE(orders.created_at)')) ->get() ->map(fn ($item) => [ 'date' => $item->date, 'qty' => (int) $item->qty, ]) ->toArray(); } public function getMarketingSales(?string $startDate, ?string $endDate, ?User $user = null): array { $query = Order::where('orders.status', OrderStatus::COMPLETED) ->whereNotNull('orders.marketing_id'); $this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $query->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $query->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $orders = (clone $query) ->join('users', 'orders.marketing_id', '=', 'users.id') ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') ->select('orders.marketing_id', 'user_profiles.full_name as marketing_name') ->selectRaw('COUNT(DISTINCT orders.id) as total_orders') ->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('COALESCE(SUM(orders.nego_price), 0) as total_nego_price') ->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 + (int) $item->total_nego_price, 'avg_order' => $totalOrders > 0 ? (int) ($totalRevenue / $totalOrders) : 0, ]; })->toArray(); } public function getOrderStats(?string $startDate, ?string $endDate, ?User $user = null): array { $baseQuery = Order::query(); $this->applyDateFilter($baseQuery, $startDate, $endDate, 'orders.created_at'); if ($user && $this->isMarketingUser($user)) { $baseQuery->where('orders.marketing_id', $user->id); } if ($user && $this->isCashierUser($user)) { $baseQuery->whereIn('orders.created_by_id', $this->getCashierUserIds()); } $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() ->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 { if ($startDate) { $query->whereDate($dateColumn, '>=', $startDate); } if ($endDate) { $query->whereDate($dateColumn, '<=', $endDate); } } private function isMarketingUser(User $user): bool { return $user->hasAnyRole([ Role::MARKETING_OFFLINE->value, Role::MARKETING_ONLINE->value, ]); } private function isCashierUser(User $user): bool { return $user->hasRole(Role::CASHIER->value); } private function getCashierUserIds(): Collection { return User::whereHas('roles', fn ($q) => $q->where('name', Role::CASHIER->value))->pluck('id'); } private function isPurchaseVisible(User $user): bool { return $user->hasAnyRole([ Role::DEVELOPER->value, Role::OWNER->value, Role::ADMIN_BAHAN_BAKU->value, ]); } private function applyMarketingFilter(Builder $query, User $user, string $column = 'marketing_id'): Builder { if ($this->isMarketingUser($user)) { $query->where($column, $user->id); } return $query; } private function countWorkingDays(Carbon $start, Carbon $end): int { $workingDays = 0; $current = $start->copy(); while ($current->lte($end)) { if ($current->dayOfWeek !== Carbon::SUNDAY) { $workingDays++; } $current->addDay(); } return $workingDays; } }