diff --git a/app/Http/Controllers/Api/Admin/HR/BusinessIncomePaymentController.php b/app/Http/Controllers/Api/Admin/HR/BusinessIncomePaymentController.php index 71404372..314a62a9 100644 --- a/app/Http/Controllers/Api/Admin/HR/BusinessIncomePaymentController.php +++ b/app/Http/Controllers/Api/Admin/HR/BusinessIncomePaymentController.php @@ -49,14 +49,20 @@ public function index(Request $request): JsonResponse|Response $earners = $this->service->getActiveEarners(); $payments = $this->service->getPayments($year, $month); - $paymentsByUser = $payments->keyBy('user_id'); $stats = $this->service->getMonthlyStats($year, $month); + $earnersForJs = $earners->map(fn ($e) => [ + 'user_id' => $e->user_id, + 'business_name' => $e->business_name ?? ($e->user?->name ?? ''), + 'user_name' => $e->user?->name ?? '', + 'business_reg_number' => $e->business_registration_number ?? '', + ])->values(); + if ($request->header('HX-Request')) { return response( view('hr.business-income-payments.partials.stats', compact('stats')). ''. - view('hr.business-income-payments.partials.spreadsheet', compact('earners', 'paymentsByUser', 'year', 'month')) + view('hr.business-income-payments.partials.spreadsheet', compact('payments', 'earnersForJs', 'year', 'month')) ); } @@ -80,7 +86,10 @@ public function bulkSave(Request $request): JsonResponse 'year' => 'required|integer|min:2020|max:2100', 'month' => 'required|integer|min:1|max:12', 'items' => 'required|array', - 'items.*.user_id' => 'required|integer', + 'items.*.payment_id' => 'nullable|integer', + 'items.*.user_id' => 'nullable|integer', + 'items.*.display_name' => 'required|string|max:100', + 'items.*.business_reg_number' => 'nullable|string|max:20', 'items.*.gross_amount' => 'required|numeric|min:0', 'items.*.service_content' => 'nullable|string|max:200', 'items.*.payment_date' => 'nullable|date', @@ -150,9 +159,8 @@ public function export(Request $request): StreamedResponse|JsonResponse $moneyColumns = ['E', 'F', 'G', 'H', 'I']; foreach ($payments as $idx => $payment) { - $earner = $payment->earner ?? null; - $name = $earner?->business_name ?? $payment->user?->name ?? '-'; - $regNumber = $earner?->business_registration_number ?? $earner?->resident_number ?? ''; + $name = $payment->display_name ?: ($payment->user?->name ?? '-'); + $regNumber = $payment->business_reg_number ?? ''; $sheet->setCellValue("A{$row}", $idx + 1); $sheet->setCellValue("B{$row}", $name); diff --git a/app/Http/Controllers/HR/BusinessIncomePaymentController.php b/app/Http/Controllers/HR/BusinessIncomePaymentController.php index 8c51f5bf..0ece1097 100644 --- a/app/Http/Controllers/HR/BusinessIncomePaymentController.php +++ b/app/Http/Controllers/HR/BusinessIncomePaymentController.php @@ -34,12 +34,18 @@ public function index(Request $request): View|Response $earners = $this->service->getActiveEarners(); $payments = $this->service->getPayments($year, $month); - $paymentsByUser = $payments->keyBy('user_id'); $stats = $this->service->getMonthlyStats($year, $month); + $earnersForJs = $earners->map(fn ($e) => [ + 'user_id' => $e->user_id, + 'business_name' => $e->business_name ?? ($e->user?->name ?? ''), + 'user_name' => $e->user?->name ?? '', + 'business_reg_number' => $e->business_registration_number ?? '', + ])->values(); + return view('hr.business-income-payments.index', [ - 'earners' => $earners, - 'paymentsByUser' => $paymentsByUser, + 'payments' => $payments, + 'earnersForJs' => $earnersForJs, 'stats' => $stats, 'year' => $year, 'month' => $month, diff --git a/app/Models/HR/BusinessIncomePayment.php b/app/Models/HR/BusinessIncomePayment.php index 37e6aa9a..f634077c 100644 --- a/app/Models/HR/BusinessIncomePayment.php +++ b/app/Models/HR/BusinessIncomePayment.php @@ -17,6 +17,8 @@ class BusinessIncomePayment extends Model protected $fillable = [ 'tenant_id', 'user_id', + 'display_name', + 'business_reg_number', 'pay_year', 'pay_month', 'service_content', diff --git a/app/Services/HR/BusinessIncomePaymentService.php b/app/Services/HR/BusinessIncomePaymentService.php index acc6148c..3d909bd7 100644 --- a/app/Services/HR/BusinessIncomePaymentService.php +++ b/app/Services/HR/BusinessIncomePaymentService.php @@ -42,9 +42,11 @@ public function getActiveEarners(): Collection /** * 일괄 저장 * - * - 지급총액 > 0: upsert (신규 생성 또는 draft 수정) + * - payment_id 기반 기존 레코드 조회 (수정 시) + * - payment_id 없고 user_id 있으면 기존 방식 조회 + * - 둘 다 없으면 신규 생성 * - 지급총액 == 0: draft면 삭제, confirmed/paid는 무시 - * - confirmed/paid 상태 레코드는 수정하지 않음 + * - 제출되지 않은 기존 draft 행 자동 삭제 (사용자가 행을 삭제한 경우) */ public function bulkSave(int $year, int $month, array $items): array { @@ -54,22 +56,39 @@ public function bulkSave(int $year, int $month, array $items): array $skipped = 0; DB::transaction(function () use ($items, $tenantId, $year, $month, &$saved, &$deleted, &$skipped) { + $submittedPaymentIds = []; + foreach ($items as $item) { - $userId = (int) ($item['user_id'] ?? 0); + $paymentId = ! empty($item['payment_id']) ? (int) $item['payment_id'] : null; + $userId = ! empty($item['user_id']) ? (int) $item['user_id'] : null; + $displayName = trim($item['display_name'] ?? ''); + $businessRegNumber = $item['business_reg_number'] ?? null; $grossAmount = (float) ($item['gross_amount'] ?? 0); - if ($userId === 0) { + if (empty($displayName)) { continue; } - // 기존 레코드 조회 (SoftDeletes 포함, 행 잠금) - $existing = BusinessIncomePayment::withTrashed() - ->where('tenant_id', $tenantId) - ->where('user_id', $userId) - ->where('pay_year', $year) - ->where('pay_month', $month) - ->lockForUpdate() - ->first(); + $existing = null; + + // payment_id로 기존 레코드 조회 + if ($paymentId) { + $existing = BusinessIncomePayment::where('id', $paymentId) + ->where('tenant_id', $tenantId) + ->lockForUpdate() + ->first(); + } + + // user_id로 기존 레코드 조회 (payment_id 없고 user_id 있는 경우) + if (! $existing && $userId) { + $existing = BusinessIncomePayment::withTrashed() + ->where('tenant_id', $tenantId) + ->where('user_id', $userId) + ->where('pay_year', $year) + ->where('pay_month', $month) + ->lockForUpdate() + ->first(); + } if ($grossAmount <= 0) { // 지급총액 0: draft면 삭제 @@ -84,6 +103,7 @@ public function bulkSave(int $year, int $month, array $items): array // confirmed/paid 상태는 수정하지 않음 if ($existing && ! $existing->trashed() && ! $existing->isEditable()) { + $submittedPaymentIds[] = $existing->id; $skipped++; continue; @@ -94,6 +114,8 @@ public function bulkSave(int $year, int $month, array $items): array $data = [ 'tenant_id' => $tenantId, 'user_id' => $userId, + 'display_name' => $displayName, + 'business_reg_number' => $businessRegNumber, 'pay_year' => $year, 'pay_month' => $month, 'service_content' => $item['service_content'] ?? null, @@ -109,18 +131,35 @@ public function bulkSave(int $year, int $month, array $items): array if ($existing && $existing->trashed()) { $existing->forceDelete(); + $existing = null; } - if ($existing && ! $existing->trashed()) { + if ($existing) { $existing->update($data); + $submittedPaymentIds[] = $existing->id; } else { $data['status'] = 'draft'; $data['created_by'] = auth()->id(); - BusinessIncomePayment::create($data); + $record = BusinessIncomePayment::create($data); + $submittedPaymentIds[] = $record->id; } $saved++; } + + // 제출되지 않은 기존 draft 행 자동 삭제 (사용자가 행을 삭제한 경우) + $orphanDrafts = BusinessIncomePayment::where('tenant_id', $tenantId) + ->where('pay_year', $year) + ->where('pay_month', $month) + ->where('status', 'draft') + ->when(count($submittedPaymentIds) > 0, fn ($q) => $q->whereNotIn('id', $submittedPaymentIds)) + ->get(); + + foreach ($orphanDrafts as $orphan) { + $orphan->update(['deleted_by' => auth()->id()]); + $orphan->delete(); + $deleted++; + } }); return [ @@ -163,28 +202,17 @@ public function getMonthlyStats(int $year, int $month): array } /** - * XLSX 내보내기 데이터 (earner 프로필 포함) + * XLSX 내보내기 데이터 */ public function getExportData(int $year, int $month): Collection { $tenantId = session('selected_tenant_id', 1); - $payments = BusinessIncomePayment::query() + return BusinessIncomePayment::query() ->with('user:id,name') ->forTenant($tenantId) ->forPeriod($year, $month) ->orderBy('id') ->get(); - - // earner 프로필 일괄 로드 (N+1 방지) - $userIds = $payments->pluck('user_id')->unique(); - $earners = BusinessIncomeEarner::whereIn('user_id', $userIds) - ->where('tenant_id', $tenantId) - ->get() - ->keyBy('user_id'); - - $payments->each(fn ($p) => $p->earner = $earners->get($p->user_id)); - - return $payments; } } diff --git a/resources/views/hr/business-income-payments/index.blade.php b/resources/views/hr/business-income-payments/index.blade.php index 3a076256..ffe410cb 100644 --- a/resources/views/hr/business-income-payments/index.blade.php +++ b/resources/views/hr/business-income-payments/index.blade.php @@ -52,8 +52,8 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 te
| 구분 | -상호/성명 | +상호/성명 | 사업자등록번호 | 용역내용 | 지급총액 | @@ -21,20 +28,21 @@실지급액 | 지급일자 | 비고 | +삭제 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + data-payment-id="{{ $payment->id }}" + data-user-id="{{ $payment->user_id ?? '' }}"> + {{-- 구분 --}} + | {{ $idx + 1 }} @if($isLocked) {{ $statusLabel }} @@ -42,16 +50,34 @@ | {{-- 상호/성명 --}} -- {{ $earner->business_name ?: ($earner->user?->name ?? '-') }} - @if($earner->business_name && $earner->user?->name) - {{ $earner->user->name }} + | + @if($isLocked) + {{ $payment->display_name ?: ($payment->user?->name ?? '-') }} + @else + @endif | {{-- 사업자등록번호 --}} -- {{ $earner->business_registration_number ?? '-' }} + | + @if($isLocked) + {{ $payment->business_reg_number ?? '-' }} + @else + + @endif | {{-- 용역내용 --}} @@ -70,7 +96,7 @@ class="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focu - {{ $payment ? number_format($payment->income_tax) : '0' }} + {{ number_format($payment->income_tax) }} {{-- 지방소득세 (자동계산) --}}- {{ $payment ? number_format($payment->local_income_tax) : '0' }} + {{ number_format($payment->local_income_tax) }} | {{-- 공제합계 (자동계산) --}}- {{ $payment ? number_format($payment->total_deductions) : '0' }} + {{ number_format($payment->total_deductions) }} | {{-- 실지급액 (자동계산) --}}- {{ $payment ? number_format($payment->net_amount) : '0' }} + {{ number_format($payment->net_amount) }} | {{-- 지급일자 --}}@@ -119,39 +145,55 @@ class="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focu class="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-blue-500 focus:border-blue-500 {{ $isLocked ? 'bg-gray-100 text-gray-500' : '' }}" style="min-width: 80px;"> | + + {{-- 삭제 --}} ++ @if(!$isLocked) + + @else + — + @endif + | |||||||
| {{ $i + 1 }} | -- | -- | -- | ||||||||||||||
| + + | +|||||||||||||||||
| 합계 | -+ | 합계 | - {{ number_format($paymentsByUser->sum('gross_amount')) }} + {{ number_format($payments->sum('gross_amount')) }} | - {{ number_format($paymentsByUser->sum('income_tax')) }} + {{ number_format($payments->sum('income_tax')) }} | - {{ number_format($paymentsByUser->sum('local_income_tax')) }} + {{ number_format($payments->sum('local_income_tax')) }} | - {{ number_format($paymentsByUser->sum('total_deductions')) }} + {{ number_format($payments->sum('total_deductions')) }} | - {{ number_format($paymentsByUser->sum('net_amount')) }} + {{ number_format($payments->sum('net_amount')) }} | -+ | |||||||||