feat:미지급금 관리 목업 데이터를 실제 DB CRUD로 전환
- 채무관리 메뉴명 → 미지급금 관리로 변경 시더 추가 - Payable 모델/컨트롤러 생성 - 지급 처리 API 추가 - React 프론트엔드 API 호출 전환 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
181
app/Http/Controllers/Finance/PayableController.php
Normal file
181
app/Http/Controllers/Finance/PayableController.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\Payable;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PayableController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$query = Payable::forTenant($tenantId);
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('vendor_name', 'like', "%{$search}%")
|
||||
->orWhere('invoice_no', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($status = $request->input('status')) {
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
}
|
||||
|
||||
if ($category = $request->input('category')) {
|
||||
if ($category !== 'all') {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
}
|
||||
|
||||
$payables = $query->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'vendorName' => $item->vendor_name,
|
||||
'invoiceNo' => $item->invoice_no,
|
||||
'issueDate' => $item->issue_date?->format('Y-m-d'),
|
||||
'dueDate' => $item->due_date?->format('Y-m-d'),
|
||||
'category' => $item->category,
|
||||
'amount' => $item->amount,
|
||||
'paidAmount' => $item->paid_amount,
|
||||
'status' => $item->status,
|
||||
'description' => $item->description,
|
||||
'memo' => $item->memo,
|
||||
];
|
||||
});
|
||||
|
||||
$all = Payable::forTenant($tenantId)->get();
|
||||
|
||||
$totalAmount = $all->sum('amount');
|
||||
$totalPaid = $all->sum('paid_amount');
|
||||
$overdueAmount = $all->where('status', 'overdue')->sum(function ($item) {
|
||||
return $item->amount - $item->paid_amount;
|
||||
});
|
||||
|
||||
$stats = [
|
||||
'totalAmount' => $totalAmount,
|
||||
'totalPaid' => $totalPaid,
|
||||
'totalUnpaid' => $totalAmount - $totalPaid,
|
||||
'overdueAmount' => $overdueAmount,
|
||||
'count' => $all->count(),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $payables,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'vendorName' => 'required|string|max:100',
|
||||
'invoiceNo' => 'required|string|max:50',
|
||||
'amount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
Payable::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'vendor_name' => $request->input('vendorName'),
|
||||
'invoice_no' => $request->input('invoiceNo'),
|
||||
'issue_date' => $request->input('issueDate'),
|
||||
'due_date' => $request->input('dueDate'),
|
||||
'category' => $request->input('category', '사무용품'),
|
||||
'amount' => $request->input('amount', 0),
|
||||
'paid_amount' => 0,
|
||||
'status' => 'unpaid',
|
||||
'description' => $request->input('description'),
|
||||
'memo' => $request->input('memo'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '미지급금이 등록되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$payable = Payable::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'vendorName' => 'required|string|max:100',
|
||||
'invoiceNo' => 'required|string|max:50',
|
||||
'amount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$payable->update([
|
||||
'vendor_name' => $request->input('vendorName'),
|
||||
'invoice_no' => $request->input('invoiceNo'),
|
||||
'issue_date' => $request->input('issueDate'),
|
||||
'due_date' => $request->input('dueDate'),
|
||||
'category' => $request->input('category'),
|
||||
'amount' => $request->input('amount'),
|
||||
'status' => $request->input('status', $payable->status),
|
||||
'description' => $request->input('description'),
|
||||
'memo' => $request->input('memo'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '미지급금이 수정되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function pay(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$payable = Payable::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'payAmount' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$payAmount = $request->input('payAmount');
|
||||
$remaining = $payable->amount - $payable->paid_amount;
|
||||
|
||||
if ($payAmount > $remaining) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '지급액이 잔액을 초과합니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$newPaid = $payable->paid_amount + $payAmount;
|
||||
$newStatus = $newPaid >= $payable->amount ? 'paid' : 'partial';
|
||||
|
||||
$payable->update([
|
||||
'paid_amount' => $newPaid,
|
||||
'status' => $newStatus,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '지급 처리되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$payable = Payable::forTenant($tenantId)->findOrFail($id);
|
||||
$payable->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '미지급금이 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
39
app/Models/Finance/Payable.php
Normal file
39
app/Models/Finance/Payable.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Payable extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'payables';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'vendor_name',
|
||||
'invoice_no',
|
||||
'issue_date',
|
||||
'due_date',
|
||||
'category',
|
||||
'amount',
|
||||
'paid_amount',
|
||||
'status',
|
||||
'description',
|
||||
'memo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'integer',
|
||||
'paid_amount' => 'integer',
|
||||
'issue_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
];
|
||||
|
||||
public function scopeForTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
35
database/seeders/PayableMenuRenameSeeder.php
Normal file
35
database/seeders/PayableMenuRenameSeeder.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Commons\Menu;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PayableMenuRenameSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = 1;
|
||||
|
||||
$menu = Menu::where('tenant_id', $tenantId)
|
||||
->where(function ($q) {
|
||||
$q->where('name', '채무관리')
|
||||
->orWhere('name', '채무 관리');
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($menu) {
|
||||
$oldName = $menu->name;
|
||||
$menu->name = '미지급금 관리';
|
||||
$menu->save();
|
||||
$this->command->info("메뉴 이름 변경: {$oldName} → 미지급금 관리");
|
||||
} else {
|
||||
$this->command->warn('채무관리 메뉴를 찾을 수 없습니다.');
|
||||
Menu::where('tenant_id', $tenantId)
|
||||
->whereNull('parent_id')
|
||||
->orderBy('sort_order')
|
||||
->get(['id', 'name', 'url'])
|
||||
->each(fn ($m) => $this->command->line(" - [{$m->id}] {$m->name} ({$m->url})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<div id="payables-root"></div>
|
||||
@endsection
|
||||
|
||||
@@ -48,13 +49,9 @@
|
||||
const RefreshCw = createIcon('refresh-cw');
|
||||
|
||||
function PayablesManagement() {
|
||||
const [payables, setPayables] = useState([
|
||||
{ id: 1, vendorName: '(주)오피스월드', invoiceNo: 'PO-2026-0123', issueDate: '2026-01-10', dueDate: '2026-01-25', amount: 3500000, paidAmount: 0, status: 'unpaid', category: '사무용품', description: '1월 사무용품 구매' },
|
||||
{ id: 2, vendorName: 'IT솔루션즈', invoiceNo: 'PO-2026-0118', issueDate: '2026-01-05', dueDate: '2026-01-20', amount: 12000000, paidAmount: 6000000, status: 'partial', category: '소프트웨어', description: 'ERP 라이선스' },
|
||||
{ id: 3, vendorName: '클라우드서비스', invoiceNo: 'PO-2025-0956', issueDate: '2025-12-20', dueDate: '2026-01-05', amount: 8500000, paidAmount: 8500000, status: 'paid', category: '서비스', description: '12월 클라우드 서비스' },
|
||||
{ id: 4, vendorName: '인테리어프로', invoiceNo: 'PO-2025-0912', issueDate: '2025-12-01', dueDate: '2025-12-20', amount: 25000000, paidAmount: 0, status: 'overdue', category: '시설', description: '사무실 리모델링' },
|
||||
{ id: 5, vendorName: '보안시스템', invoiceNo: 'PO-2026-0128', issueDate: '2026-01-15', dueDate: '2026-01-30', amount: 4800000, paidAmount: 0, status: 'unpaid', category: '장비', description: '보안 장비 구매' },
|
||||
]);
|
||||
const [payables, setPayables] = useState([]);
|
||||
const [stats, setStats] = useState({ totalAmount: 0, totalPaid: 0, totalUnpaid: 0, overdueAmount: 0, count: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
@@ -63,12 +60,15 @@ function PayablesManagement() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [modalMode, setModalMode] = useState('add');
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [showPayModal, setShowPayModal] = useState(false);
|
||||
const [payingItem, setPayingItem] = useState(null);
|
||||
const [payAmount, setPayAmount] = useState('');
|
||||
const [payDate, setPayDate] = useState('');
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const categories = ['사무용품', '소프트웨어', '서비스', '시설', '장비', '외주', '기타'];
|
||||
|
||||
const initialFormState = {
|
||||
@@ -101,31 +101,84 @@ function PayablesManagement() {
|
||||
return diff > 0 ? diff : 0;
|
||||
};
|
||||
|
||||
const fetchPayables = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/finance/payables/list');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setPayables(data.data);
|
||||
setStats(data.stats);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('미지급금 조회 실패:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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()) ||
|
||||
(item.invoiceNo || '').toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = filterStatus === 'all' || item.status === filterStatus;
|
||||
const matchesCategory = filterCategory === 'all' || item.category === filterCategory;
|
||||
return matchesSearch && matchesStatus && matchesCategory;
|
||||
});
|
||||
|
||||
const totalAmount = payables.reduce((sum, item) => sum + item.amount, 0);
|
||||
const totalPaid = payables.reduce((sum, item) => sum + item.paidAmount, 0);
|
||||
const totalUnpaid = totalAmount - totalPaid;
|
||||
const overdueAmount = payables.filter(i => i.status === 'overdue').reduce((sum, item) => sum + (item.amount - item.paidAmount), 0);
|
||||
|
||||
const handleAdd = () => { setModalMode('add'); setFormData(initialFormState); setShowModal(true); };
|
||||
const handleEdit = (item) => { setModalMode('edit'); setEditingItem(item); setFormData({ ...item }); setShowModal(true); };
|
||||
const handleSave = () => {
|
||||
if (!formData.vendorName || !formData.invoiceNo || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
if (modalMode === 'add') {
|
||||
setPayables(prev => [{ id: Date.now(), ...formData, amount: parseInt(formData.amount) || 0, paidAmount: 0 }, ...prev]);
|
||||
} else {
|
||||
setPayables(prev => prev.map(item => item.id === editingItem.id ? { ...item, ...formData, amount: parseInt(formData.amount) || 0 } : item));
|
||||
}
|
||||
setShowModal(false); setEditingItem(null);
|
||||
const handleEdit = (item) => {
|
||||
setModalMode('edit');
|
||||
setEditingItem(item);
|
||||
const safeItem = {};
|
||||
Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? ''; });
|
||||
setFormData(safeItem);
|
||||
setShowModal(true);
|
||||
};
|
||||
const handleSave = async () => {
|
||||
if (!formData.vendorName || !formData.invoiceNo || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const url = modalMode === 'add' ? '/finance/payables/store' : `/finance/payables/${editingItem.id}`;
|
||||
const body = { ...formData, amount: parseInt(formData.amount) || 0 };
|
||||
const res = await fetch(url, {
|
||||
method: modalMode === 'add' ? 'POST' : 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
const errors = data.errors ? Object.values(data.errors).flat().join('\n') : data.message;
|
||||
alert(errors || '저장에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
setShowModal(false);
|
||||
setEditingItem(null);
|
||||
fetchPayables();
|
||||
} catch (err) {
|
||||
console.error('저장 실패:', err);
|
||||
alert('저장에 실패했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await fetch(`/finance/payables/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) {
|
||||
setShowModal(false);
|
||||
fetchPayables();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('삭제 실패:', err);
|
||||
alert('삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
const handleDelete = (id) => { if (confirm('정말 삭제하시겠습니까?')) { setPayables(prev => prev.filter(item => item.id !== id)); setShowModal(false); } };
|
||||
|
||||
const handlePay = (item) => {
|
||||
setPayingItem(item);
|
||||
@@ -134,22 +187,30 @@ function PayablesManagement() {
|
||||
setShowPayModal(true);
|
||||
};
|
||||
|
||||
const processPayment = () => {
|
||||
const processPayment = async () => {
|
||||
const amount = parseInt(parseInputCurrency(payAmount)) || 0;
|
||||
if (amount <= 0) { alert('지급액을 입력해주세요.'); return; }
|
||||
const remaining = payingItem.amount - payingItem.paidAmount;
|
||||
if (amount > remaining) { alert('지급액이 잔액을 초과합니다.'); return; }
|
||||
|
||||
setPayables(prev => prev.map(item => {
|
||||
if (item.id === payingItem.id) {
|
||||
const newPaid = item.paidAmount + amount;
|
||||
const newStatus = newPaid >= item.amount ? 'paid' : 'partial';
|
||||
return { ...item, paidAmount: newPaid, status: newStatus };
|
||||
try {
|
||||
const res = await fetch(`/finance/payables/${payingItem.id}/pay`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify({ payAmount: amount }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert(data.message || '지급 처리에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
setShowPayModal(false);
|
||||
setPayingItem(null);
|
||||
setShowPayModal(false);
|
||||
setPayingItem(null);
|
||||
fetchPayables();
|
||||
} catch (err) {
|
||||
console.error('지급 처리 실패:', err);
|
||||
alert('지급 처리에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
@@ -193,20 +254,20 @@ function PayablesManagement() {
|
||||
<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-2"><span className="text-sm text-gray-500">총 채무액</span><CreditCard className="w-5 h-5 text-gray-400" /></div>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatCurrency(totalAmount)}원</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{payables.length}건</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatCurrency(stats.totalAmount)}원</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{stats.count}건</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-amber-200 p-6 bg-amber-50/30">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-amber-700">미지급잔액</span><Clock className="w-5 h-5 text-amber-500" /></div>
|
||||
<p className="text-2xl font-bold text-amber-600">{formatCurrency(totalUnpaid)}원</p>
|
||||
<p className="text-2xl font-bold text-amber-600">{formatCurrency(stats.totalUnpaid)}원</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-red-200 p-6 bg-red-50/30">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-red-700">연체금액</span><AlertTriangle className="w-5 h-5 text-red-500" /></div>
|
||||
<p className="text-2xl font-bold text-red-600">{formatCurrency(overdueAmount)}원</p>
|
||||
<p className="text-2xl font-bold text-red-600">{formatCurrency(stats.overdueAmount)}원</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-2"><span className="text-sm text-emerald-700">지급완료</span><CheckCircle className="w-5 h-5 text-emerald-500" /></div>
|
||||
<p className="text-2xl font-bold text-emerald-600">{formatCurrency(totalPaid)}원</p>
|
||||
<p className="text-2xl font-bold text-emerald-600">{formatCurrency(stats.totalPaid)}원</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -248,7 +309,9 @@ function PayablesManagement() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{filteredPayables.length === 0 ? (
|
||||
{loading ? (
|
||||
<tr><td colSpan="8" 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>
|
||||
) : filteredPayables.length === 0 ? (
|
||||
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : filteredPayables.map(item => (
|
||||
<tr key={item.id} onClick={() => handleEdit(item)} className={`hover:bg-gray-50 cursor-pointer ${item.status === 'overdue' ? 'bg-red-50/50' : ''}`}>
|
||||
@@ -295,7 +358,7 @@ function PayablesManagement() {
|
||||
<div className="flex gap-3 mt-6">
|
||||
{modalMode === 'edit' && <button onClick={() => handleDelete(editingItem.id)} className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg">삭제</button>}
|
||||
<button onClick={() => setShowModal(false)} className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">취소</button>
|
||||
<button onClick={handleSave} className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">{modalMode === 'add' ? '등록' : '저장'}</button>
|
||||
<button onClick={handleSave} disabled={saving} className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50">{saving ? '저장 중...' : (modalMode === 'add' ? '등록' : '저장')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -895,6 +895,15 @@
|
||||
return view('finance.payables');
|
||||
})->name('payables');
|
||||
|
||||
// 미지급금 관리 API
|
||||
Route::prefix('payables')->name('payables.')->group(function () {
|
||||
Route::get('/list', [\App\Http\Controllers\Finance\PayableController::class, 'index'])->name('list');
|
||||
Route::post('/store', [\App\Http\Controllers\Finance\PayableController::class, 'store'])->name('store');
|
||||
Route::put('/{id}', [\App\Http\Controllers\Finance\PayableController::class, 'update'])->name('update');
|
||||
Route::post('/{id}/pay', [\App\Http\Controllers\Finance\PayableController::class, 'pay'])->name('pay');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Finance\PayableController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// 기타
|
||||
Route::get('/refunds', function () {
|
||||
if (request()->header('HX-Request')) {
|
||||
|
||||
Reference in New Issue
Block a user