tenantId(); $query = Order::query() ->where('tenant_id', $tenantId) ->whereIn('status_code', self::PRODUCTION_STATUSES) ->with(['client', 'workOrders.process', 'workOrders.assignees.user']) ->withCount([ 'workOrders' => fn ($q) => $q->whereNotNull('process_id') ->where(fn ($q2) => $q2->whereNull('options->is_auxiliary') ->orWhere('options->is_auxiliary', false)), 'nodes', ]); // 검색어 필터 if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('order_no', 'like', "%{$search}%") ->orWhere('client_name', 'like', "%{$search}%") ->orWhere('site_name', 'like', "%{$search}%"); }); } // 생산 상태 필터 if (! empty($params['production_status'])) { switch ($params['production_status']) { case 'waiting': $query->where('status_code', Order::STATUS_IN_PROGRESS); break; case 'in_production': $query->where('status_code', Order::STATUS_IN_PRODUCTION); break; case 'completed': $query->whereIn('status_code', [ Order::STATUS_PRODUCED, Order::STATUS_SHIPPING, Order::STATUS_SHIPPED, ]); break; } } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; $result = $query->paginate($perPage); // 가공 필드 추가 $result->getCollection()->transform(function (Order $order) { $minCreatedAt = $order->workOrders->min('created_at'); $order->production_ordered_at = $minCreatedAt ? $minCreatedAt->format('Y-m-d') : null; // 개소수 (order_nodes 수) $order->node_count = $order->nodes_count ?? 0; // 주요 생산 공정 WO만 (구매품 + 보조 공정 제외) $productionWOs = $this->filterMainProductionWOs($order->workOrders); $order->work_order_progress = [ 'total' => $productionWOs->count(), 'completed' => $productionWOs->where('status', 'completed')->count() + $productionWOs->where('status', 'shipped')->count(), 'in_progress' => $productionWOs->where('status', 'in_progress')->count(), ]; // 프론트 탭용 production_status 매핑 $order->production_status = $this->mapProductionStatus($order->status_code); return $order; }); return $result; } /** * 상태별 통계 */ public function stats(): array { $tenantId = $this->tenantId(); $waiting = Order::where('tenant_id', $tenantId) ->where('status_code', Order::STATUS_IN_PROGRESS) ->count(); $inProduction = Order::where('tenant_id', $tenantId) ->where('status_code', Order::STATUS_IN_PRODUCTION) ->count(); $completed = Order::where('tenant_id', $tenantId) ->whereIn('status_code', [ Order::STATUS_PRODUCED, Order::STATUS_SHIPPING, Order::STATUS_SHIPPED, ]) ->count(); return [ 'total' => $waiting + $inProduction + $completed, 'waiting' => $waiting, 'in_production' => $inProduction, 'completed' => $completed, ]; } /** * 생산지시 상세 조회 */ public function show(int $orderId): array { $tenantId = $this->tenantId(); $order = Order::query() ->where('tenant_id', $tenantId) ->whereIn('status_code', self::PRODUCTION_STATUSES) ->with([ 'client', 'workOrders.process', 'workOrders.items', 'workOrders.assignees.user', 'nodes', ]) ->withCount('nodes') ->findOrFail($orderId); // 생산지시일 (날짜만) $minCreatedAt = $order->workOrders->min('created_at'); $order->production_ordered_at = $minCreatedAt ? $minCreatedAt->format('Y-m-d') : null; $order->production_status = $this->mapProductionStatus($order->status_code); // 주요 생산 공정 WO만 필터 (구매품 + 보조 공정 제외) $productionWorkOrders = $this->filterMainProductionWOs($order->workOrders); // WorkOrder 진행 현황 (생산 공정 기준) $workOrderProgress = [ 'total' => $productionWorkOrders->count(), 'completed' => $productionWorkOrders->where('status', 'completed')->count() + $productionWorkOrders->where('status', 'shipped')->count(), 'in_progress' => $productionWorkOrders->where('status', 'in_progress')->count(), ]; // WorkOrder 목록 가공 (생산 공정만) $workOrders = $productionWorkOrders->values()->map(function ($wo) { return [ 'id' => $wo->id, 'work_order_no' => $wo->work_order_no, 'process_name' => $wo->process?->process_name ?? '', 'quantity' => $wo->items->count(), 'status' => $wo->status, 'assignees' => $wo->assignees->map(fn ($a) => $a->user?->name ?? '')->filter()->values()->toArray(), ]; }); // BOM 데이터 (order_nodes에서 추출) $bomProcessGroups = $this->extractBomProcessGroups($order->nodes); return [ 'order' => $order->makeHidden(['workOrders', 'nodes']), 'production_ordered_at' => $order->production_ordered_at, 'production_status' => $order->production_status, 'node_count' => $order->nodes_count ?? 0, 'work_order_progress' => $workOrderProgress, 'work_orders' => $workOrders, 'bom_process_groups' => $bomProcessGroups, ]; } /** * Order status_code → 프론트 production_status 매핑 */ private function mapProductionStatus(string $statusCode): string { return match ($statusCode) { Order::STATUS_IN_PROGRESS => 'waiting', Order::STATUS_IN_PRODUCTION => 'in_production', default => 'completed', }; } /** * order_nodes에서 BOM 공정 분류 추출 * * bom_result 구조: { items: [...], success, subtotals, ... } * 각 item: { item_id, item_code, item_name, process_group, specification, quantity, unit, ... } */ private function extractBomProcessGroups($nodes): array { $groups = []; foreach ($nodes as $node) { $bomResult = $node->options['bom_result'] ?? null; if (! $bomResult || ! is_array($bomResult)) { continue; } // bom_result.items 배열에서 추출 $items = $bomResult['items'] ?? []; if (! is_array($items)) { continue; } $nodeName = $node->name ?? ''; foreach ($items as $item) { if (! is_array($item)) { continue; } $processGroup = $item['process_group'] ?? $item['category_group'] ?? '기타'; if (! isset($groups[$processGroup])) { $groups[$processGroup] = [ 'process_name' => $processGroup, 'items' => [], ]; } $groups[$processGroup]['items'][] = [ 'id' => $item['item_id'] ?? null, 'item_code' => $item['item_code'] ?? '', 'item_name' => $item['item_name'] ?? '', 'spec' => $item['specification'] ?? '', 'unit' => $item['unit'] ?? '', 'quantity' => $item['quantity'] ?? 0, 'unit_price' => $item['unit_price'] ?? 0, 'total_price' => $item['total_price'] ?? 0, 'node_name' => $nodeName, ]; } } return array_values($groups); } /** * 주요 생산 공정 WO만 필터 (구매품/서비스 + 보조 공정 제외) * * 제외 대상: * - process_id가 null인 WO (구매품/서비스) * - options.is_auxiliary가 true인 WO (재고생산 등 보조 공정) */ private function filterMainProductionWOs($workOrders): \Illuminate\Support\Collection { return $workOrders->filter(function ($wo) { if (empty($wo->process_id)) { return false; } $options = is_array($wo->options) ? $wo->options : (json_decode($wo->options, true) ?? []); return empty($options['is_auxiliary']); }); } }