From c891a82012ae86b3012d7d098552e83a02fa4aef Mon Sep 17 00:00:00 2001 From: root Date: Tue, 7 Jul 2026 21:51:51 -0400 Subject: [PATCH] fix teacher attendance --- .../TeacherAttendanceApiController.php | 6 ++- app/Http/Middleware/MultiAuth.php | 26 +++++----- .../Attendance/AttendanceQueryService.php | 20 +++++-- .../TeacherAttendanceSubmissionService.php | 21 +++++--- routes/api.php | 5 +- .../ApiAuthenticationAndAuthorizationTest.php | 1 - .../ScenarioBSundaySchoolDayTest.php | 52 +++++++++++++++++++ 7 files changed, 102 insertions(+), 29 deletions(-) diff --git a/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php b/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php index fbefa6cd..3a807c3c 100644 --- a/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php +++ b/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php @@ -42,7 +42,9 @@ class TeacherAttendanceApiController extends Controller return new TeacherAttendanceFormResource( $this->queryService->teacherAttendanceFormData( $guard, - (int) $request->query('class_section_id', 0) + (int) $request->query('class_section_id', 0), + $request->query('school_year'), + $request->query('semester') ) ); } catch (Throwable $e) { @@ -68,7 +70,7 @@ class TeacherAttendanceApiController extends Controller private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { - $userId = (int) (auth()->id() ?? 0); + $userId = (int) (auth('api')->id() ?? auth()->id() ?? 0); if ($userId <= 0) { return response()->json(['message' => 'Unauthorized.'], 401); } diff --git a/app/Http/Middleware/MultiAuth.php b/app/Http/Middleware/MultiAuth.php index 7ec6e239..17acd70d 100644 --- a/app/Http/Middleware/MultiAuth.php +++ b/app/Http/Middleware/MultiAuth.php @@ -14,6 +14,19 @@ class MultiAuth { public function handle(Request $request, Closure $next): Response { + try { + if ($apiUser = Auth::guard('api')->user()) { + Auth::setUser($apiUser); + + if ($response = $this->denyUnavailableAccount($apiUser)) { + return $response; + } + + return $next($request); + } + } catch (\Throwable) { + } + try { $sanctumUser = Auth::guard('sanctum')->user(); } catch (\InvalidArgumentException) { @@ -30,19 +43,6 @@ class MultiAuth return $next($request); } - try { - if ($apiUser = Auth::guard('api')->user()) { - Auth::setUser($apiUser); - - if ($response = $this->denyUnavailableAccount($apiUser)) { - return $response; - } - - return $next($request); - } - } catch (\Throwable) { - } - $token = $request->bearerToken(); if ($token) { try { diff --git a/app/Services/Attendance/AttendanceQueryService.php b/app/Services/Attendance/AttendanceQueryService.php index 34d9e795..8d9cf867 100644 --- a/app/Services/Attendance/AttendanceQueryService.php +++ b/app/Services/Attendance/AttendanceQueryService.php @@ -123,8 +123,12 @@ class AttendanceQueryService ]; } - public function teacherAttendanceFormData(?int $userId, int $requestedSectionId = 0): array - { + public function teacherAttendanceFormData( + ?int $userId, + int $requestedSectionId = 0, + ?string $requestedSchoolYear = null, + ?string $requestedSemester = null + ): array { if (! $userId) { throw new RuntimeException('User not logged in.'); } @@ -134,8 +138,12 @@ class AttendanceQueryService ? trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? '')) : 'Unknown Teacher'; - $semester = $this->attendanceService->currentSemester(); - $schoolYear = $this->attendanceService->currentSchoolYear(); + $semester = trim((string) $requestedSemester) !== '' + ? trim((string) $requestedSemester) + : $this->attendanceService->currentSemester(); + $schoolYear = trim((string) $requestedSchoolYear) !== '' + ? trim((string) $requestedSchoolYear) + : $this->attendanceService->currentSchoolYear(); $assignments = $this->teacherClass->getClassAssignmentsByUserId($userId, $schoolYear, $semester); @@ -187,7 +195,9 @@ class AttendanceQueryService implode(',', array_fill(0, count($sundayDates), '?')).')'; foreach ($students as $student) { - $student = is_array($student) ? $student : (array) $student; + $student = is_array($student) + ? $student + : (method_exists($student, 'toArray') ? $student->toArray() : (array) $student); unset($student['created_by'], $student['updated_by'], $student['deleted_at']); $studentId = (int) $student['id']; diff --git a/app/Services/Attendance/TeacherAttendanceSubmissionService.php b/app/Services/Attendance/TeacherAttendanceSubmissionService.php index 2b2c1ef5..16055592 100644 --- a/app/Services/Attendance/TeacherAttendanceSubmissionService.php +++ b/app/Services/Attendance/TeacherAttendanceSubmissionService.php @@ -93,8 +93,11 @@ class TeacherAttendanceSubmissionService 'semester' => $semester, 'school_year' => $schoolYear, ]; + $isTeacher = $this->attendancePolicyService->isTeacher($currentUser['roles']); + $dayStatus = strtolower((string) ($dayPolicyRow['status'] ?? 'draft')); - if (! $this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) { + if (! $this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser) + && ! ($isTeacher && $dayStatus === 'submitted')) { throw new RuntimeException('Attendance is locked for your role.'); } @@ -137,8 +140,6 @@ class TeacherAttendanceSubmissionService } } - $isTeacher = $this->attendancePolicyService->isTeacher($currentUser['roles']); - DB::transaction(function () use ( $user, $assignedTeachers, @@ -172,6 +173,15 @@ class TeacherAttendanceSubmissionService $position = strtolower((string) ($teacher['position'] ?? 'main')); $position = $position === 'ta' ? 'ta' : 'main'; + if ($isTeacher && DB::table('staff_attendance') + ->where('user_id', $teacherId) + ->whereDate('date', $attendDate) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->exists()) { + continue; + } + $staffPayload = [ 'role_name' => 'Teacher', 'status' => strtolower((string) ($teachersData[$teacherId]['status'] ?? 'present')), @@ -204,10 +214,7 @@ class TeacherAttendanceSubmissionService $existing = $existingByStudent[$studentId] ?? null; if ($isTeacher && $existing) { - $lockInfo = $detectExternalSubmission($existing); - if (($lockInfo['locked'] ?? false) === true) { - continue; - } + continue; } $this->studentAttendanceWriterService->saveOrUpdateStudentAttendance([ diff --git a/routes/api.php b/routes/api.php index b510bda2..9971e2e5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -509,10 +509,13 @@ Route::prefix('v1')->group(function () { Route::get('progress/meta', [ClassProgressController::class, 'meta']); }); - Route::middleware(['multi.auth', 'account.active', 'admin.access'])->prefix('school-years')->group(function () { + Route::middleware(['multi.auth', 'account.active'])->prefix('school-years')->group(function () { Route::get('/', [SchoolYearController::class, 'index']); Route::get('current', [SchoolYearController::class, 'current']); Route::get('options', [SchoolYearController::class, 'options']); + }); + + Route::middleware(['multi.auth', 'account.active', 'admin.access'])->prefix('school-years')->group(function () { Route::get('summary', [SchoolYearController::class, 'summarySelected'])->middleware('perm:school_year.view'); Route::get('reports/closing', [SchoolYearController::class, 'closingReportSelected'])->middleware('perm:school_year.view'); Route::post('validate-close', [SchoolYearController::class, 'validateCloseSelected'])->middleware('perm:school_year.close'); diff --git a/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php b/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php index edff0530..2e111fb5 100644 --- a/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php +++ b/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php @@ -125,7 +125,6 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase ['POST', '/api/v1/users'], ['GET', '/api/v1/administrator/dashboard/metrics'], ['GET', '/api/v1/administrator/staff'], - ['GET', '/api/v1/school-years'], ['GET', '/api/v1/administrator/role-permission/roles'], ]; } diff --git a/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php b/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php index eb0c5aea..b1542adb 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php @@ -82,6 +82,58 @@ class ScenarioBSundaySchoolDayTest extends TestCase ]); } + public function test_teacher_submission_does_not_edit_existing_attendance_entries(): void + { + $world = $this->seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + $studentId = $world['student_id']; + + $this->actingAs($teacher, 'api'); + + $payload = [ + 'class_section_id' => $classSectionId, + 'date' => '2025-10-05', + 'attendance' => [ + ['student_id' => $studentId, 'status' => 'present'], + ], + 'teachers' => [ + (string) $teacher->id => ['status' => 'present'], + ], + ]; + + $this->postJson('/api/v1/attendance/teacher/submit', $payload)->assertOk(); + + $payload['attendance'][0]['status'] = 'absent'; + $payload['attendance'][0]['reason'] = 'Changed after submit'; + $payload['teachers'][(string) $teacher->id] = [ + 'status' => 'late', + 'reason' => 'Changed after submit', + ]; + + $this->postJson('/api/v1/attendance/teacher/submit', $payload)->assertOk(); + + $this->assertDatabaseHas('attendance_data', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'date' => '2025-10-05', + 'status' => 'present', + 'reason' => null, + ]); + $this->assertDatabaseMissing('attendance_data', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'date' => '2025-10-05', + 'status' => 'absent', + ]); + $this->assertDatabaseHas('staff_attendance', [ + 'user_id' => $teacher->id, + 'date' => '2025-10-05', + 'status' => 'present', + 'reason' => null, + ]); + } + public function test_admin_views_daily_attendance_summary(): void { $world = $this->seedTeacherClassWithStudent();