- 견적서 전용 폼/조회 파셜 추가 - create/edit/show 페이지에 견적서 통합 - Alpine.js 동적 품목 테이블 (자동 세액 계산) - 공급자 정보 테넌트에서 자동 로드 - 미리보기/인쇄 기능
266 lines
17 KiB
PHP
266 lines
17 KiB
PHP
{{--
|
|
견적서 전용 폼
|
|
Props:
|
|
$tenantInfo (array) - 테넌트(회사) 정보
|
|
--}}
|
|
@php
|
|
$tenantInfo = $tenantInfo ?? [];
|
|
@endphp
|
|
|
|
<div id="quotation-form-container" style="display: none;" class="mb-4"
|
|
x-data="{
|
|
items: [{ name: '', spec: '', qty: 1, unit_price: 0, tax: 0, note: '' }],
|
|
taxRate: 10,
|
|
autoTax: true,
|
|
addItem() {
|
|
this.items.push({ name: '', spec: '', qty: 1, unit_price: 0, tax: 0, note: '' });
|
|
},
|
|
removeItem(idx) {
|
|
if (this.items.length <= 1) return;
|
|
this.items.splice(idx, 1);
|
|
},
|
|
supplyAmount(item) {
|
|
return (parseInt(item.qty) || 0) * (parseInt(item.unit_price) || 0);
|
|
},
|
|
itemTax(item) {
|
|
if (this.autoTax) return Math.round(this.supplyAmount(item) * this.taxRate / 100);
|
|
return parseInt(item.tax) || 0;
|
|
},
|
|
totalSupply() {
|
|
return this.items.reduce((s, item) => s + this.supplyAmount(item), 0);
|
|
},
|
|
totalTax() {
|
|
return this.items.reduce((s, item) => s + this.itemTax(item), 0);
|
|
},
|
|
totalAmount() {
|
|
return this.totalSupply() + this.totalTax();
|
|
},
|
|
formatMoney(n) {
|
|
return n.toLocaleString('ko-KR');
|
|
},
|
|
}">
|
|
<input type="hidden" id="qt-company-name" value="{{ $tenantInfo['company_name'] ?? '' }}">
|
|
<input type="hidden" id="qt-business-num" value="{{ $tenantInfo['business_num'] ?? '' }}">
|
|
<input type="hidden" id="qt-ceo-name" value="{{ $tenantInfo['ceo_name'] ?? '' }}">
|
|
<input type="hidden" id="qt-company-address" value="{{ $tenantInfo['address'] ?? '' }}">
|
|
<input type="hidden" id="qt-business-type" value="{{ $tenantInfo['business_type'] ?? '' }}">
|
|
<input type="hidden" id="qt-business-item" value="{{ $tenantInfo['business_item'] ?? '' }}">
|
|
<input type="hidden" id="qt-phone" value="{{ $tenantInfo['phone'] ?? '' }}">
|
|
<input type="hidden" id="qt-bank-account" value="{{ $tenantInfo['bank_account'] ?? '' }}">
|
|
|
|
<div class="space-y-4">
|
|
{{-- 1. 수신/일자 --}}
|
|
<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">1. 수신 정보</h3>
|
|
</div>
|
|
<div class="p-4 space-y-3">
|
|
<div class="flex gap-4 flex-wrap">
|
|
<div style="flex: 1 1 250px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">수신 (고객명) <span class="text-red-500">*</span></label>
|
|
<input type="text" id="qt-client-name" placeholder="고객명 귀하"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
<div style="flex: 0 0 180px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">견적일자 <span class="text-red-500">*</span></label>
|
|
<input type="date" id="qt-quote-date" value="{{ now()->format('Y-m-d') }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 2. 공급자 정보 (읽기전용) --}}
|
|
<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">2. 공급자</h3>
|
|
</div>
|
|
<div class="p-4 space-y-3">
|
|
<div class="flex gap-4 flex-wrap">
|
|
<div style="flex: 1 1 200px; max-width: 250px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">사업자등록번호</label>
|
|
<input type="text" readonly value="{{ $tenantInfo['business_num'] ?? '' }}"
|
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
|
</div>
|
|
<div style="flex: 1 1 200px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">상호</label>
|
|
<input type="text" readonly value="{{ $tenantInfo['company_name'] ?? '' }}"
|
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
|
</div>
|
|
<div style="flex: 0 0 150px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">대표자</label>
|
|
<input type="text" readonly value="{{ $tenantInfo['ceo_name'] ?? '' }}"
|
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">사업장 소재지</label>
|
|
<input type="text" readonly value="{{ $tenantInfo['address'] ?? '' }}"
|
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-700">
|
|
</div>
|
|
<div class="flex gap-4 flex-wrap">
|
|
<div style="flex: 1 1 180px; max-width: 220px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">업태</label>
|
|
<input type="text" id="qt-business-type-input" value="{{ $tenantInfo['business_type'] ?? '' }}" placeholder="업태"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
<div style="flex: 1 1 250px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">업종</label>
|
|
<input type="text" id="qt-business-item-input" value="{{ $tenantInfo['business_item'] ?? '' }}" placeholder="업종"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-4 flex-wrap">
|
|
<div style="flex: 1 1 200px; max-width: 250px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">연락처</label>
|
|
<input type="text" id="qt-phone-input" value="{{ $tenantInfo['phone'] ?? '' }}" placeholder="연락처"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
<div style="flex: 1 1 250px;">
|
|
<label class="block text-xs font-medium text-gray-500 mb-1">계좌번호</label>
|
|
<input type="text" id="qt-bank-input" value="{{ $tenantInfo['bank_account'] ?? '' }}" placeholder="(은행명) 계좌번호"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 3. 품목 목록 --}}
|
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
|
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-gray-700">3. 견적 품목</h3>
|
|
<div class="flex items-center gap-3">
|
|
<label class="inline-flex items-center gap-1 text-xs text-gray-500 cursor-pointer">
|
|
<input type="checkbox" x-model="autoTax" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
부가세 자동(10%)
|
|
</label>
|
|
<button type="button" @click="addItem()"
|
|
class="px-2 py-1 bg-blue-50 text-blue-600 hover:bg-blue-100 border border-blue-200 rounded text-xs font-medium transition">+ 품목 추가</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 overflow-x-auto">
|
|
<table class="w-full text-sm" style="min-width: 700px;">
|
|
<thead>
|
|
<tr class="bg-gray-50 text-xs text-gray-600">
|
|
<th class="px-2 py-2 text-left" style="width: 32px;">#</th>
|
|
<th class="px-2 py-2 text-left" style="min-width: 140px;">품명</th>
|
|
<th class="px-2 py-2 text-left" style="width: 80px;">규격</th>
|
|
<th class="px-2 py-2 text-right" style="width: 60px;">수량</th>
|
|
<th class="px-2 py-2 text-right" style="width: 110px;">단가</th>
|
|
<th class="px-2 py-2 text-right" style="width: 110px;">공급가액</th>
|
|
<th class="px-2 py-2 text-right" style="width: 100px;">세액</th>
|
|
<th class="px-2 py-2 text-left" style="width: 90px;">비고</th>
|
|
<th class="px-2 py-2" style="width: 32px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="(item, idx) in items" :key="idx">
|
|
<tr class="border-t border-gray-100">
|
|
<td class="px-2 py-1.5 text-xs text-gray-400" x-text="idx + 1"></td>
|
|
<td class="px-1 py-1">
|
|
<input type="text" x-model="item.name" placeholder="품명"
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</td>
|
|
<td class="px-1 py-1">
|
|
<input type="text" x-model="item.spec" placeholder="규격"
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</td>
|
|
<td class="px-1 py-1">
|
|
<input type="number" x-model.number="item.qty" min="1" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm text-right focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</td>
|
|
<td class="px-1 py-1">
|
|
<input type="text" inputmode="numeric"
|
|
:value="formatMoney(item.unit_price || 0)"
|
|
@focus="$el.value = item.unit_price || ''"
|
|
@blur="item.unit_price = parseInt($el.value.replace(/,/g,'')) || 0; $el.value = formatMoney(item.unit_price)"
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm text-right focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</td>
|
|
<td class="px-2 py-1.5 text-right text-sm font-medium text-gray-700" x-text="formatMoney(supplyAmount(item))"></td>
|
|
<td class="px-2 py-1.5 text-right text-sm text-gray-500" x-text="formatMoney(itemTax(item))"></td>
|
|
<td class="px-1 py-1">
|
|
<input type="text" x-model="item.note" placeholder=""
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
|
|
</td>
|
|
<td class="px-1 py-1 text-center">
|
|
<button type="button" @click="removeItem(idx)" x-show="items.length > 1"
|
|
class="p-1 text-red-400 hover:text-red-600 transition">
|
|
<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="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</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" x-text="formatMoney(totalSupply())"></td>
|
|
<td class="px-2 py-2 text-right text-sm text-blue-700" x-text="formatMoney(totalTax())"></td>
|
|
<td colspan="2"></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" x-text="formatMoney(totalAmount()) + '원'"></td>
|
|
<td colspan="2"></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 4. 특이사항 --}}
|
|
<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">4. 특이사항</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<textarea id="qt-remarks" rows="3" placeholder="예: *부가세액 별도* 특이사항을 적어주세요"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 미리보기 버튼 --}}
|
|
<div class="flex justify-end">
|
|
<button type="button" onclick="openQuotationPreview()"
|
|
class="px-3 py-2 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>
|
|
</div>
|
|
|
|
{{-- 견적서 미리보기 모달 --}}
|
|
<div id="quotation-preview-modal" style="display: none;" class="fixed inset-0 z-50">
|
|
<div class="absolute inset-0 bg-black/50" onclick="closeQuotationPreview()"></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="printQuotationPreview()"
|
|
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="closeQuotationPreview()"
|
|
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-preview-content" style="padding: 40px 48px; font-family: 'Pretendard', 'Malgun Gothic', sans-serif;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|