leaveService->getLeaves( $request->all(), $request->integer('per_page', 20) ); if ($request->header('HX-Request')) { return response(view('hr.leaves.partials.table', compact('leaves'))); } return response()->json([ 'success' => true, 'data' => $leaves->items(), 'meta' => [ 'current_page' => $leaves->currentPage(), 'last_page' => $leaves->lastPage(), 'per_page' => $leaves->perPage(), 'total' => $leaves->total(), ], ]); } /** * 휴가 신청 등록 */ 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', 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', 'reason' => 'nullable|string|max:1000', ]); try { $leave = $this->leaveService->storeLeave($validated); return response()->json([ 'success' => true, 'message' => '휴가 신청이 등록되었습니다.', 'data' => $leave, ], 201); } catch (\RuntimeException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 422); } 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 { $leave = $this->leaveService->approve($id); if (! $leave) { return response()->json([ 'success' => false, 'message' => '대기 중인 휴가 신청을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'message' => '승인 처리되었습니다.', 'data' => $leave, ]); } 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 { $leave = $this->leaveService->reject($id, $validated['reject_reason'] ?? null); if (! $leave) { return response()->json([ 'success' => false, 'message' => '대기 중인 휴가 신청을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'message' => '반려 처리되었습니다.', 'data' => $leave, ]); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '반려 처리 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * 취소 */ public function cancel(Request $request, int $id): JsonResponse { try { $leave = $this->leaveService->cancel($id); if (! $leave) { return response()->json([ 'success' => false, 'message' => '승인된 휴가 신청을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'message' => '취소 처리되었습니다. 연차가 복원되었습니다.', 'data' => $leave, ]); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '취소 처리 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * 잔여연차 목록 (HTMX → HTML) */ public function balance(Request $request): JsonResponse|Response { $year = $request->integer('year', now()->year); $sort = $request->input('sort', 'hire_date'); $direction = $request->input('direction', 'asc'); $balances = $this->leaveService->getBalanceSummary($year, $sort, $direction); if ($request->header('HX-Request')) { return response(view('hr.leaves.partials.balance', compact('balances', 'year', 'sort', 'direction'))); } return response()->json([ 'success' => true, 'data' => $balances, ]); } /** * 개별 사원 잔여연차 (JSON) */ public function userBalance(Request $request, int $userId): JsonResponse { $year = $request->integer('year', now()->year); $balance = $this->leaveService->getUserBalance($userId, $year); return response()->json([ 'success' => true, 'data' => $balance ? [ 'total_days' => $balance->total_days, 'used_days' => $balance->used_days, 'remaining_days' => $balance->remaining, ] : null, ]); } /** * 사용현황 통계 (HTMX → HTML) */ public function stats(Request $request): JsonResponse|Response { $year = $request->integer('year', now()->year); $stats = $this->leaveService->getUsageStats($year); if ($request->header('HX-Request')) { return response(view('hr.leaves.partials.stats', compact('stats'))); } return response()->json([ 'success' => true, 'data' => $stats, ]); } /** * CSV 내보내기 */ public function export(Request $request): StreamedResponse { $filters = $request->all(); $leaves = $this->leaveService->getExportData($filters); $headers = [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => 'attachment; filename="leaves_'.now()->format('Ymd_His').'.csv"', ]; return response()->stream(function () use ($leaves) { $output = fopen('php://output', 'w'); fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); // BOM fputcsv($output, ['사원', '부서', '유형', '시작일', '종료일', '일수', '사유', '상태', '승인자', '신청일']); foreach ($leaves as $leave) { $profile = $leave->user?->tenantProfiles?->first(); fputcsv($output, [ $leave->user?->name ?? '-', $profile?->department?->name ?? '-', $leave->type_label, $leave->start_date->format('Y-m-d'), $leave->end_date->format('Y-m-d'), $leave->days, $leave->reason ?? '-', $leave->status_label, $leave->approver?->name ?? '-', $leave->created_at->format('Y-m-d H:i'), ]); } fclose($output); }, 200, $headers); } }