From b28e8e206ce32e7d0ee06af4c84440bf0b63aefd Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 13 Apr 2026 10:55:11 +0900 Subject: [PATCH] feat: Implement drag-and-drop functionality for modal column reordering in purchase order page - Added DnD (Drag and Drop) capabilities to allow users to reorder columns in the modal for purchase orders. - Introduced a new `SortableModalHead` component to manage the sortable headers. - Implemented local storage functionality to save and retrieve the column order, enhancing user customization. - This feature aims to improve the user experience by providing flexibility in how data is displayed across multiple company implementations. --- .../(main)/COMPANY_10/purchase/order/page.tsx | 280 ++++++++++++------ .../(main)/COMPANY_29/purchase/order/page.tsx | 280 ++++++++++++------ .../(main)/COMPANY_30/purchase/order/page.tsx | 191 ++++++------ .../(main)/COMPANY_7/purchase/order/page.tsx | 280 ++++++++++++------ .../(main)/COMPANY_8/purchase/order/page.tsx | 280 ++++++++++++------ .../(main)/COMPANY_9/purchase/order/page.tsx | 280 ++++++++++++------ 6 files changed, 1047 insertions(+), 544 deletions(-) diff --git a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx index 1bc3bc88..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx @@ -15,9 +15,12 @@ import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, - Settings2, + Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -84,6 +87,49 @@ const GRID_COLUMNS_CONFIG = [ { key: "memo", label: "메모" }, ]; +const MODAL_DETAIL_COLUMNS = [ + { key: "item_code", label: "품번", width: "w-[120px]" }, + { key: "item_name", label: "품명", width: "w-[120px]" }, + { key: "supplier", label: "공급업체", width: "w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "order_qty", label: "발주수량", width: "w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "w-[80px]" }, + { key: "unit_price", label: "단가", width: "w-[100px]" }, + { key: "amount", label: "금액", width: "w-[100px]" }, + { key: "due_date", label: "납기일", width: "w-[160px]" }, + { key: "memo", label: "메모", width: "w-[120px]" }, +]; + +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; + +function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( + +
+ + {col.label} +
+
+ ); +} + export default function PurchaseOrderPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); @@ -121,8 +167,43 @@ export default function PurchaseOrderPage() { // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); + // 모달 품목 테이블 컬럼 순서 (드래그 재정렬) + const [modalColumns, setModalColumns] = useState(MODAL_DETAIL_COLUMNS); + const modalSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + + useEffect(() => { + const saved = localStorage.getItem(MODAL_COL_ORDER_KEY); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order.map((key) => MODAL_DETAIL_COLUMNS.find((c) => c.key === key)).filter(Boolean) as typeof MODAL_DETAIL_COLUMNS; + const remaining = MODAL_DETAIL_COLUMNS.filter((c) => !order.includes(c.key)); + setModalColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, []); + + const handleModalDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setModalColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + localStorage.setItem(MODAL_COL_ORDER_KEY, JSON.stringify(next.map((c) => c.key))); + return next; + }); + }; + const isReadOnly = masterForm.status === "입고완료" || masterForm.status === "취소"; + const visibleModalColumns = useMemo(() => { + return modalColumns.filter((col) => { + if (col.key === "supplier" && masterForm.input_mode !== "itemFirst") return false; + return true; + }); + }, [modalColumns, masterForm.input_mode]); + // 카테고리 로드 useEffect(() => { const loadCategories = async () => { @@ -1012,96 +1093,115 @@ export default function PurchaseOrderPage() { ) : (
- - - - {!isReadOnly && } - 품번 - 품명 - {masterForm.input_mode === "itemFirst" && ( - 공급업체 - )} - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 - - - - {detailRows.map((row, idx) => ( - - {!isReadOnly && ( - - - - )} - {row.item_code} - {row.item_name} - {masterForm.input_mode === "itemFirst" && ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - )} - {row.spec} - {row.unit} - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + +
+ + c.key)} strategy={horizontalListSortingStrategy}> + + {!isReadOnly && } + {visibleModalColumns.map((col) => ( + + ))} + + + + + {detailRows.map((row, idx) => ( + + {!isReadOnly && ( + + + )} - - {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"} - {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"} - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - {row.amount ? Number(row.amount).toLocaleString() : ""} - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - - ))} - -
+ {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} + + ))} + + +
)} diff --git a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx index 1bc3bc88..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx @@ -15,9 +15,12 @@ import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, - Settings2, + Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -84,6 +87,49 @@ const GRID_COLUMNS_CONFIG = [ { key: "memo", label: "메모" }, ]; +const MODAL_DETAIL_COLUMNS = [ + { key: "item_code", label: "품번", width: "w-[120px]" }, + { key: "item_name", label: "품명", width: "w-[120px]" }, + { key: "supplier", label: "공급업체", width: "w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "order_qty", label: "발주수량", width: "w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "w-[80px]" }, + { key: "unit_price", label: "단가", width: "w-[100px]" }, + { key: "amount", label: "금액", width: "w-[100px]" }, + { key: "due_date", label: "납기일", width: "w-[160px]" }, + { key: "memo", label: "메모", width: "w-[120px]" }, +]; + +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; + +function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( + +
+ + {col.label} +
+
+ ); +} + export default function PurchaseOrderPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); @@ -121,8 +167,43 @@ export default function PurchaseOrderPage() { // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); + // 모달 품목 테이블 컬럼 순서 (드래그 재정렬) + const [modalColumns, setModalColumns] = useState(MODAL_DETAIL_COLUMNS); + const modalSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + + useEffect(() => { + const saved = localStorage.getItem(MODAL_COL_ORDER_KEY); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order.map((key) => MODAL_DETAIL_COLUMNS.find((c) => c.key === key)).filter(Boolean) as typeof MODAL_DETAIL_COLUMNS; + const remaining = MODAL_DETAIL_COLUMNS.filter((c) => !order.includes(c.key)); + setModalColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, []); + + const handleModalDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setModalColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + localStorage.setItem(MODAL_COL_ORDER_KEY, JSON.stringify(next.map((c) => c.key))); + return next; + }); + }; + const isReadOnly = masterForm.status === "입고완료" || masterForm.status === "취소"; + const visibleModalColumns = useMemo(() => { + return modalColumns.filter((col) => { + if (col.key === "supplier" && masterForm.input_mode !== "itemFirst") return false; + return true; + }); + }, [modalColumns, masterForm.input_mode]); + // 카테고리 로드 useEffect(() => { const loadCategories = async () => { @@ -1012,96 +1093,115 @@ export default function PurchaseOrderPage() { ) : (
- - - - {!isReadOnly && } - 품번 - 품명 - {masterForm.input_mode === "itemFirst" && ( - 공급업체 - )} - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 - - - - {detailRows.map((row, idx) => ( - - {!isReadOnly && ( - - - - )} - {row.item_code} - {row.item_name} - {masterForm.input_mode === "itemFirst" && ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - )} - {row.spec} - {row.unit} - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + +
+ + c.key)} strategy={horizontalListSortingStrategy}> + + {!isReadOnly && } + {visibleModalColumns.map((col) => ( + + ))} + + + + + {detailRows.map((row, idx) => ( + + {!isReadOnly && ( + + + )} - - {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"} - {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"} - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - {row.amount ? Number(row.amount).toLocaleString() : ""} - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - - ))} - -
+ {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} + + ))} + + +
)} diff --git a/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx index 0d512a3e..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/purchase/order/page.tsx @@ -18,6 +18,9 @@ import { Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -28,11 +31,6 @@ import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; -import { - DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent, -} from "@dnd-kit/core"; -import { SortableContext, horizontalListSortingStrategy, useSortable, arrayMove } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; const MASTER_TABLE = "purchase_order_mng"; const DETAIL_TABLE = "purchase_detail"; @@ -104,7 +102,7 @@ const MODAL_DETAIL_COLUMNS = [ { key: "memo", label: "메모", width: "w-[120px]" }, ]; -const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order"; +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); @@ -114,11 +112,18 @@ function SortableModalHead({ col }: { col: { key: string; label: string; width: opacity: isDragging ? 0.5 : 1, }; return ( - +
-
- -
+ {col.label}
@@ -748,89 +753,6 @@ export default function PurchaseOrderPage() { setDetailRows((prev) => prev.filter((_, i) => i !== idx)); }; - const renderDetailCell = (col: { key: string }, row: any, idx: number) => { - switch (col.key) { - case "item_code": - return {row.item_code}; - case "item_name": - return {row.item_name}; - case "supplier": - return ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - ); - case "spec": - return {row.spec}; - case "unit": - return {row.unit}; - case "order_qty": - return ( - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - ); - case "received_qty": - return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; - case "remain_qty": - return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; - case "unit_price": - return ( - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - ); - case "amount": - return {row.amount ? Number(row.amount).toLocaleString() : ""}; - case "due_date": - return ( - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - ); - case "memo": - return ( - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - ); - default: - return null; - } - }; - const handleExcelDownload = async () => { if (orders.length === 0) { toast.error("다운로드할 데이터가 없어요."); return; } const data = orders.map((o) => { @@ -1193,7 +1115,88 @@ export default function PurchaseOrderPage() { )} - {visibleModalColumns.map((col) => renderDetailCell(col, row, idx))} + {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} ))} diff --git a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx index 1bc3bc88..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx @@ -15,9 +15,12 @@ import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, - Settings2, + Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -84,6 +87,49 @@ const GRID_COLUMNS_CONFIG = [ { key: "memo", label: "메모" }, ]; +const MODAL_DETAIL_COLUMNS = [ + { key: "item_code", label: "품번", width: "w-[120px]" }, + { key: "item_name", label: "품명", width: "w-[120px]" }, + { key: "supplier", label: "공급업체", width: "w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "order_qty", label: "발주수량", width: "w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "w-[80px]" }, + { key: "unit_price", label: "단가", width: "w-[100px]" }, + { key: "amount", label: "금액", width: "w-[100px]" }, + { key: "due_date", label: "납기일", width: "w-[160px]" }, + { key: "memo", label: "메모", width: "w-[120px]" }, +]; + +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; + +function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( + +
+ + {col.label} +
+
+ ); +} + export default function PurchaseOrderPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); @@ -121,8 +167,43 @@ export default function PurchaseOrderPage() { // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); + // 모달 품목 테이블 컬럼 순서 (드래그 재정렬) + const [modalColumns, setModalColumns] = useState(MODAL_DETAIL_COLUMNS); + const modalSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + + useEffect(() => { + const saved = localStorage.getItem(MODAL_COL_ORDER_KEY); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order.map((key) => MODAL_DETAIL_COLUMNS.find((c) => c.key === key)).filter(Boolean) as typeof MODAL_DETAIL_COLUMNS; + const remaining = MODAL_DETAIL_COLUMNS.filter((c) => !order.includes(c.key)); + setModalColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, []); + + const handleModalDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setModalColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + localStorage.setItem(MODAL_COL_ORDER_KEY, JSON.stringify(next.map((c) => c.key))); + return next; + }); + }; + const isReadOnly = masterForm.status === "입고완료" || masterForm.status === "취소"; + const visibleModalColumns = useMemo(() => { + return modalColumns.filter((col) => { + if (col.key === "supplier" && masterForm.input_mode !== "itemFirst") return false; + return true; + }); + }, [modalColumns, masterForm.input_mode]); + // 카테고리 로드 useEffect(() => { const loadCategories = async () => { @@ -1012,96 +1093,115 @@ export default function PurchaseOrderPage() { ) : (
- - - - {!isReadOnly && } - 품번 - 품명 - {masterForm.input_mode === "itemFirst" && ( - 공급업체 - )} - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 - - - - {detailRows.map((row, idx) => ( - - {!isReadOnly && ( - - - - )} - {row.item_code} - {row.item_name} - {masterForm.input_mode === "itemFirst" && ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - )} - {row.spec} - {row.unit} - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + +
+ + c.key)} strategy={horizontalListSortingStrategy}> + + {!isReadOnly && } + {visibleModalColumns.map((col) => ( + + ))} + + + + + {detailRows.map((row, idx) => ( + + {!isReadOnly && ( + + + )} - - {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"} - {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"} - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - {row.amount ? Number(row.amount).toLocaleString() : ""} - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - - ))} - -
+ {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} + + ))} + + +
)} diff --git a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx index 1bc3bc88..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx @@ -15,9 +15,12 @@ import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, - Settings2, + Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -84,6 +87,49 @@ const GRID_COLUMNS_CONFIG = [ { key: "memo", label: "메모" }, ]; +const MODAL_DETAIL_COLUMNS = [ + { key: "item_code", label: "품번", width: "w-[120px]" }, + { key: "item_name", label: "품명", width: "w-[120px]" }, + { key: "supplier", label: "공급업체", width: "w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "order_qty", label: "발주수량", width: "w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "w-[80px]" }, + { key: "unit_price", label: "단가", width: "w-[100px]" }, + { key: "amount", label: "금액", width: "w-[100px]" }, + { key: "due_date", label: "납기일", width: "w-[160px]" }, + { key: "memo", label: "메모", width: "w-[120px]" }, +]; + +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; + +function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( + +
+ + {col.label} +
+
+ ); +} + export default function PurchaseOrderPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); @@ -121,8 +167,43 @@ export default function PurchaseOrderPage() { // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); + // 모달 품목 테이블 컬럼 순서 (드래그 재정렬) + const [modalColumns, setModalColumns] = useState(MODAL_DETAIL_COLUMNS); + const modalSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + + useEffect(() => { + const saved = localStorage.getItem(MODAL_COL_ORDER_KEY); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order.map((key) => MODAL_DETAIL_COLUMNS.find((c) => c.key === key)).filter(Boolean) as typeof MODAL_DETAIL_COLUMNS; + const remaining = MODAL_DETAIL_COLUMNS.filter((c) => !order.includes(c.key)); + setModalColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, []); + + const handleModalDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setModalColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + localStorage.setItem(MODAL_COL_ORDER_KEY, JSON.stringify(next.map((c) => c.key))); + return next; + }); + }; + const isReadOnly = masterForm.status === "입고완료" || masterForm.status === "취소"; + const visibleModalColumns = useMemo(() => { + return modalColumns.filter((col) => { + if (col.key === "supplier" && masterForm.input_mode !== "itemFirst") return false; + return true; + }); + }, [modalColumns, masterForm.input_mode]); + // 카테고리 로드 useEffect(() => { const loadCategories = async () => { @@ -1012,96 +1093,115 @@ export default function PurchaseOrderPage() { ) : (
- - - - {!isReadOnly && } - 품번 - 품명 - {masterForm.input_mode === "itemFirst" && ( - 공급업체 - )} - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 - - - - {detailRows.map((row, idx) => ( - - {!isReadOnly && ( - - - - )} - {row.item_code} - {row.item_name} - {masterForm.input_mode === "itemFirst" && ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - )} - {row.spec} - {row.unit} - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + +
+ + c.key)} strategy={horizontalListSortingStrategy}> + + {!isReadOnly && } + {visibleModalColumns.map((col) => ( + + ))} + + + + + {detailRows.map((row, idx) => ( + + {!isReadOnly && ( + + + )} - - {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"} - {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"} - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - {row.amount ? Number(row.amount).toLocaleString() : ""} - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - - ))} - -
+ {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} + + ))} + + +
)} diff --git a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx index 1bc3bc88..143a84a9 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx @@ -15,9 +15,12 @@ import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, ClipboardList, Pencil, Search, X, Package, ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, - Settings2, + Settings2, GripVertical, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, arrayMove, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import { apiClient } from "@/lib/api/client"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; @@ -84,6 +87,49 @@ const GRID_COLUMNS_CONFIG = [ { key: "memo", label: "메모" }, ]; +const MODAL_DETAIL_COLUMNS = [ + { key: "item_code", label: "품번", width: "w-[120px]" }, + { key: "item_name", label: "품명", width: "w-[120px]" }, + { key: "supplier", label: "공급업체", width: "w-[150px]" }, + { key: "spec", label: "규격", width: "w-[80px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "order_qty", label: "발주수량", width: "w-[90px]" }, + { key: "received_qty", label: "입고수량", width: "w-[90px]" }, + { key: "remain_qty", label: "잔량", width: "w-[80px]" }, + { key: "unit_price", label: "단가", width: "w-[100px]" }, + { key: "amount", label: "금액", width: "w-[100px]" }, + { key: "due_date", label: "납기일", width: "w-[160px]" }, + { key: "memo", label: "메모", width: "w-[120px]" }, +]; + +const MODAL_COL_ORDER_KEY = "purchase_order_modal_col_order_c16"; + +function SortableModalHead({ col }: { col: { key: string; label: string; width: string } }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: col.key }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( + +
+ + {col.label} +
+
+ ); +} + export default function PurchaseOrderPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); @@ -121,8 +167,43 @@ export default function PurchaseOrderPage() { // 테이블 설정 const ts = useTableSettings("c16-purchase-order", DETAIL_TABLE, GRID_COLUMNS_CONFIG); + // 모달 품목 테이블 컬럼 순서 (드래그 재정렬) + const [modalColumns, setModalColumns] = useState(MODAL_DETAIL_COLUMNS); + const modalSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + + useEffect(() => { + const saved = localStorage.getItem(MODAL_COL_ORDER_KEY); + if (saved) { + try { + const order = JSON.parse(saved) as string[]; + const reordered = order.map((key) => MODAL_DETAIL_COLUMNS.find((c) => c.key === key)).filter(Boolean) as typeof MODAL_DETAIL_COLUMNS; + const remaining = MODAL_DETAIL_COLUMNS.filter((c) => !order.includes(c.key)); + setModalColumns([...reordered, ...remaining]); + } catch { /* skip */ } + } + }, []); + + const handleModalDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + setModalColumns((prev) => { + const oldIndex = prev.findIndex((c) => c.key === active.id); + const newIndex = prev.findIndex((c) => c.key === over.id); + const next = arrayMove(prev, oldIndex, newIndex); + localStorage.setItem(MODAL_COL_ORDER_KEY, JSON.stringify(next.map((c) => c.key))); + return next; + }); + }; + const isReadOnly = masterForm.status === "입고완료" || masterForm.status === "취소"; + const visibleModalColumns = useMemo(() => { + return modalColumns.filter((col) => { + if (col.key === "supplier" && masterForm.input_mode !== "itemFirst") return false; + return true; + }); + }, [modalColumns, masterForm.input_mode]); + // 카테고리 로드 useEffect(() => { const loadCategories = async () => { @@ -1012,96 +1093,115 @@ export default function PurchaseOrderPage() { ) : (
- - - - {!isReadOnly && } - 품번 - 품명 - {masterForm.input_mode === "itemFirst" && ( - 공급업체 - )} - 규격 - 단위 - 발주수량 - 입고수량 - 잔량 - 단가 - 금액 - 납기일 - 메모 - - - - {detailRows.map((row, idx) => ( - - {!isReadOnly && ( - - - - )} - {row.item_code} - {row.item_name} - {masterForm.input_mode === "itemFirst" && ( - - {isReadOnly ? ( - {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} - ) : ( - - )} - - )} - {row.spec} - {row.unit} - - {isReadOnly ? ( - {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + +
+ + c.key)} strategy={horizontalListSortingStrategy}> + + {!isReadOnly && } + {visibleModalColumns.map((col) => ( + + ))} + + + + + {detailRows.map((row, idx) => ( + + {!isReadOnly && ( + + + )} - - {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"} - {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"} - - {isReadOnly ? ( - {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} - ) : ( - updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> - )} - - {row.amount ? Number(row.amount).toLocaleString() : ""} - - {isReadOnly ? ( - {row.due_date} - ) : ( - updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> - )} - - - {isReadOnly ? ( - {row.memo} - ) : ( - updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> - )} - - - ))} - -
+ {visibleModalColumns.map((col) => { + switch (col.key) { + case "item_code": + return {row.item_code}; + case "item_name": + return {row.item_name}; + case "supplier": + return ( + + {isReadOnly ? ( + {(categoryOptions["supplier_code"] || []).find(o => o.code === row.supplier_code)?.label || row.supplier_code || "-"} + ) : ( + + )} + + ); + case "spec": + return {row.spec}; + case "unit": + return {row.unit}; + case "order_qty": + return ( + + {isReadOnly ? ( + {row.order_qty ? Number(row.order_qty).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "order_qty", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "received_qty": + return {row.received_qty ? Number(row.received_qty).toLocaleString() : "0"}; + case "remain_qty": + return {row.remain_qty ? Number(row.remain_qty).toLocaleString() : "0"}; + case "unit_price": + return ( + + {isReadOnly ? ( + {row.unit_price ? Number(row.unit_price).toLocaleString() : ""} + ) : ( + updateDetailRow(idx, "unit_price", parseNumber(e.target.value))} className="h-8 text-xs text-right font-mono" /> + )} + + ); + case "amount": + return {row.amount ? Number(row.amount).toLocaleString() : ""}; + case "due_date": + return ( + + {isReadOnly ? ( + {row.due_date} + ) : ( + updateDetailRow(idx, "due_date", e.target.value)} className="h-8 text-xs" /> + )} + + ); + case "memo": + return ( + + {isReadOnly ? ( + {row.memo} + ) : ( + updateDetailRow(idx, "memo", e.target.value)} className="h-8 text-xs" /> + )} + + ); + default: + return ; + } + })} + + ))} + + +
)}