diff --git a/src/components/material/StockStatus/StockStatusList.tsx b/src/components/material/StockStatus/StockStatusList.tsx index 8b681174..9d407023 100644 --- a/src/components/material/StockStatus/StockStatusList.tsx +++ b/src/components/material/StockStatus/StockStatusList.tsx @@ -32,8 +32,15 @@ import { type FilterFieldConfig, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; -import { getStocks, getStockStats } from './actions'; +import { getStocks, getStockStats, getStockTransactions, type StockUsageData } from './actions'; import { USE_STATUS_LABELS, ITEM_TYPE_LABELS } from './types'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Loader2, History, ExternalLink } from 'lucide-react'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getLocalDateString } from '@/lib/utils/date'; import type { StockItem, StockStats, ItemType, StockStatusType } from './types'; @@ -147,9 +154,23 @@ export function StockStatusList() { return true; }); - // ===== 행 클릭 핸들러 ===== + // ===== 행 클릭 → 액션 메뉴 ===== + const [actionMenuItemId, setActionMenuItemId] = useState(null); + const [usageModalItem, setUsageModalItem] = useState(null); + const [usageData, setUsageData] = useState(null); + const [usageLoading, setUsageLoading] = useState(false); + const handleRowClick = (item: StockItem) => { - router.push(`/ko/material/stock-status/${item.id}?mode=view`); + setActionMenuItemId(prev => prev === item.id ? null : item.id); + }; + + const handleViewUsage = async (item: StockItem) => { + setActionMenuItemId(null); + setUsageModalItem(item); + setUsageLoading(true); + const result = await getStockTransactions(item.id); + if (result.success && result.data) setUsageData(result.data); + setUsageLoading(false); }; // ===== 엑셀 컬럼 정의 ===== @@ -302,9 +323,10 @@ export function StockStatusList() { handlers: SelectionHandlers & RowClickHandlers ) => { return ( + <> handleRowClick(item)} > e.stopPropagation()}> @@ -329,6 +351,31 @@ export function StockStatusList() { + {actionMenuItemId === item.id && ( + + +
+ + +
+
+
+ )} + ); }; @@ -515,12 +562,79 @@ export function StockStatusList() { } return ( - - config={config} - initialData={filteredStocks} - initialTotalCount={filteredStocks.length} - onFilterChange={(newFilters) => setFilterValues(newFilters)} - onSearchChange={setSearchTerm} - /> + <> + + config={config} + initialData={filteredStocks} + initialTotalCount={filteredStocks.length} + onFilterChange={(newFilters) => setFilterValues(newFilters)} + onSearchChange={setSearchTerm} + /> + + {/* 사용현황 모달 */} + { if (!open) { setUsageModalItem(null); setUsageData(null); } }}> + + + + + 사용현황 + + {usageModalItem && ( +
+ {usageModalItem.itemCode} + {usageModalItem.itemName} + {usageData && ( + + 현재 재고: {usageData.availableQty} {usageModalItem.unit} + + )} +
+ )} +
+ +
+ {usageLoading ? ( +
+ +
+ ) : usageData?.transactions.length ? ( +
+ {usageData.transactions.map((tx) => ( +
+
+ {tx.qty < 0 ? '출' : '입'} +
+
+
+ {tx.typeLabel} + + {tx.qty > 0 ? '+' : ''}{tx.qty} {usageModalItem?.unit} + +
+
+ {tx.referenceNo && {tx.referenceNo}} + {tx.lotNo && LOT: {tx.lotNo}} + {tx.createdAt} +
+ {tx.remark &&

{tx.remark}

} +
+
+
잔량
+
{tx.balanceQty}
+
+
+ ))} +
+ ) : ( +
+ 사용 이력이 없습니다 +
+ )} +
+
+
+ ); } \ No newline at end of file diff --git a/src/components/material/StockStatus/actions.ts b/src/components/material/StockStatus/actions.ts index 2892a428..cb73bb94 100644 --- a/src/components/material/StockStatus/actions.ts +++ b/src/components/material/StockStatus/actions.ts @@ -14,6 +14,7 @@ import { executeServerAction } from '@/lib/api/execute-server-action'; import { executePaginatedAction } from '@/lib/api/execute-paginated-action'; import { buildApiUrl } from '@/lib/api/query-params'; +import { buildApiUrl } from '@/lib/api/query-params'; import type { StockItem, StockDetail, @@ -345,3 +346,62 @@ export async function updateStockAudit(updates: { id: string; actualQty: number if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } + +// ===== 사용현황(거래이력) 조회 ===== +export interface StockTransaction { + id: number; + type: string; + typeLabel: string; + qty: number; + balanceQty: number; + referenceType: string; + referenceId: number; + referenceNo: string | null; + lotNo: string; + reason: string | null; + remark: string | null; + itemCode: string; + itemName: string; + createdAt: string; +} + +export interface StockUsageData { + itemCode: string; + itemName: string; + currentQty: number; + availableQty: number; + transactions: StockTransaction[]; +} + +export async function getStockTransactions( + itemId: string +): Promise<{ success: boolean; data?: StockUsageData; error?: string }> { + const result = await executeServerAction<{ + item_code: string; item_name: string; current_qty: number; available_qty: number; + transactions: Array<{ + id: number; type: string; type_label: string; qty: number; balance_qty: number; + reference_type: string; reference_id: number; reference_no: string | null; + lot_no: string; reason: string | null; remark: string | null; + item_code: string; item_name: string; created_at: string; + }>; + }>({ + url: buildApiUrl(`/api/v1/stocks/${itemId}/transactions`), + errorMessage: '사용현황 조회에 실패했습니다.', + }); + if (!result.success || !result.data) return { success: false, error: result.error }; + return { + success: true, + data: { + itemCode: result.data.item_code, + itemName: result.data.item_name, + currentQty: result.data.current_qty, + availableQty: result.data.available_qty, + transactions: result.data.transactions.map((t: { id: number; type: string; type_label: string; qty: number; balance_qty: number; reference_type: string; reference_id: number; reference_no: string | null; lot_no: string; reason: string | null; remark: string | null; item_code: string; item_name: string; created_at: string }) => ({ + id: t.id, type: t.type, typeLabel: t.type_label, qty: t.qty, balanceQty: t.balance_qty, + referenceType: t.reference_type, referenceId: t.reference_id, referenceNo: t.reference_no, + lotNo: t.lot_no, reason: t.reason, remark: t.remark, + itemCode: t.item_code, itemName: t.item_name, createdAt: t.created_at, + })), + }, + }; +}