diff --git a/app/Http/Controllers/Api/Admin/DocumentApiController.php b/app/Http/Controllers/Api/Admin/DocumentApiController.php index 470e4b04..fa7d4f81 100644 --- a/app/Http/Controllers/Api/Admin/DocumentApiController.php +++ b/app/Http/Controllers/Api/Admin/DocumentApiController.php @@ -36,6 +36,14 @@ public function index(Request $request): JsonResponse $query->where('status', $request->status); } + // 양식분류 필터 + if ($request->filled('category')) { + $category = $request->category; + $query->whereHas('template', function ($q) use ($category) { + $q->where('category', $category); + }); + } + // 템플릿 필터 if ($request->filled('template_id')) { $query->where('template_id', $request->template_id); diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index 3ddeba15..92cca884 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -31,8 +31,21 @@ public function index(Request $request): View|Response })->where('is_active', true)->orderBy('name')->get() : collect(); + // 양식분류 목록 (필터용) + $categories = $tenantId + ? DocumentTemplate::where(function ($q) use ($tenantId) { + $q->whereNull('tenant_id')->orWhere('tenant_id', $tenantId); + })->where('is_active', true) + ->whereNotNull('category') + ->distinct() + ->pluck('category') + ->sort() + ->values() + : collect(); + return view('documents.index', [ 'templates' => $templates, + 'categories' => $categories, 'statuses' => Document::STATUS_LABELS, ]); } @@ -178,6 +191,9 @@ public function show(int $id): View // 연결된 작업지시서의 work_order_items 로드 $workOrderItems = collect(); + $workOrder = null; + $salesOrder = null; + $materialInputLots = collect(); if ($document->linkable_type === 'work_order' && $document->linkable_id) { $workOrderItems = DB::table('work_order_items') ->where('work_order_id', $document->linkable_id) @@ -188,6 +204,63 @@ public function show(int $id): View return $item; }); + + // 작업일지용: 작업지시 + 수주 + 자재투입LOT 로드 + $workOrder = DB::table('work_orders') + ->where('id', $document->linkable_id) + ->first(); + + if ($workOrder?->sales_order_id) { + $salesOrder = DB::table('orders') + ->where('id', $workOrder->sales_order_id) + ->first(); + } + + // 자재 투입 LOT (stock_transactions 기반, 취소 트랜잭션 상쇄 포함) + $transactions = DB::table('stock_transactions') + ->where('tenant_id', $tenantId) + ->whereIn('reference_type', ['work_order_input', 'work_order_input_cancel']) + ->where('reference_id', $document->linkable_id) + ->orderBy('created_at') + ->get(['lot_no', 'item_code', 'item_name', 'qty', 'reference_type', 'created_at']); + + $lotMap = []; + foreach ($transactions as $tx) { + $lotNo = $tx->lot_no; + if (! isset($lotMap[$lotNo])) { + $lotMap[$lotNo] = (object) [ + 'lot_no' => $lotNo, + 'item_code' => $tx->item_code, + 'item_name' => $tx->item_name, + 'total_qty' => 0, + 'input_count' => 0, + 'first_input_at' => $tx->created_at, + ]; + } + // OUT(투입)은 음수, IN(취소)은 양수 → 합산하면 순수 투입량 + $lotMap[$lotNo]->total_qty += (float) $tx->qty; + if ($tx->reference_type === 'work_order_input') { + $lotMap[$lotNo]->input_count++; + } + } + // 순수 투입량이 0 이하인 LOT 제외, qty를 절대값으로 변환 + $materialInputLots = collect(array_values($lotMap)) + ->filter(fn ($lot) => $lot->total_qty < 0) + ->map(function ($lot) { + $lot->total_qty = abs($lot->total_qty); + return $lot; + }) + ->values(); + + // 개소별 투입자재 LOT (work_order_material_inputs 기반, work_order_item_id → lot_no) + $itemLotMap = DB::table('work_order_material_inputs as mi') + ->join('stock_lots as sl', 'sl.id', '=', 'mi.stock_lot_id') + ->where('mi.work_order_id', $document->linkable_id) + ->select('mi.work_order_item_id', 'sl.lot_no', 'mi.qty') + ->orderBy('mi.work_order_item_id') + ->get() + ->groupBy('work_order_item_id') + ->map(fn ($rows) => $rows->pluck('lot_no')->unique()->join(', ')); } // 기본정보 bf_ 자동 backfill @@ -196,12 +269,17 @@ public function show(int $id): View return view('documents.show', [ 'document' => $document, 'workOrderItems' => $workOrderItems, + 'workOrder' => $workOrder, + 'salesOrder' => $salesOrder, + 'materialInputLots' => $materialInputLots, + 'itemLotMap' => $itemLotMap ?? collect(), ]); } /** * 기본정보(bf_) 레코드가 없으면 원본 데이터에서 resolve → document_data에 저장 - * React resolveFieldValue와 동일 매핑 로직 + * 검사 문서: field_key 기반 매핑 + * 작업일지: label 기반 매핑 (DB에서 직접 JOIN) */ private function resolveAndBackfillBasicFields(Document $document): void { @@ -242,31 +320,25 @@ private function resolveAndBackfillBasicFields(Document $document): void ? DB::table('orders')->find($workOrder->sales_order_id) : null; - // 검사자 정보: work_order_items[0].options.inspection_data.inspected_by - $firstItem = $workOrderItems->first(); - $inspectionData = $firstItem?->options['inspection_data'] ?? []; - $inspectedBy = $inspectionData['inspected_by'] ?? null; - $inspectedAt = $inspectionData['inspected_at'] ?? null; - $inspectorName = $inspectedBy - ? DB::table('users')->where('id', $inspectedBy)->value('name') - : null; + // 작업일지 vs 검사 문서 판별: 섹션이 없으면 작업일지 + $isWorkLog = ! $document->template->sections || $document->template->sections->isEmpty(); - // field_key → 값 매핑 - $resolveMap = [ - 'product_name' => $firstItem?->item_name ?? '', - 'specification' => $firstItem?->specification ?? '', - 'lot_no' => $order?->order_no ?? '', - 'lot_size' => $workOrderItems->count().' 개소', - 'client' => $order?->client_name ?? '', - 'site_name' => $workOrder->project_name ?? '', - 'inspection_date' => $inspectedAt ? substr($inspectedAt, 0, 10) : now()->format('Y-m-d'), - 'inspector' => $inspectorName ?? '', - ]; + if ($isWorkLog) { + $resolveMap = $this->buildWorkLogResolveMap($workOrder, $order); + } else { + $resolveMap = $this->buildInspectionResolveMap($workOrder, $workOrderItems, $order); + } // document_data에 bf_ 레코드 저장 $records = []; foreach ($basicFields as $field) { - $value = $resolveMap[$field->field_key] ?? $field->default_value ?? ''; + if ($isWorkLog) { + // 작업일지: label 기반 매핑 + $value = $resolveMap[$field->label] ?? $field->default_value ?? ''; + } else { + // 검사 문서: field_key 기반 매핑 + $value = $resolveMap[$field->field_key] ?? $field->default_value ?? ''; + } if ($value === '') { continue; } @@ -289,6 +361,60 @@ private function resolveAndBackfillBasicFields(Document $document): void } } + /** + * 작업일지용 기본필드 resolve 맵 (label → 값) + * API WorkOrderService::buildWorkLogAutoValues 와 동일 매핑 + */ + private function buildWorkLogResolveMap(object $workOrder, ?object $salesOrder): array + { + $receivedAt = $salesOrder?->received_at ?? $salesOrder?->created_at ?? null; + $orderDate = $receivedAt ? substr((string) $receivedAt, 0, 10) : ''; + $deliveryDate = $salesOrder?->delivery_date ?? null; + $deliveryStr = $deliveryDate ? substr((string) $deliveryDate, 0, 10) : ''; + $orderNo = $salesOrder?->order_no ?? ''; + + return [ + '발주처' => $salesOrder?->client_name ?? '', + '현장명' => $salesOrder?->site_name ?? $workOrder->project_name ?? '', + '작업일자' => now()->format('Y-m-d'), + 'LOT NO' => $orderNo, + '납기일' => $deliveryStr, + '작업지시번호' => $workOrder->work_order_no ?? '', + '수주일' => $orderDate, + '수주처' => $salesOrder?->client_name ?? '', + '담당자' => '', + '연락처' => '', + '제품 LOT NO' => $orderNo, + '생산담당자' => '', + '출고예정일' => $deliveryStr, + ]; + } + + /** + * 검사 문서용 기본필드 resolve 맵 (field_key → 값) + */ + private function buildInspectionResolveMap(object $workOrder, $workOrderItems, ?object $order): array + { + $firstItem = $workOrderItems->first(); + $inspectionData = $firstItem?->options['inspection_data'] ?? []; + $inspectedBy = $inspectionData['inspected_by'] ?? null; + $inspectedAt = $inspectionData['inspected_at'] ?? null; + $inspectorName = $inspectedBy + ? DB::table('users')->where('id', $inspectedBy)->value('name') + : null; + + return [ + 'product_name' => $firstItem?->item_name ?? '', + 'specification' => $firstItem?->specification ?? '', + 'lot_no' => $order?->order_no ?? '', + 'lot_size' => $workOrderItems->count().' 개소', + 'client' => $order?->client_name ?? '', + 'site_name' => $workOrder->project_name ?? '', + 'inspection_date' => $inspectedAt ? substr($inspectedAt, 0, 10) : now()->format('Y-m-d'), + 'inspector' => $inspectorName ?? '', + ]; + } + /** * 템플릿에 연결된 품목들의 규격 정보 (thickness, width, length) 조회 */ diff --git a/resources/views/documents/index.blade.php b/resources/views/documents/index.blade.php index 868447cb..112ccc24 100644 --- a/resources/views/documents/index.blade.php +++ b/resources/views/documents/index.blade.php @@ -38,7 +38,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc