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:
222
app/Http/Controllers/Finance/SettlementController.php
Normal file
222
app/Http/Controllers/Finance/SettlementController.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 이 단체를 유치한 영업파트너
|
||||
*/
|
||||
|
||||
@@ -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'];
|
||||
|
||||
372
resources/views/finance/settlement/index.blade.php
Normal file
372
resources/views/finance/settlement/index.blade.php
Normal 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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user