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>
This commit is contained in:
김보곤
2026-02-19 09:53:13 +09:00
parent 41e7eca92d
commit 7bc412d9a1
15 changed files with 2041 additions and 25 deletions

View File

@@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesCommission;
use App\Models\Sales\SalesPartner;
use App\Services\SalesCommissionService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
class SettlementController extends Controller
{
public function __construct(
private SalesCommissionService $service
) {}
/**
* 통합 정산관리 메인 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request') && !$request->header('HX-Boosted')) {
return response('', 200)->header('HX-Redirect', route('finance.settlement'));
}
$initialTab = $request->input('tab', 'commission');
// 수당 정산 탭 데이터 (기본 탭이므로 즉시 로드)
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
$filters = [
'scheduled_year' => $year,
'scheduled_month' => $month,
'status' => $request->input('status'),
'payment_type' => $request->input('payment_type'),
'partner_id' => $request->input('partner_id'),
'commission_type' => $request->input('commission_type'),
'search' => $request->input('search'),
];
$commissions = $this->service->getCommissions($filters);
$stats = $this->service->getSettlementStats($year, $month);
$partners = SalesPartner::with('user')
->active()
->orderBy('partner_code')
->get();
$pendingTenants = $this->service->getPendingPaymentTenants();
// 통합 통계 (페이지 상단)
$summaryStats = $this->getSummaryStats();
return view('finance.settlement.index', compact(
'initialTab',
'commissions',
'stats',
'partners',
'pendingTenants',
'year',
'month',
'filters',
'summaryStats'
));
}
/**
* 수당 통계카드 HTMX 갱신
*/
public function commissionStats(Request $request): View
{
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
$stats = $this->service->getSettlementStats($year, $month);
return view('finance.settlement.partials.commission.stats-cards', compact('stats', 'year', 'month'));
}
/**
* 수당 테이블 HTMX 갱신
*/
public function commissionTable(Request $request): View
{
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
$filters = [
'scheduled_year' => $year,
'scheduled_month' => $month,
'status' => $request->input('status'),
'payment_type' => $request->input('payment_type'),
'partner_id' => $request->input('partner_id'),
'commission_type' => $request->input('commission_type'),
'search' => $request->input('search'),
];
$commissions = $this->service->getCommissions($filters);
return view('finance.settlement.partials.commission.table', compact('commissions'));
}
/**
* 파트너별 현황 탭
*/
public function partnerSummary(Request $request): View
{
$query = SalesPartner::with('user');
// 검색
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('partner_code', 'like', "%{$search}%")
->orWhereHas('user', function ($uq) use ($search) {
$uq->where('name', 'like', "%{$search}%");
});
});
}
// 유형 필터
if ($type = $request->input('type')) {
if ($type === 'individual') {
$query->where('partner_type', '!=', 'corporate');
} elseif ($type === 'corporate') {
$query->where('partner_type', 'corporate');
}
}
// 상태 필터
if ($request->input('status', 'active') === 'active') {
$query->active();
}
$partners = $query->orderBy('partner_code')->paginate(20);
// 각 파트너별 수당 집계
$partnerIds = $partners->pluck('id')->toArray();
if (!empty($partnerIds)) {
$commissionStats = SalesCommission::selectRaw('
partner_id,
SUM(CASE WHEN status = "paid" THEN partner_commission ELSE 0 END) as paid_total,
SUM(CASE WHEN status IN ("pending", "approved") THEN partner_commission ELSE 0 END) as unpaid_total,
COUNT(*) as total_count,
MAX(CASE WHEN status = "paid" THEN actual_payment_date ELSE NULL END) as last_paid_date
')
->whereIn('partner_id', $partnerIds)
->groupBy('partner_id')
->get()
->keyBy('partner_id');
} else {
$commissionStats = collect();
}
return view('finance.settlement.partials.partner-summary', compact('partners', 'commissionStats'));
}
/**
* 컨설팅비용 탭
*/
public function consultingTab(Request $request): View
{
return view('finance.settlement.partials.consulting-tab');
}
/**
* 고객사정산 탭
*/
public function customerTab(Request $request): View
{
return view('finance.settlement.partials.customer-tab');
}
/**
* 구독관리 탭
*/
public function subscriptionTab(Request $request): View
{
return view('finance.settlement.partials.subscription-tab');
}
/**
* 통합 통계 데이터
*/
private function getSummaryStats(): array
{
$now = now();
// 미지급 수당 (pending + approved)
$unpaidAmount = SalesCommission::whereIn('status', [
SalesCommission::STATUS_PENDING,
SalesCommission::STATUS_APPROVED,
])->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
// 승인 대기 건수
$pendingCount = SalesCommission::where('status', SalesCommission::STATUS_PENDING)->count();
// 이번달 지급예정
$thisMonthScheduled = SalesCommission::whereIn('status', [
SalesCommission::STATUS_PENDING,
SalesCommission::STATUS_APPROVED,
])
->whereYear('scheduled_payment_date', $now->year)
->whereMonth('scheduled_payment_date', $now->month)
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
// 누적 지급완료
$totalPaid = SalesCommission::where('status', SalesCommission::STATUS_PAID)
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
->value('total') ?? 0;
return [
'unpaid_amount' => $unpaidAmount,
'pending_count' => $pendingCount,
'this_month_scheduled' => $thisMonthScheduled,
'total_paid' => $totalPaid,
];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Models\Sales;
use App\Models\Sales\SalesCommission;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -86,6 +87,14 @@ public function tenantManagements(): HasMany
return $this->hasMany(SalesTenantManagement::class, 'sales_partner_id');
}
/**
* 수수료 정산 내역
*/
public function commissions(): HasMany
{
return $this->hasMany(SalesCommission::class, 'partner_id');
}
/**
* 이 단체를 유치한 영업파트너
*/

View File

@@ -32,7 +32,7 @@ class SalesCommissionService
public function getCommissions(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$query = SalesCommission::query()
->with(['tenant', 'partner.user', 'manager', 'management']);
->with(['tenant', 'partner.user', 'manager', 'management', 'referrerPartner.user']);
// 상태 필터
if (!empty($filters['status'])) {
@@ -64,6 +64,19 @@ public function getCommissions(array $filters = [], int $perPage = 20): LengthAw
$query->paymentDateBetween($filters['payment_start_date'], $filters['payment_end_date']);
}
// 수당유형 필터
if (!empty($filters['commission_type'])) {
$commissionType = $filters['commission_type'];
if ($commissionType === 'partner') {
$query->where('partner_commission', '>', 0);
} elseif ($commissionType === 'manager') {
$query->where('manager_commission', '>', 0);
} elseif ($commissionType === 'referrer') {
$query->whereNotNull('referrer_partner_id')
->where('referrer_commission', '>', 0);
}
}
// 테넌트 검색
if (!empty($filters['search'])) {
$search = $filters['search'];

View File

@@ -0,0 +1,372 @@
@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 = '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 === '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>
{{-- 3: 컨설팅비용 (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>
{{-- 4: 고객사정산 (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>
{{-- 5: 구독관리 (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'
});
}
</script>
@endpush

View File

@@ -0,0 +1,183 @@
{{-- 정산 상세 모달 --}}
<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="closeDetailModal()" 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>
@if ($commission)
<div class="p-6">
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">테넌트</h4>
<p class="text-gray-900">{{ $commission->tenant->name ?? $commission->tenant->company_name ?? '-' }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">상태</h4>
@php
$statusColors = [
'pending' => 'bg-yellow-100 text-yellow-800',
'approved' => 'bg-blue-100 text-blue-800',
'paid' => 'bg-green-100 text-green-800',
'cancelled' => 'bg-red-100 text-red-800',
];
@endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$commission->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $commission->status_label }}
</span>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">입금 구분</h4>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ $commission->payment_type === 'deposit' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' }}">
{{ $commission->payment_type_label }}
</span>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">입금일</h4>
<p class="text-gray-900">{{ $commission->payment_date->format('Y-m-d') }}</p>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-3">금액 정보</h4>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-600">입금액</span>
<span class="font-medium">{{ number_format($commission->payment_amount) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">수당 기준액 (개발비 50%)</span>
<span class="font-medium">{{ number_format($commission->base_amount) }}</span>
</div>
</div>
</div>
<div class="bg-emerald-50 rounded-lg p-4 mb-6">
<h4 class="text-sm font-medium text-emerald-800 mb-3">수당 정보</h4>
<div class="space-y-2">
<div class="flex justify-between items-center">
<div>
<span class="text-gray-700">영업파트너</span>
<span class="text-sm text-gray-500 ml-2">{{ $commission->partner?->user?->name ?? '-' }}</span>
</div>
<div class="text-right">
<span class="text-xs text-gray-500">{{ $commission->partner_rate }}%</span>
<span class="ml-2 font-medium text-emerald-600">{{ number_format($commission->partner_commission) }}</span>
</div>
</div>
<div class="flex justify-between items-center">
<div>
<span class="text-gray-700">매니저</span>
<span class="text-sm text-gray-500 ml-2">{{ $commission->manager?->name ?? '-' }}</span>
</div>
<div class="text-right">
<span class="text-xs text-gray-500">{{ $commission->manager_rate }}%</span>
<span class="ml-2 font-medium text-blue-600">{{ number_format($commission->manager_commission) }}</span>
</div>
</div>
@if ($commission->referrer_partner_id)
<div class="flex justify-between items-center">
<div>
<span class="text-gray-700">유치파트너</span>
<span class="text-sm text-gray-500 ml-2">{{ $commission->referrerPartner?->user?->name ?? '-' }}</span>
</div>
<div class="text-right">
<span class="text-xs text-gray-500">{{ $commission->referrer_rate }}%</span>
<span class="ml-2 font-medium text-orange-600">{{ number_format($commission->referrer_commission) }}</span>
</div>
</div>
@endif
<div class="border-t border-emerald-200 pt-2 mt-2 flex justify-between">
<span class="font-medium text-gray-700"> 수당</span>
<span class="font-bold text-emerald-700">{{ number_format($commission->total_commission) }}</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">지급예정일</h4>
<p class="text-gray-900">{{ $commission->scheduled_payment_date->format('Y-m-d') }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">실제지급일</h4>
<p class="text-gray-900">{{ $commission->actual_payment_date?->format('Y-m-d') ?? '-' }}</p>
</div>
</div>
@if ($commission->approved_at)
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">승인자</h4>
<p class="text-gray-900">{{ $commission->approver?->name ?? '-' }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">승인일시</h4>
<p class="text-gray-900">{{ $commission->approved_at->format('Y-m-d H:i') }}</p>
</div>
</div>
@endif
@if ($commission->bank_reference)
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-500 mb-1">이체 참조번호</h4>
<p class="text-gray-900">{{ $commission->bank_reference }}</p>
</div>
@endif
@if ($commission->notes)
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-500 mb-1">메모</h4>
<p class="text-gray-900">{{ $commission->notes }}</p>
</div>
@endif
@if ($commission->details->count() > 0)
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-3">상품별 수당 내역</h4>
<div class="border rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500">상품</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500">개발비</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500">파트너수당</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500">매니저수당</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach ($commission->details as $detail)
<tr>
<td class="px-4 py-2 text-sm text-gray-900">{{ $detail->contractProduct?->product?->name ?? '-' }}</td>
<td class="px-4 py-2 text-sm text-right text-gray-900">{{ number_format($detail->registration_fee) }}</td>
<td class="px-4 py-2 text-sm text-right text-emerald-600">{{ number_format($detail->partner_commission) }}</td>
<td class="px-4 py-2 text-sm text-right text-blue-600">{{ number_format($detail->manager_commission) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
<div class="flex justify-end gap-2">
@if ($commission->status === 'pending')
<button type="button" onclick="approveCommission({{ $commission->id }})" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">승인</button>
<button type="button" onclick="cancelCommission({{ $commission->id }})" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">취소</button>
@elseif ($commission->status === 'approved')
<button type="button" onclick="markPaidCommission({{ $commission->id }})" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">지급완료</button>
@endif
<button type="button" onclick="closeDetailModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors">닫기</button>
</div>
</div>
@else
<div class="p-6 text-center text-gray-500">
정산 정보를 찾을 없습니다.
</div>
@endif

View File

@@ -0,0 +1,85 @@
{{-- 수당 정산 필터 --}}
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<form id="filter-form" method="GET" action="{{ route('finance.settlement') }}">
<input type="hidden" name="tab" value="commission">
<div class="grid grid-cols-1 md:grid-cols-7 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>
{{-- 수당유형 (NEW) --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">수당유형</label>
<select name="commission_type" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
<option value="">전체</option>
<option value="partner" {{ ($filters['commission_type'] ?? '') == 'partner' ? 'selected' : '' }}>파트너수당</option>
<option value="manager" {{ ($filters['commission_type'] ?? '') == 'manager' ? 'selected' : '' }}>매니저수당</option>
<option value="referrer" {{ ($filters['commission_type'] ?? '') == 'referrer' ? 'selected' : '' }}>유치수당</option>
</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.settlement') }}"
class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors">
초기화
</a>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,163 @@
{{-- 입금 등록 --}}
<form id="payment-form" onsubmit="event.preventDefault(); submitPayment();">
@csrf
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">테넌트 선택 <span class="text-red-500">*</span></label>
@if ($management)
<input type="hidden" name="management_id" value="{{ $management->id }}">
<div class="px-4 py-3 bg-gray-50 rounded-lg">
<div class="font-medium text-gray-900">{{ $management->tenant->name ?? $management->tenant->company_name }}</div>
<div class="text-sm text-gray-500">영업파트너: {{ $management->salesPartner?->user?->name ?? '-' }}</div>
</div>
@else
<select name="management_id"
onchange="onTenantSelect(this.value)"
required
class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
<option value="">-- 테넌트 선택 --</option>
@foreach ($pendingTenants as $tenant)
<option value="{{ $tenant->id }}">
{{ $tenant->tenant->name ?? $tenant->tenant->company_name }}
@if ($tenant->deposit_status === 'pending')
(계약금 대기)
@elseif ($tenant->balance_status === 'pending')
(잔금 대기)
@endif
</option>
@endforeach
</select>
@endif
</div>
@if ($management)
@if ($management->contractProducts->count() > 0)
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">계약 상품</label>
<div class="border rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500">상품명</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500">개발비</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach ($management->contractProducts as $product)
<tr>
<td class="px-4 py-2 text-sm text-gray-900">{{ $product->product?->name ?? '-' }}</td>
<td class="px-4 py-2 text-sm text-right text-gray-900">{{ number_format($product->registration_fee ?? 0) }}</td>
</tr>
@endforeach
</tbody>
<tfoot class="bg-gray-50">
<tr>
<td class="px-4 py-2 text-sm font-medium text-gray-900"> 개발비</td>
<td class="px-4 py-2 text-sm text-right font-bold text-emerald-600">
{{ number_format($management->contractProducts->sum('registration_fee')) }}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
@endif
<div class="mb-4 p-4 bg-gray-50 rounded-lg">
<div class="grid grid-cols-2 gap-4">
<div>
<span class="text-sm text-gray-500">계약금</span>
<div class="font-medium {{ $management->deposit_status === 'paid' ? 'text-green-600' : 'text-yellow-600' }}">
{{ $management->deposit_status === 'paid' ? '입금완료' : '대기' }}
@if ($management->deposit_amount)
({{ number_format($management->deposit_amount) }})
@endif
</div>
</div>
<div>
<span class="text-sm text-gray-500">잔금</span>
<div class="font-medium {{ $management->balance_status === 'paid' ? 'text-green-600' : 'text-yellow-600' }}">
{{ $management->balance_status === 'paid' ? '입금완료' : '대기' }}
@if ($management->balance_amount)
({{ number_format($management->balance_amount) }})
@endif
</div>
</div>
</div>
</div>
@endif
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">입금 구분 <span class="text-red-500">*</span></label>
<div class="flex gap-4">
<label class="inline-flex items-center">
<input type="radio" name="payment_type" value="deposit" required
{{ ($management && $management->deposit_status === 'paid') ? 'disabled' : '' }}
class="text-emerald-600 focus:ring-emerald-500">
<span class="ml-2 text-sm text-gray-700">계약금</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="payment_type" value="balance" required
{{ ($management && $management->balance_status === 'paid') ? 'disabled' : '' }}
class="text-emerald-600 focus:ring-emerald-500">
<span class="ml-2 text-sm text-gray-700">잔금</span>
</label>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">입금액 <span class="text-red-500">*</span></label>
<div class="relative">
<input type="number" name="payment_amount" required min="0" step="1"
@if ($management)
value="{{ $management->contractProducts->sum('registration_fee') / 2 }}"
@endif
class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500 pr-12">
<span class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-500"></span>
</div>
<p class="text-xs text-gray-500 mt-1"> 개발비의 50% 입금받습니다.</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">입금일 <span class="text-red-500">*</span></label>
<input type="date" name="payment_date" required value="{{ now()->format('Y-m-d') }}"
class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
</div>
@if ($management && $management->salesPartner)
@php
$totalFee = $management->contractProducts->sum('registration_fee') ?: 0;
$baseAmount = $totalFee / 2;
$partnerRate = $management->salesPartner->commission_rate ?? 20;
$managerRate = $management->salesPartner->manager_commission_rate ?? 5;
$partnerCommission = $baseAmount * ($partnerRate / 100);
$managerCommission = $management->manager_user_id ? $baseAmount * ($managerRate / 100) : 0;
@endphp
<div class="mb-4 p-4 bg-emerald-50 rounded-lg">
<h4 class="text-sm font-medium text-emerald-800 mb-2">수당 미리보기</h4>
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">기준액 (개발비의 50%)</span>
<span class="font-medium">{{ number_format($baseAmount) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">영업파트너 수당 ({{ $partnerRate }}%)</span>
<span class="font-medium text-emerald-600">{{ number_format($partnerCommission) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">매니저 수당 ({{ $managerRate }}%)</span>
<span class="font-medium text-blue-600">{{ number_format($managerCommission) }}</span>
</div>
<div class="border-t border-emerald-200 pt-1 mt-1 flex justify-between">
<span class="font-medium text-gray-700"> 수당</span>
<span class="font-bold text-emerald-700">{{ number_format($partnerCommission + $managerCommission) }}</span>
</div>
</div>
</div>
@endif
<div class="flex justify-end gap-2 mt-6">
<button type="button" onclick="closePaymentModal()" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors">취소</button>
<button type="submit" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors">입금 등록</button>
</div>
</form>

View File

@@ -0,0 +1,76 @@
{{-- 수당 유형별 통계 카드 --}}
<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-yellow-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">지급 대기</p>
<p class="text-xl font-bold text-yellow-600">{{ number_format($stats['pending']['partner_total'] + $stats['pending']['manager_total']) }}</p>
</div>
<div class="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<div class="text-xs text-gray-400 mt-1">
<span>{{ $stats['pending']['count'] }}</span>
<span class="mx-1">|</span>
<span>파트너: {{ number_format($stats['pending']['partner_total']) }}</span>
<span class="mx-1">/</span>
<span>매니저: {{ number_format($stats['pending']['manager_total']) }}</span>
</div>
</div>
{{-- 승인 완료 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-blue-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">승인 완료</p>
<p class="text-xl font-bold text-blue-600">{{ number_format($stats['approved']['partner_total'] + $stats['approved']['manager_total']) }}</p>
</div>
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ $stats['approved']['count'] }}</p>
</div>
{{-- 지급 완료 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-green-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">지급 완료</p>
<p class="text-xl font-bold text-green-600">{{ number_format($stats['paid']['partner_total'] + $stats['paid']['manager_total']) }}</p>
</div>
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ $stats['paid']['count'] }}</p>
</div>
{{-- 해당 수당 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-purple-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">{{ $year }} {{ $month }} 수당</p>
<p class="text-xl font-bold text-purple-600">{{ number_format($stats['total']['partner_commission'] + $stats['total']['manager_commission']) }}</p>
</div>
<div class="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
</div>
<div class="text-xs text-gray-400 mt-1">
<span>파트너: {{ number_format($stats['total']['partner_commission']) }}</span>
<span class="mx-1">|</span>
<span>매니저: {{ number_format($stats['total']['manager_commission']) }}</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,154 @@
{{-- 수당 정산 테이블 --}}
<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="w-12 px-4 py-3">
<input type="checkbox" onchange="toggleSelectAll(this)" class="rounded border-gray-300 text-emerald-600 focus:ring-emerald-500">
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">테넌트</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">입금구분</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">입금액</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">입금일</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">영업파트너</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">파트너수당</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">매니저</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">매니저수당</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">유치파트너</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">유치수당</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">지급예정일</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">액션</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse ($commissions as $commission)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
@if (in_array($commission->status, ['pending', 'approved']))
<input type="checkbox"
value="{{ $commission->id }}"
onchange="updateSelection()"
class="commission-checkbox rounded border-gray-300 text-emerald-600 focus:ring-emerald-500">
@endif
</td>
<td class="px-4 py-3">
<div class="text-sm font-medium text-gray-900">{{ $commission->tenant->name ?? $commission->tenant->company_name ?? '-' }}</div>
<div class="text-xs text-gray-500">ID: {{ $commission->tenant_id }}</div>
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ $commission->payment_type === 'deposit' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' }}">
{{ $commission->payment_type_label }}
</span>
</td>
<td class="px-4 py-3 text-right text-sm text-gray-900">
{{ number_format($commission->payment_amount) }}
</td>
<td class="px-4 py-3 text-center text-sm text-gray-500">
{{ $commission->payment_date->format('Y-m-d') }}
</td>
<td class="px-4 py-3">
<div class="text-sm text-gray-900">{{ $commission->partner?->user?->name ?? '-' }}</div>
<div class="text-xs text-gray-500">{{ $commission->partner_rate }}%</div>
</td>
<td class="px-4 py-3 text-right text-sm font-medium text-emerald-600">
{{ number_format($commission->partner_commission) }}
</td>
<td class="px-4 py-3">
<div class="text-sm text-gray-900">{{ $commission->manager?->name ?? '-' }}</div>
<div class="text-xs text-gray-500">{{ $commission->manager_rate }}%</div>
</td>
<td class="px-4 py-3 text-right text-sm font-medium text-blue-600">
{{ number_format($commission->manager_commission) }}
</td>
<td class="px-4 py-3">
@if ($commission->referrer_partner_id)
<div class="text-sm text-gray-900">{{ $commission->referrerPartner?->user?->name ?? '-' }}</div>
<div class="text-xs text-gray-500">{{ $commission->referrer_rate }}%</div>
@else
<span class="text-sm text-gray-400">-</span>
@endif
</td>
<td class="px-4 py-3 text-right text-sm font-medium text-orange-600">
@if ($commission->referrer_commission > 0)
{{ number_format($commission->referrer_commission) }}
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-4 py-3 text-center text-sm text-gray-500">
{{ $commission->scheduled_payment_date->format('Y-m-d') }}
</td>
<td class="px-4 py-3 text-center">
@php
$statusColors = [
'pending' => 'bg-yellow-100 text-yellow-800',
'approved' => 'bg-blue-100 text-blue-800',
'paid' => 'bg-green-100 text-green-800',
'cancelled' => 'bg-red-100 text-red-800',
];
@endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$commission->status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $commission->status_label }}
</span>
</td>
<td class="px-4 py-3 text-center">
<div class="flex items-center justify-center gap-1">
<button type="button"
onclick="openDetailModal({{ $commission->id }})"
class="p-1 text-gray-400 hover:text-gray-600"
title="상세보기">
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</button>
@if ($commission->status === 'pending')
<button type="button"
onclick="approveCommission({{ $commission->id }})"
class="p-1 text-blue-400 hover:text-blue-600"
title="승인">
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
<button type="button"
onclick="cancelCommission({{ $commission->id }})"
class="p-1 text-red-400 hover:text-red-600"
title="취소">
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</button>
@elseif ($commission->status === 'approved')
<button type="button"
onclick="markPaidCommission({{ $commission->id }})"
class="p-1 text-green-400 hover:text-green-600"
title="지급완료">
<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="M5 13l4 4L19 7"/>
</svg>
</button>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="14" class="px-4 py-8 text-center text-gray-500">
등록된 정산 내역이 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($commissions->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $commissions->links() }}
</div>
@endif
</div>

View File

@@ -0,0 +1,179 @@
{{-- 컨설팅비용 (Blade + Alpine.js) --}}
<div x-data="consultingManager()" 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="stats.totalHours + '시간'">0시간</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-cyan-500">
<p class="text-sm text-gray-500"> 수수료</p>
<p class="text-xl font-bold text-cyan-600" x-text="formatCurrency(stats.totalAmount) + '원'">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.paidAmount) + '원'">0</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-amber-500">
<p class="text-sm text-gray-500">지급예정</p>
<p class="text-xl font-bold text-amber-600" x-text="formatCurrency(stats.pendingAmount) + '원'">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-cyan-500 focus:ring-cyan-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-cyan-500 focus:ring-cyan-500">
<option value="all">전체</option>
<option value="paid">완료</option>
<option value="pending">예정</option>
</select>
</div>
<button @click="openModal('add')" class="px-4 py-2 bg-cyan-600 hover:bg-cyan-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-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-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.date"></td>
<td class="px-4 py-3 text-sm text-gray-900" x-text="item.consultant"></td>
<td class="px-4 py-3 text-sm text-gray-900" x-text="item.customer"></td>
<td class="px-4 py-3 text-sm text-gray-500" x-text="item.service"></td>
<td class="px-4 py-3 text-sm text-right text-gray-900" x-text="item.hours + 'h'"></td>
<td class="px-4 py-3 text-sm text-right font-medium text-cyan-600" x-text="formatCurrency(item.amount) + '원'"></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 === 'paid' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'"
x-text="item.status === 'paid' ? '완료' : '예정'"></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="date" x-model="form.date" 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.consultant" class="w-full px-3 py-2 border rounded-lg"></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="text" x-model="form.customer" 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.service" class="w-full px-3 py-2 border rounded-lg"></div>
</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.hours" step="0.5" 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.hourlyRate" 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.amount" class="w-full px-3 py-2 border rounded-lg text-right"></div>
</div>
<div class="grid grid-cols-2 gap-4">
<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="paid">완료</option></select>
</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-cyan-600 hover:bg-cyan-700 text-white rounded-lg disabled:opacity-50" x-text="saving ? '저장 중...' : '저장'">저장</button>
</div>
</div>
</div>
</div>
<script>
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();
}
};
}
</script>

View File

@@ -0,0 +1,183 @@
{{-- 고객사정산 (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>

View File

@@ -0,0 +1,118 @@
{{-- 파트너별 현황 --}}
<div class="space-y-6">
{{-- 필터 --}}
<div class="bg-white rounded-lg shadow-sm p-4">
<form hx-get="{{ route('finance.settlement.partner-summary') }}"
hx-target="#partner-content"
hx-trigger="submit"
class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">검색</label>
<input type="text" name="search" placeholder="파트너명 / 파트너코드"
value="{{ request('search') }}"
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 name="type" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option value="">전체</option>
<option value="individual" {{ request('type') == 'individual' ? 'selected' : '' }}>개인</option>
<option value="corporate" {{ request('type') == 'corporate' ? 'selected' : '' }}>단체</option>
</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-indigo-500 focus:ring-indigo-500">
<option value="active" {{ request('status', 'active') == 'active' ? 'selected' : '' }}>활성</option>
<option value="all" {{ request('status') == 'all' ? 'selected' : '' }}>전체</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors">
조회
</button>
</div>
</form>
</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 tracking-wider">파트너명</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">유형</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">수당률</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">계약건수</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">누적 수당</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">미지급</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">지급완료</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">최근 지급일</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">액션</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse ($partners as $partner)
@php
$stats = $commissionStats[$partner->id] ?? null;
$paidTotal = $stats->paid_total ?? 0;
$unpaidTotal = $stats->unpaid_total ?? 0;
$totalCount = $stats->total_count ?? $partner->total_contracts ?? 0;
$lastPaidDate = $stats->last_paid_date ?? null;
@endphp
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
<div class="text-sm font-medium text-gray-900">{{ $partner->user?->name ?? '-' }}</div>
<div class="text-xs text-gray-500">{{ $partner->partner_code }}</div>
</td>
<td class="px-4 py-3 text-center">
@if ($partner->partner_type === 'corporate')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700">단체</span>
@else
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700">개인</span>
@endif
</td>
<td class="px-4 py-3 text-center text-sm text-gray-900">
{{ $partner->commission_rate }}%
</td>
<td class="px-4 py-3 text-center text-sm text-gray-900">
{{ number_format($totalCount) }}
</td>
<td class="px-4 py-3 text-right text-sm font-medium text-gray-900">
{{ number_format($paidTotal + $unpaidTotal) }}
</td>
<td class="px-4 py-3 text-right text-sm font-medium {{ $unpaidTotal > 0 ? 'text-red-600' : 'text-gray-400' }}">
{{ number_format($unpaidTotal) }}
</td>
<td class="px-4 py-3 text-right text-sm font-medium text-green-600">
{{ number_format($paidTotal) }}
</td>
<td class="px-4 py-3 text-center text-sm text-gray-500">
{{ $lastPaidDate ? \Carbon\Carbon::parse($lastPaidDate)->format('Y-m-d') : '-' }}
</td>
<td class="px-4 py-3 text-center">
<a href="{{ route('finance.settlement', ['tab' => 'commission', 'partner_id' => $partner->id]) }}"
class="inline-flex items-center px-2.5 py-1 text-xs font-medium text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded transition-colors">
상세보기
</a>
</td>
</tr>
@empty
<tr>
<td colspan="9" class="px-4 py-8 text-center text-gray-500">
등록된 파트너가 없습니다.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($partners->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $partners->links() }}
</div>
@endif
</div>
</div>

View File

@@ -0,0 +1,202 @@
{{-- 구독관리 (Blade + Alpine.js) --}}
<div x-data="subscriptionManager()" 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="stats.activeCount + '개'">0</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-teal-500">
<p class="text-sm text-gray-500"> 반복 수익(MRR)</p>
<p class="text-xl font-bold text-teal-600" x-text="formatCurrency(stats.monthlyRecurring) + '원'">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"> 반복 수익(ARR)</p>
<p class="text-xl font-bold text-emerald-600" x-text="formatCurrency(stats.yearlyRecurring) + '원'">0</p>
</div>
<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="stats.totalUsers + '명'">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-teal-500 focus:ring-teal-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">플랜</label>
<select x-model="filterPlan" class="rounded-lg border-gray-300 focus:border-teal-500 focus:ring-teal-500">
<option value="all">전체</option>
<option value="Starter">Starter</option>
<option value="Business">Business</option>
<option value="Enterprise">Enterprise</option>
</select>
</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-teal-500 focus:ring-teal-500">
<option value="all">전체</option>
<option value="active">활성</option>
<option value="trial">체험</option>
<option value="cancelled">해지</option>
</select>
</div>
<button @click="openModal('add')" class="px-4 py-2 bg-teal-600 hover:bg-teal-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-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>
<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>
<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">
<div class="text-sm font-medium text-gray-900" x-text="item.customer"></div>
<div x-show="item.memo" class="text-xs text-gray-400" x-text="item.memo"></div>
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-xs font-medium"
:class="{'bg-gray-100 text-gray-700': item.plan === 'Starter', 'bg-blue-100 text-blue-700': item.plan === 'Business', 'bg-purple-100 text-purple-700': item.plan === 'Enterprise'}"
x-text="item.plan"></span>
</td>
<td class="px-4 py-3 text-sm text-right font-medium text-teal-600" x-text="formatCurrency(item.monthlyFee) + '원'"></td>
<td class="px-4 py-3 text-sm text-center text-gray-600" x-text="item.billingCycle === 'monthly' ? '월간' : '연간'"></td>
<td class="px-4 py-3 text-sm text-center text-gray-600" x-text="item.nextBilling || '-'"></td>
<td class="px-4 py-3 text-sm text-center text-gray-600" x-text="item.users + '명'"></td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
:class="{'bg-emerald-100 text-emerald-700': item.status === 'active', 'bg-blue-100 text-blue-700': item.status === 'trial', 'bg-rose-100 text-rose-700': item.status === 'cancelled', 'bg-amber-100 text-amber-700': item.status === 'paused'}"
x-text="{'active':'활성','trial':'체험','cancelled':'해지','paused':'일시정지'}[item.status] || item.status"></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><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-2 gap-4">
<div><label class="block text-sm font-medium text-gray-700 mb-1">플랜</label>
<select x-model="form.plan" class="w-full px-3 py-2 border rounded-lg"><option value="Starter">Starter</option><option value="Business">Business</option><option value="Enterprise">Enterprise</option></select>
</div>
<div><label class="block text-sm font-medium text-gray-700 mb-1"> 요금 *</label><input type="number" x-model="form.monthlyFee" class="w-full px-3 py-2 border rounded-lg text-right"></div>
</div>
<div class="grid grid-cols-2 gap-4">
<div><label class="block text-sm font-medium text-gray-700 mb-1">결제주기</label>
<select x-model="form.billingCycle" class="w-full px-3 py-2 border rounded-lg"><option value="monthly">월간</option><option value="yearly">연간</option></select>
</div>
<div><label class="block text-sm font-medium text-gray-700 mb-1">사용자 </label><input type="number" x-model="form.users" class="w-full px-3 py-2 border rounded-lg text-right"></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.startDate" 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="date" x-model="form.nextBilling" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
<div class="grid grid-cols-2 gap-4">
<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="active">활성</option><option value="trial">체험</option><option value="paused">일시정지</option><option value="cancelled">해지</option></select>
</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-teal-600 hover:bg-teal-700 text-white rounded-lg disabled:opacity-50" x-text="saving ? '저장 중...' : '저장'">저장</button>
</div>
</div>
</div>
</div>
<script>
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();
}
};
}
</script>

View File

@@ -0,0 +1,66 @@
{{-- 통합 통계 카드 ( ) --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{{-- 미지급 수당 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-red-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">미지급 수당</p>
<p class="text-xl font-bold text-red-600">{{ number_format($summaryStats['unpaid_amount']) }}</p>
</div>
<div class="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-400 mt-1">대기 + 승인 상태</p>
</div>
{{-- 승인 대기 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-yellow-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">승인 대기</p>
<p class="text-xl font-bold text-yellow-600">{{ $summaryStats['pending_count'] }}</p>
</div>
<div class="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-400 mt-1">승인 처리 필요</p>
</div>
{{-- 이번달 지급예정 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-blue-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">이번달 지급예정</p>
<p class="text-xl font-bold text-blue-600">{{ number_format($summaryStats['this_month_scheduled']) }}</p>
</div>
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ now()->format('Y년 n월') }} 예정</p>
</div>
{{-- 누적 지급완료 --}}
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-green-500">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">누적 지급완료</p>
<p class="text-xl font-bold text-green-600">{{ number_format($summaryStats['total_paid']) }}</p>
</div>
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-400 mt-1">전체 기간 합계</p>
</div>
</div>

View File

@@ -994,7 +994,16 @@
Route::delete('/{id}', [\App\Http\Controllers\Finance\PurchaseController::class, 'destroy'])->name('destroy');
});
// 영업수수료정산 (실제 구현)
// 통합 정산관리
Route::get('/settlement', [\App\Http\Controllers\Finance\SettlementController::class, 'index'])->name('settlement');
Route::get('/settlement/commission-stats', [\App\Http\Controllers\Finance\SettlementController::class, 'commissionStats'])->name('settlement.commission-stats');
Route::get('/settlement/commission-table', [\App\Http\Controllers\Finance\SettlementController::class, 'commissionTable'])->name('settlement.commission-table');
Route::get('/settlement/partner-summary', [\App\Http\Controllers\Finance\SettlementController::class, 'partnerSummary'])->name('settlement.partner-summary');
Route::get('/settlement/consulting', [\App\Http\Controllers\Finance\SettlementController::class, 'consultingTab'])->name('settlement.consulting');
Route::get('/settlement/customer', [\App\Http\Controllers\Finance\SettlementController::class, 'customerTab'])->name('settlement.customer');
Route::get('/settlement/subscription', [\App\Http\Controllers\Finance\SettlementController::class, 'subscriptionTab'])->name('settlement.subscription');
// 영업수수료정산 (실제 구현 - CRUD API는 그대로 유지)
Route::prefix('sales-commissions')->name('sales-commissions.')->group(function () {
Route::get('/', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'index'])->name('index');
Route::get('/export', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'export'])->name('export');
@@ -1011,15 +1020,9 @@
Route::post('/{id}/cancel', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'cancel'])->name('cancel');
});
// 기존 sales-commission URL 리다이렉트 (호환성)
Route::get('/sales-commission', fn () => redirect()->route('finance.sales-commissions.index'))->name('sales-commission');
Route::get('/consulting-fee', function () {
if (request()->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('finance.consulting-fee'));
}
return view('finance.consulting-fee');
})->name('consulting-fee');
// 기존 URL 리다이렉트 → 통합 정산관리
Route::get('/sales-commission', fn () => redirect()->route('finance.settlement'))->name('sales-commission');
Route::get('/consulting-fee', fn () => redirect()->route('finance.settlement', ['tab' => 'consulting']))->name('consulting-fee');
// 상담수수료 API
Route::prefix('consulting-fees')->name('consulting-fees.')->group(function () {
@@ -1029,13 +1032,7 @@
Route::delete('/{id}', [\App\Http\Controllers\Finance\ConsultingFeeController::class, 'destroy'])->name('destroy');
});
Route::get('/customer-settlement', function () {
if (request()->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('finance.customer-settlement'));
}
return view('finance.customer-settlement');
})->name('customer-settlement');
Route::get('/customer-settlement', fn () => redirect()->route('finance.settlement', ['tab' => 'customer']))->name('customer-settlement');
// 고객사별 정산 API
Route::prefix('customer-settlements')->name('customer-settlements.')->group(function () {
@@ -1045,13 +1042,7 @@
Route::delete('/{id}', [\App\Http\Controllers\Finance\CustomerSettlementController::class, 'destroy'])->name('destroy');
});
Route::get('/subscription', function () {
if (request()->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('finance.subscription'));
}
return view('finance.subscription');
})->name('subscription');
Route::get('/subscription', fn () => redirect()->route('finance.settlement', ['tab' => 'subscription']))->name('subscription');
// 구독 관리 API
Route::prefix('subscriptions')->name('subscriptions.')->group(function () {