feat: [qms] 수주로트 감사 상세 정보 확장

- 수주 상세: 개소별 제품, 모터, 절곡물, 부자재 정보 추가
- 출하 상세: 배차정보, 제품 그룹별 품목 분류 추가
- 확인 로직: documentOrders 기준 수주로트 카운트로 변경
- locations relation 경로 수정 (documentOrders.locations)
- 품질관리서 파일 정보 routeDocuments에 포함
- Shipment Client 모델 네임스페이스 수정
- DocumentService data relation null 처리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 10:14:09 +09:00
parent 597aecb5e8
commit 73c8f78788
3 changed files with 230 additions and 13 deletions

View File

@@ -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);
}
/**

View File

@@ -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,

View File

@@ -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,
],
];
}