2d9f30ebab
- 공통 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>
348 lines
16 KiB
TypeScript
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]}일`;
|
|
}
|