feat: [approval] 지출결의서 전용 폼 UI 추가
- Alpine.js 기반 지출결의서 전용 폼 컴포넌트 (_expense-form.blade.php) - 지출형식/세금계산서 라디오, 내역 테이블(동적 행 추가/삭제), 금액 자동합계 - 양식 code === 'expense' 시 Quill 대신 전용 폼 표시 (create/edit) - content JSON 구조화 저장, show 페이지 읽기전용 테이블 렌더링 - 기존 Quill 방식 하위 호환 유지
This commit is contained in:
@@ -95,6 +95,7 @@ public function store(Request $request): JsonResponse
|
|||||||
'form_id' => 'required|exists:approval_forms,id',
|
'form_id' => 'required|exists:approval_forms,id',
|
||||||
'title' => 'required|string|max:200',
|
'title' => 'required|string|max:200',
|
||||||
'body' => 'nullable|string',
|
'body' => 'nullable|string',
|
||||||
|
'content' => 'nullable|array',
|
||||||
'is_urgent' => 'boolean',
|
'is_urgent' => 'boolean',
|
||||||
'steps' => 'nullable|array',
|
'steps' => 'nullable|array',
|
||||||
'steps.*.user_id' => 'required_with:steps|exists:users,id',
|
'steps.*.user_id' => 'required_with:steps|exists:users,id',
|
||||||
@@ -118,6 +119,7 @@ public function update(Request $request, int $id): JsonResponse
|
|||||||
$request->validate([
|
$request->validate([
|
||||||
'title' => 'sometimes|string|max:200',
|
'title' => 'sometimes|string|max:200',
|
||||||
'body' => 'nullable|string',
|
'body' => 'nullable|string',
|
||||||
|
'content' => 'nullable|array',
|
||||||
'is_urgent' => 'boolean',
|
'is_urgent' => 'boolean',
|
||||||
'steps' => 'nullable|array',
|
'steps' => 'nullable|array',
|
||||||
'steps.*.user_id' => 'required_with:steps|exists:users,id',
|
'steps.*.user_id' => 'required_with:steps|exists:users,id',
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 본문 --}}
|
{{-- 본문 (일반 양식) --}}
|
||||||
<div class="mb-4">
|
<div id="body-area" class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
본문
|
본문
|
||||||
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
|
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
|
||||||
@@ -81,6 +81,9 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
|||||||
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 지출결의서 전용 폼 --}}
|
||||||
|
@include('approvals.partials._expense-form', ['initialData' => []])
|
||||||
|
|
||||||
{{-- 액션 버튼 --}}
|
{{-- 액션 버튼 --}}
|
||||||
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
|
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
|
||||||
<button onclick="saveApproval('draft')"
|
<button onclick="saveApproval('draft')"
|
||||||
@@ -170,7 +173,9 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
|
|||||||
let quillInstance = null;
|
let quillInstance = null;
|
||||||
var summarySortableInstance = null;
|
var summarySortableInstance = null;
|
||||||
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
|
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
|
||||||
|
const formCodes = @json($forms->pluck('code', 'id'));
|
||||||
const linesData = @json($lines);
|
const linesData = @json($lines);
|
||||||
|
let isExpenseForm = false;
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
@@ -332,7 +337,36 @@ function applyQuickLine(lineId) {
|
|||||||
setTimeout(updateApprovalLineSummary, 100);
|
setTimeout(updateApprovalLineSummary, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchFormMode(formId) {
|
||||||
|
const code = formCodes[formId];
|
||||||
|
const expenseContainer = document.getElementById('expense-form-container');
|
||||||
|
const bodyArea = document.getElementById('body-area');
|
||||||
|
|
||||||
|
if (code === 'expense') {
|
||||||
|
isExpenseForm = true;
|
||||||
|
expenseContainer.style.display = '';
|
||||||
|
bodyArea.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
isExpenseForm = false;
|
||||||
|
expenseContainer.style.display = 'none';
|
||||||
|
bodyArea.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyBodyTemplate(formId) {
|
function applyBodyTemplate(formId) {
|
||||||
|
// 먼저 폼 모드 전환
|
||||||
|
switchFormMode(formId);
|
||||||
|
|
||||||
|
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
||||||
|
if (isExpenseForm) {
|
||||||
|
const titleEl = document.getElementById('title');
|
||||||
|
if (!titleEl.value.trim()) {
|
||||||
|
const formSelect = document.getElementById('form_id');
|
||||||
|
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const template = formBodyTemplates[formId];
|
const template = formBodyTemplates[formId];
|
||||||
if (!template) return;
|
if (!template) return;
|
||||||
|
|
||||||
@@ -373,6 +407,9 @@ function applyBodyTemplate(formId) {
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
setTimeout(updateApprovalLineSummary, 200);
|
setTimeout(updateApprovalLineSummary, 200);
|
||||||
|
|
||||||
|
// 초기 양식에 대한 폼 모드 전환
|
||||||
|
switchFormMode(document.getElementById('form_id').value);
|
||||||
|
|
||||||
// 양식 변경 시 본문 템플릿 자동 채움
|
// 양식 변경 시 본문 템플릿 자동 채움
|
||||||
document.getElementById('form_id').addEventListener('change', function() {
|
document.getElementById('form_id').addEventListener('change', function() {
|
||||||
applyBodyTemplate(this.value);
|
applyBodyTemplate(this.value);
|
||||||
@@ -394,10 +431,19 @@ function applyBodyTemplate(formId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let expenseContent = {};
|
||||||
|
if (isExpenseForm) {
|
||||||
|
const expenseEl = document.getElementById('expense-form-container');
|
||||||
|
if (expenseEl && expenseEl._x_dataStack) {
|
||||||
|
expenseContent = expenseEl._x_dataStack[0].getFormData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
form_id: document.getElementById('form_id').value,
|
form_id: document.getElementById('form_id').value,
|
||||||
title: title,
|
title: title,
|
||||||
body: getBodyContent(),
|
body: isExpenseForm ? null : getBodyContent(),
|
||||||
|
content: isExpenseForm ? expenseContent : {},
|
||||||
is_urgent: document.getElementById('is_urgent').checked,
|
is_urgent: document.getElementById('is_urgent').checked,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ class="rounded border-gray-300 text-red-600 focus:ring-red-500">
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
{{-- 본문 (일반 양식) --}}
|
||||||
|
<div id="body-area" class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
본문
|
본문
|
||||||
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
|
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
|
||||||
@@ -103,6 +104,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
|||||||
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
<div id="quill-container" style="display: none; min-height: 300px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 지출결의서 전용 폼 --}}
|
||||||
|
@include('approvals.partials._expense-form', [
|
||||||
|
'initialData' => $approval->content ?? [],
|
||||||
|
])
|
||||||
|
|
||||||
{{-- 액션 버튼 --}}
|
{{-- 액션 버튼 --}}
|
||||||
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
|
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
|
||||||
<button onclick="updateApproval('save')"
|
<button onclick="updateApproval('save')"
|
||||||
@@ -204,7 +210,9 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
|
|||||||
let quillInstance = null;
|
let quillInstance = null;
|
||||||
var summarySortableInstance = null;
|
var summarySortableInstance = null;
|
||||||
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
|
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
|
||||||
|
const formCodes = @json($forms->pluck('code', 'id'));
|
||||||
const linesData = @json($lines);
|
const linesData = @json($lines);
|
||||||
|
let isExpenseForm = false;
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
@@ -366,7 +374,36 @@ function applyQuickLine(lineId) {
|
|||||||
setTimeout(updateApprovalLineSummary, 100);
|
setTimeout(updateApprovalLineSummary, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchFormMode(formId) {
|
||||||
|
const code = formCodes[formId];
|
||||||
|
const expenseContainer = document.getElementById('expense-form-container');
|
||||||
|
const bodyArea = document.getElementById('body-area');
|
||||||
|
|
||||||
|
if (code === 'expense') {
|
||||||
|
isExpenseForm = true;
|
||||||
|
expenseContainer.style.display = '';
|
||||||
|
bodyArea.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
isExpenseForm = false;
|
||||||
|
expenseContainer.style.display = 'none';
|
||||||
|
bodyArea.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyBodyTemplate(formId) {
|
function applyBodyTemplate(formId) {
|
||||||
|
// 먼저 폼 모드 전환
|
||||||
|
switchFormMode(formId);
|
||||||
|
|
||||||
|
// 전용 폼이면 제목만 자동 설정하고 body template 적용 건너뜀
|
||||||
|
if (isExpenseForm) {
|
||||||
|
const titleEl = document.getElementById('title');
|
||||||
|
if (!titleEl.value.trim()) {
|
||||||
|
const formSelect = document.getElementById('form_id');
|
||||||
|
titleEl.value = formSelect.options[formSelect.selectedIndex].text;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const template = formBodyTemplates[formId];
|
const template = formBodyTemplates[formId];
|
||||||
if (!template) return;
|
if (!template) return;
|
||||||
|
|
||||||
@@ -406,10 +443,16 @@ function applyBodyTemplate(formId) {
|
|||||||
|
|
||||||
// 기존 HTML body 자동 감지 → 편집기 자동 활성화 + 결재선 요약 초기화
|
// 기존 HTML body 자동 감지 → 편집기 자동 활성화 + 결재선 요약 초기화
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const existingBody = document.getElementById('body').value;
|
// 초기 양식에 대한 폼 모드 전환
|
||||||
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
|
switchFormMode(document.getElementById('form_id').value);
|
||||||
document.getElementById('useEditor').checked = true;
|
|
||||||
toggleEditor();
|
// 전용 폼이 아닌 경우에만 Quill 편집기 자동 활성화
|
||||||
|
if (!isExpenseForm) {
|
||||||
|
const existingBody = document.getElementById('body').value;
|
||||||
|
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
|
||||||
|
document.getElementById('useEditor').checked = true;
|
||||||
|
toggleEditor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alpine 초기화 후 기존 결재선 요약 표시
|
// Alpine 초기화 후 기존 결재선 요약 표시
|
||||||
@@ -436,10 +479,19 @@ function applyBodyTemplate(formId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let expenseContent = {};
|
||||||
|
if (isExpenseForm) {
|
||||||
|
const expenseEl = document.getElementById('expense-form-container');
|
||||||
|
if (expenseEl && expenseEl._x_dataStack) {
|
||||||
|
expenseContent = expenseEl._x_dataStack[0].getFormData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
form_id: document.getElementById('form_id').value,
|
form_id: document.getElementById('form_id').value,
|
||||||
title: title,
|
title: title,
|
||||||
body: getBodyContent(),
|
body: isExpenseForm ? null : getBodyContent(),
|
||||||
|
content: isExpenseForm ? expenseContent : {},
|
||||||
is_urgent: document.getElementById('is_urgent').checked,
|
is_urgent: document.getElementById('is_urgent').checked,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
};
|
};
|
||||||
|
|||||||
254
resources/views/approvals/partials/_expense-form.blade.php
Normal file
254
resources/views/approvals/partials/_expense-form.blade.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
{{--
|
||||||
|
지출결의서 전용 폼 (Alpine.js)
|
||||||
|
Props:
|
||||||
|
$initialData (array|null) - 기존 content JSON (edit 시)
|
||||||
|
--}}
|
||||||
|
@php
|
||||||
|
$initialData = $initialData ?? [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div id="expense-form-container" style="display: none;"
|
||||||
|
x-data="expenseForm({{ json_encode($initialData) }})"
|
||||||
|
x-cloak>
|
||||||
|
|
||||||
|
{{-- 지출형식 --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">지출형식</label>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<template x-for="opt in expenseTypes" :key="opt.value">
|
||||||
|
<label class="inline-flex items-center gap-1.5 cursor-pointer">
|
||||||
|
<input type="radio" name="expense_type" :value="opt.value"
|
||||||
|
x-model="formData.expense_type"
|
||||||
|
class="text-blue-600 focus:ring-blue-500">
|
||||||
|
<span class="text-sm text-gray-700" x-text="opt.label"></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 세금계산서 --}}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">세금계산서</label>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<template x-for="opt in taxInvoiceTypes" :key="opt.value">
|
||||||
|
<label class="inline-flex items-center gap-1.5 cursor-pointer">
|
||||||
|
<input type="radio" name="tax_invoice" :value="opt.value"
|
||||||
|
x-model="formData.tax_invoice"
|
||||||
|
class="text-blue-600 focus:ring-blue-500">
|
||||||
|
<span class="text-sm text-gray-700" x-text="opt.label"></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 기본 정보 --}}
|
||||||
|
<div class="grid gap-4 mb-4" style="grid-template-columns: repeat(3, 1fr);">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">작성일자</label>
|
||||||
|
<input type="date" x-model="formData.write_date"
|
||||||
|
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>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">지출부서</label>
|
||||||
|
<input type="text" x-model="formData.department" 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>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">이름</label>
|
||||||
|
<input type="text" x-model="formData.writer_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>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">제목</label>
|
||||||
|
<input type="text" x-model="formData.subject" 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 class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">내역</label>
|
||||||
|
<div class="overflow-x-auto border border-gray-200 rounded-lg">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600 whitespace-nowrap" style="min-width: 120px;">년/월/일</th>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600" style="min-width: 160px;">내용</th>
|
||||||
|
<th class="px-2 py-2 text-right text-xs font-medium text-gray-600 whitespace-nowrap" style="min-width: 120px;">금액</th>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600" style="min-width: 120px;">업체명</th>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600 whitespace-nowrap" style="min-width: 80px;">지급은행</th>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600" style="min-width: 120px;">계좌번호</th>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600 whitespace-nowrap" style="min-width: 80px;">예금주</th>
|
||||||
|
<th class="px-2 py-2 text-left text-xs font-medium text-gray-600 whitespace-nowrap" style="min-width: 100px;">비고</th>
|
||||||
|
<th class="px-2 py-2 text-center text-xs font-medium text-gray-600" style="width: 40px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="(item, index) in formData.items" :key="item._key">
|
||||||
|
<tr class="border-t border-gray-100">
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="date" x-model="item.date"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="text" x-model="item.description" placeholder="내용"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs 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.amount)"
|
||||||
|
@input="item.amount = parseMoney($event.target.value); $event.target.value = formatMoney(item.amount)"
|
||||||
|
@focus="if(item.amount === 0) $event.target.value = ''"
|
||||||
|
@blur="if($event.target.value.trim() === '') { item.amount = 0; $event.target.value = '0'; }"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs text-right focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="text" x-model="item.vendor" placeholder="업체명"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="text" x-model="item.bank" placeholder="은행"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="text" x-model="item.account_no" placeholder="계좌번호"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="text" x-model="item.depositor" placeholder="예금주"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1">
|
||||||
|
<input type="text" x-model="item.remark" placeholder="비고"
|
||||||
|
class="w-full px-2 py-1.5 border border-gray-200 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-1 py-1 text-center">
|
||||||
|
<button type="button" @click="removeItem(index)"
|
||||||
|
class="text-red-400 hover:text-red-600 transition" title="삭제"
|
||||||
|
x-show="formData.items.length > 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-gray-50 border-t border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<td class="px-2 py-2 text-xs font-semibold text-gray-700 text-right" colspan="2">합계</td>
|
||||||
|
<td class="px-2 py-2 text-xs font-bold text-blue-700 text-right" x-text="formatMoney(totalAmount)"></td>
|
||||||
|
<td colspan="6" class="px-2 py-2">
|
||||||
|
<button type="button" @click="addItem()"
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-800 font-medium transition">
|
||||||
|
+ 행 추가
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 첨부서류 메모 --}}
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">첨부서류</label>
|
||||||
|
<textarea x-model="formData.attachment_memo" rows="2" 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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function expenseForm(initialData) {
|
||||||
|
let _keyCounter = 0;
|
||||||
|
|
||||||
|
function makeItem(data) {
|
||||||
|
return {
|
||||||
|
_key: ++_keyCounter,
|
||||||
|
date: data?.date || '',
|
||||||
|
description: data?.description || '',
|
||||||
|
amount: parseInt(data?.amount) || 0,
|
||||||
|
vendor: data?.vendor || '',
|
||||||
|
bank: data?.bank || '',
|
||||||
|
account_no: data?.account_no || '',
|
||||||
|
depositor: data?.depositor || '',
|
||||||
|
remark: data?.remark || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const items = (initialData?.items && initialData.items.length > 0)
|
||||||
|
? initialData.items.map(makeItem)
|
||||||
|
: [makeItem({ date: today })];
|
||||||
|
|
||||||
|
return {
|
||||||
|
expenseTypes: [
|
||||||
|
{ value: 'corporate_card', label: '법인카드' },
|
||||||
|
{ value: 'transfer', label: '송금' },
|
||||||
|
{ value: 'cash_advance', label: '현금/가지급정산' },
|
||||||
|
{ value: 'welfare_card', label: '복지카드' },
|
||||||
|
],
|
||||||
|
taxInvoiceTypes: [
|
||||||
|
{ value: 'normal', label: '일반' },
|
||||||
|
{ value: 'deferred', label: '이월발행' },
|
||||||
|
],
|
||||||
|
formData: {
|
||||||
|
expense_type: initialData?.expense_type || 'corporate_card',
|
||||||
|
tax_invoice: initialData?.tax_invoice || 'normal',
|
||||||
|
write_date: initialData?.write_date || today,
|
||||||
|
department: initialData?.department || '',
|
||||||
|
writer_name: initialData?.writer_name || '',
|
||||||
|
subject: initialData?.subject || '',
|
||||||
|
items: items,
|
||||||
|
attachment_memo: initialData?.attachment_memo || '',
|
||||||
|
},
|
||||||
|
|
||||||
|
get totalAmount() {
|
||||||
|
return this.formData.items.reduce((sum, item) => sum + (parseInt(item.amount) || 0), 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
addItem() {
|
||||||
|
this.formData.items.push(makeItem({ date: today }));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index) {
|
||||||
|
if (this.formData.items.length > 1) {
|
||||||
|
this.formData.items.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatMoney(value) {
|
||||||
|
const num = parseInt(value) || 0;
|
||||||
|
return num === 0 ? '0' : num.toLocaleString('ko-KR');
|
||||||
|
},
|
||||||
|
|
||||||
|
parseMoney(str) {
|
||||||
|
return parseInt(String(str).replace(/[^0-9-]/g, '')) || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getFormData() {
|
||||||
|
return {
|
||||||
|
expense_type: this.formData.expense_type,
|
||||||
|
tax_invoice: this.formData.tax_invoice,
|
||||||
|
write_date: this.formData.write_date,
|
||||||
|
department: this.formData.department,
|
||||||
|
writer_name: this.formData.writer_name,
|
||||||
|
subject: this.formData.subject,
|
||||||
|
items: this.formData.items.map(item => ({
|
||||||
|
date: item.date,
|
||||||
|
description: item.description,
|
||||||
|
amount: parseInt(item.amount) || 0,
|
||||||
|
vendor: item.vendor,
|
||||||
|
bank: item.bank,
|
||||||
|
account_no: item.account_no,
|
||||||
|
depositor: item.depositor,
|
||||||
|
remark: item.remark,
|
||||||
|
})),
|
||||||
|
total_amount: this.totalAmount,
|
||||||
|
attachment_memo: this.formData.attachment_memo,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
99
resources/views/approvals/partials/_expense-show.blade.php
Normal file
99
resources/views/approvals/partials/_expense-show.blade.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{{--
|
||||||
|
지출결의서 읽기전용 렌더링
|
||||||
|
Props:
|
||||||
|
$content (array) - approvals.content JSON
|
||||||
|
--}}
|
||||||
|
@php
|
||||||
|
$expenseTypeLabels = [
|
||||||
|
'corporate_card' => '법인카드',
|
||||||
|
'transfer' => '송금',
|
||||||
|
'cash_advance' => '현금/가지급정산',
|
||||||
|
'welfare_card' => '복지카드',
|
||||||
|
];
|
||||||
|
$taxInvoiceLabels = [
|
||||||
|
'normal' => '일반',
|
||||||
|
'deferred' => '이월발행',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="space-y-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">{{ $expenseTypeLabels[$content['expense_type'] ?? ''] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">세금계산서</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $taxInvoiceLabels[$content['tax_invoice'] ?? ''] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">작성일자</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['write_date'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">지출부서</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['department'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">이름</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['writer_name'] ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($content['subject']))
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">제목</span>
|
||||||
|
<div class="text-sm font-medium mt-0.5">{{ $content['subject'] }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- 내역 테이블 --}}
|
||||||
|
@if(!empty($content['items']))
|
||||||
|
<div class="overflow-x-auto border border-gray-200 rounded-lg">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">년/월/일</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">내용</th>
|
||||||
|
<th class="px-3 py-2 text-right text-xs font-medium text-gray-600">금액</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">업체명</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">지급은행</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">계좌번호</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">예금주</th>
|
||||||
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-600">비고</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($content['items'] as $item)
|
||||||
|
<tr class="border-t border-gray-100">
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700 whitespace-nowrap">{{ $item['date'] ?? '' }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700">{{ $item['description'] ?? '' }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700 text-right font-medium whitespace-nowrap">{{ number_format($item['amount'] ?? 0) }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700">{{ $item['vendor'] ?? '' }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700">{{ $item['bank'] ?? '' }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700">{{ $item['account_no'] ?? '' }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700">{{ $item['depositor'] ?? '' }}</td>
|
||||||
|
<td class="px-3 py-2 text-xs text-gray-700">{{ $item['remark'] ?? '' }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-gray-50 border-t border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<td class="px-3 py-2 text-xs font-semibold text-gray-700 text-right" colspan="2">합계</td>
|
||||||
|
<td class="px-3 py-2 text-xs font-bold text-blue-700 text-right whitespace-nowrap">{{ number_format($content['total_amount'] ?? 0) }}</td>
|
||||||
|
<td colspan="5"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- 첨부서류 --}}
|
||||||
|
@if(!empty($content['attachment_memo']))
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-gray-500">첨부서류</span>
|
||||||
|
<div class="text-sm text-gray-700 mt-0.5 whitespace-pre-wrap">{{ $content['attachment_memo'] }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@@ -73,7 +73,9 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
|
|||||||
|
|
||||||
<div class="border-t pt-4">
|
<div class="border-t pt-4">
|
||||||
<h2 class="text-lg font-semibold text-gray-800 mb-2">{{ $approval->title }}</h2>
|
<h2 class="text-lg font-semibold text-gray-800 mb-2">{{ $approval->title }}</h2>
|
||||||
@if($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
|
@if(!empty($approval->content) && $approval->form?->code === 'expense')
|
||||||
|
@include('approvals.partials._expense-show', ['content' => $approval->content])
|
||||||
|
@elseif($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
|
||||||
<div class="prose prose-sm max-w-none text-gray-700">
|
<div class="prose prose-sm max-w-none text-gray-700">
|
||||||
{!! strip_tags($approval->body, '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>') !!}
|
{!! strip_tags($approval->body, '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>') !!}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user