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>
This commit is contained in:
425
src/components/quotes/PurchaseOrderDocument.tsx
Normal file
425
src/components/quotes/PurchaseOrderDocument.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 발주서 (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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user