diff --git a/backend-node/src/controllers/approvalController.ts b/backend-node/src/controllers/approvalController.ts index eabe77ce..194e7be2 100644 --- a/backend-node/src/controllers/approvalController.ts +++ b/backend-node/src/controllers/approvalController.ts @@ -892,6 +892,40 @@ export class ApprovalRequestController { const userName = req.user?.userName || ""; const deptName = req.user?.deptName || ""; + // ๐Ÿ”’ ์ค‘๋ณต ๊ฒฐ์žฌ ์ฐจ๋‹จ: ๊ฐ™์€ target์— ํ™œ์„ฑ/์™„๋ฃŒ๋œ ๊ฒฐ์žฌ๊ฐ€ ์žˆ์œผ๋ฉด ๊ฑฐ๋ถ€ + // (rejected, cancelled๋Š” ์žฌ์ƒ์‹  ํ—ˆ์šฉ) + if (target_record_id) { + const existing = await queryOne( + `SELECT request_id, status FROM approval_requests + WHERE target_table = $1 AND target_record_id = $2 AND company_code = $3 + AND status IN ('requested', 'in_progress', 'approved', 'post_pending') + ORDER BY request_id DESC LIMIT 1`, + [target_table, safeTargetRecordId, companyCode] + ); + if (existing) { + const statusLabel: Record = { + requested: "์š”์ฒญ๋จ", in_progress: "๊ฒฐ์žฌ์ค‘", approved: "์Šน์ธ์™„๋ฃŒ", post_pending: "ํ›„๊ฒฐ๋Œ€๊ธฐ", + }; + return res.status(409).json({ + success: false, + message: `์ด๋ฏธ ${statusLabel[existing.status] || existing.status} ์ƒํƒœ์˜ ๊ฒฐ์žฌ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. (์š”์ฒญ ID: ${existing.request_id})`, + error: { code: "DUPLICATE_APPROVAL", details: existing }, + }); + } + } + + // ๐Ÿ”’ ์ž๊ธฐ ์ž์‹  ๊ฒฐ์žฌ ์ฐจ๋‹จ: approval_type์ด 'self'๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ฒฐ์žฌ์„ ์— ๋ณธ์ธ ํฌํ•จ ๋ถˆ๊ฐ€ + if (approval_type !== "self" && Array.isArray(approvers)) { + const selfInLine = approvers.find((a: any) => (a.userId || a.user_id) === userId); + if (selfInLine) { + return res.status(400).json({ + success: false, + message: "๊ฒฐ์žฌ์„ ์— ๋ณธ์ธ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ž๊ธฐ๊ฒฐ์žฌ(์ „๊ฒฐ)๋Š” ๋ณ„๋„ ์œ ํ˜•์„ ์‚ฌ์šฉํ•ด ์ฃผ์„ธ์š”.", + error: { code: "SELF_APPROVER_NOT_ALLOWED" }, + }); + } + } + // approval_mode๋ฅผ target_record_data์— ๋ณ‘ํ•ฉ ์ €์žฅ (ํ•˜์œ„ํ˜ธํ™˜) const mergedRecordData = { ...(target_record_data || {}), diff --git a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx index 2adf7fcd..9b2e32c8 100644 --- a/frontend/app/(main)/COMPANY_10/production/bom/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/bom/page.tsx @@ -1986,7 +1986,7 @@ export default function BomManagementPage() { {/* โ”€โ”€โ”€ BOM ๋“ฑ๋ก/์ˆ˜์ • ๋ชจ๋‹ฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - { if (showItemSearchModal) e.preventDefault(); }}> + { if (showItemSearchModal) e.preventDefault(); }}> {isEditMode ? "BOM ์ˆ˜์ •" : "BOM ๋“ฑ๋ก"} {isEditMode ? "BOM ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ด์š”" : "์ƒˆ๋กœ์šด BOM์„ ๋“ฑ๋กํ•ด์š”"} diff --git a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx index 07f00fce..221a28d7 100644 --- a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx @@ -58,6 +58,7 @@ const FLAT_COLUMNS = [ { key: "unit_price", label: "๋‹จ๊ฐ€", source: "detail" }, { key: "amount", label: "๊ธˆ์•ก", source: "detail" }, { key: "due_date", label: "๋‚ฉ๊ธฐ์ผ", source: "detail" }, + { key: "approval_status", label: "๊ฒฐ์žฌ์ƒํƒœ", source: "master" }, { key: "memo", label: "๋ฉ”๋ชจ", source: "master" }, ]; @@ -66,8 +67,26 @@ const DETAIL_HEADER_COLS = FLAT_COLUMNS.filter((c) => c.source === "detail"); // ํ•„ํ„ฐ์šฉ ์ „์ฒด ํ‚ค const GRID_COLUMNS_CONFIG = FLAT_COLUMNS.map(({ key, label }) => ({ key, label })); -// ์ด ์ปฌ๋Ÿผ ์ˆ˜: ์ฒดํฌ๋ฐ•์Šค(1) + ํ”Œ๋žซ ์ปฌ๋Ÿผ(14) = 15 -const TOTAL_COLS = 15; +// ์ด ์ปฌ๋Ÿผ ์ˆ˜: ์ฒดํฌ๋ฐ•์Šค(1) + ํ”Œ๋žซ ์ปฌ๋Ÿผ(15) = 16 +const TOTAL_COLS = 16; + +// ๊ฒฐ์žฌ์ƒํƒœ ๋ผ๋ฒจ/์ƒ‰์ƒ +const APPROVAL_STATUS_LABEL: Record = { + requested: "์š”์ฒญ", + in_progress: "๊ฒฐ์žฌ์ค‘", + approved: "์Šน์ธ์™„๋ฃŒ", + rejected: "๋ฐ˜๋ ค", + cancelled: "ํšŒ์ˆ˜", + post_pending: "ํ›„๊ฒฐ๋Œ€๊ธฐ", +}; +const APPROVAL_STATUS_CLASS: Record = { + requested: "bg-secondary text-secondary-foreground", + in_progress: "bg-primary/10 text-primary border border-primary/20", + approved: "bg-emerald-500/10 text-emerald-600 border border-emerald-500/20", + rejected: "bg-destructive/10 text-destructive border border-destructive/20", + cancelled: "bg-muted text-muted-foreground", + post_pending: "bg-warning/10 text-warning", +}; // ํ—ค๋” ํ•„ํ„ฐ Popover function HeaderFilterPopover({ @@ -333,6 +352,28 @@ export default function SalesOrderPage() { } catch { /* skip */ } } + // ๊ฒฐ์žฌ ์ƒํƒœ ์กฐ์ธ (target_table='sales_order_mng', target_record_id = order_no) + let approvalMap: Record = {}; + if (orderNos.length > 0) { + try { + const apprRes = await apiClient.post(`/table-management/tables/approval_requests/data`, { + page: 1, size: orderNos.length + 10, + dataFilter: { enabled: true, filters: [ + { columnName: "target_table", operator: "equals", value: "sales_order_mng" }, + { columnName: "target_record_id", operator: "in", value: orderNos.map(String) }, + ] }, + autoFilter: true, + sort: { columnName: "request_id", order: "desc" }, + }); + const apprs = apprRes.data?.data?.data || apprRes.data?.data?.rows || []; + // ๊ฐ™์€ order_no์— ์—ฌ๋Ÿฌ ๊ฒฐ์žฌ๊ฐ€ ์žˆ์œผ๋ฉด ์ตœ์‹ ๋งŒ (sort desc ์ฒซ ๋ฒˆ์งธ) + for (const a of apprs) { + const rid = String(a.target_record_id); + if (!approvalMap[rid]) approvalMap[rid] = a; + } + } catch { /* skip */ } + } + // part_code โ†’ item_info ์กฐ์ธ const partCodes = [...new Set(rows.map((r: any) => r.part_code).filter(Boolean))]; let itemMap: Record = {}; @@ -359,6 +400,7 @@ export default function SalesOrderPage() { const item = itemMap[row.part_code]; const master = masterMap[row.order_no]; const rawUnit = row.unit || item?.inventory_unit || ""; + const appr = approvalMap[String(row.order_no)] || null; return { ...row, part_name: row.part_name || item?.item_name || "", @@ -366,6 +408,8 @@ export default function SalesOrderPage() { material: row.material || (item ? (resolveLabel("item_material", item.material) || item.material || "") : ""), unit: resolveLabel("item_inventory_unit", rawUnit) || rawUnit, memo: row.memo || master?.memo || "", + approval_status: appr?.status || "", + approval_request_id: appr?.request_id || null, _master: master || {}, }; }); @@ -381,6 +425,13 @@ export default function SalesOrderPage() { useEffect(() => { fetchOrders(); }, [fetchOrders]); + // ๊ฒฐ์žฌ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ์‹œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ + useEffect(() => { + const handler = () => fetchOrders(); + window.addEventListener("approval-processed", handler); + return () => window.removeEventListener("approval-processed", handler); + }, [fetchOrders]); + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œโ†’๋ผ๋ฒจ ๋ณ€ํ™˜ const resolveLabel = useCallback((key: string, code: string) => { if (!code) return ""; @@ -705,19 +756,16 @@ export default function SalesOrderPage() { const filters: any[] = []; if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword }); - // ๊ด€๋ฆฌํ’ˆ๋ชฉ ํ•„ํ„ฐ๋ฅผ ์„œ๋ฒ„ ์ฟผ๋ฆฌ์— ํฌํ•จ (์ฝ”๋“œ + ๋ผ๋ฒจ ์–‘์ชฝ ๋Œ€์‘) + // ๊ด€๋ฆฌํ’ˆ๋ชฉ ํ•„ํ„ฐ: ๋‹ค์ค‘๊ฐ’(์ฝค๋งˆ ๊ตฌ๋ถ„) ์ €์žฅ๋œ ๊ฒฝ์šฐ๋„ ๋งค์นญ๋˜๋„๋ก contains ์‚ฌ์šฉ if (itemSearchDivision !== "all") { - const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || ""; - // ์ฝ”๋“œ ๋˜๋Š” ๋ผ๋ฒจ์ด ์ €์žฅ๋œ ๊ฒฝ์šฐ ๋ชจ๋‘ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด in ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ - const divValues = [itemSearchDivision]; - if (divLabel) divValues.push(divLabel); - filters.push({ columnName: "division", operator: "in", value: divValues }); + filters.push({ columnName: "division", operator: "contains", value: itemSearchDivision }); } - // ๊ฑฐ๋ž˜์ฒ˜์šฐ์„  ๋‹จ๊ฐ€๋ฐฉ์‹์ผ ๋•Œ ๊ฑฐ๋ž˜์ฒ˜์— ์—ฐ๊ฒฐ๋œ ํ’ˆ๋ชฉ๋งŒ ํ•„ํ„ฐ๋ง - const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + // ๊ฑฐ๋ž˜์ฒ˜์šฐ์„  ๋‹จ๊ฐ€๋ฐฉ์‹์ผ ๋•Œ ๊ฑฐ๋ž˜์ฒ˜ ๋งคํ•‘ id ์ •๊ทœํ™” โ†’ ์„œ๋ฒ„ ํ•„ํ„ฐ ์ ์šฉ + // price_mode์˜ ๋ผ๋ฒจ๋กœ ํŒ๋‹จ (์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋Š” ํšŒ์‚ฌ๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ) + const priceModeLabel = (categoryOptions["price_mode"] || []).find((o) => o.code === masterForm.price_mode)?.label || ""; + const isCustomerPrice = priceModeLabel.includes("๊ฑฐ๋ž˜์ฒ˜"); const partnerId = masterForm.partner_id; - let customerItemIds: Set | null = null; if (isCustomerPrice && partnerId) { try { @@ -727,7 +775,36 @@ export default function SalesOrderPage() { autoFilter: true, }); const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || []; - customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean)); + const rawIds = [...new Set(mappings.map((m: any) => m.item_id).filter(Boolean))] as string[]; + if (rawIds.length === 0) { + setItemSearchResults([]); setItemTotal(0); setItemTotalPages(1); + setItemSearchLoading(false); + return; + } + // UUID์™€ ๋ฌธ์ž์—ด(item_number) ๋ถ„๋ฆฌ + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const uuidIds = rawIds.filter(v => uuidRegex.test(v)); + const codeIds = rawIds.filter(v => !uuidRegex.test(v)); + + // ๋ฌธ์ž์—ด(item_number)์„ item_info์—์„œ id๋กœ ๋ณ€ํ™˜ + let convertedIds: string[] = []; + if (codeIds.length > 0) { + const convRes = await apiClient.post(`/table-management/tables/item_info/data`, { + page: 1, size: codeIds.length + 10, + dataFilter: { enabled: true, filters: [{ columnName: "item_number", operator: "in", value: codeIds }] }, + autoFilter: true, + }); + const convRows = convRes.data?.data?.data || convRes.data?.data?.rows || []; + convertedIds = convRows.map((r: any) => r.id).filter(Boolean); + } + + const finalIds = [...new Set([...uuidIds, ...convertedIds])]; + if (finalIds.length === 0) { + setItemSearchResults([]); setItemTotal(0); setItemTotalPages(1); + setItemSearchLoading(false); + return; + } + filters.push({ columnName: "id", operator: "in", value: finalIds }); } catch { /* skip */ } } @@ -737,14 +814,9 @@ export default function SalesOrderPage() { autoFilter: true, }); const resData = res.data?.data; - let rows = resData?.data || resData?.rows || []; + const rows = resData?.data || resData?.rows || []; const serverTotal = resData?.total || resData?.totalCount || rows.length; - // ๊ฑฐ๋ž˜์ฒ˜์šฐ์„ ์ผ ๋•Œ ์—ฐ๊ฒฐ๋œ ํ’ˆ๋ชฉ๋งŒ ํ‘œ์‹œ (ํด๋ผ์ด์–ธํŠธ ํ•„ํ„ฐ) - if (customerItemIds) { - rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id)); - } - setItemSearchResults(rows); setItemTotal(serverTotal); setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s))); @@ -778,8 +850,9 @@ export default function SalesOrderPage() { const selected = Array.from(itemSelectedMap.values()); if (selected.length === 0) { toast.error("ํ’ˆ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return; } - const isStandardPrice = masterForm.price_mode === "CAT_MM0BUZKL_HJ7U" || masterForm.price_mode === "CAT_MLKG792S_54WJ"; - const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI"; + const pmLabel = (categoryOptions["price_mode"] || []).find((o) => o.code === masterForm.price_mode)?.label || ""; + const isStandardPrice = pmLabel.includes("๊ธฐ์ค€"); + const isCustomerPrice = pmLabel.includes("๊ฑฐ๋ž˜์ฒ˜"); const partnerId = masterForm.partner_id; let customerPriceMap: Record = {}; @@ -847,10 +920,10 @@ export default function SalesOrderPage() { // ๋‹จ๊ฐ€ ์žฌ๊ณ„์‚ฐ: ๋‹จ๊ฐ€๋ฐฉ์‹/๊ฑฐ๋ž˜์ฒ˜ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ํ’ˆ๋ชฉ ๋‹จ๊ฐ€ ๊ฐฑ์‹  const recalcPrices = useCallback(async (priceMode: string, partnerId: string) => { if (detailRows.length === 0) return; - const STANDARD_CODES = ["CAT_MM0BUZKL_HJ7U", "CAT_MLKG792S_54WJ"]; - const CUSTOMER_CODES = ["CAT_MM0BV3OS_41DX", "CAT_MLKG7D8K_N8SI"]; - const isStandard = STANDARD_CODES.includes(priceMode); - const isCustomer = CUSTOMER_CODES.includes(priceMode); + // price_mode ๋ผ๋ฒจ๋กœ ํŒ๋‹จ (์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋Š” ํšŒ์‚ฌ๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ) + const pmLabel = (categoryOptions["price_mode"] || []).find((o) => o.code === priceMode)?.label || ""; + const isStandard = pmLabel.includes("๊ธฐ์ค€"); + const isCustomer = pmLabel.includes("๊ฑฐ๋ž˜์ฒ˜"); if (isStandard) { // ํ’ˆ๋ชฉ ๊ธฐ์ค€๋‹จ๊ฐ€ ์กฐํšŒ @@ -925,9 +998,11 @@ export default function SalesOrderPage() { setDetailRows((prev) => prev.filter((_, i) => i !== idx)); }; - // ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ํŒ๋‹จ - const isSupplierFirst = masterForm.input_mode === "CAT_MLZWPH5R_983R" || masterForm.input_mode === "CAT_MLKG5KP8_C39W"; - const isOverseas = masterForm.sell_mode === "CAT_MLZWFF2Z_BQCV" || masterForm.sell_mode === "CAT_MLKGAR2W_HAPO"; + // ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ํŒ๋‹จ (๋ผ๋ฒจ ๊ธฐ๋ฐ˜ โ€” ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋Š” ํšŒ์‚ฌ๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ) + const inputModeLabel = (categoryOptions["input_mode"] || []).find((o) => o.code === masterForm.input_mode)?.label || ""; + const sellModeLabel = (categoryOptions["sell_mode"] || []).find((o) => o.code === masterForm.sell_mode)?.label || ""; + const isSupplierFirst = inputModeLabel.includes("๊ณต๊ธ‰") || inputModeLabel.includes("๊ฑฐ๋ž˜์ฒ˜"); + const isOverseas = sellModeLabel.includes("ํ•ด์™ธ") || sellModeLabel.includes("์ˆ˜์ถœ"); const handleExcelDownload = async () => { if (orders.length === 0) { toast.error("๋‹ค์šด๋กœ๋“œํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); return; } @@ -994,6 +1069,42 @@ export default function SalesOrderPage() { > ์‚ญ์ œ{checkedIds.length > 0 && ` (${checkedIds.length})`} +
+ ) : ( + - + )} + {row.memo || ""} ); @@ -1475,6 +1615,9 @@ export default function SalesOrderPage() {