feat:재무관리 4개 페이지 수정 (부가세/매출/미지급금)
- 부가세관리: 신고기간 1P/1C/2P/2C 형식, 세금구분(과세/영세/면세), 카드 공제분 매입 반영, 라벨 변경 - 매출관리: 작성일자/승인번호 라벨, 구분(과세/영세/면세) 추가 - 미지급금: 결제예정일/거래일자 라벨, 청구서번호 숨김, 매입세금계산서 발행여부 체크박스 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,7 @@ public function index(Request $request): JsonResponse
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('vendor_name', 'like', "%{$search}%")
|
||||
->orWhere('invoice_no', 'like', "%{$search}%");
|
||||
$q->where('vendor_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +48,7 @@ public function index(Request $request): JsonResponse
|
||||
'status' => $item->status,
|
||||
'description' => $item->description,
|
||||
'memo' => $item->memo,
|
||||
'taxInvoiceIssued' => (bool) $item->tax_invoice_issued,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'vendorName' => 'required|string|max:100',
|
||||
'invoiceNo' => 'required|string|max:50',
|
||||
'invoiceNo' => 'nullable|string|max:50',
|
||||
'amount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
@@ -97,6 +97,7 @@ public function store(Request $request): JsonResponse
|
||||
'status' => 'unpaid',
|
||||
'description' => $request->input('description'),
|
||||
'memo' => $request->input('memo'),
|
||||
'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', false),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
@@ -112,7 +113,7 @@ public function update(Request $request, int $id): JsonResponse
|
||||
|
||||
$request->validate([
|
||||
'vendorName' => 'required|string|max:100',
|
||||
'invoiceNo' => 'required|string|max:50',
|
||||
'invoiceNo' => 'nullable|string|max:50',
|
||||
'amount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
@@ -126,6 +127,7 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'status' => $request->input('status', $payable->status),
|
||||
'description' => $request->input('description'),
|
||||
'memo' => $request->input('memo'),
|
||||
'tax_invoice_issued' => $request->boolean('taxInvoiceIssued', $payable->tax_invoice_issued),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -30,8 +30,8 @@ public function index(Request $request): JsonResponse
|
||||
$records = $query->orderBy('date', 'desc')->get()->map(fn($item) => [
|
||||
'id' => $item->id, 'date' => $item->date?->format('Y-m-d'),
|
||||
'customer' => $item->customer, 'project' => $item->project,
|
||||
'type' => $item->type, 'amount' => $item->amount,
|
||||
'vat' => $item->vat, 'status' => $item->status,
|
||||
'type' => $item->type, 'taxType' => $item->tax_type ?? 'taxable',
|
||||
'amount' => $item->amount, 'vat' => $item->vat, 'status' => $item->status,
|
||||
'invoiceNo' => $item->invoice_no, 'memo' => $item->memo,
|
||||
]);
|
||||
|
||||
@@ -48,14 +48,18 @@ public function index(Request $request): JsonResponse
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['customer' => 'required|string|max:100', 'amount' => 'required|integer|min:0']);
|
||||
$request->validate([
|
||||
'customer' => 'required|string|max:100',
|
||||
'amount' => 'required|integer|min:0',
|
||||
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
|
||||
]);
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
SalesRecord::create([
|
||||
'tenant_id' => $tenantId, 'date' => $request->input('date'),
|
||||
'customer' => $request->input('customer'), 'project' => $request->input('project'),
|
||||
'type' => $request->input('type'), 'amount' => $request->input('amount', 0),
|
||||
'vat' => $request->input('vat', 0),
|
||||
'type' => $request->input('type'), 'tax_type' => $request->input('taxType', 'taxable'),
|
||||
'amount' => $request->input('amount', 0), 'vat' => $request->input('vat', 0),
|
||||
'status' => $request->input('status', 'contracted'),
|
||||
'invoice_no' => $request->input('invoiceNo'), 'memo' => $request->input('memo'),
|
||||
]);
|
||||
@@ -67,11 +71,16 @@ public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$item = SalesRecord::forTenant($tenantId)->findOrFail($id);
|
||||
$request->validate(['customer' => 'required|string|max:100', 'amount' => 'required|integer|min:0']);
|
||||
$request->validate([
|
||||
'customer' => 'required|string|max:100',
|
||||
'amount' => 'required|integer|min:0',
|
||||
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
|
||||
]);
|
||||
|
||||
$item->update([
|
||||
'date' => $request->input('date'), 'customer' => $request->input('customer'),
|
||||
'project' => $request->input('project'), 'type' => $request->input('type', $item->type),
|
||||
'tax_type' => $request->input('taxType', $item->tax_type),
|
||||
'amount' => $request->input('amount'), 'vat' => $request->input('vat', $item->vat),
|
||||
'status' => $request->input('status', $item->status),
|
||||
'invoice_no' => $request->input('invoiceNo'), 'memo' => $request->input('memo'),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\VatRecord;
|
||||
use App\Models\Finance\CardTransaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -17,8 +18,7 @@ public function index(Request $request): JsonResponse
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('partner_name', 'like', "%{$search}%")
|
||||
->orWhere('invoice_no', 'like', "%{$search}%");
|
||||
$q->where('partner_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ public function index(Request $request): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
if ($taxType = $request->input('tax_type')) {
|
||||
if ($taxType !== 'all') {
|
||||
$query->where('tax_type', $taxType);
|
||||
}
|
||||
}
|
||||
|
||||
if ($status = $request->input('status')) {
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
@@ -45,6 +51,7 @@ public function index(Request $request): JsonResponse
|
||||
'id' => $record->id,
|
||||
'period' => $record->period,
|
||||
'type' => $record->type,
|
||||
'taxType' => $record->tax_type ?? 'taxable',
|
||||
'partnerName' => $record->partner_name,
|
||||
'invoiceNo' => $record->invoice_no,
|
||||
'invoiceDate' => $record->invoice_date?->format('Y-m-d'),
|
||||
@@ -53,9 +60,53 @@ public function index(Request $request): JsonResponse
|
||||
'totalAmount' => $record->total_amount,
|
||||
'status' => $record->status,
|
||||
'memo' => $record->memo,
|
||||
'isCardTransaction' => false,
|
||||
];
|
||||
});
|
||||
|
||||
// 카드 공제분 매입 반영
|
||||
$cardRecords = collect();
|
||||
$cardPurchaseSupply = 0;
|
||||
$cardPurchaseVat = 0;
|
||||
|
||||
if ($period = $request->input('period')) {
|
||||
[$startDate, $endDate] = $this->periodToDateRange($period);
|
||||
|
||||
if ($startDate && $endDate) {
|
||||
$cardQuery = CardTransaction::forTenant($tenantId)
|
||||
->where('deduction_type', 'deductible')
|
||||
->whereBetween('transaction_date', [$startDate, $endDate]);
|
||||
|
||||
$cardTransactions = $cardQuery->orderBy('transaction_date', 'desc')->get();
|
||||
|
||||
$cardRecords = $cardTransactions->map(function ($card) use ($period) {
|
||||
$supply = (int) round($card->amount / 1.1);
|
||||
$vat = $card->amount - $supply;
|
||||
return [
|
||||
'id' => 'card_' . $card->id,
|
||||
'period' => $period,
|
||||
'type' => 'purchase',
|
||||
'taxType' => 'taxable',
|
||||
'partnerName' => $card->merchant,
|
||||
'invoiceNo' => $card->approval_no ?? '',
|
||||
'invoiceDate' => $card->transaction_date?->format('Y-m-d'),
|
||||
'supplyAmount' => $supply,
|
||||
'vatAmount' => $vat,
|
||||
'totalAmount' => $card->amount,
|
||||
'status' => 'filed',
|
||||
'memo' => $card->memo,
|
||||
'isCardTransaction' => true,
|
||||
];
|
||||
});
|
||||
|
||||
$cardPurchaseSupply = $cardRecords->sum('supplyAmount');
|
||||
$cardPurchaseVat = $cardRecords->sum('vatAmount');
|
||||
}
|
||||
}
|
||||
|
||||
// 통합 records
|
||||
$allRecords = $records->concat($cardRecords)->values();
|
||||
|
||||
// 해당 기간 통계
|
||||
$periodQuery = VatRecord::forTenant($tenantId);
|
||||
if ($period = $request->input('period')) {
|
||||
@@ -66,8 +117,10 @@ public function index(Request $request): JsonResponse
|
||||
$stats = [
|
||||
'salesSupply' => $periodRecords->where('type', 'sales')->sum('supply_amount'),
|
||||
'salesVat' => $periodRecords->where('type', 'sales')->sum('vat_amount'),
|
||||
'purchaseSupply' => $periodRecords->where('type', 'purchase')->sum('supply_amount'),
|
||||
'purchaseVat' => $periodRecords->where('type', 'purchase')->sum('vat_amount'),
|
||||
'purchaseSupply' => $periodRecords->where('type', 'purchase')->sum('supply_amount') + $cardPurchaseSupply,
|
||||
'purchaseVat' => $periodRecords->where('type', 'purchase')->sum('vat_amount') + $cardPurchaseVat,
|
||||
'cardPurchaseSupply' => $cardPurchaseSupply,
|
||||
'cardPurchaseVat' => $cardPurchaseVat,
|
||||
'total' => $periodRecords->count(),
|
||||
];
|
||||
|
||||
@@ -81,7 +134,7 @@ public function index(Request $request): JsonResponse
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $records,
|
||||
'data' => $allRecords,
|
||||
'stats' => $stats,
|
||||
'periods' => $periods,
|
||||
]);
|
||||
@@ -91,9 +144,9 @@ public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'partnerName' => 'required|string|max:100',
|
||||
'invoiceNo' => 'required|string|max:50',
|
||||
'period' => 'required|string|max:20',
|
||||
'type' => 'required|in:sales,purchase',
|
||||
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
|
||||
'supplyAmount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
@@ -103,6 +156,7 @@ public function store(Request $request): JsonResponse
|
||||
'tenant_id' => $tenantId,
|
||||
'period' => $request->input('period'),
|
||||
'type' => $request->input('type', 'sales'),
|
||||
'tax_type' => $request->input('taxType', 'taxable'),
|
||||
'partner_name' => $request->input('partnerName'),
|
||||
'invoice_no' => $request->input('invoiceNo'),
|
||||
'invoice_date' => $request->input('invoiceDate'),
|
||||
@@ -127,15 +181,16 @@ public function update(Request $request, int $id): JsonResponse
|
||||
|
||||
$request->validate([
|
||||
'partnerName' => 'required|string|max:100',
|
||||
'invoiceNo' => 'required|string|max:50',
|
||||
'period' => 'required|string|max:20',
|
||||
'type' => 'required|in:sales,purchase',
|
||||
'taxType' => 'nullable|in:taxable,zero_rated,exempt',
|
||||
'supplyAmount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$record->update([
|
||||
'period' => $request->input('period'),
|
||||
'type' => $request->input('type'),
|
||||
'tax_type' => $request->input('taxType', $record->tax_type),
|
||||
'partner_name' => $request->input('partnerName'),
|
||||
'invoice_no' => $request->input('invoiceNo'),
|
||||
'invoice_date' => $request->input('invoiceDate'),
|
||||
@@ -163,4 +218,31 @@ public function destroy(int $id): JsonResponse
|
||||
'message' => '세금계산서가 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 부가세 신고기간을 날짜 범위로 변환
|
||||
* YYYY-1P: 1기 예정 (0101~0331)
|
||||
* YYYY-1C: 1기 확정 (0401~0630)
|
||||
* YYYY-2P: 2기 예정 (0701~0930)
|
||||
* YYYY-2C: 2기 확정 (1001~1231)
|
||||
*/
|
||||
private function periodToDateRange(string $period): array
|
||||
{
|
||||
$parts = explode('-', $period);
|
||||
if (count($parts) !== 2) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$year = $parts[0];
|
||||
$code = $parts[1];
|
||||
|
||||
$ranges = [
|
||||
'1P' => ["{$year}-01-01", "{$year}-03-31"],
|
||||
'1C' => ["{$year}-04-01", "{$year}-06-30"],
|
||||
'2P' => ["{$year}-07-01", "{$year}-09-30"],
|
||||
'2C' => ["{$year}-10-01", "{$year}-12-31"],
|
||||
];
|
||||
|
||||
return $ranges[$code] ?? [null, null];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class Payable extends Model
|
||||
'status',
|
||||
'description',
|
||||
'memo',
|
||||
'tax_invoice_issued',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -30,6 +31,7 @@ class Payable extends Model
|
||||
'paid_amount' => 'integer',
|
||||
'issue_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
'tax_invoice_issued' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeForTenant($query, $tenantId)
|
||||
|
||||
@@ -11,7 +11,7 @@ class SalesRecord extends Model
|
||||
protected $table = 'sales_records';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'date', 'customer', 'project', 'type',
|
||||
'tenant_id', 'date', 'customer', 'project', 'type', 'tax_type',
|
||||
'amount', 'vat', 'status', 'invoice_no', 'memo',
|
||||
];
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class VatRecord extends Model
|
||||
'tenant_id',
|
||||
'period',
|
||||
'type',
|
||||
'tax_type',
|
||||
'partner_name',
|
||||
'invoice_no',
|
||||
'invoice_date',
|
||||
|
||||
@@ -81,7 +81,8 @@ function PayablesManagement() {
|
||||
status: 'unpaid',
|
||||
category: '사무용품',
|
||||
description: '',
|
||||
memo: ''
|
||||
memo: '',
|
||||
taxInvoiceIssued: false
|
||||
};
|
||||
const [formData, setFormData] = useState(initialFormState);
|
||||
|
||||
@@ -120,8 +121,7 @@ function PayablesManagement() {
|
||||
useEffect(() => { fetchPayables(); }, []);
|
||||
|
||||
const filteredPayables = payables.filter(item => {
|
||||
const matchesSearch = (item.vendorName || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(item.invoiceNo || '').toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesSearch = (item.vendorName || '').toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = filterStatus === 'all' || item.status === filterStatus;
|
||||
const matchesCategory = filterCategory === 'all' || item.category === filterCategory;
|
||||
return matchesSearch && matchesStatus && matchesCategory;
|
||||
@@ -132,12 +132,12 @@ function PayablesManagement() {
|
||||
setModalMode('edit');
|
||||
setEditingItem(item);
|
||||
const safeItem = {};
|
||||
Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? ''; });
|
||||
Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? (key === 'taxInvoiceIssued' ? false : ''); });
|
||||
setFormData(safeItem);
|
||||
setShowModal(true);
|
||||
};
|
||||
const handleSave = async () => {
|
||||
if (!formData.vendorName || !formData.invoiceNo || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
if (!formData.vendorName || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const url = modalMode === 'add' ? '/finance/payables/store' : `/finance/payables/${editingItem.id}`;
|
||||
@@ -214,8 +214,8 @@ function PayablesManagement() {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const rows = [['미지급금 관리'], [], ['거래처', '청구서번호', '발행일', '만기일', '분류', '청구금액', '지급액', '잔액', '상태'],
|
||||
...filteredPayables.map(item => [item.vendorName, item.invoiceNo, item.issueDate, item.dueDate, item.category, item.amount, item.paidAmount, item.amount - item.paidAmount, getStatusLabel(item.status)])];
|
||||
const rows = [['미지급금 관리'], [], ['거래처', '거래일자', '결제예정일', '분류', '청구금액', '지급액', '잔액', '상태', '세금계산서'],
|
||||
...filteredPayables.map(item => [item.vendorName, item.issueDate, item.dueDate, item.category, item.amount, item.paidAmount, item.amount - item.paidAmount, getStatusLabel(item.status), item.taxInvoiceIssued ? 'O' : 'X'])];
|
||||
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 = `미지급금관리_${new Date().toISOString().split('T')[0]}.csv`; link.click();
|
||||
@@ -275,7 +275,7 @@ function PayablesManagement() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="md:col-span-2 relative">
|
||||
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input type="text" placeholder="거래처, 청구서번호 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-rose-500" />
|
||||
<input type="text" placeholder="거래처 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-rose-500" />
|
||||
</div>
|
||||
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
|
||||
<option value="all">전체 분류</option>
|
||||
@@ -299,12 +299,12 @@ function PayablesManagement() {
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">거래처</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">청구서번호</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">만기일</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">결제예정일</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">분류</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">청구금액</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">잔액</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">상태</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">세금계산서</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">연체일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -316,12 +316,12 @@ function PayablesManagement() {
|
||||
) : filteredPayables.map(item => (
|
||||
<tr key={item.id} onClick={() => handleEdit(item)} className={`hover:bg-gray-50 cursor-pointer ${item.status === 'overdue' ? 'bg-red-50/50' : ''}`}>
|
||||
<td className="px-6 py-4"><p className="text-sm font-medium text-gray-900">{item.vendorName}</p><p className="text-xs text-gray-400">{item.description}</p></td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{item.invoiceNo}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{item.dueDate}</td>
|
||||
<td className="px-6 py-4"><span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">{item.category}</span></td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-right text-gray-900">{formatCurrency(item.amount)}원</td>
|
||||
<td className="px-6 py-4 text-sm font-bold text-right text-rose-600">{formatCurrency(item.amount - item.paidAmount)}원</td>
|
||||
<td className="px-6 py-4 text-center"><span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusStyle(item.status)}`}>{getStatusLabel(item.status)}</span></td>
|
||||
<td className="px-6 py-4 text-center">{item.taxInvoiceIssued ? <span className="text-emerald-600 font-bold text-sm">O</span> : <span className="text-gray-300 text-sm">-</span>}</td>
|
||||
<td className="px-6 py-4 text-center text-sm">{item.status === 'overdue' ? <span className="text-red-600 font-medium">{calculateOverdueDays(item.dueDate)}일</span> : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -337,13 +337,13 @@ function PayablesManagement() {
|
||||
<button onClick={() => setShowModal(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="text" value={formData.vendorName} onChange={(e) => setFormData(prev => ({ ...prev, vendorName: e.target.value }))} placeholder="거래처명" 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={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="PO-2026-001" 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={formData.vendorName} onChange={(e) => setFormData(prev => ({ ...prev, vendorName: 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="date" value={formData.issueDate} onChange={(e) => setFormData(prev => ({ ...prev, issueDate: 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="date" value={formData.dueDate} onChange={(e) => setFormData(prev => ({ ...prev, dueDate: 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="date" value={formData.issueDate} onChange={(e) => setFormData(prev => ({ ...prev, issueDate: 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="date" value={formData.dueDate} onChange={(e) => setFormData(prev => ({ ...prev, dueDate: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">분류</label><select value={formData.category} onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}</select></div>
|
||||
@@ -351,6 +351,10 @@ function PayablesManagement() {
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">적요</label><input type="text" value={formData.description} onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} placeholder="적요 입력" 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><textarea value={formData.memo} onChange={(e) => setFormData(prev => ({ ...prev, memo: e.target.value }))} placeholder="부분지급, 연체 사유 등 메모" rows="2" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<input type="checkbox" id="taxInvoiceIssued" checked={formData.taxInvoiceIssued} onChange={(e) => setFormData(prev => ({ ...prev, taxInvoiceIssued: e.target.checked }))} className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
|
||||
<label htmlFor="taxInvoiceIssued" className="text-sm font-medium text-gray-700 cursor-pointer">매입세금계산서 발행여부</label>
|
||||
</div>
|
||||
{modalMode === 'edit' && (
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">상태</label><select value={formData.status} onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="unpaid">미지급</option><option value="partial">부분지급</option><option value="paid">지급완료</option><option value="overdue">연체</option></select></div>
|
||||
)}
|
||||
|
||||
@@ -74,11 +74,26 @@ function SalesManagement() {
|
||||
{ value: 'completed', label: '매출확정', color: 'bg-gray-100 text-gray-700' }
|
||||
];
|
||||
|
||||
const getTaxTypeLabel = (taxType) => {
|
||||
const labels = { 'taxable': '과세', 'zero_rated': '영세', 'exempt': '면세' };
|
||||
return labels[taxType] || taxType;
|
||||
};
|
||||
|
||||
const getTaxTypeStyle = (taxType) => {
|
||||
const styles = {
|
||||
'taxable': 'bg-blue-100 text-blue-700',
|
||||
'zero_rated': 'bg-teal-100 text-teal-700',
|
||||
'exempt': 'bg-gray-100 text-gray-600'
|
||||
};
|
||||
return styles[taxType] || 'bg-gray-100 text-gray-700';
|
||||
};
|
||||
|
||||
const initialFormState = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
customer: '',
|
||||
project: '',
|
||||
type: '프로젝트',
|
||||
taxType: 'taxable',
|
||||
amount: '',
|
||||
vat: '',
|
||||
status: 'negotiating',
|
||||
@@ -178,8 +193,8 @@ function SalesManagement() {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const rows = [['매출관리', `${dateRange.start} ~ ${dateRange.end}`], [], ['날짜', '거래처', '프로젝트', '유형', '공급가액', 'VAT', '합계', '상태'],
|
||||
...filteredSales.map(item => [item.date, item.customer, item.project, item.type, item.amount, item.vat, item.amount + item.vat, statuses.find(s => s.value === item.status)?.label])];
|
||||
const rows = [['매출관리', `${dateRange.start} ~ ${dateRange.end}`], [], ['작성일자', '거래처', '프로젝트', '유형', '구분', '공급가액', 'VAT', '합계', '상태'],
|
||||
...filteredSales.map(item => [item.date, item.customer, item.project, item.type, getTaxTypeLabel(item.taxType), item.amount, item.vat, item.amount + item.vat, statuses.find(s => s.value === item.status)?.label])];
|
||||
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 = `매출관리_${dateRange.start}_${dateRange.end}.csv`; link.click();
|
||||
@@ -243,10 +258,11 @@ function SalesManagement() {
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">날짜</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">작성일자</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">거래처</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">프로젝트</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">유형</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">구분</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">공급가액</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">합계(VAT포함)</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-semibold text-gray-600">상태</th>
|
||||
@@ -255,20 +271,21 @@ function SalesManagement() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{loading ? (
|
||||
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">
|
||||
<tr><td colSpan="9" className="px-6 py-12 text-center text-gray-400">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<svg className="animate-spin h-5 w-5 text-gray-400" 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>
|
||||
데이터를 불러오는 중...
|
||||
</div>
|
||||
</td></tr>
|
||||
) : filteredSales.length === 0 ? (
|
||||
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
|
||||
<tr><td colSpan="9" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : filteredSales.map(item => (
|
||||
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => handleEdit(item)}>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{item.date}</td>
|
||||
<td className="px-6 py-4"><p className="text-sm font-medium text-gray-900">{item.customer}</p></td>
|
||||
<td className="px-6 py-4"><p className="text-sm text-gray-600">{item.project}</p>{item.memo && <p className="text-xs text-gray-400">{item.memo}</p>}</td>
|
||||
<td className="px-6 py-4"><span className="px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-700">{item.type}</span></td>
|
||||
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTaxTypeStyle(item.taxType)}`}>{getTaxTypeLabel(item.taxType)}</span></td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-right text-gray-900">{formatCurrency(item.amount)}원</td>
|
||||
<td className="px-6 py-4 text-sm font-bold text-right text-blue-600">{formatCurrency(item.amount + item.vat)}원</td>
|
||||
<td className="px-6 py-4 text-center"><span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(item.status)}`}>{getStatusLabel(item.status)}</span></td>
|
||||
@@ -291,7 +308,7 @@ function SalesManagement() {
|
||||
</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="date" value={formData.date} onChange={(e) => setFormData(prev => ({ ...prev, date: 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="date" value={formData.date} onChange={(e) => setFormData(prev => ({ ...prev, date: 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><select value={formData.type} onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{types.map(t => <option key={t} value={t}>{t}</option>)}</select></div>
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">거래처 *</label><input type="text" value={formData.customer} onChange={(e) => setFormData(prev => ({ ...prev, customer: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
@@ -300,9 +317,10 @@ function SalesManagement() {
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">공급가액 *</label><input type="text" value={formatInputCurrency(formData.amount)} onChange={(e) => setFormData(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">VAT</label><input type="text" value={formatInputCurrency(formData.vat)} onChange={(e) => setFormData(prev => ({ ...prev, vat: parseInputCurrency(e.target.value) }))} placeholder="자동계산" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">구분</label><select value={formData.taxType} onChange={(e) => setFormData(prev => ({ ...prev, taxType: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="taxable">과세</option><option value="zero_rated">영세</option><option value="exempt">면세</option></select></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">상태</label><select value={formData.status} onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{statuses.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}</select></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">세금계산서 번호</label><input type="text" value={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="SAL-2026-001" 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={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: 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={formData.memo} onChange={(e) => setFormData(prev => ({ ...prev, memo: e.target.value }))} placeholder="메모" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</div>
|
||||
|
||||
@@ -49,16 +49,18 @@
|
||||
|
||||
function VatManagement() {
|
||||
const [vatRecords, setVatRecords] = useState([]);
|
||||
const [stats, setStats] = useState({ salesSupply: 0, salesVat: 0, purchaseSupply: 0, purchaseVat: 0, total: 0 });
|
||||
const [stats, setStats] = useState({ salesSupply: 0, salesVat: 0, purchaseSupply: 0, purchaseVat: 0, cardPurchaseSupply: 0, cardPurchaseVat: 0, total: 0 });
|
||||
const [periods, setPeriods] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const now = new Date();
|
||||
const currentHalf = now.getMonth() < 6 ? '1H' : '2H';
|
||||
const [filterPeriod, setFilterPeriod] = useState(`${now.getFullYear()}-${currentHalf}`);
|
||||
const month = now.getMonth();
|
||||
const currentPeriodCode = month < 3 ? '1P' : month < 6 ? '1C' : month < 9 ? '2P' : '2C';
|
||||
const [filterPeriod, setFilterPeriod] = useState(`${now.getFullYear()}-${currentPeriodCode}`);
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [filterTaxType, setFilterTaxType] = useState('all');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -67,12 +69,12 @@ function VatManagement() {
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
// 기본 기간 옵션 (DB에서 가져온 기간 + 현재/이전 반기)
|
||||
// 기본 기간 옵션 (1P/1C/2P/2C 형식)
|
||||
const defaultPeriods = (() => {
|
||||
const y = now.getFullYear();
|
||||
const list = [];
|
||||
for (let i = 0; i <= 2; i++) {
|
||||
list.push(`${y - i}-1H`, `${y - i}-2H`);
|
||||
list.push(`${y - i}-1P`, `${y - i}-1C`, `${y - i}-2P`, `${y - i}-2C`);
|
||||
}
|
||||
return list;
|
||||
})();
|
||||
@@ -81,6 +83,7 @@ function VatManagement() {
|
||||
const initialFormState = {
|
||||
period: filterPeriod,
|
||||
type: 'sales',
|
||||
taxType: 'taxable',
|
||||
partnerName: '',
|
||||
invoiceNo: '',
|
||||
invoiceDate: now.toISOString().split('T')[0],
|
||||
@@ -102,7 +105,8 @@ function VatManagement() {
|
||||
|
||||
const handleSupplyAmountChange = (value) => {
|
||||
const supply = parseInt(parseInputCurrency(value)) || 0;
|
||||
const vat = Math.floor(supply * 0.1);
|
||||
const taxType = formData.taxType;
|
||||
const vat = taxType === 'taxable' ? Math.floor(supply * 0.1) : 0;
|
||||
const total = supply + vat;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
@@ -112,6 +116,18 @@ function VatManagement() {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTaxTypeChange = (newTaxType) => {
|
||||
const supply = parseInt(parseInputCurrency(formData.supplyAmount)) || 0;
|
||||
const vat = newTaxType === 'taxable' ? Math.floor(supply * 0.1) : 0;
|
||||
const total = supply + vat;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
taxType: newTaxType,
|
||||
vatAmount: vat.toString(),
|
||||
totalAmount: total.toString()
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchRecords = async (period) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -135,21 +151,27 @@ function VatManagement() {
|
||||
useEffect(() => { fetchRecords(filterPeriod); }, [filterPeriod]);
|
||||
|
||||
const filteredRecords = vatRecords.filter(item => {
|
||||
const matchesSearch = item.partnerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.invoiceNo.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesType = filterType === 'all' || item.type === filterType;
|
||||
const matchesSearch = item.partnerName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesType = filterType === 'all' || item.type === filterType || (filterType === 'purchase_card' && item.isCardTransaction);
|
||||
const matchesTaxType = filterTaxType === 'all' || item.taxType === filterTaxType;
|
||||
const matchesStatus = filterStatus === 'all' || item.status === filterStatus;
|
||||
return matchesSearch && matchesType && matchesStatus;
|
||||
if (filterType === 'purchase_card') return item.isCardTransaction && matchesSearch && matchesTaxType && matchesStatus;
|
||||
if (filterType === 'purchase' && !item.isCardTransaction) return item.type === 'purchase' && matchesSearch && matchesTaxType && matchesStatus;
|
||||
if (filterType !== 'all' && filterType !== 'purchase_card') return item.type === filterType && !item.isCardTransaction && matchesSearch && matchesTaxType && matchesStatus;
|
||||
return matchesSearch && matchesTaxType && matchesStatus;
|
||||
});
|
||||
|
||||
const salesVat = stats.salesVat || 0;
|
||||
const purchaseVat = stats.purchaseVat || 0;
|
||||
const salesSupply = stats.salesSupply || 0;
|
||||
const purchaseSupply = stats.purchaseSupply || 0;
|
||||
const cardPurchaseSupply = stats.cardPurchaseSupply || 0;
|
||||
const cardPurchaseVat = stats.cardPurchaseVat || 0;
|
||||
const netVat = salesVat - purchaseVat;
|
||||
|
||||
const handleAdd = () => { setModalMode('add'); setFormData({ ...initialFormState, period: filterPeriod }); setShowModal(true); };
|
||||
const handleEdit = (item) => {
|
||||
if (item.isCardTransaction) return;
|
||||
setModalMode('edit');
|
||||
setEditingItem(item);
|
||||
setFormData({
|
||||
@@ -161,7 +183,7 @@ function VatManagement() {
|
||||
setShowModal(true);
|
||||
};
|
||||
const handleSave = async () => {
|
||||
if (!formData.partnerName || !formData.invoiceNo || !formData.supplyAmount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
if (!formData.partnerName || !formData.supplyAmount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
@@ -210,18 +232,33 @@ function VatManagement() {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const rows = [['부가세 관리', getPeriodLabel(filterPeriod)], [], ['구분', '거래처', '세금계산서번호', '발행일', '공급가액', '부가세', '합계', '상태'],
|
||||
...filteredRecords.map(item => [getTypeLabel(item.type), item.partnerName, item.invoiceNo, item.invoiceDate, item.supplyAmount, item.vatAmount, item.totalAmount, getStatusLabel(item.status)])];
|
||||
const rows = [['부가세 관리', getPeriodLabel(filterPeriod)], [], ['구분', '세금구분', '거래처', '작성일자', '공급가액', '부가세', '합계', '상태'],
|
||||
...filteredRecords.map(item => [getTypeLabel(item.type, item.isCardTransaction), getTaxTypeLabel(item.taxType), item.partnerName, item.invoiceDate, item.supplyAmount, item.vatAmount, item.totalAmount, getStatusLabel(item.status)])];
|
||||
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 = `부가세관리_${filterPeriod}.csv`; link.click();
|
||||
};
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const getTypeLabel = (type, isCard = false) => {
|
||||
if (isCard) return '매입(카드)';
|
||||
const labels = { 'sales': '매출', 'purchase': '매입' };
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
const getTaxTypeLabel = (taxType) => {
|
||||
const labels = { 'taxable': '과세', 'zero_rated': '영세', 'exempt': '면세' };
|
||||
return labels[taxType] || taxType;
|
||||
};
|
||||
|
||||
const getTaxTypeStyle = (taxType) => {
|
||||
const styles = {
|
||||
'taxable': 'bg-blue-100 text-blue-700',
|
||||
'zero_rated': 'bg-teal-100 text-teal-700',
|
||||
'exempt': 'bg-gray-100 text-gray-600'
|
||||
};
|
||||
return styles[taxType] || 'bg-gray-100 text-gray-700';
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = { 'pending': '미신고', 'filed': '신고완료', 'paid': '납부완료' };
|
||||
return labels[status] || status;
|
||||
@@ -236,7 +273,8 @@ function VatManagement() {
|
||||
return styles[status] || 'bg-gray-100 text-gray-700';
|
||||
};
|
||||
|
||||
const getTypeStyle = (type) => {
|
||||
const getTypeStyle = (type, isCard = false) => {
|
||||
if (isCard) return 'bg-purple-100 text-purple-700';
|
||||
const styles = {
|
||||
'sales': 'bg-emerald-100 text-emerald-700',
|
||||
'purchase': 'bg-pink-100 text-pink-700'
|
||||
@@ -245,8 +283,9 @@ function VatManagement() {
|
||||
};
|
||||
|
||||
const getPeriodLabel = (period) => {
|
||||
const [year, half] = period.split('-');
|
||||
return `${year}년 ${half === '1H' ? '1기' : '2기'}`;
|
||||
const [year, code] = period.split('-');
|
||||
const labels = { '1P': '1기 예정', '1C': '1기 확정', '2P': '2기 예정', '2C': '2기 확정' };
|
||||
return `${year}년 ${labels[code] || code}`;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -318,8 +357,13 @@ function VatManagement() {
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100">
|
||||
<td className="px-6 py-3 text-sm">매입</td>
|
||||
<td className="px-6 py-3 text-sm text-right">{formatCurrency(purchaseSupply)}원</td>
|
||||
<td className="px-6 py-3 text-sm text-right text-pink-600 font-medium">({formatCurrency(purchaseVat)}원)</td>
|
||||
<td className="px-6 py-3 text-sm text-right">{formatCurrency(purchaseSupply - cardPurchaseSupply)}원</td>
|
||||
<td className="px-6 py-3 text-sm text-right text-pink-600 font-medium">({formatCurrency(purchaseVat - cardPurchaseVat)}원)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100 bg-purple-50/50">
|
||||
<td className="px-6 py-3 text-sm text-purple-700">매입(카드)</td>
|
||||
<td className="px-6 py-3 text-sm text-right text-purple-600">{formatCurrency(cardPurchaseSupply)}원</td>
|
||||
<td className="px-6 py-3 text-sm text-right text-purple-600 font-medium">({formatCurrency(cardPurchaseVat)}원)</td>
|
||||
</tr>
|
||||
<tr className="bg-gray-50 font-bold">
|
||||
<td className="px-6 py-3 text-sm">{netVat >= 0 ? '납부세액' : '환급세액'}</td>
|
||||
@@ -331,15 +375,22 @@ function VatManagement() {
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<div className="md:col-span-2 relative">
|
||||
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input type="text" placeholder="거래처, 세금계산서번호 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" />
|
||||
<input type="text" placeholder="거래처 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
|
||||
<option value="all">전체 유형</option>
|
||||
<option value="sales">매출</option>
|
||||
<option value="purchase">매입</option>
|
||||
<option value="purchase_card">매입(카드)</option>
|
||||
</select>
|
||||
<select value={filterTaxType} onChange={(e) => setFilterTaxType(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
|
||||
<option value="all">전체 세금구분</option>
|
||||
<option value="taxable">과세</option>
|
||||
<option value="zero_rated">영세</option>
|
||||
<option value="exempt">면세</option>
|
||||
</select>
|
||||
<div className="flex gap-1">
|
||||
{['all', 'pending', 'filed', 'paid'].map(status => (
|
||||
@@ -348,7 +399,7 @@ function VatManagement() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={() => { setSearchTerm(''); setFilterType('all'); setFilterStatus('all'); }} className="flex items-center justify-center gap-2 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||
<button onClick={() => { setSearchTerm(''); setFilterType('all'); setFilterTaxType('all'); setFilterStatus('all'); }} className="flex items-center justify-center gap-2 px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg">
|
||||
<RefreshCw className="w-4 h-4" /><span className="text-sm">초기화</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -359,9 +410,9 @@ function VatManagement() {
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">구분</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">세금구분</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">거래처</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">세금계산서번호</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">발행일</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-gray-600">작성일자</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">공급가액</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">부가세</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-gray-600">합계</th>
|
||||
@@ -374,10 +425,10 @@ function VatManagement() {
|
||||
) : filteredRecords.length === 0 ? (
|
||||
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : filteredRecords.map(item => (
|
||||
<tr key={item.id} onClick={() => handleEdit(item)} className="hover:bg-gray-50 cursor-pointer">
|
||||
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTypeStyle(item.type)}`}>{getTypeLabel(item.type)}</span></td>
|
||||
<tr key={item.id} onClick={() => handleEdit(item)} className={`hover:bg-gray-50 ${item.isCardTransaction ? 'bg-purple-50/40 cursor-default' : 'cursor-pointer'}`}>
|
||||
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTypeStyle(item.type, item.isCardTransaction)}`}>{getTypeLabel(item.type, item.isCardTransaction)}</span></td>
|
||||
<td className="px-6 py-4"><span className={`px-2 py-1 rounded text-xs font-medium ${getTaxTypeStyle(item.taxType)}`}>{getTaxTypeLabel(item.taxType)}</span></td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">{item.partnerName}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{item.invoiceNo}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{item.invoiceDate}</td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-right text-gray-900">{formatCurrency(item.supplyAmount)}원</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-gray-600">{formatCurrency(item.vatAmount)}원</td>
|
||||
@@ -397,16 +448,17 @@ function VatManagement() {
|
||||
<button onClick={() => setShowModal(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 className="grid grid-cols-3 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">신고기간 *</label><select value={formData.period} onChange={(e) => setFormData(prev => ({ ...prev, period: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg">{allPeriods.map(p => <option key={p} value={p}>{getPeriodLabel(p)}</option>)}</select></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">구분 *</label><select value={formData.type} onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value }))} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="sales">매출</option><option value="purchase">매입</option></select></div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">세금구분</label><select value={formData.taxType} onChange={(e) => handleTaxTypeChange(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg"><option value="taxable">과세</option><option value="zero_rated">영세</option><option value="exempt">면세</option></select></div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">거래처 *</label>
|
||||
<input type="text" value={formData.partnerName} onChange={(e) => setFormData(prev => ({ ...prev, partnerName: 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={formData.partnerName} onChange={(e) => setFormData(prev => ({ ...prev, partnerName: e.target.value }))} placeholder="거래처명" 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={formData.invoiceNo} onChange={(e) => setFormData(prev => ({ ...prev, invoiceNo: e.target.value }))} placeholder="TAX-2026-0001" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
</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="date" value={formData.invoiceDate} onChange={(e) => setFormData(prev => ({ ...prev, invoiceDate: 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="date" value={formData.invoiceDate} onChange={(e) => setFormData(prev => ({ ...prev, invoiceDate: 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={formatInputCurrency(formData.supplyAmount)} onChange={(e) => handleSupplyAmountChange(e.target.value)} placeholder="0" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-right" /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
Reference in New Issue
Block a user