feat: [approval] 지출결의서 법인카드/송금 계좌 선택 기능
- 법인카드 선택 시 카드 목록 패널 슬라이드-다운 표시 - 송금 선택 시 출금 계좌 목록 표시, 대표계좌 자동 선택 - 선택된 카드/계좌 정보를 content JSON에 스냅샷 저장 - 상세 페이지에서 선택된 카드/계좌 정보 읽기전용 표시
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Finance\BankAccount;
|
||||
use App\Models\Finance\CorporateCard;
|
||||
use App\Services\ApprovalService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -36,8 +38,9 @@ public function create(Request $request): View|Response
|
||||
|
||||
$forms = $this->service->getApprovalForms();
|
||||
$lines = $this->service->getApprovalLines();
|
||||
[$cards, $accounts] = $this->getCardAndAccountData();
|
||||
|
||||
return view('approvals.create', compact('forms', 'lines'));
|
||||
return view('approvals.create', compact('forms', 'lines', 'cards', 'accounts'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,8 +60,9 @@ public function edit(Request $request, int $id): View|Response
|
||||
|
||||
$forms = $this->service->getApprovalForms();
|
||||
$lines = $this->service->getApprovalLines();
|
||||
[$cards, $accounts] = $this->getCardAndAccountData();
|
||||
|
||||
return view('approvals.edit', compact('approval', 'forms', 'lines'));
|
||||
return view('approvals.edit', compact('approval', 'forms', 'lines', 'cards', 'accounts'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,4 +114,22 @@ public function completed(Request $request): View|Response
|
||||
|
||||
return view('approvals.completed');
|
||||
}
|
||||
|
||||
private function getCardAndAccountData(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$cards = CorporateCard::forTenant($tenantId)
|
||||
->active()
|
||||
->select('id', 'card_name', 'card_company', 'card_number', 'card_holder_name')
|
||||
->get();
|
||||
|
||||
$accounts = BankAccount::where('tenant_id', $tenantId)
|
||||
->active()
|
||||
->ordered()
|
||||
->select('id', 'bank_name', 'account_number', 'account_holder', 'is_primary')
|
||||
->get();
|
||||
|
||||
return [$cards, $accounts];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
||||
</div>
|
||||
|
||||
{{-- 지출결의서 전용 폼 --}}
|
||||
@include('approvals.partials._expense-form', ['initialData' => []])
|
||||
@include('approvals.partials._expense-form', [
|
||||
'initialData' => [],
|
||||
'cards' => $cards ?? collect(),
|
||||
'accounts' => $accounts ?? collect(),
|
||||
])
|
||||
|
||||
{{-- 액션 버튼 --}}
|
||||
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
|
||||
|
||||
@@ -124,6 +124,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
|
||||
@include('approvals.partials._expense-form', [
|
||||
'initialData' => $approval->content ?? [],
|
||||
'initialFiles' => $existingFiles,
|
||||
'cards' => $cards ?? collect(),
|
||||
'accounts' => $accounts ?? collect(),
|
||||
])
|
||||
|
||||
{{-- 액션 버튼 --}}
|
||||
|
||||
@@ -3,15 +3,20 @@
|
||||
Props:
|
||||
$initialData (array|null) - 기존 content JSON (edit 시)
|
||||
$initialFiles (array|null) - 기존 첨부파일 [{id, name, size, mime_type}] (edit 시)
|
||||
$cards (Collection|null) - 법인카드 목록
|
||||
$accounts (Collection|null) - 회사 계좌 목록
|
||||
--}}
|
||||
@php
|
||||
$initialData = $initialData ?? [];
|
||||
$initialFiles = $initialFiles ?? [];
|
||||
$cards = $cards ?? collect();
|
||||
$accounts = $accounts ?? collect();
|
||||
$userName = auth()->user()->name ?? '';
|
||||
@endphp
|
||||
|
||||
<div id="expense-form-container" style="display: none;"
|
||||
x-data="expenseForm({{ json_encode($initialData) }}, '{{ $userName }}', {{ json_encode($initialFiles) }})"
|
||||
x-data="expenseForm({{ json_encode($initialData) }}, '{{ $userName }}', {{ json_encode($initialFiles) }}, {{ $cards->toJson() }}, {{ $accounts->toJson() }})"
|
||||
x-init="initAutoSelect()"
|
||||
x-cloak>
|
||||
|
||||
{{-- 지출형식 --}}
|
||||
@@ -22,6 +27,7 @@
|
||||
<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"
|
||||
@change="initAutoSelect()"
|
||||
class="text-blue-600 focus:ring-blue-500">
|
||||
<span class="text-sm text-gray-700" x-text="opt.label"></span>
|
||||
</label>
|
||||
@@ -29,6 +35,74 @@ class="text-blue-600 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 법인카드 선택 패널 --}}
|
||||
<div x-show="formData.expense_type === 'corporate_card'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" class="mb-4">
|
||||
<div class="border border-blue-200 bg-blue-50/50 rounded-lg p-4">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-3">법인카드 선택</label>
|
||||
<template x-if="cards.length === 0">
|
||||
<div class="text-sm text-gray-500 text-center py-4">
|
||||
등록된 법인카드가 없습니다.
|
||||
<a href="/finance/corporate-cards" class="text-blue-600 hover:underline ml-1">카드 관리</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-wrap gap-3" x-show="cards.length > 0">
|
||||
<template x-for="card in cards" :key="card.id">
|
||||
<div @click="selectCard(card)"
|
||||
:class="formData.selected_card?.id === card.id ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200' : 'border-gray-200 bg-white hover:border-gray-300'"
|
||||
class="relative border rounded-lg p-3 cursor-pointer transition-all"
|
||||
style="flex: 0 0 auto; min-width: 180px; max-width: 220px;">
|
||||
{{-- 체크 아이콘 --}}
|
||||
<div x-show="formData.selected_card?.id === card.id" class="absolute top-2 right-2">
|
||||
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500" x-text="card.card_company"></div>
|
||||
<div class="text-sm font-medium text-gray-800 mt-0.5" x-text="card.card_name"></div>
|
||||
<div class="text-xs text-gray-500 mt-1 font-mono" x-text="'**** ' + card.card_number.slice(-4)"></div>
|
||||
<div class="text-xs text-gray-400 mt-0.5" x-text="card.card_holder_name"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 송금 계좌 선택 패널 --}}
|
||||
<div x-show="formData.expense_type === 'transfer'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" class="mb-4">
|
||||
<div class="border border-green-200 bg-green-50/50 rounded-lg p-4">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-3">출금 계좌 선택</label>
|
||||
<template x-if="accounts.length === 0">
|
||||
<div class="text-sm text-gray-500 text-center py-4">
|
||||
등록된 계좌가 없습니다.
|
||||
<a href="/finance/bank-accounts" class="text-blue-600 hover:underline ml-1">계좌 관리</a>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-wrap gap-3" x-show="accounts.length > 0">
|
||||
<template x-for="account in accounts" :key="account.id">
|
||||
<div @click="selectAccount(account)"
|
||||
:class="formData.selected_account?.id === account.id ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200' : 'border-gray-200 bg-white hover:border-gray-300'"
|
||||
class="relative border rounded-lg p-3 cursor-pointer transition-all"
|
||||
style="flex: 0 0 auto; min-width: 180px; max-width: 220px;">
|
||||
{{-- 체크 아이콘 --}}
|
||||
<div x-show="formData.selected_account?.id === account.id" class="absolute top-2 right-2">
|
||||
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-sm font-medium text-gray-800" x-text="account.bank_name"></span>
|
||||
<template x-if="account.is_primary">
|
||||
<span class="text-amber-500 text-xs" title="대표계좌">★</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1 font-mono" x-text="account.account_number"></div>
|
||||
<div class="text-xs text-gray-400 mt-0.5" x-text="account.account_holder"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 세금계산서 --}}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">세금계산서</label>
|
||||
@@ -207,7 +281,7 @@ class="border-2 border-dashed rounded-lg p-4 text-center transition-colors curso
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function expenseForm(initialData, authUserName, initialFiles) {
|
||||
function expenseForm(initialData, authUserName, initialFiles, cardsData, accountsData) {
|
||||
let _keyCounter = 0;
|
||||
|
||||
function makeItem(data) {
|
||||
@@ -240,6 +314,9 @@ function makeItem(data) {
|
||||
{ value: 'normal', label: '일반' },
|
||||
{ value: 'deferred', label: '이월발행' },
|
||||
],
|
||||
cards: cardsData || [],
|
||||
accounts: accountsData || [],
|
||||
|
||||
formData: {
|
||||
expense_type: initialData?.expense_type || 'corporate_card',
|
||||
tax_invoice: initialData?.tax_invoice || 'normal',
|
||||
@@ -248,6 +325,8 @@ function makeItem(data) {
|
||||
writer_name: initialData?.writer_name || authUserName,
|
||||
items: items,
|
||||
attachment_memo: initialData?.attachment_memo || '',
|
||||
selected_card: initialData?.selected_card || null,
|
||||
selected_account: initialData?.selected_account || null,
|
||||
},
|
||||
|
||||
// 파일 업로드 상태
|
||||
@@ -271,6 +350,32 @@ function makeItem(data) {
|
||||
}
|
||||
},
|
||||
|
||||
selectCard(card) {
|
||||
this.formData.selected_card = {
|
||||
id: card.id,
|
||||
card_name: card.card_name,
|
||||
card_company: card.card_company,
|
||||
card_number_last4: card.card_number.slice(-4),
|
||||
card_holder_name: card.card_holder_name,
|
||||
};
|
||||
},
|
||||
|
||||
selectAccount(account) {
|
||||
this.formData.selected_account = {
|
||||
id: account.id,
|
||||
bank_name: account.bank_name,
|
||||
account_number: account.account_number,
|
||||
account_holder: account.account_holder,
|
||||
};
|
||||
},
|
||||
|
||||
initAutoSelect() {
|
||||
if (this.formData.expense_type === 'transfer' && !this.formData.selected_account) {
|
||||
const primary = this.accounts.find(a => a.is_primary);
|
||||
if (primary) this.selectAccount(primary);
|
||||
}
|
||||
},
|
||||
|
||||
formatMoney(value) {
|
||||
const num = parseInt(value) || 0;
|
||||
return num === 0 ? '0' : num.toLocaleString('ko-KR');
|
||||
@@ -390,6 +495,8 @@ function makeItem(data) {
|
||||
})),
|
||||
total_amount: this.totalAmount,
|
||||
attachment_memo: this.formData.attachment_memo,
|
||||
selected_card: this.formData.expense_type === 'corporate_card' ? this.formData.selected_card : null,
|
||||
selected_account: this.formData.expense_type === 'transfer' ? this.formData.selected_account : null,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -41,6 +41,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 선택된 법인카드 --}}
|
||||
@if(($content['expense_type'] ?? '') === 'corporate_card' && !empty($content['selected_card']))
|
||||
@php $card = $content['selected_card']; @endphp
|
||||
<div class="flex items-center gap-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<svg class="w-5 h-5 text-blue-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">결제 카드</span>
|
||||
<div class="text-sm font-medium text-gray-800">
|
||||
{{ $card['card_company'] ?? '' }} {{ $card['card_name'] ?? '' }}
|
||||
<span class="text-gray-500 font-mono">**** {{ $card['card_number_last4'] ?? '' }}</span>
|
||||
<span class="text-gray-400 ml-1">({{ $card['card_holder_name'] ?? '' }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 선택된 출금 계좌 --}}
|
||||
@if(($content['expense_type'] ?? '') === 'transfer' && !empty($content['selected_account']))
|
||||
@php $account = $content['selected_account']; @endphp
|
||||
<div class="flex items-center gap-3 p-3 bg-green-50 border border-green-200 rounded-lg">
|
||||
<svg class="w-5 h-5 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">출금 계좌</span>
|
||||
<div class="text-sm font-medium text-gray-800">
|
||||
{{ $account['bank_name'] ?? '' }}
|
||||
<span class="text-gray-600 font-mono">{{ $account['account_number'] ?? '' }}</span>
|
||||
<span class="text-gray-400 ml-1">({{ $account['account_holder'] ?? '' }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($content['subject']))
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">제목</span>
|
||||
|
||||
Reference in New Issue
Block a user