diff --git a/app/Http/Controllers/Api/Admin/HR/AttendanceRequestController.php b/app/Http/Controllers/Api/Admin/HR/AttendanceRequestController.php deleted file mode 100644 index 4a623798..00000000 --- a/app/Http/Controllers/Api/Admin/HR/AttendanceRequestController.php +++ /dev/null @@ -1,141 +0,0 @@ -requestService->getRequests( - $request->all(), - $request->integer('per_page', 20) - ); - - if ($request->header('HX-Request')) { - return response(view('hr.attendances.partials.requests', compact('requests'))); - } - - return response()->json([ - 'success' => true, - 'data' => $requests->items(), - 'meta' => [ - 'current_page' => $requests->currentPage(), - 'last_page' => $requests->lastPage(), - 'per_page' => $requests->perPage(), - 'total' => $requests->total(), - ], - ]); - } - - /** - * 신청 등록 - */ - public function store(Request $request): JsonResponse - { - $validated = $request->validate([ - 'user_id' => 'required|integer|exists:users,id', - 'request_type' => 'required|string|in:vacation,businessTrip,remote,fieldWork', - 'start_date' => 'required|date', - 'end_date' => 'required|date|after_or_equal:start_date', - 'reason' => 'nullable|string|max:1000', - 'json_details' => 'nullable|array', - ]); - - try { - $attendanceRequest = $this->requestService->storeRequest($validated); - - return response()->json([ - 'success' => true, - 'message' => '근태 신청이 등록되었습니다.', - 'data' => $attendanceRequest, - ], 201); - } catch (\Throwable $e) { - report($e); - - return response()->json([ - 'success' => false, - 'message' => '신청 등록 중 오류가 발생했습니다.', - 'error' => config('app.debug') ? $e->getMessage() : null, - ], 500); - } - } - - /** - * 승인 - */ - public function approve(Request $request, int $id): JsonResponse - { - try { - $attendanceRequest = $this->requestService->approve($id); - - if (! $attendanceRequest) { - return response()->json([ - 'success' => false, - 'message' => '대기 중인 신청을 찾을 수 없습니다.', - ], 404); - } - - return response()->json([ - 'success' => true, - 'message' => '승인 처리되었습니다. 근태 레코드가 자동 생성되었습니다.', - 'data' => $attendanceRequest, - ]); - } catch (\Throwable $e) { - report($e); - - return response()->json([ - 'success' => false, - 'message' => '승인 처리 중 오류가 발생했습니다.', - 'error' => config('app.debug') ? $e->getMessage() : null, - ], 500); - } - } - - /** - * 반려 - */ - public function reject(Request $request, int $id): JsonResponse - { - $validated = $request->validate([ - 'reject_reason' => 'nullable|string|max:1000', - ]); - - try { - $attendanceRequest = $this->requestService->reject($id, $validated['reject_reason'] ?? null); - - if (! $attendanceRequest) { - return response()->json([ - 'success' => false, - 'message' => '대기 중인 신청을 찾을 수 없습니다.', - ], 404); - } - - return response()->json([ - 'success' => true, - 'message' => '반려 처리되었습니다.', - 'data' => $attendanceRequest, - ]); - } catch (\Throwable $e) { - report($e); - - return response()->json([ - 'success' => false, - 'message' => '반려 처리 중 오류가 발생했습니다.', - 'error' => config('app.debug') ? $e->getMessage() : null, - ], 500); - } - } -} diff --git a/app/Http/Controllers/Api/Admin/HR/LeaveController.php b/app/Http/Controllers/Api/Admin/HR/LeaveController.php index 0b7006fb..b1e90ad3 100644 --- a/app/Http/Controllers/Api/Admin/HR/LeaveController.php +++ b/app/Http/Controllers/Api/Admin/HR/LeaveController.php @@ -48,7 +48,7 @@ public function store(Request $request): JsonResponse { $validated = $request->validate([ 'user_id' => 'required|integer|exists:users,id', - 'leave_type' => 'required|string|in:annual,half_am,half_pm,sick,family,maternity,parental', + 'leave_type' => 'required|string|in:annual,half_am,half_pm,sick,family,maternity,parental,business_trip,remote,field_work,early_leave,late_reason,absent_reason', 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', 'reason' => 'nullable|string|max:1000', @@ -176,6 +176,36 @@ public function cancel(Request $request, int $id): JsonResponse } } + /** + * pending 상태 신청 삭제 + */ + public function destroy(Request $request, int $id): JsonResponse + { + try { + $leave = $this->leaveService->deletePendingLeave($id); + + if (! $leave) { + return response()->json([ + 'success' => false, + 'message' => '삭제 가능한 신청을 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'message' => '신청이 삭제되었습니다.', + ]); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '삭제 중 오류가 발생했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + /** * 잔여연차 목록 (HTMX → HTML) */ diff --git a/app/Http/Controllers/HR/AttendanceIntegratedController.php b/app/Http/Controllers/HR/AttendanceIntegratedController.php new file mode 100644 index 00000000..ef1f586c --- /dev/null +++ b/app/Http/Controllers/HR/AttendanceIntegratedController.php @@ -0,0 +1,54 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('hr.attendance.index')); + } + + $stats = $this->attendanceService->getMonthlyStats(); + $departments = $this->leaveService->getDepartments(); + $employees = $this->leaveService->getActiveEmployees(); + $statusMap = Attendance::STATUS_MAP; + $leaveTypeMap = Leave::TYPE_MAP; + $leaveStatusMap = Leave::STATUS_MAP; + + // 결재선 목록 + $approvalLines = \App\Models\Approvals\ApprovalLine::query() + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('name') + ->get(['id', 'name', 'is_default']); + + return view('hr.attendance-integrated.index', [ + 'stats' => $stats, + 'departments' => $departments, + 'employees' => $employees, + 'statusMap' => $statusMap, + 'leaveTypeMap' => $leaveTypeMap, + 'leaveStatusMap' => $leaveStatusMap, + 'approvalLines' => $approvalLines, + ]); + } +} diff --git a/app/Models/HR/AttendanceRequest.php b/app/Models/HR/AttendanceRequest.php deleted file mode 100644 index 2e0fb75c..00000000 --- a/app/Models/HR/AttendanceRequest.php +++ /dev/null @@ -1,93 +0,0 @@ - 'int', - 'user_id' => 'int', - 'approved_by' => 'int', - 'start_date' => 'date', - 'end_date' => 'date', - 'approved_at' => 'datetime', - 'json_details' => 'array', - ]; - - public const TYPE_MAP = [ - 'vacation' => '휴가', - 'businessTrip' => '출장', - 'remote' => '재택', - 'fieldWork' => '외근', - ]; - - public const STATUS_MAP = [ - 'pending' => '대기', - 'approved' => '승인', - 'rejected' => '반려', - ]; - - public const STATUS_COLORS = [ - 'pending' => 'amber', - 'approved' => 'emerald', - 'rejected' => 'red', - ]; - - public function user(): BelongsTo - { - return $this->belongsTo(User::class, 'user_id'); - } - - public function approver(): BelongsTo - { - return $this->belongsTo(User::class, 'approved_by'); - } - - public function getTypeLabelAttribute(): string - { - return self::TYPE_MAP[$this->request_type] ?? $this->request_type; - } - - public function getStatusLabelAttribute(): string - { - return self::STATUS_MAP[$this->status] ?? $this->status; - } - - public function getStatusColorAttribute(): string - { - return self::STATUS_COLORS[$this->status] ?? 'gray'; - } - - public function scopeForTenant($query, ?int $tenantId = null) - { - $tenantId = $tenantId ?? session('selected_tenant_id'); - if ($tenantId) { - return $query->where($this->table.'.tenant_id', $tenantId); - } - - return $query; - } -} diff --git a/app/Models/HR/Leave.php b/app/Models/HR/Leave.php index b94c9f68..ce4986bf 100644 --- a/app/Models/HR/Leave.php +++ b/app/Models/HR/Leave.php @@ -58,6 +58,45 @@ class Leave extends Model 'family' => '경조사', 'maternity' => '출산', 'parental' => '육아', + 'business_trip' => '출장', + 'remote' => '재택근무', + 'field_work' => '외근', + 'early_leave' => '조퇴', + 'late_reason' => '지각사유서', + 'absent_reason' => '결근사유서', + ]; + + // 그룹 상수 + public const VACATION_TYPES = ['annual', 'half_am', 'half_pm', 'sick', 'family', 'maternity', 'parental']; + + public const ATTENDANCE_REQUEST_TYPES = ['business_trip', 'remote', 'field_work', 'early_leave']; + + public const REASON_REPORT_TYPES = ['late_reason', 'absent_reason']; + + // 유형 → 결재양식코드 매핑 + public const FORM_CODE_MAP = [ + 'annual' => 'leave', 'half_am' => 'leave', 'half_pm' => 'leave', + 'sick' => 'leave', 'family' => 'leave', 'maternity' => 'leave', 'parental' => 'leave', + 'business_trip' => 'attendance_request', 'remote' => 'attendance_request', + 'field_work' => 'attendance_request', 'early_leave' => 'attendance_request', + 'late_reason' => 'reason_report', 'absent_reason' => 'reason_report', + ]; + + // 유형 → 근태상태 매핑 (승인 시 Attendance에 반영할 상태) + public const ATTENDANCE_STATUS_MAP = [ + 'annual' => 'vacation', 'half_am' => 'vacation', 'half_pm' => 'vacation', + 'sick' => 'vacation', 'family' => 'vacation', 'maternity' => 'vacation', 'parental' => 'vacation', + 'business_trip' => 'businessTrip', 'remote' => 'remote', 'field_work' => 'fieldWork', + 'early_leave' => null, + 'late_reason' => null, + 'absent_reason' => null, + ]; + + // 그룹별 라벨 + public const GROUP_LABELS = [ + 'vacation' => '휴가', + 'attendance_request' => '근태신청', + 'reason_report' => '사유서', ]; public const STATUS_MAP = [ diff --git a/app/Services/ApprovalService.php b/app/Services/ApprovalService.php index 032ac1eb..8dda23a0 100644 --- a/app/Services/ApprovalService.php +++ b/app/Services/ApprovalService.php @@ -711,11 +711,19 @@ public function getBadgeCounts(int $userId): array // ========================================================================= /** - * 결재 최종 승인 시 연동 처리 (휴가 등) + * 휴가/근태신청/사유서 관련 결재 양식인지 확인 + */ + private function isLeaveRelatedForm(?string $code): bool + { + return in_array($code, ['leave', 'attendance_request', 'reason_report']); + } + + /** + * 결재 최종 승인 시 연동 처리 (휴가/근태신청/사유서) */ private function handleApprovalCompleted(Approval $approval): void { - if (! $approval->form || $approval->form->code !== 'leave') { + if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) { return; } @@ -726,11 +734,11 @@ private function handleApprovalCompleted(Approval $approval): void } /** - * 결재 반려 시 연동 처리 (휴가 등) + * 결재 반려 시 연동 처리 (휴가/근태신청/사유서) */ private function handleApprovalRejected(Approval $approval, string $comment): void { - if (! $approval->form || $approval->form->code !== 'leave') { + if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) { return; } @@ -745,11 +753,11 @@ private function handleApprovalRejected(Approval $approval, string $comment): vo } /** - * 결재 삭제 시 연동 처리 (휴가 등) + * 결재 삭제 시 연동 처리 (휴가/근태신청/사유서) */ private function handleApprovalDeleted(Approval $approval): void { - if (! $approval->form || $approval->form->code !== 'leave') { + if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) { return; } @@ -763,11 +771,11 @@ private function handleApprovalDeleted(Approval $approval): void } /** - * 결재 회수 시 연동 처리 (휴가 등) + * 결재 회수 시 연동 처리 (휴가/근태신청/사유서) */ private function handleApprovalCancelled(Approval $approval): void { - if (! $approval->form || $approval->form->code !== 'leave') { + if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) { return; } diff --git a/app/Services/HR/AttendanceRequestService.php b/app/Services/HR/AttendanceRequestService.php deleted file mode 100644 index 8bfc534a..00000000 --- a/app/Services/HR/AttendanceRequestService.php +++ /dev/null @@ -1,148 +0,0 @@ -with(['user', 'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId), 'user.tenantProfiles.department', 'approver']) - ->forTenant($tenantId) - ->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected')") - ->orderBy('created_at', 'desc'); - - if (! empty($filters['status'])) { - $query->where('status', $filters['status']); - } - - if (! empty($filters['user_id'])) { - $query->where('user_id', $filters['user_id']); - } - - return $query->paginate($perPage); - } - - /** - * 신청 등록 - */ - public function storeRequest(array $data): AttendanceRequest - { - $tenantId = session('selected_tenant_id'); - - return AttendanceRequest::create([ - 'tenant_id' => $tenantId, - 'user_id' => $data['user_id'], - 'request_type' => $data['request_type'], - 'start_date' => $data['start_date'], - 'end_date' => $data['end_date'], - 'reason' => $data['reason'] ?? null, - 'status' => 'pending', - 'json_details' => $data['json_details'] ?? null, - ]); - } - - /** - * 승인 처리 - */ - public function approve(int $id): ?AttendanceRequest - { - $tenantId = session('selected_tenant_id'); - - $request = AttendanceRequest::query() - ->forTenant($tenantId) - ->where('status', 'pending') - ->find($id); - - if (! $request) { - return null; - } - - return DB::transaction(function () use ($request, $tenantId) { - $request->update([ - 'status' => 'approved', - 'approved_by' => auth()->id(), - 'approved_at' => now(), - ]); - - // 승인 시 해당 기간의 근태 레코드 자동 생성 - $this->createAttendanceRecords($request, $tenantId); - - return $request->fresh(['user', 'approver']); - }); - } - - /** - * 반려 처리 - */ - public function reject(int $id, ?string $reason = null): ?AttendanceRequest - { - $tenantId = session('selected_tenant_id'); - - $request = AttendanceRequest::query() - ->forTenant($tenantId) - ->where('status', 'pending') - ->find($id); - - if (! $request) { - return null; - } - - $request->update([ - 'status' => 'rejected', - 'approved_by' => auth()->id(), - 'approved_at' => now(), - 'reject_reason' => $reason, - ]); - - return $request->fresh(['user', 'approver']); - } - - /** - * 승인 후 근태 레코드 자동 생성 - */ - private function createAttendanceRecords(AttendanceRequest $request, int $tenantId): void - { - $statusMap = [ - 'vacation' => 'vacation', - 'businessTrip' => 'businessTrip', - 'remote' => 'remote', - 'fieldWork' => 'fieldWork', - ]; - - $status = $statusMap[$request->request_type] ?? $request->request_type; - - $period = CarbonPeriod::create($request->start_date, $request->end_date); - - foreach ($period as $date) { - // 주말 제외 - if ($date->isWeekend()) { - continue; - } - - Attendance::updateOrCreate( - [ - 'tenant_id' => $tenantId, - 'user_id' => $request->user_id, - 'base_date' => $date->toDateString(), - ], - [ - 'status' => $status, - 'remarks' => $request->reason ? mb_substr($request->reason, 0, 100) : null, - 'updated_by' => auth()->id(), - ] - ); - } - } -} diff --git a/app/Services/HR/AttendanceService.php b/app/Services/HR/AttendanceService.php index cde236a8..dc832eac 100644 --- a/app/Services/HR/AttendanceService.php +++ b/app/Services/HR/AttendanceService.php @@ -4,7 +4,6 @@ use App\Models\HR\Attendance; use App\Models\HR\Employee; -use App\Models\HR\LeaveBalance; use App\Models\Tenants\Department; use Carbon\Carbon; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -145,10 +144,7 @@ public function storeAttendance(array $data): Attendance $attendance->update(['created_by' => auth()->id()]); } - // 휴가 상태이면 연차 차감 - if (($data['status'] ?? '') === 'vacation') { - $this->deductLeaveBalance($tenantId, $data['user_id']); - } + // 휴가 상태: Leave 시스템에서 연차 차감 처리 (수동 등록 시에는 차감하지 않음) return $attendance->load('user'); }); @@ -202,9 +198,7 @@ public function bulkStore(array $data): array $updated++; } - if (($data['status'] ?? '') === 'vacation') { - $this->deductLeaveBalance($tenantId, $userId); - } + // 휴가 상태: Leave 시스템에서 연차 차감 처리 (일괄 등록 시에는 차감하지 않음) } }); @@ -508,26 +502,6 @@ public function getLeaveBalance(int $userId): ?LeaveBalance ->first(); } - /** - * 연차 차감 (remaining_days는 stored generated이므로 used_days만 업데이트) - */ - private function deductLeaveBalance(int $tenantId, int $userId): void - { - $year = now()->year; - - $balance = LeaveBalance::query() - ->where('tenant_id', $tenantId) - ->where('user_id', $userId) - ->where('year', $year) - ->first(); - - if ($balance && $balance->remaining_days > 0) { - $balance->update([ - 'used_days' => $balance->used_days + 1, - ]); - } - } - /** * 부서 목록 (드롭다운용) */ diff --git a/app/Services/HR/LeaveService.php b/app/Services/HR/LeaveService.php index fadd7195..8ae5a7c0 100644 --- a/app/Services/HR/LeaveService.php +++ b/app/Services/HR/LeaveService.php @@ -76,15 +76,22 @@ public function getLeaves(array $filters = [], int $perPage = 20): LengthAwarePa } /** - * 휴가 신청 등록 → 결재 자동 생성 + 상신 + * 휴가/근태신청/사유서 등록 → 결재 자동 생성 + 상신 */ public function storeLeave(array $data): Leave { $tenantId = session('selected_tenant_id'); - $days = $this->calculateDays($data['leave_type'], $data['start_date'], $data['end_date']); + $leaveType = $data['leave_type']; + + // 사유서는 days=0, 그 외는 자동 계산 + if (in_array($leaveType, Leave::REASON_REPORT_TYPES)) { + $days = 0; + } else { + $days = $this->calculateDays($leaveType, $data['start_date'], $data['end_date']); + } // 연차 차감 대상이면 잔여일수 검증 - if (in_array($data['leave_type'], Leave::DEDUCTIBLE_TYPES)) { + if (in_array($leaveType, Leave::DEDUCTIBLE_TYPES)) { $balance = LeaveBalance::query() ->forTenant($tenantId) ->forUser($data['user_id']) @@ -96,11 +103,11 @@ public function storeLeave(array $data): Leave } } - return DB::transaction(function () use ($data, $tenantId, $days) { + return DB::transaction(function () use ($data, $tenantId, $days, $leaveType) { $leave = Leave::create([ 'tenant_id' => $tenantId, 'user_id' => $data['user_id'], - 'leave_type' => $data['leave_type'], + 'leave_type' => $leaveType, 'start_date' => $data['start_date'], 'end_date' => $data['end_date'], 'days' => $days, @@ -110,7 +117,7 @@ public function storeLeave(array $data): Leave 'updated_by' => auth()->id(), ]); - // 결재 자동 생성 + 상신 (선택된 결재선 전달) + // 결재 자동 생성 + 상신 (유형에 맞는 결재양식 자동 선택) $approval = $this->createLeaveApproval($leave, $tenantId, $data['approval_line_id'] ?? null); $leave->update(['approval_id' => $approval->id]); @@ -119,7 +126,7 @@ public function storeLeave(array $data): Leave } /** - * 승인 → LeaveBalance 차감 → Attendance 자동 생성 + * 승인 → LeaveBalance 차감 → 유형별 Attendance 반영 */ public function approve(int $id): ?Leave { @@ -155,8 +162,8 @@ public function approve(int $id): ?Leave } } - // 기간 내 영업일마다 Attendance(vacation) 자동 생성 - $this->createAttendanceRecords($leave, $tenantId); + // 유형별 Attendance 반영 + $this->applyAttendanceByType($leave, $tenantId); return $leave->fresh(['user', 'approver']); }); @@ -269,13 +276,13 @@ public function deletePendingLeave(int $id): ?Leave } /** - * 결재 승인에 의한 휴가 자동 승인 + * 결재 승인에 의한 휴가/근태신청/사유서 자동 승인 */ public function approveByApproval(Leave $leave, Approval $approval): Leave { $tenantId = $leave->tenant_id; - // 최종 결재자 ID 찾기 (DB에서 fresh 조회, 기본 정렬 제거 후 역순) + // 최종 결재자 ID 찾기 $lastApprover = $approval->steps() ->where('status', 'approved') ->reorder('step_order', 'desc') @@ -288,7 +295,7 @@ public function approveByApproval(Leave $leave, Approval $approval): Leave 'updated_by' => auth()->id(), ]); - // 연차 차감 + // 연차 차감 (DEDUCTIBLE_TYPES만) if ($leave->is_deductible) { $balance = LeaveBalance::query() ->where('tenant_id', $tenantId) @@ -301,12 +308,74 @@ public function approveByApproval(Leave $leave, Approval $approval): Leave } } - // Attendance 생성 - $this->createAttendanceRecords($leave, $tenantId); + // 유형별 Attendance 반영 + $this->applyAttendanceByType($leave, $tenantId); return $leave->fresh(['user', 'approver']); } + /** + * 유형별 Attendance 반영 분기 + */ + private function applyAttendanceByType(Leave $leave, int $tenantId): void + { + $attendanceStatus = Leave::ATTENDANCE_STATUS_MAP[$leave->leave_type] ?? null; + + if ($attendanceStatus) { + // 휴가/출장/재택/외근 → Attendance 자동 생성 + $this->createAttendanceRecords($leave, $tenantId, $attendanceStatus); + } elseif ($leave->leave_type === 'early_leave') { + // 조퇴 → 기존 Attendance에 비고 기록 + $this->markEarlyLeave($leave, $tenantId); + } elseif (in_array($leave->leave_type, Leave::REASON_REPORT_TYPES)) { + // 지각/결근 사유서 → 기존 Attendance에 사유 기록 + $this->addReasonToAttendance($leave, $tenantId); + } + } + + /** + * 조퇴 승인 시 기존 Attendance에 비고 기록 + */ + private function markEarlyLeave(Leave $leave, int $tenantId): void + { + $attendance = Attendance::query() + ->where('tenant_id', $tenantId) + ->where('user_id', $leave->user_id) + ->where('base_date', $leave->start_date->toDateString()) + ->first(); + + if ($attendance) { + $remarks = $attendance->remarks; + $reason = $leave->reason ? " ({$leave->reason})" : ''; + $attendance->update([ + 'remarks' => trim(($remarks ? $remarks.' / ' : '').'조퇴'.$reason), + 'updated_by' => auth()->id(), + ]); + } + } + + /** + * 지각/결근 사유서 승인 시 기존 Attendance에 사유 기록 + */ + private function addReasonToAttendance(Leave $leave, int $tenantId): void + { + $attendance = Attendance::query() + ->where('tenant_id', $tenantId) + ->where('user_id', $leave->user_id) + ->where('base_date', $leave->start_date->toDateString()) + ->first(); + + if ($attendance) { + $typeName = $leave->leave_type === 'late_reason' ? '지각사유' : '결근사유'; + $reason = $leave->reason ?? ''; + $remarks = $attendance->remarks; + $attendance->update([ + 'remarks' => trim(($remarks ? $remarks.' / ' : '')."[{$typeName}] {$reason}"), + 'updated_by' => auth()->id(), + ]); + } + } + /** * 결재 반려에 의한 휴가 자동 반려 */ @@ -631,20 +700,23 @@ public function getActiveEmployees(): \Illuminate\Database\Eloquent\Collection // ========================================================================= /** - * 휴가신청 결재 자동 생성 + 상신 + * 휴가/근태신청/사유서 결재 자동 생성 + 상신 */ private function createLeaveApproval(Leave $leave, int $tenantId, ?int $approvalLineId = null): Approval { $approvalService = app(ApprovalService::class); - // 1. 휴가신청 양식 조회 - $form = ApprovalForm::where('code', 'leave') + // 1. 유형에 맞는 결재양식 조회 (FORM_CODE_MAP으로 동적 결정) + $formCode = Leave::FORM_CODE_MAP[$leave->leave_type] ?? 'leave'; + $form = ApprovalForm::where('code', $formCode) ->where('tenant_id', $tenantId) ->where('is_active', true) ->first(); if (! $form) { - throw new \RuntimeException('휴가신청 결재 양식이 등록되지 않았습니다.'); + $formNames = ['leave' => '휴가신청', 'attendance_request' => '근태신청', 'reason_report' => '사유서']; + $formName = $formNames[$formCode] ?? $formCode; + throw new \RuntimeException("{$formName} 결재 양식이 등록되지 않았습니다."); } // 2. 결재선 조회: 지정된 ID 우선, 없으면 기본결재선 @@ -671,15 +743,21 @@ private function createLeaveApproval(Leave $leave, int $tenantId, ?int $approval 'step_type' => $s['step_type'] ?? $s['type'] ?? 'approval', ])->toArray(); - // 5. 결재 생성 + // 5. 결재 제목 생성 (유형별 차별화) $typeName = Leave::TYPE_MAP[$leave->leave_type] ?? $leave->leave_type; $userName = $leave->user->name ?? ''; $period = $leave->start_date->format('n/j').'~'.$leave->end_date->format('n/j'); + $titlePrefix = match ($formCode) { + 'attendance_request' => '근태신청', + 'reason_report' => '사유서', + default => '휴가신청', + }; + $approval = $approvalService->createApproval([ 'form_id' => $form->id, 'line_id' => $line->id, - 'title' => "휴가신청 - {$userName} ({$typeName} {$period})", + 'title' => "{$titlePrefix} - {$userName} ({$typeName} {$period})", 'body' => $body, 'content' => [ 'leave_id' => $leave->id, @@ -701,51 +779,64 @@ private function createLeaveApproval(Leave $leave, int $tenantId, ?int $approval } /** - * 결재 본문 HTML 생성 + * 결재 본문 HTML 생성 (유형별 차별화) */ private function buildLeaveApprovalBody(Leave $leave, int $tenantId): string { $user = $leave->user; $typeName = Leave::TYPE_MAP[$leave->leave_type] ?? $leave->leave_type; - $period = $leave->start_date->format('Y-m-d').' ~ '.$leave->end_date->format('Y-m-d'); - $daysStr = ($leave->days == (int) $leave->days) ? (int) $leave->days.'일' : $leave->days.'일'; - - // 잔여연차 정보 - $balanceInfo = ''; - if (in_array($leave->leave_type, Leave::DEDUCTIBLE_TYPES)) { - $balance = LeaveBalance::query() - ->where('tenant_id', $tenantId) - ->where('user_id', $leave->user_id) - ->where('year', now()->year) - ->first(); - - if ($balance) { - $balanceInfo = $balance->remaining.'일' - .' (부여: '.$balance->total_days.' / 사용: '.$balance->used_days.')'; - } - } + $formCode = Leave::FORM_CODE_MAP[$leave->leave_type] ?? 'leave'; // 부서 정보 $profile = $user?->tenantProfiles?->where('tenant_id', $tenantId)->first(); $deptName = $profile?->department?->name ?? ''; - // HTML 테이블 본문 - $rows = [ - ['신청자', e($user->name ?? '')], - ]; + // 공통 행 + $rows = [['신청자', e($user->name ?? '')]]; if ($deptName) { $rows[] = ['부서', e($deptName)]; } - $rows[] = ['휴가유형', $typeName]; - $rows[] = ['기간', $period.' ('.$daysStr.')']; - if ($leave->reason) { - $rows[] = ['사유', e($leave->reason)]; - } - if ($balanceInfo) { - $rows[] = ['잔여연차', $balanceInfo]; + $rows[] = ['유형', $typeName]; + + // 사유서: 대상일 + 사유 + if ($formCode === 'reason_report') { + $rows[] = ['대상일', $leave->start_date->format('Y-m-d')]; + if ($leave->reason) { + $rows[] = ['사유', e($leave->reason)]; + } + + $intro = '아래와 같이 사유서를 제출합니다.'; + } else { + // 휴가/근태신청: 기간 + 일수 + $period = $leave->start_date->format('Y-m-d').' ~ '.$leave->end_date->format('Y-m-d'); + $daysStr = ($leave->days == (int) $leave->days) ? (int) $leave->days.'일' : $leave->days.'일'; + $rows[] = ['기간', $period.' ('.$daysStr.')']; + + if ($leave->reason) { + $rows[] = ['사유', e($leave->reason)]; + } + + // 잔여연차 (연차 차감 대상만) + if (in_array($leave->leave_type, Leave::DEDUCTIBLE_TYPES)) { + $balance = LeaveBalance::query() + ->where('tenant_id', $tenantId) + ->where('user_id', $leave->user_id) + ->where('year', now()->year) + ->first(); + + if ($balance) { + $rows[] = ['잔여연차', $balance->remaining.'일 (부여: '.$balance->total_days.' / 사용: '.$balance->used_days.')']; + } + } + + $intro = match ($formCode) { + 'attendance_request' => '아래와 같이 근태를 신청합니다.', + default => '아래와 같이 휴가를 신청합니다.', + }; } - $html = '
아래와 같이 휴가를 신청합니다.
'; + // HTML 테이블 생성 + $html = "{$intro}
"; $html .= '| 신청자 | -유형 | -기간 | -사유 | -상태 | -처리자 | -작업 | -
|---|---|---|---|---|---|---|
|
-
-
-
- {{ mb_substr($displayName, 0, 1) }}
-
- {{ $displayName }}
- |
- - - {{ $typeLabel }} - - | -{{ $dateRange }} | -- {{ $req->reason ?? '-' }} - | -- - {{ $statusLabel }} - - | -
- @if($req->approved_by)
- {{ $req->approver?->name ?? '-' }}
- {{ $req->approved_at?->format('m/d H:i') }}
- @else
- -
- @endif
- |
-
- @if($req->status === 'pending')
-
-
-
-
- @elseif($req->status === 'rejected' && $req->reject_reason)
-
- 사유: {{ mb_substr($req->reject_reason, 0, 20) }}{{ mb_strlen($req->reject_reason) > 20 ? '...' : '' }}
-
- @else
- -
- @endif
- |
-