Files
sam-manage/resources/views/finance/settlement/index.blade.php
김보곤 73737e637a fix:정산관리 초기 탭 HTMX 자동 로드 추가
?tab=payment 등 URL 파라미터로 직접 접속 시 로딩 스피너가
무한 표시되는 문제 수정. DOMContentLoaded에서 초기 탭의
HTMX 콘텐츠를 자동 로드하도록 변경.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:24:46 +09:00

554 lines
29 KiB
PHP

@extends('layouts.app')
@section('title', '정산관리')
@section('content')
<div class="px-4 py-6" x-data="{ activeTab: '{{ $initialTab }}' }">
{{-- 페이지 헤더 --}}
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">정산관리</h1>
<p class="text-sm text-gray-500 mt-1">통합 정산 현황</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<button type="button"
x-show="activeTab === 'commission'"
onclick="openPaymentModal()"
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
입금 등록
</button>
<a x-show="activeTab === 'commission'"
href="{{ route('finance.sales-commissions.export', ['year' => $year, 'month' => $month]) }}"
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
내보내기
</a>
</div>
</div>
{{-- 통합 통계 카드 --}}
@include('finance.settlement.partials.summary-stats', ['summaryStats' => $summaryStats])
{{-- 네비게이션 --}}
<div class="border-b border-gray-200 mb-6">
<nav class="flex -mb-px space-x-1 overflow-x-auto">
<button @click="activeTab = 'commission'"
:class="activeTab === 'commission' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="border-b-2 py-3 px-4 text-sm font-medium whitespace-nowrap transition-colors">
수당 정산
</button>
<button @click="activeTab = 'payment'"
hx-get="{{ route('finance.settlement.payment') }}"
hx-target="#payment-content"
hx-trigger="click once"
hx-indicator="#payment-loading"
:class="activeTab === 'payment' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="border-b-2 py-3 px-4 text-sm font-medium whitespace-nowrap transition-colors">
수당 지급
</button>
<button @click="activeTab = 'partner'"
hx-get="{{ route('finance.settlement.partner-summary') }}"
hx-target="#partner-content"
hx-trigger="click once"
hx-indicator="#partner-loading"
:class="activeTab === 'partner' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="border-b-2 py-3 px-4 text-sm font-medium whitespace-nowrap transition-colors">
파트너별 현황
</button>
<button @click="activeTab = 'consulting'"
hx-get="{{ route('finance.settlement.consulting') }}"
hx-target="#consulting-content"
hx-trigger="click once"
hx-indicator="#consulting-loading"
:class="activeTab === 'consulting' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="border-b-2 py-3 px-4 text-sm font-medium whitespace-nowrap transition-colors">
컨설팅비용
</button>
<button @click="activeTab = 'customer'"
hx-get="{{ route('finance.settlement.customer') }}"
hx-target="#customer-content"
hx-trigger="click once"
hx-indicator="#customer-loading"
:class="activeTab === 'customer' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="border-b-2 py-3 px-4 text-sm font-medium whitespace-nowrap transition-colors">
고객사정산
</button>
<button @click="activeTab = 'subscription'"
hx-get="{{ route('finance.settlement.subscription') }}"
hx-target="#subscription-content"
hx-trigger="click once"
hx-indicator="#subscription-loading"
:class="activeTab === 'subscription' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="border-b-2 py-3 px-4 text-sm font-medium whitespace-nowrap transition-colors">
구독관리
</button>
</nav>
</div>
{{-- 1: 수당 정산 (즉시 렌더링) --}}
<div x-show="activeTab === 'commission'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
{{-- 통계 카드 --}}
<div id="commission-stats-container">
@include('finance.settlement.partials.commission.stats-cards', ['stats' => $stats, 'year' => $year, 'month' => $month])
</div>
{{-- 필터 --}}
@include('finance.settlement.partials.commission.filters', ['filters' => $filters, 'partners' => $partners, 'year' => $year, 'month' => $month])
{{-- 일괄 처리 버튼 --}}
<div class="flex items-center gap-2 mb-4" id="bulk-actions" style="display: none;">
<span class="text-sm text-gray-600"><span id="selected-count">0</span> 선택</span>
<button type="button" onclick="bulkApprove()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors">
일괄 승인
</button>
<button type="button" onclick="bulkMarkPaid()" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors">
일괄 지급완료
</button>
</div>
{{-- 정산 테이블 --}}
<div id="commission-table-container">
@include('finance.settlement.partials.commission.table', ['commissions' => $commissions])
</div>
</div>
{{-- 2: 수당 지급 (HTMX lazy load) --}}
<div x-show="activeTab === 'payment'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div id="payment-content">
<div id="payment-loading" class="flex items-center justify-center py-12">
<svg class="w-8 h-8 animate-spin text-indigo-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="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>
<span class="ml-3 text-gray-500">로딩 ...</span>
</div>
</div>
</div>
{{-- 3: 파트너별 현황 (HTMX lazy load) --}}
<div x-show="activeTab === 'partner'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div id="partner-content">
<div id="partner-loading" class="flex items-center justify-center py-12">
<svg class="w-8 h-8 animate-spin text-indigo-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="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>
<span class="ml-3 text-gray-500">로딩 ...</span>
</div>
</div>
</div>
{{-- 4: 컨설팅비용 (HTMX lazy load) --}}
<div x-show="activeTab === 'consulting'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div id="consulting-content">
<div id="consulting-loading" class="flex items-center justify-center py-12">
<svg class="w-8 h-8 animate-spin text-indigo-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="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>
<span class="ml-3 text-gray-500">로딩 ...</span>
</div>
</div>
</div>
{{-- 5: 고객사정산 (HTMX lazy load) --}}
<div x-show="activeTab === 'customer'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div id="customer-content">
<div id="customer-loading" class="flex items-center justify-center py-12">
<svg class="w-8 h-8 animate-spin text-indigo-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="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>
<span class="ml-3 text-gray-500">로딩 ...</span>
</div>
</div>
</div>
{{-- 6: 구독관리 (HTMX lazy load) --}}
<div x-show="activeTab === 'subscription'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
<div id="subscription-content">
<div id="subscription-loading" class="flex items-center justify-center py-12">
<svg class="w-8 h-8 animate-spin text-indigo-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="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>
<span class="ml-3 text-gray-500">로딩 ...</span>
</div>
</div>
</div>
</div>
{{-- 입금 등록 모달 --}}
<div id="payment-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-800">입금 등록</h3>
<button type="button" onclick="closePaymentModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div id="payment-form-container" class="p-6">
@include('finance.settlement.partials.commission.payment-form', ['management' => null, 'pendingTenants' => $pendingTenants])
</div>
</div>
</div>
{{-- 상세 모달 --}}
<div id="detail-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-lg shadow-xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div id="detail-modal-content"></div>
</div>
</div>
@endsection
@push('scripts')
<script>
let selectedIds = [];
function updateSelection() {
selectedIds = Array.from(document.querySelectorAll('.commission-checkbox:checked'))
.map(cb => parseInt(cb.value));
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
if (selectedIds.length > 0) {
bulkActions.style.display = 'flex';
selectedCount.textContent = selectedIds.length;
} else {
bulkActions.style.display = 'none';
}
}
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.commission-checkbox');
checkboxes.forEach(cb => { cb.checked = checkbox.checked; });
updateSelection();
}
function openPaymentModal() {
document.getElementById('payment-modal').classList.remove('hidden');
}
function closePaymentModal() {
document.getElementById('payment-modal').classList.add('hidden');
}
function submitPayment() {
const form = document.getElementById('payment-form');
const formData = new FormData(form);
fetch('{{ route("finance.sales-commissions.payment") }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
closePaymentModal();
location.reload();
} else {
alert(data.message || '오류가 발생했습니다.');
}
})
.catch(error => {
console.error('Error:', error);
alert('오류가 발생했습니다.');
});
}
function openDetailModal(commissionId) {
fetch('{{ url("finance/sales-commissions") }}/' + commissionId + '/detail')
.then(response => response.text())
.then(html => {
document.getElementById('detail-modal-content').innerHTML = html;
document.getElementById('detail-modal').classList.remove('hidden');
});
}
function closeDetailModal() {
document.getElementById('detail-modal').classList.add('hidden');
}
function approveCommission(id) {
if (!confirm('승인하시겠습니까?')) return;
fetch('{{ url("finance/sales-commissions") }}/' + id + '/approve', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
location.reload();
} else {
alert(data.message || '오류가 발생했습니다.');
}
});
}
function bulkApprove() {
if (selectedIds.length === 0) { alert('선택된 항목이 없습니다.'); return; }
if (!confirm(selectedIds.length + '건을 승인하시겠습니까?')) return;
fetch('{{ route("finance.sales-commissions.bulk-approve") }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: selectedIds })
})
.then(response => response.json())
.then(data => {
if (data.success) { alert(data.message); location.reload(); }
else { alert(data.message || '오류가 발생했습니다.'); }
});
}
function markPaidCommission(id) {
const bankReference = prompt('이체 참조번호를 입력하세요 (선택사항)');
if (bankReference === null) return;
fetch('{{ url("finance/sales-commissions") }}/' + id + '/mark-paid', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ bank_reference: bankReference })
})
.then(response => response.json())
.then(data => {
if (data.success) { alert(data.message); location.reload(); }
else { alert(data.message || '오류가 발생했습니다.'); }
});
}
function bulkMarkPaid() {
if (selectedIds.length === 0) { alert('선택된 항목이 없습니다.'); return; }
const bankReference = prompt('이체 참조번호를 입력하세요 (선택사항)');
if (bankReference === null) return;
if (!confirm(selectedIds.length + '건을 지급완료 처리하시겠습니까?')) return;
fetch('{{ route("finance.sales-commissions.bulk-mark-paid") }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: selectedIds, bank_reference: bankReference })
})
.then(response => response.json())
.then(data => {
if (data.success) { alert(data.message); location.reload(); }
else { alert(data.message || '오류가 발생했습니다.'); }
});
}
function cancelCommission(id) {
if (!confirm('취소하시겠습니까?')) return;
fetch('{{ url("finance/sales-commissions") }}/' + id + '/cancel', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) { alert(data.message); location.reload(); }
else { alert(data.message || '오류가 발생했습니다.'); }
});
}
function onTenantSelect(managementId) {
if (!managementId) return;
htmx.ajax('GET', '{{ route("finance.sales-commissions.payment-form") }}?management_id=' + managementId, {
target: '#payment-form-container'
});
}
// 컨설팅비용 탭 Alpine 컴포넌트
function consultingManager() {
return {
items: [], stats: { totalAmount: 0, paidAmount: 0, pendingAmount: 0, totalHours: 0 },
loading: false, saving: false, showModal: false, modalMode: 'add', editingId: null,
searchTerm: '', filterStatus: 'all',
form: { date: '', consultant: '', customer: '', service: '', hours: 0, hourlyRate: 0, amount: 0, status: 'pending', memo: '' },
formatCurrency(val) { return Number(val || 0).toLocaleString(); },
filteredItems() {
return this.items.filter(item => {
const matchSearch = !this.searchTerm || (item.consultant || '').includes(this.searchTerm) || (item.customer || '').includes(this.searchTerm);
const matchStatus = this.filterStatus === 'all' || item.status === this.filterStatus;
return matchSearch && matchStatus;
});
},
async fetchData() {
this.loading = true;
try {
const res = await fetch('/finance/consulting-fees/list');
const data = await res.json();
if (data.success) { this.items = data.data; this.stats = data.stats; }
} finally { this.loading = false; }
},
openModal(mode, item = null) {
this.modalMode = mode; this.editingId = item?.id || null;
this.form = item ? { ...item } : { date: new Date().toISOString().split('T')[0], consultant: '', customer: '', service: '', hours: 0, hourlyRate: 0, amount: 0, status: 'pending', memo: '' };
this.showModal = true;
},
async saveItem() {
if (!this.form.consultant || !this.form.amount) { alert('필수 항목을 입력해주세요.'); return; }
this.saving = true;
try {
const url = this.modalMode === 'add' ? '/finance/consulting-fees/store' : '/finance/consulting-fees/' + this.editingId;
const res = await fetch(url, { method: this.modalMode === 'add' ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify(this.form) });
const data = await res.json();
if (!res.ok) { alert(data.errors ? Object.values(data.errors).flat().join('\n') : data.message || '저장 실패'); return; }
this.showModal = false; this.fetchData();
} finally { this.saving = false; }
},
async deleteItem(id) {
if (!confirm('정말 삭제하시겠습니까?')) return;
await fetch('/finance/consulting-fees/' + id, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } });
this.fetchData();
}
};
}
// 고객사정산 탭 Alpine 컴포넌트
function customerSettlementManager() {
return {
items: [], stats: { totalSales: 0, totalCommission: 0, totalNet: 0, settledAmount: 0 },
loading: false, saving: false, showModal: false, modalMode: 'add', editingId: null,
searchTerm: '', filterStatus: 'all',
form: { period: '', customer: '', totalSales: 0, commission: 0, expense: 0, netAmount: 0, status: 'pending', settledDate: '', memo: '' },
formatCurrency(val) { return Number(val || 0).toLocaleString(); },
filteredItems() {
return this.items.filter(item => {
const matchSearch = !this.searchTerm || (item.customer || '').includes(this.searchTerm);
const matchStatus = this.filterStatus === 'all' || item.status === this.filterStatus;
return matchSearch && matchStatus;
});
},
async fetchData() {
this.loading = true;
try {
const res = await fetch('/finance/customer-settlements/list');
const data = await res.json();
if (data.success) { this.items = data.data; this.stats = data.stats; }
} finally { this.loading = false; }
},
openModal(mode, item = null) {
this.modalMode = mode; this.editingId = item?.id || null;
this.form = item ? { ...item } : { period: new Date().toISOString().slice(0,7), customer: '', totalSales: 0, commission: 0, expense: 0, netAmount: 0, status: 'pending', settledDate: '', memo: '' };
this.showModal = true;
},
async saveItem() {
if (!this.form.customer) { alert('고객사를 입력해주세요.'); return; }
this.form.netAmount = (parseInt(this.form.totalSales)||0) - (parseInt(this.form.commission)||0) - (parseInt(this.form.expense)||0);
this.saving = true;
try {
const url = this.modalMode === 'add' ? '/finance/customer-settlements/store' : '/finance/customer-settlements/' + this.editingId;
const res = await fetch(url, { method: this.modalMode === 'add' ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify(this.form) });
const data = await res.json();
if (!res.ok) { alert(data.errors ? Object.values(data.errors).flat().join('\n') : data.message || '저장 실패'); return; }
this.showModal = false; this.fetchData();
} finally { this.saving = false; }
},
async deleteItem(id) {
if (!confirm('정말 삭제하시겠습니까?')) return;
await fetch('/finance/customer-settlements/' + id, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } });
this.fetchData();
}
};
}
// 구독관리 탭 Alpine 컴포넌트
function subscriptionManager() {
return {
items: [], stats: { activeCount: 0, monthlyRecurring: 0, yearlyRecurring: 0, totalUsers: 0 },
loading: false, saving: false, showModal: false, modalMode: 'add', editingId: null,
searchTerm: '', filterStatus: 'all', filterPlan: 'all',
form: { customer: '', plan: 'Starter', monthlyFee: 0, billingCycle: 'monthly', startDate: '', nextBilling: '', status: 'active', users: 0, memo: '' },
formatCurrency(val) { return Number(val || 0).toLocaleString(); },
filteredItems() {
return this.items.filter(item => {
const matchSearch = !this.searchTerm || (item.customer || '').toLowerCase().includes(this.searchTerm.toLowerCase());
const matchStatus = this.filterStatus === 'all' || item.status === this.filterStatus;
const matchPlan = this.filterPlan === 'all' || item.plan === this.filterPlan;
return matchSearch && matchStatus && matchPlan;
});
},
async fetchData() {
this.loading = true;
try {
const res = await fetch('/finance/subscriptions/list');
const data = await res.json();
if (data.success) { this.items = data.data; this.stats = data.stats; }
} finally { this.loading = false; }
},
openModal(mode, item = null) {
this.modalMode = mode; this.editingId = item?.id || null;
this.form = item ? { ...item } : { customer: '', plan: 'Starter', monthlyFee: 0, billingCycle: 'monthly', startDate: new Date().toISOString().split('T')[0], nextBilling: '', status: 'active', users: 0, memo: '' };
this.showModal = true;
},
async saveItem() {
if (!this.form.customer || !this.form.monthlyFee) { alert('필수 항목을 입력해주세요.'); return; }
this.saving = true;
try {
const url = this.modalMode === 'add' ? '/finance/subscriptions/store' : '/finance/subscriptions/' + this.editingId;
const body = { ...this.form, monthlyFee: parseInt(this.form.monthlyFee) || 0, users: parseInt(this.form.users) || 0 };
const res = await fetch(url, { method: this.modalMode === 'add' ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify(body) });
const data = await res.json();
if (!res.ok) { alert(data.errors ? Object.values(data.errors).flat().join('\n') : data.message || '저장 실패'); return; }
this.showModal = false; this.fetchData();
} finally { this.saving = false; }
},
async deleteItem(id) {
if (!confirm('정말 삭제하시겠습니까?')) return;
await fetch('/finance/subscriptions/' + id, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } });
this.fetchData();
}
};
}
// 초기 탭이 기본(commission)이 아닌 경우 HTMX 콘텐츠 자동 로드
document.addEventListener('DOMContentLoaded', function() {
const initialTab = '{{ $initialTab }}';
const tabRoutes = {
'payment': '{{ route("finance.settlement.payment") }}',
'partner': '{{ route("finance.settlement.partner-summary") }}',
'consulting': '{{ route("finance.settlement.consulting") }}',
'customer': '{{ route("finance.settlement.customer") }}',
'subscription': '{{ route("finance.settlement.subscription") }}',
};
if (tabRoutes[initialTab]) {
htmx.ajax('GET', tabRoutes[initialTab], {target: '#' + initialTab + '-content'});
}
});
</script>
@endpush