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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user