diff --git a/src/components/orders/OrderSalesDetailView.tsx b/src/components/orders/OrderSalesDetailView.tsx index d48f6304..4803d8e9 100644 --- a/src/components/orders/OrderSalesDetailView.tsx +++ b/src/components/orders/OrderSalesDetailView.tsx @@ -30,6 +30,9 @@ import { FileCheck, ClipboardList, CheckCircle2, + ChevronDown, + ChevronRight, + MapPin, } from "lucide-react"; import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; @@ -58,6 +61,7 @@ import { getOrderById, updateOrderStatus, type Order, + type OrderNode, type OrderStatus, } from "@/components/orders"; import { ServerErrorPage } from "@/components/common/ServerErrorPage"; @@ -96,6 +100,119 @@ function InfoItem({ label, value }: { label: string; value: string }) { ); } +// 노드 상태 뱃지 +const NODE_STATUS_CONFIG: Record = { + PENDING: { label: "대기", className: "bg-gray-100 text-gray-700 border-gray-200" }, + CONFIRMED: { label: "확정", className: "bg-blue-100 text-blue-700 border-blue-200" }, + IN_PRODUCTION: { label: "생산중", className: "bg-green-100 text-green-700 border-green-200" }, + PRODUCED: { label: "생산완료", className: "bg-blue-600 text-white border-blue-600" }, + SHIPPED: { label: "출하완료", className: "bg-gray-500 text-white border-gray-500" }, + COMPLETED: { label: "완료", className: "bg-gray-500 text-white border-gray-500" }, + CANCELLED: { label: "취소", className: "bg-red-100 text-red-700 border-red-200" }, +}; + +// 노드별 카드 컴포넌트 (재귀 지원) +function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number }) { + const [isOpen, setIsOpen] = useState(true); + const statusConfig = NODE_STATUS_CONFIG[node.statusCode] || NODE_STATUS_CONFIG.PENDING; + const options = node.options || {}; + + return ( +
0 ? 'ml-6' : ''}`}> + {/* 노드 헤더 */} + + + {/* 노드 내용 (접기/펼치기) */} + {isOpen && ( +
+ {/* 해당 노드의 자재 테이블 */} + {node.items.length > 0 && ( +
+ + + + 순번 + 품목코드 + 품명 + 규격 + 수량 + 단위 + 단가 + 금액 + + + + {node.items.map((item, index) => ( + + {index + 1} + + + {item.itemCode} + + + {item.itemName} + {item.spec} + {item.quantity} + {item.unit} + + {formatAmount(item.unitPrice)}원 + + + {formatAmount(item.amount ?? 0)}원 + + + ))} + +
+
+ )} + + {/* 하위 노드 재귀 */} + {node.children.length > 0 && ( +
+ {node.children.map((child) => ( + + ))} +
+ )} +
+ )} +
+ ); +} + interface OrderSalesDetailViewProps { orderId: string; } @@ -375,75 +492,113 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) { )} {/* 제품 내역 */} - - - 제품 내역 - - -
- - - - 순번 - 품목코드 - 품명 - - 부호 - 규격 - 수량 - 단위 - 단가 - 금액 - - - - {(order.items || []).map((item, index) => ( - - {index + 1} - - - {item.itemCode} - - - {item.itemName} - {item.type || "-"} - {item.symbol || "-"} - {item.spec} - {item.quantity} - {item.unit} - - {formatAmount(item.unitPrice)}원 - - - {formatAmount(item.amount ?? 0)}원 - - - ))} - -
-
+ {order.nodes && order.nodes.length > 0 ? ( + /* 노드별 그룹 표시 */ + + + + + 개소별 내역 ({order.nodes.length}개소) + + + + {order.nodes.map((node) => ( + + ))} - {/* 합계 */} -
-
- 소계: - - {formatAmount(order.subtotal ?? 0)}원 - + {/* 합계 */} +
+
+ 소계: + + {formatAmount(order.subtotal ?? 0)}원 + +
+
+ 할인율: + {order.discountRate}% +
+
+ 총금액: + + {formatAmount(order.totalAmount ?? 0)}원 + +
-
- 할인율: - {order.discountRate}% + + + ) : ( + /* 레거시 플랫 테이블 (노드 없는 기존 수주) */ + + + 제품 내역 + + +
+ + + + 순번 + 품목코드 + 품명 + + 부호 + 규격 + 수량 + 단위 + 단가 + 금액 + + + + {(order.items || []).map((item, index) => ( + + {index + 1} + + + {item.itemCode} + + + {item.itemName} + {item.type || "-"} + {item.symbol || "-"} + {item.spec} + {item.quantity} + {item.unit} + + {formatAmount(item.unitPrice)}원 + + + {formatAmount(item.amount ?? 0)}원 + + + ))} + +
-
- 총금액: - - {formatAmount(order.totalAmount ?? 0)}원 - + + {/* 합계 */} +
+
+ 소계: + + {formatAmount(order.subtotal ?? 0)}원 + +
+
+ 할인율: + {order.discountRate}% +
+
+ 총금액: + + {formatAmount(order.totalAmount ?? 0)}원 + +
-
-
-
+ + + )}
); }, [order, openDocumentModal]); diff --git a/src/components/orders/actions.ts b/src/components/orders/actions.ts index 890ae5f7..c5c16352 100644 --- a/src/components/orders/actions.ts +++ b/src/components/orders/actions.ts @@ -45,12 +45,14 @@ interface ApiOrder { updated_at: string; client?: ApiClient | null; items?: ApiOrderItem[]; + root_nodes?: ApiOrderNode[]; quote?: ApiQuote | null; } interface ApiOrderItem { id: number; order_id: number; + order_node_id: number | null; item_id: number | null; item_code: string | null; item_name: string; @@ -67,6 +69,24 @@ interface ApiOrderItem { sort_order: number; } +interface ApiOrderNode { + id: number; + order_id: number; + parent_id: number | null; + node_type: string; + code: string; + name: string; + status_code: string; + quantity: number; + unit_price: number; + total_price: number; + options: Record | null; + depth: number; + sort_order: number; + children?: ApiOrderNode[]; + items?: ApiOrderItem[]; +} + interface ApiClient { id: number; name: string; @@ -248,6 +268,7 @@ export interface Order { remarks?: string; note?: string; items?: OrderItem[]; + nodes?: OrderNode[]; // 목록 페이지용 추가 필드 productName?: string; // 제품명 (첫 번째 품목명) receiverAddress?: string; // 수신주소 @@ -278,6 +299,23 @@ export interface Order { }>; } +export interface OrderNode { + id: number; + parentId: number | null; + nodeType: string; + code: string; + name: string; + statusCode: string; + quantity: number; + unitPrice: number; + totalPrice: number; + options: Record | null; + depth: number; + sortOrder: number; + children: OrderNode[]; + items: OrderItem[]; +} + export interface OrderItem { id: string; itemId?: number; @@ -502,6 +540,7 @@ function transformApiToFrontend(apiData: ApiOrder): Order { remarks: apiData.remarks ?? undefined, note: apiData.note ?? undefined, items: apiData.items?.map(transformItemApiToFrontend) || [], + nodes: apiData.root_nodes?.map(transformNodeApiToFrontend) || [], // 목록 페이지용 추가 필드 productName: apiData.items?.[0]?.item_name ?? undefined, receiverAddress: apiData.options?.shipping_address ?? undefined, @@ -534,6 +573,25 @@ function transformApiToFrontend(apiData: ApiOrder): Order { }; } +function transformNodeApiToFrontend(apiNode: ApiOrderNode): OrderNode { + return { + id: apiNode.id, + parentId: apiNode.parent_id, + nodeType: apiNode.node_type, + code: apiNode.code, + name: apiNode.name, + statusCode: apiNode.status_code, + quantity: apiNode.quantity, + unitPrice: apiNode.unit_price, + totalPrice: apiNode.total_price, + options: apiNode.options, + depth: apiNode.depth, + sortOrder: apiNode.sort_order, + children: (apiNode.children || []).map(transformNodeApiToFrontend), + items: (apiNode.items || []).map(transformItemApiToFrontend), + }; +} + function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem { return { id: String(apiItem.id), diff --git a/src/components/orders/index.ts b/src/components/orders/index.ts index fa65a67a..da0f964e 100644 --- a/src/components/orders/index.ts +++ b/src/components/orders/index.ts @@ -21,6 +21,7 @@ export { type OrderFormData as OrderApiFormData, type OrderItemFormData, type OrderStats, + type OrderNode, type OrderStatus, type QuotationForSelect, type QuotationItem,