Merge pull request 'jskim-node' (#24) from jskim-node into main
Reviewed-on: https://g.wace.me/jskim/vexplor_dev/pulls/24
This commit is contained in:
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,16 +324,24 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_10`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => setCatMap(map));
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -372,7 +380,7 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
const res = await getOutboundList(params);
|
const res = await getOutboundList(params);
|
||||||
if (res.success) setData(res.data);
|
if (res.success) setData(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -389,7 +397,7 @@ export default function OutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await getOutboundWarehouses();
|
const res = await getOutboundWarehouses();
|
||||||
if (res.success) setWarehouses(res.data);
|
if (res.success) setWarehouses(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -565,7 +573,7 @@ export default function OutboundPage() {
|
|||||||
const res = await getItemSources(keyword || undefined);
|
const res = await getItemSources(keyword || undefined);
|
||||||
if (res.success) setItems(res.data);
|
if (res.success) setItems(res.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setSourceLoading(false);
|
setSourceLoading(false);
|
||||||
@@ -597,7 +605,7 @@ export default function OutboundPage() {
|
|||||||
loadSourceData(defaultType),
|
loadSourceData(defaultType),
|
||||||
]);
|
]);
|
||||||
if (numRes.success) setModalOutboundNo(numRes.data);
|
if (numRes.success) setModalOutboundNo(numRes.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
setModalOutboundNo("");
|
setModalOutboundNo("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -901,7 +909,8 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg);
|
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다.");
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1532,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,25 +324,28 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch (e: any) { console.error("[outbound] cat load error:", col, e?.message, e?.response?.status, e?.response?.data); }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => {
|
(async () => {
|
||||||
console.log("[outbound] catMap loaded:", JSON.stringify(map).slice(0, 200));
|
try {
|
||||||
setCatMap(map);
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
});
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
const result = catMap[col]?.[code] || code;
|
return catMap[col]?.[code] || code;
|
||||||
if (code.startsWith("CAT_")) console.log("[outbound] resolveCat:", col, code, "->", result, "catMap keys:", Object.keys(catMap));
|
|
||||||
return result;
|
|
||||||
}, [catMap]);
|
}, [catMap]);
|
||||||
|
|
||||||
// 소스 데이터
|
// 소스 데이터
|
||||||
@@ -377,7 +380,7 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
const res = await getOutboundList(params);
|
const res = await getOutboundList(params);
|
||||||
if (res.success) setData(res.data);
|
if (res.success) setData(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -394,7 +397,7 @@ export default function OutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await getOutboundWarehouses();
|
const res = await getOutboundWarehouses();
|
||||||
if (res.success) setWarehouses(res.data);
|
if (res.success) setWarehouses(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -570,7 +573,7 @@ export default function OutboundPage() {
|
|||||||
const res = await getItemSources(keyword || undefined);
|
const res = await getItemSources(keyword || undefined);
|
||||||
if (res.success) setItems(res.data);
|
if (res.success) setItems(res.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setSourceLoading(false);
|
setSourceLoading(false);
|
||||||
@@ -602,7 +605,7 @@ export default function OutboundPage() {
|
|||||||
loadSourceData(defaultType),
|
loadSourceData(defaultType),
|
||||||
]);
|
]);
|
||||||
if (numRes.success) setModalOutboundNo(numRes.data);
|
if (numRes.success) setModalOutboundNo(numRes.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
setModalOutboundNo("");
|
setModalOutboundNo("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -906,7 +909,8 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg);
|
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다.");
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1119,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1537,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,16 +324,24 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_29`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => setCatMap(map));
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -372,7 +380,7 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
const res = await getOutboundList(params);
|
const res = await getOutboundList(params);
|
||||||
if (res.success) setData(res.data);
|
if (res.success) setData(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -389,7 +397,7 @@ export default function OutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await getOutboundWarehouses();
|
const res = await getOutboundWarehouses();
|
||||||
if (res.success) setWarehouses(res.data);
|
if (res.success) setWarehouses(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -565,7 +573,7 @@ export default function OutboundPage() {
|
|||||||
const res = await getItemSources(keyword || undefined);
|
const res = await getItemSources(keyword || undefined);
|
||||||
if (res.success) setItems(res.data);
|
if (res.success) setItems(res.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setSourceLoading(false);
|
setSourceLoading(false);
|
||||||
@@ -597,7 +605,7 @@ export default function OutboundPage() {
|
|||||||
loadSourceData(defaultType),
|
loadSourceData(defaultType),
|
||||||
]);
|
]);
|
||||||
if (numRes.success) setModalOutboundNo(numRes.data);
|
if (numRes.success) setModalOutboundNo(numRes.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
setModalOutboundNo("");
|
setModalOutboundNo("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -901,7 +909,8 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg);
|
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다.");
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1532,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,16 +324,24 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_30`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => setCatMap(map));
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -372,7 +380,7 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
const res = await getOutboundList(params);
|
const res = await getOutboundList(params);
|
||||||
if (res.success) setData(res.data);
|
if (res.success) setData(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -389,7 +397,7 @@ export default function OutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await getOutboundWarehouses();
|
const res = await getOutboundWarehouses();
|
||||||
if (res.success) setWarehouses(res.data);
|
if (res.success) setWarehouses(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -565,7 +573,7 @@ export default function OutboundPage() {
|
|||||||
const res = await getItemSources(keyword || undefined);
|
const res = await getItemSources(keyword || undefined);
|
||||||
if (res.success) setItems(res.data);
|
if (res.success) setItems(res.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setSourceLoading(false);
|
setSourceLoading(false);
|
||||||
@@ -597,7 +605,7 @@ export default function OutboundPage() {
|
|||||||
loadSourceData(defaultType),
|
loadSourceData(defaultType),
|
||||||
]);
|
]);
|
||||||
if (numRes.success) setModalOutboundNo(numRes.data);
|
if (numRes.success) setModalOutboundNo(numRes.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
setModalOutboundNo("");
|
setModalOutboundNo("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -901,7 +909,8 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg);
|
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다.");
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1532,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,16 +324,24 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => setCatMap(map));
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -1115,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1533,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,16 +324,24 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_8`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => setCatMap(map));
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -372,7 +380,7 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
const res = await getOutboundList(params);
|
const res = await getOutboundList(params);
|
||||||
if (res.success) setData(res.data);
|
if (res.success) setData(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -389,7 +397,7 @@ export default function OutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await getOutboundWarehouses();
|
const res = await getOutboundWarehouses();
|
||||||
if (res.success) setWarehouses(res.data);
|
if (res.success) setWarehouses(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -565,7 +573,7 @@ export default function OutboundPage() {
|
|||||||
const res = await getItemSources(keyword || undefined);
|
const res = await getItemSources(keyword || undefined);
|
||||||
if (res.success) setItems(res.data);
|
if (res.success) setItems(res.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setSourceLoading(false);
|
setSourceLoading(false);
|
||||||
@@ -597,7 +605,7 @@ export default function OutboundPage() {
|
|||||||
loadSourceData(defaultType),
|
loadSourceData(defaultType),
|
||||||
]);
|
]);
|
||||||
if (numRes.success) setModalOutboundNo(numRes.data);
|
if (numRes.success) setModalOutboundNo(numRes.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
setModalOutboundNo("");
|
setModalOutboundNo("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -901,7 +909,8 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg);
|
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다.");
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1532,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ const fmtDate = (v: any) => {
|
|||||||
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
return m ? `${m[1]}-${m[2]}-${m[3]}` : s.substring(0, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** remark가 JSON이면 사람이 읽을 수 있는 텍스트로 변환 */
|
||||||
|
const parseRemark = (remark: string | null | undefined): string => {
|
||||||
|
if (!remark) return "";
|
||||||
|
const trimmed = remark.trim();
|
||||||
|
if (!trimmed.startsWith("{")) return trimmed;
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(trimmed);
|
||||||
|
switch (d.type) {
|
||||||
|
case "move":
|
||||||
|
return `창고이동 (${d.from_warehouse} → ${d.to_warehouse})`;
|
||||||
|
case "adjust":
|
||||||
|
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}→${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
|
||||||
|
case "confirm":
|
||||||
|
return `재고확인 (${d.reason || "이상없음"})`;
|
||||||
|
case "process_inbound":
|
||||||
|
return "공정입고";
|
||||||
|
default:
|
||||||
|
return d.reason || d.memo || trimmed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function InboundOutboundPage() {
|
export default function InboundOutboundPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
@@ -62,7 +86,6 @@ export default function InboundOutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const filters: any[] = [];
|
const filters: any[] = [];
|
||||||
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
if (typeFilter !== "all") filters.push({ columnName: "transaction_type", operator: "equals", value: typeFilter });
|
||||||
if (categoryFilter !== "all") filters.push({ columnName: "remark", operator: "equals", value: categoryFilter });
|
|
||||||
|
|
||||||
for (const f of searchFilters) {
|
for (const f of searchFilters) {
|
||||||
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
if (f.value) filters.push({ columnName: f.columnName, operator: f.operator, value: f.value });
|
||||||
@@ -81,6 +104,21 @@ export default function InboundOutboundPage() {
|
|||||||
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
const itemCodes = [...new Set(rows.map((r: any) => r.item_code).filter(Boolean))];
|
||||||
if (itemCodes.length > 0) {
|
if (itemCodes.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// 단위 카테고리 코드→라벨 매핑 로드
|
||||||
|
let unitLabelMap: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const catRes = await apiClient.get("/table-categories/item_info/unit/values");
|
||||||
|
if (catRes.data?.success && catRes.data.data?.length > 0) {
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
unitLabelMap[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(catRes.data.data);
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
|
||||||
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
const itemRes = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||||
page: 1, size: itemCodes.length + 10,
|
page: 1, size: itemCodes.length + 10,
|
||||||
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: itemCodes }] },
|
||||||
@@ -89,7 +127,8 @@ export default function InboundOutboundPage() {
|
|||||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||||
const map: Record<string, { item_name: string; unit: string }> = {};
|
const map: Record<string, { item_name: string; unit: string }> = {};
|
||||||
for (const i of items) {
|
for (const i of items) {
|
||||||
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: i.unit || "" };
|
const rawUnit = i.unit || "";
|
||||||
|
if (!map[i.item_number]) map[i.item_number] = { item_name: i.item_name || "", unit: unitLabelMap[rawUnit] || rawUnit };
|
||||||
}
|
}
|
||||||
setItemMap(map);
|
setItemMap(map);
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
@@ -124,7 +163,7 @@ export default function InboundOutboundPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, typeFilter, categoryFilter]);
|
}, [searchFilters, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => { fetchData(); }, [fetchData]);
|
useEffect(() => { fetchData(); }, [fetchData]);
|
||||||
|
|
||||||
@@ -132,20 +171,26 @@ export default function InboundOutboundPage() {
|
|||||||
|
|
||||||
const categoryOptions = useMemo(() => {
|
const categoryOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
data.forEach((r) => { if (r.remark) set.add(r.remark); });
|
data.forEach((r) => { if (r.remark) set.add(parseRemark(r.remark)); });
|
||||||
return Array.from(set).sort();
|
return Array.from(set).sort();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// ════════ 그룹핑 ════════
|
// ════════ 그룹핑 ════════
|
||||||
|
|
||||||
|
// 카테고리 필터 (클라이언트)
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (categoryFilter === "all") return data;
|
||||||
|
return data.filter((r) => parseRemark(r.remark) === categoryFilter);
|
||||||
|
}, [data, categoryFilter]);
|
||||||
|
|
||||||
const groupedData = useMemo(() => {
|
const groupedData = useMemo(() => {
|
||||||
if (groupBy === "none") return data;
|
if (groupBy === "none") return filteredData;
|
||||||
const groups = new Map<string, any[]>();
|
const groups = new Map<string, any[]>();
|
||||||
for (const row of data) {
|
for (const row of filteredData) {
|
||||||
let key: string;
|
let key: string;
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
case "transaction_type": key = row.transaction_type || "미지정"; break;
|
||||||
case "remark": key = row.remark || "미지정"; break;
|
case "remark": key = parseRemark(row.remark) || "미지정"; break;
|
||||||
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
case "warehouse_code": key = warehouseMap[row.warehouse_code] || row.warehouse_code || "미지정"; break;
|
||||||
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
case "item_code": key = `${row.item_code} ${itemMap[row.item_code]?.item_name || ""}`.trim() || "미지정"; break;
|
||||||
default: key = row[groupBy] || "미지정";
|
default: key = row[groupBy] || "미지정";
|
||||||
@@ -191,7 +236,7 @@ export default function InboundOutboundPage() {
|
|||||||
const rows = data.map((r, i) => ({
|
const rows = data.map((r, i) => ({
|
||||||
No: i + 1,
|
No: i + 1,
|
||||||
입출고구분: r.transaction_type || "",
|
입출고구분: r.transaction_type || "",
|
||||||
카테고리: r.remark || "",
|
카테고리: parseRemark(r.remark),
|
||||||
처리일자: fmtDate(r.transaction_date),
|
처리일자: fmtDate(r.transaction_date),
|
||||||
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
창고: warehouseMap[r.warehouse_code] || r.warehouse_code || "",
|
||||||
위치: r.location_code || "",
|
위치: r.location_code || "",
|
||||||
@@ -252,7 +297,7 @@ export default function InboundOutboundPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="w-4 h-4 text-muted-foreground" />
|
<Package className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-semibold">입출고 내역</span>
|
<span className="text-sm font-semibold">입출고 내역</span>
|
||||||
<Badge variant="secondary" className="font-mono text-xs">{data.length}건</Badge>
|
<Badge variant="secondary" className="font-mono text-xs">{filteredData.length}건</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||||
@@ -353,7 +398,7 @@ export default function InboundOutboundPage() {
|
|||||||
{row.transaction_type}
|
{row.transaction_type}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.remark || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{parseRemark(row.remark) || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
<TableCell className="text-center text-[12px]">{fmtDate(row.transaction_date)}</TableCell>
|
||||||
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
<TableCell className="text-[12px]">{warehouseMap[row.warehouse_code] || row.warehouse_code || "-"}</TableCell>
|
||||||
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
<TableCell className="text-center text-[12px]">{row.location_code || "-"}</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
} from "@/lib/api/materialStatus";
|
} from "@/lib/api/materialStatus";
|
||||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
const GRID_COLUMNS = [
|
const GRID_COLUMNS = [
|
||||||
{ key: "plan_no", label: "계획번호" },
|
{ key: "plan_no", label: "계획번호" },
|
||||||
@@ -60,26 +61,31 @@ const formatDate = (date: Date) => {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status: string) => {
|
const FALLBACK_STATUS_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "계획",
|
||||||
planned: "계획",
|
in_progress: "진행중",
|
||||||
in_progress: "진행중",
|
completed: "완료",
|
||||||
completed: "완료",
|
pending: "대기",
|
||||||
pending: "대기",
|
cancelled: "취소",
|
||||||
cancelled: "취소",
|
|
||||||
};
|
|
||||||
return map[status] || status;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusStyle = (status: string) => {
|
const STATUS_STYLE_MAP: Record<string, string> = {
|
||||||
const map: Record<string, string> = {
|
planned: "bg-secondary text-secondary-foreground border-border",
|
||||||
planned: "bg-secondary text-secondary-foreground border-border",
|
pending: "bg-secondary text-secondary-foreground border-border",
|
||||||
pending: "bg-secondary text-secondary-foreground border-border",
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
||||||
in_progress: "bg-primary/10 text-primary border-primary/20",
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
||||||
completed: "bg-accent text-accent-foreground border-accent/50",
|
cancelled: "bg-muted text-muted-foreground border-border",
|
||||||
cancelled: "bg-muted text-muted-foreground border-border",
|
};
|
||||||
};
|
|
||||||
return map[status] || "bg-muted text-muted-foreground border-border";
|
// 카테고리 라벨 기반으로 스타일 매칭
|
||||||
|
const LABEL_STYLE_MAP: Record<string, string> = {
|
||||||
|
"일반": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"긴급": "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
"계획": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"대기": "bg-secondary text-secondary-foreground border-border",
|
||||||
|
"진행중": "bg-primary/10 text-primary border-primary/20",
|
||||||
|
"완료": "bg-accent text-accent-foreground border-accent/50",
|
||||||
|
"취소": "bg-muted text-muted-foreground border-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MaterialStatusPage() {
|
export default function MaterialStatusPage() {
|
||||||
@@ -93,6 +99,32 @@ export default function MaterialStatusPage() {
|
|||||||
const [searchItemCode, setSearchItemCode] = useState("");
|
const [searchItemCode, setSearchItemCode] = useState("");
|
||||||
const [searchItemName, setSearchItemName] = useState("");
|
const [searchItemName, setSearchItemName] = useState("");
|
||||||
|
|
||||||
|
// 카테고리 코드→라벨 매핑
|
||||||
|
const [statusMap, setStatusMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get("/table-categories/work_instruction/status/values");
|
||||||
|
if (res.data?.success && res.data.data?.length > 0) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const flatten = (vals: any[]) => {
|
||||||
|
for (const v of vals) {
|
||||||
|
map[v.valueCode] = v.valueLabel;
|
||||||
|
if (v.children?.length) flatten(v.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(res.data.data);
|
||||||
|
setStatusMap(map);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusLabel = useCallback((status: string) => {
|
||||||
|
return statusMap[status] || FALLBACK_STATUS_MAP[status] || status;
|
||||||
|
}, [statusMap]);
|
||||||
|
|
||||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||||
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
||||||
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
||||||
@@ -369,7 +401,7 @@ export default function MaterialStatusPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
||||||
getStatusStyle(wo.status)
|
STATUS_STYLE_MAP[wo.status] || LABEL_STYLE_MAP[getStatusLabel(wo.status)] || "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getStatusLabel(wo.status)}
|
{getStatusLabel(wo.status)}
|
||||||
|
|||||||
@@ -324,16 +324,24 @@ export default function OutboundPage() {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const map: Record<string, Record<string, string>> = {};
|
const map: Record<string, Record<string, string>> = {};
|
||||||
Promise.all(
|
Promise.all([
|
||||||
["material", "unit"].map(async (col) => {
|
...["material", "unit"].map(async (col) => {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_9`);
|
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||||
const items = flatten(res.data?.data || []);
|
const items = flatten(res.data?.data || []);
|
||||||
map[col] = {};
|
map[col] = {};
|
||||||
for (const item of items) map[col][item.code] = item.label;
|
for (const item of items) map[col][item.code] = item.label;
|
||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
})
|
}),
|
||||||
).then(() => setCatMap(map));
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/table-categories/outbound_mng/outbound_type/values`);
|
||||||
|
const items = flatten(res.data?.data || []);
|
||||||
|
map["outbound_type"] = {};
|
||||||
|
for (const item of items) map["outbound_type"][item.code] = item.label;
|
||||||
|
} catch { /* skip */ }
|
||||||
|
})(),
|
||||||
|
]).then(() => setCatMap(map));
|
||||||
}, []);
|
}, []);
|
||||||
const resolveCat = useCallback((col: string, code: string) => {
|
const resolveCat = useCallback((col: string, code: string) => {
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -372,7 +380,7 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
const res = await getOutboundList(params);
|
const res = await getOutboundList(params);
|
||||||
if (res.success) setData(res.data);
|
if (res.success) setData(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -389,7 +397,7 @@ export default function OutboundPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await getOutboundWarehouses();
|
const res = await getOutboundWarehouses();
|
||||||
if (res.success) setWarehouses(res.data);
|
if (res.success) setWarehouses(res.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -565,7 +573,7 @@ export default function OutboundPage() {
|
|||||||
const res = await getItemSources(keyword || undefined);
|
const res = await getItemSources(keyword || undefined);
|
||||||
if (res.success) setItems(res.data);
|
if (res.success) setItems(res.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
setSourceLoading(false);
|
setSourceLoading(false);
|
||||||
@@ -597,7 +605,7 @@ export default function OutboundPage() {
|
|||||||
loadSourceData(defaultType),
|
loadSourceData(defaultType),
|
||||||
]);
|
]);
|
||||||
if (numRes.success) setModalOutboundNo(numRes.data);
|
if (numRes.success) setModalOutboundNo(numRes.data);
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
setModalOutboundNo("");
|
setModalOutboundNo("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -901,7 +909,8 @@ export default function OutboundPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다."); toast.error(msg);
|
const msg = err?.response?.data?.message || (editMode ? "수정에 실패했어요" : "출고 등록 중 오류가 발생했습니다.");
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1123,7 @@ export default function OutboundPage() {
|
|||||||
case "outbound_type": return (
|
case "outbound_type": return (
|
||||||
<TableCell key={col.key}>
|
<TableCell key={col.key}>
|
||||||
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[11px]", getTypeColor(master.outbound_type))}>
|
||||||
{master.outbound_type || "-"}
|
{resolveCat("outbound_type", master.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
);
|
);
|
||||||
@@ -1532,7 +1541,7 @@ export default function OutboundPage() {
|
|||||||
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
<TableCell className="p-2 text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="p-2">
|
<TableCell className="p-2">
|
||||||
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
<Badge variant="outline" className={cn("text-[10px]", getTypeColor(item.outbound_type))}>
|
||||||
{item.outbound_type || "-"}
|
{resolveCat("outbound_type", item.outbound_type) || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-[180px] p-2">
|
<TableCell className="max-w-[180px] p-2">
|
||||||
|
|||||||
@@ -349,7 +349,14 @@ export default function WorkInstructionPage() {
|
|||||||
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0);
|
||||||
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100));
|
||||||
};
|
};
|
||||||
const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; };
|
const getProgressLabel = (o: any) => {
|
||||||
|
const p = getProgress(o);
|
||||||
|
if (o.progress_status) {
|
||||||
|
const map: Record<string, string> = { completed: "완료", in_progress: "진행중", pending: "대기" };
|
||||||
|
return map[o.progress_status] || o.progress_status;
|
||||||
|
}
|
||||||
|
if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기";
|
||||||
|
};
|
||||||
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize));
|
||||||
|
|
||||||
const getDisplayNo = (o: any) => {
|
const getDisplayNo = (o: any) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user