feat:일일자금일보 기간별 보고서 기능 추가 (바로빌 계좌내역 기반)
This commit is contained in:
@@ -5,8 +5,10 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\DailyFundTransaction;
|
||||
use App\Models\Finance\DailyFundMemo;
|
||||
use App\Models\Barobill\BankTransaction as BarobillBankTransaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DailyFundController extends Controller
|
||||
{
|
||||
@@ -157,4 +159,116 @@ public function saveMemo(Request $request): JsonResponse
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간별 자금일보 (바로빌 계좌 거래내역 기반)
|
||||
*/
|
||||
public function periodReport(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$startDate = $request->input('start_date', now()->subMonth()->format('Ymd'));
|
||||
$endDate = $request->input('end_date', now()->format('Ymd'));
|
||||
|
||||
// YYYYMMDD 형식으로 변환
|
||||
$startDateYmd = str_replace('-', '', $startDate);
|
||||
$endDateYmd = str_replace('-', '', $endDate);
|
||||
|
||||
// 기간 내 거래내역 조회
|
||||
$transactions = BarobillBankTransaction::where('tenant_id', $tenantId)
|
||||
->whereBetween('trans_date', [$startDateYmd, $endDateYmd])
|
||||
->orderBy('trans_date', 'desc')
|
||||
->orderBy('trans_time', 'desc')
|
||||
->get();
|
||||
|
||||
// 일별로 그룹핑
|
||||
$dailyData = [];
|
||||
$accountBalances = []; // 계좌별 최신 잔액 추적
|
||||
|
||||
foreach ($transactions as $tx) {
|
||||
$date = $tx->trans_date;
|
||||
$accountNum = $tx->bank_account_num;
|
||||
|
||||
if (!isset($dailyData[$date])) {
|
||||
$dailyData[$date] = [
|
||||
'date' => $date,
|
||||
'dateFormatted' => $this->formatDateKorean($date),
|
||||
'accounts' => [],
|
||||
'deposits' => [],
|
||||
'withdrawals' => [],
|
||||
'totalDeposit' => 0,
|
||||
'totalWithdraw' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// 계좌별 데이터 집계
|
||||
if (!isset($dailyData[$date]['accounts'][$accountNum])) {
|
||||
$dailyData[$date]['accounts'][$accountNum] = [
|
||||
'bankName' => $tx->bank_name,
|
||||
'accountNum' => $accountNum,
|
||||
'deposit' => 0,
|
||||
'withdraw' => 0,
|
||||
'balance' => $tx->balance,
|
||||
];
|
||||
}
|
||||
|
||||
// 입출금 내역 추가
|
||||
if ($tx->deposit > 0) {
|
||||
$dailyData[$date]['deposits'][] = [
|
||||
'time' => $tx->trans_time,
|
||||
'bankName' => $tx->bank_name,
|
||||
'summary' => $tx->summary,
|
||||
'cast' => $tx->cast,
|
||||
'amount' => $tx->deposit,
|
||||
'balance' => $tx->balance,
|
||||
];
|
||||
$dailyData[$date]['accounts'][$accountNum]['deposit'] += $tx->deposit;
|
||||
$dailyData[$date]['totalDeposit'] += $tx->deposit;
|
||||
}
|
||||
|
||||
if ($tx->withdraw > 0) {
|
||||
$dailyData[$date]['withdrawals'][] = [
|
||||
'time' => $tx->trans_time,
|
||||
'bankName' => $tx->bank_name,
|
||||
'summary' => $tx->summary,
|
||||
'cast' => $tx->cast,
|
||||
'amount' => $tx->withdraw,
|
||||
'balance' => $tx->balance,
|
||||
];
|
||||
$dailyData[$date]['accounts'][$accountNum]['withdraw'] += $tx->withdraw;
|
||||
$dailyData[$date]['totalWithdraw'] += $tx->withdraw;
|
||||
}
|
||||
|
||||
// 해당 일자의 최신 잔액 업데이트
|
||||
$dailyData[$date]['accounts'][$accountNum]['balance'] = $tx->balance;
|
||||
}
|
||||
|
||||
// accounts를 배열로 변환
|
||||
foreach ($dailyData as $date => &$data) {
|
||||
$data['accounts'] = array_values($data['accounts']);
|
||||
}
|
||||
|
||||
// 날짜 내림차순 정렬 (최신 일자가 위)
|
||||
krsort($dailyData);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
'dailyReports' => array_values($dailyData),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function formatDateKorean(string $dateYmd): string
|
||||
{
|
||||
$year = substr($dateYmd, 0, 4);
|
||||
$month = (int) substr($dateYmd, 4, 2);
|
||||
$day = (int) substr($dateYmd, 6, 2);
|
||||
|
||||
$date = \Carbon\Carbon::createFromFormat('Ymd', $dateYmd);
|
||||
$dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][$date->dayOfWeek];
|
||||
|
||||
return "{$year}년 {$month}월 {$day}일 {$dayOfWeek}요일";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
<style>
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.print-only { display: block !important; }
|
||||
body { background: white !important; }
|
||||
}
|
||||
#daily-fund-root .min-h-screen {
|
||||
min-height: auto;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@@ -22,92 +17,29 @@
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js?v={{ time() }}"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js?v={{ time() }}"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js?v={{ time() }}"></script>
|
||||
<script src="https://unpkg.com/lucide@latest?v={{ time() }}"></script>
|
||||
<script type="text/babel" data-version="{{ time() }}">
|
||||
const { useState, useRef, useEffect } = React;
|
||||
|
||||
const createIcon = (name) => ({ className = "w-5 h-5", ...props }) => {
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current && lucide.icons[name]) {
|
||||
ref.current.innerHTML = '';
|
||||
const svg = lucide.createElement(lucide.icons[name]);
|
||||
svg.setAttribute('class', className);
|
||||
ref.current.appendChild(svg);
|
||||
}
|
||||
}, [className]);
|
||||
return <span ref={ref} className="inline-flex items-center" {...props} />;
|
||||
};
|
||||
|
||||
const Calendar = createIcon('calendar');
|
||||
const ChevronLeft = createIcon('chevron-left');
|
||||
const ChevronRight = createIcon('chevron-right');
|
||||
const Download = createIcon('download');
|
||||
const Printer = createIcon('printer');
|
||||
const Send = createIcon('send');
|
||||
const Plus = createIcon('plus');
|
||||
const Trash2 = createIcon('trash-2');
|
||||
const Edit = createIcon('edit');
|
||||
const Save = createIcon('save');
|
||||
const X = createIcon('x');
|
||||
const Check = createIcon('check');
|
||||
const Wallet = createIcon('wallet');
|
||||
const ArrowUpRight = createIcon('arrow-up-right');
|
||||
const ArrowDownRight = createIcon('arrow-down-right');
|
||||
const Building = createIcon('building');
|
||||
const FileSpreadsheet = createIcon('file-spreadsheet');
|
||||
const TrendingUp = createIcon('trending-up');
|
||||
const TrendingDown = createIcon('trending-down');
|
||||
const RefreshCw = createIcon('refresh-cw');
|
||||
const AlertCircle = createIcon('alert-circle');
|
||||
const CheckCircle = createIcon('check-circle');
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function DailyFundReport() {
|
||||
const today = new Date();
|
||||
const [selectedDate, setSelectedDate] = useState(today.toISOString().split('T')[0]);
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [addType, setAddType] = useState('income');
|
||||
const [reportSent, setReportSent] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [editType, setEditType] = useState('income');
|
||||
const monthAgo = new Date();
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||
|
||||
const [startDate, setStartDate] = useState(monthAgo.toISOString().split('T')[0]);
|
||||
const [endDate, setEndDate] = useState(today.toISOString().split('T')[0]);
|
||||
const [dailyReports, setDailyReports] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [incomeList, setIncomeList] = useState([]);
|
||||
const [expenseList, setExpenseList] = useState([]);
|
||||
const [previousBalance, setPreviousBalance] = useState(0);
|
||||
const [memo, setMemo] = useState('');
|
||||
const [author, setAuthor] = useState('');
|
||||
const [updatedAt, setUpdatedAt] = useState('');
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const [newTransaction, setNewTransaction] = useState({
|
||||
time: '',
|
||||
accountName: '',
|
||||
description: '',
|
||||
amount: '',
|
||||
category: '',
|
||||
note: ''
|
||||
});
|
||||
|
||||
const fetchData = async (date) => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/finance/daily-fund/list?date=${date || selectedDate}`);
|
||||
const res = await fetch(`/finance/daily-fund/period-report?start_date=${startDate.replace(/-/g, '')}&end_date=${endDate.replace(/-/g, '')}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setIncomeList(data.data.income || []);
|
||||
setExpenseList(data.data.expense || []);
|
||||
setPreviousBalance(data.data.previousBalance || 0);
|
||||
setMemo(data.data.memo || '');
|
||||
setAuthor(data.data.author || '');
|
||||
setUpdatedAt(data.data.updatedAt || '');
|
||||
setDailyReports(data.data.dailyReports || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('데이터 조회 실패:', err);
|
||||
@@ -116,425 +48,243 @@ function DailyFundReport() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(selectedDate); }, [selectedDate]);
|
||||
|
||||
const moveDate = (days) => {
|
||||
const date = new Date(selectedDate);
|
||||
date.setDate(date.getDate() + days);
|
||||
setSelectedDate(date.toISOString().split('T')[0]);
|
||||
};
|
||||
useEffect(() => { fetchData(); }, [startDate, endDate]);
|
||||
|
||||
const formatCurrency = (num) => {
|
||||
if (!num) return '0';
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
const formatCurrencyShort = (num) => {
|
||||
if (!num) return '0';
|
||||
if (num >= 100000000) return `${(num / 100000000).toFixed(1)}억`;
|
||||
if (num >= 10000000) return `${(num / 10000000).toFixed(0)}천만`;
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(0)}만`;
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
const formatInputCurrency = (value) => {
|
||||
if (!value && value !== 0) return '';
|
||||
const num = String(value).replace(/[^\d]/g, '');
|
||||
if (!num) return '';
|
||||
return Number(num).toLocaleString();
|
||||
};
|
||||
|
||||
const parseInputCurrency = (value) => String(value).replace(/[^\d]/g, '');
|
||||
|
||||
const getDayOfWeek = (dateStr) => {
|
||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
return days[new Date(dateStr).getDay()];
|
||||
const setThisMonth = () => {
|
||||
const now = new Date();
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
setStartDate(firstDay.toISOString().split('T')[0]);
|
||||
setEndDate(now.toISOString().split('T')[0]);
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 (${getDayOfWeek(dateStr)})`;
|
||||
};
|
||||
|
||||
const totalIncome = incomeList.reduce((sum, item) => sum + item.amount, 0);
|
||||
const totalExpense = expenseList.reduce((sum, item) => sum + item.amount, 0);
|
||||
const totalCurrentBalance = previousBalance + totalIncome - totalExpense;
|
||||
|
||||
const getCategoryTotals = (items) => {
|
||||
const totals = {};
|
||||
items.forEach(item => {
|
||||
if (!totals[item.category]) totals[item.category] = 0;
|
||||
totals[item.category] += item.amount;
|
||||
});
|
||||
return Object.entries(totals).sort((a, b) => b[1] - a[1]);
|
||||
};
|
||||
|
||||
const handleAddTransaction = async () => {
|
||||
if (!newTransaction.description || !newTransaction.amount) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch('/finance/daily-fund/store', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify({
|
||||
transactionDate: selectedDate,
|
||||
type: addType,
|
||||
time: newTransaction.time || new Date().toTimeString().slice(0, 5),
|
||||
accountName: newTransaction.accountName,
|
||||
description: newTransaction.description,
|
||||
amount: parseInt(newTransaction.amount) || 0,
|
||||
category: newTransaction.category,
|
||||
note: newTransaction.note,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setNewTransaction({ time: '', accountName: '', description: '', amount: '', category: '', note: '' });
|
||||
setShowAddModal(false);
|
||||
fetchData(selectedDate);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.message || '등록에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('등록에 실패했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTransaction = async (type, id) => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await fetch(`/finance/daily-fund/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) fetchData(selectedDate);
|
||||
} catch (err) {
|
||||
alert('삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTransaction = (type, item) => {
|
||||
setEditType(type);
|
||||
setEditingItem({ ...item, amount: item.amount.toString() });
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (!editingItem.description || !editingItem.amount) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/finance/daily-fund/${editingItem.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify({
|
||||
type: editType,
|
||||
time: editingItem.time,
|
||||
accountName: editingItem.accountName,
|
||||
description: editingItem.description,
|
||||
amount: parseInt(editingItem.amount) || 0,
|
||||
category: editingItem.category,
|
||||
note: editingItem.note,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setShowEditModal(false);
|
||||
setEditingItem(null);
|
||||
fetchData(selectedDate);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.message || '수정에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('수정에 실패했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveMemo = async () => {
|
||||
try {
|
||||
const res = await fetch('/finance/daily-fund/memo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify({ date: selectedDate, memo, author: author || '재무팀' }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setUpdatedAt(data.data.updatedAt);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('메모 저장에 실패했습니다.');
|
||||
}
|
||||
const setLastMonth = () => {
|
||||
const now = new Date();
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
setStartDate(firstDay.toISOString().split('T')[0]);
|
||||
setEndDate(lastDay.toISOString().split('T')[0]);
|
||||
};
|
||||
|
||||
const handlePrint = () => window.print();
|
||||
|
||||
const handleDownload = () => {
|
||||
const rows = [
|
||||
['일일자금일보', formatDate(selectedDate)],
|
||||
[],
|
||||
['구분', '시간', '계좌', '적요', '금액', '카테고리', '비고'],
|
||||
...incomeList.map(item => ['입금', item.time, item.accountName || '', item.description, item.amount, item.category, item.note]),
|
||||
...expenseList.map(item => ['출금', item.time, item.accountName || '', item.description, item.amount, item.category, item.note]),
|
||||
[],
|
||||
['전일 잔액', '', '', '', previousBalance],
|
||||
['금일 입금', '', '', '', totalIncome],
|
||||
['금일 출금', '', '', '', totalExpense],
|
||||
['금일 잔액', '', '', '', totalCurrentBalance]
|
||||
];
|
||||
const csvContent = rows.map(row => row.join(',')).join('\n');
|
||||
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `일일자금일보_${selectedDate}.csv`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
const handleSendReport = () => {
|
||||
setReportSent(true);
|
||||
setShowSendModal(false);
|
||||
setTimeout(() => setReportSent(false), 3000);
|
||||
};
|
||||
|
||||
const renderTransactionList = (items, type, colorClass, signPrefix) => (
|
||||
items.length === 0 ? (
|
||||
<p className="text-center text-gray-400 py-8">{type === 'income' ? '입금' : '출금'} 내역이 없습니다</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{items.map(item => (
|
||||
<div key={item.id} className={`flex items-center justify-between p-3 ${colorClass} rounded-lg border cursor-pointer hover:opacity-80 transition-colors`} onClick={() => handleEditTransaction(type, item)}>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-gray-400">{item.time}</span>
|
||||
{item.accountName && <span className="px-1.5 py-0.5 rounded text-xs bg-gray-200 text-gray-600">{item.accountName}</span>}
|
||||
<span className="text-xs text-gray-400">{item.category}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-900">{item.description}</p>
|
||||
{item.note && <p className="text-xs text-gray-400 mt-0.5">{item.note}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-semibold ${type === 'income' ? 'text-emerald-600' : 'text-rose-600'}`}>{signPrefix}{formatCurrency(item.amount)}원</span>
|
||||
<button onClick={(e) => { e.stopPropagation(); handleEditTransaction(type, item); }} className="p-1.5 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded transition-colors no-print"><Edit className="w-4 h-4" /></button>
|
||||
<button onClick={(e) => { e.stopPropagation(); handleDeleteTransaction(type, item.id); }} className="p-1.5 text-gray-400 hover:text-rose-500 hover:bg-rose-50 rounded transition-colors no-print"><Trash2 className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
<header className="bg-white border-b border-gray-200 rounded-t-xl mb-6 no-print">
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-emerald-100 rounded-xl"><FileSpreadsheet className="w-6 h-6 text-emerald-600" /></div>
|
||||
<div><h1 className="text-xl font-bold text-gray-900">일일자금일보</h1><p className="text-sm text-gray-500">Daily Fund Report</p></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={handleDownload} className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg"><Download className="w-4 h-4" /><span className="text-sm">Excel</span></button>
|
||||
<button onClick={handlePrint} className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg"><Printer className="w-4 h-4" /><span className="text-sm">인쇄</span></button>
|
||||
<button onClick={() => setShowSendModal(true)} className="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg"><Send className="w-4 h-4" /><span className="text-sm">보고</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6 no-print">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-800">일일자금일보</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Daily Fund Report</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{reportSent && (
|
||||
<div className="fixed top-20 right-6 bg-emerald-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 no-print">
|
||||
<CheckCircle className="w-5 h-5" /><span>보고가 전송되었습니다.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6 no-print">
|
||||
<div className="flex items-center justify-between">
|
||||
<button onClick={() => moveDate(-1)} className="p-2 hover:bg-gray-100 rounded-lg"><ChevronLeft className="w-5 h-5 text-gray-600" /></button>
|
||||
<div className="flex items-center gap-3">
|
||||
<Calendar className="w-5 h-5 text-gray-400" />
|
||||
<input type="date" value={selectedDate} onChange={(e) => setSelectedDate(e.target.value)} className="text-lg font-semibold text-gray-900 border-0 focus:ring-0 cursor-pointer" />
|
||||
<span className="text-lg text-gray-600">({getDayOfWeek(selectedDate)})</span>
|
||||
</div>
|
||||
<button onClick={() => moveDate(1)} className="p-2 hover:bg-gray-100 rounded-lg"><ChevronRight className="w-5 h-5 text-gray-600" /></button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button onClick={handlePrint} className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg text-sm">
|
||||
인쇄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden print-only mb-8">
|
||||
<h1 className="text-2xl font-bold text-center mb-2">일 일 자 금 일 보</h1>
|
||||
<p className="text-center text-gray-600">{formatDate(selectedDate)}</p>
|
||||
{/* 기간 선택 */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6 no-print">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<span className="text-sm font-medium text-gray-600">기간</span>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
<span className="text-gray-400">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
<button onClick={setThisMonth} className="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm">
|
||||
이번 달
|
||||
</button>
|
||||
<button onClick={setLastMonth} className="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm">
|
||||
지난 달
|
||||
</button>
|
||||
<button onClick={fetchData} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
|
||||
조회
|
||||
</button>
|
||||
<span className="text-sm text-gray-500 ml-auto">조회: {dailyReports.length}일</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
|
||||
<div className="flex items-center justify-center gap-2 text-gray-400">
|
||||
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
<svg className="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
불러오는 중...
|
||||
</div>
|
||||
</div>
|
||||
) : dailyReports.length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow-sm p-12 text-center text-gray-400">
|
||||
해당 기간에 거래내역이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4"><span className="text-sm text-gray-500">전일 잔액</span><div className="p-2 bg-gray-100 rounded-lg"><Wallet className="w-4 h-4 text-gray-600" /></div></div>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatCurrencyShort(previousBalance)}원</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{formatCurrency(previousBalance)}원</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-emerald-200 p-6 bg-emerald-50/30">
|
||||
<div className="flex items-center justify-between mb-4"><span className="text-sm text-emerald-700">금일 입금</span><div className="p-2 bg-emerald-100 rounded-lg"><ArrowUpRight className="w-4 h-4 text-emerald-600" /></div></div>
|
||||
<p className="text-2xl font-bold text-emerald-600">+{formatCurrencyShort(totalIncome)}원</p>
|
||||
<p className="text-xs text-emerald-500 mt-1">{formatCurrency(totalIncome)}원</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-rose-200 p-6 bg-rose-50/30">
|
||||
<div className="flex items-center justify-between mb-4"><span className="text-sm text-rose-700">금일 출금</span><div className="p-2 bg-rose-100 rounded-lg"><ArrowDownRight className="w-4 h-4 text-rose-600" /></div></div>
|
||||
<p className="text-2xl font-bold text-rose-600">-{formatCurrencyShort(totalExpense)}원</p>
|
||||
<p className="text-xs text-rose-500 mt-1">{formatCurrency(totalExpense)}원</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-blue-600 to-blue-700 rounded-xl p-6 text-white">
|
||||
<div className="flex items-center justify-between mb-4"><span className="text-sm text-blue-100">금일 잔액</span><div className="p-2 bg-white/20 rounded-lg"><Wallet className="w-4 h-4 text-white" /></div></div>
|
||||
<p className="text-2xl font-bold">{formatCurrencyShort(totalCurrentBalance)}원</p>
|
||||
<p className="text-xs text-blue-200 mt-1">{formatCurrency(totalCurrentBalance)}원</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2"><div className="w-3 h-3 bg-emerald-500 rounded-full"></div>입금 내역</h3>
|
||||
<button onClick={() => { setAddType('income'); setShowAddModal(true); }} className="flex items-center gap-1 px-3 py-1.5 text-sm text-emerald-600 hover:bg-emerald-50 rounded-lg no-print"><Plus className="w-4 h-4" />추가</button>
|
||||
<div className="space-y-6">
|
||||
{dailyReports.map((report, idx) => (
|
||||
<div key={report.date} className="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
{/* 일자 헤더 */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4">
|
||||
<h2 className="text-lg font-bold text-white">
|
||||
일자: {report.dateFormatted}
|
||||
</h2>
|
||||
</div>
|
||||
{renderTransactionList(incomeList, 'income', 'bg-emerald-50/50 border-emerald-100', '+')}
|
||||
{incomeList.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<p className="text-xs text-gray-500 mb-2">카테고리별 집계</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{getCategoryTotals(incomeList).map(([cat, amount]) => (
|
||||
<span key={cat} className="px-2 py-1 bg-emerald-100 text-emerald-700 rounded text-xs">{cat}: {formatCurrencyShort(amount)}원</span>
|
||||
|
||||
{/* 계좌별 잔고 테이블 */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 border">구분</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 border">입금</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 border">출금</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 border">잔액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.accounts.map((acc, accIdx) => (
|
||||
<tr key={accIdx} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm text-gray-800 border">
|
||||
<span className="font-medium">{acc.bankName}</span>
|
||||
<span className="text-gray-500 ml-2 text-xs">({acc.accountNum})</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-blue-600 border">
|
||||
{acc.deposit > 0 ? formatCurrency(acc.deposit) : ''}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-red-600 border">
|
||||
{acc.withdraw > 0 ? formatCurrency(acc.withdraw) : ''}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right font-semibold text-gray-800 border">
|
||||
{formatCurrency(acc.balance)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="bg-gray-100 font-semibold">
|
||||
<td className="px-4 py-3 text-sm text-gray-800 border">합계</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-blue-600 border">
|
||||
{formatCurrency(report.totalDeposit)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-red-600 border">
|
||||
{formatCurrency(report.totalWithdraw)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right text-gray-800 border">
|
||||
{formatCurrency(report.accounts.reduce((sum, a) => sum + a.balance, 0))}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 입출금 내역 */}
|
||||
<div className="p-6">
|
||||
<h3 className="text-base font-semibold text-gray-800 mb-4 text-center bg-blue-50 py-2 rounded">
|
||||
<예금 입출금 내역>
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 입금 내역 */}
|
||||
<div>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-blue-50">
|
||||
<th colSpan="3" className="px-4 py-2 text-center text-sm font-semibold text-blue-700 border">
|
||||
입금
|
||||
</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-600 border">입금처/적요</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-600 border w-28">금액</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-600 border w-28">잔액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.deposits.length === 0 ? (
|
||||
<tr><td colSpan="3" className="px-3 py-4 text-center text-gray-400 text-sm border">입금 내역 없음</td></tr>
|
||||
) : (
|
||||
<>
|
||||
{report.deposits.map((d, dIdx) => (
|
||||
<tr key={dIdx} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2 text-sm text-gray-800 border">
|
||||
<div className="font-medium">{d.cast || d.summary}</div>
|
||||
<div className="text-xs text-gray-500">{d.bankName}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-right text-blue-600 font-medium border">
|
||||
{formatCurrency(d.amount)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-right text-gray-600 border">
|
||||
{formatCurrency(d.balance)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="bg-blue-50 font-semibold">
|
||||
<td className="px-3 py-2 text-sm text-gray-800 border">입금 합계</td>
|
||||
<td className="px-3 py-2 text-sm text-right text-blue-700 border">
|
||||
{formatCurrency(report.totalDeposit)}
|
||||
</td>
|
||||
<td className="px-3 py-2 border"></td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 출금 내역 */}
|
||||
<div>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-red-50">
|
||||
<th colSpan="2" className="px-4 py-2 text-center text-sm font-semibold text-red-700 border">
|
||||
출금
|
||||
</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-600 border">출금처/적요</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-600 border w-28">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.withdrawals.length === 0 ? (
|
||||
<tr><td colSpan="2" className="px-3 py-4 text-center text-gray-400 text-sm border">출금 내역 없음</td></tr>
|
||||
) : (
|
||||
<>
|
||||
{report.withdrawals.map((w, wIdx) => (
|
||||
<tr key={wIdx} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2 text-sm text-gray-800 border">
|
||||
<div className="font-medium">{w.cast || w.summary}</div>
|
||||
<div className="text-xs text-gray-500">{w.bankName}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-right text-red-600 font-medium border">
|
||||
{formatCurrency(w.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="bg-red-50 font-semibold">
|
||||
<td className="px-3 py-2 text-sm text-gray-800 border">출금 합계</td>
|
||||
<td className="px-3 py-2 text-sm text-right text-red-700 border">
|
||||
{formatCurrency(report.totalWithdraw)}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2"><div className="w-3 h-3 bg-rose-500 rounded-full"></div>출금 내역</h3>
|
||||
<button onClick={() => { setAddType('expense'); setShowAddModal(true); }} className="flex items-center gap-1 px-3 py-1.5 text-sm text-rose-600 hover:bg-rose-50 rounded-lg no-print"><Plus className="w-4 h-4" />추가</button>
|
||||
</div>
|
||||
{renderTransactionList(expenseList, 'expense', 'bg-rose-50/50 border-rose-100', '-')}
|
||||
{expenseList.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<p className="text-xs text-gray-500 mb-2">카테고리별 집계</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{getCategoryTotals(expenseList).map(([cat, amount]) => (
|
||||
<span key={cat} className="px-2 py-1 bg-rose-100 text-rose-700 rounded text-xs">{cat}: {formatCurrencyShort(amount)}원</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">특이사항 / 메모</h3>
|
||||
<textarea value={memo} onChange={(e) => setMemo(e.target.value)} placeholder="특이사항이나 메모를 입력하세요..." className="w-full h-24 px-4 py-3 border border-gray-200 rounded-lg resize-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 no-print" />
|
||||
{memo && <div className="hidden print-only mt-2 p-3 bg-gray-50 rounded-lg"><p className="text-sm text-gray-700">{memo}</p></div>}
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t border-gray-200 text-sm text-gray-500">
|
||||
<div>
|
||||
{author && <p>작성자: {author}</p>}
|
||||
{updatedAt && <p>작성일시: {updatedAt}</p>}
|
||||
</div>
|
||||
<button onClick={handleSaveMemo} className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg no-print"><Save className="w-4 h-4" />저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print">
|
||||
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-gray-900">{addType === 'income' ? '입금 추가' : '출금 추가'}</h3>
|
||||
<button onClick={() => setShowAddModal(false)} className="p-1 hover:bg-gray-100 rounded-lg"><X className="w-5 h-5 text-gray-500" /></button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">시간</label><input type="time" value={newTransaction.time} onChange={(e) => setNewTransaction(prev => ({ ...prev, time: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">계좌/은행</label><input type="text" value={newTransaction.accountName} onChange={(e) => setNewTransaction(prev => ({ ...prev, accountName: e.target.value }))} placeholder="국민은행" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">적요 *</label><input type="text" value={newTransaction.description} onChange={(e) => setNewTransaction(prev => ({ ...prev, description: e.target.value }))} placeholder="거래 내용을 입력하세요" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">금액 *</label><input type="text" value={formatInputCurrency(newTransaction.amount)} onChange={(e) => setNewTransaction(prev => ({ ...prev, amount: parseInputCurrency(e.target.value) }))} placeholder="0" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">카테고리</label>
|
||||
<select value={newTransaction.category} onChange={(e) => setNewTransaction(prev => ({ ...prev, category: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">
|
||||
<option value="">선택</option>
|
||||
{addType === 'income' ? (<><option value="개발비">개발비</option><option value="구독료">구독료</option><option value="용역비">용역비</option><option value="기타수입">기타수입</option></>) : (<><option value="인건비">인건비</option><option value="운영비">운영비</option><option value="외주비">외주비</option><option value="임대료">임대료</option><option value="소모품비">소모품비</option><option value="기타지출">기타지출</option></>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">비고</label><input type="text" value={newTransaction.note} onChange={(e) => setNewTransaction(prev => ({ ...prev, note: e.target.value }))} placeholder="추가 메모" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button onClick={() => setShowAddModal(false)} className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">취소</button>
|
||||
<button onClick={handleAddTransaction} disabled={saving} className={`flex-1 px-4 py-2 text-white rounded-lg disabled:opacity-50 ${addType === 'income' ? 'bg-emerald-600 hover:bg-emerald-700' : 'bg-rose-600 hover:bg-rose-700'}`}>{saving ? '저장 중...' : '추가'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSendModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print">
|
||||
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-gray-900">일일자금일보 보고</h3>
|
||||
<button onClick={() => setShowSendModal(false)} className="p-1 hover:bg-gray-100 rounded-lg"><X className="w-5 h-5 text-gray-500" /></button>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm text-gray-600 mb-2">보고 대상: <span className="font-medium text-gray-900">대표이사</span></p>
|
||||
<p className="text-sm text-gray-600 mb-2">보고일: <span className="font-medium text-gray-900">{formatDate(selectedDate)}</span></p>
|
||||
<div className="border-t border-gray-200 mt-3 pt-3">
|
||||
<p className="text-sm text-gray-600">금일 잔액: <span className="font-bold text-blue-600">{formatCurrency(totalCurrentBalance)}원</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button onClick={() => setShowSendModal(false)} className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">취소</button>
|
||||
<button onClick={handleSendReport} className="flex-1 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg flex items-center justify-center gap-2"><Send className="w-4 h-4" />보고 전송</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEditModal && editingItem && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print">
|
||||
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-gray-900">{editType === 'income' ? '입금 수정' : '출금 수정'}</h3>
|
||||
<button onClick={() => { setShowEditModal(false); setEditingItem(null); }} className="p-1 hover:bg-gray-100 rounded-lg"><X className="w-5 h-5 text-gray-500" /></button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">시간</label><input type="time" value={editingItem.time || ''} onChange={(e) => setEditingItem(prev => ({ ...prev, time: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">계좌/은행</label><input type="text" value={editingItem.accountName || ''} onChange={(e) => setEditingItem(prev => ({ ...prev, accountName: e.target.value }))} placeholder="국민은행" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">적요 *</label><input type="text" value={editingItem.description} onChange={(e) => setEditingItem(prev => ({ ...prev, description: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">금액 *</label><input type="text" value={formatInputCurrency(editingItem.amount)} onChange={(e) => setEditingItem(prev => ({ ...prev, amount: parseInputCurrency(e.target.value) }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">카테고리</label>
|
||||
<select value={editingItem.category || ''} onChange={(e) => setEditingItem(prev => ({ ...prev, category: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">
|
||||
<option value="">선택</option>
|
||||
{editType === 'income' ? (<><option value="개발비">개발비</option><option value="구독료">구독료</option><option value="용역비">용역비</option><option value="기타수입">기타수입</option></>) : (<><option value="인건비">인건비</option><option value="운영비">운영비</option><option value="외주비">외주비</option><option value="임대료">임대료</option><option value="소모품비">소모품비</option><option value="기타지출">기타지출</option></>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">비고</label><input type="text" value={editingItem.note || ''} onChange={(e) => setEditingItem(prev => ({ ...prev, note: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button onClick={() => { setShowEditModal(false); setEditingItem(null); handleDeleteTransaction(editType, editingItem.id); }} className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg">삭제</button>
|
||||
<button onClick={() => { setShowEditModal(false); setEditingItem(null); }} className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">취소</button>
|
||||
<button onClick={handleSaveEdit} disabled={saving} className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50">{saving ? '저장 중...' : '저장'}</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -746,6 +746,7 @@
|
||||
// 일일자금일보 API
|
||||
Route::prefix('daily-fund')->name('daily-fund.')->group(function () {
|
||||
Route::get('/list', [\App\Http\Controllers\Finance\DailyFundController::class, 'index'])->name('list');
|
||||
Route::get('/period-report', [\App\Http\Controllers\Finance\DailyFundController::class, 'periodReport'])->name('period-report');
|
||||
Route::post('/store', [\App\Http\Controllers\Finance\DailyFundController::class, 'store'])->name('store');
|
||||
Route::put('/{id}', [\App\Http\Controllers\Finance\DailyFundController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Finance\DailyFundController::class, 'destroy'])->name('destroy');
|
||||
|
||||
Reference in New Issue
Block a user