[모델] - SalesCommission: 영업수수료 정산 모델 - SalesCommissionDetail: 상품별 수당 내역 모델 - SalesTenantManagement: 입금 정보 필드 추가 [서비스/컨트롤러] - SalesCommissionService: 수당 생성, 승인, 지급 처리 로직 - SalesCommissionController: 정산 관리 CRUD [뷰] - 본사 정산 관리 화면 (필터, 통계, 테이블) - 입금 등록 모달 - 상세 보기 모달 - 영업파트너 대시보드 수당 카드 [라우트] - /finance/sales-commissions/* 라우트 추가 - 기존 sales-commission 리다이렉트 호환 [메뉴] - SalesCommissionMenuSeeder: 정산관리 > 영업수수료정산 메뉴 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
377 lines
15 KiB
PHP
377 lines
15 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '영업수수료정산')
|
|
|
|
@section('content')
|
|
<div class="container mx-auto px-4 py-6">
|
|
{{-- 페이지 헤더 --}}
|
|
<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">{{ $year }}년 {{ $month }}월 지급예정</p>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<button type="button"
|
|
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 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>
|
|
|
|
{{-- 통계 카드 --}}
|
|
<div id="stats-container">
|
|
@include('finance.sales-commission.partials.stats-cards', ['stats' => $stats, 'year' => $year, 'month' => $month])
|
|
</div>
|
|
|
|
{{-- 필터 섹션 --}}
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
|
<form id="filter-form" method="GET" action="{{ route('finance.sales-commissions.index') }}">
|
|
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
|
{{-- 년/월 선택 --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">년도</label>
|
|
<select name="year" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
|
@for ($y = now()->year - 2; $y <= now()->year + 1; $y++)
|
|
<option value="{{ $y }}" {{ $year == $y ? 'selected' : '' }}>{{ $y }}년</option>
|
|
@endfor
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">월</label>
|
|
<select name="month" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
|
@for ($m = 1; $m <= 12; $m++)
|
|
<option value="{{ $m }}" {{ $month == $m ? 'selected' : '' }}>{{ $m }}월</option>
|
|
@endfor
|
|
</select>
|
|
</div>
|
|
|
|
{{-- 상태 필터 --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
|
|
<select name="status" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
|
<option value="">전체</option>
|
|
<option value="pending" {{ ($filters['status'] ?? '') == 'pending' ? 'selected' : '' }}>대기</option>
|
|
<option value="approved" {{ ($filters['status'] ?? '') == 'approved' ? 'selected' : '' }}>승인</option>
|
|
<option value="paid" {{ ($filters['status'] ?? '') == 'paid' ? 'selected' : '' }}>지급완료</option>
|
|
<option value="cancelled" {{ ($filters['status'] ?? '') == 'cancelled' ? 'selected' : '' }}>취소</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- 입금구분 필터 --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">입금구분</label>
|
|
<select name="payment_type" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
|
<option value="">전체</option>
|
|
<option value="deposit" {{ ($filters['payment_type'] ?? '') == 'deposit' ? 'selected' : '' }}>계약금</option>
|
|
<option value="balance" {{ ($filters['payment_type'] ?? '') == 'balance' ? 'selected' : '' }}>잔금</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- 영업파트너 필터 --}}
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">영업파트너</label>
|
|
<select name="partner_id" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
|
<option value="">전체</option>
|
|
@foreach ($partners as $partner)
|
|
<option value="{{ $partner->id }}" {{ ($filters['partner_id'] ?? '') == $partner->id ? 'selected' : '' }}>
|
|
{{ $partner->user->name ?? $partner->partner_code }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- 버튼 --}}
|
|
<div class="flex items-end gap-2">
|
|
<button type="submit"
|
|
class="flex-1 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors">
|
|
조회
|
|
</button>
|
|
<a href="{{ route('finance.sales-commissions.index') }}"
|
|
class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors">
|
|
초기화
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{{-- 일괄 처리 버튼 --}}
|
|
<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="table-container">
|
|
@include('finance.sales-commission.partials.commission-table', ['commissions' => $commissions])
|
|
</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.sales-commission.partials.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>
|
|
// 선택된 정산 ID 배열
|
|
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로 폼 업데이트
|
|
htmx.ajax('GET', '{{ route("finance.sales-commissions.payment-form") }}?management_id=' + managementId, {
|
|
target: '#payment-form-container'
|
|
});
|
|
}
|
|
</script>
|
|
@endpush
|