user(); $tab = $request->get('tab', 'pending'); $isAdmin = $user->user_type === 'admin' || $user->hasRole(['Super Admin', 'admin', 'Main Contractor Admin']); $isApprovingRole = $isAdmin || $user->hasRole(['Project Manager', 'Main Contractor Admin']); $isContractorSide = $user->user_type === 'contractor' || $user->hasRole(['Contractor Admin', 'Contractor', 'Subcontractor Staff', 'Site Technical', 'Construction Supervisor']); $query = ApprovalChain::query(); // Project Managers are not authorized to review/approve project estimation requests. if ($user->hasRole('Project Manager') && !$isAdmin) { $query->where(function ($q) { $q->whereNull('chain_type') ->orWhere('chain_type', '!=', 'project_estimation'); })->where('approvable_type', '!=', Project::class); } // Contractor side users are not authorized to review/approve Material Requisitions. if ($isContractorSide && !$isAdmin) { $query->where(function ($q) { $q->whereNull('chain_type') ->orWhere('chain_type', '!=', 'material_requisition'); })->where('approvable_type', '!=', \Modules\MaterialLogistics\Models\MaterialRequisition::class); } if ($tab === 'history') { if ($isApprovingRole) { $query->whereIn('status', ['approved', 'rejected']); } else { $query->whereHas('steps', function ($q) use ($user) { $q->where('approver_id', $user->id)->where('status', '!=', 'pending'); }); } } else { if ($isApprovingRole) { $query->whereIn('status', ['pending', 'in_review']); } else { $query->whereHas('steps', function ($q) use ($user) { $q->where('approver_id', $user->id)->where('status', 'pending'); })->whereIn('status', ['pending', 'in_review']); } } $chains = $query->with([ 'steps.approver' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'name'), 'initiator' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'name'), 'approvable', ]) ->latest() ->paginate(15) ->withQueryString(); // Cash advances intentionally use their own direct pending -> approved // workflow. They must still appear in the approvals workspace without // creating an ApprovalChain record. $cashAdvanceQuery = CashAdvance::withoutGlobalScopes() ->with(['project:id,name,code', 'requester:id,name,email']); if ($tab === 'history') { $cashAdvanceQuery->whereIn('status', ['approved', 'rejected']); } else { $cashAdvanceQuery->where('status', 'pending'); } $cashAdvanceQuery->whereIn('project_id', Project::query()->select('projects.id')); if (! $isAdmin) { $cashAdvanceQuery->where('requested_by', '!=', $user->id); } return Inertia::render('ApprovalWorkflow::Approvals/Index', [ 'approvals' => $chains, 'tab' => $tab, 'cashAdvances' => $cashAdvanceQuery->latest()->get(), ]); } /** * Show a specific approval chain. */ public function show(ApprovalChain $approvalChain) { $user = request()->user(); $isAdmin = $user->user_type === 'admin' || $user->hasRole(['Super Admin', 'admin', 'Main Contractor Admin']); $isContractorSide = $user->user_type === 'contractor' || $user->hasRole(['Contractor Admin', 'Contractor', 'Subcontractor Staff', 'Site Technical', 'Construction Supervisor']); if (($approvalChain->chain_type === 'project_estimation' || $approvalChain->approvable_type === Project::class) && $user->hasRole('Project Manager') && !$isAdmin) { return redirect()->route('approvals.index')->with('error', 'Project Managers are not authorized to view or approve project estimation reviews.'); } if (($approvalChain->chain_type === 'material_requisition' || $approvalChain->approvable_type === \Modules\MaterialLogistics\Models\MaterialRequisition::class) && $isContractorSide && !$isAdmin) { return redirect()->route('approvals.index')->with('error', 'Contractor side roles are not authorized to view or approve Material Requisitions.'); } $approvalChain->load([ 'steps.approver' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'name', 'email'), 'initiator' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'name', 'email'), 'approvable', ]); if ($approvalChain->approvable) { if (method_exists($approvalChain->approvable, 'requester')) { $approvalChain->approvable->load(['requester' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'name', 'email')]); } if (method_exists($approvalChain->approvable, 'creator')) { $approvalChain->approvable->load(['creator' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'name', 'email')]); } if (method_exists($approvalChain->approvable, 'items')) { $approvalChain->approvable->load('items'); } $totalCost = $approvalChain->approvable->total_cost ?? 0; if ($approvalChain->approvable instanceof \Modules\ProjectManagement\Models\Project) { $project = $approvalChain->approvable; $project->load([ 'materialsEstimates', 'tasks.taskLabors.labor', 'tasks.taskEquipments.equipment', ]); $materialsCost = $project->materialsEstimates->reduce(function ($sum, $est) { return $sum + ($est->estimated_qty * $est->unit_cost); }, 0); $laborCost = 0; foreach ($project->tasks as $task) { foreach ($task->taskLabors as $tl) { $laborCost += ($tl->estimated_hours * ($tl->labor->hourly_rate ?? 0)); } } $equipmentCost = 0; foreach ($project->tasks as $task) { foreach ($task->taskEquipments as $te) { $equipmentCost += ($te->estimated_hours * ($te->equipment->hourly_rate ?? 0)); } } $totalCost = $materialsCost + $laborCost + $equipmentCost; } $chainNotes = $approvalChain->notes ?? $approvalChain->steps->whereNotNull('notes')->filter(fn ($s) => trim($s->notes) !== '')->last()?->notes ?? $approvalChain->approvable?->notes ?? $approvalChain->approvable?->description ?? $approvalChain->approvable?->remarks ?? $approvalChain->approvable?->reason ?? $approvalChain->approvable?->justification; if (empty($approvalChain->notes) && $chainNotes) { $approvalChain->notes = $chainNotes; } $breakdownData = [ 'document_number' => $approvalChain->approvable?->document_number ?? $approvalChain->approvable?->po_number ?? ($approvalChain->approvable instanceof \Modules\FinancialManagement\Models\FinancialInvoice ? $approvalChain->approvable->invoice_number : null) ?? ($approvalChain->approvable instanceof \Modules\ProjectManagement\Models\Project ? $approvalChain->approvable->code : null), 'total_cost' => $approvalChain->approvable instanceof \Modules\FinancialManagement\Models\FinancialInvoice ? $approvalChain->approvable->total_amount : $totalCost, 'retention_amount' => $approvalChain->approvable instanceof \Modules\FinancialManagement\Models\FinancialInvoice ? $approvalChain->approvable->retention_amount : null, 'notes' => $chainNotes ?? 'None', ]; } return Inertia::render('ApprovalWorkflow::Approvals/Show', [ 'chain' => $approvalChain, 'breakdownData' => $breakdownData, ]); } /** * Approve the current step. */ public function approve(Request $request, ApprovalChain $approvalChain) { $request->validate(['notes' => 'nullable|string|max:1000']); try { $this->approvalService->approve($approvalChain, $request->user(), $request->notes); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Step approved successfully.'); } /** * Reject the current step (rejects entire chain). */ public function reject(Request $request, ApprovalChain $approvalChain) { $request->validate(['notes' => 'required|string|max:1000']); try { $this->approvalService->reject($approvalChain, $request->user(), $request->notes); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Approval rejected.'); } }