visibleTo($user); } $this->applyFilters($query, $filters, excludeDateFilter: true); return $query->sum('entitlement'); } public function getTotalHoldings(array $filters = [], $user = null): int { $query = PKJAssetEntitlement::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters, excludeDateFilter: true); return $query->sum('holding') ?? 0; } /** * Get PKJ asset holdings grouped by status */ public function getHoldingsByStatus(array $filters = [], $user = null): array { $query = PKJAssetHolding::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); return $query->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->orderBy('status') ->get() ->toArray(); } /** * Get PKJ asset holdings by specific status */ public function getHoldingsBySpecificStatus(string $status, array $filters = [], $user = null): array { $query = PKJAssetHolding::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $this->applyFilters($query, $filters); $holdings = $query->where('status', $status)->paginate(10); return [ 'data' => $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 PKJ monthly trends */ public function getMonthlyTrends(array $filters = [], $user = null): array { $dateRange = $this->getDateRange($filters); $query = PKJReport::query(); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } $trends = $query->selectRaw(' TO_CHAR(created_at, \'YYYY-MM\') as month, COUNT(*) as total_reports ') ->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]) ->groupBy('month') ->orderBy('month') ->get() ->toArray(); return $trends; } /** * Get PKJ holdings by category */ public function getHoldingsByCategory(array $filters = [], $user = null): array { // Build a subquery for filtered entitlements // This ensures all categories are included, but only matching entitlements are summed $entitlementSubquery = PKJAssetEntitlement::query() ->select('pkj_category_id', DB::raw('SUM(holding) as total_holding')) ->groupBy('pkj_category_id'); // Apply visibility scoping if user is provided if ($user) { $entitlementSubquery->visibleTo($user); } // Apply filters to entitlement subquery if (isset($filters['unit_id'])) { $entitlementSubquery->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)) { $entitlementSubquery->whereIn('unit_id', $unitIds); } else { // If no units match, set to empty result but don't filter categories $entitlementSubquery->whereRaw('1 = 0'); } } // Apply formation_id filter if (isset($filters['formation_id']) && $filters['formation_id']) { $unitIds = Unit::where('formation_id', $filters['formation_id']) ->pluck('id') ->toArray(); if (!empty($unitIds)) { $entitlementSubquery->whereIn('unit_id', $unitIds); } else { // If no units match, set to empty result but don't filter categories $entitlementSubquery->whereRaw('1 = 0'); } } // Main query: Get ALL categories and LEFT JOIN with filtered entitlements // This ensures all categories are returned even if they have no matching entitlements $query = PKJCategory::query() ->leftJoinSub($entitlementSubquery, 'filtered_entitlements', function ($join) { $join->on('pkj_categories.id', '=', 'filtered_entitlements.pkj_category_id'); }) ->selectRaw(' pkj_categories.id as category_id, pkj_categories.name as category_name, COALESCE(filtered_entitlements.total_holding, 0) as total_holding ') ->orderBy('pkj_categories.name'); return $query->get()->toArray(); } /** * Get PKJ category metrics (KEUPAYAAN, KESIAGAAN, SERVISIBILITI) */ public function getCategoryMetrics(int $categoryId, array $filters = [], $user = null): array { // Get category name $category = PKJCategory::find($categoryId); $categoryName = $category ? $category->name : 'Unknown Category'; // Get entitlement and holding data for the category $entitlementQuery = PKJAssetEntitlement::query() ->where('pkj_category_id', $categoryId); // Apply visibility scoping if user is provided if ($user) { $entitlementQuery->visibleTo($user); } // Apply filters if (isset($filters['unit_id'])) { $entitlementQuery->where('unit_id', $filters['unit_id']); } $entitlementData = $entitlementQuery->selectRaw(' SUM(entitlement) as total_entitlement, SUM(holding) as total_holding ')->first(); // Get operational/usable assets count (BDG, BT, BP statuses) for the category $operationalQuery = PKJAssetHolding::query() ->where('pkj_category_id', $categoryId) ->whereIn('status', ['BDG', 'BT', 'BP']); // Apply visibility scoping if user is provided if ($user) { $operationalQuery->visibleTo($user); } // Apply filters if (isset($filters['unit_id'])) { $operationalQuery->where('unit_id', $filters['unit_id']); } $operationalCount = $operationalQuery->count(); // Get BDG, BT, BG count specifically for servisibiliti calculation $bdgQuery = PKJAssetHolding::query() ->where('pkj_category_id', $categoryId) ->whereIn('status', ['BDG', 'BT', 'BG']); // Apply visibility scoping if user is provided if ($user) { $bdgQuery->visibleTo($user); } // Apply filters if (isset($filters['unit_id'])) { $bdgQuery->where('unit_id', $filters['unit_id']); } $bdgCount = $bdgQuery->count(); $totalEntitlement = $entitlementData ? $entitlementData->total_entitlement : 0; $totalHolding = $entitlementData ? $entitlementData->total_holding : 0; // Calculate metrics $keupayaan = $totalEntitlement > 0 ? ($totalHolding / $totalEntitlement) * 100 : 0; $kesiagaan = $totalEntitlement > 0 ? ($operationalCount / $totalEntitlement) * 100 : 0; $servisibiliti = $totalHolding > 0 ? ($bdgCount / $totalHolding) * 100 : 0; return [ 'category_id' => $categoryId, 'category_name' => $categoryName, 'keupayaan' => round($keupayaan, 2), 'kesiagaan' => round($kesiagaan, 2), 'servisibiliti' => round($servisibiliti, 2), 'total_entitlement' => $totalEntitlement, 'total_holding' => $totalHolding, 'operational_count' => $operationalCount, 'bdg_count' => $bdgCount ]; } /** * Get PKJ category status breakdown */ public function getCategoryStatusBreakdown(int $categoryId, array $filters = [], $user = null): array { // Get category name $category = PKJCategory::find($categoryId); $categoryName = $category ? $category->name : 'Unknown Category'; $query = PKJAssetHolding::query() ->where('pkj_category_id', $categoryId) ->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->orderBy('status'); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } // Apply filters if (isset($filters['unit_id'])) { $query->where('unit_id', $filters['unit_id']); } $statusCounts = $query->get()->toArray(); return [ 'category_id' => $categoryId, 'category_name' => $categoryName, 'status_breakdown' => $statusCounts ]; } /** * Get PKJ asset details by category and status. * * When the asset has no status yet, the status column in the database is NULL. * Callers pass status as '' or the string 'null' to request these "no status" assets. */ public function getAssetDetailsByCategoryAndStatus(int $categoryId, string $status, array $filters = [], $user = null): array { // Handle no-status: API passes '' or 'null'; DB column is NULL for assets not yet assigned a status $status = trim($status ?? ''); $query = PKJAssetHolding::query() ->with(['unit', 'pkjCategory', 'pkjSubcategory', 'pkjModel']) ->where('pkj_category_id', $categoryId); if ($status === '' || $status === 'null') { // Assets with status column NULL (not yet assigned a status) $query->whereNull('status'); } else { // For specific status, must match exactly and NOT be null $query->whereNotNull('status') ->where('status', '=', $status); } $query->select([ 'id', 'uuid', 'registration_number', 'kewpa_registration_number', 'status', 'unit_id', 'pkj_category_id', 'pkj_subcategory_id', 'pkj_model_id', 'purchase_date', 'receipt_date', 'purchase_price', 'contract_reference', 'country_of_manufacture', 'economic_year', 'economic_distance', 'note', 'engine_number', 'chassis_number', 'created_at', 'updated_at' ]); // Apply visibility scoping if user is provided if ($user) { $query->visibleTo($user); } // Apply filters if (isset($filters['unit_id'])) { $query->where('unit_id', $filters['unit_id']); } $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() ] ]; } /** * Apply common filters to query */ private function applyFilters($query, array $filters = [], bool $excludeDateFilter = false): void { // Only apply created_at filter if the model has created_at column $model = $query->getModel(); if ($model && method_exists($model, 'getTable')) { $schema = $model->getConnection()->getSchemaBuilder(); $columns = $schema->getColumnListing($model->getTable()); 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 ($model && method_exists($model, 'getTable')) { $columns = $schema->getColumnListing($model->getTable()); 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']) { // Get units under the government $unitIds = $this->getUnitsByGovernment($filters['government_id']); if ($model && method_exists($model, 'getTable')) { $columns = $schema->getColumnListing($model->getTable()); 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 formation_id filter if provided if (isset($filters['formation_id']) && $filters['formation_id']) { if ($model && method_exists($model, 'getTable')) { $columns = $schema->getColumnListing($model->getTable()); if (in_array('unit_id', $columns)) { // 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'); } } } } // Apply camp_id filter if provided if (isset($filters['camp_id']) && $filters['camp_id']) { if ($model && method_exists($model, 'getTable')) { $columns = $schema->getColumnListing($model->getTable()); if (in_array('camp_id', $columns)) { $query->where('camp_id', $filters['camp_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 = \Modules\Formation\Entities\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() ]; } }