diff --git a/app/Models/Tenants/Shipment.php b/app/Models/Tenants/Shipment.php index 90e8f1a8..90abc70c 100644 --- a/app/Models/Tenants/Shipment.php +++ b/app/Models/Tenants/Shipment.php @@ -147,7 +147,7 @@ public function vehicleDispatches(): HasMany */ public function client(): BelongsTo { - return $this->belongsTo(\App\Models\Clients\Client::class); + return $this->belongsTo(\App\Models\Orders\Client::class); } /** diff --git a/app/Services/DocumentService.php b/app/Services/DocumentService.php index ed25399c..b0a4a66f 100644 --- a/app/Services/DocumentService.php +++ b/app/Services/DocumentService.php @@ -1034,7 +1034,7 @@ private function formatDocumentForReact(Document $document): array 'submitted_at' => $document->submitted_at?->toIso8601String(), 'completed_at' => $document->completed_at?->toIso8601String(), 'created_at' => $document->created_at?->toIso8601String(), - 'data' => $document->data->map(fn ($d) => [ + 'data' => ($document->getRelation('data') ?? collect())->map(fn ($d) => [ 'section_id' => $d->section_id, 'column_id' => $d->column_id, 'row_index' => $d->row_index, diff --git a/app/Services/QmsLotAuditService.php b/app/Services/QmsLotAuditService.php index c776b019..511ebf95 100644 --- a/app/Services/QmsLotAuditService.php +++ b/app/Services/QmsLotAuditService.php @@ -24,7 +24,7 @@ public function index(array $params): array { $query = QualityDocument::with([ 'documentOrders.order.item', - 'locations', + 'documentOrders.locations', 'performanceReport', ]) ->where('status', QualityDocument::STATUS_COMPLETED); @@ -142,8 +142,18 @@ public function routeDocuments(int $qualityDocumentOrderId): array ); $documents[] = $this->formatDocument('product', '제품검사 성적서', $locationsWithInspection); - // 8. 품질관리서 - $documents[] = $this->formatDocument('quality', '품질관리서', collect([$qualityDoc])); + // 8. 품질관리서 (파일 정보 포함) + $qualityDoc->loadMissing('file'); + $qualityDocFormatted = $this->formatDocument('quality', '품질관리서', collect([$qualityDoc])); + + // 파일 정보 추가 + if ($qualityDoc->file) { + $qualityDocFormatted['file_id'] = $qualityDoc->file->id; + $qualityDocFormatted['file_name'] = $qualityDoc->file->display_name ?? $qualityDoc->file->original_name; + $qualityDocFormatted['file_size'] = $qualityDoc->file->file_size; + } + + $documents[] = $qualityDocFormatted; return $documents; } @@ -200,8 +210,18 @@ public function confirm(int $locationId, array $data): array private function transformReportToFrontend(QualityDocument $doc): array { $performanceReport = $doc->performanceReport; - $confirmedCount = $doc->locations->filter(function ($loc) { - return data_get($loc->options, 'lot_audit_confirmed', false); + + // 수주로트 건수 = documentOrders 수 + $totalRoutes = $doc->documentOrders->count(); + + // 확인 완료 수주로트 = 해당 주문의 모든 개소가 확인된 건수 + $confirmedRoutes = $doc->documentOrders->filter(function ($docOrder) { + $locations = $docOrder->locations; + if ($locations->isEmpty()) { + return false; + } + + return $locations->every(fn ($loc) => data_get($loc->options, 'lot_audit_confirmed', false)); })->count(); return [ @@ -209,8 +229,8 @@ private function transformReportToFrontend(QualityDocument $doc): array 'code' => $doc->quality_doc_number, 'site_name' => $doc->site_name, 'item' => $this->getFgProductName($doc), - 'route_count' => $confirmedCount, - 'total_routes' => $doc->locations->count(), + 'route_count' => $confirmedRoutes, + 'total_routes' => $totalRoutes, 'quarter' => $performanceReport ? $performanceReport->year.'년 '.$performanceReport->quarter.'분기' : '', @@ -415,21 +435,175 @@ private function getInspectionDetail(int $id, string $type): array private function getOrderDetail(int $id): array { - $order = Order::with(['nodes' => fn ($q) => $q->whereNull('parent_id')])->findOrFail($id); + $order = Order::with([ + 'client', + 'nodes' => fn ($q) => $q->whereNull('parent_id')->orderBy('id'), + ])->findOrFail($id); + + $rootNodes = $order->nodes; + $options = $order->options ?? []; + + // 개소별 제품 정보 + $products = $rootNodes->map(function ($node, $index) { + $opts = $node->options ?? []; + $vars = data_get($opts, 'bom_result.variables', []); + + return [ + 'no' => $index + 1, + 'floor' => $opts['floor'] ?? '-', + 'symbol' => $opts['symbol'] ?? '-', + 'product_name' => $opts['product_name'] ?? '-', + 'product_code' => $opts['product_code'] ?? null, + 'open_width' => $opts['open_width'] ?? null, + 'open_height' => $opts['open_height'] ?? null, + 'made_width' => $opts['width'] ?? null, + 'made_height' => $opts['height'] ?? null, + 'guide_rail' => $vars['installation_type'] ?? '-', + 'shaft' => $vars['bracket_inch'] ?? '-', + 'case_inch' => $vars['bracket_inch'] ?? '-', + 'bracket' => $vars['BRACKET_SIZE'] ?? '-', + 'capacity' => $vars['MOTOR_CAPACITY'] ?? '-', + 'finish' => $vars['finishing_type'] ?? '-', + 'product_type' => $vars['product_type'] ?? null, + 'joint_bar' => null, // 철재 전용 — 아래에서 bom_items에서 보강 + ]; + })->values()->toArray(); + + // BOM items 집계 (모든 노드에서) + $allBomItems = $rootNodes->flatMap(function ($node) { + return collect(data_get($node->options, 'bom_result.items', [])); + }); + + // 철재 제품의 조인트바 수량 보강 + foreach ($rootNodes->values() as $index => $node) { + $bomItems = collect(data_get($node->options, 'bom_result.items', [])); + $jointBar = $bomItems->first(fn ($i) => str_contains($i['item_name'] ?? '', '조인트바')); + if ($jointBar) { + $products[$index]['joint_bar'] = $jointBar['quantity'] ?? null; + } + } + + // 모터 정보 (category: motor, controller) + $motorItems = $allBomItems->filter(fn ($i) => in_array($i['item_category'] ?? '', ['motor', 'controller'])); + $motorLeft = []; + $motorRight = []; + foreach ($motorItems->groupBy('item_name') as $name => $group) { + $item = $group->first(); + $totalQty = $group->sum('quantity'); + $row = [ + 'item' => $item['item_name'], + 'type' => $item['specification'] ?? '-', + 'spec' => $item['item_code'] ?? '-', + 'qty' => $totalQty, + ]; + // 모터/브라켓 → 좌, 제어기/전동개폐기 → 우 + if (in_array($item['item_category'], ['controller'])) { + $motorRight[] = $row; + } else { + $motorLeft[] = $row; + } + } + + // 절곡물 (category: steel) + $steelItems = $allBomItems->filter(fn ($i) => ($i['item_category'] ?? '') === 'steel'); + $bendingParts = $this->groupBendingParts($steelItems); + + // 부자재 (category: parts) + $partItems = $allBomItems->filter(fn ($i) => ($i['item_category'] ?? '') === 'parts'); + $subsidiaryParts = []; + foreach ($partItems->groupBy('item_name') as $name => $group) { + $item = $group->first(); + $subsidiaryParts[] = [ + 'name' => $item['item_name'], + 'spec' => $item['specification'] ?? '-', + 'qty' => $group->sum('quantity'), + ]; + } return [ 'type' => 'order', 'data' => [ 'id' => $order->id, 'order_no' => $order->order_no, - 'status' => $order->status, + 'status_code' => $order->status_code, + 'category_code' => $order->category_code, 'received_at' => $order->received_at?->toDateString(), + 'delivery_date' => $order->delivery_date?->toDateString(), + 'delivery_method_code' => $order->delivery_method_code, 'site_name' => $order->site_name, - 'nodes_count' => $order->nodes->count(), + 'client_name' => $order->client_name ?? $order->client?->name, + 'client_contact' => $order->client_contact, + 'manager_name' => $options['manager_name'] ?? null, + 'receiver' => $options['receiver'] ?? null, + 'receiver_contact' => $options['receiver_contact'] ?? null, + 'shipping_address' => $options['shipping_address'] ?? null, + 'shipping_address_detail' => $options['shipping_address_detail'] ?? null, + 'shipping_cost_code' => $options['shipping_cost_code'] ?? null, + 'quantity' => $order->quantity, + 'supply_amount' => $order->supply_amount, + 'tax_amount' => $order->tax_amount, + 'total_amount' => $order->total_amount, + 'remarks' => $order->remarks, + 'nodes_count' => $rootNodes->count(), + 'products' => $products, + 'motors' => [ + 'left' => $motorLeft, + 'right' => $motorRight, + ], + 'bending_parts' => $bendingParts, + 'subsidiary_parts' => $subsidiaryParts, ], ]; } + /** + * 절곡물 BOM items를 그룹별로 분류 + */ + private function groupBendingParts($steelItems): array + { + $groups = [ + '가이드레일' => [], + '케이스' => [], + '하단마감' => [], + '연기차단재' => [], + '기타' => [], + ]; + + foreach ($steelItems->groupBy('item_name') as $name => $group) { + $item = $group->first(); + $totalQty = $group->sum('quantity'); + $row = [ + 'name' => $item['item_name'], + 'spec' => $item['specification'] ?? '-', + 'qty' => $totalQty, + ]; + + if (str_contains($name, '연기차단재')) { + $groups['연기차단재'][] = $row; + } elseif (str_contains($name, '가이드레일')) { + $groups['가이드레일'][] = $row; + } elseif (str_contains($name, '케이스') || str_contains($name, '마구리')) { + $groups['케이스'][] = $row; + } elseif (str_contains($name, '하장바') || str_contains($name, 'L-BAR') || str_contains($name, '보강평철')) { + $groups['하단마감'][] = $row; + } else { + $groups['기타'][] = $row; + } + } + + $result = []; + foreach ($groups as $groupName => $items) { + if (! empty($items)) { + $result[] = [ + 'group' => $groupName, + 'items' => $items, + ]; + } + } + + return $result; + } + private function getWorkOrderLogDetail(int $id): array { $workOrder = WorkOrder::with('process')->findOrFail($id); @@ -449,22 +623,65 @@ private function getWorkOrderLogDetail(int $id): array private function getShipmentDetail(int $id): array { - $shipment = Shipment::findOrFail($id); + $shipment = Shipment::with([ + 'vehicleDispatches', + 'items', + 'order.nodes' => fn ($q) => $q->whereNull('parent_id'), + ])->findOrFail($id); + + // 배차정보 + $vehicleDispatches = $shipment->vehicleDispatches->map(fn ($d) => [ + 'logistics_company' => $d->logistics_company, + 'arrival_datetime' => $d->arrival_datetime, + 'tonnage' => $d->tonnage, + 'vehicle_no' => $d->vehicle_no, + 'driver_contact' => $d->driver_contact, + 'remarks' => $d->remarks, + ])->values()->toArray(); + + // 출하 품목 → 제품 그룹별 분류 + $productGroups = []; + $otherParts = []; + foreach ($shipment->items as $item) { + $row = [ + 'item_name' => $item->item_name, + 'specification' => $item->specification, + 'quantity' => $item->quantity, + 'unit' => $item->unit, + 'lot_no' => $item->lot_no, + 'floor_unit' => $item->floor_unit, + ]; + // floor_unit가 있으면 해당 제품 그룹에, 없으면 기타 부품 + if ($item->floor_unit) { + $productGroups[$item->floor_unit][] = $row; + } else { + $otherParts[] = $row; + } + } return [ 'type' => 'shipping', 'data' => [ 'id' => $shipment->id, 'shipment_no' => $shipment->shipment_no, + 'lot_no' => $shipment->lot_no, 'status' => $shipment->status, 'scheduled_date' => $shipment->scheduled_date?->toDateString(), 'customer_name' => $shipment->customer_name, + 'customer_grade' => $shipment->customer_grade, 'site_name' => $shipment->site_name, 'delivery_address' => $shipment->delivery_address, 'delivery_method' => $shipment->delivery_method, + 'shipping_cost' => $shipment->shipping_cost, + 'receiver' => $shipment->receiver, + 'receiver_contact' => $shipment->receiver_contact, 'vehicle_no' => $shipment->vehicle_no, 'driver_name' => $shipment->driver_name, + 'driver_contact' => $shipment->driver_contact, 'remarks' => $shipment->remarks, + 'vehicle_dispatches' => $vehicleDispatches, + 'product_groups' => $productGroups, + 'other_parts' => $otherParts, ], ]; }