Files
sam-react-prod/src/components/orders/documents/SalesOrderDocument.tsx

671 lines
29 KiB
TypeScript
Raw Normal View History

"use client";
/**
* ( D1.8 )
* - (ShipmentOrderDocument)
* - (, , , )
* - /LOT
*/
import { useState } from "react";
import { getTodayString } from "@/lib/utils/date";
import { OrderItem } from "../actions";
import { ProductInfo } from "./OrderDocumentModal";
import { ConstructionApprovalTable } from "@/components/document-system";
import { formatNumber } from '@/lib/utils/amount';
// ===== 데이터 타입 =====
interface MotorRow {
item: string;
type: string;
spec: string;
qty: number;
}
interface BendingItem {
name: string;
spec: string;
qty: number;
image_url?: string | null;
}
interface BendingGroup {
group: string;
items: BendingItem[];
}
interface SubsidiaryItem {
name: string;
spec: string;
qty: number;
}
interface ProductRow {
no: number;
floor?: string;
symbol?: string;
product_name?: string;
product_type?: string;
open_width?: number | string;
open_height?: number | string;
made_width?: number | string;
made_height?: number | string;
guide_rail?: string;
shaft?: string | number;
case_inch?: string | number;
bracket?: string;
capacity?: string | number;
finish?: string;
joint_bar?: number | null;
}
interface SalesOrderDocumentProps {
documentNumber?: string;
orderNumber: string;
certificationNumber?: string;
orderDate?: string;
client: string;
siteName?: string;
manager?: string;
managerContact?: string;
deliveryRequestDate?: string;
expectedShipDate?: string;
deliveryMethod?: string;
address?: string;
recipientName?: string;
recipientContact?: string;
shutterCount?: number;
fee?: number;
items?: OrderItem[];
products?: ProductInfo[];
remarks?: string;
// 실 데이터 props
productRows?: ProductRow[];
motorsLeft?: MotorRow[];
motorsRight?: MotorRow[];
bendingParts?: BendingGroup[];
subsidiaryParts?: SubsidiaryItem[];
categoryCode?: string;
}
// ===== 공통 스타일 =====
const thBase = 'border-r border-gray-400 px-1 py-1';
const tdBase = 'border-r border-gray-300 px-1 py-1';
const tdCenter = `${tdBase} text-center`;
const imgPlaceholder = 'flex items-center justify-center border border-dashed border-gray-300 text-gray-400';
/** 절곡품 이미지 렌더링 — image_url 있으면 실제 이미지, 없으면 placeholder */
function BendingImage({ items, height = 'h-20' }: { items: BendingItem[]; height?: string }) {
const imageUrl = items.find(i => i.image_url)?.image_url;
if (imageUrl) {
return <img src={imageUrl} alt="" className={`${height} w-full object-contain`} />;
}
return <div className={`${imgPlaceholder} ${height} w-full`}>IMG</div>;
}
export function SalesOrderDocument({
documentNumber = "ABC123",
orderNumber,
certificationNumber = "-",
orderDate = getTodayString(),
client,
siteName = "-",
manager = "-",
managerContact = "-",
deliveryRequestDate = "-",
expectedShipDate = "-",
deliveryMethod = "상차",
address = "-",
recipientName = "-",
recipientContact = "-",
shutterCount = 0,
items: _items = [],
products = [],
remarks,
productRows = [],
motorsLeft = [],
motorsRight = [],
bendingParts = [],
subsidiaryParts = [],
}: SalesOrderDocumentProps) {
const [bottomFinishView, setBottomFinishView] = useState<'screen' | 'steel'>('screen');
const motorRows = Math.max(motorsLeft.length, motorsRight.length);
// 절곡물 그룹 데이터 추출
const guideRailItems = bendingParts.find(g => g.group === '가이드레일')?.items ?? [];
const caseItems = bendingParts.find(g => g.group === '케이스')?.items ?? [];
const bottomItems = bendingParts.find(g => g.group === '하단마감')?.items ?? [];
const smokeItems = bendingParts.find(g => g.group === '연기차단재')?.items ?? [];
const guideSmokeItems = smokeItems.filter(i => i.name.includes('레일') || i.name.includes('가이드'));
const caseSmokeItems = smokeItems.filter(i => i.name.includes('케이스'));
// 구분 불가한 연기차단재는 그대로 표시
const otherSmokeItems = smokeItems.filter(i =>
!i.name.includes('레일') && !i.name.includes('가이드') && !i.name.includes('케이스')
);
// 부자재 좌/우 2열 변환
const subsidiaryRows = [];
for (let i = 0; i < subsidiaryParts.length; i += 2) {
subsidiaryRows.push({
left: subsidiaryParts[i],
right: subsidiaryParts[i + 1] ?? null,
});
}
// 스크린/철재 제품 분리
const screenProducts = productRows.filter(p => p.product_type !== 'steel');
const steelProducts = productRows.filter(p => p.product_type === 'steel');
return (
<div className="bg-white p-8 min-h-full text-[11px]">
{/* ========== 헤더: 수주서 제목 + 결재란 ========== */}
<div className="flex justify-between items-start mb-4">
<div>
<h1 className="text-2xl font-bold tracking-widest mb-2"> </h1>
<div className="text-[10px] space-y-1">
<div className="flex gap-4">
<span>: <strong>{documentNumber}</strong></span>
<span>: <strong>{orderDate}</strong></span>
</div>
</div>
</div>
<ConstructionApprovalTable approvers={{ writer: { name: '홍길동' } }} />
</div>
{/* ========== 로트번호 / 제품명 / 제품코드 / 인정번호 ========== */}
<table className="border border-gray-400 w-full mb-3 text-[10px]">
<tbody>
<tr>
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-400 whitespace-nowrap"></td>
<td className="px-2 py-1 border-r border-gray-400">{orderNumber}</td>
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-400 whitespace-nowrap"></td>
<td className="px-2 py-1 border-r border-gray-400">{products[0]?.productName || productRows[0]?.product_name || "-"}</td>
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-400 whitespace-nowrap"></td>
<td className="px-2 py-1 border-r border-gray-400">{productRows[0]?.product_name?.split(' ')[0] || "KWS01"}</td>
<td className="bg-gray-100 px-2 py-1 font-medium border-r border-gray-400 whitespace-nowrap"></td>
<td className="px-2 py-1">{certificationNumber}</td>
</tr>
</tbody>
</table>
{/* ========== 3열 섹션: 신청업체 | 신청내용 | 납품정보 ========== */}
<div className="border border-gray-400 mb-4">
<div className="grid grid-cols-3">
{/* 신청업체 */}
<div className="border-r border-gray-400">
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400"></div>
<table className="w-full">
<tbody>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 w-20 font-medium"></td>
<td className="px-2 py-1">{orderDate}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{client}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 font-medium"> </td>
<td className="px-2 py-1">{manager}</td>
</tr>
<tr>
<td className="bg-gray-100 px-2 py-1 font-medium"> </td>
<td className="px-2 py-1">{managerContact}</td>
</tr>
</tbody>
</table>
</div>
{/* 신청내용 */}
<div className="border-r border-gray-400">
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400"></div>
<table className="w-full">
<tbody>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 w-20 font-medium"></td>
<td className="px-2 py-1">{siteName}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{deliveryRequestDate}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{expectedShipDate}</td>
</tr>
<tr>
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{shutterCount || productRows.length}</td>
</tr>
</tbody>
</table>
</div>
{/* 납품정보 */}
<div>
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400"></div>
<table className="w-full">
<tbody>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 w-20 font-medium"></td>
<td className="px-2 py-1">{siteName}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{recipientName}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{recipientContact}</td>
</tr>
<tr>
<td className="bg-gray-100 px-2 py-1 font-medium"></td>
<td className="px-2 py-1">{deliveryMethod}</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* 배송지 주소 - 한 줄 병합 */}
<div className="border-t border-gray-400 flex">
<div className="bg-gray-100 px-2 py-1 w-20 shrink-0 font-medium"> </div>
<div className="px-2 py-1 flex-1">{address}</div>
</div>
</div>
<p className="text-[10px] mb-4"> .</p>
{/* ========== 1. 스크린 ========== */}
{screenProducts.length > 0 && (
<div className="mb-4">
<p className="font-bold mb-2">1. </p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-8`} rowSpan={2}>No</th>
<th className={`${thBase} w-14`} rowSpan={2}></th>
<th className={`${thBase} w-14`} rowSpan={2}></th>
<th className={thBase} colSpan={2}></th>
<th className={thBase} colSpan={2}></th>
<th className={`${thBase} w-16`} rowSpan={2}><br /></th>
<th className={`${thBase} w-14`} rowSpan={2}><br />()</th>
<th className={`${thBase} w-14`} rowSpan={2}><br />()</th>
<th className={thBase} colSpan={2}></th>
<th className="px-1 py-1 w-16" rowSpan={2}></th>
</tr>
<tr className="bg-gray-50 border-b border-gray-400 text-[9px]">
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}>Kg</th>
</tr>
</thead>
<tbody>
{screenProducts.map((row) => (
<tr key={row.no} className="border-b border-gray-300">
<td className={tdCenter}>{row.no}</td>
<td className={tdCenter}>{row.floor ?? '-'}</td>
<td className={tdCenter}>{row.symbol ?? '-'}</td>
<td className={tdCenter}>{row.open_width ? formatNumber(Number(row.open_width)) : '-'}</td>
<td className={tdCenter}>{row.open_height ? formatNumber(Number(row.open_height)) : '-'}</td>
<td className={tdCenter}>{row.made_width ? formatNumber(Number(row.made_width)) : '-'}</td>
<td className={tdCenter}>{row.made_height ? formatNumber(Number(row.made_height)) : '-'}</td>
<td className={tdCenter}>{row.guide_rail ?? '-'}</td>
<td className={tdCenter}>{row.shaft ?? '-'}</td>
<td className={tdCenter}>{row.case_inch ?? '-'}</td>
<td className={tdCenter}>{row.bracket ?? '-'}</td>
<td className={tdCenter}>{row.capacity ?? '-'}</td>
<td className="px-1 py-1 text-center">{row.finish ?? '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* ========== 2. 철재 ========== */}
{steelProducts.length > 0 && (
<div className="mb-4">
<p className="font-bold mb-2">2. </p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-8`} rowSpan={2}>No.</th>
<th className={`${thBase} w-14`} rowSpan={2}></th>
<th className={thBase} colSpan={2}></th>
<th className={thBase} colSpan={2}></th>
<th className={`${thBase} w-16`} rowSpan={2}><br /></th>
<th className={`${thBase} w-14`} rowSpan={2}><br />()</th>
<th className={`${thBase} w-14`} rowSpan={2}><br />()</th>
<th className={`${thBase} w-14`} rowSpan={2}><br />()</th>
<th className={thBase} colSpan={2}></th>
<th className="px-1 py-1 w-16" rowSpan={2}></th>
</tr>
<tr className="bg-gray-50 border-b border-gray-400 text-[9px]">
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}></th>
<th className={thBase}>Kg</th>
</tr>
</thead>
<tbody>
{steelProducts.map((row) => (
<tr key={row.no} className="border-b border-gray-300">
<td className={tdCenter}>{row.no}</td>
<td className={tdCenter}>{row.symbol ?? '-'}</td>
<td className={tdCenter}>{row.open_width ? formatNumber(Number(row.open_width)) : '-'}</td>
<td className={tdCenter}>{row.open_height ? formatNumber(Number(row.open_height)) : '-'}</td>
<td className={tdCenter}>{row.made_width ? formatNumber(Number(row.made_width)) : '-'}</td>
<td className={tdCenter}>{row.made_height ? formatNumber(Number(row.made_height)) : '-'}</td>
<td className={tdCenter}>{row.guide_rail ?? '-'}</td>
<td className={tdCenter}>{row.shaft ?? '-'}</td>
<td className={tdCenter}>{row.joint_bar ?? '-'}</td>
<td className={tdCenter}>{row.case_inch ?? '-'}</td>
<td className={tdCenter}>{row.bracket ?? '-'}</td>
<td className={tdCenter}>{row.capacity ?? '-'}</td>
<td className="px-1 py-1 text-center">{row.finish ?? '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* ========== 3. 모터 ========== */}
{motorRows > 0 && (
<div className="mb-4">
<p className="font-bold mb-2">3. </p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-20`}></th>
<th className={`${thBase} w-20`}></th>
<th className={`${thBase} w-20`}></th>
<th className={`${thBase} w-10`}></th>
<th className={`${thBase} w-20`}></th>
<th className={`${thBase} w-20`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-10"></th>
</tr>
</thead>
<tbody>
{Array.from({ length: motorRows }).map((_, i) => {
const left = motorsLeft[i];
const right = motorsRight[i];
return (
<tr key={i} className="border-b border-gray-300">
<td className={tdBase}>{left?.item || ''}</td>
<td className={tdBase}>{left?.type || ''}</td>
<td className={tdBase}>{left?.spec || ''}</td>
<td className={tdCenter}>{left?.qty ?? ''}</td>
<td className={tdBase}>{right?.item || ''}</td>
<td className={tdBase}>{right?.type || ''}</td>
<td className={tdBase}>{right?.spec || ''}</td>
<td className="px-1 py-1 text-center">{right?.qty ?? ''}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* ========== 4. 절곡물 ========== */}
{bendingParts.length > 0 && (
<div className="mb-4">
<p className="font-bold mb-2">4. </p>
{/* 4-1. 가이드레일 */}
{guideRailItems.length > 0 && (
<div className="mb-3">
<p className="text-[10px] font-medium mb-1">4-1. </p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-32`}>&nbsp;</th>
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-14"></th>
</tr>
</thead>
<tbody>
{guideRailItems.map((item, i) => (
<tr key={i} className="border-b border-gray-300">
{i === 0 && (
<td className={tdCenter} rowSpan={guideRailItems.length}>
<BendingImage items={guideRailItems} height="h-20" />
</td>
)}
<td className={tdCenter}>{item.name}</td>
<td className={tdCenter}>{item.spec}</td>
<td className="px-1 py-1 text-center">{item.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 가이드레일 연기차단재 */}
{guideSmokeItems.length > 0 && (
<div className="mt-1 border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-400 text-[10px]">
<th className={`${thBase} w-32`}>&nbsp;</th>
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-14"></th>
</tr>
</thead>
<tbody>
{guideSmokeItems.map((item, i) => (
<tr key={i} className="border-b border-gray-300">
{i === 0 && (
<td className={tdCenter} rowSpan={guideSmokeItems.length}>
<BendingImage items={guideSmokeItems} height="h-14" />
</td>
)}
<td className={tdCenter}>{item.name}</td>
<td className={tdCenter}>{item.spec}</td>
<td className="px-1 py-1 text-center">{item.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<p className="mt-1 text-[10px]">
<span className="font-medium">* <span className="text-red-600 font-bold"></span> </span> - EGI 0.8T +
</p>
</div>
)}
{/* 4-2. 케이스(셔터박스) */}
{caseItems.length > 0 && (
<div className="mb-3">
<p className="text-[10px] font-medium mb-1">4-2. ()</p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-32`}>&nbsp;</th>
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-14"></th>
</tr>
</thead>
<tbody>
{caseItems.map((item, i) => (
<tr key={i} className="border-b border-gray-300">
{i === 0 && (
<td className={tdCenter} rowSpan={caseItems.length}>
<BendingImage items={caseItems} height="h-24" />
</td>
)}
<td className={tdCenter}>{item.name}</td>
<td className={tdCenter}>{item.spec}</td>
<td className="px-1 py-1 text-center">{item.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 케이스 연기차단재 */}
{caseSmokeItems.length > 0 && (
<div className="mt-1 border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-400 text-[10px]">
<th className={`${thBase} w-32`}>&nbsp;</th>
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-14"></th>
</tr>
</thead>
<tbody>
{caseSmokeItems.map((item, i) => (
<tr key={i} className="border-b border-gray-300">
{i === 0 && (
<td className={tdCenter} rowSpan={caseSmokeItems.length}>
<BendingImage items={caseSmokeItems} height="h-14" />
</td>
)}
<td className={tdCenter}>{item.name}</td>
<td className={tdCenter}>{item.spec}</td>
<td className="px-1 py-1 text-center">{item.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<p className="mt-1 text-[10px]">
<span className="font-medium">* , <span className="text-red-600 font-bold"></span> </span> - EGI 0.8T +
</p>
</div>
)}
{/* 4-3. 하단마감재 */}
{bottomItems.length > 0 && (
<div className="mb-3">
<p className="text-[10px] font-medium mb-1">4-3. </p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-32`}>&nbsp;</th>
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-14"></th>
</tr>
</thead>
<tbody>
{bottomItems.map((item, i) => (
<tr key={i} className="border-b border-gray-300">
{i === 0 && (
<td className={tdCenter} rowSpan={bottomItems.length}>
<BendingImage items={bottomItems} height="h-16" />
</td>
)}
<td className={tdCenter}>{item.name}</td>
<td className={tdCenter}>{item.spec}</td>
<td className="px-1 py-1 text-center">{item.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* 연기차단재 (구분 불가) */}
{otherSmokeItems.length > 0 && (
<div className="mb-3">
<p className="text-[10px] font-medium mb-1"></p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-400 text-[10px]">
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-14"></th>
</tr>
</thead>
<tbody>
{otherSmokeItems.map((item, i) => (
<tr key={i} className="border-b border-gray-300">
<td className={tdCenter}>{item.name}</td>
<td className={tdCenter}>{item.spec}</td>
<td className="px-1 py-1 text-center">{item.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)}
{/* ========== 5. 부자재 ========== */}
{subsidiaryParts.length > 0 && (
<div className="mb-4">
<p className="font-bold mb-2">5. </p>
<div className="border border-gray-400">
<table className="w-full">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className={`${thBase} w-10`}></th>
<th className={`${thBase} w-24`}></th>
<th className={`${thBase} w-20`}></th>
<th className="px-1 py-1 w-10"></th>
</tr>
</thead>
<tbody>
{subsidiaryRows.map((row, i) => (
<tr key={i} className="border-b border-gray-300">
<td className={tdBase}>{row.left?.name ?? ''}</td>
<td className={tdCenter}>{row.left?.spec ?? ''}</td>
<td className={tdCenter}>{row.left?.qty ?? ''}</td>
<td className={tdBase}>{row.right?.name ?? ''}</td>
<td className={tdCenter}>{row.right?.spec ?? ''}</td>
<td className="px-1 py-1 text-center">{row.right?.qty ?? ''}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* ========== 특이사항 ========== */}
{remarks && (
<div className="mb-4">
<p className="font-bold mb-2"> </p>
<div className="border border-gray-400 p-3 min-h-[40px]">
{remarks}
</div>
</div>
)}
</div>
);
}