diff options
Diffstat (limited to 'lib/po/vendor-table/vendor-po-print-dialog.tsx')
| -rw-r--r-- | lib/po/vendor-table/vendor-po-print-dialog.tsx | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/lib/po/vendor-table/vendor-po-print-dialog.tsx b/lib/po/vendor-table/vendor-po-print-dialog.tsx new file mode 100644 index 00000000..f4e30798 --- /dev/null +++ b/lib/po/vendor-table/vendor-po-print-dialog.tsx @@ -0,0 +1,514 @@ +"use client" + +import * as React from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { PrinterIcon } from "lucide-react" +import { VendorPO, VendorPOItem } from "./types" +import { formatNumber } from "@/lib/utils" +import { getVendorPOById, getVendorPOItems } from "./service" + +interface VendorPOPrintDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + po: VendorPO | null +} + +// 발주서 헤더 컴포넌트 (페이지마다 반복) +function POHeader({ po }: { po: VendorPO }) { + return ( + <div> + {/* 로고 및 타이틀 */} + <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '10px' }}> + <tbody> + <tr> + <td style={{ width: '20.33%', textAlign: 'left', verticalAlign: 'middle' }}> + <div style={{ border: '1px solid #ccc', padding: '20px', textAlign: 'center', backgroundColor: '#f9f9f9', minHeight: '80px' }}> + {/* 구매처 로고 위치 */} + <span style={{ fontSize: '10px', color: '#999' }}>PURCHASER LOGO</span> + </div> + </td> + <td style={{ width: '55.33%', textAlign: 'center', verticalAlign: 'middle' }}> + <div style={{ fontSize: '24px', fontWeight: 'bold' }}> + 발주서 / PURCHASE ORDER + </div> + </td> + <td style={{ width: '20.33%', textAlign: 'right', verticalAlign: 'middle' }}> + <div style={{ border: '1px solid #ccc', padding: '20px', textAlign: 'center', backgroundColor: '#f9f9f9', minHeight: '80px' }}> + {/* 공급자 로고 위치 */} + <span style={{ fontSize: '10px', color: '#999' }}>SUPPLIER LOGO</span> + </div> + </td> + </tr> + </tbody> + </table> + + {/* 회사 정보 및 PO 정보 */} + <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '10px' }}> + <tbody> + <tr> + <td style={{ width: '60%', verticalAlign: 'top', paddingRight: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr><td style={{ padding: '3px 0', fontSize: '11px' }}>거제조선소 : 경상남도 거제시 장평3로 80 (장평동)</td></tr> + <tr><td style={{ padding: '3px 0', fontSize: '11px' }}>SHIP YARD : 80, Jangpyeong 3-ro, Geoje-si, Gyeongsangnam-do, 53261, Rep. of KOREA</td></tr> + <tr><td style={{ padding: '3px 0', fontSize: '11px' }}>FAX NO : +82-55-630-5768</td></tr> + <tr><td style={{ padding: '3px 0', fontSize: '11px' }}>TEL NO : +82-55-631-4434</td></tr> + </tbody> + </table> + </td> + <td style={{ width: '40%', verticalAlign: 'top' }}> + <table style={{ width: '100%', borderCollapse: 'collapse', border: '2px solid #000' }}> + <tbody> + <tr> + <td style={{ borderBottom: '1px solid #000', borderRight: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '40%' }}> + PO번호 + </td> + <td style={{ borderBottom: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '60%' }}> + {po.contractNo} + </td> + </tr> + <tr> + <td style={{ borderBottom: '1px solid #000', borderRight: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '40%' }}> + REV + </td> + <td style={{ borderBottom: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '60%' }}> + {po.poVersion ? String(po.poVersion).padStart(2, '0') : '00'} + </td> + </tr> + <tr> + <td style={{ borderBottom: '1px solid #000', borderRight: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '40%' }}> + PO발행일 + </td> + <td style={{ borderBottom: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '60%' }}> + {po.contractDate || '-'} + </td> + </tr> + <tr> + <td style={{ borderRight: '1px solid #000', padding: '5px', textAlign: 'left', fontSize: '11px', width: '40%' }}> + 구매담당 + </td> + <td style={{ padding: '5px', textAlign: 'left', fontSize: '11px', width: '60%' }}> + {po.purchaseManagerName || po.purchaseGroup || '-'} + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + + {/* 출력 정보 */} + <div style={{ textAlign: 'right', marginBottom: '5px', fontSize: '11px' }}> + <span>OUTPUT : {new Date().toLocaleString('ko-KR')}</span> + </div> + </div> + ) +} + +export function VendorPOPrintDialog({ + open, + onOpenChange, + po, +}: VendorPOPrintDialogProps) { + const [loading, setLoading] = React.useState(false) + const [poDetails, setPoDetails] = React.useState<VendorPO | null>(null) + const [poItems, setPoItems] = React.useState<VendorPOItem[]>([]) + + // PO 상세 정보 로드 + React.useEffect(() => { + if (open && po) { + setLoading(true) + Promise.all([ + getVendorPOById(po.id), + getVendorPOItems(po.id) + ]) + .then(([details, items]) => { + setPoDetails(details) + setPoItems(items) + }) + .catch((error) => { + console.error("Failed to load PO details:", error) + }) + .finally(() => { + setLoading(false) + }) + } + }, [open, po]) + + const handlePrint = () => { + window.print() + } + + if (!po) return null + + const displayPO = poDetails || po + + // 총 수량 계산 + const totalQuantity = poItems.reduce((sum, item) => sum + (item.quantity || 0), 0) + + return ( + <> + {/* 인쇄 전용 스타일 - Tailwind와 독립적으로 작동 */} + <style dangerouslySetInnerHTML={{__html: ` + @media print { + @page { + size: A4; + margin: 10mm; + } + + /* Dialog 관련 UI 요소 모두 숨김 */ + body > div:not(:has(.po-print-container)) { + display: none !important; + } + + /* Dialog overlay/backdrop 숨김 */ + [data-radix-popper-content-wrapper], + [data-overlay], + [role="dialog"] > div:first-child { + display: none !important; + } + + /* DialogContent 스타일 초기화 */ + [role="dialog"] { + position: static !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + max-height: none !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + box-shadow: none !important; + background: white !important; + overflow: visible !important; + transform: none !important; + } + + .po-print-container { + font-family: 'Malgun Gothic', Arial, sans-serif !important; + font-size: 11px !important; + color: #000 !important; + background: white !important; + margin: 0 !important; + padding: 0 !important; + } + .po-page-break { + page-break-after: always; + } + .po-no-break { + page-break-inside: avoid; + } + } + `}} /> + + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-[210mm] max-h-[90vh] overflow-y-auto print:p-0 print:m-0 print:max-w-full print:max-h-none print:border-0 print:shadow-none"> + <DialogHeader className="print:hidden"> + <DialogTitle>발주서 출력</DialogTitle> + </DialogHeader> + + {loading ? ( + <div className="flex items-center justify-center py-12"> + <div className="text-muted-foreground">로딩 중...</div> + </div> + ) : ( + <div className="po-print-container"> + {/* ===== 페이지 1: 품목 리스트 ===== */} + <div className="po-page-break" style={{ maxWidth: '210mm', margin: '0 auto' }}> + <POHeader po={displayPO} /> + + {/* 공급자 정보 */} + <div style={{ border: '2px solid #000', padding: '10px', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr> + <td style={{ width: '70%', fontSize: '11px' }}> + <div style={{ marginBottom: '3px' }}>공급자명 : {displayPO.vendorName || '-'}</div> + <div>공급자 주소 : {displayPO.vendorAddress ? `${displayPO.vendorAddress}${displayPO.vendorAddressDetail ? ' ' + displayPO.vendorAddressDetail : ''}` : '-'}</div> + </td> + <td style={{ width: '30%', textAlign: 'right', fontSize: '11px' }}> + <div>FAX NO : -</div> + <div>TEL NO : {displayPO.vendorPhone || '-'}</div> + </td> + </tr> + </tbody> + </table> + </div> + + {/* 서명 섹션 */} + <div style={{ border: '2px solid #000', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr> + <td style={{ width: '50%', borderRight: '1px solid #000', padding: '10px', verticalAlign: 'top' }}> + <div style={{ fontWeight: 'bold', marginBottom: '10px', fontSize: '11px' }}>VERY TRULY YOURS</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>서명 / SIGNATURE :</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>성명 / NAME : -</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>직함 / TITLE : -</div> + <div style={{ fontSize: '11px' }}>회사명 / CO. : 삼성중공업(주)</div> + </td> + <td style={{ width: '50%', padding: '10px', verticalAlign: 'top' }}> + <div style={{ fontWeight: 'bold', marginBottom: '10px', fontSize: '11px' }}>ACCEPTED AND CONFIRMED BY</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>서명 / SIGNATURE :</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>성명 / NAME : -</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>직함 / TITLE : -</div> + <div style={{ fontSize: '11px' }}>회사명 / CO. : -</div> + </td> + </tr> + </tbody> + </table> + </div> + + {/* 프로젝트 정보 */} + <div style={{ border: '2px solid #000', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr> + <td style={{ width: '33.33%', borderRight: '1px solid #000', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + 프로젝트 / PROJECT NO + </td> + <td style={{ width: '33.33%', borderRight: '1px solid #000', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + 대표품명 / ORDER DESCRIPTION + </td> + <td style={{ width: '33.33%', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + 화폐단위 / CURRENCY + </td> + </tr> + <tr> + <td style={{ width: '33.33%', borderRight: '1px solid #000', padding: '8px', fontSize: '11px' }}> + {displayPO.projectCode || '-'} + </td> + <td style={{ width: '33.33%', borderRight: '1px solid #000', padding: '8px', fontSize: '11px' }}> + {displayPO.contractName || '-'} + </td> + <td style={{ width: '33.33%', padding: '8px', fontSize: '11px' }}> + {displayPO.currency || 'KRW'} + </td> + </tr> + </tbody> + </table> + </div> + + {/* 지급 및 인도 조건 */} + <div style={{ border: '2px solid #000', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr> + <td style={{ width: '40%', borderRight: '1px solid #000', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + 지급조건 / PAYMENT TERMS + </td> + <td style={{ width: '30%', borderRight: '1px solid #000', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + 인도조건 / DELIVERY TERMS + </td> + <td style={{ width: '15%', borderRight: '1px solid #000', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + VAT 구분 + </td> + <td style={{ width: '15%', borderBottom: '1px solid #000', padding: '8px', fontSize: '11px', fontWeight: 'bold', color: '#999' }}> + 납세자번호 + </td> + </tr> + <tr> + <td style={{ width: '40%', borderRight: '1px solid #000', padding: '8px', fontSize: '10px' }}> + {displayPO.paymentTerms || '-'} + </td> + <td style={{ width: '30%', borderRight: '1px solid #000', padding: '8px', fontSize: '11px' }}> + {displayPO.deliveryTerms || '-'} + </td> + <td style={{ width: '15%', borderRight: '1px solid #000', padding: '8px', fontSize: '11px' }}> + - + </td> + <td style={{ width: '15%', padding: '8px', fontSize: '11px' }}> + - + </td> + </tr> + </tbody> + </table> + </div> + + {/* 품목 테이블 */} + <div style={{ border: '2px solid #000', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <thead> + <tr style={{ backgroundColor: '#f0f0f0' }}> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '5%' }}>품번</th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '7%' }}>PR-NO</th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '8%' }}>자재코드</th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '22%' }}> + 품명,규격,재질<br />DESC, SPEC, MATL + </th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '7%' }}> + VALVE<br />FIT. NO + </th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '5%' }}>CERT</th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '5%' }}> + 단위<br />UNIT + </th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '7%' }}>납기일자</th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '6%' }}> + 수량<br />QTY + </th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '8%' }}> + 총중량<br />WEIGHT(KG) + </th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '10%' }}> + 단가<br />UNIT-PRICE + </th> + <th style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', fontWeight: 'bold', width: '10%' }}> + 금액<br />AMOUNT + </th> + </tr> + </thead> + <tbody> + {poItems.length > 0 ? ( + poItems.map((item, index) => ( + <tr key={index}> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'center', verticalAlign: 'top' }}> + {item.itemNo || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'center', verticalAlign: 'top' }}> + {item.prNo || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', verticalAlign: 'top' }}> + {item.materialNo || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', verticalAlign: 'top' }}> + {item.itemDescription || '-'} + {item.specification && <><br />{item.specification}</>} + {item.material && <><br />재질: {item.material}</>} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'center', verticalAlign: 'top' }}> + {item.fittingNo || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'center', verticalAlign: 'top' }}> + {item.cert || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'center', verticalAlign: 'top' }}> + {item.ZPO_UNIT || item.quantityUnit || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'center', verticalAlign: 'top' }}> + {item.ZPO_DLV_DT || item.deliveryDate || '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'right', verticalAlign: 'top' }}> + {formatNumber(item.quantity, 2)} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'right', verticalAlign: 'top' }}> + {item.totalWeight ? formatNumber(item.totalWeight, 2) : '-'} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'right', verticalAlign: 'top' }}> + {formatNumber(item.unitPrice, 2)} + </td> + <td style={{ border: '1px solid #000', padding: '5px', fontSize: '9px', textAlign: 'right', verticalAlign: 'top' }}> + {formatNumber(item.NETWR || item.contractAmount, 2)} + </td> + </tr> + )) + ) : ( + <tr> + <td colSpan={12} style={{ border: '1px solid #000', padding: '20px', textAlign: 'center', fontSize: '11px' }}> + 품목 정보가 없습니다. + </td> + </tr> + )} + </tbody> + {poItems.length > 0 && ( + <tfoot> + <tr style={{ backgroundColor: '#f0f0f0', fontWeight: 'bold' }}> + <td colSpan={8} style={{ border: '1px solid #000', padding: '5px', textAlign: 'center', fontSize: '11px' }}> + * TOTAL AMOUNT * + </td> + <td style={{ border: '1px solid #000', padding: '5px', textAlign: 'right', fontSize: '11px' }}> + {formatNumber(totalQuantity, 2)} + </td> + <td colSpan={2} style={{ border: '1px solid #000', padding: '5px' }}></td> + <td style={{ border: '1px solid #000', padding: '5px', textAlign: 'right', fontSize: '11px' }}> + {formatNumber(displayPO.totalAmount, displayPO.currency === 'KRW' || displayPO.currency === 'JPY' ? 0 : 2)} + </td> + </tr> + </tfoot> + )} + </table> + </div> + </div> + + {/* ===== 페이지 2: 특기사항 ===== */} + {displayPO.contractContent && ( + <div style={{ maxWidth: '210mm', margin: '0 auto' }}> + <POHeader po={displayPO} /> + + {/* 공급자 정보 (반복) */} + <div style={{ border: '2px solid #000', padding: '10px', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr> + <td style={{ width: '70%', fontSize: '11px' }}> + <div style={{ marginBottom: '3px' }}>공급자명 : {displayPO.vendorName || '-'}</div> + <div>공급자 주소 : {displayPO.vendorAddress ? `${displayPO.vendorAddress}${displayPO.vendorAddressDetail ? ' ' + displayPO.vendorAddressDetail : ''}` : '-'}</div> + </td> + <td style={{ width: '30%', textAlign: 'right', fontSize: '11px' }}> + <div>FAX NO : -</div> + <div>TEL NO : {displayPO.vendorPhone || '-'}</div> + </td> + </tr> + </tbody> + </table> + </div> + + {/* 서명 섹션 (반복) */} + <div style={{ border: '2px solid #000', marginBottom: '10px' }}> + <table style={{ width: '100%', borderCollapse: 'collapse' }}> + <tbody> + <tr> + <td style={{ width: '50%', borderRight: '1px solid #000', padding: '10px', verticalAlign: 'top' }}> + <div style={{ fontWeight: 'bold', marginBottom: '10px', fontSize: '11px' }}>VERY TRULY YOURS</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>서명 / SIGNATURE :</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>성명 / NAME : -</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>직함 / TITLE : -</div> + <div style={{ fontSize: '11px' }}>회사명 / CO. : 삼성중공업(주)</div> + </td> + <td style={{ width: '50%', padding: '10px', verticalAlign: 'top' }}> + <div style={{ fontWeight: 'bold', marginBottom: '10px', fontSize: '11px' }}>ACCEPTED AND CONFIRMED BY</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>서명 / SIGNATURE :</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>성명 / NAME : -</div> + <div style={{ marginBottom: '5px', fontSize: '11px' }}>직함 / TITLE : -</div> + <div style={{ fontSize: '11px' }}>회사명 / CO. : -</div> + </td> + </tr> + </tbody> + </table> + </div> + + {/* 특기사항 */} + <div style={{ border: '2px solid #000', padding: '10px' }}> + <div style={{ fontWeight: 'bold', marginBottom: '10px', fontSize: '12px' }}>특기사항 / Note</div> + <div style={{ lineHeight: '1.6', fontSize: '11px', whiteSpace: 'pre-wrap' }}> + {displayPO.contractContent} + </div> + </div> + </div> + )} + </div> + )} + + {/* 액션 버튼 (인쇄 시 숨김) */} + <div className="flex justify-end gap-2 mt-4 print:hidden"> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 닫기 + </Button> + <Button onClick={handlePrint} disabled={loading}> + <PrinterIcon className="mr-2 h-4 w-4" /> + 인쇄/PDF 저장 + </Button> + </div> + </DialogContent> + </Dialog> + </> + ) +} + |
