fix: 산출내역서 세부항목 표시 및 부호 필드 수정

- 세부산출내역서: 개소별 BOM 품목 상세 표시 (품명/규격/수량/단가/금액)
- 개소별 소계 행 추가 및 전체 합계 표시
- 부호 표시: loc.symbol → loc.code로 수정 (내역/세부산출 양쪽)
- types.ts: 할인율/할인금액 필드 추가 (discountRate, discountAmount)
This commit is contained in:
2026-01-29 10:03:52 +09:00
parent 44353c09a0
commit 82ae9ab953
2 changed files with 91 additions and 34 deletions

View File

@@ -204,7 +204,7 @@ export function QuotePreviewContent({
<tr key={loc.id} className="border-b border-gray-300">
<td className="border-r border-gray-300 px-2 py-1 text-center">{index + 1}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.floor || '-'}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.symbol || '-'}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.code || '-'}</td>
<td className="border-r border-gray-300 px-2 py-1">{loc.productCode}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.openWidth}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.openHeight}</td>
@@ -301,45 +301,94 @@ export function QuotePreviewContent({
<table className="w-full text-xs">
<thead>
<tr className="bg-gray-100 border-b border-gray-400">
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1 w-16"></th>
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1"></th>
<th className="px-2 py-1"></th>
<th className="border-r border-gray-300 px-2 py-1 w-16"></th>
<th className="border-r border-gray-300 px-2 py-1 w-12"></th>
<th className="border-r border-gray-300 px-2 py-1 w-20"></th>
<th className="px-2 py-1 w-24"></th>
</tr>
</thead>
<tbody>
{quoteData.locations.map((loc) => (
<React.Fragment key={loc.id}>
{/* 각 개소별 품목 상세 */}
<tr className="border-b border-gray-300">
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.symbol || '-'}</td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="border-r border-gray-300 px-2 py-1 text-center"></td>
<td className="border-r border-gray-300 px-2 py-1 text-center">{loc.quantity}</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">SET</td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{(loc.unitPrice || 0).toLocaleString()}
</td>
<td className="px-2 py-1 text-right">
{(loc.totalPrice || 0).toLocaleString()}
</td>
</tr>
</React.Fragment>
))}
{/* 소계 */}
<tr className="bg-gray-50 border-t border-gray-400">
<td colSpan={3} className="border-r border-gray-300 px-2 py-1 text-center font-semibold">
{quoteData.locations.map((loc, locIndex) => {
const bomItems = loc.bomResult?.items || [];
const locationSymbol = loc.code || `${loc.floor}-${locIndex + 1}`;
const locationSubtotal = loc.bomResult?.grand_total || loc.totalPrice || 0;
return (
<React.Fragment key={loc.id}>
{/* 개소 헤더 */}
<tr className="bg-blue-50 border-b border-gray-400">
<td colSpan={7} className="px-2 py-1 font-semibold text-blue-800">
[{locationSymbol}] {loc.floor} - {loc.name || loc.code} (: {loc.quantity})
</td>
</tr>
{/* BOM 품목 상세 */}
{bomItems.length > 0 ? (
bomItems.map((item, itemIndex) => (
<tr key={`${loc.id}-${itemIndex}`} className="border-b border-gray-200">
<td className="border-r border-gray-300 px-2 py-1 text-center text-gray-500">
{itemIndex === 0 ? locationSymbol : ''}
</td>
<td className="border-r border-gray-300 px-2 py-1">
{item.item_name || item.item_code}
</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">
{item.specification || '-'}
</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">
{item.quantity}
</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">
{item.unit || 'EA'}
</td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{item.unit_price.toLocaleString()}
</td>
<td className="px-2 py-1 text-right">
{item.total_price.toLocaleString()}
</td>
</tr>
))
) : (
<tr className="border-b border-gray-200">
<td className="border-r border-gray-300 px-2 py-1 text-center">{locationSymbol}</td>
<td colSpan={6} className="px-2 py-1 text-center text-gray-400">
BOM
</td>
</tr>
)}
{/* 개소별 소계 */}
<tr className="bg-gray-100 border-b border-gray-400">
<td colSpan={3} className="border-r border-gray-300 px-2 py-1 text-right font-semibold">
[{locationSymbol}] ({loc.quantity})
</td>
<td className="border-r border-gray-300 px-2 py-1 text-center font-semibold">
{loc.quantity}
</td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="border-r border-gray-300 px-2 py-1 text-right">
{(loc.bomResult?.grand_total || 0).toLocaleString()}
</td>
<td className="px-2 py-1 text-right font-semibold">
{(locationSubtotal * loc.quantity).toLocaleString()}
</td>
</tr>
</React.Fragment>
);
})}
{/* 전체 합계 */}
<tr className="bg-gray-200 border-t-2 border-gray-500">
<td colSpan={6} className="border-r border-gray-300 px-2 py-2 text-center font-bold">
</td>
<td className="border-r border-gray-300 px-2 py-1 text-center">
{quoteData.locations.reduce((sum, loc) => sum + (loc.quantity || 0), 0)}
<td className="px-2 py-2 text-right font-bold text-lg">
{subtotal.toLocaleString()}
</td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="border-r border-gray-300 px-2 py-1"></td>
<td className="px-2 py-1 text-right font-semibold">{subtotal.toLocaleString()}</td>
</tr>
</tbody>
</table>

View File

@@ -158,6 +158,8 @@ export interface QuoteApiData {
supply_amount: string | number;
tax_amount: string | number;
total_amount: string | number;
discount_rate?: number | null; // 할인율 (%)
discount_amount?: number | null; // 할인 금액
status: QuoteStatus;
current_revision: number;
is_final: boolean;
@@ -688,6 +690,8 @@ export interface QuoteFormDataV2 {
dueDate: string;
remarks: string;
status: 'draft' | 'temporary' | 'final'; // 작성중, 임시저장, 최종저장
discountRate: number; // 할인율 (%)
discountAmount: number; // 할인 금액
locations: LocationItem[];
}
@@ -853,6 +857,8 @@ export function transformV2ToApi(
quantity: data.locations.reduce((sum, loc) => sum + loc.quantity, 0),
unit_symbol: '개소',
total_amount: grandTotal,
discount_rate: data.discountRate || 0,
discount_amount: data.discountAmount || 0,
status: data.status === 'final' ? 'finalized' : 'draft',
is_final: data.status === 'final',
calculation_inputs: calculationInputs,
@@ -972,6 +978,8 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
// raw API: remarks || description, transformed: description
remarks: apiData.remarks || apiData.description || transformed.description || '',
status: mapStatus(apiData.status),
discountRate: Number(apiData.discount_rate) || 0,
discountAmount: Number(apiData.discount_amount) || 0,
locations: locations,
};
}