613 lines
27 KiB
PHP
613 lines
27 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Inertia\Inertia;
|
|
use Illuminate\Http\Request;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
use Modules\ProjectManagement\Models\Task;
|
|
use Modules\DailyReports\Models\DailyReportLabor;
|
|
use Modules\ProjectManagement\Models\TaskActivity;
|
|
use Modules\ProjectManagement\Models\TaskDelay;
|
|
use Modules\DailyReports\Models\DailyReport;
|
|
use Modules\DailyReports\Models\DailyReportEquipment;
|
|
use Modules\DailyReports\Models\DailyReportIssue;
|
|
use App\Services\WeatherService;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class DashboardController extends Controller
|
|
{
|
|
protected WeatherService $weatherService;
|
|
|
|
public function __construct(WeatherService $weatherService)
|
|
{
|
|
$this->weatherService = $weatherService;
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$projectQuery = $request->query('project');
|
|
|
|
// Fetch all projects for the selector
|
|
$projects = Project::orderBy('name')
|
|
->get(['id', 'ulid', 'name', 'code', 'status']);
|
|
|
|
// Find selected project (by ULID or ID)
|
|
$selectedProject = null;
|
|
if ($projectQuery) {
|
|
$selectedProject = Project::where('ulid', $projectQuery)
|
|
->orWhere('id', $projectQuery)
|
|
->first();
|
|
}
|
|
|
|
// Fetch Weather
|
|
$weather = $this->getWeatherData($selectedProject);
|
|
|
|
// Use the Project model's tenant scope as the source of truth for all
|
|
// dashboard data. This keeps Contractor Admin dashboards isolated to
|
|
// their own contractor projects while preserving executive visibility.
|
|
$visibleProjectIds = Project::query()->pluck('projects.id')->all();
|
|
|
|
$user = Auth::user();
|
|
|
|
// Fetch Blockers
|
|
$blockers = $this->getBlockersData($selectedProject, $visibleProjectIds);
|
|
|
|
// Fetch Activities
|
|
$activities = $this->getActivitiesData($user, $selectedProject, $visibleProjectIds);
|
|
|
|
// Fetch Resources
|
|
$resources = $this->getResourcesData($selectedProject, $visibleProjectIds);
|
|
|
|
// Fetch Role-Tailored DB Analytics
|
|
$roleAnalytics = $this->getRoleAnalyticsData($user, $selectedProject, $visibleProjectIds);
|
|
|
|
return Inertia::render('Dashboard', [
|
|
'projects' => $projects,
|
|
'selectedProject' => $selectedProject ? [
|
|
'id' => $selectedProject->id,
|
|
'ulid' => $selectedProject->ulid,
|
|
'name' => $selectedProject->name,
|
|
'code' => $selectedProject->code,
|
|
'location' => $selectedProject->location,
|
|
'status' => $selectedProject->status?->value ?? $selectedProject->status,
|
|
] : null,
|
|
'weather' => $weather,
|
|
'activities' => $activities,
|
|
'blockers' => $blockers,
|
|
'resources' => $resources,
|
|
'roleAnalytics' => $roleAnalytics,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Resolve weather data for global or project-specific view.
|
|
*/
|
|
private function getWeatherData(?Project $project): array
|
|
{
|
|
$location = $project ? ($project->location ?? 'New York, NY') : null;
|
|
if (!$location) {
|
|
$warehouse = \Modules\MaterialLogistics\Models\Warehouse::where('code', 'WH-CEN-01')->first();
|
|
$location = $warehouse->address ?? 'New York, NY';
|
|
}
|
|
|
|
$cacheKey = 'weather_' . md5($location . ($project ? '_proj' : '_global'));
|
|
return Cache::remember($cacheKey, 1800, function () use ($location, $project) {
|
|
$weather = $this->weatherService->getWeather($location);
|
|
if (!$project) {
|
|
$weather['forecast'] = 'Global overview based at Central Depot. ' . ($weather['forecast'] ?? '');
|
|
}
|
|
return $weather;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get list of critical blockers from TaskDelays and DailyReportIssues.
|
|
*/
|
|
private function getBlockersData(?Project $project, array $visibleProjectIds): array
|
|
{
|
|
$blockersList = [];
|
|
|
|
// 1. Fetch Task Delays with scoped relation columns
|
|
$delayQuery = TaskDelay::with(['task:id,ulid,project_id,name', 'task.project:id,ulid,name'])
|
|
->whereHas('task', function ($q) use ($project, $visibleProjectIds) {
|
|
if ($project) {
|
|
$q->where('project_id', $project->id);
|
|
} else {
|
|
$q->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
})
|
|
->orderByDesc('delay_date')
|
|
->take(10);
|
|
|
|
foreach ($delayQuery->get() as $delay) {
|
|
$projName = $delay->task->project->name ?? 'Unknown Project';
|
|
$projUlid = $delay->task->project->ulid ?? null;
|
|
$urgency = ($delay->lost_hours >= 8) ? 'critical' : 'high';
|
|
|
|
// Map reason_type to RFI / Material / Safety / Equipment / Regulatory
|
|
$type = match ($delay->reason_type?->value ?? $delay->reason_type) {
|
|
'supply' => 'Material',
|
|
'manpower' => 'Labor',
|
|
'equipment' => 'Equipment',
|
|
'permit' => 'Regulatory',
|
|
'weather' => 'Safety', // weather delays represent safety concerns for high work
|
|
default => 'RFI',
|
|
};
|
|
|
|
$blockersList[] = [
|
|
'id' => 'delay_' . $delay->id,
|
|
'project_ulid' => $projUlid,
|
|
'task_ulid' => $delay->task->ulid ?? null,
|
|
'type' => $type,
|
|
'title' => sprintf('[%s] %s: %s', $projName, $delay->task->name, $delay->notes ?? 'Delay recorded'),
|
|
'urgency' => $urgency,
|
|
'date' => $delay->delay_date?->format('Y-m-d') ?? now()->format('Y-m-d'),
|
|
];
|
|
}
|
|
|
|
// 2. Fetch Daily Report Issues with scoped relation columns
|
|
$issueQuery = DailyReportIssue::with(['dailyReport:id,project_id', 'dailyReport.project:id,ulid,name'])
|
|
->whereHas('dailyReport', function ($q) use ($project, $visibleProjectIds) {
|
|
if ($project) {
|
|
$q->where('project_id', $project->id);
|
|
} else {
|
|
$q->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
})
|
|
->orderByDesc('created_at')
|
|
->take(10);
|
|
|
|
foreach ($issueQuery->get() as $issue) {
|
|
$projName = $issue->dailyReport->project->name ?? 'Unknown Project';
|
|
$projUlid = $issue->dailyReport->project->ulid ?? null;
|
|
$severity = strtolower($issue->severity ?? 'medium');
|
|
$urgency = in_array($severity, ['high', 'critical']) ? 'critical' : 'high';
|
|
|
|
$type = match (strtolower($issue->category ?? '')) {
|
|
'safety' => 'Safety',
|
|
'rfi' => 'RFI',
|
|
'material' => 'Material',
|
|
'equipment' => 'Equipment',
|
|
default => 'Safety',
|
|
};
|
|
|
|
$blockersList[] = [
|
|
'id' => 'issue_' . $issue->id,
|
|
'project_ulid' => $projUlid,
|
|
'daily_report_id' => $issue->daily_report_id,
|
|
'type' => $type,
|
|
'title' => sprintf('[%s] %s', $projName, $issue->description ?? $issue->issue_description ?? 'Site Issue reported'),
|
|
'urgency' => $urgency,
|
|
'status' => $issue->status ?? 'open',
|
|
'supervisor_name' => $issue->supervisor_name,
|
|
'date' => $issue->created_at?->format('Y-m-d') ?? now()->format('Y-m-d'),
|
|
];
|
|
}
|
|
|
|
// Sort blockers by critical urgency first, then date descending
|
|
usort($blockersList, function ($a, $b) {
|
|
if ($a['urgency'] === 'critical' && $b['urgency'] !== 'critical') {
|
|
return -1;
|
|
}
|
|
if ($b['urgency'] === 'critical' && $a['urgency'] !== 'critical') {
|
|
return 1;
|
|
}
|
|
return strcmp($b['date'], $a['date']);
|
|
});
|
|
|
|
// Cap at 6 blockers
|
|
return array_slice($blockersList, 0, 6);
|
|
}
|
|
|
|
/**
|
|
* Fetch activity feed from TaskActivity and DailyReports (or contractor-specific events for contractors).
|
|
*/
|
|
private function getActivitiesData(?\App\Models\User $user, ?Project $project, array $visibleProjectIds): array
|
|
{
|
|
$activitiesList = [];
|
|
|
|
// If the user is a contractor, fetch only their contractor-specific bid invitations, submissions, and invoice claims
|
|
$isContractorUser = $user && ($user->user_type === 'contractor' || $user->contractor_id !== null || $user->hasRole('Main Contractor Admin') || $user->hasRole('Contractor Admin'));
|
|
|
|
if ($isContractorUser && $user->contractor_id) {
|
|
// 1. Contractor Invoices
|
|
$invoices = \Modules\ContractorManagement\Models\ContractorInvoice::with('project')
|
|
->where('contractor_id', $user->contractor_id)
|
|
->where(function ($q) use ($project, $visibleProjectIds) {
|
|
if ($project) {
|
|
$q->where('project_id', $project->id);
|
|
} elseif (!empty($visibleProjectIds)) {
|
|
$q->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
})
|
|
->orderByDesc('updated_at')
|
|
->take(5)
|
|
->get();
|
|
|
|
foreach ($invoices as $inv) {
|
|
$projCode = $inv->project->code ?? '';
|
|
$statusVal = $inv->status instanceof \Modules\ContractorManagement\Enums\InvoiceStatus
|
|
? $inv->status->value
|
|
: (string) ($inv->status ?? '');
|
|
$activitiesList[] = [
|
|
'id' => 'inv_act_' . $inv->id,
|
|
'time' => $inv->updated_at?->diffForHumans() ?? 'Recently',
|
|
'timestamp' => $inv->updated_at?->timestamp ?? 0,
|
|
'title' => sprintf('Invoice %s status: %s (%s)', $inv->invoice_number, str_replace('_', ' ', $statusVal), $projCode),
|
|
'status' => $statusVal === 'paid' ? 'completed' : 'info',
|
|
];
|
|
}
|
|
|
|
// Sort combined list by timestamp descending
|
|
usort($activitiesList, function ($a, $b) {
|
|
return $b['timestamp'] <=> $a['timestamp'];
|
|
});
|
|
|
|
return array_slice($activitiesList, 0, 10);
|
|
}
|
|
|
|
if (empty($visibleProjectIds) && !$project) {
|
|
return [];
|
|
}
|
|
|
|
// 1. Task Activities (Internal Managers & Site Team)
|
|
$taskActivityQuery = TaskActivity::with(['task.project', 'user'])
|
|
->whereHas('task', function ($q) use ($project, $visibleProjectIds) {
|
|
if ($project) {
|
|
$q->where('project_id', $project->id);
|
|
} else {
|
|
$q->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
})
|
|
->orderByDesc('created_at')
|
|
->take(15);
|
|
|
|
foreach ($taskActivityQuery->get() as $act) {
|
|
$projCode = $act->task->project->code ?? '';
|
|
$userName = $act->user->name ?? 'System';
|
|
|
|
$activitiesList[] = [
|
|
'id' => 'task_act_' . $act->id,
|
|
'time' => $act->created_at?->diffForHumans() ?? 'Just now',
|
|
'timestamp' => $act->created_at?->timestamp ?? 0,
|
|
'title' => sprintf('%s updated %s: %s (%s)', $userName, $act->task->name, $act->description, $projCode),
|
|
'status' => $act->type ?? 'info',
|
|
];
|
|
}
|
|
|
|
// 2. Daily Report Submissions
|
|
$dailyReportQuery = DailyReport::with(['project', 'user'])
|
|
->where(function ($q) use ($project, $visibleProjectIds) {
|
|
if ($project) {
|
|
$q->where('project_id', $project->id);
|
|
} else {
|
|
$q->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
})
|
|
->orderByDesc('report_date')
|
|
->take(10);
|
|
|
|
foreach ($dailyReportQuery->get() as $report) {
|
|
$projName = $report->project->name ?? 'Unknown';
|
|
$userName = $report->user->name ?? 'Reporter';
|
|
|
|
$activitiesList[] = [
|
|
'id' => 'report_' . $report->id,
|
|
'time' => $report->report_date?->diffForHumans() ?? 'Recently',
|
|
'timestamp' => $report->report_date?->timestamp ?? 0,
|
|
'title' => sprintf('%s submitted Daily Report %s for %s', $userName, $report->report_number, $projName),
|
|
'status' => 'completed',
|
|
];
|
|
}
|
|
|
|
// 3. Task Status transitions (if no TaskActivity records exist, fetch tasks that changed status recently)
|
|
if (empty($activitiesList)) {
|
|
$tasksQuery = Task::with('project')
|
|
->where(function ($q) use ($project, $visibleProjectIds) {
|
|
if ($project) {
|
|
$q->where('project_id', $project->id);
|
|
} else {
|
|
$q->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
})
|
|
->orderByDesc('updated_at')
|
|
->take(5);
|
|
|
|
foreach ($tasksQuery->get() as $task) {
|
|
$statusLabel = $task->status?->label() ?? $task->status;
|
|
$activitiesList[] = [
|
|
'id' => 'task_status_' . $task->id,
|
|
'time' => $task->updated_at?->diffForHumans() ?? 'Recently',
|
|
'timestamp' => $task->updated_at?->timestamp ?? 0,
|
|
'title' => sprintf('Task "%s" is in status %s (%s)', $task->name, $statusLabel, $task->project->code ?? ''),
|
|
'status' => strtolower($statusLabel) === 'completed' ? 'completed' : 'in_progress',
|
|
];
|
|
}
|
|
}
|
|
|
|
// Sort combined list by timestamp descending
|
|
usort($activitiesList, function ($a, $b) {
|
|
return $b['timestamp'] <=> $a['timestamp'];
|
|
});
|
|
|
|
return array_slice($activitiesList, 0, 10);
|
|
}
|
|
|
|
/**
|
|
* Compile deployed labor and current equipment from active project data.
|
|
*/
|
|
private function getResourcesData(?Project $project, array $visibleProjectIds): array
|
|
{
|
|
$laborExpected = 0;
|
|
$laborActual = 0;
|
|
$trades = [];
|
|
$equipmentActive = 0;
|
|
$equipmentMaintenance = 0;
|
|
$equipmentIdle = 0;
|
|
$equipmentList = [];
|
|
|
|
// Global resource roll-ups should represent active execution only.
|
|
// A selected project intentionally narrows the dashboard to that project.
|
|
$projectIds = $project
|
|
? [$project->id]
|
|
: Project::whereIn('projects.id', $visibleProjectIds)
|
|
->whereIn('status', ['active', 'planning', 'in_progress'])
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Labor totals come from every Daily Report labor entry for the active
|
|
// project set. This intentionally includes all stored report records.
|
|
$laborLogs = DailyReportLabor::whereHas('dailyReport', function ($query) use ($projectIds) {
|
|
$query->whereIn('project_id', $projectIds);
|
|
})->get();
|
|
|
|
foreach ($laborLogs as $log) {
|
|
$laborActual += (int) $log->workers_count;
|
|
$tradeName = $log->trade ?? 'General Labor';
|
|
$trades[$tradeName] = ($trades[$tradeName] ?? 0) + (int) $log->workers_count;
|
|
}
|
|
|
|
// Read equipment entries from every daily report in the project set.
|
|
// ResourceSummary must represent all stored daily-report records, not
|
|
// only the latest report for each project.
|
|
$reports = DailyReport::whereIn('project_id', $projectIds)
|
|
->with('equipment')
|
|
->get();
|
|
|
|
foreach ($reports as $report) {
|
|
foreach ($report->equipment as $log) {
|
|
$status = strtolower($log->status ?? 'active');
|
|
if (str_contains($status, 'active') || str_contains($status, 'use') || str_contains($status, 'operat')) {
|
|
$equipmentActive++;
|
|
$eqStatus = 'active';
|
|
} elseif (str_contains($status, 'main') || str_contains($status, 'repair') || str_contains($status, 'break')) {
|
|
$equipmentMaintenance++;
|
|
$eqStatus = 'maintenance';
|
|
} else {
|
|
$equipmentIdle++;
|
|
$eqStatus = 'idle';
|
|
}
|
|
|
|
$equipmentList[] = [
|
|
'name' => $log->equipment_name ?? 'Equipment',
|
|
'status' => $eqStatus,
|
|
];
|
|
}
|
|
}
|
|
|
|
if ($laborActual > 0) {
|
|
$laborExpected = max($laborActual, (int) ceil($laborActual * 1.15));
|
|
}
|
|
|
|
return [
|
|
'labor' => [
|
|
'expected' => $laborExpected,
|
|
'actual' => $laborActual,
|
|
'trades' => $trades
|
|
],
|
|
'equipment' => [
|
|
'active' => $equipmentActive,
|
|
'maintenance' => $equipmentMaintenance,
|
|
'idle' => $equipmentIdle,
|
|
'list' => $equipmentList
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Compute role-tailored analytics metrics from database tables.
|
|
*/
|
|
private function getRoleAnalyticsData(?\App\Models\User $user, ?Project $project, array $visibleProjectIds): array
|
|
{
|
|
$roleName = $user ? ($user->roles->first()?->name ?? 'User') : 'User';
|
|
$userType = $user->user_type ?? 'employee';
|
|
|
|
$pQuery = function ($query) use ($project) {
|
|
if ($project) {
|
|
$query->where('project_id', $project->id);
|
|
}
|
|
};
|
|
|
|
// 1. Executive / Super Admin / Admin Analytics (Single-Pass Aggregations)
|
|
$invoicesQuery = \Modules\FinancialManagement\Models\FinancialInvoice::query();
|
|
if ($project) {
|
|
$invoicesQuery->where('project_id', $project->id);
|
|
} else {
|
|
$invoicesQuery->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
$invoiceStats = (clone $invoicesQuery)
|
|
->selectRaw("SUM(subtotal) as total_billed, SUM(CASE WHEN status = 'paid' THEN paid_amount ELSE 0 END) as total_paid, SUM(CASE WHEN status IN ('approved', 'sent', 'payment_sent', 'paid') THEN retention_amount ELSE 0 END) as total_retention_fallback")
|
|
->first();
|
|
$totalBilled = (float) ($invoiceStats->total_billed ?? 0);
|
|
$totalPaid = (float) ($invoiceStats->total_paid ?? 0);
|
|
$retentionFallback = (float) ($invoiceStats->total_retention_fallback ?? 0);
|
|
|
|
$retentionLedgerQuery = \Modules\FinancialManagement\Models\RetentionEntry::query();
|
|
if ($project) {
|
|
$retentionLedgerQuery->where('project_id', $project->id);
|
|
} else {
|
|
$retentionLedgerQuery->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
$retentionStats = (clone $retentionLedgerQuery)
|
|
->selectRaw("SUM(CASE WHEN type = 'debit' THEN amount ELSE 0 END) as held_debit, SUM(CASE WHEN type = 'credit' AND status IN ('posted', 'paid') THEN amount ELSE 0 END) as released_credit")
|
|
->first();
|
|
$heldDebit = (float) ($retentionStats->held_debit ?? 0);
|
|
$releasedCredit = (float) ($retentionStats->released_credit ?? 0);
|
|
$totalRetention = max(0, $heldDebit - $releasedCredit);
|
|
|
|
if ($totalRetention == 0 && $retentionFallback > 0) {
|
|
$totalRetention = $retentionFallback;
|
|
}
|
|
|
|
$pendingApprovalsQuery = \Modules\ApprovalWorkflow\Models\ApprovalChain::whereIn('status', ['in_review', 'pending']);
|
|
if ($user->hasRole('Project Manager') && !($user->user_type === 'admin' || $user->hasRole(['Super Admin', 'admin', 'Main Contractor Admin']))) {
|
|
$pendingApprovalsQuery->where(function ($q) {
|
|
$q->whereNull('type')
|
|
->orWhere('type', '!=', 'project_estimation');
|
|
})->where('approvable_type', '!=', Project::class);
|
|
}
|
|
$pendingApprovalsCount = $pendingApprovalsQuery->count();
|
|
|
|
$projectsAnalyticsQuery = Project::query();
|
|
if ($project) {
|
|
$projectsAnalyticsQuery->whereKey($project->id);
|
|
} else {
|
|
$projectsAnalyticsQuery->whereIn('projects.id', $visibleProjectIds);
|
|
}
|
|
$projectStats = (clone $projectsAnalyticsQuery)
|
|
->selectRaw("COUNT(*) as total_count, SUM(CASE WHEN status IN ('planning', 'in_progress') THEN 1 ELSE 0 END) as active_count")
|
|
->first();
|
|
$totalProjectsCount = (int) ($projectStats->total_count ?? 0);
|
|
$activeProjectsCount = (int) ($projectStats->active_count ?? 0);
|
|
|
|
// 2. PM Milestone & Progress Analytics (Scoped Relation Columns)
|
|
$milestonesQuery = \Modules\ProjectManagement\Models\ProjectMilestone::with('tasks:id,milestone_id,status');
|
|
if ($project) {
|
|
$milestonesQuery->where('project_id', $project->id);
|
|
} else {
|
|
$milestonesQuery->whereIn('project_id', $visibleProjectIds);
|
|
}
|
|
$allMilestones = $milestonesQuery->get();
|
|
$totalMilestones = $allMilestones->count();
|
|
$completedMilestones = 0;
|
|
$inProgressMilestones = 0;
|
|
|
|
foreach ($allMilestones as $m) {
|
|
$taskCount = $m->tasks->count();
|
|
$completedTaskCount = $m->tasks->filter(function ($t) {
|
|
$st = is_object($t->status) ? $t->status->value : $t->status;
|
|
return in_array(strtolower($st ?? ''), ['completed', 'closed', 'done']);
|
|
})->count();
|
|
|
|
$isCompleted = ($m->actual_date !== null) || ($taskCount > 0 && $completedTaskCount === $taskCount);
|
|
|
|
if ($isCompleted) {
|
|
$completedMilestones++;
|
|
} else {
|
|
$hasActivity = $m->planned_date !== null || $m->tasks->contains(function ($t) {
|
|
$st = is_object($t->status) ? $t->status->value : $t->status;
|
|
return in_array(strtolower($st ?? ''), ['in_progress', 'completed', 'closed', 'done']);
|
|
});
|
|
if ($hasActivity) {
|
|
$inProgressMilestones++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Warehouse & Technical Inventory (Cached Short TTL)
|
|
$totalItems = Cache::remember('master_materials_count', 300, fn () => \Modules\MasterData\Models\Material::count());
|
|
$totalWarehouses = Cache::remember('master_warehouses_count', 300, fn () => \Modules\MaterialLogistics\Models\Warehouse::count());
|
|
$totalDocuments = \Modules\DocumentManagement\Models\Document::whereIn('project_id', $project ? [$project->id] : $visibleProjectIds)->count();
|
|
$pendingCashAdvances = \Modules\FinancialManagement\Models\CashAdvance::where('status', 'pending')->count();
|
|
|
|
// 5. Unconfirmed Payments
|
|
$pendingInvoicesList = \Modules\FinancialManagement\Models\FinancialInvoice::with('project:id,name,code')
|
|
->whereIn('project_id', $visibleProjectIds)
|
|
->where('status', 'payment_sent')
|
|
->get(['id', 'ulid', 'invoice_number', 'project_id', 'total_amount', 'status'])
|
|
->map(fn ($inv) => [
|
|
'id' => 'inv_' . $inv->id,
|
|
'ulid' => $inv->ulid,
|
|
'number' => $inv->invoice_number,
|
|
'project_name' => $inv->project?->name ?? 'Project',
|
|
'amount' => (float) $inv->total_amount,
|
|
'type' => 'invoice',
|
|
'confirm_url' => route('finance.confirm-payment', $inv->ulid),
|
|
'view_url' => route('finance.show', $inv->ulid),
|
|
]);
|
|
|
|
$pendingRetentionsList = \Modules\FinancialManagement\Models\RetentionEntry::with('project:id,name,code')
|
|
->whereIn('project_id', $visibleProjectIds)
|
|
->where('type', 'credit')
|
|
->whereIn('status', ['payment_sent', 'submitted'])
|
|
->get(['id', 'ulid', 'project_id', 'amount', 'status', 'description'])
|
|
->map(fn ($ret) => [
|
|
'id' => 'ret_' . $ret->id,
|
|
'ulid' => $ret->ulid,
|
|
'number' => 'Retention Release',
|
|
'project_name' => $ret->project?->name ?? 'Project',
|
|
'amount' => (float) $ret->amount,
|
|
'type' => 'retention',
|
|
'status' => is_object($ret->status) ? ($ret->status->value ?? (string) $ret->status) : (string) $ret->status,
|
|
'confirm_url' => route('retention.confirm', $ret->ulid),
|
|
'view_url' => route('retention.index'),
|
|
]);
|
|
|
|
$mergedPayments = $pendingInvoicesList->concat($pendingRetentionsList);
|
|
|
|
$rolesList = $user?->getRoleNames()->map(fn($r) => strtolower($r))->all() ?? [strtolower($roleName)];
|
|
$isExecutiveRole = in_array('super admin', $rolesList) || in_array('admin', $rolesList);
|
|
$isContractorRole = in_array('main contractor admin', $rolesList) || collect($rolesList)->contains(fn($r) => str_contains($r, 'contractor'));
|
|
$isPMRole = in_array('project manager', $rolesList) || in_array('project_manager', $rolesList);
|
|
|
|
if ($isExecutiveRole || ($userType === 'admin' && !$isContractorRole && !$isPMRole)) {
|
|
$dashboardType = 'executive';
|
|
} elseif ($isPMRole) {
|
|
$dashboardType = 'pm';
|
|
} elseif ($userType === 'contractor' || $isContractorRole) {
|
|
$dashboardType = 'contractor';
|
|
} else {
|
|
$dashboardType = 'site';
|
|
}
|
|
|
|
return [
|
|
'role' => $roleName,
|
|
'roles' => $user?->getRoleNames()->values()->all() ?? [],
|
|
'user_type' => $userType,
|
|
'dashboard_type' => $dashboardType,
|
|
'financials' => [
|
|
'total_billed' => $totalBilled,
|
|
'total_paid' => $totalPaid,
|
|
'total_retention' => $totalRetention,
|
|
'outstanding_balance' => max(0, $totalBilled - $totalPaid),
|
|
],
|
|
'projects' => [
|
|
'total' => $totalProjectsCount,
|
|
'active' => $activeProjectsCount,
|
|
],
|
|
'approvals' => [
|
|
'pending' => $pendingApprovalsCount,
|
|
],
|
|
'milestones' => [
|
|
'total' => $totalMilestones,
|
|
'completed' => $completedMilestones,
|
|
'in_progress' => $inProgressMilestones,
|
|
'completion_rate' => $totalMilestones > 0 ? round(($completedMilestones / $totalMilestones) * 100, 1) : 0,
|
|
],
|
|
'logistics' => [
|
|
'catalog_items' => $totalItems,
|
|
'warehouses' => $totalWarehouses,
|
|
'documents' => $totalDocuments,
|
|
'pending_cash_advances' => $pendingCashAdvances,
|
|
],
|
|
'unconfirmed_payments' => [
|
|
'count' => $mergedPayments->count(),
|
|
'total_amount' => (float) $mergedPayments->sum('amount'),
|
|
'items' => $mergedPayments->values()->all(),
|
|
],
|
|
];
|
|
}
|
|
}
|