tenantId(); $query = Shipment::query() ->where('tenant_id', $tenantId) ->with('items'); // 검색어 필터 if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('shipment_no', 'like', "%{$search}%") ->orWhere('lot_no', 'like', "%{$search}%") ->orWhere('customer_name', 'like', "%{$search}%") ->orWhere('site_name', 'like', "%{$search}%"); }); } // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 우선순위 필터 if (! empty($params['priority'])) { $query->where('priority', $params['priority']); } // 배송방식 필터 if (! empty($params['delivery_method'])) { $query->where('delivery_method', $params['delivery_method']); } // 예정일 범위 필터 if (! empty($params['scheduled_from'])) { $query->where('scheduled_date', '>=', $params['scheduled_from']); } if (! empty($params['scheduled_to'])) { $query->where('scheduled_date', '<=', $params['scheduled_to']); } // 출하가능 필터 if (isset($params['can_ship'])) { $query->where('can_ship', (bool) $params['can_ship']); } // 입금확인 필터 if (isset($params['deposit_confirmed'])) { $query->where('deposit_confirmed', (bool) $params['deposit_confirmed']); } // 정렬 $sortBy = $params['sort_by'] ?? 'scheduled_date'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 출하 통계 조회 */ public function stats(): array { $tenantId = $this->tenantId(); $total = Shipment::where('tenant_id', $tenantId)->count(); $scheduled = Shipment::where('tenant_id', $tenantId) ->where('status', 'scheduled') ->count(); $ready = Shipment::where('tenant_id', $tenantId) ->where('status', 'ready') ->count(); $shipping = Shipment::where('tenant_id', $tenantId) ->where('status', 'shipping') ->count(); $completed = Shipment::where('tenant_id', $tenantId) ->where('status', 'completed') ->count(); $urgent = Shipment::where('tenant_id', $tenantId) ->where('priority', 'urgent') ->whereIn('status', ['scheduled', 'ready']) ->count(); $todayScheduled = Shipment::where('tenant_id', $tenantId) ->whereDate('scheduled_date', now()) ->count(); return [ 'total' => $total, 'scheduled' => $scheduled, 'ready' => $ready, 'shipping' => $shipping, 'completed' => $completed, 'urgent' => $urgent, 'today_scheduled' => $todayScheduled, // 프론트엔드 호환 필드 (snake_case) 'today_shipment_count' => $todayScheduled, 'scheduled_count' => $scheduled, 'shipping_count' => $shipping, 'urgent_count' => $urgent, ]; } /** * 상태별 통계 (탭용) */ public function statsByStatus(): array { $tenantId = $this->tenantId(); $stats = Shipment::where('tenant_id', $tenantId) ->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->get() ->keyBy('status'); $result = []; foreach (Shipment::STATUSES as $key => $label) { $data = $stats->get($key); $result[$key] = [ 'label' => $label, 'count' => $data?->count ?? 0, ]; } return $result; } /** * 출하 상세 조회 */ public function show(int $id): Shipment { $tenantId = $this->tenantId(); return Shipment::query() ->where('tenant_id', $tenantId) ->with(['items' => function ($query) { $query->orderBy('seq'); }, 'creator', 'updater']) ->findOrFail($id); } /** * 출하 생성 */ public function store(array $data): Shipment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 출하번호 자동 생성 $shipmentNo = $data['shipment_no'] ?? Shipment::generateShipmentNo($tenantId); $shipment = Shipment::create([ 'tenant_id' => $tenantId, 'shipment_no' => $shipmentNo, 'lot_no' => $data['lot_no'] ?? null, 'order_id' => $data['order_id'] ?? null, 'scheduled_date' => $data['scheduled_date'], 'status' => $data['status'] ?? 'scheduled', 'priority' => $data['priority'] ?? 'normal', 'delivery_method' => $data['delivery_method'] ?? 'pickup', // 발주처/배송 정보 'client_id' => $data['client_id'] ?? null, 'customer_name' => $data['customer_name'] ?? null, 'site_name' => $data['site_name'] ?? null, 'delivery_address' => $data['delivery_address'] ?? null, 'receiver' => $data['receiver'] ?? null, 'receiver_contact' => $data['receiver_contact'] ?? null, // 상태 플래그 'can_ship' => $data['can_ship'] ?? false, 'deposit_confirmed' => $data['deposit_confirmed'] ?? false, 'invoice_issued' => $data['invoice_issued'] ?? false, 'customer_grade' => $data['customer_grade'] ?? null, // 상차 정보 'loading_manager' => $data['loading_manager'] ?? null, 'loading_time' => $data['loading_time'] ?? null, // 물류/배차 정보 'logistics_company' => $data['logistics_company'] ?? null, 'vehicle_tonnage' => $data['vehicle_tonnage'] ?? null, 'shipping_cost' => $data['shipping_cost'] ?? null, // 차량/운전자 정보 'vehicle_no' => $data['vehicle_no'] ?? null, 'driver_name' => $data['driver_name'] ?? null, 'driver_contact' => $data['driver_contact'] ?? null, 'expected_arrival' => $data['expected_arrival'] ?? null, // 기타 'remarks' => $data['remarks'] ?? null, 'created_by' => $userId, ]); // 출하 품목 추가 if (! empty($data['items'])) { $this->syncItems($shipment, $data['items'], $tenantId); } return $shipment->load('items'); }); } /** * 출하 수정 */ public function update(int $id, array $data): Shipment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id); $shipment->update([ 'lot_no' => $data['lot_no'] ?? $shipment->lot_no, 'order_id' => $data['order_id'] ?? $shipment->order_id, 'scheduled_date' => $data['scheduled_date'] ?? $shipment->scheduled_date, 'priority' => $data['priority'] ?? $shipment->priority, 'delivery_method' => $data['delivery_method'] ?? $shipment->delivery_method, // 발주처/배송 정보 'client_id' => $data['client_id'] ?? $shipment->client_id, 'customer_name' => $data['customer_name'] ?? $shipment->customer_name, 'site_name' => $data['site_name'] ?? $shipment->site_name, 'delivery_address' => $data['delivery_address'] ?? $shipment->delivery_address, 'receiver' => $data['receiver'] ?? $shipment->receiver, 'receiver_contact' => $data['receiver_contact'] ?? $shipment->receiver_contact, // 상태 플래그 'can_ship' => $data['can_ship'] ?? $shipment->can_ship, 'deposit_confirmed' => $data['deposit_confirmed'] ?? $shipment->deposit_confirmed, 'invoice_issued' => $data['invoice_issued'] ?? $shipment->invoice_issued, 'customer_grade' => $data['customer_grade'] ?? $shipment->customer_grade, // 상차 정보 'loading_manager' => $data['loading_manager'] ?? $shipment->loading_manager, 'loading_time' => $data['loading_time'] ?? $shipment->loading_time, // 물류/배차 정보 'logistics_company' => $data['logistics_company'] ?? $shipment->logistics_company, 'vehicle_tonnage' => $data['vehicle_tonnage'] ?? $shipment->vehicle_tonnage, 'shipping_cost' => $data['shipping_cost'] ?? $shipment->shipping_cost, // 차량/운전자 정보 'vehicle_no' => $data['vehicle_no'] ?? $shipment->vehicle_no, 'driver_name' => $data['driver_name'] ?? $shipment->driver_name, 'driver_contact' => $data['driver_contact'] ?? $shipment->driver_contact, 'expected_arrival' => $data['expected_arrival'] ?? $shipment->expected_arrival, // 기타 'remarks' => $data['remarks'] ?? $shipment->remarks, 'updated_by' => $userId, ]); // 출하 품목 동기화 if (isset($data['items'])) { $this->syncItems($shipment, $data['items'], $tenantId); } return $shipment->load('items'); }); } /** * 출하 상태 변경 */ public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id); $updateData = [ 'status' => $status, 'updated_by' => $userId, ]; // 상태별 추가 데이터 if ($status === 'ready' && isset($additionalData['loading_time'])) { $updateData['loading_time'] = $additionalData['loading_time']; } if ($status === 'shipping') { if (isset($additionalData['loading_completed_at'])) { $updateData['loading_completed_at'] = $additionalData['loading_completed_at']; } else { $updateData['loading_completed_at'] = now(); } if (isset($additionalData['vehicle_no'])) { $updateData['vehicle_no'] = $additionalData['vehicle_no']; } if (isset($additionalData['driver_name'])) { $updateData['driver_name'] = $additionalData['driver_name']; } if (isset($additionalData['driver_contact'])) { $updateData['driver_contact'] = $additionalData['driver_contact']; } } if ($status === 'completed' && isset($additionalData['confirmed_arrival'])) { $updateData['confirmed_arrival'] = $additionalData['confirmed_arrival']; } $shipment->update($updateData); return $shipment->load('items'); } /** * 출하 삭제 */ public function delete(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id); return DB::transaction(function () use ($shipment, $userId) { // 품목 삭제 $shipment->items()->delete(); // 출하 삭제 $shipment->update(['deleted_by' => $userId]); $shipment->delete(); return true; }); } /** * 출하 품목 동기화 */ protected function syncItems(Shipment $shipment, array $items, int $tenantId): void { // 기존 품목 삭제 $shipment->items()->forceDelete(); // 새 품목 생성 $seq = 1; foreach ($items as $item) { ShipmentItem::create([ 'tenant_id' => $tenantId, 'shipment_id' => $shipment->id, 'seq' => $item['seq'] ?? $seq, 'item_code' => $item['item_code'] ?? null, 'item_name' => $item['item_name'], 'floor_unit' => $item['floor_unit'] ?? null, 'specification' => $item['specification'] ?? null, 'quantity' => $item['quantity'] ?? 0, 'unit' => $item['unit'] ?? null, 'lot_no' => $item['lot_no'] ?? null, 'stock_lot_id' => $item['stock_lot_id'] ?? null, 'remarks' => $item['remarks'] ?? null, ]); $seq++; } } /** * LOT 옵션 조회 (출고 가능한 LOT 목록) */ public function getLotOptions(): array { $tenantId = $this->tenantId(); return \App\Models\Tenants\StockLot::query() ->where('tenant_id', $tenantId) ->whereIn('status', ['available', 'reserved']) ->where('qty', '>', 0) ->with('stock:id,item_code,item_name') ->orderBy('fifo_order') ->get() ->map(function ($lot) { return [ 'id' => $lot->id, 'lot_no' => $lot->lot_no, 'item_code' => $lot->stock?->item_code, 'item_name' => $lot->stock?->item_name, 'qty' => $lot->qty, 'available_qty' => $lot->available_qty, 'unit' => $lot->unit, 'location' => $lot->location, 'fifo_order' => $lot->fifo_order, ]; }) ->toArray(); } /** * 물류사 옵션 조회 */ public function getLogisticsOptions(): array { // TODO: 별도 물류사 테이블이 있다면 조회 // 현재는 기본값 반환 return [ ['value' => '한진택배', 'label' => '한진택배'], ['value' => '롯데택배', 'label' => '롯데택배'], ['value' => 'CJ대한통운', 'label' => 'CJ대한통운'], ['value' => '우체국택배', 'label' => '우체국택배'], ['value' => '로젠택배', 'label' => '로젠택배'], ['value' => '경동택배', 'label' => '경동택배'], ['value' => '직접입력', 'label' => '직접입력'], ]; } /** * 차량 톤수 옵션 조회 */ public function getVehicleTonnageOptions(): array { return [ ['value' => '1톤', 'label' => '1톤'], ['value' => '2.5톤', 'label' => '2.5톤'], ['value' => '5톤', 'label' => '5톤'], ['value' => '11톤', 'label' => '11톤'], ['value' => '25톤', 'label' => '25톤'], ]; } }