From 5f1a2117224f2504d36219ec972618303b07c196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Mar 2026 23:21:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[approval]=20=EA=B2=AC=EC=A0=81?= =?UTF-8?q?=EC=84=9C=20=EC=96=91=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 견적서 전용 폼/조회 파셜 추가 - create/edit/show 페이지에 견적서 통합 - Alpine.js 동적 품목 테이블 (자동 세액 계산) - 공급자 정보 테넌트에서 자동 로드 - 미리보기/인쇄 기능 --- resources/views/approvals/create.blade.php | 207 +++++++++++++- resources/views/approvals/edit.blade.php | 226 ++++++++++++++- .../partials/_quotation-form.blade.php | 265 ++++++++++++++++++ .../partials/_quotation-show.blade.php | 196 +++++++++++++ resources/views/approvals/show.blade.php | 2 + 5 files changed, 891 insertions(+), 5 deletions(-) create mode 100644 resources/views/approvals/partials/_quotation-form.blade.php create mode 100644 resources/views/approvals/partials/_quotation-show.blade.php diff --git a/resources/views/approvals/create.blade.php b/resources/views/approvals/create.blade.php index cdbe209a..bd96815e 100644 --- a/resources/views/approvals/create.blade.php +++ b/resources/views/approvals/create.blade.php @@ -145,6 +145,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- 'tenantInfo' => $tenantInfo ?? [], ]) + {{-- 견적서 전용 폼 --}} + @include('approvals.partials._quotation-form', [ + 'tenantInfo' => $tenantInfo ?? [], + ]) + {{-- 지출결의서 전용 폼 --}} @include('approvals.partials._expense-form', [ 'initialData' => [], @@ -334,6 +339,11 @@ class="p-1 text-gray-400 hover:text-gray-600 transition"> color: 'border-slate-300 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600', text: '이사회 개최 내용을 기록하는 공식 문서입니다. 일시, 장소, 출석현황, 의안, 의사경과, 기명날인 등을 기재하며, 법적 효력을 갖는 회의록입니다.', }, + quotation: { + title: '견적서', icon: '💵', + color: 'border-green-200 bg-green-50', titleColor: 'text-green-800', textColor: 'text-green-600', + text: '고객에게 제공할 물품/서비스의 가격을 견적하는 문서입니다. 품목, 수량, 단가, 공급가액, 세액을 기재하며, 승인 후 견적서로 사용할 수 있습니다.', + }, pr_expense: { title: '지출품의서', icon: '📋', color: 'border-orange-200 bg-orange-50', titleColor: 'text-orange-800', textColor: 'text-orange-700', @@ -367,7 +377,7 @@ class="p-1 text-gray-400 hover:text-gray-600 transition"> leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태', employment_cert: '증명서', career_cert: '증명서', appointment_cert: '증명서', seal_usage: '증명서', pr_expense: '품의', pr_contract: '품의', pr_purchase: '품의', pr_trip: '품의', pr_settlement: '품의', - expense: '재무', + quotation: '재무', expense: '재무', }; const categoryIcons = { '일반': '📄', '인사/근태': '👤', '증명서': '📜', '품의': '📋', '재무': '💰', @@ -454,6 +464,7 @@ function updateFormDescription(formId) { let isSealUsageForm = false; let isDelegationForm = false; let isBoardMinutesForm = false; +let isQuotationForm = false; // 양식코드별 표시할 유형 목록 const leaveTypesByFormCode = { @@ -705,6 +716,7 @@ function switchFormMode(formId) { const sealUsageContainer = document.getElementById('seal-usage-form-container'); const delegationContainer = document.getElementById('delegation-form-container'); const boardMinutesContainer = document.getElementById('board-minutes-form-container'); + const quotationContainer = document.getElementById('quotation-form-container'); const bodyArea = document.getElementById('body-area'); const expenseLoadBtn = document.getElementById('expense-load-btn'); @@ -721,6 +733,7 @@ function switchFormMode(formId) { sealUsageContainer.style.display = 'none'; delegationContainer.style.display = 'none'; boardMinutesContainer.style.display = 'none'; + quotationContainer.style.display = 'none'; expenseLoadBtn.style.display = 'none'; bodyArea.style.display = 'none'; isExpenseForm = false; @@ -733,6 +746,7 @@ function switchFormMode(formId) { isSealUsageForm = false; isDelegationForm = false; isBoardMinutesForm = false; + isQuotationForm = false; if (code === 'expense') { isExpenseForm = true; @@ -801,6 +815,9 @@ function switchFormMode(formId) { } else if (code === 'board_minutes') { isBoardMinutesForm = true; boardMinutesContainer.style.display = ''; + } else if (code === 'quotation') { + isQuotationForm = true; + quotationContainer.style.display = ''; } else { bodyArea.style.display = ''; } @@ -811,7 +828,7 @@ function applyBodyTemplate(formId) { switchFormMode(formId); // 전용 폼이면 제목을 양식명으로 설정하고 body template 적용 건너뜀 - if (isExpenseForm || isPurchaseRequestForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm) { + if (isExpenseForm || isPurchaseRequestForm || isLeaveForm || isCertForm || isCareerCertForm || isAppointmentCertForm || isResignationForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm) { const titleEl = document.getElementById('title'); const formSelect = document.getElementById('form_id'); titleEl.value = formSelect.options[formSelect.selectedIndex].text; @@ -1074,6 +1091,55 @@ function applyBodyTemplate(formId) { meeting_date: bmDatetime.split('T')[0], }; formBody = null; + } else if (isQuotationForm) { + const qtClientName = document.getElementById('qt-client-name').value.trim(); + if (!qtClientName) { + showToast('수신(고객명)을 입력해주세요.', 'warning'); + return; + } + const qtQuoteDate = document.getElementById('qt-quote-date').value; + if (!qtQuoteDate) { + showToast('견적일자를 입력해주세요.', 'warning'); + return; + } + + const qtContainer = document.getElementById('quotation-form-container'); + const qtAlpine = qtContainer._x_dataStack?.[0]; + const qtItems = qtAlpine ? qtAlpine.items : []; + + if (qtItems.length === 0 || !qtItems[0].name.trim()) { + showToast('품목을 1건 이상 입력해주세요.', 'warning'); + return; + } + + const mappedItems = qtItems.filter(i => i.name.trim()).map(i => ({ + name: i.name.trim(), + spec: (i.spec || '').trim(), + qty: parseInt(i.qty) || 0, + unit_price: parseInt(i.unit_price) || 0, + supply_amount: (parseInt(i.qty) || 0) * (parseInt(i.unit_price) || 0), + tax: qtAlpine.itemTax(i), + note: (i.note || '').trim(), + })); + + formContent = { + client_name: qtClientName, + quote_date: qtQuoteDate, + company_name: document.getElementById('qt-company-name').value, + business_num: document.getElementById('qt-business-num').value, + ceo_name: document.getElementById('qt-ceo-name').value, + company_address: document.getElementById('qt-company-address').value, + business_type: document.getElementById('qt-business-type-input')?.value || document.getElementById('qt-business-type').value, + business_item: document.getElementById('qt-business-item-input')?.value || document.getElementById('qt-business-item').value, + phone: document.getElementById('qt-phone-input')?.value || document.getElementById('qt-phone').value, + bank_account: document.getElementById('qt-bank-input')?.value || document.getElementById('qt-bank-account').value, + items: mappedItems, + total_supply: qtAlpine ? qtAlpine.totalSupply() : 0, + total_tax: qtAlpine ? qtAlpine.totalTax() : 0, + total_amount: qtAlpine ? qtAlpine.totalAmount() : 0, + remarks: document.getElementById('qt-remarks').value.trim(), + }; + formBody = null; } else if (isDelegationForm) { const dlAgentName = document.getElementById('dl-agent-name').value.trim(); if (!dlAgentName) { @@ -2228,6 +2294,143 @@ function printBoardMinutesPreview() { win.print(); } +// ─── 견적서 미리보기 ─── +function buildQuotationPreviewHtml(data) { + const fmt = n => (n || 0).toLocaleString('ko-KR'); + let itemsHtml = ''; + (data.items || []).forEach((item, idx) => { + itemsHtml += ` + ${idx + 1} + ${item.name || ''} + ${item.spec || ''} + ${item.qty || 0} + ${fmt(item.unit_price)} + ${fmt(item.supply_amount)} + ${fmt(item.tax)} + ${item.note || ''} + `; + }); + + return ` +
+

견 적 서

+
+
+
+

${data.client_name || ''} 귀하

+

아래와 같이 견적합니다.

+
+
+

견적일자: ${data.quote_date || ''}

+
+
+
+

견적금액

+

₩ ${fmt(data.total_amount)}

+

(공급가액 ${fmt(data.total_supply)} + 부가세 ${fmt(data.total_tax)})

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
공급자
사업자등록번호${data.business_num || ''}상호${data.company_name || ''}대표자${data.ceo_name || ''}
소재지${data.company_address || ''}
업태${data.business_type || ''}업종${data.business_item || ''}
연락처${data.phone || ''}계좌${data.bank_account || ''}
+ + + + + + + + + + + + + + ${itemsHtml} + + + + + + + + +
No품명규격수량단가공급가액세액비고
합 계${fmt(data.total_supply)}${fmt(data.total_tax)}
+ ${data.remarks ? `
특이사항

${data.remarks}

` : ''} + `; +} + +function openQuotationPreview() { + const qtContainer = document.getElementById('quotation-form-container'); + const qtAlpine = qtContainer._x_dataStack?.[0]; + const qtItems = qtAlpine ? qtAlpine.items.filter(i => i.name.trim()).map(i => ({ + name: i.name.trim(), spec: (i.spec||'').trim(), qty: parseInt(i.qty)||0, unit_price: parseInt(i.unit_price)||0, + supply_amount: (parseInt(i.qty)||0) * (parseInt(i.unit_price)||0), tax: qtAlpine.itemTax(i), note: (i.note||'').trim(), + })) : []; + const data = { + client_name: document.getElementById('qt-client-name').value.trim(), + quote_date: document.getElementById('qt-quote-date').value, + company_name: document.getElementById('qt-company-name').value, + business_num: document.getElementById('qt-business-num').value, + ceo_name: document.getElementById('qt-ceo-name').value, + company_address: document.getElementById('qt-company-address').value, + business_type: document.getElementById('qt-business-type-input')?.value || '', + business_item: document.getElementById('qt-business-item-input')?.value || '', + phone: document.getElementById('qt-phone-input')?.value || '', + bank_account: document.getElementById('qt-bank-input')?.value || '', + items: qtItems, + total_supply: qtAlpine ? qtAlpine.totalSupply() : 0, + total_tax: qtAlpine ? qtAlpine.totalTax() : 0, + total_amount: qtAlpine ? qtAlpine.totalAmount() : 0, + remarks: document.getElementById('qt-remarks').value.trim(), + }; + document.getElementById('quotation-preview-content').innerHTML = buildQuotationPreviewHtml(data); + document.getElementById('quotation-preview-modal').style.display = ''; + document.body.style.overflow = 'hidden'; +} + +function closeQuotationPreview() { + document.getElementById('quotation-preview-modal').style.display = 'none'; + document.body.style.overflow = ''; +} + +function printQuotationPreview() { + const content = document.getElementById('quotation-preview-content').innerHTML; + const win = window.open('', '_blank'); + win.document.write('견적서'); + win.document.write(''); + win.document.write(''); + win.document.write(content); + win.document.write(''); + win.document.close(); + win.print(); +} + function printDelegationPreview() { const content = document.getElementById('delegation-preview-content').innerHTML; const win = window.open('', '_blank'); diff --git a/resources/views/approvals/edit.blade.php b/resources/views/approvals/edit.blade.php index d3434a92..496e0233 100644 --- a/resources/views/approvals/edit.blade.php +++ b/resources/views/approvals/edit.blade.php @@ -163,6 +163,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- 'tenantInfo' => $tenantInfo ?? [], ]) + {{-- 견적서 전용 폼 --}} + @include('approvals.partials._quotation-form', [ + 'tenantInfo' => $tenantInfo ?? [], + ]) + {{-- 지출결의서 전용 폼 --}} @php $existingFiles = []; @@ -302,6 +307,7 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon let isSealUsageForm = false; let isDelegationForm = false; let isBoardMinutesForm = false; +let isQuotationForm = false; const formDescriptions = { BUSINESS_DRAFT: { @@ -364,6 +370,11 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon color: 'border-slate-300 bg-slate-50', titleColor: 'text-slate-800', textColor: 'text-slate-600', text: '이사회 개최 내용을 기록하는 공식 문서입니다. 일시, 장소, 출석현황, 의안, 의사경과, 기명날인 등을 기재하며, 법적 효력을 갖는 회의록입니다.', }, + quotation: { + title: '견적서', icon: '💰', + color: 'border-emerald-200 bg-emerald-50', titleColor: 'text-emerald-800', textColor: 'text-emerald-600', + text: '고객에게 제출할 견적서를 작성하는 문서입니다. 품목, 수량, 단가, 공급가액, 부가세를 기재하며, 승인 후 공식 견적서로 사용할 수 있습니다.', + }, pr_expense: { title: '지출품의서', icon: '📋', color: 'border-orange-200 bg-orange-50', titleColor: 'text-orange-800', textColor: 'text-orange-700', @@ -397,7 +408,7 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon leave: '인사/근태', attendance_request: '인사/근태', resignation: '인사/근태', reason_report: '인사/근태', delegation: '인사/근태', board_minutes: '인사/근태', employment_cert: '증명서', career_cert: '증명서', appointment_cert: '증명서', seal_usage: '증명서', pr_expense: '품의', pr_contract: '품의', pr_purchase: '품의', pr_trip: '품의', pr_settlement: '품의', - expense: '재무', + expense: '재무', quotation: '재무', }; const categoryIcons = { '일반': '📄', '인사/근태': '👤', '증명서': '📜', '품의': '📋', '재무': '💰', @@ -637,6 +648,7 @@ function switchFormMode(formId) { const sealUsageContainer = document.getElementById('seal-usage-form-container'); const delegationContainer = document.getElementById('delegation-form-container'); const boardMinutesContainer = document.getElementById('board-minutes-form-container'); + const quotationContainer = document.getElementById('quotation-form-container'); const bodyArea = document.getElementById('body-area'); expenseContainer.style.display = 'none'; @@ -645,6 +657,7 @@ function switchFormMode(formId) { sealUsageContainer.style.display = 'none'; delegationContainer.style.display = 'none'; boardMinutesContainer.style.display = 'none'; + quotationContainer.style.display = 'none'; bodyArea.style.display = 'none'; isExpenseForm = false; isPurchaseRequestForm = false; @@ -652,6 +665,7 @@ function switchFormMode(formId) { isSealUsageForm = false; isDelegationForm = false; isBoardMinutesForm = false; + isQuotationForm = false; if (code === 'expense') { isExpenseForm = true; @@ -677,6 +691,9 @@ function switchFormMode(formId) { } else if (code === 'board_minutes') { isBoardMinutesForm = true; boardMinutesContainer.style.display = ''; + } else if (code === 'quotation') { + isQuotationForm = true; + quotationContainer.style.display = ''; } else { bodyArea.style.display = ''; } @@ -687,7 +704,7 @@ function applyBodyTemplate(formId) { switchFormMode(formId); // 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀 - if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm) { + if (isExpenseForm || isPurchaseRequestForm || isCertForm || isSealUsageForm || isDelegationForm || isBoardMinutesForm || isQuotationForm) { const titleEl = document.getElementById('title'); if (!titleEl.value.trim()) { const formSelect = document.getElementById('form_id'); @@ -831,8 +848,42 @@ function applyBodyTemplate(formId) { } } + // 견적서 기존 데이터 복원 + if (isQuotationForm) { + const qtContent = @json($approval->content ?? []); + if (qtContent.client_name) { + document.getElementById('qt-client-name').value = qtContent.client_name || ''; + document.getElementById('qt-quote-date').value = qtContent.quote_date || ''; + document.getElementById('qt-remarks').value = qtContent.remarks || ''; + if (qtContent.business_type) { + const btInput = document.getElementById('qt-business-type-input'); + if (btInput) btInput.value = qtContent.business_type; + } + if (qtContent.business_item) { + const biInput = document.getElementById('qt-business-item-input'); + if (biInput) biInput.value = qtContent.business_item; + } + if (qtContent.phone) { + const phInput = document.getElementById('qt-phone-input'); + if (phInput) phInput.value = qtContent.phone; + } + if (qtContent.bank_account) { + const baInput = document.getElementById('qt-bank-input'); + if (baInput) baInput.value = qtContent.bank_account; + } + + const qtAlpine = document.getElementById('quotation-form-container')._x_dataStack?.[0]; + if (qtAlpine && qtContent.items && qtContent.items.length > 0) { + qtAlpine.items = qtContent.items.map(i => ({ + name: i.name || '', spec: i.spec || '', qty: i.qty || 1, + unit_price: i.unit_price || 0, tax: i.tax || 0, note: i.note || '', + })); + } + } + } + // 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화 - if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm) { + if (!isExpenseForm && !isPurchaseRequestForm && !isCertForm && !isSealUsageForm && !isDelegationForm && !isBoardMinutesForm && !isQuotationForm) { const existingBody = document.getElementById('body').value; if (/<[a-z][\s\S]*>/i.test(existingBody)) { document.getElementById('useEditor').checked = true; @@ -961,6 +1012,38 @@ function applyBodyTemplate(formId) { meeting_date: bmDatetime.split('T')[0], }; formBody = null; + } else if (isQuotationForm) { + const qtClientName = document.getElementById('qt-client-name').value.trim(); + if (!qtClientName) { showToast('수신(고객명)을 입력해주세요.', 'warning'); return; } + const qtQuoteDate = document.getElementById('qt-quote-date').value; + if (!qtQuoteDate) { showToast('견적일자를 입력해주세요.', 'warning'); return; } + + const qtContainer = document.getElementById('quotation-form-container'); + const qtAlpine = qtContainer._x_dataStack?.[0]; + const qtItems = qtAlpine ? qtAlpine.items : []; + if (qtItems.length === 0 || !qtItems[0].name.trim()) { showToast('품목을 1건 이상 입력해주세요.', 'warning'); return; } + + const mappedItems = qtItems.filter(i => i.name.trim()).map(i => ({ + name: i.name.trim(), spec: (i.spec || '').trim(), qty: parseInt(i.qty) || 0, + unit_price: parseInt(i.unit_price) || 0, supply_amount: (parseInt(i.qty) || 0) * (parseInt(i.unit_price) || 0), + tax: qtAlpine.itemTax(i), note: (i.note || '').trim(), + })); + + formContent = { + client_name: qtClientName, quote_date: qtQuoteDate, + company_name: document.getElementById('qt-company-name').value, + business_num: document.getElementById('qt-business-num').value, + ceo_name: document.getElementById('qt-ceo-name').value, + company_address: document.getElementById('qt-company-address').value, + business_type: document.getElementById('qt-business-type-input')?.value || document.getElementById('qt-business-type').value, + business_item: document.getElementById('qt-business-item-input')?.value || document.getElementById('qt-business-item').value, + phone: document.getElementById('qt-phone-input')?.value || document.getElementById('qt-phone').value, + bank_account: document.getElementById('qt-bank-input')?.value || document.getElementById('qt-bank-account').value, + items: mappedItems, total_supply: qtAlpine ? qtAlpine.totalSupply() : 0, + total_tax: qtAlpine ? qtAlpine.totalTax() : 0, total_amount: qtAlpine ? qtAlpine.totalAmount() : 0, + remarks: document.getElementById('qt-remarks').value.trim(), + }; + formBody = null; } else if (isDelegationForm) { const dlAgentName = document.getElementById('dl-agent-name').value.trim(); if (!dlAgentName) { @@ -1490,5 +1573,142 @@ function printDelegationPreview() { win.document.close(); win.print(); } + +// ─── 견적서 미리보기 ─── +function buildQuotationPreviewHtml(data) { + const fmt = n => (n || 0).toLocaleString('ko-KR'); + let itemsHtml = ''; + (data.items || []).forEach((item, idx) => { + itemsHtml += ` + ${idx + 1} + ${item.name || ''} + ${item.spec || ''} + ${item.qty || 0} + ${fmt(item.unit_price)} + ${fmt(item.supply_amount)} + ${fmt(item.tax)} + ${item.note || ''} + `; + }); + + return ` +
+

견 적 서

+
+
+
+

${data.client_name || ''} 귀하

+

아래와 같이 견적합니다.

+
+
+

견적일자: ${data.quote_date || ''}

+
+
+
+

견적금액

+

₩ ${fmt(data.total_amount)}

+

(공급가액 ${fmt(data.total_supply)} + 부가세 ${fmt(data.total_tax)})

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
공급자
사업자등록번호${data.business_num || ''}상호${data.company_name || ''}대표자${data.ceo_name || ''}
소재지${data.company_address || ''}
업태${data.business_type || ''}업종${data.business_item || ''}
연락처${data.phone || ''}계좌${data.bank_account || ''}
+ + + + + + + + + + + + + + ${itemsHtml} + + + + + + + + +
No품명규격수량단가공급가액세액비고
합 계${fmt(data.total_supply)}${fmt(data.total_tax)}
+ ${data.remarks ? `
특이사항

${data.remarks}

` : ''} + `; +} + +function openQuotationPreview() { + const qtContainer = document.getElementById('quotation-form-container'); + const qtAlpine = qtContainer._x_dataStack?.[0]; + const qtItems = qtAlpine ? qtAlpine.items.filter(i => i.name.trim()).map(i => ({ + name: i.name.trim(), spec: (i.spec||'').trim(), qty: parseInt(i.qty)||0, unit_price: parseInt(i.unit_price)||0, + supply_amount: (parseInt(i.qty)||0) * (parseInt(i.unit_price)||0), tax: qtAlpine.itemTax(i), note: (i.note||'').trim(), + })) : []; + const data = { + client_name: document.getElementById('qt-client-name').value.trim(), + quote_date: document.getElementById('qt-quote-date').value, + company_name: document.getElementById('qt-company-name').value, + business_num: document.getElementById('qt-business-num').value, + ceo_name: document.getElementById('qt-ceo-name').value, + company_address: document.getElementById('qt-company-address').value, + business_type: document.getElementById('qt-business-type-input')?.value || '', + business_item: document.getElementById('qt-business-item-input')?.value || '', + phone: document.getElementById('qt-phone-input')?.value || '', + bank_account: document.getElementById('qt-bank-input')?.value || '', + items: qtItems, + total_supply: qtAlpine ? qtAlpine.totalSupply() : 0, + total_tax: qtAlpine ? qtAlpine.totalTax() : 0, + total_amount: qtAlpine ? qtAlpine.totalAmount() : 0, + remarks: document.getElementById('qt-remarks').value.trim(), + }; + document.getElementById('quotation-preview-content').innerHTML = buildQuotationPreviewHtml(data); + document.getElementById('quotation-preview-modal').style.display = ''; + document.body.style.overflow = 'hidden'; +} + +function closeQuotationPreview() { + document.getElementById('quotation-preview-modal').style.display = 'none'; + document.body.style.overflow = ''; +} + +function printQuotationPreview() { + const content = document.getElementById('quotation-preview-content').innerHTML; + const win = window.open('', '_blank'); + win.document.write('견적서'); + win.document.write(''); + win.document.write(''); + win.document.write(content); + win.document.write(''); + win.document.close(); + win.print(); +} @endpush diff --git a/resources/views/approvals/partials/_quotation-form.blade.php b/resources/views/approvals/partials/_quotation-form.blade.php new file mode 100644 index 00000000..0ac9a1e3 --- /dev/null +++ b/resources/views/approvals/partials/_quotation-form.blade.php @@ -0,0 +1,265 @@ +{{-- + 견적서 전용 폼 + Props: + $tenantInfo (array) - 테넌트(회사) 정보 +--}} +@php + $tenantInfo = $tenantInfo ?? []; +@endphp + + + +{{-- 견적서 미리보기 모달 --}} + diff --git a/resources/views/approvals/partials/_quotation-show.blade.php b/resources/views/approvals/partials/_quotation-show.blade.php new file mode 100644 index 00000000..aeed8cd9 --- /dev/null +++ b/resources/views/approvals/partials/_quotation-show.blade.php @@ -0,0 +1,196 @@ +{{-- + 견적서 읽기전용 렌더링 + Props: + $content (array) - approvals.content JSON +--}} +
+ {{-- 미리보기 버튼 --}} +
+ +
+ + {{-- 수신/일자 --}} +
+
+

수신 정보

+
+
+
+
+ 수신 (고객명) +
{{ $content['client_name'] ?? '-' }}
+
+
+ 견적일자 +
{{ $content['quote_date'] ?? '-' }}
+
+
+
+
+ + {{-- 공급자 --}} +
+
+

공급자

+
+
+
+
+ 상호 +
{{ $content['company_name'] ?? '-' }}
+
+
+ 대표자 +
{{ $content['ceo_name'] ?? '-' }}
+
+
+ 사업자등록번호 +
{{ $content['business_num'] ?? '-' }}
+
+
+
+
+ + {{-- 품목 테이블 --}} +
+
+

견적 품목

+
+
+ + + + + + + + + + + + + + + @foreach(($content['items'] ?? []) as $idx => $item) + + + + + + + + + + + @endforeach + + + + + + + + + + + + + + +
#품명규격수량단가공급가액세액비고
{{ $idx + 1 }}{{ $item['name'] ?? '' }}{{ $item['spec'] ?? '' }}{{ $item['qty'] ?? 0 }}{{ number_format($item['unit_price'] ?? 0) }}{{ number_format($item['supply_amount'] ?? 0) }}{{ number_format($item['tax'] ?? 0) }}{{ $item['note'] ?? '' }}
합 계{{ number_format($content['total_supply'] ?? 0) }}{{ number_format($content['total_tax'] ?? 0) }}
견적금액{{ number_format($content['total_amount'] ?? 0) }}원
+
+
+ + {{-- 특이사항 --}} + @if(!empty($content['remarks'])) +
+
+

특이사항

+
+
+
{{ $content['remarks'] }}
+
+
+ @endif +
+ +{{-- 미리보기 모달 --}} + + +@push('scripts') + +@endpush diff --git a/resources/views/approvals/show.blade.php b/resources/views/approvals/show.blade.php index 6f62744e..0c709b8d 100644 --- a/resources/views/approvals/show.blade.php +++ b/resources/views/approvals/show.blade.php @@ -119,6 +119,8 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition @include('approvals.partials._delegation-show', ['content' => $approval->content]) @elseif(!empty($approval->content) && $approval->form?->code === 'board_minutes') @include('approvals.partials._board-minutes-show', ['content' => $approval->content]) + @elseif(!empty($approval->content) && $approval->form?->code === 'quotation') + @include('approvals.partials._quotation-show', ['content' => $approval->content]) @elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
{!! strip_tags($approval->body, '