Files
sam-manage/resources/views/finance/dashboard.blade.php
2026-02-10 21:20:36 +09:00

422 lines
24 KiB
PHP

@extends('layouts.app')
@section('title', '재무 대시보드')
@section('content')
<div class="px-4 py-6">
{{-- 페이지 헤더 --}}
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">재무 대시보드</h1>
<p class="text-sm text-gray-500 mt-1">{{ now()->format('Y년 n월 j일') }} 현재</p>
</div>
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
<a href="{{ route('finance.accounts.index') }}"
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded-lg transition-colors">
<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="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>
보유계좌관리
</a>
<a href="{{ route('finance.fund-schedule') }}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
자금일정
</a>
</div>
</div>
{{-- 요약 카드 --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{{-- 잔액 --}}
<div class="bg-white rounded-lg shadow-sm p-5 border-l-4 border-blue-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500"> 잔액</p>
<p class="text-2xl font-bold text-gray-800 mt-1" data-total-balance>{{ number_format($accountSummary['total_balance']) }}</p>
<p class="text-xs text-gray-400 mt-1">{{ $accountSummary['total_accounts'] }} 계좌</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
{{-- 예정 수입 --}}
<div class="bg-white rounded-lg shadow-sm p-5 border-l-4 border-green-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">예정 수입</p>
<p class="text-2xl font-bold text-green-600 mt-1">+{{ number_format($monthlySummary['income']['pending']) }}</p>
<p class="text-xs text-gray-400 mt-1">이번 예정</p>
</div>
<div class="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11l5-5m0 0l5 5m-5-5v12"/>
</svg>
</div>
</div>
</div>
{{-- 예정 지출 --}}
<div class="bg-white rounded-lg shadow-sm p-5 border-l-4 border-red-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">예정 지출</p>
<p class="text-2xl font-bold text-red-600 mt-1">-{{ number_format($monthlySummary['expense']['pending']) }}</p>
<p class="text-xs text-gray-400 mt-1">이번 예정</p>
</div>
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 13l-5 5m0 0l-5-5m5 5V6"/>
</svg>
</div>
</div>
</div>
{{-- 7일내 일정 --}}
<div class="bg-white rounded-lg shadow-sm p-5 border-l-4 border-yellow-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">7일내 일정</p>
<p class="text-2xl font-bold text-gray-800 mt-1">{{ $scheduleSummary['upcoming_7days'] }}</p>
<p class="text-xs text-gray-400 mt-1">처리 필요</p>
</div>
<div class="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
</div>
{{-- 이번 요약 --}}
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">{{ now()->format('n월') }} 자금 일정 요약</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center p-4 bg-green-50 rounded-lg">
<p class="text-sm text-gray-500 mb-1">수입</p>
<p class="text-xl font-bold text-green-600">+{{ number_format($monthlySummary['income']['total']) }}</p>
<p class="text-xs text-gray-400 mt-1">
완료: {{ number_format($monthlySummary['income']['completed']) }} /
예정: {{ number_format($monthlySummary['income']['pending']) }}
</p>
</div>
<div class="text-center p-4 bg-red-50 rounded-lg">
<p class="text-sm text-gray-500 mb-1">지출</p>
<p class="text-xl font-bold text-red-600">-{{ number_format($monthlySummary['expense']['total']) }}</p>
<p class="text-xs text-gray-400 mt-1">
완료: {{ number_format($monthlySummary['expense']['completed']) }} /
예정: {{ number_format($monthlySummary['expense']['pending']) }}
</p>
</div>
<div class="text-center p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-gray-500 mb-1">순수익</p>
<p class="text-xl font-bold {{ $monthlySummary['net'] >= 0 ? 'text-blue-600' : 'text-red-600' }}">
{{ $monthlySummary['net'] >= 0 ? '+' : '' }}{{ number_format($monthlySummary['net']) }}
</p>
<p class="text-xs text-gray-400 mt-1"> {{ $monthlySummary['total_count'] }} 일정</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{{-- 계좌별 잔액 --}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">계좌별 잔액</h2>
<div class="flex items-center gap-3">
<button type="button" onclick="refreshAccountBalances()" id="refreshBalanceBtn" class="text-xs text-gray-500 hover:text-blue-600 flex items-center gap-1" title="잔액 새로고침">
<svg id="refreshBalanceIcon" 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
<span id="refreshBalanceText">새로고침</span>
</button>
<a href="{{ route('finance.accounts.index') }}" class="text-sm text-blue-600 hover:text-blue-700">
전체보기
</a>
</div>
</div>
<div id="accountBalancesList" class="divide-y divide-gray-100 max-h-80 overflow-y-auto">
@forelse($accountBalances as $account)
<div class="px-6 py-3 flex items-center justify-between hover:bg-gray-50" data-account-number="{{ str_replace('-', '', $account->account_number) }}">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<span class="text-xs font-medium text-gray-600">{{ mb_substr($account->bank_name, 0, 2) }}</span>
</div>
<div>
<p class="text-sm font-medium text-gray-800">
{{ $account->account_name ?: $account->bank_name }}
</p>
<p class="text-xs text-gray-500">{{ $account->account_number }}</p>
</div>
</div>
<div class="text-right">
<p class="text-sm font-semibold text-gray-800 account-balance" data-original="{{ $account->balance }}">{{ number_format($account->balance) }}</p>
<p class="text-xs text-gray-400">{{ $account->account_type }}</p>
</div>
</div>
@empty
<div class="px-6 py-8 text-center text-gray-400">
등록된 계좌가 없습니다.
</div>
@endforelse
</div>
</div>
{{-- 향후 7 자금 일정 --}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">향후 7 자금 일정</h2>
<a href="{{ route('finance.fund-schedule') }}" class="text-sm text-blue-600 hover:text-blue-700">
전체보기
</a>
</div>
<div class="divide-y divide-gray-100 max-h-80 overflow-y-auto">
@forelse($upcomingSchedules as $schedule)
<div class="px-6 py-3 flex items-center justify-between hover:bg-gray-50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full flex items-center justify-center {{ $schedule->schedule_type === 'income' ? 'bg-green-100' : 'bg-red-100' }}">
@if($schedule->schedule_type === 'income')
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11l5-5m0 0l5 5m-5-5v12"/>
</svg>
@else
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 13l-5 5m0 0l-5-5m5 5V6"/>
</svg>
@endif
</div>
<div>
<p class="text-sm font-medium text-gray-800">{{ $schedule->title }}</p>
<p class="text-xs text-gray-500">
{{ $schedule->scheduled_date->format('m/d') }}
@if($schedule->counterparty)
- {{ $schedule->counterparty }}
@endif
</p>
</div>
</div>
<div class="text-right">
<p class="text-sm font-semibold {{ $schedule->schedule_type === 'income' ? 'text-green-600' : 'text-red-600' }}">
{{ $schedule->schedule_type === 'income' ? '+' : '-' }}{{ number_format($schedule->amount) }}
</p>
<span class="text-xs px-2 py-0.5 rounded-full
{{ $schedule->status === 'pending' ? 'bg-yellow-100 text-yellow-700' : '' }}
{{ $schedule->status === 'completed' ? 'bg-green-100 text-green-700' : '' }}
{{ $schedule->status === 'cancelled' ? 'bg-gray-100 text-gray-700' : '' }}">
{{ $schedule->status === 'pending' ? '대기' : ($schedule->status === 'completed' ? '완료' : '취소') }}
</span>
</div>
</div>
@empty
<div class="px-6 py-8 text-center text-gray-400">
7일내 예정된 일정이 없습니다.
</div>
@endforelse
</div>
</div>
</div>
{{-- 최근 거래내역 (바로빌) --}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">최근 거래내역</h2>
<a href="{{ route('barobill.eaccount.index') }}" class="text-sm text-blue-600 hover:text-blue-700">
전체보기
</a>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">날짜</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">은행</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">적요</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상대방</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">입금</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">출금</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">잔액</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($recentTransactions as $tx)
@php
$transDate = \Carbon\Carbon::createFromFormat('Ymd', $tx->trans_date);
@endphp
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $transDate->format('m/d') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ $tx->bank_name ?? '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">
{{ Str::limit($tx->summary, 15) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{{ Str::limit($tx->cast, 10) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-medium {{ $tx->deposit > 0 ? 'text-green-600' : 'text-gray-300' }}">
{{ $tx->deposit > 0 ? '+' . number_format($tx->deposit) : '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-medium {{ $tx->withdraw > 0 ? 'text-red-600' : 'text-gray-300' }}">
{{ $tx->withdraw > 0 ? '-' . number_format($tx->withdraw) : '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500">
{{ number_format($tx->balance) }}
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
최근 7일간 거래내역이 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- 최근 카드 사용내역 (바로빌) --}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden mt-6">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">최근 카드 사용내역</h2>
<a href="{{ route('barobill.ecard.index') }}" class="text-sm text-blue-600 hover:text-blue-700">
전체보기
</a>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500">사용일시</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500">카드번호</th>
<th class="px-2 py-3 text-center text-xs font-medium text-gray-500 w-12">공제</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500">가맹점</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500">내역</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500">금액</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500">계정과목</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($recentCardTransactions as $card)
@php
$useDate = \Carbon\Carbon::createFromFormat('Ymd', $card->use_date);
$useTime = $card->use_time ? substr($card->use_time, 0, 2) . ':' . substr($card->use_time, 2, 2) : '';
$isCancel = $card->approval_type === '취소';
$isDeductible = $card->deduction_type === 'deductible';
$cardLast4 = substr($card->card_num, -4);
@endphp
<tr class="hover:bg-gray-50 {{ $isCancel ? 'bg-red-50' : '' }}">
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{{ $useDate->format('m/d') }} {{ $useTime }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
****-{{ $cardLast4 }}
</td>
<td class="px-2 py-3 whitespace-nowrap text-center">
<span class="px-1.5 py-0.5 rounded text-xs font-medium {{ $isDeductible ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' }}">
{{ $isDeductible ? '공제' : '불공' }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-800" title="{{ $card->merchant_name }}">
{{ Str::limit($card->merchant_name, 25) }}
</td>
<td class="px-4 py-3 text-sm text-gray-600" title="{{ $card->description }}">
{{ Str::limit($card->description ?: '-', 25) }}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-right font-medium {{ $isCancel ? 'text-red-600' : 'text-blue-600' }}">
{{ $isCancel ? '-' : '' }}{{ number_format($card->approval_amount) }}
</td>
<td class="px-4 py-3 text-sm text-gray-600" title="{{ $card->account_name }}">
{{ Str::limit($card->account_name ?: '-', 10) }}
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
최근 7일간 카드 사용내역이 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<script>
async function refreshAccountBalances() {
const btn = document.getElementById('refreshBalanceBtn');
const icon = document.getElementById('refreshBalanceIcon');
const text = document.getElementById('refreshBalanceText');
// 로딩 상태
btn.disabled = true;
icon.classList.add('animate-spin');
text.textContent = '조회중...';
try {
// DB에 저장된 최신 거래내역의 잔액 조회
const res = await fetch('{{ route("barobill.eaccount.latest-balances") }}');
const data = await res.json();
if (data.success && data.balances && data.balances.length > 0) {
// 계좌번호 -> 잔액 맵 생성
const balanceMap = {};
data.balances.forEach(b => {
const num = (b.bankAccountNum || '').replace(/-/g, '');
balanceMap[num] = b.balance || 0;
});
// 각 계좌 잔액 업데이트
let totalBalance = 0;
let matchedCount = 0;
document.querySelectorAll('#accountBalancesList [data-account-number]').forEach(row => {
const accNum = row.dataset.accountNumber;
const balanceEl = row.querySelector('.account-balance');
if (balanceEl && balanceMap.hasOwnProperty(accNum)) {
const newBalance = balanceMap[accNum];
balanceEl.textContent = new Intl.NumberFormat('ko-KR').format(newBalance) + '원';
balanceEl.classList.add('text-blue-600');
setTimeout(() => balanceEl.classList.remove('text-blue-600'), 2000);
totalBalance += newBalance;
matchedCount++;
} else if (balanceEl) {
totalBalance += parseFloat(balanceEl.dataset.original || 0);
}
});
// 총 잔액 업데이트
const totalBalanceEl = document.querySelector('[data-total-balance]');
if (totalBalanceEl && matchedCount > 0) {
totalBalanceEl.textContent = new Intl.NumberFormat('ko-KR').format(totalBalance) + '원';
}
text.textContent = matchedCount > 0 ? '완료' : '매칭없음';
setTimeout(() => { text.textContent = '새로고침'; }, 2000);
} else {
text.textContent = data.count === 0 ? '데이터없음' : '실패';
setTimeout(() => { text.textContent = '새로고침'; }, 2000);
}
} catch (e) {
console.error('잔액 조회 오류:', e);
text.textContent = '오류';
setTimeout(() => { text.textContent = '새로고침'; }, 2000);
} finally {
btn.disabled = false;
icon.classList.remove('animate-spin');
}
}
// 즉시 실행 (HTMX 페이지 전환에서도 동작)
refreshAccountBalances();
</script>
@endsection