Files
sam-manage/app/Http/Controllers/Finance/SalesCommissionController.php
pro 5d7de6d13b feat:영업수수료 정산 기능 구현
[모델]
- SalesCommission: 영업수수료 정산 모델
- SalesCommissionDetail: 상품별 수당 내역 모델
- SalesTenantManagement: 입금 정보 필드 추가

[서비스/컨트롤러]
- SalesCommissionService: 수당 생성, 승인, 지급 처리 로직
- SalesCommissionController: 정산 관리 CRUD

[뷰]
- 본사 정산 관리 화면 (필터, 통계, 테이블)
- 입금 등록 모달
- 상세 보기 모달
- 영업파트너 대시보드 수당 카드

[라우트]
- /finance/sales-commissions/* 라우트 추가
- 기존 sales-commission 리다이렉트 호환

[메뉴]
- SalesCommissionMenuSeeder: 정산관리 > 영업수수료정산 메뉴 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:14:11 +09:00

382 lines
12 KiB
PHP

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