refactor:고객사정산 탭 재설계 (실 테넌트 데이터 기반)
- customerTab() 메서드: SalesTenantManagement 기반 쿼리로 재작성 - getCustomerStats() private 메서드 추가 (총개발비/수금완료/미수금/개발진행/구독전환) - customer-tab.blade.php: Alpine.js CRUD → 순수 Blade 테이블로 전체 교체 - index.blade.php: 미사용 customerSettlementManager() Alpine 함수 제거 - 필터: 검색/개발상태/수금상태/담당파트너 4종 - 테이블: 고객사/파트너/매니저/개발비/1차/2차/구독료/개발상태 8열 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sales\SalesCommission;
|
||||
use App\Models\Sales\SalesContractProduct;
|
||||
use App\Models\Sales\SalesPartner;
|
||||
use App\Models\Sales\SalesTenantManagement;
|
||||
use App\Services\SalesCommissionService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -169,7 +171,91 @@ public function consultingTab(Request $request): View
|
||||
*/
|
||||
public function customerTab(Request $request): View
|
||||
{
|
||||
return view('finance.settlement.partials.customer-tab');
|
||||
$query = SalesTenantManagement::with([
|
||||
'tenant',
|
||||
'tenantProspect',
|
||||
'salesPartner.user',
|
||||
'manager',
|
||||
'commissions',
|
||||
])->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING);
|
||||
|
||||
// 필터: 검색 (회사명)
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->whereHas('tenant', fn ($tq) => $tq->where('company_name', 'like', "%{$search}%"))
|
||||
->orWhereHas('tenantProspect', fn ($pq) => $pq->where('company_name', 'like', "%{$search}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// 필터: 개발 상태
|
||||
if ($hqStatus = $request->input('hq_status')) {
|
||||
$query->where('hq_status', $hqStatus);
|
||||
}
|
||||
|
||||
// 필터: 담당 파트너
|
||||
if ($partnerId = $request->input('partner_id')) {
|
||||
$query->where('sales_partner_id', $partnerId);
|
||||
}
|
||||
|
||||
// 필터: 수금 상태
|
||||
$paymentStatus = $request->input('payment_status');
|
||||
|
||||
$managements = $query->orderByDesc('id')->paginate(20)->withQueryString();
|
||||
|
||||
// 수금 상태 필터 (컬렉션 레벨)
|
||||
if ($paymentStatus) {
|
||||
$managements->setCollection(
|
||||
$managements->getCollection()->filter(function ($mgmt) use ($paymentStatus) {
|
||||
$depositPaid = $mgmt->deposit_status === 'paid';
|
||||
$balancePaid = $mgmt->balance_status === 'paid';
|
||||
return match ($paymentStatus) {
|
||||
'fully_paid' => $depositPaid && $balancePaid,
|
||||
'partial' => ($depositPaid || $balancePaid) && !($depositPaid && $balancePaid),
|
||||
'unpaid' => !$depositPaid && !$balancePaid,
|
||||
default => true,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 구독료 일괄 조회 (N+1 방지)
|
||||
$tenantIds = $managements->getCollection()
|
||||
->pluck('tenant_id')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
$subscriptionFees = [];
|
||||
if (!empty($tenantIds)) {
|
||||
$subscriptionFees = SalesContractProduct::whereIn('tenant_id', $tenantIds)
|
||||
->selectRaw('tenant_id, SUM(subscription_fee) as total_subscription_fee')
|
||||
->groupBy('tenant_id')
|
||||
->pluck('total_subscription_fee', 'tenant_id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
// 파트너 목록 (필터용)
|
||||
$partners = SalesPartner::with('user')
|
||||
->active()
|
||||
->orderBy('partner_code')
|
||||
->get();
|
||||
|
||||
// hqStatusLabels에서 pending 제외
|
||||
$hqStatusLabels = collect(SalesTenantManagement::$hqStatusLabels)
|
||||
->except(SalesTenantManagement::HQ_STATUS_PENDING)
|
||||
->toArray();
|
||||
|
||||
// 통계 카드
|
||||
$customerStats = $this->getCustomerStats();
|
||||
|
||||
return view('finance.settlement.partials.customer-tab', compact(
|
||||
'managements',
|
||||
'subscriptionFees',
|
||||
'partners',
|
||||
'hqStatusLabels',
|
||||
'customerStats',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,6 +403,48 @@ public function paymentStats(Request $request): View|Response
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객사정산 통계 데이터
|
||||
*/
|
||||
private function getCustomerStats(): array
|
||||
{
|
||||
$baseQuery = SalesTenantManagement::where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING);
|
||||
|
||||
// 총 개발비
|
||||
$totalFee = (clone $baseQuery)->sum('total_registration_fee');
|
||||
$totalCount = (clone $baseQuery)->count();
|
||||
|
||||
// 수금완료 (deposit + balance 모두 paid인 건의 합계)
|
||||
$collectedAmount = (clone $baseQuery)
|
||||
->where('deposit_status', 'paid')
|
||||
->sum('deposit_amount')
|
||||
+ (clone $baseQuery)
|
||||
->where('balance_status', 'paid')
|
||||
->sum('balance_amount');
|
||||
|
||||
// 미수금
|
||||
$uncollectedAmount = $totalFee - $collectedAmount;
|
||||
|
||||
// 개발 진행 중 (handover 제외)
|
||||
$inProgressCount = (clone $baseQuery)
|
||||
->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_HANDOVER)
|
||||
->count();
|
||||
|
||||
// 구독 전환 (handover + tenant active)
|
||||
$subscriptionCount = SalesTenantManagement::where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER)
|
||||
->whereHas('tenant', fn ($q) => $q->whereNull('deleted_at'))
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total_fee' => $totalFee,
|
||||
'total_count' => $totalCount,
|
||||
'collected_amount' => $collectedAmount,
|
||||
'uncollected_amount' => max(0, $uncollectedAmount),
|
||||
'in_progress_count' => $inProgressCount,
|
||||
'subscription_count' => $subscriptionCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 통계 데이터
|
||||
*/
|
||||
|
||||
@@ -369,54 +369,6 @@ function onTenantSelect(managementId) {
|
||||
});
|
||||
}
|
||||
|
||||
// 고객사정산 탭 Alpine 컴포넌트
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 구독관리 탭 Alpine 컴포넌트
|
||||
function subscriptionManager() {
|
||||
return {
|
||||
|
||||
@@ -1,44 +1,77 @@
|
||||
{{-- 고객사정산 탭 (Blade + Alpine.js) --}}
|
||||
<div x-data="customerSettlementManager()" x-init="fetchData()">
|
||||
{{-- 고객사정산 탭 (실 테넌트 데이터 기반) --}}
|
||||
<div class="space-y-6">
|
||||
{{-- 통계 카드 --}}
|
||||
<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="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<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>
|
||||
<p class="text-sm text-gray-500">총 개발비</p>
|
||||
<p class="text-xl font-bold text-indigo-600">{{ number_format($customerStats['total_fee']) }}원</p>
|
||||
<p class="text-xs text-gray-400 mt-1">{{ $customerStats['total_count'] }}건</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-green-500">
|
||||
<p class="text-sm text-gray-500">수금완료</p>
|
||||
<p class="text-xl font-bold text-green-600">{{ number_format($customerStats['collected_amount']) }}원</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-red-500">
|
||||
<p class="text-sm text-gray-500">미수금</p>
|
||||
<p class="text-xl font-bold text-red-600">{{ number_format($customerStats['uncollected_amount']) }}원</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-blue-500">
|
||||
<p class="text-sm text-gray-500">개발 진행 중</p>
|
||||
<p class="text-xl font-bold text-blue-600">{{ $customerStats['in_progress_count'] }}건</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>
|
||||
<p class="text-sm text-gray-500">구독 전환</p>
|
||||
<p class="text-xl font-bold text-emerald-600">{{ $customerStats['subscription_count'] }}건</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]">
|
||||
{{-- 필터 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<form hx-get="{{ route('finance.settlement.customer') }}"
|
||||
hx-target="#customer-content"
|
||||
hx-trigger="submit"
|
||||
class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<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">
|
||||
<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 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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">개발 상태</label>
|
||||
<select name="hq_status" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">전체</option>
|
||||
@foreach ($hqStatusLabels as $value => $label)
|
||||
<option value="{{ $value }}" {{ request('hq_status') == $value ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">수금 상태</label>
|
||||
<select name="payment_status" class="w-full rounded-lg border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">전체</option>
|
||||
<option value="fully_paid" {{ request('payment_status') == 'fully_paid' ? 'selected' : '' }}>수금완료</option>
|
||||
<option value="partial" {{ request('payment_status') == 'partial' ? 'selected' : '' }}>부분수금</option>
|
||||
<option value="unpaid" {{ request('payment_status') == 'unpaid' ? '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-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">전체</option>
|
||||
@foreach ($partners as $partner)
|
||||
<option value="{{ $partner->id }}" {{ request('partner_id') == $partner->id ? 'selected' : '' }}>
|
||||
{{ $partner->user?->name ?? $partner->partner_code }}
|
||||
</option>
|
||||
@endforeach
|
||||
</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>
|
||||
|
||||
{{-- 테이블 --}}
|
||||
@@ -47,89 +80,136 @@
|
||||
<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>
|
||||
<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-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-right text-xs font-medium text-gray-500 uppercase tracking-wider">1차(계약금)</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">2차(잔금)</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>
|
||||
</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">
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse ($managements as $mgmt)
|
||||
@php
|
||||
$companyName = $mgmt->tenant->company_name ?? $mgmt->tenantProspect->company_name ?? '-';
|
||||
$tenantActive = $mgmt->tenant && !$mgmt->tenant->trashed();
|
||||
$monthlyFee = $subscriptionFees[$mgmt->tenant_id] ?? 0;
|
||||
$firstSubscriptionAt = $mgmt->commissions->first()?->first_subscription_at;
|
||||
@endphp
|
||||
<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 class="px-4 py-3">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $companyName }}</div>
|
||||
@if ($tenantActive)
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">활성</span>
|
||||
@elseif ($mgmt->tenant)
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">비활성</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700">가망</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- 담당파트너 --}}
|
||||
<td class="px-4 py-3 text-sm text-gray-900">
|
||||
{{ $mgmt->salesPartner?->user?->name ?? '-' }}
|
||||
</td>
|
||||
|
||||
{{-- 담당매니저 --}}
|
||||
<td class="px-4 py-3 text-sm text-gray-900">
|
||||
{{ $mgmt->manager?->name ?? '-' }}
|
||||
</td>
|
||||
|
||||
{{-- 개발비 총액 --}}
|
||||
<td class="px-4 py-3 text-sm text-right font-medium text-gray-900">
|
||||
@if ($mgmt->total_registration_fee > 0)
|
||||
{{ number_format($mgmt->total_registration_fee) }}원
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- 1차(계약금) --}}
|
||||
<td class="px-4 py-3 text-sm text-right">
|
||||
@if ($mgmt->deposit_amount > 0)
|
||||
<div class="font-medium text-gray-900">{{ number_format($mgmt->deposit_amount) }}원</div>
|
||||
@if ($mgmt->deposit_status === 'paid')
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">완료</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700">대기</span>
|
||||
@endif
|
||||
@if ($mgmt->deposit_paid_date)
|
||||
<div class="text-xs text-gray-400 mt-0.5">{{ $mgmt->deposit_paid_date->format('Y-m-d') }}</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- 2차(잔금) --}}
|
||||
<td class="px-4 py-3 text-sm text-right">
|
||||
@if ($mgmt->balance_amount > 0)
|
||||
<div class="font-medium text-gray-900">{{ number_format($mgmt->balance_amount) }}원</div>
|
||||
@if ($mgmt->balance_status === 'paid')
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">완료</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-700">대기</span>
|
||||
@endif
|
||||
@if ($mgmt->balance_paid_date)
|
||||
<div class="text-xs text-gray-400 mt-0.5">{{ $mgmt->balance_paid_date->format('Y-m-d') }}</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- 구독료 --}}
|
||||
<td class="px-4 py-3 text-sm text-right">
|
||||
@if ($monthlyFee > 0)
|
||||
<div class="font-medium text-gray-900">월 {{ number_format($monthlyFee) }}원</div>
|
||||
@if ($firstSubscriptionAt)
|
||||
<div class="text-xs text-gray-400 mt-0.5">첫입금 {{ \Carbon\Carbon::parse($firstSubscriptionAt)->format('Y-m-d') }}</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</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>
|
||||
@php
|
||||
$statusColor = match ($mgmt->hq_status) {
|
||||
'review' => 'bg-purple-100 text-purple-700',
|
||||
'planning' => 'bg-blue-100 text-blue-700',
|
||||
'coding' => 'bg-indigo-100 text-indigo-700',
|
||||
'dev_test' => 'bg-cyan-100 text-cyan-700',
|
||||
'dev_done' => 'bg-teal-100 text-teal-700',
|
||||
'int_test' => 'bg-amber-100 text-amber-700',
|
||||
'handover' => 'bg-green-100 text-green-700',
|
||||
default => 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
@endphp
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium {{ $statusColor }}">
|
||||
{{ $mgmt->hq_status_label }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-gray-500">
|
||||
개발이 시작된 고객사가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</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>
|
||||
@if ($managements->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-200">
|
||||
{{ $managements->links() }}
|
||||
</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>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user