selectRaw('SUM(stock) as total_stock, SUM(stock * price) as total_value') ->first(); $stockByUnit = 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') ->groupBy('raw_materials.unit') ->get() ->mapWithKeys(fn ($item) => [ $item->unit => (float) $item->total_stock, ]) ->toArray(); return [ 'total_stock' => (float) ($stockSummary->total_stock ?? 0), 'total_value' => (int) ($stockSummary->total_value ?? 0), 'by_unit' => $stockByUnit, ]; } public function getProductStock(): array { $variantSummary = ProductVariant::query() ->selectRaw('SUM(stock) as total_stock, SUM(reject_stock) as total_reject, COUNT(*) as total_variants') ->first(); $totalStock = (int) ($variantSummary->total_stock ?? 0); $totalValue = Cutting::query() ->whereNotNull('cost_per_unit') ->join('cutting_results', 'cuttings.id', '=', 'cutting_results.cutting_id') ->selectRaw('SUM(cutting_results.warehouse_stock * cuttings.cost_per_unit) as total_value') ->value('total_value'); $totalProducts = Product::query()->count(); $totalCategories = Category::query()->count(); return [ 'total_stock' => $totalStock, 'total_reject' => (int) ($variantSummary->total_reject ?? 0), 'total_value' => (int) ($totalValue ?? 0), 'total_variants' => (int) ($variantSummary->total_variants ?? 0), 'total_products' => $totalProducts, 'total_categories' => $totalCategories, ]; } public function getLowStockProducts(): array { return ProductVariant::query() ->where('stock', '<=', ProductVariant::minStock()) ->join('products', 'product_variants.product_id', '=', 'products.id') ->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, product_variants.stock") ->orderBy('stock') ->limit(5) ->get() ->map(fn ($item) => [ 'name' => $item->full_name, 'stock' => (int) $item->stock, ]) ->toArray(); } public function getLowStockMaterials(): array { return RawMaterialPrice::query() ->where('stock', '<=', 5) ->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id') ->selectRaw("CONCAT(raw_materials.name, ' - ', raw_material_prices.variant) as full_name, raw_material_prices.stock, raw_materials.unit") ->orderBy('stock') ->limit(5) ->get() ->map(fn ($item) => [ 'name' => $item->full_name, 'stock' => (float) $item->stock, 'unit' => $item->unit, ]) ->toArray(); } public function getTopSuppliers(): array { return Purchase::query() ->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(): array { return Order::query() ->completed() ->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 getTopProducts(): array { 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) ->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 getPurchaseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { $query = Purchase::query(); if ($startDate && $endDate) { $query->whereBetween('created_at', [$startDate, $endDate]); } $purchaseSummary = $query->selectRaw('COUNT(*) as total_purchases, SUM(total) as total_spent, SUM(discount) as total_discount') ->first(); return [ 'total_purchases' => (int) ($purchaseSummary->total_purchases ?? 0), 'total_spent' => (int) ($purchaseSummary->total_spent ?? 0), 'total_discount' => (int) ($purchaseSummary->total_discount ?? 0), ]; } public function getCuttingSummary(): array { $totalCost = Cutting::query() ->selectRaw('COALESCE(SUM(total_material_cost), 0) + COALESCE(SUM(sewing_cost), 0) + COALESCE(SUM(other_cost), 0) as total_cost') ->value('total_cost'); $totalResults = CuttingResult::query() ->selectRaw('SUM(cutting_result) as total_cutting, SUM(warehouse_stock) as total_warehouse, SUM(cutting_reject) as total_reject') ->first(); return [ 'total_cost' => (int) ($totalCost ?? 0), 'total_cutting' => (int) ($totalResults->total_cutting ?? 0), 'total_warehouse' => (int) ($totalResults->total_warehouse ?? 0), 'total_reject' => (int) ($totalResults->total_reject ?? 0), ]; } public function getCuttingByStatus(): array { return Cutting::query() ->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->get() ->map(fn ($item) => [ 'status' => $item->status instanceof CuttingStatus ? $item->status->value : $item->status, 'label' => $item->status instanceof CuttingStatus ? $item->status->label() : CuttingStatus::from($item->status)->label(), 'count' => (int) $item->count, ]) ->toArray(); } public function getOrderStats(): array { $byChannel = Order::query() ->selectRaw('channel, COUNT(*) as count, SUM(total_amount) as total') ->groupBy('channel') ->get() ->map(fn ($item) => [ 'channel' => $item->channel instanceof OrderChannel ? $item->channel->value : $item->channel, 'label' => $item->channel instanceof OrderChannel ? $item->channel->label() : OrderChannel::from($item->channel)->label(), 'count' => (int) $item->count, 'total' => (int) $item->total, ]); $byPaymentType = Order::query() ->selectRaw('payment_type, COUNT(*) as count, SUM(total_amount) as total') ->groupBy('payment_type') ->get() ->map(fn ($item) => [ 'payment_type' => $item->payment_type instanceof PaymentType ? $item->payment_type->value : $item->payment_type, 'label' => $item->payment_type instanceof PaymentType ? $item->payment_type->label() : PaymentType::from($item->payment_type)->label(), 'count' => (int) $item->count, 'total' => (int) $item->total, ]); $byMarketing = Order::query() ->whereNotNull('marketing_id') ->join('users', 'orders.marketing_id', '=', 'users.id') ->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id') ->selectRaw('COALESCE(user_profiles.full_name, users.username) as name, COUNT(*) as count, SUM(orders.total_amount) as total') ->groupBy('orders.marketing_id', 'users.username', 'user_profiles.full_name') ->orderByDesc('total') ->limit(5) ->get() ->map(fn ($item) => [ 'name' => $item->name, 'count' => (int) $item->count, 'total' => (int) $item->total, ]); $byStatus = Order::query() ->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->get() ->map(fn ($item) => [ 'status' => $item->status instanceof OrderStatus ? $item->status->value : $item->status, 'label' => $item->status instanceof OrderStatus ? $item->status->label() : OrderStatus::from($item->status)->label(), 'count' => (int) $item->count, ]); return [ 'by_channel' => $byChannel->toArray(), 'by_payment_type' => $byPaymentType->toArray(), 'by_marketing' => $byMarketing->toArray(), 'by_status' => $byStatus->toArray(), ]; } public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { $query = Order::query()->completed(); if ($startDate && $endDate) { $query->whereBetween('created_at', [$startDate, $endDate]); } $revenueSummary = $query->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order') ->first(); $feesQuery = Order::query()->completed()->whereNotNull('marketplace_settings_snapshot'); if ($startDate && $endDate) { $feesQuery->whereBetween('created_at', [$startDate, $endDate]); } $totalMarketplaceFees = $feesQuery->get() ->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); return [ 'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0), 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), 'total_marketplace_fees' => $totalMarketplaceFees, 'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, 'total_shipping' => (int) ($revenueSummary->total_shipping ?? 0), 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), ]; } public function getMarketplaceSummary(): array { $marketplaceOrders = Order::query() ->completed() ->whereIn('channel', [OrderChannel::SHOPEE, OrderChannel::TIKTOK]) ->whereNotNull('marketplace_settings_snapshot') ->get(); $totalRevenue = $marketplaceOrders->sum('total_amount'); $totalFees = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); $totalNet = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['net_amount'] ?? 0)); $totalOrders = $marketplaceOrders->count(); return [ 'total_revenue' => (int) $totalRevenue, 'total_fees' => (int) $totalFees, 'total_net' => (int) $totalNet, 'total_orders' => (int) $totalOrders, ]; } public function getCashAccounts(): array { return CashAccount::query() ->selectRaw('name, balance') ->get() ->map(fn ($item) => [ 'name' => $item->name, 'balance' => (int) $item->balance, ]) ->toArray(); } public function getCashSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { $query = CashTransaction::query(); if ($startDate && $endDate) { $query->whereBetween('created_at', [$startDate, $endDate]); } $summary = $query->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_transactions' => (int) ($summary->total_transactions ?? 0), 'total_deposit' => (int) ($summary->total_deposit ?? 0), 'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0), ]; } public function getMonthlyCashFlow(): array { $months = collect(); for ($monthOffset = 5; $monthOffset >= 0; $monthOffset--) { $date = Carbon::now()->subMonths($monthOffset); $months->push([ 'year' => $date->year, 'month' => $date->month, 'label' => $date->translatedFormat('M Y'), ]); } $result = $months->map(function ($month) { $start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth(); $end = $start->copy()->endOfMonth(); $monthlyTransactions = CashTransaction::query() ->whereBetween('created_at', [$start, $end]) ->selectRaw(" COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as deposits, COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as withdrawals ") ->first(); return [ 'label' => $month['label'], 'deposits' => (int) $monthlyTransactions->deposits, 'withdrawals' => (int) $monthlyTransactions->withdrawals, ]; }); return $result->toArray(); } public function getMonthlyExpenses(?Carbon $startDate = null, ?Carbon $endDate = null): array { $query = Expense::query(); if ($startDate && $endDate) { $query->whereBetween('created_at', [$startDate, $endDate]); } $monthlyExpenses = $query->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count') ->first(); return [ 'total' => (int) ($monthlyExpenses->total ?? 0), 'count' => (int) ($monthlyExpenses->count ?? 0), ]; } public function getKasbonSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { $pendingQuery = EmployeeAdvance::query()->pending(); $approvedQuery = EmployeeAdvance::query()->approved(); $paidQuery = EmployeeAdvance::query()->paid(); $employeeQuery = EmployeeAdvance::query(); if ($startDate && $endDate) { $pendingQuery->whereBetween('created_at', [$startDate, $endDate]); $approvedQuery->whereBetween('created_at', [$startDate, $endDate]); $paidQuery->whereBetween('created_at', [$startDate, $endDate]); $employeeQuery->whereBetween('created_at', [$startDate, $endDate]); } $pending = $pendingQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first(); $approved = $approvedQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first(); $paid = $paidQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first(); $totalEmployees = $employeeQuery->distinct('employee_id')->count('employee_id'); return [ 'pending' => [ 'count' => (int) ($pending->count ?? 0), 'total' => (int) ($pending->total ?? 0), ], 'approved' => [ 'count' => (int) ($approved->count ?? 0), 'total' => (int) ($approved->total ?? 0), ], 'paid' => [ 'count' => (int) ($paid->count ?? 0), 'total' => (int) ($paid->total ?? 0), ], 'total' => (int) ($pending->total ?? 0) + (int) ($approved->total ?? 0) + (int) ($paid->total ?? 0), 'total_employees' => $totalEmployees, ]; } public function getPayrollSummary(Carbon $startOfMonth, Carbon $endOfMonth): array { $period = PayrollPeriod::query() ->where('year', $startOfMonth->year) ->where('month', $startOfMonth->month) ->first(); if (! $period) { return [ 'has_period' => false, 'total_employees' => 0, 'total_amount' => 0, 'paid_amount' => 0, 'unpaid_amount' => 0, 'paid_count' => 0, 'unpaid_count' => 0, ]; } $payrolls = Payroll::query() ->where('payroll_period_id', $period->id) ->selectRaw(" COUNT(*) as total_employees, COALESCE(SUM(total_amount), 0) as total_amount, COALESCE(SUM(CASE WHEN status = 'paid' THEN total_amount ELSE 0 END), 0) as paid_amount, COALESCE(SUM(CASE WHEN status = 'unpaid' THEN total_amount ELSE 0 END), 0) as unpaid_amount, SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid_count, SUM(CASE WHEN status = 'unpaid' THEN 1 ELSE 0 END) as unpaid_count ") ->first(); return [ 'has_period' => true, 'period_status' => $period->status->value, 'total_employees' => (int) ($payrolls->total_employees ?? 0), 'total_amount' => (int) ($payrolls->total_amount ?? 0), 'paid_amount' => (int) ($payrolls->paid_amount ?? 0), 'unpaid_amount' => (int) ($payrolls->unpaid_amount ?? 0), 'paid_count' => (int) ($payrolls->paid_count ?? 0), 'unpaid_count' => (int) ($payrolls->unpaid_count ?? 0), ]; } public function getLeaveRequestSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array { $query = LeaveRequest::query(); if ($startDate && $endDate) { $query->where(function ($query) use ($startDate, $endDate) { $query->whereBetween('start_date', [$startDate, $endDate]) ->orWhereBetween('end_date', [$startDate, $endDate]); }); } $leaveRequestSummary = $query->selectRaw(" COUNT(*) as total, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count, SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count ") ->first(); return [ 'total' => (int) ($leaveRequestSummary->total ?? 0), 'pending' => (int) ($leaveRequestSummary->pending_count ?? 0), 'approved' => (int) ($leaveRequestSummary->approved_count ?? 0), 'rejected' => (int) ($leaveRequestSummary->rejected_count ?? 0), ]; } public function getEmployeeSummary(): array { $total = Employee::query()->count(); $byStatus = Employee::query() ->selectRaw('employment_status, COUNT(*) as count') ->groupBy('employment_status') ->get() ->map(fn ($item) => [ 'status' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->value : $item->employment_status, 'label' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->label() : EmploymentStatus::from($item->employment_status)->label(), 'count' => (int) $item->count, ]); return [ 'total' => $total, 'by_status' => $byStatus->toArray(), ]; } public function getAttendanceToday(?Carbon $startDate = null, ?Carbon $endDate = null): array { $totalEmployees = Employee::query()->count(); if ($startDate && $endDate) { $present = Attendance::query() ->whereBetween('attendance_date', [$startDate, $endDate]) ->count(); $onLeave = LeaveRequest::query() ->approved() ->where(function ($query) use ($startDate, $endDate) { $query->whereBetween('start_date', [$startDate, $endDate]) ->orWhereBetween('end_date', [$startDate, $endDate]); }) ->count(); $days = (int) $startDate->copy()->startOfDay()->diffInDays($endDate->copy()->startOfDay()) + 1; $totalPossible = $totalEmployees * $days; $absent = max(0, $totalPossible - $present - $onLeave); return [ 'total_employees' => (int) $totalPossible, 'present' => (int) $present, 'absent' => (int) $absent, 'on_leave' => (int) $onLeave, ]; } else { $today = Carbon::today(); $present = Attendance::query() ->where('attendance_date', $today) ->distinct('employee_id') ->count('employee_id'); $onLeave = LeaveRequest::query() ->approved() ->where('start_date', '<=', $today) ->where('end_date', '>=', $today) ->distinct('employee_id') ->count('employee_id'); $notPresent = max(0, $totalEmployees - $present); $absent = max(0, $notPresent - $onLeave); return [ 'total_employees' => $totalEmployees, 'present' => $present, 'absent' => $absent, 'on_leave' => $onLeave, ]; } } public function getMonthlyRevenueTrend(): array { $months = collect(); for ($monthOffset = 5; $monthOffset >= 0; $monthOffset--) { $date = Carbon::now()->subMonths($monthOffset); $months->push([ 'year' => $date->year, 'month' => $date->month, 'label' => $date->translatedFormat('M Y'), ]); } $result = $months->map(function ($month) { $start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth(); $end = $start->copy()->endOfMonth(); $monthlyRevenue = Order::query() ->completed() ->whereBetween('created_at', [$start, $end]) ->selectRaw('COALESCE(SUM(total_amount), 0) as total, COUNT(*) as count') ->first(); return [ 'label' => $month['label'], 'total' => (int) $monthlyRevenue->total, 'count' => (int) $monthlyRevenue->count, ]; }); return $result->toArray(); } public function getMonthlyPurchaseTrend(): array { $months = collect(); for ($monthOffset = 5; $monthOffset >= 0; $monthOffset--) { $date = Carbon::now()->subMonths($monthOffset); $months->push([ 'year' => $date->year, 'month' => $date->month, 'label' => $date->translatedFormat('M Y'), ]); } $result = $months->map(function ($month) { $start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth(); $end = $start->copy()->endOfMonth(); $monthlyPurchases = Purchase::query() ->whereBetween('created_at', [$start, $end]) ->selectRaw('COALESCE(SUM(total), 0) as total, COUNT(*) as count') ->first(); return [ 'label' => $month['label'], 'total' => (int) $monthlyPurchases->total, 'count' => (int) $monthlyPurchases->count, ]; }); return $result->toArray(); } }