Files
sam-manage/resources/views/finance/sales-commission/index.blade.php
pro 5d7de6d13b feat:영업수수료 정산 기능 구현
[모델]
- SalesCommission: 영업수수료 정산 모델
- SalesCommissionDetail: 상품별 수당 내역 모델
- SalesTenantManagement: 입금 정보 필드 추가

[서비스/컨트롤러]
- SalesCommissionService: 수당 생성, 승인, 지급 처리 로직
- SalesCommissionController: 정산 관리 CRUD

[뷰]
- 본사 정산 관리 화면 (필터, 통계, 테이블)
- 입금 등록 모달
- 상세 보기 모달
- 영업파트너 대시보드 수당 카드

[라우트]
- /finance/sales-commissions/* 라우트 추가
- 기존 sales-commission 리다이렉트 호환

[메뉴]
- SalesCommissionMenuSeeder: 정산관리 > 영업수수료정산 메뉴 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:14:11 +09:00

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