first init

This commit is contained in:
ISMAIL MASSERAN
2026-06-08 11:37:14 +08:00
commit 94ecbe5887
1058 changed files with 87732 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
'name' => 'Dashboard',
];
@@ -0,0 +1,16 @@
<?php
namespace Modules\Dashboard\Database\Seeders;
use Illuminate\Database\Seeder;
class DashboardDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// $this->call([]);
}
}
@@ -0,0 +1,109 @@
<?php
namespace Modules\Dashboard\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Dashboard\Services\KJC\KJCDashboardService;
use Modules\Dashboard\Services\PKJ\PKJDashboardService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class DashboardController extends Controller
{
protected KJCDashboardService $kjcDashboardService;
protected PKJDashboardService $pkjDashboardService;
public function __construct(
KJCDashboardService $kjcDashboardService,
PKJDashboardService $pkjDashboardService
) {
$this->kjcDashboardService = $kjcDashboardService;
$this->pkjDashboardService = $pkjDashboardService;
}
/**
* Get dashboard overview data
*/
public function getOverview(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$overview = [
'total_units' => $this->kjcDashboardService->getTotalUnits($filters),
'total_camps' => $this->kjcDashboardService->getTotalCamps($filters),
'total_formations' => $this->kjcDashboardService->getTotalFormations($filters),
'kjc' => [
'total_assets' => $this->kjcDashboardService->getTotalAssets($filters, $user),
'operational_assets' => $this->kjcDashboardService->getOperationalAssets($filters, $user),
'under_repair' => $this->kjcDashboardService->getUnderRepairAssets($filters, $user),
'maintenance_due' => $this->kjcDashboardService->getMaintenanceDueAssets($filters, $user),
'active_repairs' => $this->kjcDashboardService->getActiveRepairs($filters),
'pending_reports' => $this->kjcDashboardService->getPendingReports($filters)
],
'pkj' => [
'total_assets' => $this->pkjDashboardService->getTotalAssets($filters, $user),
'operational_assets' => $this->pkjDashboardService->getOperationalAssets($filters, $user),
'under_repair' => $this->pkjDashboardService->getUnderRepairAssets($filters, $user),
'maintenance_due' => $this->pkjDashboardService->getMaintenanceDueAssets($filters, $user),
'active_repairs' => $this->pkjDashboardService->getActiveRepairs($filters),
'pending_reports' => $this->pkjDashboardService->getPendingReports($filters)
]
];
return response()->json([
'success' => true,
'data' => $overview
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch dashboard overview',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get stat cards data
*/
public function getStatCards(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$statCards = [
'kjc' => [
'total_assets' => $this->kjcDashboardService->getTotalAssets($filters, $user),
'operational_assets' => $this->kjcDashboardService->getOperationalAssets($filters, $user),
'under_repair' => $this->kjcDashboardService->getUnderRepairAssets($filters, $user),
'maintenance_due' => $this->kjcDashboardService->getMaintenanceDueAssets($filters, $user),
],
'pkj' => [
'total_assets' => $this->pkjDashboardService->getTotalAssets($filters, $user),
'operational_assets' => $this->pkjDashboardService->getOperationalAssets($filters, $user),
'under_repair' => $this->pkjDashboardService->getUnderRepairAssets($filters, $user),
'maintenance_due' => $this->pkjDashboardService->getMaintenanceDueAssets($filters, $user),
],
'overview' => [
'total_units' => $this->kjcDashboardService->getTotalUnits($filters),
'active_repairs' => $this->kjcDashboardService->getActiveRepairs($filters) + $this->pkjDashboardService->getActiveRepairs($filters),
'pending_reports' => $this->kjcDashboardService->getPendingReports($filters) + $this->pkjDashboardService->getPendingReports($filters),
]
];
return response()->json([
'success' => true,
'data' => $statCards
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch stat cards',
'error' => $e->getMessage()
], 500);
}
}
}
@@ -0,0 +1,302 @@
<?php
namespace Modules\Dashboard\Http\Controllers\KJC;
use App\Http\Controllers\Controller;
use Modules\KJCAssetEntitlement\Entities\KJCAssetEntitlement;
use Modules\KJCAssetHolding\Entities\KJCAssetHolding;
use Modules\Dashboard\Services\KJC\KJCDashboardService;
use Modules\Dashboard\Repositories\Contracts\KJCDashboardRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class KJCDashboardController extends Controller
{
protected KJCDashboardService $kjcDashboardService;
protected KJCDashboardRepositoryInterface $kjcDashboardRepository;
public function __construct(KJCDashboardService $kjcDashboardService, KJCDashboardRepositoryInterface $kjcDashboardRepository)
{
$this->kjcDashboardService = $kjcDashboardService;
$this->kjcDashboardRepository = $kjcDashboardRepository;
}
/**
* Get KJC entitlement vs holdings pie chart
*/
public function getEntitlementVsHoldingsPieChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->kjcDashboardService->getEntitlementVsHoldingsPieChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch KJC entitlement vs holdings chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get KJC holdings status breakdown pie chart
*/
public function getHoldingsStatusBreakdownPieChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->kjcDashboardService->getHoldingsStatusBreakdownPieChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch KJC holdings status breakdown chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get KJC unit performance chart
*/
public function getUnitPerformanceChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->kjcDashboardService->getUnitPerformanceChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch KJC unit performance chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get KJC monthly trends chart
*/
public function getMonthlyTrendsChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->kjcDashboardService->getMonthlyTrendsChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch KJC monthly trends chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get KJC historical metrics bar chart
*/
public function getHistoricalMetricsBarChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->kjcDashboardService->getHistoricalMetricsBarChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch KJC historical metrics chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get drill-down data for entitlement vs holdings
*/
public function getEntitlementVsHoldingsDetails(Request $request): JsonResponse
{
try {
$filters = $request->only(['type', 'date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$type = $filters['type'] ?? 'entitlement';
if ($type === 'entitlement') {
// Return entitlement details
$entitlements = KJCAssetEntitlement::with(['kjcSubcategory'])
->paginate(20);
$data = [
'title' => 'KJC Asset Entitlements',
'filters' => array_merge($filters, ['total_count' => $entitlements->total()]),
'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()
]
];
} else {
// Return holdings details
$query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']);
if (isset($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (isset($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to']);
}
if (isset($filters['unit_id'])) {
$query->where('unit_id', $filters['unit_id']);
}
$holdings = $query->paginate(20);
$data = [
'title' => 'KJC Asset Holdings',
'filters' => array_merge($filters, ['total_count' => $holdings->total()]),
'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()
]
];
}
return response()->json([
'success' => true,
'data' => $data
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch entitlement vs holdings details',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get drill-down data for holdings status
*/
public function getHoldingsStatusDetails(Request $request): JsonResponse
{
try {
$filters = $request->only(['status', 'date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']);
if (isset($filters['status'])) {
$query->where('status', $filters['status']);
}
if (isset($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (isset($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to']);
}
if (isset($filters['unit_id'])) {
$query->where('unit_id', $filters['unit_id']);
}
$holdings = $query->paginate(20);
// Map status codes to readable names
$statusNames = [
'BP' => 'Beroperasi (Operational)',
'BDG' => 'Baik Diselenggara (Well Maintained)',
'TBDG' => 'Tidak Baik Diselenggara (Poorly Maintained)'
];
$statusName = $statusNames[$filters['status']] ?? $filters['status'];
return response()->json([
'success' => true,
'data' => [
'title' => "KJC Assets with Status: {$statusName}",
'filters' => array_merge($filters, ['total_count' => $holdings->total()]),
'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()
]
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch holdings status details',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get KJC asset details by status (drill-down table, paginated).
* Status can be 'null' for assets not yet assigned a status (DB column NULL).
*/
public function getAssetDetailsByStatus(Request $request, string $status): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id', 'page', 'per_page']);
$user = $request->user();
$status = trim($status);
$status = $status === 'null' ? '' : $status;
$details = $this->kjcDashboardRepository->getAssetDetailsByStatus($status, $filters, $user);
return response()->json([
'success' => true,
'data' => $details
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch KJC asset details',
'error' => $e->getMessage()
], 500);
}
}
}
@@ -0,0 +1,188 @@
<?php
namespace Modules\Dashboard\Http\Controllers\PKJ;
use App\Http\Controllers\Controller;
use Modules\Dashboard\Services\PKJ\PKJDashboardService;
use Modules\Dashboard\Repositories\Contracts\PKJDashboardRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class PKJDashboardController extends Controller
{
protected PKJDashboardService $pkjDashboardService;
protected PKJDashboardRepositoryInterface $pkjDashboardRepository;
public function __construct(PKJDashboardService $pkjDashboardService, PKJDashboardRepositoryInterface $pkjDashboardRepository)
{
$this->pkjDashboardService = $pkjDashboardService;
$this->pkjDashboardRepository = $pkjDashboardRepository;
}
/**
* Get PKJ entitlement vs holdings pie chart
*/
public function getEntitlementVsHoldingsPieChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->pkjDashboardService->getEntitlementVsHoldingsPieChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ entitlement vs holdings chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get PKJ holdings status breakdown pie chart
*/
public function getHoldingsStatusBreakdownPieChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->pkjDashboardService->getHoldingsStatusBreakdownPieChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ holdings status breakdown chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get PKJ monthly trends chart
*/
public function getMonthlyTrendsChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->pkjDashboardService->getMonthlyTrendsChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ monthly trends chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get PKJ holdings by category bar chart
*/
public function getHoldingsByCategoryBarChart(Request $request): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->pkjDashboardService->getHoldingsByCategoryBarChart($filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ holdings by category chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get PKJ category metrics bar chart (drill-down)
*/
public function getCategoryMetricsBarChart(Request $request, int $categoryId): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->pkjDashboardService->getCategoryMetricsBarChart($categoryId, $filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ category metrics chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get PKJ category status breakdown pie chart (drill-down)
*/
public function getCategoryStatusBreakdownPieChart(Request $request, int $categoryId): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']);
$user = $request->user(); // Get authenticated user
$chartData = $this->pkjDashboardService->getCategoryStatusBreakdownPieChart($categoryId, $filters, $user);
return response()->json([
'success' => true,
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ category status breakdown chart',
'error' => $e->getMessage()
], 500);
}
}
/**
* Get PKJ asset details by category and status (drill-down)
*/
public function getAssetDetailsByCategoryAndStatus(Request $request, int $categoryId, string $status): JsonResponse
{
try {
$filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id', 'page', 'per_page']);
$user = $request->user(); // Get authenticated user
// Convert 'null' string to empty string for null status handling
// Trim whitespace to ensure exact matching
$status = trim($status);
$status = $status === 'null' ? '' : $status;
$details = $this->pkjDashboardRepository->getAssetDetailsByCategoryAndStatus($categoryId, $status, $filters, $user);
return response()->json([
'success' => true,
'data' => $details
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to fetch PKJ asset details',
'error' => $e->getMessage()
], 500);
}
}
}
View File
@@ -0,0 +1,169 @@
<?php
namespace Modules\Dashboard\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Nwidart\Modules\Traits\PathNamespace;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class DashboardServiceProvider extends ServiceProvider
{
use PathNamespace;
protected string $name = 'Dashboard';
protected string $nameLower = 'dashboard';
/**
* Boot the application events.
*/
public function boot(): void
{
$this->registerCommands();
$this->registerCommandSchedules();
$this->registerTranslations();
$this->registerConfig();
$this->registerViews();
$this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations'));
}
/**
* Register the service provider.
*/
public function register(): void
{
$this->app->register(EventServiceProvider::class);
$this->app->register(RouteServiceProvider::class);
// Register dashboard repositories
$this->app->bind(
\Modules\Dashboard\Repositories\Contracts\KJCDashboardRepositoryInterface::class,
\Modules\Dashboard\Repositories\KJCDashboardRepository::class
);
$this->app->bind(
\Modules\Dashboard\Repositories\Contracts\PKJDashboardRepositoryInterface::class,
\Modules\Dashboard\Repositories\PKJDashboardRepository::class
);
// Register dashboard services
$this->app->singleton(\Modules\Dashboard\Services\KJC\KJCDashboardService::class);
$this->app->singleton(\Modules\Dashboard\Services\PKJ\PKJDashboardService::class);
}
/**
* Register commands in the format of Command::class
*/
protected function registerCommands(): void
{
// $this->commands([]);
}
/**
* Register command Schedules.
*/
protected function registerCommandSchedules(): void
{
// $this->app->booted(function () {
// $schedule = $this->app->make(Schedule::class);
// $schedule->command('inspire')->hourly();
// });
}
/**
* Register translations.
*/
public function registerTranslations(): void
{
$langPath = resource_path('lang/modules/'.$this->nameLower);
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, $this->nameLower);
$this->loadJsonTranslationsFrom($langPath);
} else {
$this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower);
$this->loadJsonTranslationsFrom(module_path($this->name, 'Lang'));
}
}
/**
* Register config.
*/
protected function registerConfig(): void
{
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
if (is_dir($configPath)) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
$segments = explode('.', $this->nameLower.'.'.$config_key);
// Remove duplicated adjacent segments
$normalized = [];
foreach ($segments as $segment) {
if (end($normalized) !== $segment) {
$normalized[] = $segment;
}
}
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
$this->publishes([$file->getPathname() => config_path($config)], 'config');
$this->merge_config_from($file->getPathname(), $key);
}
}
}
}
/**
* Merge config from the given path recursively.
*/
protected function merge_config_from(string $path, string $key): void
{
$existing = config($key, []);
$module_config = require $path;
config([$key => array_replace_recursive($existing, $module_config)]);
}
/**
* Register views.
*/
public function registerViews(): void
{
$viewPath = resource_path('views/modules/'.$this->nameLower);
$sourcePath = module_path($this->name, 'Resources/Views');
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower);
}
/**
* Get the services provided by the provider.
*/
public function provides(): array
{
return [];
}
private function getPublishableViewPaths(): array
{
$paths = [];
foreach (config('view.paths') as $path) {
if (is_dir($path.'/modules/'.$this->nameLower)) {
$paths[] = $path.'/modules/'.$this->nameLower;
}
}
return $paths;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Modules\Dashboard\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event handler mappings for the application.
*
* @var array<string, array<int, string>>
*/
protected $listen = [];
/**
* Indicates if events should be discovered.
*
* @var bool
*/
protected static $shouldDiscoverEvents = true;
/**
* Configure the proper event listeners for email verification.
*/
protected function configureEmailVerification(): void {}
}
@@ -0,0 +1,49 @@
<?php
namespace Modules\Dashboard\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
protected string $name = 'Dashboard';
/**
* Called before routes are registered.
*
* Register any model bindings or pattern based filters.
*/
public function boot(): void
{
parent::boot();
}
/**
* Define the routes for the application.
*/
public function map(): void
{
$this->mapApiRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*/
protected function mapWebRoutes(): void
{
Route::middleware('web')->group(module_path($this->name, '/Routes/web.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*/
protected function mapApiRoutes(): void
{
Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php'));
}
}
@@ -0,0 +1,57 @@
<?php
namespace Modules\Dashboard\Repositories\Contracts;
interface KJCDashboardRepositoryInterface
{
/**
* Get total KJC asset entitlement
*/
public function getTotalEntitlement(array $filters = [], $user = null): int;
/**
* Get total KJC asset holdings
*/
public function getTotalHoldings(array $filters = [], $user = null): int;
/**
* Get KJC asset holdings by status
*/
public function getHoldingsByStatus(array $filters = [], $user = null): array;
/**
* Get KJC asset holdings with pagination
*/
public function getHoldingsPaginated(array $filters = [], int $perPage = 20, $user = null): array;
/**
* Get KJC asset entitlements with pagination
*/
public function getEntitlementsPaginated(array $filters = [], int $perPage = 20, $user = null): array;
/**
* Get KJC asset holdings by specific status
*/
public function getHoldingsBySpecificStatus(string $status, array $filters = [], $user = null): array;
/**
* Get KJC asset holdings grouped by unit
*/
public function getHoldingsByUnit(array $filters = [], $user = null): array;
/**
* Get KJC asset holdings monthly trends
*/
public function getMonthlyTrends(array $filters = [], $user = null): array;
/**
* Get KJC historical metrics (KEUPAYAAN, KESIAGAAN, SERVISIBILITI) by month
*/
public function getHistoricalMetricsByMonth(array $filters = []): array;
/**
* 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;
}
@@ -0,0 +1,51 @@
<?php
namespace Modules\Dashboard\Repositories\Contracts;
interface PKJDashboardRepositoryInterface
{
/**
* Get total PKJ asset entitlement
*/
public function getTotalEntitlement(array $filters = [], $user = null): int;
/**
* Get total PKJ asset holdings
*/
public function getTotalHoldings(array $filters = [], $user = null): int;
/**
* Get PKJ asset holdings grouped by status
*/
public function getHoldingsByStatus(array $filters = [], $user = null): array;
/**
* Get PKJ asset holdings by specific status
*/
public function getHoldingsBySpecificStatus(string $status, array $filters = [], $user = null): array;
/**
* Get PKJ monthly trends
*/
public function getMonthlyTrends(array $filters = [], $user = null): array;
/**
* Get PKJ holdings by category
*/
public function getHoldingsByCategory(array $filters = [], $user = null): array;
/**
* Get PKJ category metrics (KEUPAYAAN, KESIAGAAN, SERVISIBILITI)
*/
public function getCategoryMetrics(int $categoryId, array $filters = [], $user = null): array;
/**
* Get PKJ category status breakdown
*/
public function getCategoryStatusBreakdown(int $categoryId, array $filters = [], $user = null): array;
/**
* Get PKJ asset details by category and status
*/
public function getAssetDetailsByCategoryAndStatus(int $categoryId, string $status, array $filters = [], $user = null): array;
}
@@ -0,0 +1,645 @@
<?php
namespace Modules\Dashboard\Repositories;
use Modules\Dashboard\Repositories\Contracts\KJCDashboardRepositoryInterface;
use Modules\KJCAssetEntitlement\Entities\KJCAssetEntitlement;
use Modules\KJCAssetHolding\Entities\KJCAssetHolding;
use App\Models\KJCHistoricalEntitlement;
use App\Models\KJCHistoricalHoldingStatus;
use Modules\Unit\Entities\Unit;
use Modules\Formation\Entities\Formation;
use App\Services\VisibilityService;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class KJCDashboardRepository implements KJCDashboardRepositoryInterface
{
public function __construct(
protected VisibilityService $visibilityService
) {}
/**
* Get total KJC asset entitlement
*/
public function getTotalEntitlement(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('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');
}
}
}
}
@@ -0,0 +1,495 @@
<?php
namespace Modules\Dashboard\Repositories;
use Modules\Dashboard\Repositories\Contracts\PKJDashboardRepositoryInterface;
use Modules\PKJAssetEntitlement\Entities\PKJAssetEntitlement;
use Modules\PKJAssetHolding\Entities\PKJAssetHolding;
use Modules\PKJRepair\Entities\PKJRepair;
use Modules\PKJReport\Entities\PKJReport;
use Modules\PKJVariant\Entities\PKJCategory;
use Illuminate\Support\Facades\DB;
use Modules\Unit\Entities\Unit;
use App\Services\VisibilityService;
use Carbon\Carbon;
class PKJDashboardRepository implements PKJDashboardRepositoryInterface
{
public function __construct(
protected VisibilityService $visibilityService
) {}
/**
* Get total PKJ asset entitlement
*/
public function getTotalEntitlement(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('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()
];
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Dashboard\Http\Controllers\DashboardController;
use Modules\Dashboard\Http\Controllers\KJC\KJCDashboardController;
use Modules\Dashboard\Http\Controllers\PKJ\PKJDashboardController;
Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () {
// Main dashboard endpoints
Route::get('/dashboard/stat-cards', [DashboardController::class, 'getStatCards']);
Route::get('/dashboard/overview', [DashboardController::class, 'getOverview']);
// KJC specific endpoints
Route::prefix('dashboard/kjc')->group(function () {
// Pie charts
Route::get('/entitlement-vs-holdings-pie', [KJCDashboardController::class, 'getEntitlementVsHoldingsPieChart']);
Route::get('/holdings-status-breakdown-pie', [KJCDashboardController::class, 'getHoldingsStatusBreakdownPieChart']);
// Bar charts
Route::get('/historical-metrics-bar', [KJCDashboardController::class, 'getHistoricalMetricsBarChart']);
// Other charts
Route::get('/unit-performance-chart', [KJCDashboardController::class, 'getUnitPerformanceChart']);
Route::get('/monthly-trends-chart', [KJCDashboardController::class, 'getMonthlyTrendsChart']);
// Drill-down endpoints
Route::get('/drill-down/entitlement-vs-holdings', [KJCDashboardController::class, 'getEntitlementVsHoldingsDetails']);
Route::get('/drill-down/holdings-status', [KJCDashboardController::class, 'getHoldingsStatusDetails']);
Route::get('/status/{status}/details', [KJCDashboardController::class, 'getAssetDetailsByStatus']);
});
// PKJ specific endpoints
Route::prefix('dashboard/pkj')->group(function () {
Route::get('/entitlement-vs-holdings-pie', [PKJDashboardController::class, 'getEntitlementVsHoldingsPieChart']);
Route::get('/holdings-status-breakdown-pie', [PKJDashboardController::class, 'getHoldingsStatusBreakdownPieChart']);
Route::get('/monthly-trends', [PKJDashboardController::class, 'getMonthlyTrendsChart']);
Route::get('/holdings-by-category-bar', [PKJDashboardController::class, 'getHoldingsByCategoryBarChart']);
// Drill-down endpoints
Route::get('/category/{categoryId}/metrics-bar', [PKJDashboardController::class, 'getCategoryMetricsBarChart']);
Route::get('/category/{categoryId}/status-breakdown-pie', [PKJDashboardController::class, 'getCategoryStatusBreakdownPieChart']);
Route::get('/category/{categoryId}/status/{status}/details', [PKJDashboardController::class, 'getAssetDetailsByCategoryAndStatus']);
});
});
@@ -0,0 +1,486 @@
<?php
namespace Modules\Dashboard\Services;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Unit\Entities\Unit;
use Modules\Camp\Entities\Camp;
use Modules\Formation\Entities\Formation;
use App\Services\VisibilityService;
abstract class DashboardService
{
public function __construct(
protected VisibilityService $visibilityService
) {}
/**
* Get date range for filtering
*/
protected 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 common filters to query
*/
protected function applyCommonFilters($query, array $filters = []): void
{
$dateRange = $this->getDateRange($filters);
$query->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]);
// Government filter: Filter by government (get all units under that government)
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
$query->whereNull('id');
}
}
if (isset($filters['unit_id']) && $filters['unit_id']) {
$query->where('unit_id', $filters['unit_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']) {
$query->whereHas('unit', function ($q) use ($filters) {
$q->where('formation_id', $filters['formation_id']);
});
}
}
/**
* Apply visibility scoping to query based on user permissions
*/
protected function applyVisibilityScoping($query, $user): void
{
if (!$user) {
return;
}
// Check if the model has the HasVisibility trait
if (method_exists($query->getModel(), 'scopeVisibleTo')) {
$query->visibleTo($user);
}
}
/**
* Apply visibility scoping to raw database queries
* Uses VisibilityService for consistent logic
*/
protected function applyRawVisibilityScoping($query, $user, string $tableName): void
{
if (!$user) {
return;
}
$userUnit = Unit::find($user->unit_id);
if (!$userUnit) {
$query->whereRaw('1 = 0');
return;
}
$unitIds = $this->visibilityService->getVisibleUnitIds($user);
if (empty($unitIds)) {
$query->whereRaw('1 = 0');
} else {
$query->whereIn('unit_id', $unitIds);
}
}
/**
* Get all units by government ID
* Uses VisibilityService for consistent logic
*/
protected 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 total units count
*/
public function getTotalUnits(array $filters = []): int
{
$query = Unit::query();
if (isset($filters['camp_id']) && $filters['camp_id']) {
$query->where('camp_id', $filters['camp_id']);
}
if (isset($filters['formation_id']) && $filters['formation_id']) {
$query->where('formation_id', $filters['formation_id']);
}
return $query->count();
}
/**
* Get total camps count
*/
public function getTotalCamps(array $filters = []): int
{
return Camp::count();
}
/**
* Get total formations count
*/
public function getTotalFormations(array $filters = []): int
{
return Formation::count();
}
/**
* Get active repairs count (to be implemented by child classes)
*/
abstract public function getActiveRepairs(array $filters = []): int;
/**
* Get pending reports count (to be implemented by child classes)
*/
abstract public function getPendingReports(array $filters = []): int;
/**
* Get total assets count (to be implemented by child classes)
*/
abstract public function getTotalAssets(array $filters = []): int;
/**
* Get operational assets count (to be implemented by child classes)
*/
abstract public function getOperationalAssets(array $filters = []): int;
/**
* Get under repair assets count (to be implemented by child classes)
*/
abstract public function getUnderRepairAssets(array $filters = []): int;
/**
* Get maintenance due assets count (to be implemented by child classes)
*/
abstract public function getMaintenanceDueAssets(array $filters = []): int;
/**
* Format chart data with common structure
*/
protected function formatChartData(
string $chartType,
string $title,
array $series,
array $categories = [],
array $drillDown = []
): array {
return [
'chart_type' => $chartType,
'title' => $title,
'series' => $series,
'categories' => $categories,
'drill_down' => $drillDown
];
}
/**
* Format pie chart data
*/
protected function formatPieChartData(
string $title,
array $data,
string $drillDownEndpoint = '',
array $drillDownParameters = []
): array {
$total = array_sum(array_column($data, 'value'));
$series = [];
foreach ($data as $item) {
$percentage = $total > 0 ? round(($item['value'] / $total) * 100, 1) : 0;
$seriesItem = [
'name' => $item['name'],
'value' => $item['value'],
'percentage' => $percentage
];
// Preserve status_code if it exists (for drill-down functionality)
if (isset($item['status_code'])) {
$seriesItem['status_code'] = $item['status_code'];
}
$series[] = $seriesItem;
}
return $this->formatChartData(
'pie',
$title,
$series,
[],
[
'enabled' => !empty($drillDownEndpoint),
'endpoint' => $drillDownEndpoint,
'parameters' => $drillDownParameters
]
);
}
/**
* Format bar chart data
*/
protected function formatBarChartData(
string $title,
array $series,
array $categories,
string $drillDownEndpoint = '',
array $drillDownParameters = []
): array {
return $this->formatChartData(
'bar',
$title,
$series,
$categories,
[
'enabled' => !empty($drillDownEndpoint),
'endpoint' => $drillDownEndpoint,
'parameters' => $drillDownParameters
]
);
}
/**
* Format line chart data
*/
protected function formatLineChartData(
string $title,
array $series,
array $categories,
string $drillDownEndpoint = '',
array $drillDownParameters = []
): array {
return $this->formatChartData(
'line',
$title,
$series,
$categories,
[
'enabled' => !empty($drillDownEndpoint),
'endpoint' => $drillDownEndpoint,
'parameters' => $drillDownParameters
]
);
}
/**
* Get monthly categories for date range
*/
protected function getMonthlyCategories(array $filters = []): array
{
$dateRange = $this->getDateRange($filters);
$categories = [];
$current = $dateRange['from']->copy()->startOfMonth();
$end = $dateRange['to']->copy()->endOfMonth();
while ($current->lte($end)) {
$categories[] = $current->format('M Y');
$current->addMonth();
}
return $categories;
}
/**
* Get weekly categories for date range
*/
protected function getWeeklyCategories(array $filters = []): array
{
$dateRange = $this->getDateRange($filters);
$categories = [];
$current = $dateRange['from']->copy()->startOfWeek();
$end = $dateRange['to']->copy()->endOfWeek();
while ($current->lte($end)) {
$categories[] = 'Week ' . $current->weekOfYear . ' ' . $current->year;
$current->addWeek();
}
return $categories;
}
/**
* Get daily categories for date range
*/
protected function getDailyCategories(array $filters = []): array
{
$dateRange = $this->getDateRange($filters);
$categories = [];
$current = $dateRange['from']->copy();
$end = $dateRange['to']->copy();
while ($current->lte($end)) {
$categories[] = $current->format('M d');
$current->addDay();
}
return $categories;
}
/**
* Calculate percentage change
*/
protected function calculatePercentageChange(int $current, int $previous): float
{
if ($previous === 0) {
return $current > 0 ? 100.0 : 0.0;
}
return round((($current - $previous) / $previous) * 100, 1);
}
/**
* Get status color mapping
*/
protected function getStatusColors(): array
{
return [
'operational' => '#4caf50',
'under_repair' => '#ff9800',
'maintenance_due' => '#f44336',
'out_of_service' => '#9e9e9e',
'completed' => '#4caf50',
'pending' => '#ff9800',
'in_progress' => '#2196f3',
'cancelled' => '#f44336'
];
}
/**
* Get priority color mapping
*/
protected function getPriorityColors(): array
{
return [
'high' => '#f44336',
'medium' => '#ff9800',
'low' => '#4caf50'
];
}
/**
* Format currency
*/
protected function formatCurrency(float $amount): string
{
return 'RM ' . number_format($amount, 2);
}
/**
* Format duration in hours
*/
protected function formatDuration(float $hours): string
{
if ($hours < 24) {
return round($hours, 1) . ' hours';
}
$days = floor($hours / 24);
$remainingHours = $hours % 24;
if ($remainingHours > 0) {
return $days . ' days ' . round($remainingHours, 1) . ' hours';
}
return $days . ' days';
}
/**
* Get common drill-down parameters
*/
protected function getCommonDrillDownParameters(): array
{
return ['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id'];
}
/**
* Build drill-down endpoint
*/
protected function buildDrillDownEndpoint(string $baseEndpoint, array $parameters = []): string
{
$endpoint = $baseEndpoint;
if (!empty($parameters)) {
$endpoint .= '?' . http_build_query($parameters);
}
return $endpoint;
}
/**
* Get summary statistics for drill-down
*/
protected function getSummaryStatistics($query, array $filters = []): array
{
$total = $query->count();
// Get breakdown by unit
$byUnit = $query->clone()
->join('units', function ($join) {
$join->on('units.id', '=', $this->getUnitIdColumn());
})
->selectRaw('units.name as unit_name, COUNT(*) as count')
->groupBy('units.name')
->get()
->toArray();
return [
'total' => $total,
'by_unit' => $byUnit
];
}
/**
* Get unit ID column name (to be implemented by child classes)
*/
abstract protected function getUnitIdColumn(): string;
/**
* Get asset model class (to be implemented by child classes)
*/
abstract protected function getAssetModelClass(): string;
/**
* Get repair model class (to be implemented by child classes)
*/
abstract protected function getRepairModelClass(): string;
}
@@ -0,0 +1,315 @@
<?php
namespace Modules\Dashboard\Services\KJC;
use Modules\Dashboard\Services\DashboardService;
use Modules\Dashboard\Repositories\Contracts\KJCDashboardRepositoryInterface;
use Modules\KJCRepair\Entities\KJCRepair;
use Modules\KJCReport\Entities\KJCReport;
use Modules\Unit\Entities\Unit;
use App\Services\VisibilityService;
use Carbon\Carbon;
class KJCDashboardService extends DashboardService
{
protected KJCDashboardRepositoryInterface $kjcDashboardRepository;
public function __construct(
VisibilityService $visibilityService,
KJCDashboardRepositoryInterface $kjcDashboardRepository
) {
parent::__construct($visibilityService);
$this->kjcDashboardRepository = $kjcDashboardRepository;
}
/**
* Get total KJC assets count
*/
public function getTotalAssets(array $filters = [], $user = null): int
{
return $this->kjcDashboardRepository->getTotalHoldings($filters, $user);
}
/**
* Get operational KJC assets count (BP status)
*/
public function getOperationalAssets(array $filters = [], $user = null): int
{
$holdings = $this->kjcDashboardRepository->getHoldingsBySpecificStatus('BP', $filters, $user);
return $holdings['pagination']['total'];
}
/**
* Get under repair KJC assets count (BDG status)
*/
public function getUnderRepairAssets(array $filters = [], $user = null): int
{
$holdings = $this->kjcDashboardRepository->getHoldingsBySpecificStatus('BDG', $filters, $user);
return $holdings['pagination']['total'];
}
/**
* Get maintenance due KJC assets count (TBDG status)
*/
public function getMaintenanceDueAssets(array $filters = [], $user = null): int
{
$holdings = $this->kjcDashboardRepository->getHoldingsBySpecificStatus('TBDG', $filters, $user);
return $holdings['pagination']['total'];
}
/**
* Get active KJC repairs count
*/
public function getActiveRepairs(array $filters = []): int
{
$query = KJCRepair::whereIn('status', ['pending', 'in_progress']);
$this->applyCommonFilters($query, $filters);
return $query->count();
}
/**
* Get pending KJC reports count
*/
public function getPendingReports(array $filters = []): int
{
$query = KJCReport::where('status', 'pending');
$this->applyCommonFilters($query, $filters);
return $query->count();
}
/**
* Get KJC entitlement vs holdings pie chart
*/
public function getEntitlementVsHoldingsPieChart(array $filters = [], $user = null): array
{
// Get total entitlement
$totalEntitlement = $this->kjcDashboardRepository->getTotalEntitlement($filters, $user);
// Get total holdings
$totalHoldings = $this->kjcDashboardRepository->getTotalHoldings($filters, $user);
// Prepare data for pie chart (only showing Perjawatan and Pegangan)
$data = [
[
'name' => 'Perjawatan',
'value' => $totalEntitlement,
'color' => '#4caf50'
],
[
'name' => 'Pegangan',
'value' => $totalHoldings,
'color' => '#2196f3'
]
];
return $this->formatPieChartData(
'Perjawatan vs Pegangan KJC',
$data,
'/v1/dashboard/kjc/drill-down/entitlement-vs-holdings',
['type']
);
}
/**
* Get KJC holdings status breakdown pie chart
*/
public function getHoldingsStatusBreakdownPieChart(array $filters = [], $user = null): array
{
$statusCounts = $this->kjcDashboardRepository->getHoldingsByStatus($filters, $user);
$statusNames = [
'BP' => 'BP',
'BDG' => 'BDG',
'TBDG' => 'TBDG',
'BT' => 'BT',
'TBT' => 'TBT',
'BG' => 'BG',
'TBG' => 'TBG',
];
$data = [];
foreach ($statusCounts as $item) {
$statusCode = $item['status'];
if ($statusCode === null || $statusCode === '') {
$statusName = 'Belum Diberi Status';
} else {
$statusName = $statusNames[$statusCode] ?? $statusCode;
}
$data[] = [
'name' => $statusName,
'value' => $item['count'],
'percentage' => 0,
'status_code' => $statusCode,
];
}
// Calculate percentages
$total = array_sum(array_column($data, 'value'));
foreach ($data as &$row) {
$row['percentage'] = $total > 0 ? round(($row['value'] / $total) * 100, 2) : 0;
}
return $this->formatPieChartData(
'KJC Status Pegangan',
$data,
'/v1/dashboard/kjc/drill-down/holdings-status',
['status']
);
}
/**
* Get KJC unit performance chart
*/
public function getUnitPerformanceChart(array $filters = []): array
{
$units = Unit::withCount([
'kjcAssetHoldings as bp_count' => function ($query) use ($filters) {
$query->where('status', 'BP');
$this->applyCommonFilters($query, $filters);
},
'kjcAssetHoldings as bdg_count' => function ($query) use ($filters) {
$query->where('status', 'BDG');
$this->applyCommonFilters($query, $filters);
},
'kjcAssetHoldings as tbdg_count' => function ($query) use ($filters) {
$query->where('status', 'TBDG');
$this->applyCommonFilters($query, $filters);
}
])->get();
$unitNames = [];
$bpCounts = [];
$bdgCounts = [];
$tbdgCounts = [];
foreach ($units as $unit) {
$unitNames[] = $unit->name;
$bpCounts[] = $unit->bp_count;
$bdgCounts[] = $unit->bdg_count;
$tbdgCounts[] = $unit->tbdg_count;
}
return $this->formatBarChartData(
'KJC Unit 2025',
[
['name' => 'Baik dalam Perhatian (BP)', 'data' => $bpCounts],
['name' => 'Boleh Digunakan (BDG)', 'data' => $bdgCounts],
['name' => 'Tidak Boleh Digunakan (TBDG)', 'data' => $tbdgCounts]
],
$unitNames,
'/v1/dashboard/kjc/drill-down/unit-performance',
['unit_id']
);
}
/**
* Get KJC monthly trends chart
*/
public function getMonthlyTrendsChart(array $filters = [], $user = null): array
{
$dateRange = $this->getDateRange($filters);
$categories = $this->getMonthlyCategories($filters);
$monthlyData = $this->kjcDashboardRepository->getMonthlyTrends($filters, $user);
// Initialize data arrays
$bpData = array_fill(0, count($categories), 0);
$bdgData = array_fill(0, count($categories), 0);
$tbdgData = array_fill(0, count($categories), 0);
// Fill data arrays
foreach ($monthlyData as $item) {
$index = array_search($item['month'], $categories);
if ($index !== false) {
switch ($item['status']) {
case 'BP':
$bpData[$index] = $item['count'];
break;
case 'BDG':
$bdgData[$index] = $item['count'];
break;
case 'TBDG':
$tbdgData[$index] = $item['count'];
break;
}
}
}
return $this->formatLineChartData(
'KJC Trend Bulanan 2025',
[
['name' => 'Baik dalam Perhatian (BP)', 'data' => $bpData],
['name' => 'Boleh Digunakan (BDG)', 'data' => $bdgData],
['name' => 'Tidak Boleh Digunakan (TBDG)', 'data' => $tbdgData]
],
$categories,
'/v1/dashboard/kjc/drill-down/monthly-trends',
['month', 'status']
);
}
/**
* Get KJC historical metrics bar chart (KEUPAYAAN, KESIAGAAN, SERVISIBILITI)
*/
public function getHistoricalMetricsBarChart(array $filters = [], $user = null): array
{
$monthlyData = $this->kjcDashboardRepository->getHistoricalMetricsByMonth($filters, $user);
// Extract data for chart
$categories = [];
$keupayaanData = [];
$kesiagaanData = [];
$servisibilitiData = [];
foreach ($monthlyData as $data) {
$monthDate = Carbon::createFromFormat('Y-m', $data['month']);
$categories[] = $monthDate->format('M'); // Remove year from categories
$keupayaanData[] = $data['keupayaan'];
$kesiagaanData[] = $data['kesiagaan'];
$servisibilitiData[] = $data['servisibiliti'];
}
// Use year from user selection (date_from/date_to) or fall back to current year
$year = Carbon::now()->year;
if (!empty($filters['date_from'])) {
$year = Carbon::parse($filters['date_from'])->year;
} elseif (!empty($filters['date_to'])) {
$year = Carbon::parse($filters['date_to'])->year;
}
return $this->formatBarChartData(
"Metrik Tahunan KJC {$year}",
[
['name' => 'KEUPAYAAN (%)', 'data' => $keupayaanData],
['name' => 'KESIAGAAN (%)', 'data' => $kesiagaanData],
['name' => 'SERVISIBILITI (%)', 'data' => $servisibilitiData]
],
$categories
);
}
/**
* Get unit ID column name for KJC
*/
protected function getUnitIdColumn(): string
{
return 'kjc_asset_holdings.unit_id';
}
/**
* Get asset model class for KJC
*/
protected function getAssetModelClass(): string
{
return KJCAssetHolding::class;
}
/**
* Get repair model class for KJC
*/
protected function getRepairModelClass(): string
{
return KJCRepair::class;
}
}
@@ -0,0 +1,319 @@
<?php
namespace Modules\Dashboard\Services\PKJ;
use Modules\Dashboard\Services\DashboardService;
use Modules\Dashboard\Repositories\Contracts\PKJDashboardRepositoryInterface;
use Modules\PKJAssetEntitlement\Entities\PKJAssetEntitlement;
use Modules\PKJAssetHolding\Entities\PKJAssetHolding;
use Modules\PKJRepair\Entities\PKJRepair;
use Modules\PKJReport\Entities\PKJReport;
use App\Services\VisibilityService;
use Carbon\Carbon;
class PKJDashboardService extends DashboardService
{
protected PKJDashboardRepositoryInterface $pkjDashboardRepository;
public function __construct(
VisibilityService $visibilityService,
PKJDashboardRepositoryInterface $pkjDashboardRepository
) {
parent::__construct($visibilityService);
$this->pkjDashboardRepository = $pkjDashboardRepository;
}
/**
* Get total PKJ assets
*/
public function getTotalAssets(array $filters = [], $user = null): int
{
return $this->pkjDashboardRepository->getTotalHoldings($filters, $user);
}
/**
* Get operational PKJ assets (BP status)
*/
public function getOperationalAssets(array $filters = [], $user = null): int
{
$holdings = $this->pkjDashboardRepository->getHoldingsBySpecificStatus('BP', $filters, $user);
return $holdings['pagination']['total'];
}
/**
* Get under repair PKJ assets (TBDG status)
*/
public function getUnderRepairAssets(array $filters = [], $user = null): int
{
$holdings = $this->pkjDashboardRepository->getHoldingsBySpecificStatus('TBDG', $filters, $user);
return $holdings['pagination']['total'];
}
/**
* Get maintenance due PKJ assets (BDG status)
*/
public function getMaintenanceDueAssets(array $filters = [], $user = null): int
{
$holdings = $this->pkjDashboardRepository->getHoldingsBySpecificStatus('BDG', $filters, $user);
return $holdings['pagination']['total'];
}
/**
* Get active PKJ repairs
*/
public function getActiveRepairs(array $filters = []): int
{
$dateRange = $this->getDateRange($filters);
return PKJRepair::query()
->whereBetween('created_at', [$dateRange['from'], $dateRange['to']])
->where('status', 'active')
->count();
}
/**
* Get pending PKJ reports
*/
public function getPendingReports(array $filters = []): int
{
$dateRange = $this->getDateRange($filters);
return PKJReport::query()
->whereBetween('created_at', [$dateRange['from'], $dateRange['to']])
->where('status', 'pending')
->count();
}
/**
* Get PKJ entitlement vs holdings pie chart
*/
public function getEntitlementVsHoldingsPieChart(array $filters = [], $user = null): array
{
$totalEntitlement = $this->pkjDashboardRepository->getTotalEntitlement($filters, $user);
$totalHoldings = $this->pkjDashboardRepository->getTotalHoldings($filters, $user);
$data = [
[
'name' => 'Perjawatan',
'value' => $totalEntitlement,
'percentage' => ($totalEntitlement + $totalHoldings) > 0 ? round(($totalEntitlement / ($totalEntitlement + $totalHoldings)) * 100, 2) : 0
],
[
'name' => 'Pegangan',
'value' => $totalHoldings,
'percentage' => ($totalEntitlement + $totalHoldings) > 0 ? round(($totalHoldings / ($totalEntitlement + $totalHoldings)) * 100, 2) : 0
]
];
return $this->formatPieChartData(
'Perjawatan vs Pegangan PKJ',
$data,
'', // No drill-down
[]
);
}
/**
* Get PKJ holdings status breakdown pie chart
*/
public function getHoldingsStatusBreakdownPieChart(array $filters = [], $user = null): array
{
$statusCounts = $this->pkjDashboardRepository->getHoldingsByStatus($filters, $user);
// Map status codes to readable names
$statusNames = [
'BP' => 'Baik dalam Perhatian',
'BDG' => 'Boleh Digunakan',
'TBDG' => 'Tidak Boleh Digunakan',
'BT' => 'Boleh Tembak',
'BG' => 'Baik Guna'
];
$data = [];
foreach ($statusCounts as $status) {
$statusName = $statusNames[$status['status']] ?? $status['status'];
$data[] = [
'name' => $statusName,
'value' => $status['count'],
'percentage' => 0 // Will be calculated after total
];
}
// Calculate percentages
$total = array_sum(array_column($data, 'value'));
foreach ($data as &$item) {
$item['percentage'] = $total > 0 ? round(($item['value'] / $total) * 100, 2) : 0;
}
return $this->formatPieChartData(
'PKJ Status Pegangan',
$data,
'', // No drill-down
[]
);
}
/**
* Get PKJ monthly trends chart
*/
public function getMonthlyTrendsChart(array $filters = [], $user = null): array
{
$monthlyData = $this->pkjDashboardRepository->getMonthlyTrends($filters, $user);
$categories = [];
$data = [];
foreach ($monthlyData as $trend) {
$categories[] = Carbon::createFromFormat('Y-m', $trend['month'])->format('M Y');
$data[] = $trend['total_reports'];
}
return $this->formatBarChartData(
'Trend Bulanan PKJ 2025',
[
['name' => 'Laporan', 'data' => $data]
],
$categories
);
}
/**
* Get PKJ holdings by category bar chart
*/
public function getHoldingsByCategoryBarChart(array $filters = [], $user = null): array
{
$categoryData = $this->pkjDashboardRepository->getHoldingsByCategory($filters, $user);
$categories = [];
$categoryIds = [];
$data = [];
foreach ($categoryData as $category) {
$categories[] = $category['category_name'];
$categoryIds[] = (int) $category['category_id'];
$data[] = (int) $category['total_holding'];
}
$chartData = $this->formatBarChartData(
'Pegangan PKJ Mengikut Kategori',
[
['name' => 'Jumlah Pegangan', 'data' => $data]
],
$categories
);
// Add category IDs to the chart data for drill-down functionality
$chartData['category_ids'] = $categoryIds;
return $chartData;
}
/**
* Get PKJ category metrics bar chart (drill-down)
*/
public function getCategoryMetricsBarChart(int $categoryId, array $filters = [], $user = null): array
{
$metrics = $this->pkjDashboardRepository->getCategoryMetrics($categoryId, $filters, $user);
$categories = ['KEUPAYAAN (%)', 'KESIAGAAN (%)', 'SERVISIBILITI (%)'];
$data = [
$metrics['keupayaan'],
$metrics['kesiagaan'],
$metrics['servisibiliti']
];
$chartData = $this->formatBarChartData(
'Metrik Kategori PKJ',
[
['name' => 'Metrics', 'data' => $data]
],
$categories
);
// Add category information to the response
$chartData['category_id'] = $metrics['category_id'];
$chartData['category_name'] = $metrics['category_name'];
return $chartData;
}
/**
* Get PKJ category status breakdown pie chart (drill-down)
*/
public function getCategoryStatusBreakdownPieChart(int $categoryId, array $filters = [], $user = null): array
{
$breakdown = $this->pkjDashboardRepository->getCategoryStatusBreakdown($categoryId, $filters, $user);
// Map status codes to readable names
$statusNames = [
'BP' => 'Baik dalam Perhatian',
'BDG' => 'Boleh Digunakan',
'TBDG' => 'Tidak Boleh Digunakan',
'BT' => 'Boleh Tembak',
'TBT' => 'Tidak Boleh Tembak',
'BG' => 'Baik Guna',
'TBG' => 'Tidak Baik Guna',
];
$data = [];
foreach ($breakdown['status_breakdown'] as $status) {
$statusCode = $status['status'];
// In the breakdown, status_code is null when the asset has no status (DB column is NULL).
if ($statusCode === null || $statusCode === '') {
$statusName = 'Status Tidak Diketahui';
} else {
$statusName = $statusNames[$statusCode] ?? $statusCode;
}
$data[] = [
'name' => $statusName,
'value' => $status['count'],
'percentage' => 0, // Will be calculated after total
'status_code' => $statusCode // null when DB status column is NULL (Belum Diberi Status)
];
}
// Calculate percentages
$total = array_sum(array_column($data, 'value'));
foreach ($data as &$item) {
$item['percentage'] = $total > 0 ? round(($item['value'] / $total) * 100, 2) : 0;
}
$chartData = $this->formatPieChartData(
'PKJ Status Pegangan Mengikut Kategori',
$data,
'/v1/dashboard/pkj/category/' . $categoryId . '/status-details',
['status']
);
// Add category information to the response
$chartData['category_id'] = $breakdown['category_id'];
$chartData['category_name'] = $breakdown['category_name'];
return $chartData;
}
/**
* Get unit ID column name for PKJ
*/
protected function getUnitIdColumn(): string
{
return 'pkj_asset_holdings.unit_id';
}
/**
* Get asset model class for PKJ
*/
protected function getAssetModelClass(): string
{
return PKJAssetHolding::class;
}
/**
* Get repair model class for PKJ
*/
protected function getRepairModelClass(): string
{
return PKJRepair::class;
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "nwidart/dashboard",
"description": "",
"authors": [
{
"name": "Nicolas Widart",
"email": "n.widart@gmail.com"
}
],
"extra": {
"laravel": {
"providers": [],
"aliases": {}
}
},
"autoload": {
"psr-4": {
"Modules\\Dashboard\\": "App",
"Modules\\Dashboard\\Database\\Factories\\": "database/factories/",
"Modules\\Dashboard\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Modules\\Dashboard\\Tests\\": "tests/"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Dashboard",
"alias": "dashboard",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Dashboard\\Providers\\DashboardServiceProvider"
],
"files": []
}
+15
View File
@@ -0,0 +1,15 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"axios": "^1.1.2",
"laravel-vite-plugin": "^0.7.5",
"sass": "^1.69.5",
"postcss": "^8.3.7",
"vite": "^4.0.0"
}
}