first init
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user