feat: POP 재고관리 전면 구현 — 재고조정/재고이동/다중품목 공정
재고조정: - fullBleed 좌우 분할, 숫자키패드 모달, 위치불일치 QR스캔+모달 - 임시저장 cart_items 상태관리 (saved/cancelled/confirmed) - 조정이력 별도 페이지, DateRangePicker 통일 - popInventoryController 11개 API (adjust-batch, stock-detail, locations 등) 재고이동: - 창고 탭: 탭 버튼 패턴 + flat 리스트 (아코디언 제거) - 공정 탭: 공정명/설비 필터 모달 (작업지시번호 탭 제거) - move-batch API: 창고→창고 + 공정→창고 (source_type 확장) - 품목 이력 바텀시트 (transaction_type별 색상) 다중품목 공정실행: - syncWorkInstructions LIMIT 1 제거 → detail 전체 순회 - batch_id 기반 품목별 공정 분리 - WorkOrderList/ProcessWork 품목 구분 표시 기타: - PopShell fullBleed 모드 추가 - alert() → 토스트 메시지 교체 - MonitoringSettings import 수정
This commit is contained in:
@@ -11,9 +11,10 @@ interface PopShellProps {
|
||||
title?: string;
|
||||
showBack?: boolean;
|
||||
headerRight?: ReactNode;
|
||||
fullBleed?: boolean;
|
||||
}
|
||||
|
||||
export function PopShell({ children, showBanner = true, title, showBack = false, headerRight }: PopShellProps) {
|
||||
export function PopShell({ children, showBanner = true, title, showBack = false, headerRight, fullBleed = false }: PopShellProps) {
|
||||
const router = useRouter();
|
||||
const { user, logout } = useAuth();
|
||||
const displayName = user?.userName || user?.userId || "사용자";
|
||||
@@ -319,7 +320,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
|
||||
</div>}
|
||||
|
||||
{/* ===== MAIN CONTENT ===== */}
|
||||
<main className="max-w-[1400px] mx-auto w-full px-4 sm:px-6 lg:px-8 py-5 sm:py-6 flex flex-col gap-5 sm:gap-6 flex-1 overflow-y-auto">
|
||||
<main className={fullBleed
|
||||
? "flex-1 overflow-hidden"
|
||||
: "max-w-[1400px] mx-auto w-full px-4 sm:px-6 lg:px-8 py-5 sm:py-6 flex flex-col gap-5 sm:gap-6 flex-1 overflow-y-auto"
|
||||
}>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { DateRangePicker } from "./DateRangePicker";
|
||||
|
||||
export function AdjustHistory() {
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dateFrom, setDateFrom] = useState(() => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 30);
|
||||
return d.toISOString().slice(0, 10);
|
||||
});
|
||||
const [dateTo, setDateTo] = useState(() => new Date().toISOString().slice(0, 10));
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
const fetchHistory = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, string> = {};
|
||||
if (dateFrom) params.date_from = dateFrom;
|
||||
if (dateTo) params.date_to = dateTo;
|
||||
if (keyword) params.item_code = keyword;
|
||||
const res = await apiClient.get("/pop/inventory/adjust-history", { params });
|
||||
setItems(res.data?.data || []);
|
||||
} catch {
|
||||
setItems([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dateFrom, dateTo, keyword]);
|
||||
|
||||
useEffect(() => { fetchHistory(); }, [fetchHistory]);
|
||||
|
||||
const filtered = items;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
{/* 필터 */}
|
||||
<div className="bg-white border-b border-gray-200 px-4 py-3 shrink-0">
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-[auto_1fr] gap-2">
|
||||
<DateRangePicker
|
||||
from={dateFrom}
|
||||
to={dateTo}
|
||||
onChange={(f, t) => { setDateFrom(f); setDateTo(t); }}
|
||||
/>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-400 mb-1 block">품목검색</label>
|
||||
<input
|
||||
type="text"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder="품목코드 검색"
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-amber-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchHistory}
|
||||
className="px-5 py-2.5 rounded-lg bg-amber-500 text-white text-sm font-bold active:bg-amber-600 shrink-0"
|
||||
>
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI */}
|
||||
<div className="bg-white border-b border-gray-200 shrink-0">
|
||||
<div className="grid grid-cols-3 divide-x divide-gray-100">
|
||||
<div className="flex flex-col items-center py-3">
|
||||
<span className="text-xs font-bold text-gray-400">전체</span>
|
||||
<span className="text-2xl font-extrabold text-gray-900">{filtered.length}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center py-3">
|
||||
<span className="text-xs font-bold text-gray-400">확인</span>
|
||||
<span className="text-2xl font-extrabold text-green-600">
|
||||
{filtered.filter((h: any) => h.transaction_type === "조정확인").length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center py-3">
|
||||
<span className="text-xs font-bold text-gray-400">조정</span>
|
||||
<span className="text-2xl font-extrabold text-amber-600">
|
||||
{filtered.filter((h: any) => h.transaction_type === "조정").length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 리스트 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<span className="text-sm font-bold text-gray-600">조정 이력</span>
|
||||
<span className="text-sm text-gray-400">총 {filtered.length}건</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-16">
|
||||
<div className="w-10 h-10 border-4 border-amber-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-gray-400">
|
||||
<span className="text-5xl mb-3">📋</span>
|
||||
<p className="text-lg font-semibold">조정 이력이 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{filtered.map((h: any) => {
|
||||
const isConfirm = h.transaction_type === "조정확인";
|
||||
const qty = parseFloat(h.quantity || "0");
|
||||
return (
|
||||
<div key={h.id} className={`flex items-center gap-3 px-4 py-4 ${isConfirm ? "bg-white" : "bg-amber-50/50"}`}>
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center text-white text-xl shrink-0 ${
|
||||
isConfirm ? "bg-green-500" : "bg-amber-500"
|
||||
}`}>
|
||||
{isConfirm ? "✓" : "~"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-lg font-bold text-gray-900">{h.item_code}</p>
|
||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||
{h.reason && (
|
||||
<span className={`text-sm px-2.5 py-0.5 rounded-lg font-bold ${
|
||||
isConfirm ? "bg-green-100 text-green-700" : "bg-amber-100 text-amber-700"
|
||||
}`}>{h.reason}</span>
|
||||
)}
|
||||
<span className="text-sm text-gray-400">{h.warehouse_code}</span>
|
||||
<span className="text-sm text-gray-400">{h.manager_name || h.writer}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<p className={`text-xl font-bold ${qty === 0 ? "text-green-600" : qty > 0 ? "text-blue-600" : "text-red-600"}`}>
|
||||
{qty === 0 ? "이상없음" : (qty > 0 ? `+${qty}` : qty)}
|
||||
</p>
|
||||
{h.system_qty != null && qty !== 0 && (
|
||||
<p className="text-sm text-gray-400">{h.system_qty} → {h.actual_qty ?? h.system_qty}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
{h.transaction_date ? new Date(h.transaction_date).toLocaleDateString() : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ export function InOutHistory() {
|
||||
const filtered = items.filter((item) => {
|
||||
if (activeTab === "inbound" && item.direction !== "입고") return false;
|
||||
if (activeTab === "outbound" && item.direction !== "출고") return false;
|
||||
if (activeTab === "transfer") return false; // 준비 중
|
||||
if (activeTab === "transfer") return false;
|
||||
if (keyword) {
|
||||
const kw = keyword.toLowerCase();
|
||||
if (
|
||||
@@ -260,39 +260,24 @@ export function InOutHistory() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Back + Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/pop/inventory")}
|
||||
className="w-10 h-10 rounded-xl bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 active:scale-95 transition-all"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
<div className="flex flex-col h-full bg-gray-100">
|
||||
{/* Header bar */}
|
||||
<div className="bg-white border-b-2 border-gray-200 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/pop/inventory")}
|
||||
className="w-11 h-11 rounded-xl bg-gray-100 flex items-center justify-center active:bg-gray-200"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 19.5L8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">
|
||||
입출고관리
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
입고·출고 내역을 조회합니다
|
||||
</p>
|
||||
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 className="text-xl font-bold text-gray-900 flex-1">입출고관리</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-3 sm:p-4">
|
||||
<div className="bg-white border-b border-gray-200 px-4 py-3 shrink-0">
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<DateRangePicker
|
||||
@@ -304,39 +289,33 @@ export function InOutHistory() {
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-400 mb-1 block">
|
||||
품목검색
|
||||
</label>
|
||||
<label className="text-xs font-bold text-gray-500 mb-1 block">품목검색</label>
|
||||
<input
|
||||
type="text"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder="품목명 / 코드 검색"
|
||||
className="w-full px-2 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-cyan-400"
|
||||
className="w-full px-3 py-3 border border-gray-200 rounded-xl text-base focus:outline-none focus:border-cyan-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-semibold text-gray-400 mb-1 block">
|
||||
창고
|
||||
</label>
|
||||
<label className="text-xs font-bold text-gray-500 mb-1 block">창고</label>
|
||||
<select
|
||||
value={warehouse}
|
||||
onChange={(e) => setWarehouse(e.target.value)}
|
||||
className="w-full px-2 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-cyan-400 bg-white"
|
||||
className="w-full px-3 py-3 border border-gray-200 rounded-xl text-base focus:outline-none focus:border-cyan-400 bg-white"
|
||||
>
|
||||
<option value="전체">전체</option>
|
||||
{warehouses.map((w) => (
|
||||
<option key={w.code} value={w.name}>
|
||||
{w.name}
|
||||
</option>
|
||||
<option key={w.code} value={w.name}>{w.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1.5 shrink-0 pb-[1px]">
|
||||
<div className="flex gap-1.5 shrink-0">
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="h-[42px] px-4 rounded-lg text-sm font-semibold text-white active:scale-95 transition-all"
|
||||
className="px-5 py-3 rounded-xl text-base font-bold text-white active:scale-95 transition-all"
|
||||
style={{ background: "linear-gradient(135deg,#06b6d4,#0e7490)" }}
|
||||
>
|
||||
조회
|
||||
@@ -348,7 +327,7 @@ export function InOutHistory() {
|
||||
setKeyword("");
|
||||
setWarehouse("전체");
|
||||
}}
|
||||
className="h-[42px] w-[42px] rounded-lg text-sm font-semibold text-gray-500 bg-gray-100 active:scale-95 transition-all flex items-center justify-center"
|
||||
className="w-12 h-12 rounded-xl text-lg font-bold text-gray-500 bg-gray-100 active:scale-95 transition-all flex items-center justify-center"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
@@ -356,121 +335,77 @@ export function InOutHistory() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-3 sm:p-4">
|
||||
<div className="grid grid-cols-4 gap-0">
|
||||
<KpiCell
|
||||
icon="📥"
|
||||
value={loading ? "-" : kpi.inbound.toLocaleString()}
|
||||
label="입고"
|
||||
color="text-blue-600"
|
||||
/>
|
||||
<KpiCell
|
||||
icon="📤"
|
||||
value={loading ? "-" : kpi.outbound.toLocaleString()}
|
||||
label="출고"
|
||||
color="text-green-600"
|
||||
/>
|
||||
<KpiCell
|
||||
icon="🔄"
|
||||
value={loading ? "-" : kpi.transfer.toLocaleString()}
|
||||
label="이동"
|
||||
color="text-gray-400"
|
||||
/>
|
||||
<KpiCell
|
||||
icon="📊"
|
||||
value={loading ? "-" : kpi.total.toLocaleString()}
|
||||
label="전체"
|
||||
color="text-gray-900"
|
||||
/>
|
||||
{/* KPI + Tabs */}
|
||||
<div className="bg-white border-b border-gray-200 shrink-0">
|
||||
<div className="grid grid-cols-4 divide-x divide-gray-100">
|
||||
<KpiCell icon="📥" value={loading ? "-" : kpi.inbound.toLocaleString()} label="입고" color="text-blue-600" />
|
||||
<KpiCell icon="📤" value={loading ? "-" : kpi.outbound.toLocaleString()} label="출고" color="text-green-600" />
|
||||
<KpiCell icon="🔄" value={loading ? "-" : kpi.transfer.toLocaleString()} label="이동" color="text-gray-400" />
|
||||
<KpiCell icon="📊" value={loading ? "-" : kpi.total.toLocaleString()} label="전체" color="text-gray-900" />
|
||||
</div>
|
||||
<div className="flex gap-1.5 px-4 pb-3 pt-1">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => !tab.disabled && setActiveTab(tab.key)}
|
||||
disabled={tab.disabled}
|
||||
className={`px-5 py-2.5 rounded-xl text-base font-bold transition-all active:scale-[0.97] ${
|
||||
tab.disabled
|
||||
? "text-gray-300 bg-gray-50 cursor-not-allowed"
|
||||
: activeTab === tab.key
|
||||
? "text-white shadow-md"
|
||||
: "text-gray-600 bg-gray-100"
|
||||
}`}
|
||||
style={
|
||||
!tab.disabled && activeTab === tab.key
|
||||
? { background: "linear-gradient(135deg,#06b6d4,#0e7490)" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{tab.label} {tab.count}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => !tab.disabled && setActiveTab(tab.key)}
|
||||
disabled={tab.disabled}
|
||||
className={`shrink-0 px-4 py-2 rounded-full text-sm font-semibold transition-all ${
|
||||
tab.disabled
|
||||
? "text-gray-300 bg-gray-50 cursor-not-allowed"
|
||||
: activeTab === tab.key
|
||||
? "text-white shadow-sm"
|
||||
: "text-gray-600 bg-gray-100 hover:bg-gray-200 active:scale-95"
|
||||
}`}
|
||||
style={
|
||||
!tab.disabled && activeTab === tab.key
|
||||
? { background: "linear-gradient(135deg,#06b6d4,#0e7490)" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{tab.label} {tab.count}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-xs font-semibold text-gray-500">
|
||||
입출고 내역
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">총 {filtered.length}건</span>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<span className="text-sm font-bold text-gray-600">입출고 내역</span>
|
||||
<span className="text-sm text-gray-400">총 {filtered.length}건</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col gap-3 py-4">
|
||||
<div className="flex flex-col">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white rounded-2xl border border-gray-100 p-4 animate-pulse"
|
||||
>
|
||||
<div key={i} className="bg-white border-b border-gray-100 px-4 py-4 animate-pulse">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gray-100" />
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-100" />
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<div className="h-4 bg-gray-100 rounded w-2/3" />
|
||||
<div className="h-3 bg-gray-50 rounded w-1/2" />
|
||||
</div>
|
||||
<div className="h-5 w-12 bg-gray-100 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
|
||||
<svg
|
||||
className="w-16 h-16 mb-4 opacity-20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm font-medium text-gray-500 mb-1">
|
||||
입출고 내역이 없습니다
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">검색 조건을 변경해보세요</p>
|
||||
<div className="flex flex-col items-center justify-center py-20 text-gray-400 bg-white">
|
||||
<span className="text-5xl mb-3">📦</span>
|
||||
<p className="text-lg font-semibold mb-1">입출고 내역이 없습니다</p>
|
||||
<p className="text-sm text-gray-400">검색 조건을 변경해보세요</p>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 hover:shadow-md transition-shadow cursor-pointer active:scale-[0.98]"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="divide-y divide-gray-100">
|
||||
{filtered.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="w-full bg-white px-4 py-4 flex items-center gap-3 text-left active:bg-gray-50 transition-colors"
|
||||
>
|
||||
{/* Direction icon */}
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl flex items-center justify-center text-white text-lg shrink-0 ${
|
||||
item.direction === "입고" ? "" : ""
|
||||
}`}
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center text-white text-xl shrink-0"
|
||||
style={{
|
||||
background:
|
||||
item.direction === "입고"
|
||||
@@ -484,39 +419,30 @@ export function InOutHistory() {
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold text-gray-900 truncate">
|
||||
<span className="text-lg font-bold text-gray-900 truncate">
|
||||
{item.itemName}
|
||||
{item.itemCode ? ` (${item.itemCode})` : ""}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] font-bold px-1.5 py-0.5 rounded-full shrink-0 ${item.statusColor}`}
|
||||
>
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full shrink-0 ${item.statusColor}`}>
|
||||
{item.statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
<div className="text-sm text-gray-400 mt-0.5">
|
||||
{item.type} · {item.warehouse}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Qty + Time */}
|
||||
<div className="text-right shrink-0">
|
||||
<p
|
||||
className="text-base font-bold text-gray-900"
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
<p className="text-xl font-bold text-gray-900" style={{ fontVariantNumeric: "tabular-nums" }}>
|
||||
{item.qty.toLocaleString()}{" "}
|
||||
<span className="text-xs font-normal text-gray-400">
|
||||
{item.unit}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
{item.time}
|
||||
<span className="text-sm font-normal text-gray-400">{item.unit}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">{item.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -526,165 +452,87 @@ export function InOutHistory() {
|
||||
className="fixed inset-0 z-50 flex items-end justify-center"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 transition-opacity" />
|
||||
{/* Sheet */}
|
||||
<div className="absolute inset-0 bg-black/40" />
|
||||
<div
|
||||
className="relative w-full max-w-lg bg-white rounded-t-3xl shadow-2xl max-h-[85vh] overflow-y-auto animate-slide-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Handle bar */}
|
||||
<div className="sticky top-0 bg-white pt-3 pb-2 flex justify-center rounded-t-3xl z-10">
|
||||
<div className="w-10 h-1 rounded-full bg-gray-300" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 pb-4 border-b border-gray-100">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{selectedItem.direction === "입고" ? "입고" : "출고"} 상세 —{" "}
|
||||
{selectedItem.docNumber}
|
||||
{selectedItem.direction === "입고" ? "입고" : "출고"} 상세 — {selectedItem.docNumber}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-gray-400 hover:bg-gray-100"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<button onClick={() => setSelectedItem(null)} className="w-10 h-10 rounded-xl flex items-center justify-center text-gray-400 bg-gray-100 active:bg-gray-200">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-5 py-4 space-y-5">
|
||||
{/* Row 1: 전표번호 + 구분 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<DetailField label="전표번호" value={selectedItem.docNumber} />
|
||||
<DetailField label="구분" value={selectedItem.type} />
|
||||
</div>
|
||||
|
||||
{/* Row 2: 일시 + 상태 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<DetailField label="일시" value={selectedItem.fullDate} />
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-cyan-600 mb-1">
|
||||
상태
|
||||
</p>
|
||||
<span
|
||||
className={`inline-block text-xs font-bold px-2.5 py-1 rounded-full ${selectedItem.statusColor}`}
|
||||
>
|
||||
<p className="text-xs font-bold text-cyan-600 mb-1">상태</p>
|
||||
<span className={`inline-block text-sm font-bold px-3 py-1 rounded-lg ${selectedItem.statusColor}`}>
|
||||
{selectedItem.statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100" />
|
||||
|
||||
{/* Row 3: 품목 */}
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-cyan-600 mb-1">
|
||||
품목
|
||||
</p>
|
||||
<p className="text-base font-bold text-gray-900">
|
||||
<p className="text-xs font-bold text-cyan-600 mb-1">품목</p>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
{selectedItem.itemName}
|
||||
{selectedItem.itemCode ? ` (${selectedItem.itemCode})` : ""}
|
||||
{selectedItem.spec ? (
|
||||
<span className="text-sm font-normal text-gray-400 ml-2">
|
||||
{selectedItem.spec}
|
||||
</span>
|
||||
<span className="text-sm font-normal text-gray-400 ml-2">{selectedItem.spec}</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Row 4: 수량 + LOT */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-cyan-600 mb-1">
|
||||
수량
|
||||
</p>
|
||||
<p
|
||||
className="text-xl font-bold text-cyan-600"
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
<p className="text-xs font-bold text-cyan-600 mb-1">수량</p>
|
||||
<p className="text-2xl font-bold text-cyan-600" style={{ fontVariantNumeric: "tabular-nums" }}>
|
||||
{selectedItem.qty.toLocaleString()}{" "}
|
||||
<span className="text-sm font-normal text-gray-400">
|
||||
{selectedItem.unit}
|
||||
</span>
|
||||
<span className="text-base font-normal text-gray-400">{selectedItem.unit}</span>
|
||||
</p>
|
||||
</div>
|
||||
<DetailField
|
||||
label="LOT번호"
|
||||
value={selectedItem.lotNumber || "-"}
|
||||
/>
|
||||
<DetailField label="LOT번호" value={selectedItem.lotNumber || "-"} />
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100" />
|
||||
|
||||
{/* Row 5: 창고/위치 + 거래처 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-cyan-600 mb-1">
|
||||
창고 / 위치
|
||||
</p>
|
||||
<p className="text-sm font-bold text-gray-900">
|
||||
{selectedItem.warehouse}
|
||||
</p>
|
||||
<p className="text-xs font-bold text-cyan-600 mb-1">창고 / 위치</p>
|
||||
<p className="text-base font-bold text-gray-900">{selectedItem.warehouse}</p>
|
||||
{selectedItem.locationCode && (
|
||||
<p className="text-xs text-gray-400">
|
||||
{selectedItem.locationCode}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">{selectedItem.locationCode}</p>
|
||||
)}
|
||||
</div>
|
||||
<DetailField label="거래처" value={selectedItem.partnerName} />
|
||||
</div>
|
||||
|
||||
{/* Row 6: 작업자 + 비고 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<DetailField
|
||||
label="작업자"
|
||||
value={selectedItem.writer || "-"}
|
||||
/>
|
||||
<DetailField label="작업자" value={selectedItem.writer || "-"} />
|
||||
<DetailField label="비고" value={selectedItem.memo || "-"} />
|
||||
</div>
|
||||
|
||||
{/* Row 7: 참조번호 + 금액 (있을 때만) */}
|
||||
{(selectedItem.referenceNumber ||
|
||||
selectedItem.totalAmount > 0) && (
|
||||
{(selectedItem.referenceNumber || selectedItem.totalAmount > 0) && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{selectedItem.referenceNumber ? (
|
||||
<DetailField
|
||||
label="참조번호"
|
||||
value={selectedItem.referenceNumber}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<DetailField label="참조번호" value={selectedItem.referenceNumber} />
|
||||
) : <div />}
|
||||
{selectedItem.totalAmount > 0 ? (
|
||||
<DetailField
|
||||
label="금액"
|
||||
value={`${selectedItem.totalAmount.toLocaleString()}원`}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<DetailField label="금액" value={`${selectedItem.totalAmount.toLocaleString()}원`} />
|
||||
) : <div />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-5 py-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="w-full py-3 rounded-xl text-sm font-bold text-gray-600 bg-gray-100 active:scale-95 transition-all"
|
||||
className="w-full py-4 rounded-xl text-lg font-bold text-gray-600 bg-gray-100 active:bg-gray-200 active:scale-[0.98] transition-all"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
@@ -694,14 +542,14 @@ export function InOutHistory() {
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes slide-up {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
`}</style>
|
||||
@keyframes slide-up {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -713,8 +561,8 @@ export function InOutHistory() {
|
||||
function DetailField({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-cyan-600 mb-1">{label}</p>
|
||||
<p className="text-sm font-semibold text-gray-900">{value}</p>
|
||||
<p className="text-xs font-bold text-cyan-600 mb-1">{label}</p>
|
||||
<p className="text-base font-semibold text-gray-900">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -731,17 +579,15 @@ function KpiCell({
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center py-2">
|
||||
<span className="text-lg mb-0.5">{icon}</span>
|
||||
<div className="flex flex-col items-center py-3">
|
||||
<span className="text-xl mb-0.5">{icon}</span>
|
||||
<span
|
||||
className={`text-xl sm:text-2xl font-extrabold leading-none ${color}`}
|
||||
className={`text-2xl sm:text-3xl font-extrabold leading-none ${color}`}
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<span className="text-[10px] font-medium text-gray-400 mt-1">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-gray-400 mt-1">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,3 @@
|
||||
export { AdjustHistory } from "./AdjustHistory";
|
||||
export { InOutHistory } from "./InOutHistory";
|
||||
export { InventoryHome } from "./InventoryHome";
|
||||
|
||||
@@ -51,6 +51,7 @@ interface ProcessData {
|
||||
target_location_code: string | null;
|
||||
is_rework: string;
|
||||
routing_detail_id: string | null;
|
||||
batch_id?: string | null;
|
||||
}
|
||||
|
||||
interface WorkInstructionInfo {
|
||||
@@ -451,6 +452,27 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// batch_id가 있으면 해당 품목의 이름을 조회 (다중 품목 지원)
|
||||
if (procData.batch_id) {
|
||||
try {
|
||||
const batchItemRes = await dataApi.getTableData("item_info", {
|
||||
size: 1,
|
||||
filters: { item_number: procData.batch_id },
|
||||
});
|
||||
const batchItem = batchItemRes.data?.[0] as Record<string, unknown> | undefined;
|
||||
if (batchItem) {
|
||||
itemName = String(batchItem.item_name || procData.batch_id);
|
||||
itemCode = String(batchItem.item_number || procData.batch_id);
|
||||
} else {
|
||||
itemName = procData.batch_id;
|
||||
itemCode = procData.batch_id;
|
||||
}
|
||||
} catch {
|
||||
itemName = procData.batch_id;
|
||||
itemCode = procData.batch_id;
|
||||
}
|
||||
}
|
||||
|
||||
setWiInfo({
|
||||
work_instruction_no: String(wi.work_instruction_no || ""),
|
||||
item_name: itemName,
|
||||
|
||||
@@ -111,6 +111,8 @@ interface WorkOrderProcess {
|
||||
accepted_by?: string;
|
||||
accepted_at?: string | null;
|
||||
created_date?: string;
|
||||
batch_id?: string | null;
|
||||
equipment_code?: string;
|
||||
}
|
||||
|
||||
interface ProcessMng {
|
||||
@@ -222,12 +224,14 @@ function FullscreenWorkModal({
|
||||
processId,
|
||||
myProcesses,
|
||||
instructionMap,
|
||||
itemNameMap,
|
||||
onSwitch,
|
||||
onClose,
|
||||
}: {
|
||||
processId: string;
|
||||
myProcesses: WorkOrderProcess[];
|
||||
instructionMap: Record<string, WorkInstruction>;
|
||||
itemNameMap: Record<string, string>;
|
||||
onSwitch: (id: string) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
@@ -302,10 +306,9 @@ function FullscreenWorkModal({
|
||||
{wi?.work_instruction_no || "작업지시"}
|
||||
</div>
|
||||
<AutoScrollText className="text-sm text-gray-500 mb-0.5">
|
||||
📦 {wi?.item_name || ""}
|
||||
{wi?.item_code || wi?.item_number
|
||||
? `(${wi?.item_code || wi?.item_number})`
|
||||
: ""}
|
||||
📦 {proc.batch_id
|
||||
? `${itemNameMap[proc.batch_id] || proc.batch_id}(${proc.batch_id})`
|
||||
: `${wi?.item_name || ""}${wi?.item_code || wi?.item_number ? `(${wi?.item_code || wi?.item_number})` : ""}`}
|
||||
</AutoScrollText>
|
||||
<div className="text-sm text-gray-600 mb-0.5">
|
||||
{proc.process_name} · {proc.equipment_code || "미배정"}
|
||||
@@ -463,7 +466,11 @@ function CompressedProcessSteps({
|
||||
allProcesses?: WorkOrderProcess[];
|
||||
}) {
|
||||
const sorted = [...processes]
|
||||
.filter((p) => !p.parent_process_id)
|
||||
.filter((p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 표시 (다중 품목 구분)
|
||||
(!batchId && !p.batch_id) ||
|
||||
(batchId && p.batch_id === batchId)
|
||||
))
|
||||
.sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10));
|
||||
|
||||
if (sorted.length === 0) return null;
|
||||
@@ -479,7 +486,7 @@ function CompressedProcessSteps({
|
||||
if (batchId && allProcesses) {
|
||||
const batchSplits = allProcesses.filter(
|
||||
(p) =>
|
||||
(p as Record<string, unknown>).batch_id === batchId &&
|
||||
p.batch_id === batchId &&
|
||||
p.parent_process_id &&
|
||||
p.status === "completed",
|
||||
);
|
||||
@@ -963,6 +970,7 @@ export function WorkOrderList() {
|
||||
const [allProcesses, setAllProcesses] = useState<WorkOrderProcess[]>([]);
|
||||
const [processList, setProcessList] = useState<ProcessMng[]>([]);
|
||||
const [equipmentList, setEquipmentList] = useState<EquipmentMng[]>([]);
|
||||
const [itemNameMap, setItemNameMap] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabFilter>("acceptable");
|
||||
@@ -1044,22 +1052,31 @@ export function WorkOrderList() {
|
||||
wiRaw = wiRes.data;
|
||||
}
|
||||
// wi_id → id 매핑 + 중복 제거 (header+detail 조인이므로 첫 행만)
|
||||
// item_number → item_name 매핑 구축 (다중 품목 표시용)
|
||||
const seen = new Set<string>();
|
||||
const wiData: WorkInstruction[] = [];
|
||||
const newItemNameMap: Record<string, string> = {};
|
||||
for (const raw of wiRaw) {
|
||||
const wiId = String(raw.wi_id || raw.id || "");
|
||||
// item_number → item_name 매핑 (모든 행에서 수집)
|
||||
const rawItemNumber = String(raw.item_number || "");
|
||||
const rawItemName = String(raw.item_name || "");
|
||||
if (rawItemNumber && rawItemName) {
|
||||
newItemNameMap[rawItemNumber] = rawItemName;
|
||||
}
|
||||
if (!wiId || seen.has(wiId)) continue;
|
||||
seen.add(wiId);
|
||||
wiData.push({
|
||||
...raw,
|
||||
id: wiId,
|
||||
item_name: String(raw.item_name || ""),
|
||||
item_name: rawItemName,
|
||||
item_code: String(raw.item_code || ""),
|
||||
item_number: String(raw.item_number || ""),
|
||||
item_number: rawItemNumber,
|
||||
qty: parseInt(String(raw.total_qty || raw.qty || 0), 10),
|
||||
} as unknown as WorkInstruction);
|
||||
}
|
||||
setInstructions(wiData);
|
||||
setItemNameMap(newItemNameMap);
|
||||
|
||||
const procRes = await dataApi.getTableData("work_order_process", {
|
||||
size: 1000,
|
||||
@@ -1365,7 +1382,11 @@ export function WorkOrderList() {
|
||||
const openDetailModal = (proc: WorkOrderProcess) => {
|
||||
const wi = instructionMap[proc.wo_id];
|
||||
const siblings = (processesByWo[proc.wo_id] || [])
|
||||
.filter((p) => !p.parent_process_id)
|
||||
.filter((p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 형제 (다중 품목 구분)
|
||||
(!proc.batch_id && !p.batch_id) ||
|
||||
(proc.batch_id && p.batch_id === proc.batch_id)
|
||||
))
|
||||
.sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10));
|
||||
|
||||
const totalQty = wi ? wi.qty : parseInt(proc.plan_qty || "0", 10);
|
||||
@@ -1448,7 +1469,11 @@ export function WorkOrderList() {
|
||||
/* ---- Helper: get previous process info ---- */
|
||||
const getPrevProcessInfo = (proc: WorkOrderProcess) => {
|
||||
const siblings = (processesByWo[proc.wo_id] || [])
|
||||
.filter((p) => !p.parent_process_id)
|
||||
.filter((p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 형제 (다중 품목 구분)
|
||||
(!proc.batch_id && !p.batch_id) ||
|
||||
(proc.batch_id && p.batch_id === proc.batch_id)
|
||||
))
|
||||
.sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10));
|
||||
|
||||
const currentIdx = siblings.findIndex((p) => p.id === proc.id);
|
||||
@@ -1726,7 +1751,11 @@ export function WorkOrderList() {
|
||||
const wi = instructionMap[proc.wo_id];
|
||||
const badge = STATUS_BADGE[proc.status] || STATUS_BADGE.waiting;
|
||||
const siblingProcesses = (processesByWo[proc.wo_id] || []).filter(
|
||||
(p) => !p.parent_process_id,
|
||||
(p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 형제 (다중 품목 구분)
|
||||
(!proc.batch_id && !p.batch_id) ||
|
||||
(proc.batch_id && p.batch_id === proc.batch_id)
|
||||
),
|
||||
);
|
||||
const planQty = parseInt(proc.plan_qty || "0", 10);
|
||||
const goodQty = parseInt(proc.good_qty || "0", 10);
|
||||
@@ -1883,10 +1912,9 @@ export function WorkOrderList() {
|
||||
|
||||
{/* Sub-info: item name + equipment */}
|
||||
<AutoScrollText className="text-sm text-gray-500 mb-3">
|
||||
📦 {wi?.item_name || "품목"}
|
||||
{wi?.item_code || wi?.item_number
|
||||
? `(${wi?.item_code || wi?.item_number})`
|
||||
: ""}
|
||||
📦 {proc.batch_id
|
||||
? `${itemNameMap[proc.batch_id] || proc.batch_id}(${proc.batch_id})`
|
||||
: `${wi?.item_name || "품목"}${wi?.item_code || wi?.item_number ? `(${wi?.item_code || wi?.item_number})` : ""}`}
|
||||
{" · "}
|
||||
{!isRework
|
||||
? `⚙️ ${eqName}`
|
||||
@@ -1901,9 +1929,7 @@ export function WorkOrderList() {
|
||||
status={proc.status}
|
||||
onClick={() => openDetailModal(proc)}
|
||||
batchId={
|
||||
(proc as Record<string, unknown>).batch_id as
|
||||
| string
|
||||
| undefined
|
||||
proc.batch_id ?? undefined
|
||||
}
|
||||
allProcesses={allProcesses}
|
||||
/>
|
||||
@@ -1965,7 +1991,7 @@ export function WorkOrderList() {
|
||||
proc.id,
|
||||
proc.process_name,
|
||||
proc.seq_no,
|
||||
(proc as Record<string, unknown>)
|
||||
(proc as unknown as Record<string, unknown>)
|
||||
.rework_source_id as string | undefined,
|
||||
)
|
||||
}
|
||||
@@ -2080,6 +2106,7 @@ export function WorkOrderList() {
|
||||
p.status === "in_progress",
|
||||
)}
|
||||
instructionMap={instructionMap}
|
||||
itemNameMap={itemNameMap}
|
||||
onSwitch={(id) => setWorkModalProcessId(id)}
|
||||
onClose={() => {
|
||||
setWorkModalProcessId(null);
|
||||
|
||||
Reference in New Issue
Block a user