Files
wace_rps/frontend/components/sales/OrderFormViewDialog.tsx
T
hjjeong 2d9f30ebab 영업관리 첨부파일 모달·주문서 뷰 + DataGrid frozen 컬럼
- 공통 AttachmentDialog 컴포넌트 신설 (목록/업로드/다운로드/삭제, doc_type 단일·배열 지원)
- 견적 add_est_cnt(클립) → AttachmentDialog (estimate02)
- 주문 cu01_cnt(클립) → AttachmentDialog (FTC_ORDER,ORDER), has_order_data(폴더) → OrderFormViewDialog
- OrderFormViewDialog 신설 — wace orderFormView 한국 표준 주문서 양식 + 인쇄
- 백엔드 fileController.getFileList docType 콤마 멀티값 지원, salesOrderMgmt.getOrderFormView API 추가
- DataGrid 확장: column.onClick / frozen prop / table-fixed / sticky-left + selected·hover 일관 처리
- 4개 메뉴 첫 컬럼 frozen 적용 (영업번호/프로젝트번호)
- 주문서관리 발주일·발주번호·요청납기 컬럼 너비 확장

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:48:13 +09:00

348 lines
16 KiB
TypeScript

"use client";
/**
* OrderFormViewDialog — 주문서 자동생성 뷰 (wace orderFormView.jsp 대응)
*
* 주문관리 그리드 "주문서" 폴더 컬럼 클릭 시 표시.
* 한국 표준 주문서 양식 (공급받는자/공급자/품목/합계).
*/
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Printer } from "lucide-react";
import { salesOrderMgmtApi } from "@/lib/api/salesOrderMgmt";
import { toast } from "sonner";
// 공급자(우리 회사) 정보 — wace 원본 하드코딩값. 추후 회사 마스터 테이블로 이전.
const SUPPLIER = {
busRegNo: "314-81-75146",
name: "주식회사알피에스본사",
ceo: "이동헌",
address: "대전광역시 유성구 국제과학10로 8(둔곡동)",
busType: "제조업",
busItem: "금속절삭가공기계,반도체제조용기계",
};
interface OrderFormInfo {
objid: string;
contract_no: string;
po_no: string;
order_date: string;
client_nm?: string;
client_bus_reg_no?: string;
client_ceo_nm?: string;
client_addr?: string;
client_bus_type?: string;
client_bus_item?: string;
client_tel_no?: string;
client_fax_no?: string;
client_email?: string;
writer_name?: string;
writer_contact?: string;
order_supply_price?: string | number;
order_vat?: string | number;
order_total_amount?: string | number;
vat_note?: string;
reg_datetime?: string;
}
interface OrderFormItem {
seq: number;
part_no?: string;
part_name?: string;
spec?: string;
unit_name?: string;
due_date?: string;
order_quantity?: string | number;
order_unit_price?: string | number;
order_supply_price?: string | number;
order_vat?: string | number;
order_total_amount?: string | number;
}
export interface OrderFormViewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
objid: string | null | undefined;
}
export function OrderFormViewDialog({ open, onOpenChange, objid }: OrderFormViewDialogProps) {
const [info, setInfo] = useState<OrderFormInfo | null>(null);
const [items, setItems] = useState<OrderFormItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!open || !objid) {
setInfo(null);
setItems([]);
return;
}
setLoading(true);
salesOrderMgmtApi
.formView(String(objid))
.then((data) => {
setInfo(data.info);
setItems(data.items ?? []);
})
.catch((e: any) => {
toast.error("주문서 데이터 조회 실패: " + (e?.message ?? ""));
setInfo(null);
setItems([]);
})
.finally(() => setLoading(false));
}, [open, objid]);
const orderDateText = formatOrderDate(info?.order_date);
const totalQty = items.reduce((acc, it) => acc + toNum(it.order_quantity), 0);
const totalSupply = items.reduce((acc, it) => acc + toNum(it.order_supply_price), 0);
function handlePrint() {
const node = document.getElementById("order-form-print-area");
if (!node) return;
const w = window.open("", "_blank", "width=950,height=800");
if (!w) return;
w.document.write(`
<html><head><title>주문서</title>
<style>
body{font-family:'맑은 고딕',sans-serif;margin:18px;color:#000;font-size:12px;}
.order-title{text-align:center;font-size:24px;font-weight:bold;letter-spacing:18px;margin:8px 0 14px;}
.header-row{font-size:11px;margin-bottom:2px;}
table{border-collapse:collapse;width:100%;}
td,th{border:1px solid #000;padding:3px 5px;}
.lbl{background:#f3f3f3;text-align:center;font-weight:bold;}
.vl{background:#e8e8e8;text-align:center;font-weight:bold;font-size:13px;}
.tc{text-align:center;}
.tr{text-align:right;}
.item-tbl thead th{background:#fff8c5;font-weight:bold;text-align:center;}
.total-row td{background:#ffffcc;font-weight:bold;text-align:center;letter-spacing:8px;}
</style></head><body>${node.innerHTML}</body></html>
`);
w.document.close();
w.focus();
setTimeout(() => { w.print(); }, 250);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> {info?.contract_no ?? ""}</DialogTitle>
</DialogHeader>
{loading && (
<div className="text-center text-muted-foreground py-10"> </div>
)}
{!loading && !info && (
<div className="text-center text-muted-foreground py-10"> .</div>
)}
{!loading && info && (
<div id="order-form-print-area" className="text-[11px] text-black bg-white p-3">
<div className="text-center text-2xl font-bold tracking-[18px] my-2"> </div>
<div className="text-[11px] mb-0.5"> : {orderDateText}</div>
<div className="text-[11px] mb-1"> : {info.po_no ?? ""}</div>
{/* 공급받는자 / 공급자 */}
<table className="w-full border-collapse text-[11px] mt-1">
<colgroup>
<col style={{ width: "28px" }} />
<col style={{ width: "62px" }} />
<col />
<col style={{ width: "45px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "28px" }} />
<col style={{ width: "62px" }} />
<col />
<col style={{ width: "45px" }} />
<col style={{ width: "90px" }} />
</colgroup>
<tbody>
<tr>
<td rowSpan={4} className="vl border border-black bg-gray-200 text-center font-bold" style={{ writingMode: "vertical-rl" as any }}></td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td colSpan={3} className="border border-black px-1.5">{info.client_bus_reg_no ?? ""}</td>
<td rowSpan={4} className="vl border border-black bg-gray-200 text-center font-bold" style={{ writingMode: "vertical-rl" as any }}> </td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td colSpan={3} className="border border-black px-1.5">{SUPPLIER.busRegNo}</td>
</tr>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{info.client_nm ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td className="border border-black px-1.5">{info.client_ceo_nm ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{SUPPLIER.name}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td className="border border-black px-1.5">{SUPPLIER.ceo}</td>
</tr>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td colSpan={3} className="border border-black px-1.5">{info.client_addr ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td colSpan={3} className="border border-black px-1.5">{SUPPLIER.address}</td>
</tr>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{info.client_bus_type ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td className="border border-black px-1.5">{info.client_bus_item ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{SUPPLIER.busType}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td className="border border-black px-1.5 text-[9px]">{SUPPLIER.busItem}</td>
</tr>
</tbody>
</table>
{/* 납품처 / 담당자 */}
<table className="w-full border-collapse text-[11px] -mt-px">
<colgroup>
<col style={{ width: "70px" }} />
<col />
<col style={{ width: "70px" }} />
<col style={{ width: "130px" }} />
<col style={{ width: "70px" }} />
<col style={{ width: "130px" }} />
</colgroup>
<tbody>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{info.client_nm ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td className="border border-black px-1.5">{info.client_tel_no ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"></td>
<td className="border border-black px-1.5">{info.client_fax_no ?? ""}</td>
</tr>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{info.client_addr ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold"> </td>
<td className="border border-black px-1.5">{info.writer_name ?? ""}</td>
<td className="lbl border border-black bg-gray-100 text-center font-bold">C.P.</td>
<td className="border border-black px-1.5">{info.writer_contact ?? ""}</td>
</tr>
</tbody>
</table>
{/* 품목 테이블 */}
<table className="item-tbl w-full border-collapse text-[11px] -mt-px">
<colgroup>
<col style={{ width: "32px" }} />
<col style={{ width: "85px" }} />
<col />
<col />
<col style={{ width: "38px" }} />
<col style={{ width: "78px" }} />
<col style={{ width: "48px" }} />
<col style={{ width: "68px" }} />
<col style={{ width: "78px" }} />
</colgroup>
<thead>
<tr>
{["No.", "품번", "품명", "규격", "단위", "납기일", "수량", "단가", "금액"].map((h) => (
<th key={h} className="border border-black bg-yellow-50 font-bold text-center px-1.5 py-1">{h}</th>
))}
</tr>
</thead>
<tbody>
{items.length === 0 && (
<tr><td colSpan={9} className="border border-black text-center py-3 text-muted-foreground"> </td></tr>
)}
{items.map((it, i) => (
<tr key={i}>
<td className="border border-black tc text-center px-1">{i + 1}</td>
<td className="border border-black tc text-center px-1">{it.part_no ?? ""}</td>
<td className="border border-black px-1.5">{it.part_name ?? ""}</td>
<td className="border border-black px-1.5">{it.spec ?? ""}</td>
<td className="border border-black tc text-center px-1">{it.unit_name ?? ""}</td>
<td className="border border-black tc text-center px-1">{it.due_date ?? ""}</td>
<td className="border border-black tr text-right px-1.5">{fmt(it.order_quantity)}</td>
<td className="border border-black tr text-right px-1.5">{fmt(it.order_unit_price)}</td>
<td className="border border-black tr text-right px-1.5">{fmt(it.order_supply_price)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="total-row">
<td colSpan={9} className="border border-black bg-yellow-100 text-center font-bold tracking-[8px] py-1"> </td>
</tr>
<tr>
<td colSpan={6} className="border border-black tc"></td>
<td className="border border-black tr text-right px-1.5 font-bold">{fmt(totalQty)}</td>
<td className="border border-black tr"></td>
<td className="border border-black tr text-right px-1.5 font-bold">{fmt(totalSupply)}</td>
</tr>
</tfoot>
</table>
{/* 비고 / 합계 요약 */}
<table className="w-full border-collapse text-[11px] -mt-px">
<colgroup>
<col style={{ width: "40px" }} />
<col />
<col style={{ width: "120px" }} />
<col style={{ width: "150px" }} />
</colgroup>
<tbody>
<tr>
<td rowSpan={3} className="vl border border-black bg-gray-200 text-center font-bold tracking-[8px] text-[13px]" style={{ writingMode: "vertical-rl" as any }}> </td>
<td rowSpan={3} className="border border-black"></td>
<td className="lbl border border-black bg-gray-100 text-center font-bold tracking-[2px]"> </td>
<td className="border border-black tr text-right px-1.5">{fmt(info.order_supply_price)}</td>
</tr>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold tracking-[2px]"> </td>
<td className="border border-black tr text-right px-1.5">{fmt(info.order_vat)}</td>
</tr>
<tr>
<td className="lbl border border-black bg-gray-100 text-center font-bold tracking-[5px]"> </td>
<td className="border border-black tr text-right px-1.5 font-bold">{fmt(info.order_total_amount)}</td>
</tr>
</tbody>
</table>
<div className="flex justify-between border border-black border-t-0 px-1.5 py-1 text-[11px]">
<span>{info.vat_note ?? ""}</span>
<span></span>
</div>
</div>
)}
<DialogFooter className="flex sm:justify-between gap-2 print:hidden">
<Button variant="outline" size="sm" onClick={handlePrint} disabled={!info}>
<Printer className="h-4 w-4 mr-1" />
</Button>
<Button variant="ghost" onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function toNum(v: any): number {
if (v == null || v === "") return 0;
const n = Number(String(v).replace(/,/g, ""));
return isNaN(n) ? 0 : n;
}
function fmt(v: any): string {
const n = toNum(v);
if (n === 0) return "";
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatOrderDate(s?: string): string {
if (!s) return "";
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!m) return s;
return `${m[1]}${m[2]}${m[3]}`;
}