Files
sam-manage/resources/views/approvals/partials/_quotation-show.blade.php
김보곤 5f1a211722 feat: [approval] 견적서 양식 추가
- 견적서 전용 폼/조회 파셜 추가
- create/edit/show 페이지에 견적서 통합
- Alpine.js 동적 품목 테이블 (자동 세액 계산)
- 공급자 정보 테넌트에서 자동 로드
- 미리보기/인쇄 기능
2026-03-06 23:22:21 +09:00

197 lines
14 KiB
PHP

{{--
견적서 읽기전용 렌더링
Props:
$content (array) - approvals.content JSON
--}}
<div class="space-y-4">
{{-- 미리보기 버튼 --}}
<div class="flex justify-end gap-2">
<button type="button" onclick="openQuotationShowPreview()"
class="px-3 py-1.5 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 border border-indigo-200 rounded-lg text-sm font-medium transition inline-flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
견적서 미리보기
</button>
</div>
{{-- 수신/일자 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">수신 정보</h3>
</div>
<div class="p-4">
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
<div>
<span class="text-xs text-gray-500">수신 (고객명)</span>
<div class="text-sm font-medium mt-0.5">{{ $content['client_name'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">견적일자</span>
<div class="text-sm font-medium mt-0.5">{{ $content['quote_date'] ?? '-' }}</div>
</div>
</div>
</div>
</div>
{{-- 공급자 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">공급자</h3>
</div>
<div class="p-4">
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
<div>
<span class="text-xs text-gray-500">상호</span>
<div class="text-sm font-medium mt-0.5">{{ $content['company_name'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">대표자</span>
<div class="text-sm font-medium mt-0.5">{{ $content['ceo_name'] ?? '-' }}</div>
</div>
<div>
<span class="text-xs text-gray-500">사업자등록번호</span>
<div class="text-sm font-medium mt-0.5">{{ $content['business_num'] ?? '-' }}</div>
</div>
</div>
</div>
</div>
{{-- 품목 테이블 --}}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">견적 품목</h3>
</div>
<div class="p-4 overflow-x-auto">
<table class="w-full text-sm" style="min-width: 600px;">
<thead>
<tr class="bg-gray-50 text-xs text-gray-600 border-b border-gray-200">
<th class="px-2 py-2 text-left">#</th>
<th class="px-2 py-2 text-left">품명</th>
<th class="px-2 py-2 text-left">규격</th>
<th class="px-2 py-2 text-right">수량</th>
<th class="px-2 py-2 text-right">단가</th>
<th class="px-2 py-2 text-right">공급가액</th>
<th class="px-2 py-2 text-right">세액</th>
<th class="px-2 py-2 text-left">비고</th>
</tr>
</thead>
<tbody>
@foreach(($content['items'] ?? []) as $idx => $item)
<tr class="border-t border-gray-100">
<td class="px-2 py-1.5 text-xs text-gray-400">{{ $idx + 1 }}</td>
<td class="px-2 py-1.5">{{ $item['name'] ?? '' }}</td>
<td class="px-2 py-1.5 text-gray-500">{{ $item['spec'] ?? '' }}</td>
<td class="px-2 py-1.5 text-right">{{ $item['qty'] ?? 0 }}</td>
<td class="px-2 py-1.5 text-right">{{ number_format($item['unit_price'] ?? 0) }}</td>
<td class="px-2 py-1.5 text-right font-medium">{{ number_format($item['supply_amount'] ?? 0) }}</td>
<td class="px-2 py-1.5 text-right text-gray-500">{{ number_format($item['tax'] ?? 0) }}</td>
<td class="px-2 py-1.5 text-gray-500">{{ $item['note'] ?? '' }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr class="border-t-2 border-gray-300 bg-gray-50 font-semibold">
<td colspan="5" class="px-2 py-2 text-right text-sm"> </td>
<td class="px-2 py-2 text-right text-sm text-blue-700">{{ number_format($content['total_supply'] ?? 0) }}</td>
<td class="px-2 py-2 text-right text-sm text-blue-700">{{ number_format($content['total_tax'] ?? 0) }}</td>
<td></td>
</tr>
<tr class="bg-blue-50 font-bold">
<td colspan="5" class="px-2 py-2 text-right text-sm text-blue-800">견적금액</td>
<td colspan="2" class="px-2 py-2 text-right text-sm text-blue-800">{{ number_format($content['total_amount'] ?? 0) }}</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
{{-- 특이사항 --}}
@if(!empty($content['remarks']))
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200">
<h3 class="text-sm font-semibold text-gray-700">특이사항</h3>
</div>
<div class="p-4">
<div class="text-sm text-gray-700 whitespace-pre-wrap">{{ $content['remarks'] }}</div>
</div>
</div>
@endif
</div>
{{-- 미리보기 모달 --}}
<div id="quotation-show-preview-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeQuotationShowPreview()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 780px;">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="text-base font-semibold text-gray-800">견적서 미리보기</h3>
<div class="flex items-center gap-2">
<button type="button" onclick="printQuotationShowPreview()"
class="px-3 py-1.5 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg text-xs font-medium transition inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
인쇄
</button>
<button type="button" onclick="closeQuotationShowPreview()"
class="p-1 text-gray-400 hover:text-gray-600 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div class="overflow-y-auto" style="max-height: 80vh;">
<div id="quotation-show-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function buildQuotationShowPreviewHtml(data) {
const fmt = n => (n || 0).toLocaleString('ko-KR');
let itemsHtml = '';
(data.items || []).forEach((item, idx) => {
itemsHtml += '<tr style="border-bottom:1px solid #ddd;">' +
'<td style="padding:6px 8px;text-align:center;font-size:12px;">' + (idx+1) + '</td>' +
'<td style="padding:6px 8px;font-size:12px;">' + (item.name||'') + '</td>' +
'<td style="padding:6px 8px;font-size:12px;color:#555;">' + (item.spec||'') + '</td>' +
'<td style="padding:6px 8px;text-align:right;font-size:12px;">' + (item.qty||0) + '</td>' +
'<td style="padding:6px 8px;text-align:right;font-size:12px;">' + fmt(item.unit_price) + '</td>' +
'<td style="padding:6px 8px;text-align:right;font-size:12px;font-weight:600;">' + fmt(item.supply_amount) + '</td>' +
'<td style="padding:6px 8px;text-align:right;font-size:12px;color:#555;">' + fmt(item.tax) + '</td>' +
'<td style="padding:6px 8px;font-size:12px;color:#555;">' + (item.note||'') + '</td></tr>';
});
return '<div style="text-align:center;margin-bottom:32px;"><h1 style="font-size:28px;font-weight:800;letter-spacing:8px;margin:0;">견 적 서</h1></div>' +
'<div style="display:flex;justify-content:space-between;margin-bottom:24px;"><div style="font-size:14px;"><p style="margin:0 0 4px;"><strong>' + (data.client_name||'') + '</strong> 귀하</p><p style="margin:0;font-size:12px;color:#555;">아래와 같이 견적합니다.</p></div><div style="font-size:12px;text-align:right;color:#555;"><p style="margin:0;">견적일자: ' + (data.quote_date||'') + '</p></div></div>' +
'<div style="background:#EFF6FF;border:2px solid #3B82F6;border-radius:8px;padding:16px 24px;text-align:center;margin-bottom:24px;"><p style="margin:0 0 4px;font-size:13px;color:#555;">견적금액</p><p style="margin:0;font-size:24px;font-weight:800;color:#1E40AF;">₩ ' + fmt(data.total_amount) + '</p><p style="margin:4px 0 0;font-size:11px;color:#666;">(공급가액 ' + fmt(data.total_supply) + ' + 부가세 ' + fmt(data.total_tax) + ')</p></div>' +
'<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:12px;"><caption style="text-align:left;font-weight:700;font-size:13px;margin-bottom:8px;">공급자</caption><tbody>' +
'<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;background:#F9FAFB;width:100px;font-weight:600;font-size:11px;">사업자등록번호</td><td style="padding:6px 8px;">' + (data.business_num||'') + '</td><td style="padding:6px 8px;background:#F9FAFB;width:60px;font-weight:600;font-size:11px;">상호</td><td style="padding:6px 8px;">' + (data.company_name||'') + '</td><td style="padding:6px 8px;background:#F9FAFB;width:60px;font-weight:600;font-size:11px;">대표자</td><td style="padding:6px 8px;">' + (data.ceo_name||'') + '</td></tr>' +
'<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;background:#F9FAFB;font-weight:600;font-size:11px;">소재지</td><td colspan="5" style="padding:6px 8px;">' + (data.company_address||'') + '</td></tr>' +
'</tbody></table>' +
'<table style="width:100%;border-collapse:collapse;margin-bottom:16px;"><thead><tr style="background:#F9FAFB;border-bottom:2px solid #ccc;"><th style="padding:8px;text-align:center;font-size:11px;width:32px;">No</th><th style="padding:8px;text-align:left;font-size:11px;">품명</th><th style="padding:8px;text-align:left;font-size:11px;">규격</th><th style="padding:8px;text-align:right;font-size:11px;">수량</th><th style="padding:8px;text-align:right;font-size:11px;">단가</th><th style="padding:8px;text-align:right;font-size:11px;">공급가액</th><th style="padding:8px;text-align:right;font-size:11px;">세액</th><th style="padding:8px;text-align:left;font-size:11px;">비고</th></tr></thead>' +
'<tbody>' + itemsHtml + '</tbody>' +
'<tfoot><tr style="border-top:2px solid #999;background:#F9FAFB;"><td colspan="5" style="padding:8px;text-align:right;font-weight:700;font-size:12px;">합 계</td><td style="padding:8px;text-align:right;font-weight:700;font-size:12px;color:#1E40AF;">' + fmt(data.total_supply) + '</td><td style="padding:8px;text-align:right;font-weight:700;font-size:12px;color:#1E40AF;">' + fmt(data.total_tax) + '</td><td></td></tr></tfoot></table>' +
(data.remarks ? '<div style="margin-top:16px;padding:12px 16px;background:#FFFBEB;border:1px solid #FDE68A;border-radius:6px;font-size:12px;"><strong style="font-size:11px;color:#92400E;">특이사항</strong><p style="margin:4px 0 0;white-space:pre-wrap;">' + data.remarks + '</p></div>' : '');
}
function openQuotationShowPreview() {
const data = @json($content);
document.getElementById('quotation-show-preview-content').innerHTML = buildQuotationShowPreviewHtml(data);
document.getElementById('quotation-show-preview-modal').style.display = '';
}
function closeQuotationShowPreview() { document.getElementById('quotation-show-preview-modal').style.display = 'none'; }
function printQuotationShowPreview() {
const content = document.getElementById('quotation-show-preview-content').innerHTML;
const win = window.open('', '_blank');
win.document.write('<html><head><title>견적서</title><style>body{font-family:"Pretendard","Malgun Gothic",sans-serif;padding:40px 48px;}@media print{body{padding:20px 30px;}}</style></head><body>' + content + '</body></html>');
win.document.close();
win.print();
}
</script>
@endpush