486 lines
13 KiB
PHP
486 lines
13 KiB
PHP
<?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;
|
|
} |