visibleTo($user); } $this->applyFilters($query, $filters, excludeDateFilter: true); return $query->sum('entitlement'); } /** * Get total KJC asset holdings */ public function getTotalHoldings(array $filters = [], $user = null): int { $query = KJCAssetEntitlement::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters, excludeDateFilter: true); return $query->sum('holding'); } /** * Get KJC asset holdings by status. * Does not filter by date so totals match full KJCAssetHolding counts (unit/government/formation filters still apply). */ public function getHoldingsByStatus(array $filters = [], $user = null): array { $query = KJCAssetHolding::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters, excludeDateFilter: true); return $query->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->get() ->toArray(); } /** * Get KJC asset holdings with pagination */ public function getHoldingsPaginated(array $filters = [], int $perPage = 20, $user = null): array { $query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); $holdings = $query->paginate($perPage); return [ 'items' => $holdings->items(), 'pagination' => [ 'current_page' => $holdings->currentPage(), 'per_page' => $holdings->perPage(), 'total' => $holdings->total(), 'last_page' => $holdings->lastPage(), 'from' => $holdings->firstItem(), 'to' => $holdings->lastItem() ] ]; } /** * Get KJC asset entitlements with pagination */ public function getEntitlementsPaginated(array $filters = [], int $perPage = 20, $user = null): array { $query = KJCAssetEntitlement::with(['kjcSubcategory']); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); $entitlements = $query->paginate($perPage); return [ 'items' => $entitlements->items(), 'pagination' => [ 'current_page' => $entitlements->currentPage(), 'per_page' => $entitlements->perPage(), 'total' => $entitlements->total(), 'last_page' => $entitlements->lastPage(), 'from' => $entitlements->firstItem(), 'to' => $entitlements->lastItem() ] ]; } /** * Get KJC asset holdings by specific status */ public function getHoldingsBySpecificStatus(string $status, array $filters = [], $user = null): array { $query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']) ->where('status', $status); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); $holdings = $query->paginate(20); return [ 'items' => $holdings->items(), 'pagination' => [ 'current_page' => $holdings->currentPage(), 'per_page' => $holdings->perPage(), 'total' => $holdings->total(), 'last_page' => $holdings->lastPage(), 'from' => $holdings->firstItem(), 'to' => $holdings->lastItem() ] ]; } /** * Get KJC asset details by status (paginated). * When status is '' or 'null', returns assets where status column is NULL (not yet assigned). */ public function getAssetDetailsByStatus(string $status, array $filters = [], $user = null): array { $status = trim($status ?? ''); $query = KJCAssetHolding::query() ->with(['kjcCategory', 'kjcSubcategory', 'kjcModel', 'unit']) ->select([ 'id', 'uuid', 'kjc_category_id', 'kjc_subcategory_id', 'kjc_model_id', 'registration_number', 'kewpa_registration_number', 'status', 'unit_id', 'purchase_date', 'receipt_date', 'purchase_price', 'contract_reference', 'economic_year', 'economic_distance', 'note', 'engine_number', 'chassis_number', 'created_at', 'updated_at' ]); if ($status === '' || $status === 'null') { $query->whereNull('status'); } else { $query->whereNotNull('status') ->where('status', '=', $status); } if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters, excludeDateFilter: true); $perPage = isset($filters['per_page']) ? (int) $filters['per_page'] : 10; $page = isset($filters['page']) ? (int) $filters['page'] : null; $assets = $query->paginate($perPage, ['*'], 'page', $page); return [ 'data' => $assets->items(), 'pagination' => [ 'current_page' => $assets->currentPage(), 'per_page' => $assets->perPage(), 'total' => $assets->total(), 'last_page' => $assets->lastPage(), 'from' => $assets->firstItem(), 'to' => $assets->lastItem(), 'has_more_pages' => $assets->hasMorePages() ] ]; } /** * Get KJC asset holdings grouped by unit */ public function getHoldingsByUnit(array $filters = [], $user = null): array { $query = KJCAssetHolding::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); return $query->join('units', 'kjc_asset_holdings.unit_id', '=', 'units.id') ->selectRaw('units.name as unit_name, COUNT(*) as count') ->groupBy('units.name') ->get() ->toArray(); } /** * Get KJC asset holdings monthly trends */ public function getMonthlyTrends(array $filters = [], $user = null): array { $dateRange = $this->getDateRange($filters); $query = KJCAssetHolding::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); return $query->selectRaw(' DATE_FORMAT(created_at, "%Y-%m") as month, status, COUNT(*) as count ') ->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]) ->groupBy('month', 'status') ->orderBy('month') ->get() ->toArray(); } /** * Get KJC historical metrics (KEUPAYAAN, KESIAGAAN, SERVISIBILITI) by month * 1. KEUPAYAAN (%) = (PEGANGAN/PERJAWATAN) * 100 * 2. SIAPSIAGA or KESIAGAAN (%) = (BDG/perjawatan) * 100 * 3. SERVISIBILITI (%) = (BDG/pegangan) * 100 */ public function getHistoricalMetricsByMonth(array $filters = [], $user = null): array { $dateRange = $this->getDateRange($filters); $currentMonth = Carbon::now()->format('Y-m'); // Get entitlement and holding data by month from historical tables using Eloquent $entitlementQuery = KJCHistoricalEntitlement::query() ->selectRaw(' TO_CHAR(kjc_historical_entitlement.date, \'YYYY-MM\') as month, SUM(kjc_historical_entitlement.entitlement) as total_entitlement, SUM(kjc_historical_entitlement.holding) as total_holding ') ->whereBetween('kjc_historical_entitlement.date', [$dateRange['from'], $dateRange['to']]); // Apply filters to historical entitlement query $this->applyHistoricalFiltersToEloquent($entitlementQuery, $filters); // Apply visibility scoping if user is provided (uses HasVisibility trait) if ($user) { $entitlementQuery->visibleTo($user); } $entitlementData = $entitlementQuery->groupBy('month') ->orderBy('month') ->get() ->keyBy('month'); // Get BDG status count by month from historical tables using Eloquent $bdgQuery = KJCHistoricalHoldingStatus::query() ->selectRaw(' TO_CHAR(kjc_historical_holding_status.date, \'YYYY-MM\') as month, COUNT(*) as bdg_count ') ->where('kjc_historical_holding_status.status', 'BDG') ->whereBetween('kjc_historical_holding_status.date', [$dateRange['from'], $dateRange['to']]); // Apply filters to historical holding status query $this->applyHistoricalFiltersToEloquent($bdgQuery, $filters); // Apply visibility scoping if user is provided (uses HasVisibility trait) if ($user) { $bdgQuery->visibleTo($user); } $bdgData = $bdgQuery->groupBy('month') ->orderBy('month') ->get() ->keyBy('month'); // Get current month data from real-time tables if current month is in range $currentMonthEntitlement = 0; $currentMonthHolding = 0; $currentMonthBdgCount = 0; if ($this->isCurrentMonthInRange($currentMonth, $dateRange)) { // Get current month entitlement data (all entitlements, not filtered by updated_at) // This ensures we get real-time entitlement values regardless of when they were last updated $currentEntitlementQuery = KJCAssetEntitlement::query(); if ($user) { $currentEntitlementQuery->visibleTo($user); } $this->applyCurrentMonthFilters($currentEntitlementQuery, $filters); $currentMonthEntitlement = $currentEntitlementQuery->sum('entitlement'); // Calculate current month holdings from actual KJCAssetHolding records // This ensures we get real-time holding counts even if entitlement.holding is stale $currentHoldingQuery = KJCAssetHolding::query(); if ($user) { $currentHoldingQuery->visibleTo($user); } $this->applyCurrentMonthFilters($currentHoldingQuery, $filters); $currentMonthHolding = $currentHoldingQuery->count(); // Get current month BDG count (only holdings updated in current month) // This shows BDG holdings that were updated/changed status in the current month $currentBdgQuery = KJCAssetHolding::query() ->where('status', 'BDG') ->whereMonth('updated_at', Carbon::now()->month) ->whereYear('updated_at', Carbon::now()->year); if ($user) { $currentBdgQuery->visibleTo($user); } $this->applyCurrentMonthFilters($currentBdgQuery, $filters); $currentMonthBdgCount = $currentBdgQuery->count(); } // Generate all months in the range $months = []; $current = $dateRange['from']->copy()->startOfMonth(); $end = $dateRange['to']->copy()->endOfMonth(); while ($current->lte($end)) { $monthKey = $current->format('Y-m'); $months[] = $monthKey; $current->addMonth(); } // Calculate metrics for each month $result = []; foreach ($months as $month) { $entitlement = $entitlementData->get($month); $bdg = $bdgData->get($month); // Use current month real-time data if this is the current month if ($month === $currentMonth && $this->isCurrentMonthInRange($currentMonth, $dateRange)) { $totalEntitlement = $currentMonthEntitlement; $totalHolding = $currentMonthHolding; $bdgCount = $currentMonthBdgCount; } else { $totalEntitlement = $entitlement ? $entitlement->total_entitlement : 0; $totalHolding = $entitlement ? $entitlement->total_holding : 0; $bdgCount = $bdg ? $bdg->bdg_count : 0; } // Calculate metrics $keupayaan = $totalEntitlement > 0 ? ($totalHolding / $totalEntitlement) * 100 : 0; $kesiagaan = $totalEntitlement > 0 ? ($bdgCount / $totalEntitlement) * 100 : 0; $servisibiliti = $totalHolding > 0 ? ($bdgCount / $totalHolding) * 100 : 0; $result[] = [ 'month' => $month, 'keupayaan' => round($keupayaan, 2), 'kesiagaan' => round($kesiagaan, 2), 'servisibiliti' => round($servisibiliti, 2), 'total_entitlement' => $totalEntitlement, 'total_holding' => $totalHolding, 'bdg_count' => $bdgCount ]; } return $result; } /** * Apply common filters to query */ private function applyFilters($query, array $filters = [], bool $excludeDateFilter = false): void { $model = $query->getModel(); if ($model && method_exists($model, 'getTable')) { $schema = $model->getConnection()->getSchemaBuilder(); $columns = $schema->getColumnListing($model->getTable()); // Only apply created_at filter if the model has created_at column (skip when excludeDateFilter) if (!$excludeDateFilter && in_array('created_at', $columns)) { $dateRange = $this->getDateRange($filters); $query->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]); } // Apply unit_id filter if model has unit_id column if (isset($filters['unit_id']) && $filters['unit_id']) { if (in_array('unit_id', $columns)) { $query->where('unit_id', $filters['unit_id']); } } // Apply government_id filter if provided if (isset($filters['government_id']) && $filters['government_id']) { $unitIds = $this->getUnitsByGovernment($filters['government_id']); if (in_array('unit_id', $columns)) { if (!empty($unitIds)) { $query->whereIn('unit_id', $unitIds); } else { // No units found for this government - return empty result $query->whereNull('id'); } } } // Apply camp_id filter if (isset($filters['camp_id']) && $filters['camp_id']) { $query->whereHas('unit', function ($q) use ($filters) { $q->where('camp_id', $filters['camp_id']); }); } // Apply formation_id filter if (isset($filters['formation_id']) && $filters['formation_id']) { // Get units under this formation $unitIds = Unit::where('formation_id', $filters['formation_id']) ->pluck('id') ->toArray(); if (!empty($unitIds)) { if (in_array('unit_id', $columns)) { $query->whereIn('unit_id', $unitIds); } else { // Fallback to whereHas if no unit_id column $query->whereHas('unit', function ($q) use ($filters) { $q->where('formation_id', $filters['formation_id']); }); } } else { // No units found for this formation - return empty result $query->whereNull('id'); } } } } /** * Get units by government ID * Uses VisibilityService for consistent logic */ private function getUnitsByGovernment(int $governmentId): array { $governmentUnit = Unit::where('government_id', $governmentId)->first(); if (!$governmentUnit) { // If no direct unit found, try to get from formation $formation = Formation::where('government_id', $governmentId)->first(); if ($formation) { $governmentUnit = Unit::where('formation_id', $formation->id)->first(); } } if (!$governmentUnit) { return []; } return $this->visibilityService->getUnitsUnderGovernment($governmentUnit); } /** * Get date range for filtering */ private function getDateRange(array $filters = []): array { $dateFrom = $filters['date_from'] ?? Carbon::now()->startOfYear(); $dateTo = $filters['date_to'] ?? Carbon::now()->endOfYear(); return [ 'from' => Carbon::parse($dateFrom)->startOfDay(), 'to' => Carbon::parse($dateTo)->endOfDay() ]; } /** * Apply filters to historical Eloquent queries * Handles government_id, formation_id, unit_id, and camp_id filters */ private function applyHistoricalFiltersToEloquent($query, array $filters = []): void { $model = $query->getModel(); $isHoldingStatus = $model instanceof KJCHistoricalHoldingStatus; // Apply unit_id filter if (isset($filters['unit_id']) && $filters['unit_id']) { if ($isHoldingStatus) { // For holding status, filter through the relationship $query->whereHas('kjcAssetHolding', function ($q) use ($filters) { $q->where('unit_id', $filters['unit_id']); }); } else { $query->where('unit_id', $filters['unit_id']); } } // Apply government_id filter if (isset($filters['government_id']) && $filters['government_id']) { $unitIds = $this->getUnitsByGovernment($filters['government_id']); if (!empty($unitIds)) { if ($isHoldingStatus) { $query->whereHas('kjcAssetHolding', function ($q) use ($unitIds) { $q->whereIn('unit_id', $unitIds); }); } else { $query->whereIn('unit_id', $unitIds); } } else { // No units found for this government - return empty result $query->whereRaw('1 = 0'); } } // Apply formation_id filter if (isset($filters['formation_id']) && $filters['formation_id']) { // Get units under this formation $unitIds = Unit::where('formation_id', $filters['formation_id']) ->pluck('id') ->toArray(); if (!empty($unitIds)) { if ($isHoldingStatus) { $query->whereHas('kjcAssetHolding', function ($q) use ($unitIds) { $q->whereIn('unit_id', $unitIds); }); } else { $query->whereIn('unit_id', $unitIds); } } else { // No units found for this formation - return empty result $query->whereRaw('1 = 0'); } } // Apply camp_id filter if (isset($filters['camp_id']) && $filters['camp_id']) { if ($isHoldingStatus) { $query->whereHas('kjcAssetHolding.unit', function ($q) use ($filters) { $q->where('camp_id', $filters['camp_id']); }); } else { $query->whereHas('unit', function ($q) use ($filters) { $q->where('camp_id', $filters['camp_id']); }); } } } /** * Check if current month is within the date range */ private function isCurrentMonthInRange(string $currentMonth, array $dateRange): bool { $currentMonthDate = Carbon::createFromFormat('Y-m', $currentMonth)->startOfMonth(); return $currentMonthDate->between($dateRange['from'], $dateRange['to']); } /** * Apply filters for current month data queries (without date range filtering) */ private function applyCurrentMonthFilters($query, array $filters = []): void { if (isset($filters['unit_id']) && $filters['unit_id']) { $query->where('unit_id', $filters['unit_id']); } // Apply government_id filter if (isset($filters['government_id']) && $filters['government_id']) { $unitIds = $this->getUnitsByGovernment($filters['government_id']); if (!empty($unitIds)) { $query->whereIn('unit_id', $unitIds); } else { // No units found for this government - return empty result $query->whereNull('id'); } } if (isset($filters['camp_id']) && $filters['camp_id']) { $query->whereHas('unit', function ($q) use ($filters) { $q->where('camp_id', $filters['camp_id']); }); } if (isset($filters['formation_id']) && $filters['formation_id']) { // Get units under this formation $unitIds = Unit::where('formation_id', $filters['formation_id']) ->pluck('id') ->toArray(); if (!empty($unitIds)) { $query->whereIn('unit_id', $unitIds); } else { // No units found for this formation - return empty result $query->whereNull('id'); } } } }