fix teacher attendance
API CI/CD / Validate (composer + pint) (push) Successful in 3m10s
API CI/CD / Test (PHPUnit) (push) Failing after 5m2s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 49s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
API CI/CD / Validate (composer + pint) (push) Successful in 3m10s
API CI/CD / Test (PHPUnit) (push) Failing after 5m2s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 49s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
This commit is contained in:
@@ -42,7 +42,9 @@ class TeacherAttendanceApiController extends Controller
|
|||||||
return new TeacherAttendanceFormResource(
|
return new TeacherAttendanceFormResource(
|
||||||
$this->queryService->teacherAttendanceFormData(
|
$this->queryService->teacherAttendanceFormData(
|
||||||
$guard,
|
$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) {
|
} catch (Throwable $e) {
|
||||||
@@ -68,7 +70,7 @@ class TeacherAttendanceApiController extends Controller
|
|||||||
|
|
||||||
private function authenticatedUserIdOrUnauthorized(): int|JsonResponse
|
private function authenticatedUserIdOrUnauthorized(): int|JsonResponse
|
||||||
{
|
{
|
||||||
$userId = (int) (auth()->id() ?? 0);
|
$userId = (int) (auth('api')->id() ?? auth()->id() ?? 0);
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
return response()->json(['message' => 'Unauthorized.'], 401);
|
return response()->json(['message' => 'Unauthorized.'], 401);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ class MultiAuth
|
|||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next): Response
|
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 {
|
try {
|
||||||
$sanctumUser = Auth::guard('sanctum')->user();
|
$sanctumUser = Auth::guard('sanctum')->user();
|
||||||
} catch (\InvalidArgumentException) {
|
} catch (\InvalidArgumentException) {
|
||||||
@@ -30,19 +43,6 @@ class MultiAuth
|
|||||||
return $next($request);
|
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();
|
$token = $request->bearerToken();
|
||||||
if ($token) {
|
if ($token) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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) {
|
if (! $userId) {
|
||||||
throw new RuntimeException('User not logged in.');
|
throw new RuntimeException('User not logged in.');
|
||||||
}
|
}
|
||||||
@@ -134,8 +138,12 @@ class AttendanceQueryService
|
|||||||
? trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? ''))
|
? trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? ''))
|
||||||
: 'Unknown Teacher';
|
: 'Unknown Teacher';
|
||||||
|
|
||||||
$semester = $this->attendanceService->currentSemester();
|
$semester = trim((string) $requestedSemester) !== ''
|
||||||
$schoolYear = $this->attendanceService->currentSchoolYear();
|
? trim((string) $requestedSemester)
|
||||||
|
: $this->attendanceService->currentSemester();
|
||||||
|
$schoolYear = trim((string) $requestedSchoolYear) !== ''
|
||||||
|
? trim((string) $requestedSchoolYear)
|
||||||
|
: $this->attendanceService->currentSchoolYear();
|
||||||
|
|
||||||
$assignments = $this->teacherClass->getClassAssignmentsByUserId($userId, $schoolYear, $semester);
|
$assignments = $this->teacherClass->getClassAssignmentsByUserId($userId, $schoolYear, $semester);
|
||||||
|
|
||||||
@@ -187,7 +195,9 @@ class AttendanceQueryService
|
|||||||
implode(',', array_fill(0, count($sundayDates), '?')).')';
|
implode(',', array_fill(0, count($sundayDates), '?')).')';
|
||||||
|
|
||||||
foreach ($students as $student) {
|
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']);
|
unset($student['created_by'], $student['updated_by'], $student['deleted_at']);
|
||||||
|
|
||||||
$studentId = (int) $student['id'];
|
$studentId = (int) $student['id'];
|
||||||
|
|||||||
@@ -93,8 +93,11 @@ class TeacherAttendanceSubmissionService
|
|||||||
'semester' => $semester,
|
'semester' => $semester,
|
||||||
'school_year' => $schoolYear,
|
'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.');
|
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 (
|
DB::transaction(function () use (
|
||||||
$user,
|
$user,
|
||||||
$assignedTeachers,
|
$assignedTeachers,
|
||||||
@@ -172,6 +173,15 @@ class TeacherAttendanceSubmissionService
|
|||||||
$position = strtolower((string) ($teacher['position'] ?? 'main'));
|
$position = strtolower((string) ($teacher['position'] ?? 'main'));
|
||||||
$position = $position === 'ta' ? 'ta' : '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 = [
|
$staffPayload = [
|
||||||
'role_name' => 'Teacher',
|
'role_name' => 'Teacher',
|
||||||
'status' => strtolower((string) ($teachersData[$teacherId]['status'] ?? 'present')),
|
'status' => strtolower((string) ($teachersData[$teacherId]['status'] ?? 'present')),
|
||||||
@@ -204,11 +214,8 @@ class TeacherAttendanceSubmissionService
|
|||||||
$existing = $existingByStudent[$studentId] ?? null;
|
$existing = $existingByStudent[$studentId] ?? null;
|
||||||
|
|
||||||
if ($isTeacher && $existing) {
|
if ($isTeacher && $existing) {
|
||||||
$lockInfo = $detectExternalSubmission($existing);
|
|
||||||
if (($lockInfo['locked'] ?? false) === true) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$this->studentAttendanceWriterService->saveOrUpdateStudentAttendance([
|
$this->studentAttendanceWriterService->saveOrUpdateStudentAttendance([
|
||||||
'class_section_id' => $classSectionId,
|
'class_section_id' => $classSectionId,
|
||||||
|
|||||||
+4
-1
@@ -509,10 +509,13 @@ Route::prefix('v1')->group(function () {
|
|||||||
Route::get('progress/meta', [ClassProgressController::class, 'meta']);
|
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('/', [SchoolYearController::class, 'index']);
|
||||||
Route::get('current', [SchoolYearController::class, 'current']);
|
Route::get('current', [SchoolYearController::class, 'current']);
|
||||||
Route::get('options', [SchoolYearController::class, 'options']);
|
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('summary', [SchoolYearController::class, 'summarySelected'])->middleware('perm:school_year.view');
|
||||||
Route::get('reports/closing', [SchoolYearController::class, 'closingReportSelected'])->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');
|
Route::post('validate-close', [SchoolYearController::class, 'validateCloseSelected'])->middleware('perm:school_year.close');
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase
|
|||||||
['POST', '/api/v1/users'],
|
['POST', '/api/v1/users'],
|
||||||
['GET', '/api/v1/administrator/dashboard/metrics'],
|
['GET', '/api/v1/administrator/dashboard/metrics'],
|
||||||
['GET', '/api/v1/administrator/staff'],
|
['GET', '/api/v1/administrator/staff'],
|
||||||
['GET', '/api/v1/school-years'],
|
|
||||||
['GET', '/api/v1/administrator/role-permission/roles'],
|
['GET', '/api/v1/administrator/role-permission/roles'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
public function test_admin_views_daily_attendance_summary(): void
|
||||||
{
|
{
|
||||||
$world = $this->seedTeacherClassWithStudent();
|
$world = $this->seedTeacherClassWithStudent();
|
||||||
|
|||||||
Reference in New Issue
Block a user