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 || ''} |
+
+
+
+
+
+
+ | No |
+ 품명 |
+ 규격 |
+ 수량 |
+ 단가 |
+ 공급가액 |
+ 세액 |
+ 비고 |
+
+
+ ${itemsHtml}
+
+
+ | 합 계 |
+ ${fmt(data.total_supply)} |
+ ${fmt(data.total_tax)} |
+ |
+
+
+
+ ${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 || ''} |
+
+
+
+
+
+
+ | No |
+ 품명 |
+ 규격 |
+ 수량 |
+ 단가 |
+ 공급가액 |
+ 세액 |
+ 비고 |
+
+
+ ${itemsHtml}
+
+
+ | 합 계 |
+ ${fmt(data.total_supply)} |
+ ${fmt(data.total_tax)} |
+ |
+
+
+
+ ${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)
+
+ | {{ $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'] ?? '' }} |
+
+ @endforeach
+
+
+
+ | 합 계 |
+ {{ 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, '