Files
sam-react-prod/src/components/quotes/PurchaseOrderDocument.tsx
byeongcheolryu 751e65f59b fix: 품목관리 수정 기능 버그 수정 및 Sales 페이지 추가
## 품목관리 수정 버그 수정
- FG(제품) 수정 시 품목명 반영 안되는 문제 해결
  - productName → name 필드 매핑 추가
  - FG 품목코드 = 품목명 동기화 로직 추가
- Materials(SM, RM, CS) 수정페이지 진입 오류 해결
- UNIQUE 제약조건 위반 오류 해결

## Sales 페이지
- 거래처관리 (client-management-sales-admin) 페이지 구현
- 견적관리 (quote-management) 페이지 구현
- 관련 컴포넌트 및 훅 추가

## 기타
- 회원가입 페이지 차단 처리
- 디버깅용 콘솔 로그 추가 (PUT 요청/응답 확인용)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 20:52:42 +09:00

426 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 발주서 (Purchase Order Document)
*
* - 로트번호 및 결재란
* - 신청업체 정보
* - 신청내용
* - 부자재 목록
*/
import { QuoteFormData } from "./QuoteRegistration";
interface PurchaseOrderDocumentProps {
quote: QuoteFormData;
}
export function PurchaseOrderDocument({ quote }: PurchaseOrderDocumentProps) {
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`;
};
// 발주번호 생성 (견적번호 기반)
const purchaseOrderNumber = quote.id?.replace('Q', 'KQ#-SC-') + '-01' || 'KQ#-SC-XXXXXX-01';
// BOM에서 부자재 목록 추출 (샘플 데이터)
const materialItems = quote.items?.map((item, index) => ({
no: index + 1,
name: item.productName || '아연도각파이프',
spec: `${item.openWidth}×${item.openHeight}`,
length: Number(item.openHeight) || 3000,
quantity: item.quantity || 1,
note: ''
})) || [
{ no: 1, name: '아연도각파이프', spec: '100-50-2T', length: 3000, quantity: 6, note: '' },
{ no: 2, name: '아연도각파이프', spec: '100-100-2T', length: 3000, quantity: 6, note: '' },
{ no: 3, name: '아연도앵글', spec: '50-50-4T', length: 2500, quantity: 10, note: '' },
{ no: 4, name: '외주 발주 코팅 비비그레스', spec: '', length: 0, quantity: 1, note: '' },
];
return (
<>
<style>{`
@media print {
@page {
size: A4 portrait;
margin: 10mm;
}
body {
background: white !important;
}
#purchase-order-content {
background: white !important;
padding: 0 !important;
}
}
/* 발주서 공문서 스타일 */
.purchase-order {
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
background: white;
color: #000;
line-height: 1.4;
}
.po-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #000;
}
.po-title {
flex: 1;
text-align: center;
}
.po-title h1 {
font-size: 36px;
font-weight: 700;
letter-spacing: 8px;
margin: 0;
}
.po-approval-section {
border: 2px solid #000;
background: white;
}
.po-lot-number-row {
display: grid;
grid-template-columns: 100px 1fr;
border-bottom: 2px solid #000;
}
.po-lot-label {
background: #e8e8e8;
border-right: 2px solid #000;
padding: 8px;
text-align: center;
font-weight: 700;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.po-lot-value {
background: white;
padding: 8px;
text-align: center;
font-weight: 700;
font-size: 14px;
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
.po-approval-box {
width: 100%;
border: none;
display: grid;
grid-template-columns: 60px 1fr;
grid-template-rows: auto auto auto;
}
.po-approval-merged-vertical-cell {
border-right: 1px solid #000;
padding: 4px;
text-align: center;
font-weight: 600;
font-size: 11px;
background: white;
display: flex;
align-items: center;
justify-content: center;
grid-row: 1 / 4;
}
.po-approval-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border-bottom: 1px solid #000;
}
.po-approval-header-cell {
border-right: 1px solid #000;
padding: 8px;
text-align: center;
font-weight: 600;
font-size: 12px;
background: white;
}
.po-approval-header-cell:last-child {
border-right: none;
}
.po-approval-content-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
border-bottom: 1px solid #000;
}
.po-approval-name-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.po-approval-signature-cell {
border-right: 1px solid #000;
padding: 8px;
text-align: center;
font-size: 11px;
height: 50px;
background: white;
}
.po-approval-signature-cell:last-child {
border-right: none;
}
.po-approval-name-cell {
border-right: 1px solid #000;
padding: 6px;
text-align: center;
font-weight: 600;
font-size: 11px;
background: white;
}
.po-approval-name-cell:last-child {
border-right: none;
}
.po-section-table {
width: 100%;
border-collapse: collapse;
border: 2px solid #000;
margin-bottom: 15px;
}
.po-section-header {
background: #e8e8e8;
border: 1px solid #666;
padding: 8px;
text-align: center;
font-weight: 700;
font-size: 13px;
width: 120px;
}
.po-section-content {
border: 1px solid #999;
padding: 8px;
font-size: 12px;
}
.po-materials-table {
width: 100%;
border-collapse: collapse;
border: 2px solid #000;
margin-bottom: 20px;
}
.po-materials-table th {
background: #e8e8e8;
border: 1px solid #666;
padding: 8px;
text-align: center;
font-weight: 700;
font-size: 12px;
}
.po-materials-table td {
border: 1px solid #999;
padding: 8px;
font-size: 12px;
}
.po-notes {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border: 1px solid #ddd;
font-size: 11px;
line-height: 1.6;
}
`}</style>
{/* 발주서 내용 */}
<div id="purchase-order-content" className="purchase-order p-12 print:p-8">
{/* 헤더: 제목 + 결재란 */}
<div className="po-header">
{/* 제목 */}
<div className="po-title">
<h1> </h1>
</div>
{/* 로트번호 + 결재란 */}
<div className="po-approval-section">
{/* 로트번호 */}
<div className="po-lot-number-row">
<div className="po-lot-label">
</div>
<div className="po-lot-value">
{purchaseOrderNumber}
</div>
</div>
{/* 결재란 */}
<div className="po-approval-box">
<div className="po-approval-merged-vertical-cell"><br/></div>
{/* 결재란 헤더 */}
<div className="po-approval-header">
<div className="po-approval-header-cell"></div>
<div className="po-approval-header-cell"></div>
<div className="po-approval-header-cell"></div>
</div>
{/* 결재+서명란 */}
<div className="po-approval-content-row">
<div className="po-approval-signature-cell"></div>
<div className="po-approval-signature-cell"></div>
<div className="po-approval-signature-cell"></div>
</div>
{/* 이름란 */}
<div className="po-approval-name-row">
<div className="po-approval-name-cell">/</div>
<div className="po-approval-name-cell"></div>
<div className="po-approval-name-cell"></div>
</div>
</div>
</div>
</div>
{/* 신청업체 */}
<table className="po-section-table">
<tbody>
<tr>
<th className="po-section-header" rowSpan={3}> </th>
<th className="po-section-header" style={{ width: '100px' }}></th>
<td className="po-section-content">{quote.clientName || '-'}</td>
<th className="po-section-header" style={{ width: '100px' }}></th>
<td className="po-section-content">{formatDate(quote.registrationDate || '')}</td>
</tr>
<tr>
<th className="po-section-header"></th>
<td className="po-section-content">{quote.manager || '-'}</td>
<th className="po-section-header"></th>
<td className="po-section-content">{quote.contact || '-'}</td>
</tr>
<tr>
<th className="po-section-header">F A X</th>
<td className="po-section-content">-</td>
<th className="po-section-header">()</th>
<td className="po-section-content">{quote.items?.reduce((sum, item) => sum + (item.quantity || 0), 0) || 0}</td>
</tr>
</tbody>
</table>
{/* 신청내용 */}
<table className="po-section-table">
<tbody>
<tr>
<th className="po-section-header" rowSpan={5}> </th>
<th className="po-section-header" style={{ width: '100px' }}></th>
<td className="po-section-content" colSpan={3}>{quote.siteName || '-'}</td>
</tr>
<tr>
<th className="po-section-header"></th>
<td className="po-section-content" colSpan={3}>{formatDate(quote.dueDate || '')}</td>
</tr>
<tr>
<th className="po-section-header"></th>
<td className="po-section-content">{formatDate(quote.registrationDate || '')}</td>
<th className="po-section-header" style={{ width: '100px' }}></th>
<td className="po-section-content"></td>
</tr>
<tr>
<th className="po-section-header"></th>
<td className="po-section-content" colSpan={3}> 16-180</td>
</tr>
<tr>
<th className="po-section-header"></th>
<td className="po-section-content">{quote.manager || '-'}</td>
<th className="po-section-header" style={{ width: '100px' }}></th>
<td className="po-section-content">{quote.contact || '-'}</td>
</tr>
</tbody>
</table>
{/* 발주개소 정보 */}
<table className="po-section-table">
<tbody>
<tr>
<th className="po-section-header" style={{ width: '120px' }}></th>
<td className="po-section-content" style={{ width: '200px' }}>-</td>
<th className="po-section-header" style={{ width: '120px' }}></th>
<td className="po-section-content">{quote.items?.reduce((sum, item) => sum + (item.quantity || 0), 0) || 0}</td>
</tr>
</tbody>
</table>
{/* 유의사항 */}
<div style={{ marginBottom: '10px', fontSize: '11px', lineHeight: '1.6' }}>
<p>1. .</p>
<p>2. .</p>
</div>
{/* 부자재 테이블 */}
<div style={{ fontWeight: '700', marginBottom: '10px', fontSize: '14px' }}> </div>
<table className="po-materials-table">
<thead>
<tr>
<th style={{ width: '50px' }}></th>
<th></th>
<th style={{ width: '200px' }}></th>
<th style={{ width: '100px' }}>(mm)</th>
<th style={{ width: '80px' }}></th>
<th style={{ width: '150px' }}></th>
</tr>
</thead>
<tbody>
{materialItems.map((item) => (
<tr key={item.no}>
<td style={{ textAlign: 'center' }}>{item.no}</td>
<td>{item.name}</td>
<td>{item.spec}</td>
<td style={{ textAlign: 'right' }}>{item.length > 0 ? item.length.toLocaleString() : ''}</td>
<td style={{ textAlign: 'center', fontWeight: '600' }}>{item.quantity}</td>
<td>{item.note}</td>
</tr>
))}
</tbody>
</table>
{/* 특이사항 */}
<div style={{ marginBottom: '20px', padding: '10px', border: '1px solid #ddd', background: '#fafafa' }}>
<strong style={{ fontSize: '12px' }}></strong>
<div style={{ fontSize: '11px', marginTop: '5px', lineHeight: '1.6' }}>
{quote.remarks || '스크린 셔터 부품구성표 기반 자동 견적'}
</div>
</div>
{/* 하단 안내사항 */}
<div className="po-notes">
<p style={{ fontWeight: '600', marginBottom: '5px' }}> </p>
<p> .</p>
<p> , .</p>
<p> .</p>
<p style={{ marginTop: '10px', textAlign: 'center', fontWeight: '600' }}>
: {quote.writer || '담당자'} | {quote.contact || '031-983-5130'}
</p>
</div>
</div>
</>
);
}