refactor: Update inventory and unit handling across logistics and purchase pages
- Changed references from `unit` to `inventory_unit` in various logistics and purchase components to improve consistency and clarity. - Updated API calls to fetch inventory unit values instead of generic unit values, ensuring accurate data representation. - Enhanced data mapping and rendering logic to reflect the new inventory unit structure, improving user experience and data integrity. - These changes aim to streamline inventory management processes and enhance usability across multiple company implementations.
This commit is contained in:
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubcontractorItemPage() {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
@@ -158,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -518,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1184,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1228,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -414,7 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -501,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -833,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1134,7 +1135,7 @@ export default function BomManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 버전 ID 가져오기
|
||||
// 현재 버전 ID 가져오기 (initialize-version 후 최신 값)
|
||||
let versionId: string | null = null;
|
||||
if (bomId) {
|
||||
try {
|
||||
@@ -1205,6 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1213,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1228,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -2258,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2409,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function PurchaseOrderPage() {
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -346,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -642,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1246,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -1152,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1164,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -959,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2472,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2514,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -976,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2461,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2503,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function SalesOrderPage() {
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -358,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -829,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1533,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1542,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1707,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,13 +162,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -1159,7 +1158,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1171,7 +1170,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubcontractorItemPage() {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
@@ -158,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -518,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1184,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1228,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -414,8 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// item_info의 division, unit 카테고리
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -502,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -834,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1206,7 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + unit)
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1215,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1230,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -2260,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2411,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function PurchaseOrderPage() {
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -346,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -642,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1246,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -1152,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1164,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -959,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2472,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2514,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -976,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2461,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2503,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function SalesOrderPage() {
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -358,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -829,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1533,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1542,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1707,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,13 +162,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -1159,7 +1158,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1171,7 +1170,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubcontractorItemPage() {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
@@ -158,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -518,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1184,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1228,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -414,7 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -501,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -833,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1134,7 +1135,7 @@ export default function BomManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 버전 ID 가져오기
|
||||
// 현재 버전 ID 가져오기 (initialize-version 후 최신 값)
|
||||
let versionId: string | null = null;
|
||||
if (bomId) {
|
||||
try {
|
||||
@@ -1205,6 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1213,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1228,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -2258,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2409,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function PurchaseOrderPage() {
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -346,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -642,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1246,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -1152,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1164,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -959,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2472,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2514,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -976,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2461,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2503,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function SalesOrderPage() {
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -358,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -829,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1533,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1542,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1707,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,13 +162,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -1159,7 +1158,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1171,7 +1170,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubcontractorItemPage() {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
@@ -158,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -518,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1184,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1228,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -414,7 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -501,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -833,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1134,7 +1135,7 @@ export default function BomManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 버전 ID 가져오기
|
||||
// 현재 버전 ID 가져오기 (initialize-version 후 최신 값)
|
||||
let versionId: string | null = null;
|
||||
if (bomId) {
|
||||
try {
|
||||
@@ -1205,6 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1213,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1228,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -2258,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2409,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function PurchaseOrderPage() {
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -346,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -642,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1246,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -1152,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1164,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -959,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2472,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2514,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -976,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2461,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2503,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod
|
||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
|
||||
const DETAIL_TABLE = "sales_order_detail";
|
||||
const MASTER_TABLE = "sales_order_mng";
|
||||
@@ -175,6 +176,10 @@ export default function SalesOrderPage() {
|
||||
const [detailRows, setDetailRows] = useState<any[]>([]);
|
||||
const [allowPriceEdit, setAllowPriceEdit] = useState(true);
|
||||
|
||||
// 수주번호 자동 채번
|
||||
const [orderNoRuleId, setOrderNoRuleId] = useState<string | null>(null);
|
||||
const [orderNoPreview, setOrderNoPreview] = useState<string | null>(null);
|
||||
|
||||
// 품목 선택 모달
|
||||
const [itemSelectOpen, setItemSelectOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
@@ -196,6 +201,7 @@ export default function SalesOrderPage() {
|
||||
|
||||
// 카테고리 옵션
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
|
||||
// 체크된 행 (다중선택)
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
@@ -213,82 +219,89 @@ export default function SalesOrderPage() {
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
const catColumns = ["sell_mode", "input_mode", "price_mode", "incoterms", "payment_term"];
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const LABEL_REPLACE: Record<string, string> = {
|
||||
"공급업체 우선": "거래처 우선",
|
||||
"공급업체우선": "거래처 우선",
|
||||
};
|
||||
const dedup = (items: { code: string; label: string }[]) => {
|
||||
const seen = new Set<string>();
|
||||
return items
|
||||
.map((item) => ({ ...item, label: LABEL_REPLACE[item.label] || item.label }))
|
||||
.filter((item) => {
|
||||
const key = item.label.replace(/\s/g, "");
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
try {
|
||||
const catColumns = ["sell_mode", "input_mode", "price_mode", "incoterms", "payment_term"];
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const LABEL_REPLACE: Record<string, string> = {
|
||||
"공급업체 우선": "거래처 우선",
|
||||
"공급업체우선": "거래처 우선",
|
||||
};
|
||||
const dedup = (items: { code: string; label: string }[]) => {
|
||||
const seen = new Set<string>();
|
||||
return items
|
||||
.map((item) => ({ ...item, label: LABEL_REPLACE[item.label] || item.label }))
|
||||
.filter((item) => {
|
||||
const key = item.label.replace(/\s/g, "");
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
await Promise.all(
|
||||
catColumns.map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${MASTER_TABLE}/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[col] = dedup(flatten(res.data.data));
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
// 거래처 목록
|
||||
try {
|
||||
const custRes = await apiClient.post(`/table-management/tables/customer_mng/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
};
|
||||
await Promise.all(
|
||||
catColumns.map(async (col) => {
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
optMap["manager_id"] = users.map((u: any) => ({
|
||||
code: u.user_id || u.id,
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${MASTER_TABLE}/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[col] = dedup(flatten(res.data.data));
|
||||
optMap[`item_${col}`] = flatten(res.data.data);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
// 거래처 목록
|
||||
try {
|
||||
const custRes = await apiClient.post(`/table-management/tables/customer_mng/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
optMap["manager_id"] = users.map((u: any) => ({
|
||||
code: u.user_id || u.id,
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[`item_${col}`] = flatten(res.data.data);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
// division 기본값
|
||||
const divs = optMap["item_division"] || [];
|
||||
const salesDiv = divs.find((o: any) => o.label === "영업관리")
|
||||
|| divs.find((o: any) => o.label === "제품")
|
||||
|| divs.find((o: any) => o.label === "판매품");
|
||||
if (salesDiv) setItemSearchDivision(salesDiv.code);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
// division 기본값
|
||||
const divs = optMap["item_division"] || [];
|
||||
const salesDiv = divs.find((o: any) => o.label === "영업관리")
|
||||
|| divs.find((o: any) => o.label === "제품")
|
||||
|| divs.find((o: any) => o.label === "판매품");
|
||||
if (salesDiv) setItemSearchDivision(salesDiv.code);
|
||||
};
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
// 데이터 조회
|
||||
const fetchOrders = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const filters = searchFilters.map(f => ({
|
||||
@@ -345,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -364,7 +377,7 @@ export default function SalesOrderPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions]);
|
||||
}, [searchFilters, categoryOptions, isCategoriesLoaded]);
|
||||
|
||||
useEffect(() => { fetchOrders(); }, [fetchOrders]);
|
||||
|
||||
@@ -513,7 +526,7 @@ export default function SalesOrderPage() {
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = () => {
|
||||
const openRegisterModal = async () => {
|
||||
const defaultSellMode = categoryOptions["sell_mode"]?.[0]?.code || "";
|
||||
const defaultInputMode = categoryOptions["input_mode"]?.[0]?.code || "";
|
||||
const defaultPriceMode = categoryOptions["price_mode"]?.[0]?.code || "";
|
||||
@@ -524,7 +537,23 @@ export default function SalesOrderPage() {
|
||||
setDetailRows([]);
|
||||
setDeliveryOptions([]);
|
||||
setIsEditMode(false);
|
||||
setOrderNoRuleId(null);
|
||||
setOrderNoPreview(null);
|
||||
setIsModalOpen(true);
|
||||
|
||||
// 수주번호 자동 채번 조회
|
||||
try {
|
||||
const ruleRes = await apiClient.get("/numbering-rules/by-column/sales_order_mng/order_no");
|
||||
if (ruleRes.data?.success && ruleRes.data?.data?.ruleId) {
|
||||
const ruleId = ruleRes.data.data.ruleId;
|
||||
setOrderNoRuleId(ruleId);
|
||||
const previewRes = await previewNumberingCode(ruleId);
|
||||
if (previewRes.success && previewRes.data?.generatedCode) {
|
||||
setOrderNoPreview(previewRes.data.generatedCode);
|
||||
setMasterForm((prev) => ({ ...prev, order_no: previewRes.data.generatedCode }));
|
||||
}
|
||||
}
|
||||
} catch { /* 채번 규칙 없으면 수동 입력 */ }
|
||||
};
|
||||
|
||||
// 수정 모달 열기
|
||||
@@ -603,6 +632,22 @@ export default function SalesOrderPage() {
|
||||
|
||||
// 저장 (마스터 + 디테일)
|
||||
const handleSave = async () => {
|
||||
// 채번 규칙이 있으면 allocate, 없으면 수동 입력 필수
|
||||
if (!isEditMode && orderNoRuleId) {
|
||||
try {
|
||||
const allocRes = await allocateNumberingCode(orderNoRuleId);
|
||||
if (allocRes.success && allocRes.data?.generatedCode) {
|
||||
setMasterForm((prev) => ({ ...prev, order_no: allocRes.data.generatedCode }));
|
||||
masterForm.order_no = allocRes.data.generatedCode;
|
||||
} else {
|
||||
toast.error("수주번호 채번에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
toast.error("수주번호 채번에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!masterForm.order_no && !isEditMode) {
|
||||
toast.error("수주번호는 필수입니다.");
|
||||
return;
|
||||
@@ -784,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1216,8 +1261,10 @@ export default function SalesOrderPage() {
|
||||
</Label>
|
||||
<Input
|
||||
value={masterForm.order_no || ""}
|
||||
onChange={(e) => setMasterForm((p) => ({ ...p, order_no: e.target.value }))}
|
||||
placeholder="수주번호" className="h-9" disabled={isEditMode}
|
||||
onChange={(e) => !orderNoRuleId && setMasterForm((p) => ({ ...p, order_no: e.target.value }))}
|
||||
readOnly={!!orderNoRuleId || isEditMode}
|
||||
placeholder={orderNoRuleId ? "자동 채번" : "수주번호"}
|
||||
className={cn("h-9", (orderNoRuleId || isEditMode) && "bg-muted cursor-not-allowed")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -1486,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1495,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1660,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,13 +162,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -1159,7 +1158,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1171,7 +1170,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -67,7 +67,6 @@ export default function SubcontractorItemPage() {
|
||||
|
||||
// 카테고리
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
|
||||
// 외주업체 추가 모달
|
||||
const [subSelectOpen, setSubSelectOpen] = useState(false);
|
||||
@@ -90,33 +89,27 @@ export default function SubcontractorItemPage() {
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
// 외주업체 거래유형 (subcontractor_mng.division)
|
||||
return result;
|
||||
};
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUBCONTRACTOR_TABLE}/division/values`);
|
||||
if (res.data?.success) optMap["subcontractor_division"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
// 외주업체 거래유형 (subcontractor_mng.division)
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUBCONTRACTOR_TABLE}/division/values`);
|
||||
if (res.data?.success) optMap["subcontractor_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
@@ -150,7 +143,6 @@ export default function SubcontractorItemPage() {
|
||||
)?.code;
|
||||
|
||||
const fetchItems = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -166,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
@@ -182,7 +176,7 @@ export default function SubcontractorItemPage() {
|
||||
} finally {
|
||||
setItemLoading(false);
|
||||
}
|
||||
}, [searchKeyword, categoryOptions, outsourcingDivisionCode, isCategoriesLoaded]);
|
||||
}, [searchKeyword, categoryOptions, outsourcingDivisionCode]);
|
||||
|
||||
useEffect(() => { fetchItems(); }, [fetchItems]);
|
||||
|
||||
|
||||
@@ -122,49 +122,41 @@ export default function SubcontractorManagementPage() {
|
||||
|
||||
// 카테고리
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUBCONTRACTOR_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
// item_info의 division/unit/material 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
return result;
|
||||
};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUBCONTRACTOR_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
@@ -197,7 +189,6 @@ export default function SubcontractorManagementPage() {
|
||||
|
||||
// 외주업체 목록 조회
|
||||
const fetchSubcontractors = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setSubcontractorLoading(true);
|
||||
try {
|
||||
const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value }));
|
||||
@@ -224,7 +215,7 @@ export default function SubcontractorManagementPage() {
|
||||
} finally {
|
||||
setSubcontractorLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions, isCategoriesLoaded]);
|
||||
}, [searchFilters, categoryOptions]);
|
||||
|
||||
useEffect(() => { fetchSubcontractors(); }, [fetchSubcontractors]);
|
||||
|
||||
@@ -431,7 +422,7 @@ export default function SubcontractorManagementPage() {
|
||||
const outsourcingCode = categoryOptions["item_division"]?.find((o) => o.label === "외주관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (!outsourcingCode) return true; // 카테고리 미로드 시 전체 표시
|
||||
if (!outsourcingCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(outsourcingCode);
|
||||
}));
|
||||
@@ -527,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1193,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1237,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -309,7 +309,6 @@ export default function BomManagementPage() {
|
||||
|
||||
// 카테고리 옵션
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
// 사용자 맵 (userId → userName)
|
||||
const [userMap, setUserMap] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -415,8 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// item_info의 division, unit 카테고리
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -434,11 +433,7 @@ export default function BomManagementPage() {
|
||||
}
|
||||
|
||||
setCategoryOptions(results);
|
||||
} catch {
|
||||
// skip
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
loadCategories();
|
||||
// 사용자 목록 로드
|
||||
@@ -455,7 +450,6 @@ export default function BomManagementPage() {
|
||||
|
||||
// ─── BOM 상세 로드 ────────────────────────────
|
||||
const fetchBomDetail = useCallback(async (bomId: string) => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
// 헤더 조회
|
||||
@@ -508,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -541,7 +535,7 @@ export default function BomManagementPage() {
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, [categoryOptions, isCategoriesLoaded]);
|
||||
}, [categoryOptions]);
|
||||
|
||||
// 버전 목록 로드
|
||||
const fetchVersions = useCallback(async (bomId: string) => {
|
||||
@@ -840,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1212,6 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1220,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1235,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -1535,12 +1530,51 @@ export default function BomManagementPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 편집 버튼 */}
|
||||
<div className="flex items-center justify-end px-4 py-2 border-b bg-muted/50 shrink-0">
|
||||
<Button size="sm" variant="ghost" onClick={openEditModal}>
|
||||
<FileText className="w-3.5 h-3.5 mr-1" />
|
||||
편집
|
||||
</Button>
|
||||
{/* 상세 카드 */}
|
||||
<div className="border-b shrink-0">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b bg-muted/50">
|
||||
<h3 className="text-[13px] font-bold text-foreground">BOM 상세정보</h3>
|
||||
<Button size="sm" variant="ghost" onClick={openEditModal}>
|
||||
<FileText className="w-3.5 h-3.5 mr-1" />
|
||||
편집
|
||||
</Button>
|
||||
</div>
|
||||
{detailLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : bomHeader ? (
|
||||
<div className="grid grid-cols-2 text-sm">
|
||||
<div className="flex flex-col gap-1 p-3 border-b border-r">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">품목코드</span>
|
||||
<span className="font-mono text-xs">{bomHeader.item_code || bomHeader.item_number || "-"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-3 border-b">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">품명</span>
|
||||
<span className="text-xs">{bomHeader.item_name || "-"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-3 border-b border-r">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">BOM 유형</span>
|
||||
<span className="text-xs">{BOM_TYPE_OPTIONS.find((o) => o.code === bomHeader.bom_type)?.label || bomHeader.bom_type || "-"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-3 border-b">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">버전</span>
|
||||
<span className="text-xs">{bomHeader.version || "-"}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-3 border-b border-r">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">기준수량</span>
|
||||
<span className="text-xs">{bomHeader.base_qty || "1"} {bomHeader.unit || ""}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-3 border-b">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">상태</span>
|
||||
{renderStatusBadge(bomHeader.status)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-3 col-span-2">
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">메모</span>
|
||||
<span className="text-xs text-muted-foreground">{bomHeader.remark || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* 하단 탭: 트리뷰 / 버전 / 이력 */}
|
||||
@@ -2226,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2377,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -161,7 +161,6 @@ export default function PurchaseOrderPage() {
|
||||
|
||||
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 테이블 설정
|
||||
@@ -207,75 +206,69 @@ export default function PurchaseOrderPage() {
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const catColumns = ["input_mode", "price_mode"];
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const dedup = (items: { code: string; label: string }[]) => {
|
||||
const seen = new Set<string>();
|
||||
return items.filter((item) => {
|
||||
const key = item.label.replace(/\s/g, "");
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
await Promise.all(
|
||||
catColumns.map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${MASTER_TABLE}/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[col] = dedup(flatten(res.data.data));
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
try {
|
||||
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
|
||||
page: 1, size: 5000, autoFilter: true,
|
||||
});
|
||||
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
|
||||
optMap["supplier_code"] = supps.map((s: any) => ({
|
||||
code: s.supplier_code,
|
||||
label: `${s.supplier_name} (${s.supplier_code})`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 5000, autoFilter: true,
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
optMap["manager"] = users.map((u: any) => ({
|
||||
code: u.user_id || u.id,
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
const catColumns = ["input_mode", "price_mode"];
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const dedup = (items: { code: string; label: string }[]) => {
|
||||
const seen = new Set<string>();
|
||||
return items.filter((item) => {
|
||||
const key = item.label.replace(/\s/g, "");
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
await Promise.all(
|
||||
catColumns.map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/${MASTER_TABLE}/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[`item_${col}`] = flatten(res.data.data);
|
||||
optMap[col] = dedup(flatten(res.data.data));
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
const divs = optMap["item_division"] || [];
|
||||
const purchaseDiv = divs.find((o) => o.label === "구매관리")
|
||||
|| divs.find((o) => o.label === "원자재")
|
||||
|| divs.find((o) => o.label === "부자재");
|
||||
if (purchaseDiv) setItemSearchDivision(purchaseDiv.code);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
})
|
||||
);
|
||||
try {
|
||||
const suppRes = await apiClient.post(`/table-management/tables/supplier_mng/data`, {
|
||||
page: 1, size: 5000, autoFilter: true,
|
||||
});
|
||||
const supps = suppRes.data?.data?.data || suppRes.data?.data?.rows || [];
|
||||
optMap["supplier_code"] = supps.map((s: any) => ({
|
||||
code: s.supplier_code,
|
||||
label: `${s.supplier_name} (${s.supplier_code})`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 5000, autoFilter: true,
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
optMap["manager"] = users.map((u: any) => ({
|
||||
code: u.user_id || u.id,
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[`item_${col}`] = flatten(res.data.data);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
const divs = optMap["item_division"] || [];
|
||||
const purchaseDiv = divs.find((o) => o.label === "구매관리")
|
||||
|| divs.find((o) => o.label === "원자재")
|
||||
|| divs.find((o) => o.label === "부자재");
|
||||
if (purchaseDiv) setItemSearchDivision(purchaseDiv.code);
|
||||
};
|
||||
loadCategories();
|
||||
}, []);
|
||||
@@ -285,7 +278,6 @@ export default function PurchaseOrderPage() {
|
||||
|
||||
// 데이터 조회
|
||||
const fetchOrders = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
// searchFilters를 detail / master로 분리
|
||||
@@ -354,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -374,7 +366,7 @@ export default function PurchaseOrderPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions, isCategoriesLoaded]);
|
||||
}, [searchFilters, categoryOptions]);
|
||||
|
||||
useEffect(() => { fetchOrders(); }, [fetchOrders]);
|
||||
|
||||
@@ -650,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1254,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -236,7 +235,6 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 카테고리
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string; isDefault?: boolean }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
const [priceCategoryOptions, setPriceCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
|
||||
// 공급업체 추가 모달
|
||||
@@ -270,45 +268,39 @@ export default function PurchaseItemPage() {
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const optMap: Record<string, { code: string; label: string; isDefault?: boolean }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string; isDefault?: boolean }[] => {
|
||||
const result: { code: string; label: string; isDefault?: boolean }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel, isDefault: v.isDefault });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
await Promise.all(
|
||||
CATEGORY_COLUMNS.map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) optMap[col] = flatten(res.data.data);
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
// 공급업체 거래유형 (supplier_mng.division)
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUPPLIER_TABLE}/division/values`);
|
||||
if (res.data?.success) optMap["supplier_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
// 단가 카테고리
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/supplier_item_prices/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
const optMap: Record<string, { code: string; label: string; isDefault?: boolean }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string; isDefault?: boolean }[] => {
|
||||
const result: { code: string; label: string; isDefault?: boolean }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel, isDefault: v.isDefault });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
return result;
|
||||
};
|
||||
await Promise.all(
|
||||
CATEGORY_COLUMNS.map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) optMap[col] = flatten(res.data.data);
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
// 공급업체 거래유형 (supplier_mng.division)
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUPPLIER_TABLE}/division/values`);
|
||||
if (res.data?.success) optMap["supplier_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
// 단가 카테고리
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/supplier_item_prices/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
@@ -320,7 +312,6 @@ export default function PurchaseItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
@@ -358,7 +349,7 @@ export default function PurchaseItemPage() {
|
||||
} finally {
|
||||
setItemLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions, isCategoriesLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [searchFilters, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => { fetchItems(); }, [fetchItems]);
|
||||
|
||||
@@ -1160,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1172,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -174,7 +174,6 @@ export default function SupplierManagementPage() {
|
||||
|
||||
// 카테고리
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
const [employeeOptions, setEmployeeOptions] = useState<{ user_id: string; user_name: string; position_name?: string }[]>([]);
|
||||
|
||||
|
||||
@@ -189,41 +188,35 @@ export default function SupplierManagementPage() {
|
||||
return result;
|
||||
};
|
||||
const load = async () => {
|
||||
try {
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${SUPPLIER_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
|
||||
// 세금유형 카테고리
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const taxRes = await apiClient.get(`/table-categories/supplier_tax_type/tax_type_name/values`);
|
||||
if (taxRes.data?.success) setTaxTypeOptions(flatten(taxRes.data.data || []));
|
||||
const res = await apiClient.get(`/table-categories/${SUPPLIER_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
|
||||
// 세금유형 카테고리
|
||||
try {
|
||||
const taxRes = await apiClient.get(`/table-categories/supplier_tax_type/tax_type_name/values`);
|
||||
if (taxRes.data?.success) setTaxTypeOptions(flatten(taxRes.data.data || []));
|
||||
} catch { /* skip */ }
|
||||
};
|
||||
load();
|
||||
apiClient.post(`/table-management/tables/user_info/data`, { page: 1, size: 500, autoFilter: true })
|
||||
@@ -237,7 +230,6 @@ export default function SupplierManagementPage() {
|
||||
|
||||
// 공급업체 목록 조회
|
||||
const fetchSuppliers = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setSupplierLoading(true);
|
||||
try {
|
||||
const filters = searchFilters.map(f => ({
|
||||
@@ -284,7 +276,7 @@ export default function SupplierManagementPage() {
|
||||
} finally {
|
||||
setSupplierLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions, employeeOptions, mainContactMap, isCategoriesLoaded]);
|
||||
}, [searchFilters, categoryOptions, employeeOptions, mainContactMap]);
|
||||
|
||||
useEffect(() => { fetchSuppliers(); }, [fetchSuppliers]);
|
||||
|
||||
@@ -967,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2480,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2522,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -174,7 +174,6 @@ export default function CustomerManagementPage() {
|
||||
|
||||
// 카테고리
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
const [employeeOptions, setEmployeeOptions] = useState<{ user_id: string; user_name: string; position_name?: string }[]>([]);
|
||||
|
||||
|
||||
@@ -189,41 +188,35 @@ export default function CustomerManagementPage() {
|
||||
return result;
|
||||
};
|
||||
const load = async () => {
|
||||
try {
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${CUSTOMER_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
|
||||
// 세금유형 카테고리
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["division", "status"]) {
|
||||
try {
|
||||
const taxRes = await apiClient.get(`/table-categories/customer_tax_type/tax_type_name/values`);
|
||||
if (taxRes.data?.success) setTaxTypeOptions(flatten(taxRes.data.data || []));
|
||||
const res = await apiClient.get(`/table-categories/${CUSTOMER_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${PRICE_TABLE}/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
|
||||
// 세금유형 카테고리
|
||||
try {
|
||||
const taxRes = await apiClient.get(`/table-categories/customer_tax_type/tax_type_name/values`);
|
||||
if (taxRes.data?.success) setTaxTypeOptions(flatten(taxRes.data.data || []));
|
||||
} catch { /* skip */ }
|
||||
};
|
||||
load();
|
||||
apiClient.post(`/table-management/tables/user_info/data`, { page: 1, size: 500, autoFilter: true })
|
||||
@@ -237,7 +230,6 @@ export default function CustomerManagementPage() {
|
||||
|
||||
// 거래처 목록 조회
|
||||
const fetchCustomers = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setCustomerLoading(true);
|
||||
try {
|
||||
const filters = searchFilters.map(f => ({
|
||||
@@ -284,7 +276,7 @@ export default function CustomerManagementPage() {
|
||||
} finally {
|
||||
setCustomerLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions, employeeOptions, mainContactMap, isCategoriesLoaded]);
|
||||
}, [searchFilters, categoryOptions, employeeOptions, mainContactMap]);
|
||||
|
||||
useEffect(() => { fetchCustomers(); }, [fetchCustomers]);
|
||||
|
||||
@@ -984,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2469,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2511,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function SalesOrderPage() {
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -358,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -829,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1533,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1542,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1707,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,7 +162,7 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
@@ -225,7 +225,6 @@ export default function SalesItemPage() {
|
||||
|
||||
// 카테고리
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string; isDefault?: boolean }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
const [priceCategoryOptions, setPriceCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
|
||||
// 거래처 추가 모달
|
||||
@@ -271,43 +270,37 @@ export default function SalesItemPage() {
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const optMap: Record<string, { code: string; label: string; isDefault?: boolean }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string; isDefault?: boolean }[] => {
|
||||
const result: { code: string; label: string; isDefault?: boolean }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel, isDefault: v.isDefault });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
const optMap: Record<string, { code: string; label: string; isDefault?: boolean }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string; isDefault?: boolean }[] => {
|
||||
const result: { code: string; label: string; isDefault?: boolean }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel, isDefault: v.isDefault });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
// 거래처 거래유형 (customer_mng.division)
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/customer_mng/division/values`);
|
||||
if (res.data?.success) optMap["customer_division"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
// 단가 카테고리
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/customer_item_prices/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
// 거래처 거래유형 (customer_mng.division)
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/customer_mng/division/values`);
|
||||
if (res.data?.success) optMap["customer_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
// 단가 카테고리
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/customer_item_prices/${col}/values`);
|
||||
if (res.data?.success) priceOpts[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setPriceCategoryOptions(priceOpts);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
@@ -319,7 +312,6 @@ export default function SalesItemPage() {
|
||||
|
||||
// 좌측: 품목 조회
|
||||
const fetchItems = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setItemLoading(true);
|
||||
try {
|
||||
const filters: { columnName: string; operator: string; value: any }[] = [];
|
||||
@@ -358,7 +350,7 @@ export default function SalesItemPage() {
|
||||
} finally {
|
||||
setItemLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions, isCategoriesLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [searchFilters, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => { fetchItems(); }, [fetchItems]);
|
||||
|
||||
@@ -1167,7 +1159,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1179,7 +1171,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubcontractorItemPage() {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
@@ -158,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
|
||||
@@ -141,8 +141,7 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
// item_info의 division/unit/material 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -519,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1185,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1229,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -414,7 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -501,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -833,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1134,6 +1135,7 @@ export default function BomManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 버전 ID 가져오기 (initialize-version 후 최신 값)
|
||||
let versionId: string | null = null;
|
||||
if (bomId) {
|
||||
try {
|
||||
@@ -1204,6 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1212,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1227,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -2257,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2408,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function PurchaseOrderPage() {
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -346,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -642,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1246,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -1152,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1164,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -959,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2472,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2514,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -976,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2461,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2503,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function SalesOrderPage() {
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -358,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -829,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1533,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1542,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1707,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,13 +162,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -1159,7 +1158,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1171,7 +1170,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
@@ -164,8 +164,8 @@ export default function InventoryStatusPage() {
|
||||
}
|
||||
// item_info 단위 카테고리
|
||||
try {
|
||||
const res = await apiClient.get("/table-categories/item_info/unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
|
||||
const res = await apiClient.get("/table-categories/item_info/inventory_unit/values?filterCompanyCode=COMPANY_16");
|
||||
if (res.data?.success) optMap["item_inventory_unit"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
setCategoryOptions(optMap);
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export default function InventoryStatusPage() {
|
||||
const raw = stockRes.data?.data?.data || stockRes.data?.data?.rows || [];
|
||||
const items = itemRes.data?.data?.data || itemRes.data?.data?.rows || [];
|
||||
const warehouses = whRes.data?.data?.data || whRes.data?.data?.rows || [];
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.unit || "" }]));
|
||||
const itemMap = new Map(items.map((i: any) => [i.item_number || i.item_code, { name: i.item_name || "", unit: i.inventory_unit || "" }]));
|
||||
const whMap = new Map(warehouses.map((w: any) => [w.warehouse_code, w.warehouse_name || w.warehouse_code]));
|
||||
const resolve = (col: string, code: string) => {
|
||||
if (!code) return "";
|
||||
@@ -213,7 +213,7 @@ export default function InventoryStatusPage() {
|
||||
return {
|
||||
...r,
|
||||
item_name: itemInfo?.name || "",
|
||||
unit: resolve("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolve("item_inventory_unit", rawUnit) || rawUnit,
|
||||
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
|
||||
status: resolve("status", r.status),
|
||||
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function OutboundPage() {
|
||||
};
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all([
|
||||
...["material", "unit"].map(async (col) => {
|
||||
...["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_7`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -679,7 +679,7 @@ export default function OutboundPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
outbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1696,7 +1696,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ReceivingPage() {
|
||||
// 재질, 단위 카테고리
|
||||
const map: Record<string, Record<string, string>> = {};
|
||||
Promise.all(
|
||||
["material", "unit"].map(async (col) => {
|
||||
["material", "inventory_unit"].map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?filterCompanyCode=COMPANY_16`);
|
||||
const items = flatten(res.data?.data || []);
|
||||
@@ -711,7 +711,7 @@ export default function ReceivingPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.spec || "",
|
||||
material: item.material || "",
|
||||
unit: item.unit || "EA",
|
||||
unit: item.inventory_unit || "EA",
|
||||
inbound_qty: 0,
|
||||
unit_price: item.standard_price,
|
||||
total_amount: 0,
|
||||
@@ -1762,7 +1762,7 @@ function SourceItemTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
|
||||
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2">{resolveCat("inventory_unit", item.unit) || "-"}</TableCell>
|
||||
<TableCell className="p-2 text-right">
|
||||
{Number(item.standard_price).toLocaleString()}
|
||||
</TableCell>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubcontractorItemPage() {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
for (const col of ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`);
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
@@ -158,12 +158,14 @@ export default function SubcontractorItemPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const raw = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const CATS = ["material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"];
|
||||
const data = raw.map((r: any) => {
|
||||
const converted = { ...r };
|
||||
for (const col of CATS) {
|
||||
if (converted[col]) converted[col] = resolve(col, converted[col]);
|
||||
}
|
||||
// item_info의 inventory_unit을 단위 표시용 unit에 매핑
|
||||
converted.unit = converted.inventory_unit || converted.unit || "";
|
||||
return converted;
|
||||
});
|
||||
setItems(data);
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -518,7 +518,7 @@ export default function SubcontractorManagementPage() {
|
||||
// 우측 품목 편집 열기 — 해당 item_number의 모든 매핑+단가를 로드
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -1184,7 +1184,7 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1228,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -414,7 +414,8 @@ export default function BomManagementPage() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const itemCol of ["division", "unit"]) {
|
||||
// item_info의 division, inventory_unit 카테고리
|
||||
for (const itemCol of ["division", "inventory_unit"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${itemCol}/values`);
|
||||
if (res.data?.data?.length > 0) {
|
||||
@@ -501,7 +502,7 @@ export default function BomManagementPage() {
|
||||
item_number: item?.item_number || "",
|
||||
item_name: item?.item_name || "",
|
||||
item_type: divisionLabel,
|
||||
unit: d.unit || item?.unit || "",
|
||||
unit: d.unit || item?.inventory_unit || "",
|
||||
spec: item?.size || item?.spec || "",
|
||||
writer: d.writer || "",
|
||||
updated_date: d.updated_at || d.updated_date || "",
|
||||
@@ -833,7 +834,7 @@ export default function BomManagementPage() {
|
||||
item_type: item.division || "",
|
||||
level: String(newLevel),
|
||||
quantity: "1",
|
||||
unit: item.unit || "",
|
||||
unit: item.inventory_unit || "",
|
||||
process_type: "",
|
||||
loss_rate: "0",
|
||||
remark: "",
|
||||
@@ -1134,7 +1135,7 @@ export default function BomManagementPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 버전 ID 가져오기
|
||||
// 현재 버전 ID 가져오기 (initialize-version 후 최신 값)
|
||||
let versionId: string | null = null;
|
||||
if (bomId) {
|
||||
try {
|
||||
@@ -1205,6 +1206,7 @@ export default function BomManagementPage() {
|
||||
});
|
||||
rows = res2.data?.data?.data || res2.data?.data?.rows || [];
|
||||
}
|
||||
// 카테고리 코드 → 라벨 변환 (division + inventory_unit)
|
||||
const resolved = rows.map((r: any) => {
|
||||
const out = { ...r };
|
||||
if (out.division) {
|
||||
@@ -1213,8 +1215,8 @@ export default function BomManagementPage() {
|
||||
return categoryOptions["division"]?.find((o) => o.code === t)?.label || t;
|
||||
}).filter((v: string) => v && v !== "s").join(", ");
|
||||
}
|
||||
if (out.unit) {
|
||||
out.unit = categoryOptions["unit"]?.find((o) => o.code === out.unit)?.label || out.unit;
|
||||
if (out.inventory_unit) {
|
||||
out.inventory_unit = categoryOptions["inventory_unit"]?.find((o) => o.code === out.inventory_unit)?.label || out.inventory_unit;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -1228,11 +1230,11 @@ export default function BomManagementPage() {
|
||||
|
||||
const resolveUnit = (code: string) => {
|
||||
if (!code) return "";
|
||||
return categoryOptions["unit"]?.find((o) => o.code === code)?.label || code;
|
||||
return categoryOptions["inventory_unit"]?.find((o) => o.code === code)?.label || code;
|
||||
};
|
||||
|
||||
const selectItem = (item: any) => {
|
||||
const unitLabel = resolveUnit(item.unit);
|
||||
const unitLabel = resolveUnit(item.inventory_unit);
|
||||
if (itemSearchTarget === "master") {
|
||||
setMasterForm((prev) => ({
|
||||
...prev,
|
||||
@@ -2258,7 +2260,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -2409,7 +2411,7 @@ export default function BomManagementPage() {
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{item.item_number}</span>
|
||||
<span className="text-xs">{item.item_name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.unit || ""}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{item.inventory_unit || ""}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function PurchaseOrderPage() {
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["unit", "material", "division"]) {
|
||||
for (const col of ["inventory_unit", "material", "division"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
@@ -346,12 +346,12 @@ export default function PurchaseOrderPage() {
|
||||
.map((row: any) => {
|
||||
const item = itemMap[row.item_code];
|
||||
const master = masterMap[row.purchase_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
item_name: row.item_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
status: master?.status || "",
|
||||
supplier_name: master?.supplier_name || "",
|
||||
order_date: master?.order_date || "",
|
||||
@@ -642,7 +642,7 @@ export default function PurchaseOrderPage() {
|
||||
item_name: item.item_name,
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
order_qty: "",
|
||||
received_qty: "0",
|
||||
remain_qty: "0",
|
||||
@@ -1246,7 +1246,7 @@ export default function PurchaseOrderPage() {
|
||||
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -142,13 +142,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -175,7 +174,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가/구매단가" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
{ key: "status", label: "상태" },
|
||||
@@ -1152,7 +1151,7 @@ export default function PurchaseItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 구매단가: i.standard_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "구매품목관리.xlsx", "구매품목");
|
||||
@@ -1164,7 +1163,7 @@ export default function PurchaseItemPage() {
|
||||
item_number: { width: "w-[110px]" },
|
||||
item_name: { minWidth: "min-w-[130px]" },
|
||||
size: { width: "w-[80px]" },
|
||||
unit: { width: "w-[60px]" },
|
||||
inventory_unit: { width: "w-[60px]" },
|
||||
standard_price: { width: "w-[90px]", align: "right", formatNumber: true },
|
||||
currency_code: { width: "w-[50px]" },
|
||||
status: { width: "w-[60px]" },
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -959,7 +959,7 @@ export default function SupplierManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2472,7 +2472,7 @@ export default function SupplierManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2514,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import {
|
||||
Plus, Trash2, Save, Loader2, Pencil, Inbox, Settings2, Search, ClipboardList,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +87,11 @@ export default function ItemInspectionInfoPage() {
|
||||
const [itemModalOpen, setItemModalOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [filteredItems, setFilteredItems] = useState<typeof itemOptions>([]);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemTotal, setItemTotal] = useState(0);
|
||||
const itemPageSize = 20;
|
||||
const itemTotalPages = Math.max(1, Math.ceil(itemTotal / itemPageSize));
|
||||
|
||||
/* ═══════════════════ FK 옵션 로드 ═══════════════════ */
|
||||
useEffect(() => {
|
||||
@@ -101,7 +107,7 @@ export default function ItemInspectionInfoPage() {
|
||||
code: r.item_number || r.item_code || "",
|
||||
name: r.item_name || "",
|
||||
item_type: r.type || r.item_type || "",
|
||||
unit: r.unit || "",
|
||||
unit: r.inventory_unit || "",
|
||||
})));
|
||||
|
||||
const insps = inspRes.data?.data?.data || inspRes.data?.data?.rows || [];
|
||||
@@ -142,12 +148,27 @@ export default function ItemInspectionInfoPage() {
|
||||
}, []);
|
||||
|
||||
/* ═══════════════════ 품목 선택 모달 ═══════════════════ */
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setFilteredItems(itemOptions); setItemModalOpen(true); };
|
||||
const handleItemSearch = () => {
|
||||
const kw = itemSearchKeyword.trim().toLowerCase();
|
||||
if (!kw) { setFilteredItems(itemOptions); return; }
|
||||
setFilteredItems(itemOptions.filter(o => o.code.toLowerCase().includes(kw) || o.name.toLowerCase().includes(kw)));
|
||||
const searchItemServer = async (page?: number) => {
|
||||
const p = page ?? itemPage;
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword.trim()) {
|
||||
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword.trim() });
|
||||
}
|
||||
const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, {
|
||||
page: p, size: itemPageSize,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
autoFilter: true,
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
const rows = resData?.data || resData?.rows || [];
|
||||
setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: r.type || "", unit: r.inventory_unit || "" })));
|
||||
setItemTotal(resData?.total || resData?.totalCount || rows.length);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
const openItemModal = () => { setItemSearchKeyword(""); setItemPage(1); setItemModalOpen(true); searchItemServer(1); };
|
||||
const handleItemSearch = () => { setItemPage(1); searchItemServer(1); };
|
||||
const selectItem = (item: typeof itemOptions[0]) => {
|
||||
setForm(p => ({ ...p, item_code: item.code, item_name: item.name }));
|
||||
setItemModalOpen(false);
|
||||
@@ -567,33 +588,35 @@ export default function ItemInspectionInfoPage() {
|
||||
|
||||
{/* ═══════════════════ 등록/수정 모달 ═══════════════════ */}
|
||||
<Dialog open={modalOpen} onOpenChange={(open) => { if (!open) setItemModalOpen(false); setModalOpen(open); }}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] overflow-y-auto", itemModalOpen ? "sm:max-w-xl" : "sm:max-w-4xl")}>
|
||||
<DialogContent className={cn("max-w-[95vw] max-h-[85vh] flex flex-col overflow-hidden", itemModalOpen ? "sm:max-w-3xl" : "sm:max-w-4xl")}>
|
||||
{itemModalOpen ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>품목코드 또는 품목명으로 검색</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Input className="h-9 flex-1" placeholder="품목코드 또는 품목명" value={itemSearchKeyword} onChange={(e) => setItemSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleItemSearch(); }} />
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch}><Search className="w-4 h-4 mr-1" />검색</Button>
|
||||
<Button size="sm" className="h-9" onClick={handleItemSearch} disabled={itemSearchLoading}>
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" />검색</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-auto max-h-[50vh]">
|
||||
<div className="flex-1 border rounded-lg overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="text-[11px] font-bold">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[140px]">품목코드</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목명</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold">단위</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[100px]">품목유형</TableHead>
|
||||
<TableHead className="text-[11px] font-bold w-[80px]">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">검색 결과가 없어요</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">{itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"}</TableCell></TableRow>
|
||||
) : filteredItems.map((item) => (
|
||||
<TableRow key={item.code} className="cursor-pointer hover:bg-primary/5" onClick={() => selectItem(item)}>
|
||||
<TableCell className="text-sm">{item.code}</TableCell>
|
||||
<TableCell className="text-sm font-mono">{item.code}</TableCell>
|
||||
<TableCell className="text-sm">{item.name}</TableCell>
|
||||
<TableCell className="text-sm">{item.item_type}</TableCell>
|
||||
<TableCell className="text-sm">{item.unit}</TableCell>
|
||||
@@ -602,7 +625,31 @@ export default function ItemInspectionInfoPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
{/* 페이지네이션 (EDataTable 스타일) */}
|
||||
<div className="shrink-0 flex items-center justify-between border-t px-2 py-2 text-xs text-muted-foreground">
|
||||
<span>전체 <span className="font-medium text-foreground">{itemTotal.toLocaleString()}</span>건</span>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => { setItemPage(1); searchItemServer(1); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsLeft className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { const p = itemPage - 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage <= 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronLeft className="h-3.5 w-3.5" /></button>
|
||||
{Array.from({ length: Math.min(5, itemTotalPages) }, (_, i) => {
|
||||
const start = Math.max(1, Math.min(itemPage - 2, itemTotalPages - 4));
|
||||
const p = start + i;
|
||||
if (p > itemTotalPages) return null;
|
||||
return (
|
||||
<button key={p} onClick={() => { setItemPage(p); searchItemServer(p); }}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
p === itemPage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>{p}</button>
|
||||
);
|
||||
})}
|
||||
<button onClick={() => { const p = itemPage + 1; setItemPage(p); searchItemServer(p); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronRight className="h-3.5 w-3.5" /></button>
|
||||
<button onClick={() => { setItemPage(itemTotalPages); searchItemServer(itemTotalPages); }} disabled={itemPage >= itemTotalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed"><ChevronsRight className="h-3.5 w-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="shrink-0"><Button variant="outline" onClick={() => setItemModalOpen(false)}>취소</Button></DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
for (const col of ["division", "inventory_unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
@@ -976,7 +976,7 @@ export default function CustomerManagementPage() {
|
||||
// 품목 편집 열기
|
||||
const openEditItem = async (row: any) => {
|
||||
const itemKey = row.item_number || row.item_id;
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", unit: "" };
|
||||
let itemInfo: any = { item_number: itemKey, item_name: row.item_name || "", size: "", inventory_unit: "" };
|
||||
try {
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 1,
|
||||
@@ -2461,7 +2461,7 @@ export default function CustomerManagementPage() {
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2503,7 +2503,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod
|
||||
import { useTableSettings } from "@/hooks/useTableSettings";
|
||||
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
||||
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
|
||||
const DETAIL_TABLE = "sales_order_detail";
|
||||
const MASTER_TABLE = "sales_order_mng";
|
||||
@@ -175,6 +176,10 @@ export default function SalesOrderPage() {
|
||||
const [detailRows, setDetailRows] = useState<any[]>([]);
|
||||
const [allowPriceEdit, setAllowPriceEdit] = useState(true);
|
||||
|
||||
// 수주번호 자동 채번
|
||||
const [orderNoRuleId, setOrderNoRuleId] = useState<string | null>(null);
|
||||
const [orderNoPreview, setOrderNoPreview] = useState<string | null>(null);
|
||||
|
||||
// 품목 선택 모달
|
||||
const [itemSelectOpen, setItemSelectOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
@@ -196,6 +201,7 @@ export default function SalesOrderPage() {
|
||||
|
||||
// 카테고리 옵션
|
||||
const [categoryOptions, setCategoryOptions] = useState<Record<string, { code: string; label: string }[]>>({});
|
||||
const [isCategoriesLoaded, setIsCategoriesLoaded] = useState(false);
|
||||
|
||||
// 체크된 행 (다중선택)
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
@@ -213,82 +219,89 @@ export default function SalesOrderPage() {
|
||||
// 카테고리 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
const catColumns = ["sell_mode", "input_mode", "price_mode", "incoterms", "payment_term"];
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const LABEL_REPLACE: Record<string, string> = {
|
||||
"공급업체 우선": "거래처 우선",
|
||||
"공급업체우선": "거래처 우선",
|
||||
};
|
||||
const dedup = (items: { code: string; label: string }[]) => {
|
||||
const seen = new Set<string>();
|
||||
return items
|
||||
.map((item) => ({ ...item, label: LABEL_REPLACE[item.label] || item.label }))
|
||||
.filter((item) => {
|
||||
const key = item.label.replace(/\s/g, "");
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
try {
|
||||
const catColumns = ["sell_mode", "input_mode", "price_mode", "incoterms", "payment_term"];
|
||||
const optMap: Record<string, { code: string; label: string }[]> = {};
|
||||
const flatten = (vals: any[]): { code: string; label: string }[] => {
|
||||
const result: { code: string; label: string }[] = [];
|
||||
for (const v of vals) {
|
||||
result.push({ code: v.valueCode, label: v.valueLabel });
|
||||
if (v.children?.length) result.push(...flatten(v.children));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const LABEL_REPLACE: Record<string, string> = {
|
||||
"공급업체 우선": "거래처 우선",
|
||||
"공급업체우선": "거래처 우선",
|
||||
};
|
||||
const dedup = (items: { code: string; label: string }[]) => {
|
||||
const seen = new Set<string>();
|
||||
return items
|
||||
.map((item) => ({ ...item, label: LABEL_REPLACE[item.label] || item.label }))
|
||||
.filter((item) => {
|
||||
const key = item.label.replace(/\s/g, "");
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
await Promise.all(
|
||||
catColumns.map(async (col) => {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${MASTER_TABLE}/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[col] = dedup(flatten(res.data.data));
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
// 거래처 목록
|
||||
try {
|
||||
const custRes = await apiClient.post(`/table-management/tables/customer_mng/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
};
|
||||
await Promise.all(
|
||||
catColumns.map(async (col) => {
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
optMap["manager_id"] = users.map((u: any) => ({
|
||||
code: u.user_id || u.id,
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["inventory_unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${MASTER_TABLE}/${col}/values`);
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[col] = dedup(flatten(res.data.data));
|
||||
optMap[`item_${col}`] = flatten(res.data.data);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
})
|
||||
);
|
||||
// 거래처 목록
|
||||
try {
|
||||
const custRes = await apiClient.post(`/table-management/tables/customer_mng/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
const userRes = await apiClient.post(`/table-management/tables/user_info/data`, {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const users = userRes.data?.data?.data || userRes.data?.data?.rows || [];
|
||||
optMap["manager_id"] = users.map((u: any) => ({
|
||||
code: u.user_id || u.id,
|
||||
label: `${u.user_name || u.name || u.user_id}${u.position_name ? ` (${u.position_name})` : ""}`,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// item_info 카테고리
|
||||
for (const col of ["unit", "material", "division", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success && res.data.data?.length > 0) {
|
||||
optMap[`item_${col}`] = flatten(res.data.data);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
// division 기본값
|
||||
const divs = optMap["item_division"] || [];
|
||||
const salesDiv = divs.find((o: any) => o.label === "영업관리")
|
||||
|| divs.find((o: any) => o.label === "제품")
|
||||
|| divs.find((o: any) => o.label === "판매품");
|
||||
if (salesDiv) setItemSearchDivision(salesDiv.code);
|
||||
} catch (err) {
|
||||
console.error("카테고리 로드 실패:", err);
|
||||
} finally {
|
||||
setIsCategoriesLoaded(true);
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
// division 기본값
|
||||
const divs = optMap["item_division"] || [];
|
||||
const salesDiv = divs.find((o: any) => o.label === "영업관리")
|
||||
|| divs.find((o: any) => o.label === "제품")
|
||||
|| divs.find((o: any) => o.label === "판매품");
|
||||
if (salesDiv) setItemSearchDivision(salesDiv.code);
|
||||
};
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
// 데이터 조회
|
||||
const fetchOrders = useCallback(async () => {
|
||||
if (!isCategoriesLoaded) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const filters = searchFilters.map(f => ({
|
||||
@@ -345,13 +358,13 @@ export default function SalesOrderPage() {
|
||||
const data = rows.map((row: any) => {
|
||||
const item = itemMap[row.part_code];
|
||||
const master = masterMap[row.order_no];
|
||||
const rawUnit = row.unit || item?.unit || "";
|
||||
const rawUnit = row.unit || item?.inventory_unit || "";
|
||||
return {
|
||||
...row,
|
||||
part_name: row.part_name || item?.item_name || "",
|
||||
spec: row.spec || item?.size || "",
|
||||
material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""),
|
||||
unit: resolveLabel("item_unit", rawUnit) || rawUnit,
|
||||
unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit,
|
||||
memo: row.memo || master?.memo || "",
|
||||
_master: master || {},
|
||||
};
|
||||
@@ -364,7 +377,7 @@ export default function SalesOrderPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchFilters, categoryOptions]);
|
||||
}, [searchFilters, categoryOptions, isCategoriesLoaded]);
|
||||
|
||||
useEffect(() => { fetchOrders(); }, [fetchOrders]);
|
||||
|
||||
@@ -513,7 +526,7 @@ export default function SalesOrderPage() {
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = () => {
|
||||
const openRegisterModal = async () => {
|
||||
const defaultSellMode = categoryOptions["sell_mode"]?.[0]?.code || "";
|
||||
const defaultInputMode = categoryOptions["input_mode"]?.[0]?.code || "";
|
||||
const defaultPriceMode = categoryOptions["price_mode"]?.[0]?.code || "";
|
||||
@@ -524,7 +537,23 @@ export default function SalesOrderPage() {
|
||||
setDetailRows([]);
|
||||
setDeliveryOptions([]);
|
||||
setIsEditMode(false);
|
||||
setOrderNoRuleId(null);
|
||||
setOrderNoPreview(null);
|
||||
setIsModalOpen(true);
|
||||
|
||||
// 수주번호 자동 채번 조회
|
||||
try {
|
||||
const ruleRes = await apiClient.get("/numbering-rules/by-column/sales_order_mng/order_no");
|
||||
if (ruleRes.data?.success && ruleRes.data?.data?.ruleId) {
|
||||
const ruleId = ruleRes.data.data.ruleId;
|
||||
setOrderNoRuleId(ruleId);
|
||||
const previewRes = await previewNumberingCode(ruleId);
|
||||
if (previewRes.success && previewRes.data?.generatedCode) {
|
||||
setOrderNoPreview(previewRes.data.generatedCode);
|
||||
setMasterForm((prev) => ({ ...prev, order_no: previewRes.data.generatedCode }));
|
||||
}
|
||||
}
|
||||
} catch { /* 채번 규칙 없으면 수동 입력 */ }
|
||||
};
|
||||
|
||||
// 수정 모달 열기
|
||||
@@ -603,6 +632,22 @@ export default function SalesOrderPage() {
|
||||
|
||||
// 저장 (마스터 + 디테일)
|
||||
const handleSave = async () => {
|
||||
// 채번 규칙이 있으면 allocate, 없으면 수동 입력 필수
|
||||
if (!isEditMode && orderNoRuleId) {
|
||||
try {
|
||||
const allocRes = await allocateNumberingCode(orderNoRuleId);
|
||||
if (allocRes.success && allocRes.data?.generatedCode) {
|
||||
setMasterForm((prev) => ({ ...prev, order_no: allocRes.data.generatedCode }));
|
||||
masterForm.order_no = allocRes.data.generatedCode;
|
||||
} else {
|
||||
toast.error("수주번호 채번에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
toast.error("수주번호 채번에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!masterForm.order_no && !isEditMode) {
|
||||
toast.error("수주번호는 필수입니다.");
|
||||
return;
|
||||
@@ -784,7 +829,7 @@ export default function SalesOrderPage() {
|
||||
spec: item.size || "",
|
||||
material: getCategoryLabel("item_material", item.material) || item.material || "",
|
||||
packing_material: "",
|
||||
unit: getCategoryLabel("item_unit", item.unit) || item.unit || "",
|
||||
unit: getCategoryLabel("item_inventory_unit", item.inventory_unit) || item.inventory_unit || "",
|
||||
qty: "1",
|
||||
pack_qty: "0",
|
||||
unit_price: unitPrice,
|
||||
@@ -1216,8 +1261,10 @@ export default function SalesOrderPage() {
|
||||
</Label>
|
||||
<Input
|
||||
value={masterForm.order_no || ""}
|
||||
onChange={(e) => setMasterForm((p) => ({ ...p, order_no: e.target.value }))}
|
||||
placeholder="수주번호" className="h-9" disabled={isEditMode}
|
||||
onChange={(e) => !orderNoRuleId && setMasterForm((p) => ({ ...p, order_no: e.target.value }))}
|
||||
readOnly={!!orderNoRuleId || isEditMode}
|
||||
placeholder={orderNoRuleId ? "자동 채번" : "수주번호"}
|
||||
className={cn("h-9", (orderNoRuleId || isEditMode) && "bg-muted cursor-not-allowed")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -1486,7 +1533,7 @@ export default function SalesOrderPage() {
|
||||
<Select value={row.unit || ""} onValueChange={(v) => updateDetailRow(idx, "unit", v)}>
|
||||
<SelectTrigger className="h-8 text-xs w-full"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["item_unit"] || []).map((o) => (
|
||||
{(categoryOptions["item_inventory_unit"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1495,8 +1542,9 @@ export default function SalesOrderPage() {
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={row.qty || "1"}
|
||||
min="0"
|
||||
value={row.qty ?? ""}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onChange={(e) => updateDetailRow(idx, "qty", e.target.value)}
|
||||
className="h-8 text-xs text-right font-mono w-full"
|
||||
/>
|
||||
@@ -1660,7 +1708,7 @@ export default function SalesOrderPage() {
|
||||
{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">
|
||||
{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}
|
||||
{categoryOptions["item_inventory_unit"]?.find((o) => o.code === item.inventory_unit)?.label || item.inventory_unit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -149,7 +149,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
{ key: "item_number", label: "품번" },
|
||||
{ key: "item_name", label: "품명" },
|
||||
{ key: "size", label: "규격" },
|
||||
{ key: "unit", label: "단위" },
|
||||
{ key: "inventory_unit", label: "단위" },
|
||||
{ key: "standard_price", label: "기준단가" },
|
||||
{ key: "selling_price", label: "판매가격" },
|
||||
{ key: "currency_code", label: "통화" },
|
||||
@@ -162,13 +162,12 @@ const FORM_FIELDS = [
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
{ key: "size", label: "규격", type: "text" },
|
||||
{ key: "unit", label: "단위", type: "category" },
|
||||
{ key: "inventory_unit", label: "단위", type: "category" },
|
||||
{ key: "material", label: "재질", type: "category" },
|
||||
{ key: "status", label: "상태", type: "category" },
|
||||
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
|
||||
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
|
||||
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
|
||||
{ key: "inventory_unit", label: "재고단위", type: "category" },
|
||||
{ key: "selling_price", label: "판매가격", type: "text" },
|
||||
{ key: "standard_price", label: "기준단가", type: "text" },
|
||||
{ key: "currency_code", label: "통화", type: "category" },
|
||||
@@ -1159,7 +1158,7 @@ export default function SalesItemPage() {
|
||||
const handleExcelDownload = async () => {
|
||||
if (items.length === 0) return;
|
||||
const data = items.map((i) => ({
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit,
|
||||
품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.inventory_unit,
|
||||
기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status,
|
||||
}));
|
||||
await exportToExcel(data, "판매품목정보.xlsx", "판매품목");
|
||||
@@ -1171,7 +1170,7 @@ export default function SalesItemPage() {
|
||||
{ key: "item_number", label: "품번", width: "w-[110px]" },
|
||||
{ key: "item_name", label: "품명", minWidth: "min-w-[130px]" },
|
||||
{ key: "size", label: "규격", width: "w-[80px]" },
|
||||
{ key: "unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "inventory_unit", label: "단위", width: "w-[60px]" },
|
||||
{ key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "currency_code", label: "통화", width: "w-[50px]" },
|
||||
|
||||
+1
@@ -249,6 +249,7 @@ export function ProcessWorkStandardComponent({
|
||||
}
|
||||
detailTypes={config.detailTypes}
|
||||
editItem={editingItem}
|
||||
selectedItemCode={selection.itemCode || undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+28
-2
@@ -21,7 +21,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DetailTypeDefinition, WorkItem } from "../types";
|
||||
import { DetailTypeDefinition, WorkItem, WorkItemDetail } from "../types";
|
||||
import { DetailFormModal } from "./DetailFormModal";
|
||||
|
||||
interface ModalDetail {
|
||||
id: string;
|
||||
@@ -50,6 +51,7 @@ interface WorkItemAddModalProps {
|
||||
phaseLabel: string;
|
||||
detailTypes: DetailTypeDefinition[];
|
||||
editItem?: WorkItem | null;
|
||||
selectedItemCode?: string;
|
||||
}
|
||||
|
||||
export function WorkItemAddModal({
|
||||
@@ -60,11 +62,13 @@ export function WorkItemAddModal({
|
||||
phaseLabel,
|
||||
detailTypes,
|
||||
editItem,
|
||||
selectedItemCode,
|
||||
}: WorkItemAddModalProps) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [isRequired, setIsRequired] = useState("Y");
|
||||
const [description, setDescription] = useState("");
|
||||
const [details, setDetails] = useState<ModalDetail[]>([]);
|
||||
const [detailFormOpen, setDetailFormOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && editItem) {
|
||||
@@ -211,7 +215,7 @@ export function WorkItemAddModal({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 gap-1 text-[10px]"
|
||||
onClick={addDetail}
|
||||
onClick={() => setDetailFormOpen(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
상세 추가
|
||||
@@ -345,6 +349,28 @@ export function WorkItemAddModal({
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
{/* 상세 항목 추가 모달 */}
|
||||
<DetailFormModal
|
||||
open={detailFormOpen}
|
||||
onClose={() => setDetailFormOpen(false)}
|
||||
onSubmit={(data) => {
|
||||
setDetails((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
detail_type: data.detail_type || detailTypes[0]?.value || "",
|
||||
content: data.content || "",
|
||||
is_required: data.is_required || "N",
|
||||
sort_order: prev.length + 1,
|
||||
},
|
||||
]);
|
||||
setDetailFormOpen(false);
|
||||
}}
|
||||
detailTypes={detailTypes}
|
||||
mode="add"
|
||||
selectedItemCode={selectedItemCode}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user