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:
pro
2026-01-29 18:14:11 +09:00
parent 19e85e4b02
commit 5d7de6d13b
15 changed files with 2441 additions and 7 deletions

View 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);
}
}

View File

@@ -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');
}
}

View 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]);
}
}

View 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;
}
}

View File

@@ -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로 조회 또는 생성
*/

View 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,
]);
}
}

View 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('메뉴 시더 완료!');
}
}

View 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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,10 @@
{{-- 대시보드 데이터 컨테이너 (HTMX로 새로고침되는 영역) --}}
{{-- 영업파트너 수당 현황 (파트너인 경우에만 표시) --}}
@if (isset($partner) && $partner)
@include('sales.dashboard.partials.my-commission')
@endif
{{-- 전체 누적 실적 --}}
@include('sales.dashboard.partials.stats')

View File

@@ -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">
전체보기 &rarr;
</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>

View File

@@ -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'));