attendanceService->getAttendances( $request->all(), $request->integer('per_page', 20) ); if ($request->header('HX-Request')) { $viewName = $request->input('view') === 'manage' ? 'hr.attendances.partials.table-manage' : 'hr.attendances.partials.table'; return response(view($viewName, compact('attendances'))); } return response()->json([ 'success' => true, 'data' => $attendances->items(), 'meta' => [ 'current_page' => $attendances->currentPage(), 'last_page' => $attendances->lastPage(), 'per_page' => $attendances->perPage(), 'total' => $attendances->total(), ], ]); } /** * 월간 통계 (HTMX → HTML / 일반 → JSON) */ public function stats(Request $request): JsonResponse|Response { $stats = $this->attendanceService->getMonthlyStats( $request->integer('year') ?: null, $request->integer('month') ?: null ); if ($request->header('HX-Request')) { return response(view('hr.attendances.partials.stats', compact('stats'))); } return response()->json([ 'success' => true, 'data' => $stats, ]); } /** * 월간 캘린더 (HTMX → HTML) */ public function calendar(Request $request): JsonResponse|Response { $year = $request->integer('year') ?: now()->year; $month = $request->integer('month') ?: now()->month; $userId = $request->integer('user_id') ?: null; $attendances = $this->attendanceService->getMonthlyCalendarData($year, $month, $userId); $calendarData = $attendances->groupBy(fn ($att) => $att->base_date->format('Y-m-d')); $employees = $this->attendanceService->getActiveEmployees(); if ($request->header('HX-Request')) { return response(view('hr.attendances.partials.calendar', compact( 'year', 'month', 'calendarData', 'employees' ))->with('selectedUserId', $userId)); } return response()->json([ 'success' => true, 'data' => $calendarData, ]); } /** * 사원별 월간 요약 (HTMX → HTML) */ public function summary(Request $request): JsonResponse|Response { $year = $request->integer('year') ?: now()->year; $month = $request->integer('month') ?: now()->month; $summary = $this->attendanceService->getEmployeeMonthlySummary($year, $month); if ($request->header('HX-Request')) { return response(view('hr.attendances.partials.summary', compact('summary', 'year', 'month'))); } return response()->json([ 'success' => true, 'data' => $summary, ]); } /** * 초과근무 알림 (HTMX → HTML) */ public function overtimeAlerts(Request $request): JsonResponse|Response { $alerts = $this->attendanceService->getOvertimeAlerts(); if ($request->header('HX-Request')) { return response(view('hr.attendances.partials.overtime-alerts', compact('alerts'))); } return response()->json([ 'success' => true, 'data' => $alerts, ]); } /** * 잔여 연차 조회 */ public function leaveBalance(Request $request, int $userId): JsonResponse { $balance = $this->attendanceService->getLeaveBalance($userId); return response()->json([ 'success' => true, 'data' => [ 'total' => $balance?->total ?? 0, 'used' => $balance?->used ?? 0, 'remaining' => $balance?->remaining ?? 0, ], ]); } /** * 엑셀(CSV) 내보내기 */ public function export(Request $request): StreamedResponse { $attendances = $this->attendanceService->getExportData($request->all()); $tenantId = session('selected_tenant_id'); $filename = '근태현황_'.now()->format('Ymd').'.csv'; return response()->streamDownload(function () use ($attendances) { $file = fopen('php://output', 'w'); fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM fputcsv($file, ['날짜', '사원명', '부서', '상태', '출근', '퇴근', '비고']); foreach ($attendances as $att) { $profile = $att->user?->tenantProfiles?->first(); $displayName = $profile?->display_name ?? $att->user?->name ?? '-'; $department = $profile?->department?->name ?? '-'; $statusLabel = Attendance::STATUS_MAP[$att->status] ?? $att->status; $checkIn = $att->check_in ? substr($att->check_in, 0, 5) : ''; $checkOut = $att->check_out ? substr($att->check_out, 0, 5) : ''; fputcsv($file, [ $att->base_date->format('Y-m-d'), $displayName, $department, $statusLabel, $checkIn, $checkOut, $att->remarks ?? '', ]); } fclose($file); }, $filename, [ 'Content-Type' => 'text/csv; charset=UTF-8', ]); } /** * 일괄 삭제 */ public function bulkDestroy(Request $request): JsonResponse|Response { $validated = $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer', ]); try { $count = $this->attendanceService->bulkDelete($validated['ids']); if ($request->header('HX-Request')) { $attendances = $this->attendanceService->getAttendances( $request->except('ids'), $request->integer('per_page', 20) ); return response(view('hr.attendances.partials.table', compact('attendances'))); } return response()->json([ 'success' => true, 'message' => "{$count}건의 근태가 삭제되었습니다.", ]); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '일괄 삭제 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * 근태 등록 */ public function store(Request $request): JsonResponse { $validated = $request->validate([ 'user_id' => 'required|integer|exists:users,id', 'base_date' => 'required|date', 'status' => 'required|string|in:onTime,late,absent,vacation,businessTrip,fieldWork,overtime,remote', 'check_in' => 'nullable|date_format:H:i', 'check_out' => 'nullable|date_format:H:i', 'remarks' => 'nullable|string|max:500', ]); try { $attendance = $this->attendanceService->storeAttendance($validated); return response()->json([ 'success' => true, 'message' => '근태가 등록되었습니다.', 'data' => $attendance, ], 201); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '근태 등록 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * 일괄 등록 */ public function bulkStore(Request $request): JsonResponse { $validated = $request->validate([ 'user_ids' => 'required|array|min:1', 'user_ids.*' => 'integer|exists:users,id', 'base_date' => 'required|date', 'status' => 'required|string|in:onTime,late,absent,vacation,businessTrip,fieldWork,overtime,remote', 'check_in' => 'nullable|date_format:H:i', 'check_out' => 'nullable|date_format:H:i', 'remarks' => 'nullable|string|max:500', ]); try { $result = $this->attendanceService->bulkStore($validated); return response()->json([ 'success' => true, 'message' => "신규 {$result['created']}건, 수정 {$result['updated']}건 처리되었습니다.", 'data' => $result, ], 201); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '일괄 등록 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * 근태 수정 */ public function update(Request $request, int $id): JsonResponse { $validated = $request->validate([ 'status' => 'sometimes|required|string|in:onTime,late,absent,vacation,businessTrip,fieldWork,overtime,remote', 'check_in' => 'nullable|date_format:H:i', 'check_out' => 'nullable|date_format:H:i', 'remarks' => 'nullable|string|max:500', ]); try { $attendance = $this->attendanceService->updateAttendance($id, $validated); if (! $attendance) { return response()->json([ 'success' => false, 'message' => '근태 정보를 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'message' => '근태가 수정되었습니다.', 'data' => $attendance, ]); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '근태 수정 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } /** * 근태 삭제 */ public function destroy(Request $request, int $id): JsonResponse|Response { try { $force = $request->boolean('force'); if ($force) { if (! auth()->user()->isSuperAdmin()) { return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403); } $result = $this->attendanceService->forceDeleteAttendance($id); $message = '근태가 영구 삭제되었습니다.'; } else { $result = $this->attendanceService->deleteAttendance($id); $message = '근태가 삭제되었습니다.'; } if (! $result) { return response()->json([ 'success' => false, 'message' => '근태 정보를 찾을 수 없습니다.', ], 404); } if ($request->header('HX-Request')) { $attendances = $this->attendanceService->getAttendances( $request->all(), $request->integer('per_page', 20) ); return response(view('hr.attendances.partials.table', compact('attendances'))); } return response()->json([ 'success' => true, 'message' => $message, ]); } catch (\Throwable $e) { report($e); return response()->json([ 'success' => false, 'message' => '근태 삭제 중 오류가 발생했습니다.', 'error' => config('app.debug') ? $e->getMessage() : null, ], 500); } } }