?tab=payment 등 URL 파라미터로 직접 접속 시 로딩 스피너가 무한 표시되는 문제 수정. DOMContentLoaded에서 초기 탭의 HTMX 콘텐츠를 자동 로드하도록 변경. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
554 lines
29 KiB
PHP
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
|