feat:영업수수료 정산 기능 구현
[모델] - SalesCommission: 영업수수료 정산 모델 - SalesCommissionDetail: 상품별 수당 내역 모델 - SalesTenantManagement: 입금 정보 필드 추가 [서비스/컨트롤러] - SalesCommissionService: 수당 생성, 승인, 지급 처리 로직 - SalesCommissionController: 정산 관리 CRUD [뷰] - 본사 정산 관리 화면 (필터, 통계, 테이블) - 입금 등록 모달 - 상세 보기 모달 - 영업파트너 대시보드 수당 카드 [라우트] - /finance/sales-commissions/* 라우트 추가 - 기존 sales-commission 리다이렉트 호환 [메뉴] - SalesCommissionMenuSeeder: 정산관리 > 영업수수료정산 메뉴 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
381
app/Http/Controllers/Finance/SalesCommissionController.php
Normal file
381
app/Http/Controllers/Finance/SalesCommissionController.php
Normal file
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sales\SalesCommission;
|
||||
use App\Models\Sales\SalesPartner;
|
||||
use App\Models\Sales\SalesTenantManagement;
|
||||
use App\Services\SalesCommissionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 영업수수료 정산 컨트롤러
|
||||
*/
|
||||
class SalesCommissionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private SalesCommissionService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 정산 목록
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
// HTMX 요청 시 전체 페이지로 리다이렉트 (JavaScript 필요)
|
||||
if ($request->header('HX-Request') && !$request->header('HX-Boosted')) {
|
||||
return response('', 200)->header('HX-Redirect', route('finance.sales-commissions.index'));
|
||||
}
|
||||
|
||||
// 필터 파라미터
|
||||
$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'),
|
||||
'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();
|
||||
|
||||
return view('finance.sales-commission.index', compact(
|
||||
'commissions',
|
||||
'stats',
|
||||
'partners',
|
||||
'pendingTenants',
|
||||
'year',
|
||||
'month',
|
||||
'filters'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 정산 상세 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$commission = $this->service->getCommissionById($id);
|
||||
|
||||
if (!$commission) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '정산 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $commission,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 정산 상세 모달 (HTMX)
|
||||
*/
|
||||
public function detail(int $id): View
|
||||
{
|
||||
$commission = $this->service->getCommissionById($id);
|
||||
|
||||
return view('finance.sales-commission.partials.detail-modal', compact('commission'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입금 등록 (수당 생성)
|
||||
*/
|
||||
public function registerPayment(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'management_id' => 'required|integer|exists:sales_tenant_managements,id',
|
||||
'payment_type' => 'required|in:deposit,balance',
|
||||
'payment_amount' => 'required|numeric|min:0',
|
||||
'payment_date' => 'required|date',
|
||||
]);
|
||||
|
||||
try {
|
||||
$commission = $this->service->createCommission(
|
||||
$validated['management_id'],
|
||||
$validated['payment_type'],
|
||||
$validated['payment_amount'],
|
||||
$validated['payment_date']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '입금이 등록되었습니다.',
|
||||
'data' => $commission,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인 처리
|
||||
*/
|
||||
public function approve(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$commission = $this->service->approve($id, auth()->id());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '승인되었습니다.',
|
||||
'data' => $commission,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 승인
|
||||
*/
|
||||
public function bulkApprove(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'integer|exists:sales_commissions,id',
|
||||
]);
|
||||
|
||||
try {
|
||||
$count = $this->service->bulkApprove($validated['ids'], auth()->id());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "{$count}건이 승인되었습니다.",
|
||||
'count' => $count,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지급완료 처리
|
||||
*/
|
||||
public function markPaid(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'bank_reference' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
try {
|
||||
$commission = $this->service->markAsPaid($id, $validated['bank_reference'] ?? null);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '지급완료 처리되었습니다.',
|
||||
'data' => $commission,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 지급완료
|
||||
*/
|
||||
public function bulkMarkPaid(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'integer|exists:sales_commissions,id',
|
||||
'bank_reference' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
try {
|
||||
$count = $this->service->bulkMarkAsPaid($validated['ids'], $validated['bank_reference'] ?? null);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "{$count}건이 지급완료 처리되었습니다.",
|
||||
'count' => $count,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 취소 처리
|
||||
*/
|
||||
public function cancel(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$commission = $this->service->cancel($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '취소되었습니다.',
|
||||
'data' => $commission,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 정산 테이블 부분 새로고침 (HTMX)
|
||||
*/
|
||||
public function table(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'),
|
||||
'search' => $request->input('search'),
|
||||
];
|
||||
|
||||
$commissions = $this->service->getCommissions($filters);
|
||||
|
||||
return view('finance.sales-commission.partials.commission-table', compact('commissions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 카드 부분 새로고침 (HTMX)
|
||||
*/
|
||||
public function stats(Request $request): View
|
||||
{
|
||||
$year = $request->input('year', now()->year);
|
||||
$month = $request->input('month', now()->month);
|
||||
|
||||
$stats = $this->service->getSettlementStats($year, $month);
|
||||
|
||||
return view('finance.sales-commission.partials.stats-cards', compact('stats', 'year', 'month'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 입금 등록 폼 (HTMX 모달)
|
||||
*/
|
||||
public function paymentForm(Request $request): View
|
||||
{
|
||||
$managementId = $request->input('management_id');
|
||||
$management = null;
|
||||
|
||||
if ($managementId) {
|
||||
$management = SalesTenantManagement::with(['tenant', 'salesPartner.user', 'contractProducts.product'])
|
||||
->find($managementId);
|
||||
}
|
||||
|
||||
// 입금 대기 테넌트 목록
|
||||
$pendingTenants = $this->service->getPendingPaymentTenants();
|
||||
|
||||
return view('finance.sales-commission.partials.payment-form', compact('management', 'pendingTenants'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀 다운로드
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$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'),
|
||||
];
|
||||
|
||||
// 전체 데이터 조회 (페이지네이션 없이)
|
||||
$commissions = SalesCommission::query()
|
||||
->with(['tenant', 'partner.user', 'manager'])
|
||||
->when(!empty($filters['status']), fn($q) => $q->where('status', $filters['status']))
|
||||
->when(!empty($filters['payment_type']), fn($q) => $q->where('payment_type', $filters['payment_type']))
|
||||
->when(!empty($filters['partner_id']), fn($q) => $q->where('partner_id', $filters['partner_id']))
|
||||
->forScheduledMonth($year, $month)
|
||||
->orderBy('scheduled_payment_date')
|
||||
->get();
|
||||
|
||||
// CSV 생성
|
||||
$filename = "sales_commission_{$year}_{$month}.csv";
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
];
|
||||
|
||||
$callback = function () use ($commissions) {
|
||||
$file = fopen('php://output', 'w');
|
||||
|
||||
// BOM for UTF-8
|
||||
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
||||
|
||||
// 헤더
|
||||
fputcsv($file, [
|
||||
'번호', '테넌트', '입금구분', '입금액', '입금일',
|
||||
'기준액', '영업파트너', '파트너수당', '매니저', '매니저수당',
|
||||
'지급예정일', '상태', '실제지급일'
|
||||
]);
|
||||
|
||||
// 데이터
|
||||
foreach ($commissions as $commission) {
|
||||
fputcsv($file, [
|
||||
$commission->id,
|
||||
$commission->tenant->name ?? $commission->tenant->company_name,
|
||||
$commission->payment_type_label,
|
||||
number_format($commission->payment_amount),
|
||||
$commission->payment_date->format('Y-m-d'),
|
||||
number_format($commission->base_amount),
|
||||
$commission->partner?->user?->name ?? '-',
|
||||
number_format($commission->partner_commission),
|
||||
$commission->manager?->name ?? '-',
|
||||
number_format($commission->manager_commission),
|
||||
$commission->scheduled_payment_date->format('Y-m-d'),
|
||||
$commission->status_label,
|
||||
$commission->actual_payment_date?->format('Y-m-d') ?? '-',
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace App\Http\Controllers\Sales;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sales\SalesPartner;
|
||||
use App\Models\Sales\SalesTenantManagement;
|
||||
use App\Models\Sales\TenantProspect;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\SalesCommissionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -16,6 +18,10 @@
|
||||
*/
|
||||
class SalesDashboardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private SalesCommissionService $commissionService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 대시보드 화면
|
||||
*/
|
||||
@@ -23,6 +29,9 @@ public function index(Request $request): View
|
||||
{
|
||||
$data = $this->getDashboardData($request);
|
||||
|
||||
// 영업파트너 수당 정보 추가
|
||||
$data = array_merge($data, $this->getCommissionData());
|
||||
|
||||
return view('sales.dashboard.index', $data);
|
||||
}
|
||||
|
||||
@@ -235,4 +244,26 @@ public function getManagers(Request $request): JsonResponse
|
||||
'managers' => $managers,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업파트너 수당 정보 조회
|
||||
*/
|
||||
private function getCommissionData(): array
|
||||
{
|
||||
$user = auth()->user();
|
||||
$commissionSummary = [];
|
||||
$recentCommissions = collect();
|
||||
|
||||
// 현재 사용자가 영업파트너인지 확인
|
||||
$partner = SalesPartner::where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
|
||||
if ($partner) {
|
||||
$commissionSummary = $this->commissionService->getPartnerCommissionSummary($partner->id);
|
||||
$recentCommissions = $this->commissionService->getRecentCommissions($partner->id, 5);
|
||||
}
|
||||
|
||||
return compact('commissionSummary', 'recentCommissions', 'partner');
|
||||
}
|
||||
}
|
||||
|
||||
293
app/Models/Sales/SalesCommission.php
Normal file
293
app/Models/Sales/SalesCommission.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Sales;
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 영업수수료 정산 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $management_id
|
||||
* @property string $payment_type
|
||||
* @property float $payment_amount
|
||||
* @property string $payment_date
|
||||
* @property float $base_amount
|
||||
* @property float $partner_rate
|
||||
* @property float $manager_rate
|
||||
* @property float $partner_commission
|
||||
* @property float $manager_commission
|
||||
* @property string $scheduled_payment_date
|
||||
* @property string $status
|
||||
* @property string|null $actual_payment_date
|
||||
* @property int $partner_id
|
||||
* @property int|null $manager_user_id
|
||||
* @property string|null $notes
|
||||
* @property string|null $bank_reference
|
||||
* @property int|null $approved_by
|
||||
* @property \Carbon\Carbon|null $approved_at
|
||||
*/
|
||||
class SalesCommission extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'sales_commissions';
|
||||
|
||||
/**
|
||||
* 상태 상수
|
||||
*/
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_APPROVED = 'approved';
|
||||
const STATUS_PAID = 'paid';
|
||||
const STATUS_CANCELLED = 'cancelled';
|
||||
|
||||
/**
|
||||
* 입금 구분 상수
|
||||
*/
|
||||
const PAYMENT_DEPOSIT = 'deposit';
|
||||
const PAYMENT_BALANCE = 'balance';
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
public static array $statusLabels = [
|
||||
self::STATUS_PENDING => '대기',
|
||||
self::STATUS_APPROVED => '승인',
|
||||
self::STATUS_PAID => '지급완료',
|
||||
self::STATUS_CANCELLED => '취소',
|
||||
];
|
||||
|
||||
/**
|
||||
* 입금 구분 라벨
|
||||
*/
|
||||
public static array $paymentTypeLabels = [
|
||||
self::PAYMENT_DEPOSIT => '계약금',
|
||||
self::PAYMENT_BALANCE => '잔금',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'management_id',
|
||||
'payment_type',
|
||||
'payment_amount',
|
||||
'payment_date',
|
||||
'base_amount',
|
||||
'partner_rate',
|
||||
'manager_rate',
|
||||
'partner_commission',
|
||||
'manager_commission',
|
||||
'scheduled_payment_date',
|
||||
'status',
|
||||
'actual_payment_date',
|
||||
'partner_id',
|
||||
'manager_user_id',
|
||||
'notes',
|
||||
'bank_reference',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'payment_amount' => 'decimal:2',
|
||||
'base_amount' => 'decimal:2',
|
||||
'partner_rate' => 'decimal:2',
|
||||
'manager_rate' => 'decimal:2',
|
||||
'partner_commission' => 'decimal:2',
|
||||
'manager_commission' => 'decimal:2',
|
||||
'payment_date' => 'date',
|
||||
'scheduled_payment_date' => 'date',
|
||||
'actual_payment_date' => 'date',
|
||||
'approved_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 테넌트 관계
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업관리 관계
|
||||
*/
|
||||
public function management(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SalesTenantManagement::class, 'management_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 영업파트너 관계
|
||||
*/
|
||||
public function partner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SalesPartner::class, 'partner_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 매니저(사용자) 관계
|
||||
*/
|
||||
public function manager(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'manager_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상세 내역 관계
|
||||
*/
|
||||
public function details(): HasMany
|
||||
{
|
||||
return $this->hasMany(SalesCommissionDetail::class, 'commission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인자 관계
|
||||
*/
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨 Accessor
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::$statusLabels[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 입금 구분 라벨 Accessor
|
||||
*/
|
||||
public function getPaymentTypeLabelAttribute(): string
|
||||
{
|
||||
return self::$paymentTypeLabels[$this->payment_type] ?? $this->payment_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 총 수당액 Accessor
|
||||
*/
|
||||
public function getTotalCommissionAttribute(): float
|
||||
{
|
||||
return $this->partner_commission + $this->manager_commission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지급예정일 계산 (입금일 익월 10일)
|
||||
*/
|
||||
public static function calculateScheduledPaymentDate(Carbon $paymentDate): Carbon
|
||||
{
|
||||
return $paymentDate->copy()->addMonth()->day(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인 처리
|
||||
*/
|
||||
public function approve(int $approverId): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->update([
|
||||
'status' => self::STATUS_APPROVED,
|
||||
'approved_by' => $approverId,
|
||||
'approved_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지급완료 처리
|
||||
*/
|
||||
public function markAsPaid(?string $bankReference = null): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_APPROVED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->update([
|
||||
'status' => self::STATUS_PAID,
|
||||
'actual_payment_date' => now()->format('Y-m-d'),
|
||||
'bank_reference' => $bankReference,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 취소 처리
|
||||
*/
|
||||
public function cancel(): bool
|
||||
{
|
||||
if ($this->status === self::STATUS_PAID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->update([
|
||||
'status' => self::STATUS_CANCELLED,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대기 상태 스코프
|
||||
*/
|
||||
public function scopePending(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', self::STATUS_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인완료 스코프
|
||||
*/
|
||||
public function scopeApproved(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지급완료 스코프
|
||||
*/
|
||||
public function scopePaid(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', self::STATUS_PAID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 영업파트너 스코프
|
||||
*/
|
||||
public function scopeForPartner(Builder $query, int $partnerId): Builder
|
||||
{
|
||||
return $query->where('partner_id', $partnerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 매니저 스코프
|
||||
*/
|
||||
public function scopeForManager(Builder $query, int $managerUserId): Builder
|
||||
{
|
||||
return $query->where('manager_user_id', $managerUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 월 지급예정 스코프
|
||||
*/
|
||||
public function scopeForScheduledMonth(Builder $query, int $year, int $month): Builder
|
||||
{
|
||||
return $query->whereYear('scheduled_payment_date', $year)
|
||||
->whereMonth('scheduled_payment_date', $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기간 입금 스코프
|
||||
*/
|
||||
public function scopePaymentDateBetween(Builder $query, string $startDate, string $endDate): Builder
|
||||
{
|
||||
return $query->whereBetween('payment_date', [$startDate, $endDate]);
|
||||
}
|
||||
}
|
||||
68
app/Models/Sales/SalesCommissionDetail.php
Normal file
68
app/Models/Sales/SalesCommissionDetail.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Sales;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 영업수수료 상세 모델 (상품별 수당 내역)
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $commission_id
|
||||
* @property int $contract_product_id
|
||||
* @property float $registration_fee
|
||||
* @property float $base_amount
|
||||
* @property float $partner_rate
|
||||
* @property float $manager_rate
|
||||
* @property float $partner_commission
|
||||
* @property float $manager_commission
|
||||
*/
|
||||
class SalesCommissionDetail extends Model
|
||||
{
|
||||
protected $table = 'sales_commission_details';
|
||||
|
||||
protected $fillable = [
|
||||
'commission_id',
|
||||
'contract_product_id',
|
||||
'registration_fee',
|
||||
'base_amount',
|
||||
'partner_rate',
|
||||
'manager_rate',
|
||||
'partner_commission',
|
||||
'manager_commission',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'registration_fee' => 'decimal:2',
|
||||
'base_amount' => 'decimal:2',
|
||||
'partner_rate' => 'decimal:2',
|
||||
'manager_rate' => 'decimal:2',
|
||||
'partner_commission' => 'decimal:2',
|
||||
'manager_commission' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* 수수료 정산 관계
|
||||
*/
|
||||
public function commission(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SalesCommission::class, 'commission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 상품 관계
|
||||
*/
|
||||
public function contractProduct(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SalesContractProduct::class, 'contract_product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 총 수당액 Accessor
|
||||
*/
|
||||
public function getTotalCommissionAttribute(): float
|
||||
{
|
||||
return $this->partner_commission + $this->manager_commission;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,14 @@ class SalesTenantManagement extends Model
|
||||
'hq_status',
|
||||
'incentive_status',
|
||||
'notes',
|
||||
// 입금 정보
|
||||
'deposit_amount',
|
||||
'deposit_paid_date',
|
||||
'deposit_status',
|
||||
'balance_amount',
|
||||
'balance_paid_date',
|
||||
'balance_status',
|
||||
'total_registration_fee',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -76,6 +84,12 @@ class SalesTenantManagement extends Model
|
||||
'onboarding_completed_at' => 'datetime',
|
||||
'membership_paid_at' => 'datetime',
|
||||
'commission_paid_at' => 'datetime',
|
||||
// 입금 정보
|
||||
'deposit_amount' => 'decimal:2',
|
||||
'deposit_paid_date' => 'date',
|
||||
'balance_amount' => 'decimal:2',
|
||||
'balance_paid_date' => 'date',
|
||||
'total_registration_fee' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -198,6 +212,22 @@ public function consultations(): HasMany
|
||||
return $this->hasMany(SalesConsultation::class, 'tenant_id', 'tenant_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수수료 정산 관계
|
||||
*/
|
||||
public function commissions(): HasMany
|
||||
{
|
||||
return $this->hasMany(SalesCommission::class, 'management_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 상품 관계
|
||||
*/
|
||||
public function contractProducts(): HasMany
|
||||
{
|
||||
return $this->hasMany(SalesContractProduct::class, 'management_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 ID로 조회 또는 생성
|
||||
*/
|
||||
|
||||
449
app/Services/SalesCommissionService.php
Normal file
449
app/Services/SalesCommissionService.php
Normal file
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Sales\SalesCommission;
|
||||
use App\Models\Sales\SalesCommissionDetail;
|
||||
use App\Models\Sales\SalesContractProduct;
|
||||
use App\Models\Sales\SalesPartner;
|
||||
use App\Models\Sales\SalesTenantManagement;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SalesCommissionService
|
||||
{
|
||||
/**
|
||||
* 기본 수당률
|
||||
*/
|
||||
const DEFAULT_PARTNER_RATE = 20.00;
|
||||
const DEFAULT_MANAGER_RATE = 5.00;
|
||||
|
||||
// =========================================================================
|
||||
// 정산 목록 조회
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 정산 목록 조회 (페이지네이션)
|
||||
*/
|
||||
public function getCommissions(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$query = SalesCommission::query()
|
||||
->with(['tenant', 'partner.user', 'manager', 'management']);
|
||||
|
||||
// 상태 필터
|
||||
if (!empty($filters['status'])) {
|
||||
$query->where('status', $filters['status']);
|
||||
}
|
||||
|
||||
// 입금구분 필터
|
||||
if (!empty($filters['payment_type'])) {
|
||||
$query->where('payment_type', $filters['payment_type']);
|
||||
}
|
||||
|
||||
// 영업파트너 필터
|
||||
if (!empty($filters['partner_id'])) {
|
||||
$query->where('partner_id', $filters['partner_id']);
|
||||
}
|
||||
|
||||
// 매니저 필터
|
||||
if (!empty($filters['manager_user_id'])) {
|
||||
$query->where('manager_user_id', $filters['manager_user_id']);
|
||||
}
|
||||
|
||||
// 지급예정 년/월 필터
|
||||
if (!empty($filters['scheduled_year']) && !empty($filters['scheduled_month'])) {
|
||||
$query->forScheduledMonth((int) $filters['scheduled_year'], (int) $filters['scheduled_month']);
|
||||
}
|
||||
|
||||
// 입금일 기간 필터
|
||||
if (!empty($filters['payment_start_date']) && !empty($filters['payment_end_date'])) {
|
||||
$query->paymentDateBetween($filters['payment_start_date'], $filters['payment_end_date']);
|
||||
}
|
||||
|
||||
// 테넌트 검색
|
||||
if (!empty($filters['search'])) {
|
||||
$search = $filters['search'];
|
||||
$query->whereHas('tenant', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('company_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query
|
||||
->orderBy('scheduled_payment_date', 'desc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 정산 상세 조회
|
||||
*/
|
||||
public function getCommissionById(int $id): ?SalesCommission
|
||||
{
|
||||
return SalesCommission::with([
|
||||
'tenant',
|
||||
'partner.user',
|
||||
'manager',
|
||||
'management',
|
||||
'details.contractProduct.product',
|
||||
'approver',
|
||||
])->find($id);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 수당 생성 (입금 시)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 입금 등록 및 수당 생성
|
||||
*/
|
||||
public function createCommission(int $managementId, string $paymentType, float $paymentAmount, string $paymentDate): SalesCommission
|
||||
{
|
||||
return DB::transaction(function () use ($managementId, $paymentType, $paymentAmount, $paymentDate) {
|
||||
$management = SalesTenantManagement::with(['salesPartner', 'contractProducts.product'])
|
||||
->findOrFail($managementId);
|
||||
|
||||
// 영업파트너 필수 체크
|
||||
if (!$management->sales_partner_id) {
|
||||
throw new \Exception('영업파트너가 지정되지 않았습니다.');
|
||||
}
|
||||
|
||||
$partner = $management->salesPartner;
|
||||
$paymentDateCarbon = Carbon::parse($paymentDate);
|
||||
|
||||
// 계약 상품이 없으면 기본 계산
|
||||
$contractProducts = $management->contractProducts;
|
||||
$totalRegistrationFee = $contractProducts->sum('registration_fee') ?: $paymentAmount * 2;
|
||||
$baseAmount = $totalRegistrationFee / 2; // 가입비의 50%
|
||||
|
||||
// 수당률 (영업파트너 설정 또는 기본값)
|
||||
$partnerRate = $partner->commission_rate ?? self::DEFAULT_PARTNER_RATE;
|
||||
$managerRate = $partner->manager_commission_rate ?? self::DEFAULT_MANAGER_RATE;
|
||||
|
||||
// 수당 계산
|
||||
$partnerCommission = $baseAmount * ($partnerRate / 100);
|
||||
$managerCommission = $management->manager_user_id
|
||||
? $baseAmount * ($managerRate / 100)
|
||||
: 0;
|
||||
|
||||
// 지급예정일 (익월 10일)
|
||||
$scheduledPaymentDate = SalesCommission::calculateScheduledPaymentDate($paymentDateCarbon);
|
||||
|
||||
// 정산 생성
|
||||
$commission = SalesCommission::create([
|
||||
'tenant_id' => $management->tenant_id,
|
||||
'management_id' => $managementId,
|
||||
'payment_type' => $paymentType,
|
||||
'payment_amount' => $paymentAmount,
|
||||
'payment_date' => $paymentDate,
|
||||
'base_amount' => $baseAmount,
|
||||
'partner_rate' => $partnerRate,
|
||||
'manager_rate' => $managerRate,
|
||||
'partner_commission' => $partnerCommission,
|
||||
'manager_commission' => $managerCommission,
|
||||
'scheduled_payment_date' => $scheduledPaymentDate,
|
||||
'status' => SalesCommission::STATUS_PENDING,
|
||||
'partner_id' => $partner->id,
|
||||
'manager_user_id' => $management->manager_user_id,
|
||||
]);
|
||||
|
||||
// 상품별 상세 내역 생성
|
||||
foreach ($contractProducts as $contractProduct) {
|
||||
$productBaseAmount = ($contractProduct->registration_fee ?? 0) / 2;
|
||||
$productPartnerRate = $contractProduct->product->partner_commission ?? $partnerRate;
|
||||
$productManagerRate = $contractProduct->product->manager_commission ?? $managerRate;
|
||||
|
||||
SalesCommissionDetail::create([
|
||||
'commission_id' => $commission->id,
|
||||
'contract_product_id' => $contractProduct->id,
|
||||
'registration_fee' => $contractProduct->registration_fee ?? 0,
|
||||
'base_amount' => $productBaseAmount,
|
||||
'partner_rate' => $productPartnerRate,
|
||||
'manager_rate' => $productManagerRate,
|
||||
'partner_commission' => $productBaseAmount * ($productPartnerRate / 100),
|
||||
'manager_commission' => $productBaseAmount * ($productManagerRate / 100),
|
||||
]);
|
||||
}
|
||||
|
||||
// management 입금 정보 업데이트
|
||||
$updateData = [];
|
||||
if ($paymentType === SalesCommission::PAYMENT_DEPOSIT) {
|
||||
$updateData = [
|
||||
'deposit_amount' => $paymentAmount,
|
||||
'deposit_paid_date' => $paymentDate,
|
||||
'deposit_status' => 'paid',
|
||||
];
|
||||
} else {
|
||||
$updateData = [
|
||||
'balance_amount' => $paymentAmount,
|
||||
'balance_paid_date' => $paymentDate,
|
||||
'balance_status' => 'paid',
|
||||
];
|
||||
}
|
||||
|
||||
// 총 가입비 업데이트
|
||||
$updateData['total_registration_fee'] = $totalRegistrationFee;
|
||||
$management->update($updateData);
|
||||
|
||||
return $commission->load(['tenant', 'partner.user', 'manager', 'details']);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 승인/지급 처리
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 승인 처리
|
||||
*/
|
||||
public function approve(int $commissionId, int $approverId): SalesCommission
|
||||
{
|
||||
$commission = SalesCommission::findOrFail($commissionId);
|
||||
|
||||
if (!$commission->approve($approverId)) {
|
||||
throw new \Exception('승인할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
return $commission->fresh(['tenant', 'partner.user', 'manager']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 승인
|
||||
*/
|
||||
public function bulkApprove(array $ids, int $approverId): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
DB::transaction(function () use ($ids, $approverId, &$count) {
|
||||
$commissions = SalesCommission::whereIn('id', $ids)
|
||||
->where('status', SalesCommission::STATUS_PENDING)
|
||||
->get();
|
||||
|
||||
foreach ($commissions as $commission) {
|
||||
if ($commission->approve($approverId)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지급완료 처리
|
||||
*/
|
||||
public function markAsPaid(int $commissionId, ?string $bankReference = null): SalesCommission
|
||||
{
|
||||
$commission = SalesCommission::findOrFail($commissionId);
|
||||
|
||||
if (!$commission->markAsPaid($bankReference)) {
|
||||
throw new \Exception('지급완료 처리할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
// 영업파트너 누적 수당 업데이트
|
||||
$this->updatePartnerTotalCommission($commission->partner_id);
|
||||
|
||||
return $commission->fresh(['tenant', 'partner.user', 'manager']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 지급완료
|
||||
*/
|
||||
public function bulkMarkAsPaid(array $ids, ?string $bankReference = null): int
|
||||
{
|
||||
$count = 0;
|
||||
$partnerIds = [];
|
||||
|
||||
DB::transaction(function () use ($ids, $bankReference, &$count, &$partnerIds) {
|
||||
$commissions = SalesCommission::whereIn('id', $ids)
|
||||
->where('status', SalesCommission::STATUS_APPROVED)
|
||||
->get();
|
||||
|
||||
foreach ($commissions as $commission) {
|
||||
if ($commission->markAsPaid($bankReference)) {
|
||||
$count++;
|
||||
$partnerIds[] = $commission->partner_id;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 영업파트너 누적 수당 일괄 업데이트
|
||||
foreach (array_unique($partnerIds) as $partnerId) {
|
||||
$this->updatePartnerTotalCommission($partnerId);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 취소 처리
|
||||
*/
|
||||
public function cancel(int $commissionId): SalesCommission
|
||||
{
|
||||
$commission = SalesCommission::findOrFail($commissionId);
|
||||
|
||||
if (!$commission->cancel()) {
|
||||
throw new \Exception('취소할 수 없는 상태입니다.');
|
||||
}
|
||||
|
||||
return $commission->fresh(['tenant', 'partner.user', 'manager']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 영업파트너/매니저 대시보드용
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 영업파트너 수당 요약
|
||||
*/
|
||||
public function getPartnerCommissionSummary(int $partnerId): array
|
||||
{
|
||||
$commissions = SalesCommission::forPartner($partnerId)->get();
|
||||
|
||||
$thisMonth = now()->format('Y-m');
|
||||
$thisMonthStart = now()->startOfMonth()->format('Y-m-d');
|
||||
$thisMonthEnd = now()->endOfMonth()->format('Y-m-d');
|
||||
|
||||
return [
|
||||
// 이번 달 지급예정 (승인 완료된 건)
|
||||
'scheduled_this_month' => $commissions
|
||||
->where('status', SalesCommission::STATUS_APPROVED)
|
||||
->filter(fn($c) => $c->scheduled_payment_date->format('Y-m') === $thisMonth)
|
||||
->sum('partner_commission'),
|
||||
|
||||
// 누적 수령 수당
|
||||
'total_received' => $commissions
|
||||
->where('status', SalesCommission::STATUS_PAID)
|
||||
->sum('partner_commission'),
|
||||
|
||||
// 대기중 수당
|
||||
'pending_amount' => $commissions
|
||||
->where('status', SalesCommission::STATUS_PENDING)
|
||||
->sum('partner_commission'),
|
||||
|
||||
// 이번 달 신규 계약 건수
|
||||
'contracts_this_month' => $commissions
|
||||
->filter(fn($c) => $c->payment_date >= $thisMonthStart && $c->payment_date <= $thisMonthEnd)
|
||||
->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 매니저 수당 요약
|
||||
*/
|
||||
public function getManagerCommissionSummary(int $managerUserId): array
|
||||
{
|
||||
$commissions = SalesCommission::forManager($managerUserId)->get();
|
||||
|
||||
$thisMonth = now()->format('Y-m');
|
||||
|
||||
return [
|
||||
// 이번 달 지급예정 (승인 완료된 건)
|
||||
'scheduled_this_month' => $commissions
|
||||
->where('status', SalesCommission::STATUS_APPROVED)
|
||||
->filter(fn($c) => $c->scheduled_payment_date->format('Y-m') === $thisMonth)
|
||||
->sum('manager_commission'),
|
||||
|
||||
// 누적 수령 수당
|
||||
'total_received' => $commissions
|
||||
->where('status', SalesCommission::STATUS_PAID)
|
||||
->sum('manager_commission'),
|
||||
|
||||
// 대기중 수당
|
||||
'pending_amount' => $commissions
|
||||
->where('status', SalesCommission::STATUS_PENDING)
|
||||
->sum('manager_commission'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 수당 내역 (대시보드용)
|
||||
*/
|
||||
public function getRecentCommissions(int $partnerId, int $limit = 5): Collection
|
||||
{
|
||||
return SalesCommission::forPartner($partnerId)
|
||||
->with(['tenant', 'management'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 통계
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 정산 통계 (본사 대시보드용)
|
||||
*/
|
||||
public function getSettlementStats(int $year, int $month): array
|
||||
{
|
||||
$commissions = SalesCommission::forScheduledMonth($year, $month)->get();
|
||||
|
||||
return [
|
||||
// 상태별 건수 및 금액
|
||||
'pending' => [
|
||||
'count' => $commissions->where('status', SalesCommission::STATUS_PENDING)->count(),
|
||||
'partner_total' => $commissions->where('status', SalesCommission::STATUS_PENDING)->sum('partner_commission'),
|
||||
'manager_total' => $commissions->where('status', SalesCommission::STATUS_PENDING)->sum('manager_commission'),
|
||||
],
|
||||
'approved' => [
|
||||
'count' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->count(),
|
||||
'partner_total' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->sum('partner_commission'),
|
||||
'manager_total' => $commissions->where('status', SalesCommission::STATUS_APPROVED)->sum('manager_commission'),
|
||||
],
|
||||
'paid' => [
|
||||
'count' => $commissions->where('status', SalesCommission::STATUS_PAID)->count(),
|
||||
'partner_total' => $commissions->where('status', SalesCommission::STATUS_PAID)->sum('partner_commission'),
|
||||
'manager_total' => $commissions->where('status', SalesCommission::STATUS_PAID)->sum('manager_commission'),
|
||||
],
|
||||
|
||||
// 전체 합계
|
||||
'total' => [
|
||||
'count' => $commissions->count(),
|
||||
'base_amount' => $commissions->sum('base_amount'),
|
||||
'partner_commission' => $commissions->sum('partner_commission'),
|
||||
'manager_commission' => $commissions->sum('manager_commission'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 입금 대기 중인 테넌트 목록
|
||||
*/
|
||||
public function getPendingPaymentTenants(): Collection
|
||||
{
|
||||
return SalesTenantManagement::with(['tenant', 'salesPartner.user', 'manager'])
|
||||
->contracted()
|
||||
->where(function ($query) {
|
||||
$query->where('deposit_status', 'pending')
|
||||
->orWhere('balance_status', 'pending');
|
||||
})
|
||||
->orderBy('contracted_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 내부 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 영업파트너 누적 수당 업데이트
|
||||
*/
|
||||
private function updatePartnerTotalCommission(int $partnerId): void
|
||||
{
|
||||
$totalPaid = SalesCommission::forPartner($partnerId)
|
||||
->paid()
|
||||
->sum('partner_commission');
|
||||
|
||||
$contractCount = SalesCommission::forPartner($partnerId)
|
||||
->paid()
|
||||
->count();
|
||||
|
||||
SalesPartner::where('id', $partnerId)->update([
|
||||
'total_commission' => $totalPaid,
|
||||
'total_contracts' => $contractCount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
101
database/seeders/SalesCommissionMenuSeeder.php
Normal file
101
database/seeders/SalesCommissionMenuSeeder.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Commons\Menu;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 영업수수료정산 메뉴 시더
|
||||
*
|
||||
* 실행 방법:
|
||||
* docker exec sam-mng-1 php artisan db:seed --class=SalesCommissionMenuSeeder
|
||||
*/
|
||||
class SalesCommissionMenuSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// 재무관리 메뉴 찾기 (tenant_id = 1인 HQ 테넌트용)
|
||||
$financeMenu = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', 1)
|
||||
->where('name', '재무관리')
|
||||
->first();
|
||||
|
||||
if (!$financeMenu) {
|
||||
$this->command->warn('재무관리 메뉴를 찾을 수 없습니다. 먼저 재무관리 메뉴를 생성해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 정산관리 서브메뉴 찾기 또는 생성
|
||||
$settlementMenu = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', 1)
|
||||
->where('parent_id', $financeMenu->id)
|
||||
->where('name', '정산관리')
|
||||
->first();
|
||||
|
||||
if (!$settlementMenu) {
|
||||
// 정산관리 메뉴가 없으면 생성
|
||||
$maxOrder = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', 1)
|
||||
->where('parent_id', $financeMenu->id)
|
||||
->max('sort_order') ?? 0;
|
||||
|
||||
$settlementMenu = Menu::create([
|
||||
'tenant_id' => 1,
|
||||
'parent_id' => $financeMenu->id,
|
||||
'name' => '정산관리',
|
||||
'url' => '#',
|
||||
'icon' => 'calculator',
|
||||
'is_active' => true,
|
||||
'hidden' => false,
|
||||
'sort_order' => $maxOrder + 1,
|
||||
]);
|
||||
|
||||
$this->command->info('정산관리 메뉴를 생성했습니다.');
|
||||
}
|
||||
|
||||
// 영업수수료정산 메뉴 추가/업데이트
|
||||
$existingMenu = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', 1)
|
||||
->where('parent_id', $settlementMenu->id)
|
||||
->where('url', '/finance/sales-commissions')
|
||||
->first();
|
||||
|
||||
if ($existingMenu) {
|
||||
$existingMenu->update([
|
||||
'name' => '영업수수료정산',
|
||||
'is_active' => true,
|
||||
'hidden' => false,
|
||||
'options' => [
|
||||
'route_name' => 'finance.sales-commissions.index',
|
||||
'description' => '영업파트너 및 매니저 수당 정산 관리',
|
||||
],
|
||||
]);
|
||||
$this->command->info('영업수수료정산 메뉴를 업데이트했습니다.');
|
||||
} else {
|
||||
$maxOrder = Menu::withoutGlobalScopes()
|
||||
->where('tenant_id', 1)
|
||||
->where('parent_id', $settlementMenu->id)
|
||||
->max('sort_order') ?? 0;
|
||||
|
||||
Menu::create([
|
||||
'tenant_id' => 1,
|
||||
'parent_id' => $settlementMenu->id,
|
||||
'name' => '영업수수료정산',
|
||||
'url' => '/finance/sales-commissions',
|
||||
'icon' => 'currency-dollar',
|
||||
'is_active' => true,
|
||||
'hidden' => false,
|
||||
'sort_order' => $maxOrder + 1,
|
||||
'options' => [
|
||||
'route_name' => 'finance.sales-commissions.index',
|
||||
'description' => '영업파트너 및 매니저 수당 정산 관리',
|
||||
],
|
||||
]);
|
||||
$this->command->info('영업수수료정산 메뉴를 생성했습니다.');
|
||||
}
|
||||
|
||||
$this->command->info('메뉴 시더 완료!');
|
||||
}
|
||||
}
|
||||
376
resources/views/finance/sales-commission/index.blade.php
Normal file
376
resources/views/finance/sales-commission/index.blade.php
Normal file
@@ -0,0 +1,376 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '영업수수료정산')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
{{-- 페이지 헤더 --}}
|
||||
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">영업수수료정산</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $year }}년 {{ $month }}월 지급예정</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button"
|
||||
onclick="openPaymentModal()"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
입금 등록
|
||||
</button>
|
||||
<a href="{{ route('finance.sales-commissions.export', ['year' => $year, 'month' => $month]) }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
엑셀 다운로드
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 통계 카드 --}}
|
||||
<div id="stats-container">
|
||||
@include('finance.sales-commission.partials.stats-cards', ['stats' => $stats, 'year' => $year, 'month' => $month])
|
||||
</div>
|
||||
|
||||
{{-- 필터 섹션 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form id="filter-form" method="GET" action="{{ route('finance.sales-commissions.index') }}">
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
{{-- 년/월 선택 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">년도</label>
|
||||
<select name="year" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
||||
@for ($y = now()->year - 2; $y <= now()->year + 1; $y++)
|
||||
<option value="{{ $y }}" {{ $year == $y ? 'selected' : '' }}>{{ $y }}년</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">월</label>
|
||||
<select name="month" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
||||
@for ($m = 1; $m <= 12; $m++)
|
||||
<option value="{{ $m }}" {{ $month == $m ? 'selected' : '' }}>{{ $m }}월</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 상태 필터 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
|
||||
<select name="status" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
||||
<option value="">전체</option>
|
||||
<option value="pending" {{ ($filters['status'] ?? '') == 'pending' ? 'selected' : '' }}>대기</option>
|
||||
<option value="approved" {{ ($filters['status'] ?? '') == 'approved' ? 'selected' : '' }}>승인</option>
|
||||
<option value="paid" {{ ($filters['status'] ?? '') == 'paid' ? 'selected' : '' }}>지급완료</option>
|
||||
<option value="cancelled" {{ ($filters['status'] ?? '') == 'cancelled' ? 'selected' : '' }}>취소</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 입금구분 필터 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">입금구분</label>
|
||||
<select name="payment_type" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
||||
<option value="">전체</option>
|
||||
<option value="deposit" {{ ($filters['payment_type'] ?? '') == 'deposit' ? 'selected' : '' }}>계약금</option>
|
||||
<option value="balance" {{ ($filters['payment_type'] ?? '') == 'balance' ? 'selected' : '' }}>잔금</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 영업파트너 필터 --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">영업파트너</label>
|
||||
<select name="partner_id" class="w-full rounded-lg border-gray-300 focus:border-emerald-500 focus:ring-emerald-500">
|
||||
<option value="">전체</option>
|
||||
@foreach ($partners as $partner)
|
||||
<option value="{{ $partner->id }}" {{ ($filters['partner_id'] ?? '') == $partner->id ? 'selected' : '' }}>
|
||||
{{ $partner->user->name ?? $partner->partner_code }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 버튼 --}}
|
||||
<div class="flex items-end gap-2">
|
||||
<button type="submit"
|
||||
class="flex-1 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors">
|
||||
조회
|
||||
</button>
|
||||
<a href="{{ route('finance.sales-commissions.index') }}"
|
||||
class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors">
|
||||
초기화
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- 일괄 처리 버튼 --}}
|
||||
<div class="flex items-center gap-2 mb-4" id="bulk-actions" style="display: none;">
|
||||
<span class="text-sm text-gray-600"><span id="selected-count">0</span>건 선택</span>
|
||||
<button type="button" onclick="bulkApprove()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors">
|
||||
일괄 승인
|
||||
</button>
|
||||
<button type="button" onclick="bulkMarkPaid()" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors">
|
||||
일괄 지급완료
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 정산 테이블 --}}
|
||||
<div id="table-container">
|
||||
@include('finance.sales-commission.partials.commission-table', ['commissions' => $commissions])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 입금 등록 모달 --}}
|
||||
<div id="payment-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-800">입금 등록</h3>
|
||||
<button type="button" onclick="closePaymentModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="payment-form-container" class="p-6">
|
||||
@include('finance.sales-commission.partials.payment-form', ['management' => null, 'pendingTenants' => $pendingTenants])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 상세 모달 --}}
|
||||
<div id="detail-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div id="detail-modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// 선택된 정산 ID 배열
|
||||
let selectedIds = [];
|
||||
|
||||
// 체크박스 변경 시
|
||||
function updateSelection() {
|
||||
selectedIds = Array.from(document.querySelectorAll('.commission-checkbox:checked'))
|
||||
.map(cb => parseInt(cb.value));
|
||||
|
||||
const bulkActions = document.getElementById('bulk-actions');
|
||||
const selectedCount = document.getElementById('selected-count');
|
||||
|
||||
if (selectedIds.length > 0) {
|
||||
bulkActions.style.display = 'flex';
|
||||
selectedCount.textContent = selectedIds.length;
|
||||
} else {
|
||||
bulkActions.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 전체 선택/해제
|
||||
function toggleSelectAll(checkbox) {
|
||||
const checkboxes = document.querySelectorAll('.commission-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = checkbox.checked;
|
||||
});
|
||||
updateSelection();
|
||||
}
|
||||
|
||||
// 입금 등록 모달 열기
|
||||
function openPaymentModal() {
|
||||
document.getElementById('payment-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 입금 등록 모달 닫기
|
||||
function closePaymentModal() {
|
||||
document.getElementById('payment-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 입금 등록 제출
|
||||
function submitPayment() {
|
||||
const form = document.getElementById('payment-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch('{{ route("finance.sales-commissions.payment") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
closePaymentModal();
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '오류가 발생했습니다.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
// 상세 모달 열기
|
||||
function openDetailModal(commissionId) {
|
||||
fetch('{{ url("finance/sales-commissions") }}/' + commissionId + '/detail')
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.getElementById('detail-modal-content').innerHTML = html;
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// 상세 모달 닫기
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detail-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 승인 처리
|
||||
function approveCommission(id) {
|
||||
if (!confirm('승인하시겠습니까?')) return;
|
||||
|
||||
fetch('{{ url("finance/sales-commissions") }}/' + id + '/approve', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 일괄 승인
|
||||
function bulkApprove() {
|
||||
if (selectedIds.length === 0) {
|
||||
alert('선택된 항목이 없습니다.');
|
||||
return;
|
||||
}
|
||||
if (!confirm(selectedIds.length + '건을 승인하시겠습니까?')) return;
|
||||
|
||||
fetch('{{ route("finance.sales-commissions.bulk-approve") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ids: selectedIds })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 지급완료 처리
|
||||
function markPaidCommission(id) {
|
||||
const bankReference = prompt('이체 참조번호를 입력하세요 (선택사항)');
|
||||
if (bankReference === null) return;
|
||||
|
||||
fetch('{{ url("finance/sales-commissions") }}/' + id + '/mark-paid', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ bank_reference: bankReference })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 일괄 지급완료
|
||||
function bulkMarkPaid() {
|
||||
if (selectedIds.length === 0) {
|
||||
alert('선택된 항목이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const bankReference = prompt('이체 참조번호를 입력하세요 (선택사항)');
|
||||
if (bankReference === null) return;
|
||||
|
||||
if (!confirm(selectedIds.length + '건을 지급완료 처리하시겠습니까?')) return;
|
||||
|
||||
fetch('{{ route("finance.sales-commissions.bulk-mark-paid") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ids: selectedIds, bank_reference: bankReference })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 취소 처리
|
||||
function cancelCommission(id) {
|
||||
if (!confirm('취소하시겠습니까?')) return;
|
||||
|
||||
fetch('{{ url("finance/sales-commissions") }}/' + id + '/cancel', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 테넌트 선택 시 금액 자동 계산
|
||||
function onTenantSelect(managementId) {
|
||||
if (!managementId) return;
|
||||
|
||||
// HTMX로 폼 업데이트
|
||||
htmx.ajax('GET', '{{ route("finance.sales-commissions.payment-form") }}?management_id=' + managementId, {
|
||||
target: '#payment-form-container'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,138 @@
|
||||
{{-- 정산 테이블 --}}
|
||||
<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-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 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="12" 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,196 @@
|
||||
{{-- 정산 상세 모달 --}}
|
||||
<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>
|
||||
<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,185 @@
|
||||
{{-- 입금 등록 폼 --}}
|
||||
<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,70 @@
|
||||
{{-- 통계 카드 --}}
|
||||
<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>
|
||||
<p class="text-xs text-gray-400 mt-1">{{ $stats['pending']['count'] }}건</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($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>
|
||||
@@ -1,5 +1,10 @@
|
||||
{{-- 대시보드 데이터 컨테이너 (HTMX로 새로고침되는 영역) --}}
|
||||
|
||||
{{-- 영업파트너 수당 현황 (파트너인 경우에만 표시) --}}
|
||||
@if (isset($partner) && $partner)
|
||||
@include('sales.dashboard.partials.my-commission')
|
||||
@endif
|
||||
|
||||
{{-- 전체 누적 실적 --}}
|
||||
@include('sales.dashboard.partials.stats')
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{{-- 영업파트너 수당 현황 카드 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-800">내 수당 현황</h3>
|
||||
<a href="{{ route('finance.sales-commissions.index') }}"
|
||||
class="text-sm text-emerald-600 hover:text-emerald-700">
|
||||
전체보기 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
{{-- 수당 요약 --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{{-- 이번 달 지급예정 --}}
|
||||
<div class="bg-emerald-50 rounded-lg p-4">
|
||||
<div class="text-sm text-emerald-600 mb-1">이번 달 지급예정</div>
|
||||
<div class="text-xl font-bold text-emerald-700">
|
||||
{{ number_format($commissionSummary['scheduled_this_month'] ?? 0) }}원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 누적 수령액 --}}
|
||||
<div class="bg-blue-50 rounded-lg p-4">
|
||||
<div class="text-sm text-blue-600 mb-1">누적 수령액</div>
|
||||
<div class="text-xl font-bold text-blue-700">
|
||||
{{ number_format($commissionSummary['total_received'] ?? 0) }}원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 대기중 수당 --}}
|
||||
<div class="bg-yellow-50 rounded-lg p-4">
|
||||
<div class="text-sm text-yellow-600 mb-1">대기중 수당</div>
|
||||
<div class="text-xl font-bold text-yellow-700">
|
||||
{{ number_format($commissionSummary['pending_amount'] ?? 0) }}원
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 이번 달 계약 건수 --}}
|
||||
<div class="bg-purple-50 rounded-lg p-4">
|
||||
<div class="text-sm text-purple-600 mb-1">이번 달 계약</div>
|
||||
<div class="text-xl font-bold text-purple-700">
|
||||
{{ $commissionSummary['contracts_this_month'] ?? 0 }}건
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 최근 수당 내역 --}}
|
||||
@if (isset($recentCommissions) && $recentCommissions->count() > 0)
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-3">최근 수당 내역</h4>
|
||||
<div class="space-y-2">
|
||||
@foreach ($recentCommissions as $commission)
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
|
||||
{{ $commission->payment_type === 'deposit' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700' }}">
|
||||
{{ $commission->payment_type_label }}
|
||||
</span>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ $commission->tenant->name ?? $commission->tenant->company_name ?? '-' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ $commission->payment_date->format('Y-m-d') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-bold text-emerald-600">
|
||||
+{{ number_format($commission->partner_commission) }}원
|
||||
</div>
|
||||
@php
|
||||
$statusColors = [
|
||||
'pending' => 'text-yellow-600',
|
||||
'approved' => 'text-blue-600',
|
||||
'paid' => 'text-green-600',
|
||||
'cancelled' => 'text-red-600',
|
||||
];
|
||||
@endphp
|
||||
<div class="text-xs {{ $statusColors[$commission->status] ?? 'text-gray-500' }}">
|
||||
{{ $commission->status_label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<svg class="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||
<p>아직 수당 내역이 없습니다.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -682,13 +682,25 @@
|
||||
return view('finance.purchase');
|
||||
})->name('purchase');
|
||||
|
||||
// 정산관리
|
||||
Route::get('/sales-commission', function () {
|
||||
if (request()->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('finance.sales-commission'));
|
||||
}
|
||||
return view('finance.sales-commission');
|
||||
})->name('sales-commission');
|
||||
// 영업수수료정산 (실제 구현)
|
||||
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');
|
||||
Route::get('/payment-form', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'paymentForm'])->name('payment-form');
|
||||
Route::get('/table', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'table'])->name('table');
|
||||
Route::get('/stats', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'stats'])->name('stats');
|
||||
Route::post('/payment', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'registerPayment'])->name('payment');
|
||||
Route::post('/bulk-approve', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'bulkApprove'])->name('bulk-approve');
|
||||
Route::post('/bulk-mark-paid', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'bulkMarkPaid'])->name('bulk-mark-paid');
|
||||
Route::get('/{id}', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'show'])->name('show');
|
||||
Route::get('/{id}/detail', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'detail'])->name('detail');
|
||||
Route::post('/{id}/approve', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'approve'])->name('approve');
|
||||
Route::post('/{id}/mark-paid', [\App\Http\Controllers\Finance\SalesCommissionController::class, 'markPaid'])->name('mark-paid');
|
||||
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'));
|
||||
|
||||
Reference in New Issue
Block a user