ultimatepos/Modules/IndustrialEngineering/Services/BomTemplateService.php

311 lines
14 KiB
PHP

<?php
namespace Modules\IndustrialEngineering\Services;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use Modules\IndustrialEngineering\Models\Bom;
use Modules\IndustrialEngineering\Models\BomLine;
use Modules\IndustrialEngineering\Models\BomStructureTemplate;
class BomTemplateService
{
public function __construct(
protected BomTreeService $bomTreeService
) {
}
/**
* Built-in structure presets (no DB row required).
*/
public function builtinPresets(): array
{
return [
'air_section_full' => [
'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<int, BomLine>}
*/
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<int, array<string, mixed>> $nodes
* @return array{created_count:int, lines:array<int, BomLine>}
*/
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<int, BomLine>
*/
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;
}
}