diff --git a/app/Services/QmsLotAuditService.php b/app/Services/QmsLotAuditService.php index dbd367c..951be48 100644 --- a/app/Services/QmsLotAuditService.php +++ b/app/Services/QmsLotAuditService.php @@ -23,8 +23,7 @@ class QmsLotAuditService extends Service public function index(array $params): array { $query = QualityDocument::with([ - 'documentOrders.order.nodes' => fn ($q) => $q->whereNull('parent_id'), - 'documentOrders.order.nodes.items.item', + 'documentOrders.order.item', 'locations', 'performanceReport', ]) @@ -89,7 +88,7 @@ public function routeDocuments(int $qualityDocumentOrderId): array { $docOrder = QualityDocumentOrder::with([ 'order.workOrders.process', - 'locations', + 'locations.orderItem', 'qualityDocument', ])->findOrFail($qualityDocumentOrderId); @@ -119,17 +118,20 @@ public function routeDocuments(int $qualityDocumentOrderId): array // 2. 수주서 $documents[] = $this->formatDocument('order', '수주서', collect([$order])); - // 3. 작업일지 (subType: process.process_name 기반) - $documents[] = $this->formatDocumentWithSubType('log', '작업일지', $workOrders); + // 3. 작업일지 (공정별 1개씩 — 같은 공정의 WO는 그룹핑) + $workOrdersByProcess = $workOrders->groupBy('process_id')->map(fn ($group) => $group->first()); + $documents[] = $this->formatDocumentWithSubType('log', '작업일지', $workOrdersByProcess); - // 4. 중간검사 성적서 (PQC) + // 4. 중간검사 성적서 (PQC — 공정별 1개씩) $pqcInspections = Inspection::where('inspection_type', 'PQC') ->whereIn('work_order_id', $workOrders->pluck('id')) - ->where('status', 'completed') ->with('workOrder.process') ->get(); - $documents[] = $this->formatDocumentWithSubType('report', '중간검사 성적서', $pqcInspections, 'workOrder'); + // 공정별 그룹핑 (같은 공정의 PQC는 최신 1개만) + $pqcByProcess = $pqcInspections->groupBy(fn ($insp) => $insp->workOrder?->process_id) + ->map(fn ($group) => $group->sortByDesc('inspection_date')->first()); + $documents[] = $this->formatDocumentWithSubType('report', '중간검사 성적서', $pqcByProcess, 'workOrder'); // 5. 납품확인서 $shipments = $order->shipments()->get(); @@ -138,9 +140,11 @@ public function routeDocuments(int $qualityDocumentOrderId): array // 6. 출고증 $documents[] = $this->formatDocument('shipping', '출고증', $shipments); - // 7. 제품검사 성적서 - $locationsWithDoc = $docOrder->locations->filter(fn ($loc) => $loc->document_id); - $documents[] = $this->formatDocument('product', '제품검사 성적서', $locationsWithDoc); + // 7. 제품검사 성적서 (FQC 문서 또는 inspection_data 완료건) + $locationsWithInspection = $docOrder->locations->filter( + fn ($loc) => $loc->document_id || $loc->inspection_status === 'completed' + ); + $documents[] = $this->formatDocument('product', '제품검사 성적서', $locationsWithInspection); // 8. 품질관리서 $documents[] = $this->formatDocument('quality', '품질관리서', collect([$qualityDoc])); @@ -220,30 +224,14 @@ private function transformReportToFrontend(QualityDocument $doc): array } /** - * BOM 최상위(FG) 제품명 추출 - * Order → root OrderNode(parent_id=null) → 대표 OrderItem → Item(FG).name + * 수주 대표 제품명 추출 + * Order.item_id → Item.name */ private function getFgProductName(QualityDocument $doc): string { - $firstDocOrder = $doc->documentOrders->first(); - if (! $firstDocOrder) { - return ''; - } + $order = $doc->documentOrders->first()?->order; - $order = $firstDocOrder->order; - if (! $order) { - return ''; - } - - // eager loaded with whereNull('parent_id') filter - $rootNode = $order->nodes->first(); - if (! $rootNode) { - return ''; - } - - $representativeItem = $rootNode->items->first(); - - return $representativeItem?->item?->name ?? ''; + return $order?->item?->name ?? ''; } private function transformRouteToFrontend(QualityDocumentOrder $docOrder, QualityDocument $qualityDoc): array @@ -313,11 +301,12 @@ private function formatDocumentWithSubType(string $type, string $title, $collect 'items' => $collection->values()->map(function ($item) use ($type, $workOrderRelation) { $formatted = $this->formatDocumentItem($type, $item); - // subType: process.process_name 기반 + // subType: process.process_name 기반 + work_order_id 전달 $workOrder = $workOrderRelation ? $item->{$workOrderRelation} : $item; if ($workOrder instanceof WorkOrder) { $processName = $workOrder->process?->process_name; $formatted['sub_type'] = $this->mapProcessToSubType($processName); + $formatted['work_order_id'] = $workOrder->id; } return $formatted; @@ -354,7 +343,7 @@ private function formatDocumentItem(string $type, $item): array ], 'product' => [ 'id' => (string) $item->id, - 'title' => '제품검사 성적서', + 'title' => trim(($item->orderItem?->floor_code ?? '').' '.($item->orderItem?->symbol_code ?? '')) ?: '제품검사 성적서', 'date' => $item->updated_at?->toDateString() ?? '', 'code' => '', ], @@ -479,9 +468,16 @@ private function getShipmentDetail(int $id): array private function getLocationDetail(int $id): array { - $location = QualityDocumentLocation::with(['orderItem', 'document'])->findOrFail($id); + $location = QualityDocumentLocation::with([ + 'orderItem', + 'document.template.sections.items', + 'document.template.columns', + 'document.template.approvalLines', + 'document.template.basicFields', + 'document.data', + ])->findOrFail($id); - return [ + $result = [ 'type' => 'product', 'data' => [ 'id' => $location->id, @@ -494,6 +490,86 @@ private function getLocationDetail(int $id): array 'document_id' => $location->document_id, ], ]; + + // FQC 문서가 있으면 template + data 포함 + if ($location->document) { + $doc = $location->document; + $result['data']['fqc_document'] = [ + 'id' => $doc->id, + 'template_id' => $doc->template_id, + 'document_no' => $doc->document_no, + 'title' => $doc->title, + 'status' => $doc->status, + 'created_at' => $doc->created_at?->toIso8601String(), + 'template' => $this->formatFqcTemplate($doc->template), + 'data' => $doc->data->map(fn ($d) => [ + 'section_id' => $d->section_id, + 'column_id' => $d->column_id, + 'row_index' => $d->row_index, + 'field_key' => $d->field_key, + 'field_value' => $d->field_value, + ])->all(), + ]; + } + + return $result; + } + + private function formatFqcTemplate($template): ?array + { + if (! $template) { + return null; + } + + return [ + 'id' => $template->id, + 'name' => $template->name, + 'category' => $template->category, + 'title' => $template->title, + 'approval_lines' => $template->approvalLines->map(fn ($a) => [ + 'id' => $a->id, + 'name' => $a->name, + 'department' => $a->department, + 'sort_order' => $a->sort_order, + ])->all(), + 'basic_fields' => $template->basicFields->map(fn ($f) => [ + 'id' => $f->id, + 'label' => $f->label, + 'field_key' => $f->field_key, + 'field_type' => $f->field_type, + 'default_value' => $f->default_value, + 'is_required' => $f->is_required, + 'sort_order' => $f->sort_order, + ])->all(), + 'sections' => $template->sections->map(fn ($s) => [ + 'id' => $s->id, + 'name' => $s->name, + 'title' => $s->title, + 'description' => $s->description, + 'image_path' => $s->image_path, + 'sort_order' => $s->sort_order, + 'items' => $s->items->map(fn ($i) => [ + 'id' => $i->id, + 'section_id' => $i->section_id, + 'item_name' => $i->item ?? '', + 'standard' => $i->standard, + 'tolerance' => $i->tolerance, + 'measurement_type' => $i->measurement_type, + 'frequency' => $i->frequency, + 'sort_order' => $i->sort_order, + 'category' => $i->category, + 'method' => $i->method, + ])->all(), + ])->all(), + 'columns' => $template->columns->map(fn ($c) => [ + 'id' => $c->id, + 'label' => $c->label, + 'column_type' => $c->column_type, + 'width' => $c->width, + 'group_name' => $c->group_name, + 'sort_order' => $c->sort_order, + ])->all(), + ]; } private function getQualityDocDetail(int $id): array