420 lines
19 KiB
PHP
420 lines
19 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '정산관리')
|
|
|
|
@section('content')
|
|
<div class="px-4 py-6" x-data="{ activeTab: '{{ $initialTab }}', commissionSubTab: 'partner' }">
|
|
{{-- 페이지 헤더 --}}
|
|
<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 = '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">
|
|
<div class="inline-flex rounded-lg bg-gray-100 p-1">
|
|
<button @click="commissionSubTab = 'partner'; clearAllCheckboxes()"
|
|
:class="commissionSubTab === 'partner' ? 'bg-white text-indigo-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'"
|
|
class="px-4 py-1.5 text-sm font-medium rounded-md transition-all">
|
|
영업파트너 수당
|
|
</button>
|
|
<button @click="commissionSubTab = 'manager'; clearAllCheckboxes()"
|
|
:class="commissionSubTab === 'manager' ? 'bg-white text-indigo-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'"
|
|
class="px-4 py-1.5 text-sm font-medium rounded-md transition-all">
|
|
매니저 수당
|
|
</button>
|
|
</div>
|
|
|
|
{{-- 일괄 처리 버튼 --}}
|
|
<div class="flex items-center gap-2 ml-auto" 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>
|
|
|
|
{{-- 서브탭 1: 영업파트너 수당 테이블 --}}
|
|
<div x-show="commissionSubTab === 'partner'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
|
<div id="commission-table-container">
|
|
@include('finance.settlement.partials.commission.partner-commission-table', ['commissions' => $commissions])
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 서브탭 2: 매니저 수당 테이블 --}}
|
|
<div x-show="commissionSubTab === 'manager'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
|
<div id="manager-table-container">
|
|
@include('finance.settlement.partials.commission.manager-commission-table', ['commissions' => $commissions])
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 탭 2: 고객사정산 (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>
|
|
|
|
{{-- 탭 3: 구독관리 (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 clearAllCheckboxes() {
|
|
document.querySelectorAll('.commission-checkbox').forEach(cb => { cb.checked = false; });
|
|
document.querySelectorAll('thead input[type="checkbox"]').forEach(cb => { cb.checked = false; });
|
|
selectedIds = [];
|
|
document.getElementById('bulk-actions').style.display = 'none';
|
|
}
|
|
|
|
function updateSelection() {
|
|
const container = document.querySelector('[x-show="commissionSubTab === \'partner\'"]')?.offsetParent !== null
|
|
? document.querySelector('[x-show="commissionSubTab === \'partner\'"]')
|
|
: document.querySelector('[x-show="commissionSubTab === \'manager\'"]');
|
|
selectedIds = container
|
|
? Array.from(container.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 container = checkbox.closest('.bg-white.rounded-lg');
|
|
container.querySelectorAll('.commission-checkbox').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 unapproveCommission(id) {
|
|
if (!confirm('승인을 취소하시겠습니까?')) return;
|
|
|
|
fetch('{{ url("finance/sales-commissions") }}/' + id + '/unapprove', {
|
|
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 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 saveSettlementDate(commissionId, field, value) {
|
|
fetch(`{{ url('finance/sales-commissions') }}/${commissionId}/update-date`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ field: field, value: value })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
alert(data.message || '저장에 실패했습니다.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
});
|
|
}
|
|
|
|
function formatCommissionInput(el) {
|
|
const raw = el.value.replace(/,/g, '').replace(/[^0-9]/g, '');
|
|
el.value = raw ? Number(raw).toLocaleString() : '';
|
|
}
|
|
|
|
function onTenantSelect(managementId) {
|
|
if (!managementId) return;
|
|
htmx.ajax('GET', '{{ route("finance.sales-commissions.payment-form") }}?management_id=' + managementId, {
|
|
target: '#payment-form-container'
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const initialTab = '{{ $initialTab }}';
|
|
const tabRoutes = {
|
|
'customer': '{{ route("finance.settlement.customer") }}',
|
|
'subscription': '{{ route("finance.settlement.subscription") }}',
|
|
};
|
|
if (tabRoutes[initialTab]) {
|
|
htmx.ajax('GET', tabRoutes[initialTab], {target: '#' + initialTab + '-content'});
|
|
}
|
|
});
|
|
</script>
|
|
@endpush
|