[ 'key' => 'air_section_full', 'name' => __('industrialengineering::lang.preset_air_section_full'), 'description' => __('industrialengineering::lang.preset_air_section_full_desc'), 'category' => 'air', 'nodes' => [ [ 'name' => __('industrialengineering::lang.preset_air_section_name'), 'item_type' => 'assembly', 'position_code' => 'M01-AIR', 'quantity' => 1, 'children' => [ [ 'name' => __('industrialengineering::lang.preset_pump_name'), 'item_type' => 'subassembly', 'position_code' => 'M01-PMP', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_motor_electric'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_motor_gas'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_pressure_sensor'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_check_valve'), 'item_type' => 'part', 'quantity' => 1], ], ], [ 'name' => __('industrialengineering::lang.preset_dryer_name'), 'item_type' => 'subassembly', 'position_code' => 'M01-DRY', 'quantity' => 1, ], [ 'name' => __('industrialengineering::lang.preset_tank_name'), 'item_type' => 'subassembly', 'position_code' => 'M01-TNK', 'quantity' => 1, ], ], ], ], ], 'air_section_children' => [ 'key' => 'air_section_children', 'name' => __('industrialengineering::lang.preset_air_children'), 'description' => __('industrialengineering::lang.preset_air_children_desc'), 'category' => 'air', 'nodes' => [ [ 'name' => __('industrialengineering::lang.preset_pump_name'), 'item_type' => 'subassembly', 'position_code' => 'M01-PMP', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_motor_electric'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_motor_gas'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_pressure_sensor'), 'item_type' => 'part', 'quantity' => 1], ], ], [ 'name' => __('industrialengineering::lang.preset_dryer_name'), 'item_type' => 'subassembly', 'position_code' => 'M01-DRY', 'quantity' => 1, ], [ 'name' => __('industrialengineering::lang.preset_tank_name'), 'item_type' => 'subassembly', 'position_code' => 'M01-TNK', 'quantity' => 1, ], ], ], 'pump_kit' => [ 'key' => 'pump_kit', 'name' => __('industrialengineering::lang.preset_pump_kit'), 'description' => __('industrialengineering::lang.preset_pump_kit_desc'), 'category' => 'pump', 'nodes' => [ ['name' => __('industrialengineering::lang.preset_motor_electric'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_motor_gas'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_pressure_sensor'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_check_valve'), 'item_type' => 'part', 'quantity' => 1], ], ], 'compressor_station_20' => [ 'key' => 'compressor_station_20', 'name' => __('industrialengineering::lang.preset_compressor_station_20'), 'description' => __('industrialengineering::lang.preset_compressor_station_20_desc'), 'category' => 'air', 'nodes' => [ [ 'name' => __('industrialengineering::lang.preset_air_pump'), 'item_type' => 'device', 'position_code' => 'DEV-PMP', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_motor_55kw'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_pump_impeller'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_air_filter'), 'item_type' => 'consumable', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_mechanical_seal'), 'item_type' => 'consumable', 'quantity' => 1], ], ], [ 'name' => __('industrialengineering::lang.preset_industrial_vacuum'), 'item_type' => 'device', 'position_code' => 'DEV-VAC', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_vacuum_pump'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_inlet_filter'), 'item_type' => 'consumable', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_solenoid_valve'), 'item_type' => 'part', 'quantity' => 1], ], ], [ 'name' => __('industrialengineering::lang.preset_piston_compressor'), 'item_type' => 'device', 'position_code' => 'DEV-PST', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_cylinder'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_connecting_pipe'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_oil_filter'), 'item_type' => 'consumable', 'quantity' => 1], ], ], [ 'name' => __('industrialengineering::lang.preset_screw_compressor'), 'item_type' => 'device', 'position_code' => 'DEV-SCR', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_rotor'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_separator'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_oil_filter'), 'item_type' => 'consumable', 'quantity' => 1], ], ], [ 'name' => __('industrialengineering::lang.preset_dryer_assembly'), 'item_type' => 'assembly', 'position_code' => 'ASM-DRY', 'quantity' => 1, 'children' => [ ['name' => __('industrialengineering::lang.preset_refrigerant_dryer'), 'item_type' => 'part', 'quantity' => 1], ['name' => __('industrialengineering::lang.preset_condensate_drain'), 'item_type' => 'part', 'quantity' => 1], ], ], ], ], ]; } public function listForBusiness(int $businessId): array { $builtins = array_values($this->builtinPresets()); $saved = BomStructureTemplate::forBusiness($businessId) ->where('is_system', false) ->orderByDesc('updated_at') ->get() ->map(fn (BomStructureTemplate $t) => [ 'id' => $t->id, 'key' => 'saved:'.$t->id, 'name' => $t->name, 'description' => $t->description, 'category' => $t->category, 'is_saved' => true, 'updated_at' => $t->updated_at?->toDateTimeString(), ]) ->all(); return [ 'builtin' => $builtins, 'saved' => $saved, ]; } /** * Apply preset key or saved template id under a parent BOM line. * * @return array{created_count:int, lines:array} */ public function apply(Bom $bom, ?int $parentLineId, string $templateKey): array { $nodes = $this->resolveTemplateNodes($bom->business_id, $templateKey); return $this->bulkAddNodes($bom, $parentLineId, $nodes); } /** * Recursively add multiple nodes under a parent. * * @param array> $nodes * @return array{created_count:int, lines:array} */ public function bulkAddNodes(Bom $bom, ?int $parentLineId, array $nodes): array { return DB::transaction(function () use ($bom, $parentLineId, $nodes) { $created = []; foreach ($nodes as $nodeDef) { $created = array_merge($created, $this->addNodeRecursive($bom, $parentLineId, $nodeDef)); } return [ 'created_count' => count($created), 'lines' => $created, ]; }); } public function saveFromBomLine(BomLine $line, string $name, ?string $description = null, ?string $category = 'custom'): BomStructureTemplate { $structure = $this->exportLineSubtree($line); return BomStructureTemplate::create([ 'business_id' => $line->business_id, 'name' => $name, 'description' => $description, 'category' => $category ?: 'custom', 'structure_json' => ['nodes' => [$structure]], 'is_system' => false, 'created_by' => auth()->id(), ]); } /** * Export a BOM line and its descendants to a portable node definition. */ public function exportLineSubtree(BomLine $line): array { $line->loadMissing(['itemMaster', 'children.itemMaster']); $item = $line->itemMaster; $def = [ 'name' => $item?->name, 'item_type' => $item?->item_type ?? 'part', 'item_code' => $item?->item_code, 'quantity' => (float) $line->quantity, 'position_code' => $line->position_code, 'find_number' => $line->find_number, ]; if ($line->children->isNotEmpty()) { $def['children'] = $line->children ->sortBy('sort_order') ->map(fn (BomLine $child) => $this->exportLineSubtree($child)) ->values() ->all(); } return $def; } protected function resolveTemplateNodes(int $businessId, string $templateKey): array { if (str_starts_with($templateKey, 'saved:')) { $id = (int) substr($templateKey, 6); $template = BomStructureTemplate::forBusiness($businessId)->findOrFail($id); $json = $template->structure_json ?? []; return $json['nodes'] ?? (is_array($json) && isset($json[0]) ? $json : []); } $presets = $this->builtinPresets(); if (! isset($presets[$templateKey])) { throw new InvalidArgumentException(__('industrialengineering::lang.template_not_found')); } return $presets[$templateKey]['nodes'] ?? []; } /** * @return array */ protected function addNodeRecursive(Bom $bom, ?int $parentLineId, array $nodeDef): array { $children = $nodeDef['children'] ?? []; unset($nodeDef['children']); $line = $this->bomTreeService->addNode($bom, $parentLineId, $nodeDef); $created = [$line]; foreach ($children as $childDef) { $created = array_merge($created, $this->addNodeRecursive($bom, $line->id, $childDef)); } return $created; } }