17b08c7a09
- backend purchaseOrderMailService 신설 — getOrderMailInfo / getPartnerManagerList / sendOrderMail (SMTP PURCHASE, 발송 성공 시 mail_send_yn='Y'/mail_send_date 갱신) - backend routes — GET /order-form/mail-info/:objid, POST /order-form/mail, GET /options/partner-managers/:partnerObjid - frontend lib/utils/purchaseOrderPdf — html2canvas-pro + jsPDF (A4, scale=2, input/textarea → 텍스트 변환). download:true 면 파일 저장, 아니면 base64 반환 - PurchaseOrderMailDialog 신설 — EstimateMailDialog 패턴 단순화 (한글/영문 본문 분기, 공급업체 단일 email 자동 채움) - 3개 양식 다이얼로그 — 읽기전용 + 저장된 발주서일 때 "메일 발송" + "PDF 다운로드" 버튼 노출. window.print 간이판 제거 - 3개 양식 다이얼로그 — "행 추가"/"선택 행 삭제" 버튼 + 그리드 체크박스 컬럼 제거 (wace 운영판은 모두 주석 처리/부재. 발주서는 품의서에서 자동 채움된 품목 그대로 사용)
616 lines
28 KiB
TypeScript
616 lines
28 KiB
TypeScript
"use client";
|
||
|
||
// 구매관리 > 발주서관리 — general 양식 등록/수정 다이얼로그 (PDF 양식 1:1)
|
||
// wace 원본: purchaseOrder/purchaseOrderFormPopup_general.jsp (운영판 폼)
|
||
// - 좌 박스 5필드: 발주번호 / 발주일자 / 수신업체 / 합계금액(VAT별도) / 결제방식
|
||
// (납기일·납품장소는 운영판 주석 처리됨 — 그대로 미노출)
|
||
// - 우 박스: "담당자" 라벨 + 담당자1 + 담당자1 연락처/이메일 + 담당자2 + 담당자2 연락처/이메일
|
||
// + "㈜알피에스 대표이사 이 동 헌" + "대전광역시 유성구 국제과학10로8(둔곡동 402-4번지)"
|
||
// - 저장/닫기 버튼: 폼 박스 우측 상단
|
||
// - 그리드 컬럼: ☑ / No / 품명 / 규격 / 수량 / 단위 / 배송지 / 단가 / 공급가액 / 비고 / 입고요청일
|
||
// - 푸터: 총 공급 가액 (VAT별도) + 보안 문구 (빨간색)
|
||
//
|
||
// 저장: purchaseApi.saveOrderForm({ master, parts, deletedPartObjids })
|
||
// - SUPPLY_UNIT_PRICE = ORDER_QTY × PARTNER_PRICE (행 자동 계산)
|
||
// - TOTAL_SUPPLY_PRICE = Σ SUPPLY_UNIT_PRICE (마스터 자동 합산)
|
||
|
||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Download, Mail } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
|
||
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
|
||
import { DateInput } from "@/components/common/DateInput";
|
||
import { NumberInput } from "@/components/common/NumberInput";
|
||
import { purchaseApi, OptionItem } from "@/lib/api/purchase";
|
||
import { generatePurchaseOrderPdf } from "@/lib/utils/purchaseOrderPdf";
|
||
import { PurchaseOrderMailDialog } from "./PurchaseOrderMailDialog";
|
||
|
||
interface Props {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onSaved?: (result: { objid: string; purchase_order_no: string }) => void;
|
||
/** 수정 모드 시 POM OBJID — 미지정이고 proposalObjid 있으면 신규 (품의서 자동채움) */
|
||
pomObjid?: string;
|
||
/** 신규 등록 시 발주 원천 품의서 OBJID */
|
||
proposalObjid?: string;
|
||
}
|
||
|
||
interface PartRow {
|
||
rowKey: string;
|
||
objid: string;
|
||
part_objid: string;
|
||
part_no: string;
|
||
part_name: string;
|
||
spec: string;
|
||
order_qty: number | "";
|
||
qty: number | "";
|
||
unit: string;
|
||
part_delivery_place: string;
|
||
partner_price: number | "";
|
||
supply_unit_price: number;
|
||
remark: string;
|
||
delivery_request_date: string;
|
||
currency?: string;
|
||
_src?: string;
|
||
}
|
||
|
||
interface MasterState {
|
||
objid: string;
|
||
purchase_order_no: string;
|
||
purchase_date: string;
|
||
partner_objid: string;
|
||
payment_terms: string;
|
||
sales_mng_user_id: string;
|
||
manager_name: string;
|
||
manager_position: string;
|
||
manager_phone: string;
|
||
manager_email: string;
|
||
sales_mng_user_id2: string;
|
||
manager_name2: string;
|
||
manager_position2: string;
|
||
manager_phone2: string;
|
||
manager_email2: string;
|
||
title: string;
|
||
request_content: string;
|
||
sales_request_objid: string;
|
||
contract_mgmt_objid: string;
|
||
form_type: string;
|
||
status: string;
|
||
/** 결재 상태 — wace isModify 분기에 사용 ('결재중'/'결재완료' 시 읽기전용) */
|
||
appr_status: string;
|
||
}
|
||
|
||
const EMPTY_MASTER: MasterState = {
|
||
objid: "", purchase_order_no: "", purchase_date: "",
|
||
partner_objid: "", payment_terms: "",
|
||
sales_mng_user_id: "", manager_name: "", manager_position: "",
|
||
manager_phone: "", manager_email: "",
|
||
sales_mng_user_id2: "", manager_name2: "", manager_position2: "",
|
||
manager_phone2: "", manager_email2: "",
|
||
title: "", request_content: "",
|
||
sales_request_objid: "", contract_mgmt_objid: "",
|
||
form_type: "general", status: "create",
|
||
appr_status: "",
|
||
};
|
||
|
||
const UNIT_GROUP_ID = "0001399"; // wace unit_cd (0001400=EA)
|
||
const DELIVERY_PLACE_GROUP = "0001146"; // wace delivery_place_cd
|
||
const PAYMENT_TERMS_GROUP = "0001074"; // wace payment_terms_cd
|
||
|
||
let _rk = 0;
|
||
const nextKey = () => `r${++_rk}_${Date.now()}`;
|
||
|
||
const toNum = (v: any): number => {
|
||
if (v == null || v === "") return 0;
|
||
const n = Number(String(v).replace(/,/g, ""));
|
||
return Number.isFinite(n) ? n : 0;
|
||
};
|
||
const fmt = (n: number) => n.toLocaleString("ko-KR");
|
||
|
||
interface UserOptionExt extends OptionItem {
|
||
name?: string;
|
||
position?: string;
|
||
phone?: string;
|
||
email?: string;
|
||
}
|
||
|
||
export function PurchaseOrderGeneralFormDialog({
|
||
open, onClose, onSaved, pomObjid, proposalObjid,
|
||
}: Props) {
|
||
const isEdit = !!pomObjid;
|
||
const [master, setMaster] = useState<MasterState>(EMPTY_MASTER);
|
||
const [parts, setParts] = useState<PartRow[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
const [vendorOpts, setVendorOpts] = useState<SmartSelectOption[]>([]);
|
||
const [userOpts, setUserOpts] = useState<UserOptionExt[]>([]);
|
||
const userOptsForSelect: SmartSelectOption[] = useMemo(
|
||
() => userOpts.map((u) => ({ code: u.code, label: u.label })),
|
||
[userOpts],
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
setMaster(EMPTY_MASTER);
|
||
setParts([]);
|
||
|
||
(async () => {
|
||
try {
|
||
const [vs, us] = await Promise.all([
|
||
purchaseApi.listVendors(),
|
||
purchaseApi.listUsers(),
|
||
]);
|
||
setVendorOpts(vs.map((v) => ({ code: v.code, label: v.label })));
|
||
setUserOpts(us as UserOptionExt[]);
|
||
} catch {/* skip */}
|
||
})();
|
||
|
||
setLoading(true);
|
||
(async () => {
|
||
try {
|
||
if (isEdit && pomObjid) {
|
||
const r = await purchaseApi.getOrderForm(pomObjid);
|
||
applyServerData(r.master ?? {}, r.parts ?? []);
|
||
} else if (proposalObjid) {
|
||
const r = await purchaseApi.initOrderForm(proposalObjid);
|
||
applyServerData(r.master ?? {}, r.parts ?? []);
|
||
}
|
||
} catch (e: any) {
|
||
toast.error(e?.response?.data?.message ?? e?.message ?? "초기 로드 실패");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
})();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [open, isEdit, pomObjid, proposalObjid]);
|
||
|
||
const applyServerData = (m: Record<string, any>, ps: Record<string, any>[]) => {
|
||
setMaster({
|
||
objid: String(m.objid ?? ""),
|
||
purchase_order_no: String(m.purchase_order_no ?? ""),
|
||
purchase_date: String(m.purchase_date ?? ""),
|
||
partner_objid: String(m.partner_objid ?? ""),
|
||
payment_terms: String(m.payment_terms ?? ""),
|
||
sales_mng_user_id: String(m.sales_mng_user_id ?? ""),
|
||
manager_name: String(m.manager_name ?? ""),
|
||
manager_position: String(m.manager_position ?? ""),
|
||
manager_phone: String(m.manager_phone ?? ""),
|
||
manager_email: String(m.manager_email ?? ""),
|
||
sales_mng_user_id2: String(m.sales_mng_user_id2 ?? ""),
|
||
manager_name2: String(m.manager_name2 ?? ""),
|
||
manager_position2: String(m.manager_position2 ?? ""),
|
||
manager_phone2: String(m.manager_phone2 ?? ""),
|
||
manager_email2: String(m.manager_email2 ?? ""),
|
||
title: String(m.title ?? ""),
|
||
request_content: String(m.request_content ?? ""),
|
||
sales_request_objid: String(m.sales_request_objid ?? m.proposal_objid ?? ""),
|
||
contract_mgmt_objid: String(m.contract_mgmt_objid ?? ""),
|
||
form_type: String(m.form_type ?? "general"),
|
||
status: String(m.status ?? "create"),
|
||
appr_status: String(m.appr_status ?? ""),
|
||
});
|
||
setParts(ps.map((p) => ({
|
||
rowKey: nextKey(),
|
||
objid: String(p.objid ?? ""),
|
||
part_objid: String(p.part_objid ?? ""),
|
||
part_no: String(p.part_no ?? ""),
|
||
part_name: String(p.part_name ?? ""),
|
||
spec: String(p.spec ?? ""),
|
||
order_qty: p.order_qty === "" || p.order_qty == null ? "" : Number(p.order_qty),
|
||
qty: p.qty === "" || p.qty == null
|
||
? (p.order_qty === "" || p.order_qty == null ? "" : Number(p.order_qty))
|
||
: Number(p.qty),
|
||
unit: String(p.unit || "0001400"),
|
||
part_delivery_place: String(p.part_delivery_place || "RPS"),
|
||
partner_price: p.partner_price === "" || p.partner_price == null ? "" : Number(p.partner_price),
|
||
supply_unit_price: toNum(p.supply_unit_price ?? toNum(p.order_qty) * toNum(p.partner_price)),
|
||
remark: String(p.remark ?? ""),
|
||
delivery_request_date: String(p.delivery_request_date ?? ""),
|
||
currency: String(p.currency ?? ""),
|
||
_src: p._src,
|
||
})));
|
||
};
|
||
|
||
const totalSupplyPrice = useMemo(
|
||
() => parts.reduce((sum, p) => sum + toNum(p.supply_unit_price), 0),
|
||
[parts],
|
||
);
|
||
|
||
// 읽기전용 모드 — wace _general.jsp isModify 분기 1:1
|
||
// APPR_STATUS='결재중' / '결재완료' / STATUS='cancel' 일 때 입력 잠금
|
||
const isReadOnly = useMemo(() => {
|
||
const a = master.appr_status;
|
||
return a === "결재중" || a === "결재완료" || master.status === "cancel";
|
||
}, [master.appr_status, master.status]);
|
||
|
||
const pdfContainerRef = useRef<HTMLDivElement>(null);
|
||
const [mailOpen, setMailOpen] = useState(false);
|
||
const [generatingPdf, setGeneratingPdf] = useState(false);
|
||
|
||
/** PDF 다운로드 — html2canvas + jsPDF 로 발주서 양식 컨테이너 캡처 */
|
||
const handleDownload = async () => {
|
||
if (!pdfContainerRef.current) return;
|
||
setGeneratingPdf(true);
|
||
try {
|
||
const filename = `${master.purchase_order_no || "purchase_order"}.pdf`;
|
||
await generatePurchaseOrderPdf(pdfContainerRef.current, { download: true, filename });
|
||
} catch (e: any) {
|
||
toast.error("PDF 생성 실패: " + (e?.message ?? ""));
|
||
} finally {
|
||
setGeneratingPdf(false);
|
||
}
|
||
};
|
||
|
||
/** 메일 발송 — 다이얼로그가 onRequestPdf 콜백으로 PDF 생성 요청 */
|
||
const handleRequestPdf = async (): Promise<string> => {
|
||
if (!pdfContainerRef.current) throw new Error("발주서 컨테이너를 찾을 수 없습니다");
|
||
return generatePurchaseOrderPdf(pdfContainerRef.current);
|
||
};
|
||
|
||
// 담당자 select 변경 시 hidden 필드(name/position/phone/email) 자동 채움
|
||
const onManagerChange = (slot: 1 | 2, userId: string) => {
|
||
const u = userOpts.find((o) => o.code === userId);
|
||
setMaster((prev) => ({
|
||
...prev,
|
||
[`sales_mng_user_id${slot === 2 ? "2" : ""}`]: userId,
|
||
[`manager_name${slot === 2 ? "2" : ""}`]: u?.name ?? u?.label ?? "",
|
||
[`manager_position${slot === 2 ? "2" : ""}`]: u?.position ?? "",
|
||
[`manager_phone${slot === 2 ? "2" : ""}`]: u?.phone ?? "",
|
||
[`manager_email${slot === 2 ? "2" : ""}`]: u?.email ?? "",
|
||
}));
|
||
};
|
||
|
||
const updateRow = (rowKey: string, patch: Partial<PartRow>) => {
|
||
setParts((prev) => prev.map((r) => {
|
||
if (r.rowKey !== rowKey) return r;
|
||
const merged: PartRow = { ...r, ...patch };
|
||
const q = toNum(merged.order_qty);
|
||
const u = toNum(merged.partner_price);
|
||
merged.qty = merged.order_qty;
|
||
merged.supply_unit_price = q * u;
|
||
return merged;
|
||
}));
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
if (!master.partner_objid) { toast.warning("수신업체를 선택하세요"); return; }
|
||
if (parts.length === 0) { toast.warning("발주 품목이 없습니다"); return; }
|
||
setSaving(true);
|
||
try {
|
||
const payload = {
|
||
master: {
|
||
...master,
|
||
total_supply_price: String(totalSupplyPrice),
|
||
total_supply_unit_price: String(totalSupplyPrice),
|
||
total_price: String(totalSupplyPrice),
|
||
},
|
||
parts: parts.map((p) => ({
|
||
objid: p.objid,
|
||
part_objid: p.part_objid,
|
||
part_no: p.part_no,
|
||
part_name: p.part_name,
|
||
spec: p.spec,
|
||
order_qty: toNum(p.order_qty),
|
||
qty: toNum(p.qty || p.order_qty),
|
||
unit: p.unit,
|
||
part_delivery_place: p.part_delivery_place,
|
||
partner_price: toNum(p.partner_price),
|
||
supply_unit_price: toNum(p.supply_unit_price),
|
||
remark: p.remark,
|
||
delivery_request_date: p.delivery_request_date,
|
||
currency: p.currency,
|
||
})),
|
||
deletedPartObjids: [],
|
||
};
|
||
const res = await purchaseApi.saveOrderForm(payload);
|
||
toast.success(`저장 완료 (${res.purchase_order_no})`);
|
||
onSaved?.(res);
|
||
onClose();
|
||
} catch (e: any) {
|
||
toast.error(e?.response?.data?.message ?? e?.message ?? "저장 실패");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
// ───────── 운영판 PDF 양식 1:1 (테두리 박스 + 좌/우 분할) ─────────
|
||
return (
|
||
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
|
||
<DialogContent className="max-w-[1280px] w-[96vw] max-h-[94vh] overflow-hidden flex flex-col p-0 gap-0 bg-white">
|
||
{/* Radix UI 접근성 — 시각 노출 없이 타이틀/설명 제공 */}
|
||
<DialogTitle className="sr-only">발주서 (일반)</DialogTitle>
|
||
<DialogDescription className="sr-only">wace 운영판 일반 발주서 PDF 양식</DialogDescription>
|
||
<div ref={pdfContainerRef} className="flex-1 overflow-y-auto p-4 text-[12px]" style={{ fontFamily: "'Malgun Gothic', '맑은 고딕', sans-serif" }}>
|
||
{/* ── 상단 메인 박스 (PDF 양식) ── */}
|
||
<div className="border-2 border-black">
|
||
{/* 1행: 로고 + 타이틀 */}
|
||
<div className="flex items-center border-b-2 border-black">
|
||
<div className="w-[140px] text-center py-2">
|
||
{/* wace `<%=request.getContextPath()%>/images/logo.png` 1:1 */}
|
||
<img src="/images/rps-logo-on.png" alt="RPS Logo"
|
||
style={{ maxWidth: "100px", height: "auto" }} />
|
||
</div>
|
||
<div className="flex-1 text-center py-3 text-[28px] font-bold" style={{ letterSpacing: "20px" }}>
|
||
발 주 서
|
||
</div>
|
||
<div className="w-[140px]"></div>
|
||
</div>
|
||
|
||
{/* 2행: 좌(기본정보) + 우(담당자) */}
|
||
<div className="flex">
|
||
{/* 좌: 기본정보 5필드 */}
|
||
<div className="w-[42%] p-3 border-r border-black">
|
||
<table className="w-full border-collapse">
|
||
<tbody>
|
||
<tr>
|
||
<td className="py-1.5 font-semibold whitespace-nowrap pr-3 w-[160px]">1. 발 주 번 호 :</td>
|
||
<td className="py-1.5 font-bold">
|
||
<span>{master.purchase_order_no || <span className="text-gray-400">자동생성</span>}</span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">2. 발 주 일 자 :</td>
|
||
<td className="py-1.5">
|
||
<div className="w-[210px]">
|
||
<DateInput value={master.purchase_date}
|
||
onChange={(v) => setMaster({ ...master, purchase_date: v })}
|
||
disabled={isReadOnly} />
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">3. 수 신 업 체 :</td>
|
||
<td className="py-1.5">
|
||
<div className="w-[260px]">
|
||
<SmartSelect options={vendorOpts}
|
||
value={master.partner_objid}
|
||
onValueChange={(v) => setMaster({ ...master, partner_objid: v })}
|
||
disabled={isReadOnly} />
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">4. 합 계 금 액(VAT별도) :</td>
|
||
<td className="py-1.5 font-bold">
|
||
{fmt(totalSupplyPrice)} <span className="font-normal">원</span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="py-1.5 font-semibold whitespace-nowrap pr-3">5. 결 제 방 식 :</td>
|
||
<td className="py-1.5">
|
||
<div className="w-[260px]">
|
||
<CommCodeSelect groupId={PAYMENT_TERMS_GROUP}
|
||
value={master.payment_terms}
|
||
onValueChange={(v) => setMaster({ ...master, payment_terms: v })}
|
||
withAll={false}
|
||
disabled={isReadOnly} />
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* 우: 담당자 박스 + 회사정보 */}
|
||
<div className="flex-1 p-3">
|
||
<table className="w-full border-collapse border border-black text-[11px]">
|
||
<tbody>
|
||
<tr>
|
||
<td rowSpan={4} className="w-[60px] text-center font-bold border border-black align-middle bg-gray-100"
|
||
style={{ letterSpacing: "3px" }}>
|
||
담<br/>당<br/>자
|
||
</td>
|
||
<td className="border border-black px-2 py-1 text-center">
|
||
<div className="w-full max-w-[220px] mx-auto">
|
||
<SmartSelect options={userOptsForSelect}
|
||
value={master.sales_mng_user_id}
|
||
onValueChange={(v) => onManagerChange(1, v)}
|
||
disabled={isReadOnly} />
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border border-black px-2 py-1 text-center relative">
|
||
<span>(<span>{master.manager_phone || "-"}</span> / <span>{master.manager_email || "-"}</span>)</span>
|
||
{/* 직인 — wace `images/stamp_rps.png` (운영 파일은 stamp_seal.png) onerror hide */}
|
||
<img src="/images/rps-stamp-seal.png" alt="직인"
|
||
className="absolute"
|
||
style={{ right: 5, top: -10, width: 45, height: 45, opacity: 0.9 }}
|
||
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none"; }} />
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border border-black px-2 py-1 text-center">
|
||
<div className="w-full max-w-[220px] mx-auto">
|
||
<SmartSelect options={userOptsForSelect}
|
||
value={master.sales_mng_user_id2}
|
||
onValueChange={(v) => onManagerChange(2, v)}
|
||
disabled={isReadOnly} />
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border border-black px-2 py-1 text-center">
|
||
<span>(<span>{master.manager_phone2 || "-"}</span> / <span>{master.manager_email2 || "-"}</span>)</span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={2} className="border border-black border-b-0 px-2 py-1.5 text-center text-[14px] font-bold">
|
||
㈜알피에스 대표이사 이 동 헌
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={2} className="border border-black border-t-0 px-2 py-1.5 text-center text-[11px] font-bold">
|
||
대전광역시 유성구 국제과학10로8(둔곡동 402-4번지)
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 읽기전용 안내 — wace isModify=false 분기 */}
|
||
{isReadOnly && (
|
||
<div className="mt-2 text-[11px] text-amber-700 bg-amber-50 border border-amber-300 px-3 py-1.5 rounded">
|
||
결재 진행/완료 또는 취소 상태의 발주서는 수정할 수 없습니다.
|
||
{master.appr_status ? ` (결재상태: ${master.appr_status})` : ""}
|
||
{master.status === "cancel" ? " (취소됨)" : ""}
|
||
</div>
|
||
)}
|
||
|
||
{/* 버튼 영역 (그리드 위, 우측 정렬) — wace _general.jsp 941-948 1:1 */}
|
||
<div className="flex justify-end gap-2 my-3">
|
||
{!isReadOnly && (
|
||
<Button size="sm" className="h-8 px-5 text-[13px]"
|
||
style={{ background: "#dfeffc", color: "#000" }}
|
||
onClick={handleSave} disabled={saving || loading}>
|
||
{saving ? "저장 중..." : "저장"}
|
||
</Button>
|
||
)}
|
||
{isReadOnly && master.objid && (
|
||
<>
|
||
<Button size="sm" variant="outline" className="h-8 gap-1 px-3 text-xs"
|
||
onClick={() => setMailOpen(true)}>
|
||
<Mail className="h-3.5 w-3.5" /> 메일 발송
|
||
</Button>
|
||
<Button size="sm" className="h-8 gap-1 px-3 text-[13px]"
|
||
style={{ background: "#dfeffc", color: "#000" }}
|
||
onClick={handleDownload} disabled={generatingPdf}>
|
||
<Download className="h-3.5 w-3.5" />
|
||
{generatingPdf ? "생성 중..." : "PDF 다운로드"}
|
||
</Button>
|
||
</>
|
||
)}
|
||
<Button size="sm" variant="outline" className="h-8 px-5 text-[13px]"
|
||
style={{ background: "#dfeffc" }}
|
||
onClick={onClose} disabled={saving}>
|
||
닫기
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 그리드 */}
|
||
<div className="border border-black overflow-x-auto">
|
||
<table className="w-full text-[11px] border-collapse">
|
||
<thead>
|
||
<tr className="bg-[#f0f4fa]">
|
||
<th className="w-10 border border-black px-1 py-1.5">No</th>
|
||
<th className="min-w-[140px] border border-black px-1 py-1.5">품명</th>
|
||
<th className="min-w-[120px] border border-black px-1 py-1.5">규격</th>
|
||
<th className="w-[70px] border border-black px-1 py-1.5">수량</th>
|
||
<th className="w-[80px] border border-black px-1 py-1.5">단위</th>
|
||
<th className="w-[110px] border border-black px-1 py-1.5">배송지</th>
|
||
<th className="w-[100px] border border-black px-1 py-1.5">단가</th>
|
||
<th className="w-[110px] border border-black px-1 py-1.5">공급가액</th>
|
||
<th className="min-w-[160px] border border-black px-1 py-1.5">비고</th>
|
||
<th className="w-[110px] border border-black px-1 py-1.5">입고요청일</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{parts.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={10} className="text-center py-8 border border-black text-gray-500">
|
||
품의서에서 진입하면 품목이 자동으로 채워집니다
|
||
</td>
|
||
</tr>
|
||
) : parts.map((row, i) => (
|
||
<tr key={row.rowKey} className="hover:bg-blue-50/30">
|
||
<td className="border border-black px-1 py-0.5 text-center text-gray-600">{i + 1}</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<Input className="h-7 text-[11px] px-1.5 border-0 focus-visible:ring-1"
|
||
value={row.part_name}
|
||
onChange={(e) => updateRow(row.rowKey, { part_name: e.target.value })}
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<Input className="h-7 text-[11px] px-1.5 border-0 focus-visible:ring-1"
|
||
value={row.spec}
|
||
onChange={(e) => updateRow(row.rowKey, { spec: e.target.value })}
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<NumberInput value={row.order_qty} decimals={0}
|
||
onChange={(v) => updateRow(row.rowKey, { order_qty: v })}
|
||
className="h-7 text-[11px]"
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<CommCodeSelect groupId={UNIT_GROUP_ID}
|
||
value={row.unit}
|
||
onValueChange={(v) => updateRow(row.rowKey, { unit: v })}
|
||
withAll={false} className="h-7"
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<CommCodeSelect groupId={DELIVERY_PLACE_GROUP}
|
||
value={row.part_delivery_place}
|
||
onValueChange={(v) => updateRow(row.rowKey, { part_delivery_place: v })}
|
||
withAll={false} className="h-7"
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<NumberInput value={row.partner_price} decimals={0}
|
||
onChange={(v) => updateRow(row.rowKey, { partner_price: v })}
|
||
className="h-7 text-[11px]"
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5 text-right tabular-nums pr-2">
|
||
{fmt(row.supply_unit_price)}
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<Input className="h-7 text-[11px] px-1.5 border-0 focus-visible:ring-1"
|
||
value={row.remark}
|
||
onChange={(e) => updateRow(row.rowKey, { remark: e.target.value })}
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
<td className="border border-black px-1 py-0.5">
|
||
<DateInput value={row.delivery_request_date}
|
||
onChange={(v) => updateRow(row.rowKey, { delivery_request_date: v })}
|
||
size="sm"
|
||
disabled={isReadOnly} />
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* 합계 영역 */}
|
||
<div className="flex justify-end mt-3">
|
||
<table className="border-collapse">
|
||
<tbody>
|
||
<tr>
|
||
<td className="bg-[#e8f0e1] border border-black px-6 py-2 text-[13px] font-bold whitespace-nowrap" style={{ letterSpacing: "2px" }}>
|
||
총 공 급 가 액 (VAT별도)
|
||
</td>
|
||
<td className="border border-black px-4 py-2 text-right font-bold text-[16px] min-w-[200px] tabular-nums">
|
||
{fmt(totalSupplyPrice)}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* 보안 문구 */}
|
||
<div className="text-right text-[11px] text-red-600 mt-3">
|
||
※ 보안문서(CONFIDENTIAL) : ㈜알피에스의 승인(APPROVAL) 없이 외부로 반출하거나 공유 할수 없습니다.
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
|
||
<PurchaseOrderMailDialog
|
||
open={mailOpen}
|
||
onOpenChange={setMailOpen}
|
||
pomObjid={master.objid || null}
|
||
formType="general"
|
||
onRequestPdf={handleRequestPdf}
|
||
onSent={() => setMailOpen(false)}
|
||
/>
|
||
</Dialog>
|
||
);
|
||
}
|