feat: [finance] 경조사비 관리 페이지 추가
- 거래처 경조사비 관리대장 CRUD (등록/수정/삭제) - 축의/부조 구분, 부조금(현금/계좌이체/카드), 선물(종류/금액) 관리 - 연도별 필터, 구분별 필터, 거래처/내역 검색 - 통계 카드 (총건수, 총금액, 부조금 합계, 선물 합계, 축의/부조 비율) - CSV 내보내기 - 라우트: /finance/condolence-expenses
This commit is contained in:
170
app/Http/Controllers/Finance/CondolenceExpenseController.php
Normal file
170
app/Http/Controllers/Finance/CondolenceExpenseController.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\CondolenceExpense;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CondolenceExpenseController extends Controller
|
||||
{
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('finance.condolence-expenses'));
|
||||
}
|
||||
|
||||
return view('finance.condolence-expenses');
|
||||
}
|
||||
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$query = CondolenceExpense::forTenant($tenantId);
|
||||
|
||||
if ($year = $request->input('year')) {
|
||||
$query->whereYear('event_date', $year);
|
||||
}
|
||||
|
||||
if ($category = $request->input('category')) {
|
||||
if ($category !== 'all') {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
}
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('partner_name', 'like', "%{$search}%")
|
||||
->orWhere('description', 'like', "%{$search}%")
|
||||
->orWhere('memo', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$records = $query->orderBy('event_date', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
'id' => $item->id,
|
||||
'event_date' => $item->event_date?->format('Y-m-d'),
|
||||
'expense_date' => $item->expense_date?->format('Y-m-d'),
|
||||
'partner_name' => $item->partner_name,
|
||||
'description' => $item->description,
|
||||
'category' => $item->category,
|
||||
'has_cash' => $item->has_cash,
|
||||
'cash_method' => $item->cash_method,
|
||||
'cash_amount' => $item->cash_amount,
|
||||
'has_gift' => $item->has_gift,
|
||||
'gift_type' => $item->gift_type,
|
||||
'gift_amount' => $item->gift_amount,
|
||||
'total_amount' => $item->total_amount,
|
||||
'memo' => $item->memo,
|
||||
]);
|
||||
|
||||
$all = CondolenceExpense::forTenant($tenantId);
|
||||
if ($year) {
|
||||
$all = $all->whereYear('event_date', $year);
|
||||
}
|
||||
$all = $all->get();
|
||||
|
||||
$stats = [
|
||||
'totalCount' => $all->count(),
|
||||
'totalAmount' => $all->sum('total_amount'),
|
||||
'cashTotal' => $all->sum('cash_amount'),
|
||||
'giftTotal' => $all->sum('gift_amount'),
|
||||
'congratulationCount' => $all->where('category', 'congratulation')->count(),
|
||||
'condolenceCount' => $all->where('category', 'condolence')->count(),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $records,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'partner_name' => 'required|string|max:100',
|
||||
'category' => 'required|in:congratulation,condolence',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$cashAmount = (int) $request->input('cash_amount', 0);
|
||||
$giftAmount = (int) $request->input('gift_amount', 0);
|
||||
|
||||
CondolenceExpense::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'event_date' => $request->input('event_date'),
|
||||
'expense_date' => $request->input('expense_date'),
|
||||
'partner_name' => $request->input('partner_name'),
|
||||
'description' => $request->input('description'),
|
||||
'category' => $request->input('category'),
|
||||
'has_cash' => $request->boolean('has_cash'),
|
||||
'cash_method' => $request->input('cash_method'),
|
||||
'cash_amount' => $cashAmount,
|
||||
'has_gift' => $request->boolean('has_gift'),
|
||||
'gift_type' => $request->input('gift_type'),
|
||||
'gift_amount' => $giftAmount,
|
||||
'total_amount' => $cashAmount + $giftAmount,
|
||||
'memo' => $request->input('memo'),
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '경조사비가 등록되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$item = CondolenceExpense::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'partner_name' => 'required|string|max:100',
|
||||
'category' => 'required|in:congratulation,condolence',
|
||||
]);
|
||||
|
||||
$cashAmount = (int) $request->input('cash_amount', 0);
|
||||
$giftAmount = (int) $request->input('gift_amount', 0);
|
||||
|
||||
$item->update([
|
||||
'event_date' => $request->input('event_date'),
|
||||
'expense_date' => $request->input('expense_date'),
|
||||
'partner_name' => $request->input('partner_name'),
|
||||
'description' => $request->input('description'),
|
||||
'category' => $request->input('category'),
|
||||
'has_cash' => $request->boolean('has_cash'),
|
||||
'cash_method' => $request->input('cash_method'),
|
||||
'cash_amount' => $cashAmount,
|
||||
'has_gift' => $request->boolean('has_gift'),
|
||||
'gift_type' => $request->input('gift_type'),
|
||||
'gift_amount' => $giftAmount,
|
||||
'total_amount' => $cashAmount + $giftAmount,
|
||||
'memo' => $request->input('memo'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '경조사비가 수정되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$item = CondolenceExpense::forTenant($tenantId)->findOrFail($id);
|
||||
$item->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '경조사비가 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
48
app/Models/Finance/CondolenceExpense.php
Normal file
48
app/Models/Finance/CondolenceExpense.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CondolenceExpense extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'condolence_expenses';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'event_date',
|
||||
'expense_date',
|
||||
'partner_name',
|
||||
'description',
|
||||
'category',
|
||||
'has_cash',
|
||||
'cash_method',
|
||||
'cash_amount',
|
||||
'has_gift',
|
||||
'gift_type',
|
||||
'gift_amount',
|
||||
'total_amount',
|
||||
'options',
|
||||
'memo',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'event_date' => 'date',
|
||||
'expense_date' => 'date',
|
||||
'has_cash' => 'boolean',
|
||||
'has_gift' => 'boolean',
|
||||
'cash_amount' => 'integer',
|
||||
'gift_amount' => 'integer',
|
||||
'total_amount' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeForTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
512
resources/views/finance/condolence-expenses.blade.php
Normal file
512
resources/views/finance/condolence-expenses.blade.php
Normal file
@@ -0,0 +1,512 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '경조사비 관리')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
@media print { .no-print { display: none !important; } }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div x-data="condolenceExpenseApp()" x-init="init()">
|
||||
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex items-center justify-between mb-4 no-print">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-800">경조사비 관리</h1>
|
||||
<p class="text-xs text-gray-500 mt-0.5">거래처 경조사비 관리대장</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="exportCsv()" class="px-3 py-1.5 text-xs border border-gray-300 rounded-lg hover:bg-gray-50 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="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
CSV
|
||||
</button>
|
||||
<button @click="openModal('add')" class="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700 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="M12 4v16m8-8H4"/></svg>
|
||||
등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 통계 카드 --}}
|
||||
<div class="grid gap-3 mb-4 no-print" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-3">
|
||||
<div class="text-xs text-gray-500">총 건수</div>
|
||||
<div class="text-lg font-bold text-gray-800 mt-0.5" x-text="stats.totalCount + '건'"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-3">
|
||||
<div class="text-xs text-gray-500">총 금액</div>
|
||||
<div class="text-lg font-bold text-blue-600 mt-0.5" x-text="formatMoney(stats.totalAmount) + '원'"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-3">
|
||||
<div class="text-xs text-gray-500">부조금 합계</div>
|
||||
<div class="text-lg font-bold text-indigo-600 mt-0.5" x-text="formatMoney(stats.cashTotal) + '원'"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-3">
|
||||
<div class="text-xs text-gray-500">선물 합계</div>
|
||||
<div class="text-lg font-bold text-green-600 mt-0.5" x-text="formatMoney(stats.giftTotal) + '원'"></div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">축의</div>
|
||||
<div class="text-lg font-bold text-red-500 mt-0.5" x-text="stats.congratulationCount + '건'"></div>
|
||||
</div>
|
||||
<div class="text-gray-300 text-lg">/</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">부조</div>
|
||||
<div class="text-lg font-bold text-gray-700 mt-0.5" x-text="stats.condolenceCount + '건'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 필터 --}}
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-3 mb-4 no-print">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<div style="flex: 0 0 100px;">
|
||||
<select x-model="filterYear" @change="loadData()" class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<template x-for="y in yearOptions" :key="y">
|
||||
<option :value="y" x-text="y + '년'"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 0 0 100px;">
|
||||
<select x-model="filterCategory" @change="loadData()" class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="all">전체 구분</option>
|
||||
<option value="congratulation">축의</option>
|
||||
<option value="condolence">부조</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; max-width: 300px;">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-2.5 top-2 w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
<input type="text" x-model="searchQuery" @input.debounce.300ms="loadData()" placeholder="거래처명, 내역 검색..."
|
||||
class="w-full pl-8 pr-3 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 테이블 --}}
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 border-b border-gray-200">
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 40px;">No</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 100px;">경조사일자</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 100px;">지출일자</th>
|
||||
<th class="px-3 py-2 text-left text-gray-600 font-semibold" style="min-width: 140px;">거래처명</th>
|
||||
<th class="px-3 py-2 text-left text-gray-600 font-semibold" style="min-width: 140px;">내역</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 60px;">구분</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 50px;">부조금</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 80px;">지출방법</th>
|
||||
<th class="px-3 py-2 text-right text-gray-600 font-semibold" style="width: 100px;">부조금액</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 50px;">선물</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold" style="width: 80px;">선물종류</th>
|
||||
<th class="px-3 py-2 text-right text-gray-600 font-semibold" style="width: 100px;">선물금액</th>
|
||||
<th class="px-3 py-2 text-right text-gray-600 font-semibold" style="width: 100px;">총금액</th>
|
||||
<th class="px-3 py-2 text-left text-gray-600 font-semibold" style="min-width: 100px;">비고</th>
|
||||
<th class="px-3 py-2 text-center text-gray-600 font-semibold no-print" style="width: 70px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="loading">
|
||||
<tr><td colspan="15" class="px-3 py-8 text-center text-gray-400">로딩 중...</td></tr>
|
||||
</template>
|
||||
<template x-if="!loading && records.length === 0">
|
||||
<tr><td colspan="15" class="px-3 py-8 text-center text-gray-400">등록된 경조사비가 없습니다.</td></tr>
|
||||
</template>
|
||||
<template x-for="(item, idx) in records" :key="item.id">
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50 transition">
|
||||
<td class="px-3 py-2 text-center text-gray-500" x-text="idx + 1"></td>
|
||||
<td class="px-3 py-2 text-center" x-text="item.event_date || '-'"></td>
|
||||
<td class="px-3 py-2 text-center" x-text="item.expense_date || '-'"></td>
|
||||
<td class="px-3 py-2 font-medium text-gray-800" x-text="item.partner_name"></td>
|
||||
<td class="px-3 py-2 text-gray-600" x-text="item.description || '-'"></td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span class="inline-block px-1.5 py-0.5 rounded text-xs font-medium"
|
||||
:class="item.category === 'congratulation' ? 'bg-red-50 text-red-600' : 'bg-gray-100 text-gray-600'"
|
||||
x-text="item.category === 'congratulation' ? '축의' : '부조'"></span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center" x-text="item.has_cash ? '여' : '부'"></td>
|
||||
<td class="px-3 py-2 text-center text-gray-600" x-text="cashMethodLabel(item.cash_method)"></td>
|
||||
<td class="px-3 py-2 text-right font-medium" x-text="item.cash_amount ? formatMoney(item.cash_amount) : '-'"></td>
|
||||
<td class="px-3 py-2 text-center" x-text="item.has_gift ? '여' : '부'"></td>
|
||||
<td class="px-3 py-2 text-center text-gray-600" x-text="item.gift_type || '-'"></td>
|
||||
<td class="px-3 py-2 text-right font-medium" x-text="item.gift_amount ? formatMoney(item.gift_amount) : '-'"></td>
|
||||
<td class="px-3 py-2 text-right font-bold text-blue-600" x-text="formatMoney(item.total_amount)"></td>
|
||||
<td class="px-3 py-2 text-gray-500" x-text="item.memo || ''"></td>
|
||||
<td class="px-3 py-2 text-center no-print">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button @click="openModal('edit', item)" class="p-1 text-gray-400 hover:text-blue-600 transition" title="수정">
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
</button>
|
||||
<button @click="deleteItem(item.id)" class="p-1 text-gray-400 hover:text-red-600 transition" title="삭제">
|
||||
<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="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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
{{-- 합계 행 --}}
|
||||
<tfoot x-show="records.length > 0">
|
||||
<tr class="bg-gray-50 border-t-2 border-gray-300 font-semibold text-xs">
|
||||
<td colspan="8" class="px-3 py-2 text-right text-gray-600">합계</td>
|
||||
<td class="px-3 py-2 text-right text-indigo-600" x-text="formatMoney(footerCashTotal)"></td>
|
||||
<td colspan="2" class="px-3 py-2"></td>
|
||||
<td class="px-3 py-2 text-right text-green-600" x-text="formatMoney(footerGiftTotal)"></td>
|
||||
<td class="px-3 py-2 text-right text-blue-700" x-text="formatMoney(footerTotal)"></td>
|
||||
<td colspan="2" class="px-3 py-2"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 등록/수정 모달 --}}
|
||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50" style="display: none;">
|
||||
<div class="absolute inset-0 bg-black/50" @click="showModal = false"></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" style="max-width: 600px;" @click.stop>
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-sm font-semibold text-gray-800" x-text="modalMode === 'add' ? '경조사비 등록' : '경조사비 수정'"></h3>
|
||||
<button @click="showModal = false" 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 class="p-5 space-y-4 overflow-y-auto" style="max-height: 70vh;">
|
||||
{{-- 날짜 --}}
|
||||
<div class="flex gap-3">
|
||||
<div style="flex: 1;">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">경조사일자</label>
|
||||
<input type="date" x-model="form.event_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 style="flex: 1;">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">지출일자</label>
|
||||
<input type="date" x-model="form.expense_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>
|
||||
|
||||
{{-- 거래처/내역 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">거래처명 / 대상자 <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="form.partner_name" placeholder="예: A회사 홍길동 상무" 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="flex gap-3">
|
||||
<div style="flex: 2;">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">내역</label>
|
||||
<input type="text" x-model="form.description" 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 120px;">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">구분 <span class="text-red-500">*</span></label>
|
||||
<select x-model="form.category" 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">
|
||||
<option value="congratulation">축의</option>
|
||||
<option value="condolence">부조</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 부조금 --}}
|
||||
<div class="border border-gray-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" x-model="form.has_cash" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<span class="text-xs font-semibold text-gray-700">부조금</span>
|
||||
</label>
|
||||
</div>
|
||||
<div x-show="form.has_cash" class="flex gap-3">
|
||||
<div style="flex: 1;">
|
||||
<label class="block text-xs text-gray-500 mb-1">지출방법</label>
|
||||
<select x-model="form.cash_method" class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="cash">현금</option>
|
||||
<option value="transfer">계좌이체</option>
|
||||
<option value="card">카드</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label class="block text-xs text-gray-500 mb-1">금액</label>
|
||||
<input type="text" inputmode="numeric" x-model="form.cash_amount_display"
|
||||
@input="form.cash_amount_display = formatMoneyInput($event.target.value); updateTotal()"
|
||||
@focus="form.cash_amount_display = unformat(form.cash_amount_display)"
|
||||
@blur="form.cash_amount_display = formatMoneyInput(form.cash_amount_display)"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs text-right focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 선물 --}}
|
||||
<div class="border border-gray-200 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" x-model="form.has_gift" class="rounded border-gray-300 text-green-600 focus:ring-green-500">
|
||||
<span class="text-xs font-semibold text-gray-700">선물</span>
|
||||
</label>
|
||||
</div>
|
||||
<div x-show="form.has_gift" class="flex gap-3">
|
||||
<div style="flex: 1;">
|
||||
<label class="block text-xs text-gray-500 mb-1">종류</label>
|
||||
<input type="text" x-model="form.gift_type" placeholder="예: 화환, 과일바구니" class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label class="block text-xs text-gray-500 mb-1">금액</label>
|
||||
<input type="text" inputmode="numeric" x-model="form.gift_amount_display"
|
||||
@input="form.gift_amount_display = formatMoneyInput($event.target.value); updateTotal()"
|
||||
@focus="form.gift_amount_display = unformat(form.gift_amount_display)"
|
||||
@blur="form.gift_amount_display = formatMoneyInput(form.gift_amount_display)"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs text-right focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 총금액 --}}
|
||||
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
||||
<span class="text-sm font-semibold text-gray-700">총 금액</span>
|
||||
<span class="text-lg font-bold text-blue-600" x-text="formatMoney(form.total_amount) + '원'"></span>
|
||||
</div>
|
||||
|
||||
{{-- 비고 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
|
||||
<input type="text" x-model="form.memo" 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 justify-end gap-2 px-5 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<button @click="showModal = false" class="px-4 py-2 text-xs border border-gray-300 rounded-lg hover:bg-gray-100 transition">취소</button>
|
||||
<button @click="saveItem()" :disabled="saving" class="px-4 py-2 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50">
|
||||
<span x-text="saving ? '저장 중...' : (modalMode === 'add' ? '등록' : '수정')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function condolenceExpenseApp() {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
|
||||
return {
|
||||
records: [],
|
||||
stats: { totalCount: 0, totalAmount: 0, cashTotal: 0, giftTotal: 0, congratulationCount: 0, condolenceCount: 0 },
|
||||
loading: true,
|
||||
saving: false,
|
||||
showModal: false,
|
||||
modalMode: 'add',
|
||||
filterYear: currentYear,
|
||||
filterCategory: 'all',
|
||||
searchQuery: '',
|
||||
form: {},
|
||||
|
||||
get yearOptions() {
|
||||
const years = [];
|
||||
for (let y = currentYear; y >= currentYear - 5; y--) years.push(y);
|
||||
return years;
|
||||
},
|
||||
|
||||
get footerCashTotal() {
|
||||
return this.records.reduce((sum, r) => sum + (r.cash_amount || 0), 0);
|
||||
},
|
||||
get footerGiftTotal() {
|
||||
return this.records.reduce((sum, r) => sum + (r.gift_amount || 0), 0);
|
||||
},
|
||||
get footerTotal() {
|
||||
return this.records.reduce((sum, r) => sum + (r.total_amount || 0), 0);
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.loadData();
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams({ year: this.filterYear, category: this.filterCategory });
|
||||
if (this.searchQuery) params.append('search', this.searchQuery);
|
||||
const res = await fetch('/finance/condolence-expenses/list?' + params);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.records = data.data;
|
||||
this.stats = data.stats;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('데이터 로딩 실패:', e);
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
this.form = {
|
||||
id: null,
|
||||
event_date: today,
|
||||
expense_date: today,
|
||||
partner_name: '',
|
||||
description: '',
|
||||
category: 'congratulation',
|
||||
has_cash: true,
|
||||
cash_method: 'transfer',
|
||||
cash_amount: 0,
|
||||
cash_amount_display: '',
|
||||
has_gift: false,
|
||||
gift_type: '',
|
||||
gift_amount: 0,
|
||||
gift_amount_display: '',
|
||||
total_amount: 0,
|
||||
memo: '',
|
||||
};
|
||||
},
|
||||
|
||||
openModal(mode, item = null) {
|
||||
this.modalMode = mode;
|
||||
if (mode === 'edit' && item) {
|
||||
this.form = {
|
||||
id: item.id,
|
||||
event_date: item.event_date || '',
|
||||
expense_date: item.expense_date || '',
|
||||
partner_name: item.partner_name,
|
||||
description: item.description || '',
|
||||
category: item.category,
|
||||
has_cash: item.has_cash,
|
||||
cash_method: item.cash_method || 'transfer',
|
||||
cash_amount: item.cash_amount || 0,
|
||||
cash_amount_display: item.cash_amount ? this.formatMoney(item.cash_amount) : '',
|
||||
has_gift: item.has_gift,
|
||||
gift_type: item.gift_type || '',
|
||||
gift_amount: item.gift_amount || 0,
|
||||
gift_amount_display: item.gift_amount ? this.formatMoney(item.gift_amount) : '',
|
||||
total_amount: item.total_amount || 0,
|
||||
memo: item.memo || '',
|
||||
};
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
updateTotal() {
|
||||
const cash = this.form.has_cash ? this.parseNumber(this.form.cash_amount_display) : 0;
|
||||
const gift = this.form.has_gift ? this.parseNumber(this.form.gift_amount_display) : 0;
|
||||
this.form.cash_amount = cash;
|
||||
this.form.gift_amount = gift;
|
||||
this.form.total_amount = cash + gift;
|
||||
},
|
||||
|
||||
async saveItem() {
|
||||
if (!this.form.partner_name.trim()) {
|
||||
if (typeof showToast === 'function') showToast('거래처명을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
this.updateTotal();
|
||||
this.saving = true;
|
||||
|
||||
const payload = {
|
||||
event_date: this.form.event_date,
|
||||
expense_date: this.form.expense_date,
|
||||
partner_name: this.form.partner_name,
|
||||
description: this.form.description,
|
||||
category: this.form.category,
|
||||
has_cash: this.form.has_cash,
|
||||
cash_method: this.form.has_cash ? this.form.cash_method : null,
|
||||
cash_amount: this.form.has_cash ? this.form.cash_amount : 0,
|
||||
has_gift: this.form.has_gift,
|
||||
gift_type: this.form.has_gift ? this.form.gift_type : null,
|
||||
gift_amount: this.form.has_gift ? this.form.gift_amount : 0,
|
||||
memo: this.form.memo,
|
||||
};
|
||||
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
const isEdit = this.modalMode === 'edit' && this.form.id;
|
||||
const url = isEdit ? `/finance/condolence-expenses/${this.form.id}` : '/finance/condolence-expenses/store';
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
if (typeof showToast === 'function') showToast(data.message, 'success');
|
||||
this.showModal = false;
|
||||
await this.loadData();
|
||||
} else {
|
||||
if (typeof showToast === 'function') showToast(data.message || '저장 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('저장 실패:', e);
|
||||
if (typeof showToast === 'function') showToast('저장 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
async deleteItem(id) {
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
const res = await fetch(`/finance/condolence-expenses/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
if (typeof showToast === 'function') showToast(data.message, 'success');
|
||||
await this.loadData();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('삭제 실패:', e);
|
||||
}
|
||||
},
|
||||
|
||||
exportCsv() {
|
||||
const headers = ['No', '경조사일자', '지출일자', '거래처명', '내역', '구분', '부조금여부', '지출방법', '부조금액', '선물여부', '선물종류', '선물금액', '총금액', '비고'];
|
||||
const rows = this.records.map((r, i) => [
|
||||
i + 1, r.event_date || '', r.expense_date || '', r.partner_name, r.description || '',
|
||||
r.category === 'congratulation' ? '축의' : '부조',
|
||||
r.has_cash ? '여' : '부', this.cashMethodLabel(r.cash_method), r.cash_amount || 0,
|
||||
r.has_gift ? '여' : '부', r.gift_type || '', r.gift_amount || 0,
|
||||
r.total_amount || 0, r.memo || '',
|
||||
]);
|
||||
|
||||
const BOM = '\uFEFF';
|
||||
const csv = BOM + [headers, ...rows].map(r => r.map(c => '"' + String(c).replace(/"/g, '""') + '"').join(',')).join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `경조사비_관리대장_${this.filterYear}.csv`;
|
||||
a.click();
|
||||
},
|
||||
|
||||
cashMethodLabel(method) {
|
||||
const map = { cash: '현금', transfer: '계좌이체', card: '카드' };
|
||||
return map[method] || method || '-';
|
||||
},
|
||||
|
||||
formatMoney(val) {
|
||||
if (!val && val !== 0) return '-';
|
||||
return Number(val).toLocaleString('ko-KR');
|
||||
},
|
||||
|
||||
formatMoneyInput(val) {
|
||||
const num = String(val).replace(/[^\d]/g, '');
|
||||
return num ? Number(num).toLocaleString('ko-KR') : '';
|
||||
},
|
||||
|
||||
unformat(val) {
|
||||
return String(val).replace(/[^\d]/g, '');
|
||||
},
|
||||
|
||||
parseNumber(val) {
|
||||
return parseInt(String(val).replace(/[^\d]/g, ''), 10) || 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -1449,6 +1449,15 @@
|
||||
Route::put('/{id}', [\App\Http\Controllers\Finance\VatRecordController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Finance\VatRecordController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// 경조사비 관리
|
||||
Route::get('/condolence-expenses', [\App\Http\Controllers\Finance\CondolenceExpenseController::class, 'index'])->name('condolence-expenses');
|
||||
Route::prefix('condolence-expenses')->name('condolence-expenses.')->group(function () {
|
||||
Route::get('/list', [\App\Http\Controllers\Finance\CondolenceExpenseController::class, 'list'])->name('list');
|
||||
Route::post('/store', [\App\Http\Controllers\Finance\CondolenceExpenseController::class, 'store'])->name('store');
|
||||
Route::put('/{id}', [\App\Http\Controllers\Finance\CondolenceExpenseController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Finance\CondolenceExpenseController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user