Estimated -> Scheduled -> Procuring -> InProgress */ public function getProjectWorkflowState(Project $project): string { $hasEstimates = $project->materialsEstimates()->exists(); $hasTasks = $project->tasks()->exists(); if ($project->status === ProjectStatus::Completed || $project->status === ProjectStatus::Closed) { return $project->status->value; } if ($project->status === ProjectStatus::InProgress) { return 'in_progress'; } if (!$hasEstimates) { return 'planning'; // Step 1: Project Creation / Planning } if (!$hasTasks) { return 'estimated'; // Step 2: Materials Estimated, Tasks not yet scheduled } // Has estimates and tasks, check if procurement (MR/PO) has occurred return 'scheduled'; // Step 3: Tasks Scheduled & Resources assigned } /** * Guard: check if Material Requisitions can be created for this project. * Enforced strictly: Must have estimates and tasks first. */ public function canCreateRequisition(Project $project): bool { $state = $this->getProjectWorkflowState($project); // Allowed if project is scheduled, procuring, or in progress return in_array($state, ['scheduled', 'procuring', 'in_progress']) || $project->status === ProjectStatus::InProgress; } /** * Guard: check if Purchase Orders can be created. * Enforced strictly: Requires an approved material requisition for the project. */ public function canCreatePurchaseOrder(Project $project): bool { // Requisition must exist and be approved for this project $hasApprovedMr = \Modules\MaterialLogistics\Models\MaterialRequisition::where('project_id', $project->id) ->where('status', 'approved') ->exists(); return $hasApprovedMr || $project->status === ProjectStatus::InProgress; } /** * Retrieve the budget analysis for a project (Estimated vs. Requisitioned vs. PO value) */ public function getBudgetAnalysis(Project $project): array { $estimatedValue = $project->materialsEstimates->sum(fn ($est) => (float) $est->estimated_qty * (float) $est->unit_cost); // Sum labor & equipment estimates $laborEstimates = 0.00; $equipmentEstimates = 0.00; foreach ($project->tasks as $task) { $laborEstimates += $task->estimated_labor_cost; $equipmentEstimates += $task->estimated_equipment_cost; } $totalEstimated = $estimatedValue + $laborEstimates + $equipmentEstimates; // Sum actual PO costs for warehouses belonging to this project $poTotal = 0.00; $warehouseIds = \Modules\MaterialLogistics\Models\Warehouse::where('project_id', $project->id)->pluck('id'); if ($warehouseIds->isNotEmpty()) { $poTotal = (float) \Modules\MaterialLogistics\Models\PurchaseOrder::whereIn('target_warehouse_id', $warehouseIds) ->whereIn('status', ['approved', 'delivered']) ->get() ->sum('total_cost'); } return [ 'contract_value' => (float) $project->contract_value, 'materials_estimated' => $estimatedValue, 'labor_estimated' => $laborEstimates, 'equipment_estimated' => $equipmentEstimates, 'total_estimated' => $totalEstimated, 'po_total' => $poTotal, 'remaining_estimate' => max(0.00, $totalEstimated - $poTotal), 'is_over_budget' => $poTotal > $totalEstimated || $project->rollup_capitalization > $project->rollup_contract_value, ]; } }