latest() ->paginate(15); return Inertia::render('MaterialLogistics::Transfers/Index', [ 'transfers' => $transfers ]); } public function create() { $projects = Project::select('id', 'name')->get(); return Inertia::render('MaterialLogistics::Transfers/Create', [ 'projects' => $projects, ]); } public function projectInventory($projectId) { $inventory = \Modules\MaterialLogistics\Models\ProjectInventory::with('material:id,name,sku,unit') ->where('project_id', $projectId) ->where('on_hand_qty', '>', 0) ->get() ->map(function ($inv) { return [ 'id' => $inv->id, 'material_id' => $inv->material_id, 'material_name' => $inv->material->name, 'material_sku' => $inv->material->sku, 'material_unit' => $inv->material->unit, 'available_qty' => $inv->available_qty, ]; }); return response()->json($inventory); } public function store(Request $request) { $validated = $request->validate([ 'from_project_id' => 'required|exists:projects,id|different:to_project_id', 'to_project_id' => 'required|exists:projects,id', 'notes' => 'nullable|string', 'items' => 'required|array|min:1', 'items.*.material_id' => 'required|exists:materials,id', 'items.*.requested_quantity' => 'required|numeric|min:0.01', 'items.*.notes' => 'nullable|string', ]); DB::transaction(function () use ($validated, $request) { $transfer = MaterialTransfer::create([ 'transfer_number' => MaterialTransfer::generateTransferNumber(), 'from_project_id' => $validated['from_project_id'], 'to_project_id' => $validated['to_project_id'], 'status' => MaterialTransferStatus::Draft, 'requested_by' => $request->user()->id, 'notes' => $validated['notes'] ?? null, ]); foreach ($validated['items'] as $item) { $transfer->items()->create([ 'material_id' => $item['material_id'], 'requested_quantity' => $item['requested_quantity'], 'notes' => $item['notes'] ?? null, ]); } }); return redirect()->route('material-transfers.index')->with('success', 'Transfer draft created successfully.'); } public function show(MaterialTransfer $materialTransfer) { $materialTransfer->load(['fromProject', 'toProject', 'requester', 'approver', 'dispatcher', 'receiver', 'items.material']); return Inertia::render('MaterialLogistics::Transfers/Show', [ 'transfer' => $materialTransfer ]); } public function edit(MaterialTransfer $materialTransfer) { abort_if($materialTransfer->status !== MaterialTransferStatus::Draft, 403, 'Only draft transfers can be edited.'); $materialTransfer->load('items.material'); $projects = Project::select('id', 'name')->get(); $materials = Material::select('id', 'name', 'sku', 'unit')->get(); return Inertia::render('MaterialLogistics::Transfers/Edit', [ 'transfer' => $materialTransfer, 'projects' => $projects, 'materials' => $materials, ]); } public function update(Request $request, MaterialTransfer $materialTransfer) { abort_if($materialTransfer->status !== MaterialTransferStatus::Draft, 403, 'Only draft transfers can be edited.'); $validated = $request->validate([ 'from_project_id' => 'required|exists:projects,id|different:to_project_id', 'to_project_id' => 'required|exists:projects,id', 'notes' => 'nullable|string', 'items' => 'required|array|min:1', 'items.*.id' => 'nullable|exists:material_transfer_items,id', 'items.*.material_id' => 'required|exists:materials,id', 'items.*.requested_quantity' => 'required|numeric|min:0.01', 'items.*.notes' => 'nullable|string', ]); DB::transaction(function () use ($validated, $materialTransfer) { $materialTransfer->update([ 'from_project_id' => $validated['from_project_id'], 'to_project_id' => $validated['to_project_id'], 'notes' => $validated['notes'] ?? null, ]); // Keep track of which items are updated to delete removed ones $keptItemIds = []; foreach ($validated['items'] as $itemData) { if (isset($itemData['id'])) { $item = $materialTransfer->items()->find($itemData['id']); $item->update([ 'material_id' => $itemData['material_id'], 'requested_quantity' => $itemData['requested_quantity'], 'notes' => $itemData['notes'] ?? null, ]); $keptItemIds[] = $item->id; } else { $newItem = $materialTransfer->items()->create([ 'material_id' => $itemData['material_id'], 'requested_quantity' => $itemData['requested_quantity'], 'notes' => $itemData['notes'] ?? null, ]); $keptItemIds[] = $newItem->id; } } $materialTransfer->items()->whereNotIn('id', $keptItemIds)->delete(); }); return redirect()->route('material-transfers.show', $materialTransfer)->with('success', 'Transfer draft updated successfully.'); } public function destroy(MaterialTransfer $materialTransfer) { abort_if($materialTransfer->status !== MaterialTransferStatus::Draft, 403, 'Only draft transfers can be deleted.'); $materialTransfer->delete(); return redirect()->route('material-transfers.index')->with('success', 'Transfer deleted successfully.'); } // --- Action Methods --- public function submit(Request $request, MaterialTransfer $materialTransfer, \Modules\ApprovalWorkflow\Services\ApprovalService $approvalService) { abort_if($materialTransfer->status !== MaterialTransferStatus::Draft, 403, 'Only draft transfers can be submitted.'); $approverIds = \App\Models\User::role('Project Manager')->pluck('id')->toArray(); if (empty($approverIds)) { $approverIds = [1]; // Fallback to admin/first user } $approvalService->createChain( $materialTransfer, $approverIds, 'material_transfer', $request->user()->id, ); $materialTransfer->update([ 'status' => MaterialTransferStatus::PendingApproval, ]); return back()->with('success', 'Transfer submitted for approval.'); } public function dispatch(Request $request, MaterialTransfer $materialTransfer) { abort_if($materialTransfer->status !== MaterialTransferStatus::Approved, 403, 'Only approved transfers can be dispatched.'); $validated = $request->validate([ 'items' => 'required|array', 'items.*.id' => 'required|exists:material_transfer_items,id', 'items.*.dispatched_quantity' => 'required|numeric|min:0', ]); DB::transaction(function () use ($validated, $materialTransfer, $request) { foreach ($validated['items'] as $itemData) { $item = $materialTransfer->items()->find($itemData['id']); $item->update([ 'dispatched_quantity' => $itemData['dispatched_quantity'], ]); if ($itemData['dispatched_quantity'] > 0) { // Generate Dispatch Inventory Movement (OUT from from_project_id) // Wait, we need the current balance to calculate balance_after. For simplicity, setting 0 here, or we can look it up. // Assuming inventory system handles balance calculation via observers or similar. InventoryMovement::create([ 'material_id' => $item->material_id, 'project_id' => $materialTransfer->from_project_id, 'movement_type' => MovementType::Dispatch, 'direction' => 'out', 'quantity' => $itemData['dispatched_quantity'], 'from_location_type' => 'project', 'from_location_id' => $materialTransfer->from_project_id, 'to_location_type' => 'project', 'to_location_id' => $materialTransfer->to_project_id, 'reference_type' => MaterialTransfer::class, 'reference_id' => $materialTransfer->id, 'dispatched_by' => $request->user()->id, 'performed_by' => $request->user()->id, ]); } } $materialTransfer->update([ 'status' => MaterialTransferStatus::InTransit, 'dispatched_by' => $request->user()->id, ]); }); return back()->with('success', 'Transfer dispatched successfully.'); } public function receive(Request $request, MaterialTransfer $materialTransfer) { abort_if($materialTransfer->status !== MaterialTransferStatus::InTransit, 403, 'Only in-transit transfers can be received.'); $validated = $request->validate([ 'items' => 'required|array', 'items.*.id' => 'required|exists:material_transfer_items,id', 'items.*.received_quantity' => 'required|numeric|min:0', 'items.*.damaged_quantity' => 'required|numeric|min:0', 'items.*.lost_quantity' => 'required|numeric|min:0', ]); DB::transaction(function () use ($validated, $materialTransfer, $request) { foreach ($validated['items'] as $itemData) { $item = $materialTransfer->items()->find($itemData['id']); // Validate that dispatched == received + damaged + lost $totalAccounted = $itemData['received_quantity'] + $itemData['damaged_quantity'] + $itemData['lost_quantity']; // Not enforcing strict sum here to allow flexibility, but ideally $totalAccounted <= $item->dispatched_quantity $item->update([ 'received_quantity' => $itemData['received_quantity'], 'damaged_quantity' => $itemData['damaged_quantity'], 'lost_quantity' => $itemData['lost_quantity'], ]); if ($itemData['received_quantity'] > 0) { // Generate Receipt Inventory Movement (IN to to_project_id) InventoryMovement::create([ 'material_id' => $item->material_id, 'project_id' => $materialTransfer->to_project_id, 'movement_type' => MovementType::SiteReceipt, 'direction' => 'in', 'quantity' => $itemData['received_quantity'], 'from_location_type' => 'project', 'from_location_id' => $materialTransfer->from_project_id, 'to_location_type' => 'project', 'to_location_id' => $materialTransfer->to_project_id, 'reference_type' => MaterialTransfer::class, 'reference_id' => $materialTransfer->id, 'received_by' => $request->user()->id, 'performed_by' => $request->user()->id, ]); } $lostOrDamaged = $itemData['damaged_quantity'] + $itemData['lost_quantity']; if ($lostOrDamaged > 0) { // Technically it left Project A, but never arrived in Project B. // Project A's inventory is already deducted. // We generate an explicit adjustment to log the loss for Project B or transit account. // For now, we will associate it with Project B as an 'out' adjustment to balance any 'in' that was expected, or simply log it. // Actually, since it never entered Project B, no inventory is strictly deducted from B. // Just logging an Adjustment record for tracking. InventoryMovement::create([ 'material_id' => $item->material_id, 'project_id' => $materialTransfer->to_project_id, 'movement_type' => MovementType::Adjustment, 'direction' => 'out', // represents loss 'quantity' => $lostOrDamaged, 'from_location_type' => 'project', 'from_location_id' => $materialTransfer->to_project_id, 'to_location_type' => 'external', // Lost/Damaged 'to_location_id' => 0, 'reference_type' => MaterialTransfer::class, 'reference_id' => $materialTransfer->id, 'notes' => 'Lost or Damaged during transfer from Project A', 'adjusted_by' => $request->user()->id, 'performed_by' => $request->user()->id, ]); } } // Financial Capitalization Ledger Sync: // Calculate total monetary value of received items and adjust project float $totalMonetaryValue = 0; foreach ($materialTransfer->items as $item) { $unitCost = $item->material->unit_cost ?? 0; $receivedQty = $item->received_quantity ?? 0; $totalMonetaryValue += ($receivedQty * $unitCost); } if ($totalMonetaryValue > 0) { $fromProject = Project::find($materialTransfer->from_project_id); $toProject = Project::find($materialTransfer->to_project_id); if ($fromProject) { $fromProject->decrement('total_capitalization', $totalMonetaryValue); } if ($toProject) { $toProject->increment('total_capitalization', $totalMonetaryValue); } } $materialTransfer->update([ 'status' => MaterialTransferStatus::Received, 'received_by' => $request->user()->id, ]); }); return back()->with('success', 'Transfer received successfully.'); } }