validate([ 'file' => 'required|file|mimes:xlsx,csv,xls|max:10240', ]); $import = new MaterialImport(); try { Excel::import($import, $request->file('file')); } catch (\Exception $e) { return back()->with('error', 'Import failed: ' . $e->getMessage()); } $failures = $import->failures(); $failureCount = $failures->count(); $message = "{$import->getImportedCount()} materials imported."; if ($import->getSkippedCount() > 0) { $message .= " {$import->getSkippedCount()} skipped (duplicate SKU)."; } if (count($import->getGroupsCreated()) > 0) { $message .= " Groups: " . implode(', ', $import->getGroupsCreated()) . "."; } if ($failureCount > 0) { $message .= " {$failureCount} rows had validation errors."; } return back()->with('success', $message); } // --- BOQ Import --- public function boqTemplate() { return Excel::download(new BoqTemplateExport(), 'boq_template.xlsx'); } public function boqPreview(Request $request) { $request->validate([ 'file' => 'required|file|mimes:xlsx,csv,xls|max:10240', ]); $import = new BoqImport(); try { Excel::import($import, $request->file('file')); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to parse file: ' . $e->getMessage(), ], 422); } if (!empty($import->getErrors())) { return response()->json([ 'success' => false, 'message' => implode('; ', $import->getErrors()), ], 422); } return response()->json([ 'success' => true, 'header' => $import->getHeader(), 'items' => $import->getItems(), ]); } public function boqConfirm(Request $request) { $validated = $request->validate([ 'project_ulid' => 'required|string', 'task_ulid' => 'required|string', 'header' => 'required|array', 'header.project_name' => 'nullable|string', 'header.owner_name' => 'nullable|string', 'header.location' => 'nullable|string', 'header.date' => 'nullable|string', 'items' => 'required|array|min:1', 'items.*.material' => 'required|string|max:255', 'items.*.unit' => 'required|string|max:20', 'items.*.approved_qty' => 'nullable|numeric|min:0', 'items.*.request_qty' => 'nullable|numeric|min:0', 'items.*.work_area' => 'nullable|string|max:255', ]); $project = Project::where('ulid', $validated['project_ulid'])->firstOrFail(); $task = Task::where('ulid', $validated['task_ulid']) ->where('project_id', $project->id) ->firstOrFail(); $created = 0; DB::transaction(function () use ($validated, $project, $task, &$created) { // Update project fields from header $updates = []; if (!empty($validated['header']['location'])) { $updates['location'] = $validated['header']['location']; } if (!empty($validated['header']['date'])) { try { $updates['start_date'] = Carbon::parse($validated['header']['date'])->format('Y-m-d'); } catch (\Exception) { // Skip invalid date } } if (!empty($updates)) { $project->update($updates); } // Create materials and assign to task foreach ($validated['items'] as $item) { $material = Material::create([ 'name' => $item['material'], 'unit' => $item['unit'], 'unit_cost' => 0, 'category' => null, 'status' => 'active', ]); $task->taskMaterials()->create([ 'material_id' => $material->id, 'planned_qty' => $item['request_qty'] ?? 0, 'actual_qty' => $item['approved_qty'] ?? 0, 'unit_cost' => 0, 'notes' => $item['work_area'] ?? null, ]); $created++; } // Recalculate project capitalization $project->load('tasks.taskMaterials'); $project->recalculateCapitalization(); }); return back()->with('success', "{$created} materials imported and assigned to task \"{$task->name}\"."); } }