feat(WEB): 부실채권, 재고, 입고, 수주 UI 개선

- BadDebtCollection 액션/타입 리팩토링
- ReceivingProcessDialog 입고처리 개선
- StockStatusList 재고현황 UI 개선
- OrderSalesDetailView 수주 상세 수정
- UniversalListPage 범용 리스트 개선
- production-order 페이지 수정
This commit is contained in:
2026-01-23 21:32:24 +09:00
parent 9fb5c171eb
commit a0343eec93
12 changed files with 315 additions and 251 deletions

View File

@@ -1,11 +1,11 @@
/**
* 재고 현황 서버 액션
*
* API Endpoints:
* - GET /api/v1/stocks - 목록 조회
* API Endpoints (Item 기준):
* - GET /api/v1/stocks - 목록 조회 (Item + Stock LEFT JOIN)
* - GET /api/v1/stocks/stats - 통계 조회
* - GET /api/v1/stocks/stats-by-type - 품목유형별 통계 조회
* - GET /api/v1/stocks/{id} - 상세 조회 (LOT 포함)
* - GET /api/v1/stocks/{id} - 상세 조회 (Item 기준, LOT 포함)
*/
'use server';
@@ -23,16 +23,12 @@ import type {
LotStatusType,
} from './types';
// ===== API 데이터 타입 =====
interface StockApiData {
// ===== API 데이터 타입 (Item 기준) =====
// Stock 관계 데이터
interface StockRelationData {
id: number;
tenant_id: number;
item_code: string;
item_name: string;
item_type: ItemType;
item_type_label?: string;
specification?: string;
unit: string;
item_id: number;
stock_qty: string | number;
safety_stock: string | number;
reserved_qty: string | number;
@@ -42,12 +38,30 @@ interface StockApiData {
days_elapsed?: number;
location?: string;
status: StockStatusType;
status_label?: string;
last_receipt_date?: string;
last_issue_date?: string;
lots?: StockLotApiData[];
}
// Item API 응답 데이터
interface ItemApiData {
id: number;
tenant_id: number;
code: string; // Item.code (기존 item_code)
name: string; // Item.name (기존 item_name)
item_type: ItemType; // Item.item_type (RM, SM, CS)
unit: string;
category_id?: number;
category?: {
id: number;
name: string;
};
description?: string;
attributes?: Record<string, unknown>;
is_active: boolean;
created_at?: string;
updated_at?: string;
lots?: StockLotApiData[];
stock?: StockRelationData | null; // Stock 관계 (없으면 null)
}
interface StockLotApiData {
@@ -71,8 +85,8 @@ interface StockLotApiData {
updated_at?: string;
}
interface StockApiPaginatedResponse {
data: StockApiData[];
interface ItemApiPaginatedResponse {
data: ItemApiData[];
current_page: number;
last_page: number;
per_page: number;
@@ -84,6 +98,7 @@ interface StockApiStatsResponse {
normal_count: number;
low_count: number;
out_count: number;
no_stock_count: number;
}
interface StockApiStatsByTypeResponse {
@@ -95,19 +110,23 @@ interface StockApiStatsByTypeResponse {
}
// ===== API → Frontend 변환 (목록용) =====
function transformApiToListItem(data: StockApiData): StockItem {
function transformApiToListItem(data: ItemApiData): StockItem {
const stock = data.stock;
const hasStock = !!stock;
return {
id: String(data.id),
itemCode: data.item_code,
itemName: data.item_name,
itemCode: data.code,
itemName: data.name,
itemType: data.item_type,
unit: data.unit || 'EA',
stockQty: parseFloat(String(data.stock_qty)) || 0,
safetyStock: parseFloat(String(data.safety_stock)) || 0,
lotCount: data.lot_count || 0,
lotDaysElapsed: data.days_elapsed || 0,
status: data.status,
location: data.location || '-',
stockQty: hasStock ? (parseFloat(String(stock.stock_qty)) || 0) : 0,
safetyStock: hasStock ? (parseFloat(String(stock.safety_stock)) || 0) : 0,
lotCount: hasStock ? (stock.lot_count || 0) : 0,
lotDaysElapsed: hasStock ? (stock.days_elapsed || 0) : 0,
status: hasStock ? stock.status : null,
location: hasStock ? (stock.location || '-') : '-',
hasStock,
};
}
@@ -129,22 +148,38 @@ function transformApiToLot(data: StockLotApiData): LotDetail {
}
// ===== API → Frontend 변환 (상세용) =====
function transformApiToDetail(data: StockApiData): StockDetail {
function transformApiToDetail(data: ItemApiData): StockDetail {
const stock = data.stock;
const hasStock = !!stock;
// description 또는 attributes에서 규격 정보 추출
let specification = '-';
if (data.description) {
specification = data.description;
} else if (data.attributes && typeof data.attributes === 'object') {
// attributes에서 규격 관련 정보 추출 시도
const attrs = data.attributes as Record<string, unknown>;
if (attrs.specification) {
specification = String(attrs.specification);
}
}
return {
id: String(data.id),
itemCode: data.item_code,
itemName: data.item_name,
itemCode: data.code,
itemName: data.name,
itemType: data.item_type,
category: '-', // API에서 category 제공 안 함
specification: data.specification || '-',
category: data.category?.name || '-',
specification,
unit: data.unit || 'EA',
currentStock: parseFloat(String(data.stock_qty)) || 0,
safetyStock: parseFloat(String(data.safety_stock)) || 0,
location: data.location || '-',
lotCount: data.lot_count || 0,
lastReceiptDate: data.last_receipt_date || '-',
status: data.status,
lots: (data.lots || []).map(transformApiToLot),
currentStock: hasStock ? (parseFloat(String(stock.stock_qty)) || 0) : 0,
safetyStock: hasStock ? (parseFloat(String(stock.safety_stock)) || 0) : 0,
location: hasStock ? (stock.location || '-') : '-',
lotCount: hasStock ? (stock.lot_count || 0) : 0,
lastReceiptDate: hasStock ? (stock.last_receipt_date || '-') : '-',
status: hasStock ? stock.status : null,
hasStock,
lots: hasStock && stock.lots ? stock.lots.map(transformApiToLot) : [],
};
}
@@ -155,6 +190,7 @@ function transformApiToStats(data: StockApiStatsResponse): StockStats {
normalCount: data.normal_count,
lowCount: data.low_count,
outCount: data.out_count,
noStockCount: data.no_stock_count || 0,
};
}
@@ -238,7 +274,7 @@ export async function getStocks(params?: {
};
}
const paginatedData: StockApiPaginatedResponse = result.data || {
const paginatedData: ItemApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
@@ -340,7 +376,7 @@ export async function getStockStatsByType(): Promise<{
}
}
// ===== 재고 상세 조회 (LOT 포함) =====
// ===== 재고 상세 조회 (Item 기준, LOT 포함) =====
export async function getStockById(id: string): Promise<{
success: boolean;
data?: StockDetail;