Files
sam-manage/resources/views/finance/settlement/partials/customer-tab.blade.php
김보곤 7bc412d9a1 feat:통합 정산관리 페이지 구현 (5개 탭 기반)
- SettlementController 신규 생성 (통합 정산관리 메인 + 탭별 HTMX)
- 5개 탭: 수당정산, 파트너별현황(NEW), 컨설팅비용, 고객사정산, 구독관리
- 수당정산 탭: 기존 영업수수료정산 이관 + 유치수당 컬럼/수당유형 필터 추가
- 파트너별 현황 탭: SalesPartner 수당 집계 + 필터/페이지네이션
- 컨설팅/고객사/구독 탭: React → Blade+Alpine.js 전환 (기존 API 재사용)
- 통합 통계카드 (미지급수당/승인대기/이번달예정/누적지급)
- 기존 4개 URL → 통합 페이지 리다이렉트
- SalesPartner 모델에 commissions 관계 추가
- SalesCommissionService에 commission_type 필터 + referrerPartner eager load 추가

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

184 lines
13 KiB
PHP

{{-- 고객사정산 (Blade + Alpine.js) --}}
<div x-data="customerSettlementManager()" x-init="fetchData()">
{{-- 통계 카드 --}}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-gray-400">
<p class="text-sm text-gray-500"> 매출</p>
<p class="text-xl font-bold text-gray-700" x-text="formatCurrency(stats.totalSales) + '원'">0</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-indigo-500">
<p class="text-sm text-gray-500">정산금액</p>
<p class="text-xl font-bold text-indigo-600" x-text="formatCurrency(stats.totalNet) + '원'">0</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-emerald-500">
<p class="text-sm text-gray-500">정산완료</p>
<p class="text-xl font-bold text-emerald-600" x-text="formatCurrency(stats.settledAmount) + '원'">0</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-rose-500">
<p class="text-sm text-gray-500">수수료 합계</p>
<p class="text-xl font-bold text-rose-600" x-text="formatCurrency(stats.totalCommission) + '원'">0</p>
</div>
</div>
{{-- 필터 + 등록 버튼 --}}
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">검색</label>
<input type="text" x-model="searchTerm" placeholder="고객사 검색" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
<select x-model="filterStatus" class="rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option value="all">전체</option>
<option value="settled">완료</option>
<option value="pending">대기</option>
</select>
</div>
<button @click="openModal('add')" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors">
+ 등록
</button>
</div>
</div>
{{-- 테이블 --}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">정산월</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">고객사</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">매출액</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">수수료</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">비용</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">정산금액</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">상태</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">관리</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<template x-if="loading">
<tr><td colspan="8" class="px-4 py-8 text-center text-gray-400">로딩 ...</td></tr>
</template>
<template x-if="!loading && filteredItems().length === 0">
<tr><td colspan="8" class="px-4 py-8 text-center text-gray-400">데이터가 없습니다.</td></tr>
</template>
<template x-for="item in filteredItems()" :key="item.id">
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-900" x-text="item.period"></td>
<td class="px-4 py-3 text-sm font-medium text-gray-900" x-text="item.customer"></td>
<td class="px-4 py-3 text-sm text-right text-gray-900" x-text="formatCurrency(item.totalSales) + '원'"></td>
<td class="px-4 py-3 text-sm text-right text-rose-600" x-text="formatCurrency(item.commission) + '원'"></td>
<td class="px-4 py-3 text-sm text-right text-gray-500" x-text="formatCurrency(item.expense) + '원'"></td>
<td class="px-4 py-3 text-sm text-right font-medium text-indigo-600" x-text="formatCurrency(item.netAmount) + '원'"></td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
:class="item.status === 'settled' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'"
x-text="item.status === 'settled' ? '완료' : '대기'"></span>
</td>
<td class="px-4 py-3 text-center">
<button @click="openModal('edit', item)" class="p-1 text-gray-400 hover:text-blue-500" title="수정">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</button>
<button @click="deleteItem(item.id)" class="p-1 text-gray-400 hover:text-red-500" title="삭제">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
{{-- 모달 --}}
<div x-show="showModal" x-cloak class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white rounded-xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto" @click.outside="showModal = false">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-bold text-gray-900" x-text="modalMode === 'add' ? '정산 등록' : '정산 수정'"></h3>
<button @click="showModal = false" class="p-1 hover:bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-500" 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 class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div><label class="block text-sm font-medium text-gray-700 mb-1">정산월</label><input type="month" x-model="form.period" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
<select x-model="form.status" class="w-full px-3 py-2 border rounded-lg"><option value="pending">대기</option><option value="settled">완료</option></select>
</div>
</div>
<div><label class="block text-sm font-medium text-gray-700 mb-1">고객사 *</label><input type="text" x-model="form.customer" class="w-full px-3 py-2 border rounded-lg"></div>
<div class="grid grid-cols-3 gap-4">
<div><label class="block text-sm font-medium text-gray-700 mb-1">매출액</label><input type="number" x-model="form.totalSales" class="w-full px-3 py-2 border rounded-lg text-right"></div>
<div><label class="block text-sm font-medium text-gray-700 mb-1">수수료</label><input type="number" x-model="form.commission" class="w-full px-3 py-2 border rounded-lg text-right"></div>
<div><label class="block text-sm font-medium text-gray-700 mb-1">비용</label><input type="number" x-model="form.expense" class="w-full px-3 py-2 border rounded-lg text-right"></div>
</div>
<div class="bg-indigo-50 rounded-lg p-3">
<div class="flex justify-between text-sm">
<span class="text-gray-600">정산금액 (매출 - 수수료 - 비용)</span>
<span class="font-bold text-indigo-600" x-text="formatCurrency((parseInt(form.totalSales)||0) - (parseInt(form.commission)||0) - (parseInt(form.expense)||0)) + '원'"></span>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div><label class="block text-sm font-medium text-gray-700 mb-1">정산일</label><input type="date" x-model="form.settledDate" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="block text-sm font-medium text-gray-700 mb-1">메모</label><input type="text" x-model="form.memo" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="showModal = false" class="flex-1 px-4 py-2 border text-gray-700 rounded-lg hover:bg-gray-50">취소</button>
<button @click="saveItem()" :disabled="saving" class="flex-1 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg disabled:opacity-50" x-text="saving ? '저장 중...' : '저장'">저장</button>
</div>
</div>
</div>
</div>
<script>
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();
}
};
}
</script>