'원자재', 'SM' => '부자재', 'CS' => '소모품', ]; /** * 재고 목록 조회 (Item 메인 + Stock LEFT JOIN) */ public function index(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); // Item 테이블이 메인 (materials 타입만: SM, RM, CS) $query = Item::query() ->where('items.tenant_id', $tenantId) ->materials() // SM, RM, CS만 ->with('stock'); // 검색어 필터 (Item 기준) if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('items.code', 'like', "%{$search}%") ->orWhere('items.name', 'like', "%{$search}%"); }); } // 품목유형 필터 (Item.item_type 기준: RM, SM, CS) if (! empty($params['item_type'])) { $query->where('items.item_type', strtoupper($params['item_type'])); } // 재고 상태 필터 (Stock.status) if (! empty($params['status'])) { $query->whereHas('stock', function ($q) use ($params) { $q->where('status', $params['status']); }); } // 위치 필터 (Stock.location) if (! empty($params['location'])) { $query->whereHas('stock', function ($q) use ($params) { $q->where('location', 'like', "%{$params['location']}%"); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'code'; $sortDir = $params['sort_dir'] ?? 'asc'; // Item 테이블 기준 정렬 필드 매핑 $sortMapping = [ 'item_code' => 'items.code', 'item_name' => 'items.name', 'item_type' => 'items.item_type', 'code' => 'items.code', 'name' => 'items.name', ]; $sortColumn = $sortMapping[$sortBy] ?? 'items.code'; $query->orderBy($sortColumn, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 재고 통계 조회 (Item 기준) */ public function stats(): array { $tenantId = $this->tenantId(); // 전체 자재 품목 수 (Item 기준) $totalItems = Item::where('tenant_id', $tenantId) ->materials() ->count(); // 재고 상태별 카운트 (Stock이 있는 Item 기준) $normalCount = Item::where('items.tenant_id', $tenantId) ->materials() ->whereHas('stock', function ($q) { $q->where('status', 'normal'); }) ->count(); $lowCount = Item::where('items.tenant_id', $tenantId) ->materials() ->whereHas('stock', function ($q) { $q->where('status', 'low'); }) ->count(); $outCount = Item::where('items.tenant_id', $tenantId) ->materials() ->whereHas('stock', function ($q) { $q->where('status', 'out'); }) ->count(); // 재고 정보가 없는 Item 수 $noStockCount = Item::where('items.tenant_id', $tenantId) ->materials() ->whereDoesntHave('stock') ->count(); return [ 'total_items' => $totalItems, 'normal_count' => $normalCount, 'low_count' => $lowCount, 'out_count' => $outCount, 'no_stock_count' => $noStockCount, ]; } /** * 재고 상세 조회 (Item 기준, LOT 포함) */ public function show(int $id): Item { $tenantId = $this->tenantId(); return Item::query() ->where('tenant_id', $tenantId) ->materials() ->with(['stock.lots' => function ($query) { $query->orderBy('fifo_order'); }]) ->findOrFail($id); } /** * 품목코드로 재고 조회 (Item 기준) */ public function findByItemCode(string $itemCode): ?Item { $tenantId = $this->tenantId(); return Item::query() ->where('tenant_id', $tenantId) ->materials() ->where('code', $itemCode) ->with('stock') ->first(); } /** * 품목유형별 통계 (Item.item_type 기준) */ public function statsByItemType(): array { $tenantId = $this->tenantId(); // Item 기준으로 통계 (materials 타입만) $stats = Item::where('tenant_id', $tenantId) ->materials() ->selectRaw('item_type, COUNT(*) as count') ->groupBy('item_type') ->get() ->keyBy('item_type'); // 재고 수량 합계 (Stock이 있는 경우) $stockQtys = Item::where('items.tenant_id', $tenantId) ->materials() ->join('stocks', 'items.id', '=', 'stocks.item_id') ->selectRaw('items.item_type, SUM(stocks.stock_qty) as total_qty') ->groupBy('items.item_type') ->get() ->keyBy('item_type'); $result = []; foreach (self::ITEM_TYPE_LABELS as $key => $label) { $itemData = $stats->get($key); $stockData = $stockQtys->get($key); $result[$key] = [ 'label' => $label, 'count' => $itemData?->count ?? 0, 'total_qty' => $stockData?->total_qty ?? 0, ]; } return $result; } }