fn ($userQuery) => $userQuery->withoutGlobalScopes()->select('id', 'ulid', 'name'), 'approver' => fn ($userQuery) => $userQuery->withoutGlobalScopes()->select('id', 'ulid', 'name'), 'project:id,ulid,name,code', 'requisitions:id,ulid,document_number', 'items:id,purchase_order_id,quantity,unit_cost' ]); // Purchase Orders inherit visibility from the projects available to // the authenticated site or contractor user. $query->whereIn('project_id', $this->availableProjectsQuery()->select('projects.id')); if ($request->filled('project_ulid')) { $project = Project::where('ulid', $request->project_ulid)->first(); if ($project) { $query->where('project_id', $project->id); } } $purchaseOrders = $query->latest()->paginate(20); return Inertia::render('MaterialLogistics::PurchaseOrders/Index', [ 'purchaseOrders' => $purchaseOrders, 'projects' => Project::active()->select('id', 'ulid', 'name', 'code')->get(), 'warehouses' => Warehouse::active()->select('id', 'ulid', 'name', 'code')->get(), ]); } public function create(Request $request) { $selectedProject = null; $budgetAnalysis = null; if ($request->filled('project_ulid')) { $selectedProject = Project::where('ulid', $request->project_ulid)->firstOrFail(); $workflowService = new ProjectWorkflowService(); if (!$workflowService->canCreatePurchaseOrder($selectedProject)) { return redirect()->back()->with('error', 'Cannot create Purchase Order: Project does not have approved Material Requisitions.'); } $budgetAnalysis = $workflowService->getBudgetAnalysis($selectedProject); } $materials = Material::select('id', 'ulid', 'name', 'unit', 'unit_cost') ->where('status', 'active') ->orderBy('name') ->get(); // Filter approved requisitions by project if selected $requisitionsQuery = MaterialRequisition::with(['items.material:id,ulid,name,unit,sku', 'requester:id,name']) ->where('status', 'approved') ->doesntHave('purchaseOrders'); if ($selectedProject) { $requisitionsQuery->where('project_id', $selectedProject->id); } $approvedRequisitions = $requisitionsQuery->orderByDesc('created_at')->get(); // Purchase Orders may be approved by an explicit permission or by // the system's Project Manager/executive roles. $approverIds = \App\Models\User::withoutGlobalScopes() ->where(function ($query) { $query->whereHas('roles', function ($roleQuery) { $roleQuery->whereIn('name', [ 'Project Manager', 'project_manager', 'Super Admin', 'admin', ]); }) ->orWhere('user_type', 'admin') ; }) ->where('status', 'active') ->pluck('id') ->values() ->toArray(); if (empty($approverIds)) { return back()->with('error', 'No approvers found in the system for Purchase Orders.'); } $warehousesQuery = Warehouse::active() ->whereDoesntHave('project', function ($q) { $q->whereIn('status', ['closed', 'completed']); }); if ($selectedProject) { $warehousesQuery->where('project_id', $selectedProject->id); } $warehouses = $warehousesQuery->with('project:id,ulid,name') ->select('id', 'ulid', 'name', 'code', 'project_id') ->get(); return Inertia::render('MaterialLogistics::PurchaseOrders/Form', [ 'materials' => $materials, 'approvedRequisitions' => $approvedRequisitions, 'projects' => Project::active()->select('id', 'ulid', 'name', 'code')->get(), 'selectedProject' => $selectedProject, 'budgetAnalysis' => $budgetAnalysis, 'warehouses' => $warehouses, ]); } public function store(Request $request) { $validated = $request->validate([ 'project_ulid' => 'required|string|exists:projects,ulid', 'target_warehouse_ulid' => 'required|string', 'supplier' => 'nullable|string|max:255', 'notes' => 'nullable|string|max:1000', 'requisition_ulids' => 'nullable|array', 'requisition_ulids.*' => 'string', 'items' => 'required|array|min:1', 'items.*.material_ulid' => 'required|string', 'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.unit_cost' => 'required|numeric|min:0', 'items.*.requisition_item_id' => 'nullable|integer', ]); $project = Project::where('ulid', $validated['project_ulid'])->firstOrFail(); $targetWarehouse = Warehouse::where('ulid', $validated['target_warehouse_ulid'])->firstOrFail(); if ($targetWarehouse->project_id !== $project->id) { return redirect()->back()->with('error', 'Target warehouse does not belong to the selected project.'); } $workflowService = new ProjectWorkflowService(); if (!$workflowService->canCreatePurchaseOrder($project)) { return redirect()->back()->with('error', 'Cannot create Purchase Order: Project workflow state does not permit PO creation.'); } $validationError = $this->validatePoItemsAgainstRequisitions($validated['items'], $validated['requisition_ulids'] ?? []); if ($validationError) { return redirect()->back()->withInput()->with('error', $validationError); } $docService = new DocumentNumberService(); $po = DB::transaction(function () use ($validated, $project, $targetWarehouse, $docService) { $po = PurchaseOrder::create([ 'project_id' => $project->id, 'target_warehouse_id' => $targetWarehouse->id, 'document_number' => $docService->nextPoNumber(), 'supplier' => $validated['supplier'] ?? null, 'status' => 'draft', 'requested_by' => auth()->id(), 'notes' => $validated['notes'] ?? null, ]); foreach ($validated['items'] as $item) { $material = Material::where('ulid', $item['material_ulid'])->firstOrFail(); $po->items()->create([ 'material_id' => $material->id, 'material_requisition_item_id' => $item['requisition_item_id'] ?? null, 'quantity' => $item['quantity'], 'unit_cost' => $item['unit_cost'], ]); } // Link to MRs if provided if (!empty($validated['requisition_ulids'])) { $mrIds = MaterialRequisition::whereIn('ulid', $validated['requisition_ulids']) ->pluck('id'); $po->requisitions()->attach($mrIds); } return $po; }); return redirect()->route('purchase-orders.show', $po)->with('success', 'Purchase Order created.'); } public function edit(PurchaseOrder $purchaseOrder) { if ($purchaseOrder->status !== 'draft') { return redirect()->back()->with('error', 'Only draft POs can be edited.'); } $purchaseOrder->load('items.material:id,ulid,name,unit,unit_cost', 'requisitions:id,ulid,document_number', 'targetWarehouse:id,ulid,name,code', 'project:id,ulid,name,code'); $materials = Material::select('id', 'ulid', 'name', 'unit', 'unit_cost') ->where('status', 'active') ->orderBy('name') ->get(); $approvedRequisitions = MaterialRequisition::with(['items.material:id,ulid,name,unit,sku', 'requester:id,name']) ->where('status', 'approved') ->where(function ($query) use ($purchaseOrder) { $query->doesntHave('purchaseOrders') ->orWhereHas('purchaseOrders', function ($q) use ($purchaseOrder) { $q->where('purchase_orders.id', $purchaseOrder->id); }); }) ->orderByDesc('created_at') ->get(); $budgetAnalysis = null; if ($purchaseOrder->project) { $workflowService = new ProjectWorkflowService(); $budgetAnalysis = $workflowService->getBudgetAnalysis($purchaseOrder->project); } return Inertia::render('MaterialLogistics::PurchaseOrders/Form', [ 'purchaseOrder' => $purchaseOrder, 'materials' => $materials, 'approvedRequisitions' => $approvedRequisitions, 'projects' => Project::active()->select('id', 'ulid', 'name', 'code')->get(), 'selectedProject' => $purchaseOrder->project, 'budgetAnalysis' => $budgetAnalysis, 'warehouses' => Warehouse::active()->with('project:id,ulid,name')->select('id', 'ulid', 'name', 'code', 'project_id')->get(), ]); } public function update(Request $request, PurchaseOrder $purchaseOrder) { if ($purchaseOrder->status !== 'draft') { return redirect()->back()->with('error', 'Only draft POs can be updated.'); } $validated = $request->validate([ 'project_ulid' => 'required|string|exists:projects,ulid', 'target_warehouse_ulid' => 'required|string', 'supplier' => 'nullable|string|max:255', 'notes' => 'nullable|string|max:1000', 'requisition_ulids' => 'nullable|array', 'requisition_ulids.*' => 'string', 'items' => 'required|array|min:1', 'items.*.material_ulid' => 'required|string', 'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.unit_cost' => 'required|numeric|min:0', 'items.*.requisition_item_id' => 'nullable|integer', ]); $project = Project::where('ulid', $validated['project_ulid'])->firstOrFail(); $targetWarehouse = Warehouse::where('ulid', $validated['target_warehouse_ulid'])->firstOrFail(); if ($targetWarehouse->project_id !== $project->id) { return redirect()->back()->with('error', 'Target warehouse does not belong to the selected project.'); } $workflowService = new ProjectWorkflowService(); if (!$workflowService->canCreatePurchaseOrder($project)) { return redirect()->back()->with('error', 'Cannot update Purchase Order: Project workflow state does not permit PO updates.'); } $validationError = $this->validatePoItemsAgainstRequisitions($validated['items'], $validated['requisition_ulids'] ?? []); if ($validationError) { return redirect()->back()->withInput()->with('error', $validationError); } DB::transaction(function () use ($validated, $project, $targetWarehouse, $purchaseOrder) { $purchaseOrder->update([ 'project_id' => $project->id, 'target_warehouse_id' => $targetWarehouse->id, 'supplier' => $validated['supplier'] ?? null, 'notes' => $validated['notes'] ?? null, ]); $purchaseOrder->items()->delete(); foreach ($validated['items'] as $item) { $material = Material::where('ulid', $item['material_ulid'])->firstOrFail(); $purchaseOrder->items()->create([ 'material_id' => $material->id, 'material_requisition_item_id' => $item['requisition_item_id'] ?? null, 'quantity' => $item['quantity'], 'unit_cost' => $item['unit_cost'], ]); } $purchaseOrder->requisitions()->detach(); if (!empty($validated['requisition_ulids'])) { $mrIds = MaterialRequisition::whereIn('ulid', $validated['requisition_ulids']) ->pluck('id'); $purchaseOrder->requisitions()->attach($mrIds); } }); return redirect()->route('purchase-orders.show', $purchaseOrder)->with('success', 'Purchase Order updated.'); } public function show(PurchaseOrder $purchaseOrder) { $purchaseOrder->load([ 'targetWarehouse:id,ulid,name', 'requester' => fn ($query) => $query->withoutGlobalScopes()->select('id', 'ulid', 'name'), 'approver' => fn ($query) => $query->withoutGlobalScopes()->select('id', 'ulid', 'name'), 'project:id,ulid,name,code', 'items.material' => function ($query) { $query->select('id', 'ulid', 'name', 'unit', 'unit_cost', 'type') ->with('components.component:id,name,unit'); }, 'requisitions:id,ulid,document_number', 'approvalChains.steps.approver:id,ulid,name', ]); if ($purchaseOrder->status === 'submitted' && $purchaseOrder->approvalChains->isEmpty()) { $approverIds = \App\Models\User::withoutGlobalScopes() ->where('user_type', '!=', 'contractor') ->where(function ($query) { $query->whereHas('roles', function ($roleQuery) { $roleQuery->whereIn('name', ['Project Manager', 'project_manager', 'Super Admin', 'admin', 'Main Contractor Admin']); })->orWhere('user_type', 'admin'); }) ->where('status', 'active') ->pluck('id') ->values() ->toArray(); if (!empty($approverIds)) { $approvalService = app(\Modules\ApprovalWorkflow\Services\ApprovalService::class); $approvalService->createChain($purchaseOrder, $approverIds, 'purchase_order', $purchaseOrder->created_by); $purchaseOrder->load('approvalChains.steps.approver:id,ulid,name'); } } $warehouses = Warehouse::active()->select('id', 'ulid', 'name', 'code')->get(); $budgetAnalysis = null; if ($purchaseOrder->project) { $workflowService = new ProjectWorkflowService(); $budgetAnalysis = $workflowService->getBudgetAnalysis($purchaseOrder->project); } return Inertia::render('MaterialLogistics::PurchaseOrders/Show', [ 'purchaseOrder' => $purchaseOrder, 'warehouses' => $warehouses, 'budgetAnalysis' => $budgetAnalysis, ]); } public function submitForApproval(PurchaseOrder $purchaseOrder) { if ($purchaseOrder->status !== 'draft') { return back()->with('error', 'Only draft purchase orders can be submitted.'); } // Use the same role-based approval rule during submission as on the // Purchase Order creation form. $approverIds = \App\Models\User::withoutGlobalScopes() ->where(function ($query) { $query->whereHas('roles', function ($roleQuery) { $roleQuery->whereIn('name', [ 'Project Manager', 'project_manager', 'Super Admin', 'admin', ]); }) ->orWhere('user_type', 'admin') ; }) ->where('status', 'active') ->pluck('id') ->values() ->toArray(); if (empty($approverIds)) { return back()->with('error', 'No approvers found in the system for Purchase Orders.'); } $approvalService = app(ApprovalService::class); $approvalService->createChain( $purchaseOrder, $approverIds, 'purchase_order', auth()->id(), ); $purchaseOrder->update(['status' => 'submitted']); return back()->with('success', 'Purchase Order submitted for approval.'); } public function downloadPdf(PurchaseOrder $purchaseOrder) { $purchaseOrder->load([ 'requester' => fn ($query) => $query->withoutGlobalScopes()->select('id', 'name'), 'approver' => fn ($query) => $query->withoutGlobalScopes()->select('id', 'name'), 'items.material:id,name,unit', 'requisitions:id,document_number', 'approvalChains.steps.approver:id,name', ]); $approverDetails = []; if ($purchaseOrder->approvalChains->isNotEmpty()) { $chain = $purchaseOrder->approvalChains->sortByDesc('created_at')->first(); foreach ($chain->steps as $step) { $stepStatus = $step->status instanceof \BackedEnum ? $step->status->value : $step->status; $approver = $step->approver ?: ($step->approver_id ? \App\Models\User::withoutGlobalScopes()->find($step->approver_id) : null); if ($stepStatus === 'approved' && $approver) { $approverDetails[] = [ 'name' => $approver->name, 'role' => 'Approver', 'date' => $step->acted_at?->format('M d, Y'), ]; } } } if (empty($approverDetails) && $purchaseOrder->status === 'approved' && $purchaseOrder->approver) { $approverDetails[] = [ 'name' => $purchaseOrder->approver->name, 'role' => 'Approver', 'date' => $purchaseOrder->approved_at ? $purchaseOrder->approved_at->format('M d, Y') : null, ]; } $pdf = Pdf::loadView('materiallogistics::pdf.purchase-order', [ 'purchaseOrder' => $purchaseOrder, 'approverDetails' => $approverDetails, ]); return $pdf->download("PO-{$purchaseOrder->document_number}.pdf"); } /** * JSON API: Search approved POs for the PO Lookup component. */ public function searchApproved(Request $request) { $request->validate([ 'q' => 'nullable|string|max:100', 'warehouse_ulid' => 'nullable|string', 'project_ulid' => 'nullable|string', ]); $query = PurchaseOrder::whereIn('status', ['approved', 'delivered']) ->with('items.material:id,ulid,name,unit,unit_cost'); if ($request->filled('q')) { $query->where('document_number', 'like', "%{$request->q}%"); } if ($request->filled('warehouse_ulid')) { $warehouse = Warehouse::where('ulid', $request->warehouse_ulid)->first(); if ($warehouse) { $query->where('target_warehouse_id', $warehouse->id); } } if ($request->filled('project_ulid')) { $project = Project::where('ulid', $request->project_ulid)->first(); if ($project) { $query->where('project_id', $project->id); } } $results = $query->select('id', 'ulid', 'document_number', 'supplier') ->latest() ->limit(20) ->get(); return response()->json($results); } public function markAsPaid(Request $request, PurchaseOrder $purchaseOrder) { if ($purchaseOrder->status !== 'approved') { return back()->with('error', 'Only approved purchase orders can be marked as paid.'); } if ($purchaseOrder->isPaid()) { return back()->with('error', 'This purchase order is already paid.'); } $hasFileInfo = extension_loaded('fileinfo'); $rules = [ 'receipt' => 'required|file|max:10240', 'warehouse_ulid' => 'nullable|string', ]; if ($hasFileInfo) { $rules['receipt'] .= '|mimes:pdf'; } $validated = $request->validate($rules); $file = $request->file('receipt'); if (!$hasFileInfo) { $extension = strtolower($file->getClientOriginalExtension() ?: pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION)); if ($extension !== 'pdf') { return back()->withErrors(['receipt' => 'The receipt must be a file of type: pdf, jpg, jpeg, png.']); } } // Store receipt file if ($hasFileInfo) { $path = $file->store("po-receipts/{$purchaseOrder->id}", 'public'); } else { $extension = strtolower($file->getClientOriginalExtension() ?: pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION)); $fileName = \Illuminate\Support\Str::random(40) . '.' . $extension; $path = "po-receipts/{$purchaseOrder->id}/" . $fileName; \Illuminate\Support\Facades\Storage::disk('public')->put($path, fopen($file->getRealPath(), 'r')); } DB::transaction(function () use ($purchaseOrder, $path, $file, $request) { $purchaseOrder->update([ 'payment_status' => 'paid', 'paid_at' => now(), 'receipt_path' => $path, 'receipt_original_name' => $file->getClientOriginalName(), ]); if ($request->filled('warehouse_ulid')) { $warehouse = Warehouse::where('ulid', $request->warehouse_ulid)->firstOrFail(); $purchaseOrder->update([ 'status' => 'delivered', 'target_warehouse_id' => $warehouse->id, ]); $warehouseService = app(\Modules\MaterialLogistics\Services\WarehouseService::class); $purchaseOrder->load('items.material', 'items.requisitionItem'); foreach ($purchaseOrder->items as $item) { $warehouseService->receiveStock( warehouse: $warehouse, material: $item->material, quantity: (float) $item->quantity, unitCost: (float) $item->unit_cost, purchaseOrder: $purchaseOrder, materialRequisitionId: $item->requisitionItem?->material_requisition_id, performedBy: auth()->id(), ); } } }); $message = $request->filled('warehouse_ulid') ? 'Purchase Order marked as paid and materials delivered to warehouse.' : 'Purchase Order marked as paid.'; return back()->with('success', $message); } public function markAsDelivered(Request $request, PurchaseOrder $purchaseOrder) { if ($purchaseOrder->status !== 'approved') { return back()->with('error', 'Only approved purchase orders can be marked as delivered.'); } if (!$purchaseOrder->target_warehouse_id) { return back()->with('error', 'No target warehouse assigned to this Purchase Order.'); } $request->validate([ 'delivery_notes' => 'nullable|string|max:1000', 'items' => 'nullable|array', 'items.*.id' => 'required_with:items|integer', 'items.*.delivered_quantity' => 'required_with:items|numeric|min:0', ]); $submittedItems = collect($request->input('items', []))->keyBy('id'); $hasShortages = false; $totalMissingCount = 0; DB::transaction(function () use ($purchaseOrder, $request, $submittedItems, &$hasShortages, &$totalMissingCount) { $purchaseOrder->load('items.material', 'items.requisitionItem'); $warehouseService = app(\Modules\MaterialLogistics\Services\WarehouseService::class); $warehouse = $purchaseOrder->targetWarehouse; foreach ($purchaseOrder->items as $item) { $orderedQty = (float) $item->quantity; if ($submittedItems->has($item->id)) { $deliveredQty = min($orderedQty, max(0, (float) $submittedItems[$item->id]['delivered_quantity'])); } else { $deliveredQty = $orderedQty; } $missingQty = max(0, $orderedQty - $deliveredQty); if ($missingQty > 0) { $hasShortages = true; $totalMissingCount += $missingQty; } $item->update([ 'delivered_quantity' => $deliveredQty, 'missing_quantity' => $missingQty, ]); if ($deliveredQty > 0) { $warehouseService->receiveStock( warehouse: $warehouse, material: $item->material, quantity: $deliveredQty, unitCost: (float) $item->unit_cost, purchaseOrder: $purchaseOrder, materialRequisitionId: $item->requisitionItem?->material_requisition_id, performedBy: auth()->id(), ); } } $purchaseOrder->update([ 'status' => 'delivered', 'delivered_at' => now(), 'delivery_notes' => $request->input('delivery_notes'), ]); }); $message = $hasShortages ? "Purchase Order marked as delivered. Shortage of {$totalMissingCount} unit(s) recorded and available for project re-requisition." : 'Purchase Order marked as delivered. Materials are now in the warehouse.'; return back()->with('success', $message); } public function downloadReceipt(PurchaseOrder $purchaseOrder) { if (!$purchaseOrder->hasReceipt()) { abort(404, 'No receipt uploaded.'); } $disk = \Storage::disk('public'); if (!$disk->exists($purchaseOrder->receipt_path)) { abort(404, 'Receipt file not found.'); } return $disk->download( $purchaseOrder->receipt_path, $purchaseOrder->receipt_original_name ); } public function viewReceipt(PurchaseOrder $purchaseOrder) { if (!$purchaseOrder->hasReceipt()) { abort(404, 'No receipt uploaded.'); } $disk = \Storage::disk('public'); if (!$disk->exists($purchaseOrder->receipt_path)) { abort(404, 'Receipt file not found.'); } return response()->file($disk->path($purchaseOrder->receipt_path), [ 'Content-Type' => $disk->mimeType($purchaseOrder->receipt_path) ?: 'application/octet-stream', 'Content-Disposition' => 'inline; filename="' . addslashes($purchaseOrder->receipt_original_name ?: 'receipt') . '"', ]); } public function approve(Request $request, PurchaseOrder $purchaseOrder) { $user = $request->user(); $isAdminOrPm = $user->user_type === 'admin' || $user->hasRole(['Super Admin', 'admin', 'Project Manager', 'project_manager', 'Main Contractor Admin']) || $user->can('approve_po'); if (!$isAdminOrPm) { return back()->with('error', 'Unauthorized to approve purchase orders.'); } $request->validate(['notes' => 'nullable|string|max:1000']); $chain = $purchaseOrder->approvalChains()->whereIn('status', ['pending', 'in_review'])->first(); if ($chain) { $approvalService = app(\Modules\ApprovalWorkflow\Services\ApprovalService::class); $approvalService->approve($chain, $user, $request->notes); } else { $purchaseOrder->update([ 'status' => 'approved', 'approved_by' => $user->id, 'approved_at' => now(), ]); } return back()->with('success', 'Purchase Order approved successfully.'); } public function reject(Request $request, PurchaseOrder $purchaseOrder) { $user = $request->user(); $isAdminOrPm = $user->user_type === 'admin' || $user->hasRole(['Super Admin', 'admin', 'Project Manager', 'project_manager', 'Main Contractor Admin']) || $user->can('approve_po'); if (!$isAdminOrPm) { return back()->with('error', 'Unauthorized to reject purchase orders.'); } $request->validate(['notes' => 'required|string|max:1000']); $chain = $purchaseOrder->approvalChains()->whereIn('status', ['pending', 'in_review'])->first(); if ($chain) { $approvalService = app(\Modules\ApprovalWorkflow\Services\ApprovalService::class); $approvalService->reject($chain, $user, $request->notes); } else { $purchaseOrder->update([ 'status' => 'rejected', 'notes' => $purchaseOrder->notes ? $purchaseOrder->notes . "\n[Rejection Reason]: " . $request->notes : "[Rejection Reason]: " . $request->notes, ]); $purchaseOrder->requisitions()->detach(); } return back()->with('success', 'Purchase Order rejected.'); } /** * Validate that purchase order items do not exceed the requested quantities from linked Material Requisitions. */ protected function validatePoItemsAgainstRequisitions(array $items, array $requisitionUlids = []): ?string { $hasReqUlids = !empty($requisitionUlids); $reqItemIds = array_values(array_filter(array_column($items, 'requisition_item_id'))); $hasReqItemIds = !empty($reqItemIds); if (!$hasReqUlids && !$hasReqItemIds) { return null; } // Load linked MR items $mrItems = MaterialRequisitionItem::with('material') ->where(function ($query) use ($requisitionUlids, $reqItemIds, $hasReqUlids, $hasReqItemIds) { if ($hasReqUlids) { $query->whereHas('requisition', function ($q) use ($requisitionUlids) { $q->whereIn('ulid', $requisitionUlids); }); } if ($hasReqItemIds) { if ($hasReqUlids) { $query->orWhereIn('id', $reqItemIds); } else { $query->whereIn('id', $reqItemIds); } } }) ->get(); $mrItemsById = $mrItems->keyBy('id'); $totalRequestedByMaterial = $mrItems->groupBy('material_id')->map(fn($group) => (float) $group->sum('quantity')); $totalOrderedByMaterial = []; foreach ($items as $item) { $material = Material::where('ulid', $item['material_ulid'])->first(); if (!$material) { continue; } $orderQty = (float) $item['quantity']; $totalOrderedByMaterial[$material->id] = ($totalOrderedByMaterial[$material->id] ?? 0) + $orderQty; // Direct item-level check if (!empty($item['requisition_item_id']) && isset($mrItemsById[$item['requisition_item_id']])) { $mrItem = $mrItemsById[$item['requisition_item_id']]; $maxAllowed = (float) $mrItem->quantity; if ($orderQty > $maxAllowed + 0.0001) { return "Ordered quantity (" . number_format($orderQty, 2) . ") for '{$material->name}' exceeds the requested quantity of " . number_format($maxAllowed, 2) . " {$material->unit} on the linked Material Requisition."; } } } // Aggregated material check foreach ($totalOrderedByMaterial as $materialId => $totalOrdered) { $maxRequested = (float) ($totalRequestedByMaterial[$materialId] ?? 0); if ($maxRequested > 0 && $totalOrdered > $maxRequested + 0.0001) { $material = Material::find($materialId); $materialName = $material ? $material->name : 'Material'; $unit = $material ? $material->unit : 'unit'; return "Total ordered quantity (" . number_format($totalOrdered, 2) . ") for '{$materialName}' exceeds the total requested quantity of " . number_format($maxRequested, 2) . " {$unit} across linked Material Requisitions."; } } return null; } private function availableProjectsQuery() { // Project's TenantScope is the source of truth for project visibility. return Project::query(); } }