startOfMonth(); $end = $endDate ? Carbon::parse($endDate) : Carbon::now(); $workingDays = 0; $current = $start->copy(); while ($current->lte($end)) { if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) { $workingDays++; } $current->addDay(); } $totalEmployees = Employee::whereHas('user', fn($q) => $q->where('is_active', true))->count(); $present = Attendance::whereBetween('attendance_date', [$start, $end])->count(); $leaveDays = LeaveRequest::approved() ->where('start_date', '<=', $end) ->where('end_date', '>=', $start) ->get() ->reduce(function ($carry, $leave) use ($start, $end) { $leaveStart = max($leave->start_date->timestamp, $start->timestamp); $leaveEnd = min($leave->end_date->timestamp, $end->timestamp); $days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1; return $carry + max(0, $days); }, 0); $absent = max(0, $workingDays - $present - $leaveDays); return [ 'total_employees' => $totalEmployees, 'present' => $present, 'absent' => $absent, 'on_leave' => $leaveDays, 'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 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 (! in_array($current->dayOfWeek, [Carbon::SATURDAY, 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(): array { $cashAccount = CashAccount::first(); if (! $cashAccount) { return [ 'total_balance' => 0, 'total_transactions' => 0, 'total_deposit' => 0, 'total_withdrawal' => 0, ]; } $transactions = $cashAccount->cashTransactions(); return [ 'total_balance' => $cashAccount->balance, 'total_transactions' => (clone $transactions)->count(), 'total_deposit' => (int) (clone $transactions)->where('type', 'deposit')->sum('amount'), 'total_withdrawal' => (int) (clone $transactions)->where('type', 'withdrawal')->sum('amount'), ]; } public function getRawMaterialStock(): array { $prices = RawMaterialPrice::select('stock', 'price', 'raw_material_id') ->with('rawMaterial:id,unit') ->get(); $totalStock = $prices->sum('stock'); $totalValue = $prices->sum(fn($p) => $p->stock * $p->price); $byUnit = [ 'yard' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'), 'meter' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'), 'kilogram' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'), ]; return [ 'total_stock' => $totalStock, 'total_value' => $totalValue, 'by_unit' => $byUnit, ]; } public function getProductStock(): array { $variants = ProductVariant::select('id', 'product_id', 'stock', 'reject_stock', 'retail_stock') ->with(['product:id,name', 'productPrices' => fn ($q) => $q->where('type', PriceType::CAPITAL)]) ->get(); $totalStock = $variants->sum('stock'); $totalReject = $variants->sum('reject_stock'); $totalRetail = $variants->sum('retail_stock'); $totalValue = $variants->sum(function ($v) { $capitalPrice = $v->productPrices->first()?->price ?? 0; return $v->stock * $capitalPrice; }); return [ 'total_stock' => $totalStock, 'total_reject' => $totalReject, 'total_retail' => $totalRetail, 'total_value' => $totalValue, 'total_products' => ProductVariant::distinct('product_id')->count('product_id'), 'total_variants' => ProductVariant::count(), 'total_categories' => DB::table('product_categories')->distinct('category_id')->count('category_id'), ]; } public function getRevenueSummary(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); $stats = (clone $query) ->selectRaw('COUNT(*) as total_orders') ->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(discount), 0) as total_discount') ->selectRaw('COALESCE(SUM(discount), 0) as total_deduction') ->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net') ->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse") ->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail") ->first(); return [ 'total_revenue' => (int) $stats->total_revenue, 'total_discount' => (int) $stats->total_discount, 'total_deduction' => (int) $stats->total_deduction, 'net' => (int) $stats->net, 'net_warehouse' => (int) $stats->net_warehouse, 'net_retail' => (int) $stats->net_retail, '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): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw('COALESCE(SUM(total_amount), 0) as total') ->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net') ->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse") ->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail") ->selectRaw('COALESCE(SUM(discount), 0) as deduction') ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')")) ->get() ->map(fn($item) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction'])); return $monthly->values()->toArray(); } public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); $monthly = (clone $query) ->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month") ->selectRaw("SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END) as store") ->selectRaw("SUM(CASE WHEN channel = 'shopee' THEN total_amount ELSE 0 END) as shopee") ->selectRaw("SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END) as tiktok") ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')")) ->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')")) ->get() ->map(fn($item) => $item->only(['month', 'store', 'shopee', 'tiktok'])); return $monthly->values()->toArray(); } public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); $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): array { $expenseQuery = Expense::query(); $this->applyDateFilter($expenseQuery, $startDate, $endDate); $advanceQuery = EmployeeAdvance::where('status', 'paid'); $this->applyDateFilter($advanceQuery, $startDate, $endDate); $purchaseQuery = Purchase::query(); $this->applyDateFilter($purchaseQuery, $startDate, $endDate); $expenseTotal = (clone $expenseQuery)->sum('amount'); $advanceTotal = (clone $advanceQuery)->sum('amount'); $purchaseTotal = (clone $purchaseQuery)->sum('total'); return [ 'total' => $expenseTotal + $advanceTotal + $purchaseTotal, 'purchase_total' => $purchaseTotal, 'expense_total' => $expenseTotal, 'advance_total' => $advanceTotal, ]; } public function getMonthlyExpense(?string $startDate, ?string $endDate): array { $expenseMonthly = Expense::query(); $this->applyDateFilter($expenseMonthly, $startDate, $endDate); $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'); $purchaseMonthly = Purchase::query(); $this->applyDateFilter($purchaseMonthly, $startDate, $endDate); $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'); $advanceMonthly = EmployeeAdvance::where('status', 'paid'); $this->applyDateFilter($advanceMonthly, $startDate, $endDate); $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, $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['purchase'] + $row['expense'] + $row['advance']; } return array_values($allMonths); } public function getBusyHours(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); $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): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); $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'); $grossProfit = $stats->total_revenue - $stats->hpp; $netProfit = $grossProfit; $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, ]; } public function getTopSuppliers(?string $startDate, ?string $endDate): array { $query = Purchase::query(); $this->applyDateFilter($query, $startDate, $endDate); 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): array { $query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id'); $this->applyDateFilter($query, $startDate, $endDate); 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): array { $query = Order::where('orders.status', OrderStatus::COMPLETED); $this->applyDateFilter($query, $startDate, $endDate); 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 getMarketingSales(?string $startDate, ?string $endDate): array { $query = Order::where('orders.status', OrderStatus::COMPLETED) ->whereNotNull('orders.marketing_id'); $this->applyDateFilter($query, $startDate, $endDate); return (clone $query)->toBase() ->join('users', 'orders.marketing_id', '=', 'users.id') ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') ->leftJoin('order_items', 'orders.id', '=', 'order_items.order_id') ->select('user_profiles.full_name as marketing_name') ->selectRaw('COUNT(DISTINCT orders.id) as total_orders') ->selectRaw('COALESCE(SUM(order_items.quantity), 0) as total_products_sold') ->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue') ->selectRaw('COALESCE(SUM(orders.subtotal), 0) as total_subtotal') ->selectRaw('COALESCE(SUM(orders.discount), 0) as total_discount') ->selectRaw('ROUND(COALESCE(SUM(orders.total_amount), 0) / COUNT(DISTINCT orders.id)) as avg_order') ->groupBy('user_profiles.full_name') ->orderByDesc('total_revenue') ->get() ->toArray(); } 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); } } }