diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php old mode 100755 new mode 100644 diff --git a/app/Http/Controllers/Api/CiRequestAdapter.php b/app/Http/Controllers/Api/CiRequestAdapter.php old mode 100755 new mode 100644 diff --git a/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php b/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php index 4680440b..318e1a49 100644 --- a/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php +++ b/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php @@ -12,6 +12,7 @@ use App\Http\Resources\ClassProgress\ClassProgressReportResource; use App\Http\Resources\ClassProgress\ClassProgressShowResource; use App\Models\ClassProgressAttachment; use App\Models\ClassProgressReport; +use App\Models\Configuration; use App\Models\User; use App\Services\ClassProgress\ClassProgressAttachmentService; use App\Services\ClassProgress\ClassProgressMetaService; @@ -36,14 +37,43 @@ class ClassProgressController extends BaseApiController public function meta(ClassProgressMetaRequest $request): JsonResponse { - $classId = $request->validated('class_id'); - $sundayCount = (int) ($request->validated('sunday_count') ?? 12); + return $this->respondSuccess( + $this->buildMetaPayload( + $request->validated('class_id'), + (int) ($request->validated('sunday_count') ?? 12) + ), + 'Progress metadata loaded.' + ); + } - $payload = [ - 'subject_sections' => $this->metaService->subjectSections(), - 'status_options' => $this->metaService->statusOptions(), - 'sunday_options' => $this->metaService->sundayOptionsAlignedWithCi($sundayCount), - 'curriculum' => $this->metaService->curriculumOptions($classId ? (int) $classId : null), + /** + * Legacy teacher portal URL for the weekly class progress submit page. + */ + public function legacySubmitForm(ClassProgressMetaRequest $request): JsonResponse + { + $auth = $this->requireAuthenticatedUser($request->user()); + if ($auth instanceof JsonResponse) { + return $auth; + } + + $schoolYear = (string) (Configuration::getConfig('school_year') ?? ''); + $semester = (string) (Configuration::getConfig('semester') ?? ''); + $assignments = $this->queryService->teacherAssignments($auth, $schoolYear, $semester); + + $requestedClassId = (int) ($request->validated('class_id') ?? 0); + $activeClassId = $requestedClassId > 0 ? $requestedClassId : (int) ($assignments[0]['class_id'] ?? 0); + $activeAssignment = collect($assignments)->first( + fn (array $assignment) => (int) ($assignment['class_id'] ?? 0) === $activeClassId + ) ?? ($assignments[0] ?? []); + $sundayCount = (int) ($request->validated('sunday_count') ?? 12); + $payload = $this->buildMetaPayload($activeClassId > 0 ? $activeClassId : null, $sundayCount); + $payload['classes'] = $assignments; + $payload['defaults'] = [ + 'class_id' => $activeClassId > 0 ? $activeClassId : null, + 'class_section_id' => (int) ($activeAssignment['class_section_id'] ?? 0) ?: null, + 'school_year' => $schoolYear !== '' ? $schoolYear : null, + 'semester' => $semester !== '' ? $semester : null, + 'week_start' => $this->metaService->pickDefaultWeekStart($payload['sunday_options']), ]; return $this->respondSuccess($payload, 'Progress metadata loaded.'); @@ -196,10 +226,28 @@ class ClassProgressController extends BaseApiController */ private function requireAuthenticatedUser($user): User|JsonResponse { + if (!$user instanceof User) { + $user = auth()->user(); + } + + if (!$user instanceof User) { + $user = $this->laravelRequest->user(); + } + if (!$user instanceof User) { return $this->respondError('Unauthorized.', Response::HTTP_UNAUTHORIZED); } return $user; } + + private function buildMetaPayload(?int $classId, int $sundayCount): array + { + return [ + 'subject_sections' => $this->metaService->subjectSections(), + 'status_options' => $this->metaService->statusOptions(), + 'sunday_options' => $this->metaService->sundayOptionsAlignedWithCi($sundayCount), + 'curriculum' => $this->metaService->curriculumOptions($classId), + ]; + } } diff --git a/app/Http/Controllers/Api/Core/BaseApiController.php b/app/Http/Controllers/Api/Core/BaseApiController.php index 9f52b054..9f48d3de 100644 --- a/app/Http/Controllers/Api/Core/BaseApiController.php +++ b/app/Http/Controllers/Api/Core/BaseApiController.php @@ -229,6 +229,12 @@ class BaseApiController extends Controller protected function getCurrentUserId(): ?int { $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + $userId = (int) (optional(auth()->user())->id ?? 0); + } + if ($userId <= 0) { + $userId = (int) (optional($this->laravelRequest->user())->id ?? 0); + } return $userId > 0 ? $userId : null; } diff --git a/app/Http/Controllers/Api/Incidents/IncidentController.php b/app/Http/Controllers/Api/Incidents/IncidentController.php index db8be062..e2849d2d 100644 --- a/app/Http/Controllers/Api/Incidents/IncidentController.php +++ b/app/Http/Controllers/Api/Incidents/IncidentController.php @@ -11,6 +11,7 @@ use App\Http\Resources\Incidents\IncidentAnalysisStudentResource; use App\Http\Resources\Incidents\IncidentGradeResource; use App\Http\Resources\Incidents\IncidentResource; use App\Http\Resources\Incidents\IncidentStudentOptionResource; +use App\Models\Configuration; use App\Services\Incidents\CurrentIncidentService; use App\Services\Incidents\IncidentAnalysisService; use App\Services\Incidents\IncidentHistoryService; @@ -39,6 +40,19 @@ class IncidentController extends BaseApiController ]); } + public function formMeta(): JsonResponse + { + $data = $this->currentService->listCurrent(); + + return response()->json([ + 'ok' => true, + 'incidents' => IncidentResource::collection($data['incidents']), + 'grades' => IncidentGradeResource::collection($data['grades']), + 'school_year' => Configuration::getConfig('school_year'), + 'semester' => Configuration::getConfig('semester'), + ]); + } + public function history(IncidentListRequest $request): JsonResponse { $payload = $request->validated(); diff --git a/app/Http/Controllers/Api/Reports/ReportCardsController.php b/app/Http/Controllers/Api/Reports/ReportCardsController.php index 32a00196..1fc98ea0 100644 --- a/app/Http/Controllers/Api/Reports/ReportCardsController.php +++ b/app/Http/Controllers/Api/Reports/ReportCardsController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Api\Reports; use App\Http\Controllers\Api\Core\BaseApiController; -use App\Http\Requests\Reports\ReportCards\ReportCardAcknowledgementRequest; use App\Http\Requests\Reports\ReportCards\ReportCardCompletenessRequest; use App\Http\Requests\Reports\ReportCards\ReportCardMetaRequest; use App\Http\Requests\Reports\ReportCards\ReportCardPdfRequest; @@ -14,7 +13,9 @@ use App\Http\Resources\Reports\ReportCards\ReportCardStudentMetaResource; use App\Models\Configuration; use App\Services\Reports\ReportCards\ReportCardService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Validator; use Symfony\Component\HttpFoundation\Response; class ReportCardsController extends BaseApiController @@ -67,12 +68,28 @@ class ReportCardsController extends BaseApiController ], $result['message'] ?? 'Success'); } - public function acknowledgement(ReportCardAcknowledgementRequest $request): JsonResponse + public function acknowledgement(Request $request): JsonResponse { - $payload = $request->validated(); - $studentId = (int) ($payload['student_id'] ?? 0); - $schoolYear = $this->resolveSchoolYear($payload['school_year'] ?? null); - $semester = $this->resolveSemester($payload['semester'] ?? null); + $payload = [ + 'student_id' => $request->query('student_id', $request->query('studentId')), + 'school_year' => $request->query('school_year', $request->query('schoolYear')), + 'semester' => $request->query('semester'), + ]; + + $validator = Validator::make($payload, [ + 'student_id' => ['required', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + ]); + + if ($validator->fails()) { + return $this->error('Validation failed.', Response::HTTP_UNPROCESSABLE_ENTITY, $validator->errors()->toArray()); + } + + $validated = $validator->validated(); + $studentId = (int) ($validated['student_id'] ?? 0); + $schoolYear = $this->resolveSchoolYear($validated['school_year'] ?? null); + $semester = $this->resolveSemester($validated['semester'] ?? null); $result = $this->service->acknowledgement($studentId, $schoolYear, $semester); diff --git a/app/Http/Controllers/Api/ScannerController.php b/app/Http/Controllers/Api/ScannerController.php new file mode 100644 index 00000000..d8d3264d --- /dev/null +++ b/app/Http/Controllers/Api/ScannerController.php @@ -0,0 +1,876 @@ +json()->all(); + if (!is_array($data) || $data === []) { + $data = $request->all(); + } + + $badgeCode = trim((string) ($data['badgeCode'] ?? $data['BadgeCode'] ?? $data['badge_code'] ?? $data['barcode'] ?? $data['Barcode'] ?? '')); + $stationId = trim((string) ($data['stationId'] ?? $data['StationId'] ?? $data['station_id'] ?? $data['deviceId'] ?? $data['DeviceId'] ?? '')); + $action = strtolower(trim((string) ($data['action'] ?? 'attendance'))); + $scanStatus = strtolower(trim((string) ($data['status'] ?? 'present'))); + $scannedAt = $this->normalizeScannedAt((string) ($data['scannedAt'] ?? $data['ScannedAt'] ?? $data['scanned_at'] ?? '')); + $operatorId = (int) (auth()->id() ?? 0); + + if ($badgeCode === '') { + $this->logScan($badgeCode, null, null, $action, 'rejected', $operatorId, $stationId, 'Badge code is required.', $scannedAt); + return response()->json($this->failure('Badge code is required.'), 400); + } + + if (!in_array($scanStatus, ['present', 'late'], true)) { + $scanStatus = 'present'; + } + + $student = $this->findStudentByBadge($badgeCode); + if ($student) { + return $this->processStudentScan($student, $badgeCode, $stationId, $action, $scanStatus, $scannedAt, $operatorId); + } + + $staffUser = $this->findStaffUserByBadge($badgeCode); + if ($staffUser) { + return $this->processStaffScan($staffUser, $badgeCode, $stationId, $action, $scanStatus, $scannedAt, $operatorId); + } + + $this->logScan($badgeCode, null, null, $action, 'not_found', $operatorId, $stationId, 'Badge not found.', $scannedAt); + return response()->json($this->failure('Badge not found.'), 404); + } + + private function processStudentScan(Student $student, string $badgeCode, string $stationId, string $action, string $status, string $scannedAt, int $operatorId): JsonResponse + { + $term = $this->currentTerm($student->toArray()); + $schoolNow = $this->schoolLocalDateTime($scannedAt); + $date = $schoolNow->format('Y-m-d'); + $lateCutoff = $this->lateCutoffForDate($schoolNow); + $autoLate = $this->isAfterLateCutoff($schoolNow, $lateCutoff); + if ($autoLate) { + $status = 'late'; + } + + $assignment = $this->studentAssignment((int) $student->id, $term['semester'], $term['school_year']); + if (!$assignment) { + $this->logScan($badgeCode, 'student', (int) $student->id, $action, 'rejected', $operatorId, $stationId, 'Student has no class assignment for current term.', $scannedAt); + return response()->json($this->failure('Student has no class assignment for current term.'), 409); + } + + $classSectionId = (int) $assignment['class_section_id']; + $classId = (int) ($assignment['class_id'] ?? 0); + $existing = $this->findAttendanceRow((int) $student->id, (string) ($student->school_id ?? ''), $date, $term['semester'], $term['school_year']); + $oldStatus = $existing?->status; + $now = utc_now(); + + $row = [ + 'class_id' => $classId, + 'class_section_id' => $classSectionId, + 'student_id' => (int) $student->id, + 'school_id' => (string) ($student->school_id ?? ''), + 'date' => $date, + 'status' => $status, + 'reason' => null, + 'semester' => $term['semester'], + 'school_year' => $term['school_year'], + 'modified_by' => $operatorId, + 'updated_at' => $now, + ]; + + if (Schema::hasColumn('attendance_data', 'is_reported')) { + $row['is_reported'] = 'no'; + } + if (Schema::hasColumn('attendance_data', 'reported')) { + $row['reported'] = 0; + } + if (Schema::hasColumn('attendance_data', 'is_notified')) { + $row['is_notified'] = 'no'; + } + + if ($existing) { + $existing->fill($row); + $existing->save(); + + if (strtolower((string) $oldStatus) !== $status) { + $this->adjustAttendanceRecord( + $classSectionId, + (int) $student->id, + (string) ($student->school_id ?? ''), + $status, + $term['semester'], + $term['school_year'], + $oldStatus, + $operatorId + ); + } + } else { + $row['created_at'] = $now; + AttendanceData::query()->create($row); + $this->addAttendanceRecord($classSectionId, (int) $student->id, (string) ($student->school_id ?? ''), $status, $term['semester'], $term['school_year'], $operatorId); + } + + $this->ensureAttendanceDay($classSectionId, $date, $term['semester'], $term['school_year']); + $message = $status === 'late' ? 'Student late attendance recorded.' : 'Student attendance recorded.'; + $this->logScan($badgeCode, 'student', (int) $student->id, $action, 'accepted', $operatorId, $stationId, $message, $scannedAt); + + $studentSummary = $this->studentSummary((int) $student->id, (int) ($student->parent_id ?? 0), $term['semester'], $term['school_year']); + $person = $this->studentPayload($student->toArray(), $assignment); + $lateSlip = null; + $warnings = []; + + if ($autoLate) { + $lateSlip = $this->createLateSlipPayload( + $student->toArray(), + $assignment, + $term, + $schoolNow, + $lateCutoff, + $operatorId, + $existing ? (string) ($oldStatus ?? '') : null + ); + $warnings[] = 'Student arrived after ' . $lateCutoff->format('h:i A') . '. Late slip receipt generated.'; + } + + return response()->json($this->successPayload([ + 'personType' => 'student', + 'person' => $person, + 'Person' => $this->clientPersonPayload($person, 'student', true), + 'attendance' => [ + 'status' => $status, + 'date' => $date, + 'scannedAt' => $scannedAt, + 'duplicate' => (bool) $existing, + ], + 'Attendance' => $this->clientAttendancePayload($status, $message, $scannedAt), + 'studentSummary' => $studentSummary, + 'StudentSummary' => $this->clientStudentSummaryPayload($studentSummary), + 'lateSlip' => $lateSlip, + 'LateSlip' => $lateSlip, + 'warnings' => $warnings, + 'Warnings' => $warnings, + ], $message)); + } + + private function processStaffScan(User $user, string $badgeCode, string $stationId, string $action, string $status, string $scannedAt, int $operatorId): JsonResponse + { + $term = $this->currentTerm($user->toArray()); + $date = substr($scannedAt, 0, 10); + $roleName = $this->primaryRoleName((int) $user->id); + + StaffAttendance::upsertOne( + (int) $user->id, + $roleName, + $date, + $term['semester'], + $term['school_year'], + $status, + null, + $operatorId + ); + + $this->logScan($badgeCode, 'staff', (int) $user->id, $action, 'accepted', $operatorId, $stationId, 'Staff attendance recorded.', $scannedAt); + + $person = [ + 'id' => (int) $user->id, + 'name' => trim((string) ($user->firstname ?? '') . ' ' . (string) ($user->lastname ?? '')), + 'email' => $user->email, + 'role' => $roleName, + 'badgeCode' => $badgeCode, + ]; + + return response()->json($this->successPayload([ + 'personType' => 'staff', + 'person' => $person, + 'Person' => $this->clientPersonPayload($person, 'staff', true), + 'attendance' => [ + 'status' => $status, + 'date' => $date, + 'scannedAt' => $scannedAt, + ], + 'Attendance' => $this->clientAttendancePayload($status, 'Staff attendance recorded.', $scannedAt), + 'studentSummary' => null, + 'StudentSummary' => null, + 'warnings' => [], + 'Warnings' => [], + ], 'Staff attendance recorded.')); + } + + private function findStudentByBadge(string $badgeCode): ?Student + { + return Student::query() + ->where(function ($query) use ($badgeCode) { + $query->where('rfid_tag', $badgeCode) + ->orWhere('school_id', $badgeCode); + }) + ->first(); + } + + private function findStaffUserByBadge(string $badgeCode): ?User + { + $user = User::query() + ->where(function ($query) use ($badgeCode) { + $query->where('rfid_tag', $badgeCode); + if (ctype_digit($badgeCode)) { + $query->orWhere('school_id', (int) $badgeCode); + } + }) + ->first(); + + if (!$user || !$this->isStaffUser((int) $user->id)) { + return null; + } + + return $user; + } + + private function isStaffUser(int $userId): bool + { + $roles = DB::table('user_roles as ur') + ->selectRaw('LOWER(r.name) AS role_name') + ->leftJoin('roles as r', 'r.id', '=', 'ur.role_id') + ->where('ur.user_id', $userId) + ->get(); + + foreach ($roles as $role) { + $name = trim((string) ($role->role_name ?? '')); + if ($name !== '' && !in_array($name, ['parent', 'guest', 'student'], true)) { + return true; + } + } + + return false; + } + + private function primaryRoleName(int $userId): ?string + { + return DB::table('user_roles as ur') + ->leftJoin('roles as r', 'r.id', '=', 'ur.role_id') + ->where('ur.user_id', $userId) + ->orderBy('r.name') + ->value('r.name'); + } + + private function studentAssignment(int $studentId, string $semester, string $schoolYear): ?array + { + $builder = DB::table('student_class as sc') + ->select('sc.class_section_id', 'cs.class_id', 'cs.class_section_name') + ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') + ->where('sc.student_id', $studentId); + + if (Schema::hasColumn('student_class', 'school_year')) { + $builder->where('sc.school_year', $schoolYear); + } + if (Schema::hasColumn('student_class', 'semester')) { + $builder->where('sc.semester', $semester); + } + + $row = $builder->orderByDesc('sc.id')->first(); + if (!$row && Schema::hasColumn('student_class', 'school_year')) { + $row = DB::table('student_class as sc') + ->select('sc.class_section_id', 'cs.class_id', 'cs.class_section_name') + ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') + ->where('sc.student_id', $studentId) + ->where('sc.school_year', $schoolYear) + ->orderByDesc('sc.id') + ->first(); + } + + return $row ? (array) $row : null; + } + + private function findAttendanceRow(int $studentId, string $schoolId, string $date, string $semester, string $schoolYear): ?AttendanceData + { + $row = AttendanceData::query() + ->where('student_id', $studentId) + ->whereDate('date', $date) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->first(); + + if ($row) { + return $row; + } + + return AttendanceData::query() + ->where('school_id', $schoolId) + ->whereDate('date', $date) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->first(); + } + + private function adjustAttendanceRecord(int $classSectionId, int $studentId, string $schoolId, string $newStatus, string $semester, string $schoolYear, ?string $oldStatus, int $operatorId): void + { + $record = AttendanceRecord::query() + ->where('student_id', $studentId) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->first(); + + if (!$record) { + $this->addAttendanceRecord($classSectionId, $studentId, $schoolId, $newStatus, $semester, $schoolYear, $operatorId); + return; + } + + $presence = (int) ($record->total_presence ?? 0); + $absence = (int) ($record->total_absence ?? 0); + $late = (int) ($record->total_late ?? 0); + + if ($oldStatus === 'present') { + $presence--; + } elseif ($oldStatus === 'absent') { + $absence--; + } elseif ($oldStatus === 'late') { + $late--; + } + + if ($newStatus === 'present') { + $presence++; + } elseif ($newStatus === 'absent') { + $absence++; + } elseif ($newStatus === 'late') { + $late++; + } + + $record->fill([ + 'class_section_id' => $classSectionId, + 'total_presence' => max(0, $presence), + 'total_absence' => max(0, $absence), + 'total_late' => max(0, $late), + 'modified_by' => $operatorId, + 'updated_at' => utc_now(), + ]); + $record->save(); + } + + private function addAttendanceRecord(int $classSectionId, int $studentId, string $schoolId, string $status, string $semester, string $schoolYear, int $operatorId): void + { + $record = AttendanceRecord::query() + ->where('student_id', $studentId) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->first(); + + $field = $status === 'late' ? 'total_late' : ($status === 'absent' ? 'total_absence' : 'total_presence'); + if ($record) { + $record->fill([ + 'class_section_id' => $classSectionId, + $field => (int) ($record->{$field} ?? 0) + 1, + 'total_attendance' => (int) ($record->total_attendance ?? 0) + 1, + 'modified_by' => $operatorId, + 'updated_at' => utc_now(), + ]); + $record->save(); + return; + } + + AttendanceRecord::query()->create([ + 'class_section_id' => $classSectionId, + 'student_id' => $studentId, + 'school_id' => $schoolId, + 'total_presence' => $status === 'present' ? 1 : 0, + 'total_absence' => $status === 'absent' ? 1 : 0, + 'total_late' => $status === 'late' ? 1 : 0, + 'total_attendance' => 1, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'modified_by' => $operatorId, + 'created_at' => utc_now(), + 'updated_at' => utc_now(), + ]); + } + + private function ensureAttendanceDay(int $classSectionId, string $date, string $semester, string $schoolYear): void + { + if (AttendanceDay::findBySectionDate($classSectionId, $date, $semester, $schoolYear)) { + return; + } + + AttendanceDay::query()->create([ + 'class_section_id' => $classSectionId, + 'date' => $date, + 'status' => 'draft', + 'semester' => $semester, + 'school_year' => $schoolYear, + 'created_at' => utc_now(), + 'updated_at' => utc_now(), + ]); + } + + private function studentPayload(array $student, array $assignment): array + { + return [ + 'id' => (int) ($student['id'] ?? 0), + 'schoolId' => $student['school_id'] ?? null, + 'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')), + 'firstName' => $student['firstname'] ?? null, + 'lastName' => $student['lastname'] ?? null, + 'grade' => $student['registration_grade'] ?? null, + 'classSectionId' => isset($assignment['class_section_id']) ? (int) $assignment['class_section_id'] : null, + 'classSectionName' => $assignment['class_section_name'] ?? null, + 'badgeCode' => !empty($student['rfid_tag']) ? $student['rfid_tag'] : ($student['school_id'] ?? null), + ]; + } + + private function studentSummary(int $studentId, int $parentId, string $semester, string $schoolYear): array + { + $attendance = AttendanceRecord::query() + ->where('student_id', $studentId) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->first(); + + $score = SemesterScore::query() + ->where('student_id', $studentId) + ->where('semester', $semester) + ->where('school_year', $schoolYear) + ->orderByDesc('id') + ->first(); + + $flags = CurrentFlag::query() + ->where('student_id', $studentId) + ->where('school_year', $schoolYear) + ->whereIn('flag_state', ['open', 'Opened', 'Open']) + ->orderByDesc('flag_datetime') + ->limit(10) + ->get(); + + $payments = Payment::query() + ->where('parent_id', $parentId) + ->where('school_year', $schoolYear) + ->orderByDesc('payment_date') + ->limit(5) + ->get(); + + return [ + 'attendance' => [ + 'present' => (int) ($attendance->total_presence ?? 0), + 'late' => (int) ($attendance->total_late ?? 0), + 'absent' => (int) ($attendance->total_absence ?? 0), + 'total' => (int) ($attendance->total_attendance ?? 0), + 'score' => isset($score?->attendance_score) ? (float) $score->attendance_score : null, + ], + 'grading' => [ + 'homeworkAvg' => isset($score?->homework_avg) ? (float) $score->homework_avg : null, + 'quizAvg' => isset($score?->quiz_avg) ? (float) $score->quiz_avg : null, + 'projectAvg' => isset($score?->project_avg) ? (float) $score->project_avg : null, + 'midtermExamScore' => isset($score?->midterm_exam_score) ? (float) $score->midterm_exam_score : null, + 'finalExamScore' => isset($score?->final_exam_score) ? (float) $score->final_exam_score : null, + 'semesterScore' => isset($score?->semester_score) ? (float) $score->semester_score : null, + ], + 'discipline' => $flags->map(static fn (CurrentFlag $flag): array => [ + 'id' => (int) $flag->id, + 'flag' => $flag->flag, + 'state' => $flag->flag_state, + 'description' => $flag->open_description, + 'date' => $flag->flag_datetime?->toDateTimeString(), + ])->all(), + 'payments' => $payments->map(static fn (Payment $payment): array => [ + 'id' => (int) $payment->id, + 'invoiceId' => isset($payment->invoice_id) ? (int) $payment->invoice_id : null, + 'totalAmount' => isset($payment->total_amount) ? (float) $payment->total_amount : null, + 'paidAmount' => isset($payment->paid_amount) ? (float) $payment->paid_amount : null, + 'balance' => isset($payment->balance) ? (float) $payment->balance : null, + 'status' => $payment->status, + 'date' => $payment->payment_date?->toDateTimeString(), + ])->all(), + ]; + } + + private function clientPersonPayload(array $person, string $type, bool $active): array + { + return [ + 'id' => (int) ($person['id'] ?? 0), + 'name' => (string) ($person['name'] ?? ''), + 'type' => $type, + 'barcode' => (string) ($person['badgeCode'] ?? $person['schoolId'] ?? ''), + 'active' => $active, + ]; + } + + private function clientAttendancePayload(string $status, string $message, string $scannedAt): array + { + return [ + 'status' => $status, + 'message' => $message, + 'time' => gmdate('c', strtotime($scannedAt) ?: time()), + ]; + } + + private function clientStudentSummaryPayload(array $summary): array + { + $attendance = $summary['attendance'] ?? []; + $payments = $summary['payments'] ?? []; + $lastAttendance = $attendance['lastAttendance'] ?? null; + $outstandingBalance = 0.0; + $paymentStatus = 'Unknown'; + + if (!empty($payments)) { + $latest = $payments[0]; + $paymentStatus = (string) ($latest['status'] ?? 'Unknown'); + $outstandingBalance = (float) ($latest['balance'] ?? 0); + } + + $issues = $this->scanIssues($summary); + + return [ + 'lastAttendance' => $lastAttendance, + 'attendanceCount' => (int) ($attendance['total'] ?? 0), + 'paymentStatus' => $paymentStatus, + 'outstandingBalance' => $outstandingBalance, + 'packageStatus' => $outstandingBalance > 0 ? 'Balance due' : 'Clear', + 'issueLevel' => $this->highestIssueLevel($issues), + 'issues' => $issues, + ]; + } + + private function scanIssues(array $summary): array + { + $issues = []; + $payments = $summary['payments'] ?? []; + if (!empty($payments)) { + $latest = $payments[0]; + $balance = (float) ($latest['balance'] ?? 0); + $status = strtolower(trim((string) ($latest['status'] ?? ''))); + if ($balance > 0) { + $issues[] = [ + 'category' => 'payment', + 'severity' => 'critical', + 'title' => 'Payment balance due', + 'detail' => '$' . number_format($balance, 2) . ' outstanding', + ]; + } elseif ($status !== '' && !in_array($status, ['paid', 'full', 'full payment', 'complete', 'completed'], true)) { + $issues[] = [ + 'category' => 'payment', + 'severity' => 'warning', + 'title' => 'Payment status needs review', + 'detail' => (string) ($latest['status'] ?? ''), + ]; + } + } + + $grading = $summary['grading'] ?? []; + $semesterScore = $grading['semesterScore'] ?? null; + if ($semesterScore !== null && (float) $semesterScore < 70) { + $issues[] = [ + 'category' => 'grades', + 'severity' => (float) $semesterScore < 60 ? 'critical' : 'warning', + 'title' => 'Grade concern', + 'detail' => 'Semester score ' . number_format((float) $semesterScore, 1), + ]; + } + + foreach (['homeworkAvg' => 'Homework', 'quizAvg' => 'Quiz', 'projectAvg' => 'Project'] as $key => $label) { + if (isset($grading[$key]) && $grading[$key] !== null && (float) $grading[$key] < 70) { + $issues[] = [ + 'category' => 'grades', + 'severity' => 'warning', + 'title' => $label . ' average below target', + 'detail' => number_format((float) $grading[$key], 1), + ]; + } + } + + $attendance = $summary['attendance'] ?? []; + if ((int) ($attendance['late'] ?? 0) >= 3) { + $issues[] = [ + 'category' => 'attendance', + 'severity' => 'warning', + 'title' => 'Repeated lateness', + 'detail' => (int) $attendance['late'] . ' late attendance records', + ]; + } + + foreach (($summary['discipline'] ?? []) as $flag) { + $flagType = strtolower(trim((string) ($flag['flag'] ?? ''))); + $category = $this->scanIssueCategory($flagType); + $issues[] = [ + 'category' => $category, + 'severity' => in_array($category, ['behavior', 'discipline'], true) ? 'critical' : 'warning', + 'title' => $this->scanIssueTitle($flagType, $category), + 'detail' => trim((string) ($flag['description'] ?? '')) ?: null, + ]; + } + + return $issues; + } + + private function scanIssueCategory(string $flagType): string + { + if (str_contains($flagType, 'payment')) { + return 'payment'; + } + if (str_contains($flagType, 'grade')) { + return 'grades'; + } + if (str_contains($flagType, 'attendance') || str_contains($flagType, 'tard')) { + return 'attendance'; + } + if ( + str_contains($flagType, 'behavior') + || str_contains($flagType, 'defiance') + || str_contains($flagType, 'disrespect') + || str_contains($flagType, 'bullying') + || str_contains($flagType, 'fighting') + || str_contains($flagType, 'threat') + || str_contains($flagType, 'harassment') + ) { + return 'behavior'; + } + + return 'discipline'; + } + + private function scanIssueTitle(string $flagType, string $category): string + { + $label = trim(str_replace('_', ' ', $flagType)); + if ($label !== '') { + return ucwords($label) . ' flag'; + } + + return match ($category) { + 'payment' => 'Payment flag', + 'grades' => 'Grade flag', + 'attendance' => 'Attendance flag', + 'behavior' => 'Behavior flag', + default => 'Discipline flag', + }; + } + + private function highestIssueLevel(array $issues): string + { + $level = 'clear'; + foreach ($issues as $issue) { + $severity = strtolower((string) ($issue['severity'] ?? '')); + if ($severity === 'critical') { + return 'critical'; + } + if ($severity === 'warning') { + $level = 'warning'; + } elseif ($severity === 'info' && $level === 'clear') { + $level = 'info'; + } + } + + return $level; + } + + private function createLateSlipPayload(array $student, array $assignment, array $term, \DateTimeImmutable $schoolNow, \DateTimeImmutable $lateCutoff, int $operatorId, ?string $previousStatus): array + { + $studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')); + $grade = (string) ($student['registration_grade'] ?? $assignment['class_section_name'] ?? ''); + $cutoffLabel = $lateCutoff->format('h:i A'); + $reason = 'Arrived after ' . $cutoffLabel; + $adminName = $this->operatorName($operatorId); + $slip = [ + 'school_year' => $term['school_year'], + 'semester' => $term['semester'], + 'student_name' => $studentName, + 'slip_date' => $schoolNow->format('Y-m-d'), + 'time_in' => $schoolNow->format('H:i:s'), + 'grade' => $grade, + 'reason' => $reason, + 'admin_name' => $adminName, + ]; + + $logId = null; + $logged = false; + if ($previousStatus !== 'late') { + try { + $created = LateSlipLog::query()->create([ + 'school_year' => $slip['school_year'], + 'semester' => $slip['semester'], + 'student_name' => $slip['student_name'], + 'slip_date' => $slip['slip_date'], + 'time_in' => $slip['time_in'], + 'grade' => $slip['grade'], + 'reason' => $slip['reason'], + 'admin_name' => $slip['admin_name'], + 'printed_by' => $operatorId ?: null, + 'printed_at' => utc_now(), + ]); + $logId = (int) ($created->id ?? 0); + $logged = $logId > 0; + } catch (\Throwable $e) { + Log::error('Badge scan late slip log failed: ' . $e->getMessage()); + } + } + + $displayDate = $schoolNow->format('m/d/Y'); + $displayTime = $schoolNow->format('h:i A'); + + return [ + 'required' => true, + 'logged' => $logged, + 'logId' => $logId ?: null, + 'logUrl' => url('/administrator/late_slips'), + 'schoolYear' => $slip['school_year'], + 'semester' => $slip['semester'], + 'studentName' => $studentName, + 'date' => $displayDate, + 'timeIn' => $displayTime, + 'grade' => $grade, + 'reason' => $reason, + 'adminName' => $adminName, + 'cutoffTime' => $cutoffLabel, + 'message' => 'Late slip receipt generated for arrival after ' . $cutoffLabel . '.', + 'text' => implode("\n", [ + 'Al Rahma School at ISGL', + 'School Year: ' . $slip['school_year'], + 'Student Name: ' . $studentName, + 'Date: ' . $displayDate . ' Time In: ' . $displayTime, + 'Grade: ' . $grade, + 'Reason: ' . $reason, + 'Admin: ' . $adminName, + ]), + ]; + } + + private function isAfterLateCutoff(\DateTimeImmutable $schoolNow, \DateTimeImmutable $cutoff): bool + { + return $schoolNow >= $cutoff; + } + + private function lateCutoffForDate(\DateTimeImmutable $schoolNow): \DateTimeImmutable + { + $configured = trim((string) (Configuration::getConfig(self::LATE_CUTOFF_CONFIG_KEY) ?: self::DEFAULT_LATE_CUTOFF)); + $timezone = $schoolNow->getTimezone(); + $date = $schoolNow->format('Y-m-d'); + + foreach (['H:i:s', 'H:i', 'g:i A', 'h:i A', 'g:i a', 'h:i a'] as $format) { + $parsed = \DateTimeImmutable::createFromFormat('!' . $format, $configured, $timezone); + if ($parsed instanceof \DateTimeImmutable) { + return $schoolNow->setTime((int) $parsed->format('H'), (int) $parsed->format('i'), (int) $parsed->format('s')); + } + } + + $timestamp = strtotime($date . ' ' . $configured); + if ($timestamp !== false) { + return (new \DateTimeImmutable('@' . $timestamp))->setTimezone($timezone); + } + + Log::warning(self::LATE_CUTOFF_CONFIG_KEY . ' has invalid value "' . $configured . '"; using ' . self::DEFAULT_LATE_CUTOFF); + return $schoolNow->setTime(10, 0, 0); + } + + private function schoolLocalDateTime(string $scannedAt): \DateTimeImmutable + { + $timezone = new \DateTimeZone('America/New_York'); + try { + $utc = new \DateTimeZone('UTC'); + return (new \DateTimeImmutable($scannedAt, $utc))->setTimezone($timezone); + } catch (\Throwable) { + return new \DateTimeImmutable('now', $timezone); + } + } + + private function operatorName(int $operatorId): string + { + if ($operatorId > 0) { + try { + $user = User::query()->select('firstname', 'lastname', 'email')->find($operatorId); + if ($user) { + $name = trim((string) ($user->firstname ?? '') . ' ' . (string) ($user->lastname ?? '')); + if ($name !== '') { + return $name; + } + + return (string) ($user->email ?? ''); + } + } catch (\Throwable $e) { + Log::error('Badge scan operator lookup failed: ' . $e->getMessage()); + } + } + + return ''; + } + + private function currentTerm(array $row): array + { + $semester = (string) (Configuration::getConfig('semester') ?: ($row['semester'] ?? '')); + $schoolYear = (string) (Configuration::getConfig('school_year') ?: ($row['school_year'] ?? '')); + + return [ + 'semester' => $semester, + 'school_year' => $schoolYear, + ]; + } + + private function normalizeScannedAt(string $value): string + { + if ($value !== '') { + $timestamp = strtotime($value); + if ($timestamp !== false) { + return date('Y-m-d H:i:s', $timestamp); + } + } + + return utc_now(); + } + + private function logScan(string $badgeCode, ?string $personType, ?int $personId, string $action, string $status, int $operatorId, string $stationId, string $message, string $scannedAt): void + { + try { + if (!Schema::hasTable('badge_scan_logs')) { + return; + } + + DB::table('badge_scan_logs')->insert([ + 'badge_code' => $badgeCode, + 'person_type' => $personType, + 'person_id' => $personId, + 'scan_action' => $action, + 'scan_status' => $status, + 'operator_user_id' => $operatorId ?: null, + 'station_id' => $stationId ?: null, + 'message' => $message, + 'scanned_at' => $scannedAt, + 'created_at' => utc_now(), + ]); + } catch (\Throwable $e) { + Log::error('Badge scan log failed: ' . $e->getMessage()); + } + } + + private function successPayload(array $payload, string $message): array + { + return [ + 'ok' => true, + 'status' => true, + 'success' => true, + 'message' => $message, + ] + $payload; + } + + private function failure(string $message): array + { + return [ + 'ok' => false, + 'status' => false, + 'success' => false, + 'message' => $message, + ]; + } +} diff --git a/app/Http/Controllers/Api/Scores/FinalController.php b/app/Http/Controllers/Api/Scores/FinalController.php index 0c4e432f..d44b3fec 100644 --- a/app/Http/Controllers/Api/Scores/FinalController.php +++ b/app/Http/Controllers/Api/Scores/FinalController.php @@ -25,7 +25,7 @@ class FinalController extends BaseApiController { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ - 'class_section_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['nullable', 'integer', 'min:1'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); @@ -38,7 +38,31 @@ class FinalController extends BaseApiController } $payload = $validator->validated(); - $data = $this->service->list('final_exam', (int) $payload['class_section_id'], $payload['semester'] ?? null, $payload['school_year'] ?? null); + $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; + + if ($classSectionId === null || $classSectionId <= 0) { + $userId = $this->authenticatedUserIdOrUnauthorized(); + if ($userId instanceof JsonResponse) { + return $userId; + } + + $classSectionId = $this->service->resolveClassSectionIdForTeacher( + $userId, + null, + $payload['school_year'] ?? null + ); + + if ($classSectionId === null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'class_section_id' => ['Provide class_section_id or assign this account to a class for the selected school year.'], + ], + ], 422); + } + } + + $data = $this->service->list('final_exam', $classSectionId, $payload['semester'] ?? null, $payload['school_year'] ?? null); return response()->json([ 'ok' => true, diff --git a/app/Http/Controllers/Api/Scores/HomeworkController.php b/app/Http/Controllers/Api/Scores/HomeworkController.php index d5fe6beb..67e7b2af 100644 --- a/app/Http/Controllers/Api/Scores/HomeworkController.php +++ b/app/Http/Controllers/Api/Scores/HomeworkController.php @@ -26,7 +26,7 @@ class HomeworkController extends BaseApiController { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ - 'class_section_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['nullable', 'integer', 'min:1'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); @@ -39,8 +39,32 @@ class HomeworkController extends BaseApiController } $payload = $validator->validated(); + $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; + + if ($classSectionId === null || $classSectionId <= 0) { + $userId = $this->authenticatedUserIdOrUnauthorized(); + if ($userId instanceof JsonResponse) { + return $userId; + } + + $classSectionId = $this->service->resolveClassSectionIdForTeacher( + $userId, + null, + $payload['school_year'] ?? null + ); + + if ($classSectionId === null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'class_section_id' => ['Provide class_section_id or assign this account to a class for the selected school year.'], + ], + ], 422); + } + } + $data = $this->service->list( - (int) $payload['class_section_id'], + $classSectionId, $payload['semester'] ?? null, $payload['school_year'] ?? null ); diff --git a/app/Http/Controllers/Api/Scores/MidtermController.php b/app/Http/Controllers/Api/Scores/MidtermController.php index 98825cf6..fbb7d894 100644 --- a/app/Http/Controllers/Api/Scores/MidtermController.php +++ b/app/Http/Controllers/Api/Scores/MidtermController.php @@ -25,7 +25,7 @@ class MidtermController extends BaseApiController { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ - 'class_section_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['nullable', 'integer', 'min:1'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); @@ -38,7 +38,31 @@ class MidtermController extends BaseApiController } $payload = $validator->validated(); - $data = $this->service->list('midterm_exam', (int) $payload['class_section_id'], $payload['semester'] ?? null, $payload['school_year'] ?? null); + $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; + + if ($classSectionId === null || $classSectionId <= 0) { + $userId = $this->authenticatedUserIdOrUnauthorized(); + if ($userId instanceof JsonResponse) { + return $userId; + } + + $classSectionId = $this->service->resolveClassSectionIdForTeacher( + $userId, + null, + $payload['school_year'] ?? null + ); + + if ($classSectionId === null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'class_section_id' => ['Provide class_section_id or assign this account to a class for the selected school year.'], + ], + ], 422); + } + } + + $data = $this->service->list('midterm_exam', $classSectionId, $payload['semester'] ?? null, $payload['school_year'] ?? null); return response()->json([ 'ok' => true, diff --git a/app/Http/Controllers/Api/Scores/ParticipationController.php b/app/Http/Controllers/Api/Scores/ParticipationController.php index 68341a9a..f3db3349 100644 --- a/app/Http/Controllers/Api/Scores/ParticipationController.php +++ b/app/Http/Controllers/Api/Scores/ParticipationController.php @@ -25,7 +25,7 @@ class ParticipationController extends BaseApiController { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ - 'class_section_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['nullable', 'integer', 'min:1'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); @@ -38,8 +38,32 @@ class ParticipationController extends BaseApiController } $payload = $validator->validated(); + $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; + + if ($classSectionId === null || $classSectionId <= 0) { + $userId = $this->authenticatedUserIdOrUnauthorized(); + if ($userId instanceof JsonResponse) { + return $userId; + } + + $classSectionId = $this->service->resolveClassSectionIdForTeacher( + $userId, + null, + $payload['school_year'] ?? null + ); + + if ($classSectionId === null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'class_section_id' => ['Provide class_section_id or assign this account to a class for the selected school year.'], + ], + ], 422); + } + } + $data = $this->service->list( - (int) $payload['class_section_id'], + $classSectionId, $payload['semester'] ?? null, $payload['school_year'] ?? null ); diff --git a/app/Http/Controllers/Api/Scores/ProjectController.php b/app/Http/Controllers/Api/Scores/ProjectController.php index 2a564586..68fc5312 100644 --- a/app/Http/Controllers/Api/Scores/ProjectController.php +++ b/app/Http/Controllers/Api/Scores/ProjectController.php @@ -26,7 +26,7 @@ class ProjectController extends BaseApiController { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ - 'class_section_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['nullable', 'integer', 'min:1'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); @@ -39,8 +39,32 @@ class ProjectController extends BaseApiController } $payload = $validator->validated(); + $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; + + if ($classSectionId === null || $classSectionId <= 0) { + $userId = $this->authenticatedUserIdOrUnauthorized(); + if ($userId instanceof JsonResponse) { + return $userId; + } + + $classSectionId = $this->service->resolveClassSectionIdForTeacher( + $userId, + null, + $payload['school_year'] ?? null + ); + + if ($classSectionId === null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'class_section_id' => ['Provide class_section_id or assign this account to a class for the selected school year.'], + ], + ], 422); + } + } + $data = $this->service->list( - (int) $payload['class_section_id'], + $classSectionId, $payload['semester'] ?? null, $payload['school_year'] ?? null ); diff --git a/app/Http/Controllers/Api/Scores/QuizController.php b/app/Http/Controllers/Api/Scores/QuizController.php index 24af02a0..77db8e6e 100644 --- a/app/Http/Controllers/Api/Scores/QuizController.php +++ b/app/Http/Controllers/Api/Scores/QuizController.php @@ -26,7 +26,7 @@ class QuizController extends BaseApiController { $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); $validator = Validator::make($data, [ - 'class_section_id' => ['required', 'integer', 'min:1'], + 'class_section_id' => ['nullable', 'integer', 'min:1'], 'semester' => ['nullable', 'string', 'max:50'], 'school_year' => ['nullable', 'string', 'max:50'], ]); @@ -39,8 +39,32 @@ class QuizController extends BaseApiController } $payload = $validator->validated(); + $classSectionId = isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null; + + if ($classSectionId === null || $classSectionId <= 0) { + $userId = $this->authenticatedUserIdOrUnauthorized(); + if ($userId instanceof JsonResponse) { + return $userId; + } + + $classSectionId = $this->service->resolveClassSectionIdForTeacher( + $userId, + null, + $payload['school_year'] ?? null + ); + + if ($classSectionId === null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'class_section_id' => ['Provide class_section_id or assign this account to a class for the selected school year.'], + ], + ], 422); + } + } + $data = $this->service->list( - (int) $payload['class_section_id'], + $classSectionId, $payload['semester'] ?? null, $payload['school_year'] ?? null ); diff --git a/app/Http/Controllers/Api/Settings/SchoolCalendarController.php b/app/Http/Controllers/Api/Settings/SchoolCalendarController.php index 19489819..d6cf29e8 100644 --- a/app/Http/Controllers/Api/Settings/SchoolCalendarController.php +++ b/app/Http/Controllers/Api/Settings/SchoolCalendarController.php @@ -43,7 +43,23 @@ class SchoolCalendarController extends BaseApiController public function index(SchoolCalendarIndexRequest $request): JsonResponse { - $filters = $request->validated(); + return $this->respondWithEvents($request->validated()); + } + + /** + * Legacy teacher portal URL: same response as + * GET /api/v1/settings/school-calendar/events?audience=teacher + */ + public function teacherCalendarLegacy(SchoolCalendarIndexRequest $request): JsonResponse + { + return $this->respondWithEvents([ + ...$request->validated(), + 'audience' => 'teacher', + ]); + } + + private function respondWithEvents(array $filters): JsonResponse + { $audience = $filters['audience'] ?? null; $includeMeetings = array_key_exists('include_meetings', $filters) ? (bool) $filters['include_meetings'] diff --git a/app/Http/Controllers/Api/StatsController.php b/app/Http/Controllers/Api/StatsController.php old mode 100755 new mode 100644 diff --git a/app/Http/Controllers/Api/Students/StudentController.php b/app/Http/Controllers/Api/Students/StudentController.php index f7ac6005..cad842e1 100644 --- a/app/Http/Controllers/Api/Students/StudentController.php +++ b/app/Http/Controllers/Api/Students/StudentController.php @@ -100,6 +100,61 @@ class StudentController extends BaseApiController ]); } + public function scoreCardSelectable(Request $request): JsonResponse + { + $data = array_merge($request->query->all(), $request->all(), $request->json()->all()); + $validator = Validator::make($data, [ + 'q' => ['nullable', 'string', 'max:255'], + 'limit' => ['nullable', 'integer', 'min:1', 'max:1000'], + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $payload = $validator->validated(); + $query = trim((string) ($payload['q'] ?? '')); + $limit = (int) ($payload['limit'] ?? ($query !== '' ? 50 : 1000)); + + $studentsQuery = Student::query() + ->select('id', 'school_id', 'firstname', 'lastname', 'is_active') + ->where('is_active', 1); + + if ($query !== '') { + $studentsQuery->where(function ($builder) use ($query) { + $builder->where('firstname', 'like', '%' . $query . '%') + ->orWhere('lastname', 'like', '%' . $query . '%') + ->orWhere('school_id', 'like', '%' . $query . '%'); + }); + } + + $students = $studentsQuery + ->orderBy('lastname') + ->orderBy('firstname') + ->limit($limit) + ->get() + ->map(fn (Student $student) => [ + 'id' => (int) $student->id, + 'student_id' => (int) $student->id, + 'school_id' => (string) ($student->school_id ?? ''), + 'firstname' => (string) ($student->firstname ?? ''), + 'lastname' => (string) ($student->lastname ?? ''), + 'name' => trim(((string) ($student->firstname ?? '')) . ' ' . ((string) ($student->lastname ?? ''))), + 'is_active' => (int) ($student->is_active ?? 0), + ]) + ->values() + ->all(); + + return response()->json([ + 'ok' => true, + 'students' => $students, + 'selectable' => $students, + ]); + } + public function show(int $studentId): JsonResponse { $student = Student::query()->find($studentId); diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php index 1b4b2ad2..b071d6de 100644 --- a/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php +++ b/app/Http/Requests/ClassPreparation/ClassPreparationAdjustmentRequest.php @@ -8,7 +8,7 @@ class ClassPreparationAdjustmentRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php index 25892303..7f941536 100644 --- a/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php +++ b/app/Http/Requests/ClassPreparation/ClassPreparationIndexRequest.php @@ -8,7 +8,7 @@ class ClassPreparationIndexRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php index 1c06d114..6d9694fa 100644 --- a/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php +++ b/app/Http/Requests/ClassPreparation/ClassPreparationMarkPrintedRequest.php @@ -8,7 +8,7 @@ class ClassPreparationMarkPrintedRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php b/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php index 4459fa63..ebc5859f 100644 --- a/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php +++ b/app/Http/Requests/ClassPreparation/ClassPreparationPrintRequest.php @@ -8,7 +8,7 @@ class ClassPreparationPrintRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } protected function prepareForValidation(): void diff --git a/app/Http/Requests/Classes/ClassSectionIndexRequest.php b/app/Http/Requests/Classes/ClassSectionIndexRequest.php index a9ec26c5..6595737f 100644 --- a/app/Http/Requests/Classes/ClassSectionIndexRequest.php +++ b/app/Http/Requests/Classes/ClassSectionIndexRequest.php @@ -8,7 +8,7 @@ class ClassSectionIndexRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/Classes/ClassSectionStoreRequest.php b/app/Http/Requests/Classes/ClassSectionStoreRequest.php index 9d9d84bb..14c75f30 100644 --- a/app/Http/Requests/Classes/ClassSectionStoreRequest.php +++ b/app/Http/Requests/Classes/ClassSectionStoreRequest.php @@ -9,7 +9,7 @@ class ClassSectionStoreRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/Classes/ClassSectionUpdateRequest.php b/app/Http/Requests/Classes/ClassSectionUpdateRequest.php index 50b0778c..ebc8696c 100644 --- a/app/Http/Requests/Classes/ClassSectionUpdateRequest.php +++ b/app/Http/Requests/Classes/ClassSectionUpdateRequest.php @@ -9,7 +9,7 @@ class ClassSectionUpdateRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/Security/IpBanIndexRequest.php b/app/Http/Requests/Security/IpBanIndexRequest.php index 0720fc30..8c957fb8 100644 --- a/app/Http/Requests/Security/IpBanIndexRequest.php +++ b/app/Http/Requests/Security/IpBanIndexRequest.php @@ -8,7 +8,7 @@ class IpBanIndexRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/Security/IpBanRequest.php b/app/Http/Requests/Security/IpBanRequest.php index 85ca2767..1c89a523 100644 --- a/app/Http/Requests/Security/IpBanRequest.php +++ b/app/Http/Requests/Security/IpBanRequest.php @@ -9,7 +9,7 @@ class IpBanRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Requests/Security/IpUnbanRequest.php b/app/Http/Requests/Security/IpUnbanRequest.php index 682a3353..e5ad0e3a 100644 --- a/app/Http/Requests/Security/IpUnbanRequest.php +++ b/app/Http/Requests/Security/IpUnbanRequest.php @@ -9,7 +9,7 @@ class IpUnbanRequest extends ApiFormRequest { public function authorize(): bool { - return $this->user() !== null; + return $this->user() !== null || auth()->user() !== null; } public function rules(): array diff --git a/app/Http/Resources/Roles/UserWithRolesResource.php b/app/Http/Resources/Roles/UserWithRolesResource.php index a5b752c9..f059d620 100644 --- a/app/Http/Resources/Roles/UserWithRolesResource.php +++ b/app/Http/Resources/Roles/UserWithRolesResource.php @@ -8,17 +8,18 @@ class UserWithRolesResource extends JsonResource { public function toArray($request): array { - $roles = $this['roles'] ?? []; + $row = is_array($this->resource) ? $this->resource : (array) $this->resource; + $roles = $row['roles'] ?? []; if (is_string($roles)) { $roles = array_filter(array_map('trim', explode(',', $roles))); } return [ - 'id' => (int) ($this['id'] ?? 0), - 'firstname' => (string) ($this['firstname'] ?? ''), - 'lastname' => (string) ($this['lastname'] ?? ''), - 'email' => (string) ($this['email'] ?? ''), - 'account_id' => (string) ($this['account_id'] ?? ''), + 'id' => (int) ($row['id'] ?? 0), + 'firstname' => (string) ($row['firstname'] ?? ''), + 'lastname' => (string) ($row['lastname'] ?? ''), + 'email' => (string) ($row['email'] ?? ''), + 'account_id' => (string) ($row['account_id'] ?? ''), 'roles' => array_values(array_filter($roles)), ]; } diff --git a/app/Services/ClassPreparation/ClassPreparationCalculatorService.php b/app/Services/ClassPreparation/ClassPreparationCalculatorService.php index 939cc867..8b23c58e 100644 --- a/app/Services/ClassPreparation/ClassPreparationCalculatorService.php +++ b/app/Services/ClassPreparation/ClassPreparationCalculatorService.php @@ -115,7 +115,7 @@ class ClassPreparationCalculatorService $schoolYear = $schoolYear ?: (string) Configuration::getConfig('school_year'); $baseQuery = DB::table('teacher_class') - ->select('COUNT(*) AS cnt') + ->selectRaw('COUNT(*) AS cnt') ->where('class_section_id', $classSectionId) ->whereIn('position', ['main', 'ta']); diff --git a/app/Services/Parents/ParentAttendanceReportCalendarService.php b/app/Services/Parents/ParentAttendanceReportCalendarService.php index 1f5a443c..1bccf3d5 100644 --- a/app/Services/Parents/ParentAttendanceReportCalendarService.php +++ b/app/Services/Parents/ParentAttendanceReportCalendarService.php @@ -52,12 +52,12 @@ class ParentAttendanceReportCalendarService $rows = DB::table('calendar_events') ->select('date') ->where('no_school', 1) - ->groupStart() - ->where('school_year', $schoolYear) - ->orWhere('school_year IS NULL', null, false) - ->groupEnd() - ->where('date >=', $rangeStartStr) - ->where('date <=', $rangeEndStr) + ->where(function ($query) use ($schoolYear) { + $query->where('school_year', $schoolYear) + ->orWhereNull('school_year'); + }) + ->where('date', '>=', $rangeStartStr) + ->where('date', '<=', $rangeEndStr) ->get() ->toArray(); diff --git a/app/Services/Scores/ExamScoreService.php b/app/Services/Scores/ExamScoreService.php index d6addf87..5e5a9a2d 100644 --- a/app/Services/Scores/ExamScoreService.php +++ b/app/Services/Scores/ExamScoreService.php @@ -5,6 +5,7 @@ namespace App\Services\Scores; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\Student; +use App\Models\TeacherClass; use Illuminate\Support\Facades\DB; class ExamScoreService @@ -13,6 +14,36 @@ class ExamScoreService { } + public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int + { + if ($teacherId <= 0) { + return null; + } + + $schoolYear = $this->term->schoolYear($schoolYearInput); + $query = TeacherClass::query()->where('teacher_id', $teacherId); + if ($schoolYear !== '') { + $query->where('school_year', $schoolYear); + } + + $allowedIds = $query->pluck('class_section_id') + ->map(fn ($v) => (int) $v) + ->filter(fn ($v) => $v > 0) + ->unique() + ->values() + ->all(); + + if ($allowedIds === []) { + return null; + } + + if ($requestedClassSectionId !== null && $requestedClassSectionId > 0 && in_array($requestedClassSectionId, $allowedIds, true)) { + return $requestedClassSectionId; + } + + return $allowedIds[0]; + } + public function list(string $table, int $classSectionId, ?string $semester, ?string $schoolYear): array { $semester = $this->term->semesterLabel($semester); diff --git a/app/Services/Scores/HomeworkScoreService.php b/app/Services/Scores/HomeworkScoreService.php index 2b5fb319..8b47f517 100644 --- a/app/Services/Scores/HomeworkScoreService.php +++ b/app/Services/Scores/HomeworkScoreService.php @@ -7,6 +7,7 @@ use App\Models\Homework; use App\Models\MissingScoreOverride; use App\Models\Student; use App\Models\StudentClass; +use App\Models\TeacherClass; class HomeworkScoreService { @@ -14,6 +15,40 @@ class HomeworkScoreService { } + /** + * When the client omits class_section_id (legacy teacher portal), use the teacher's assignment + * for the requested school year, same idea as ScoreDashboardService::overview. + */ + public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int + { + if ($teacherId <= 0) { + return null; + } + + $schoolYear = $this->term->schoolYear($schoolYearInput); + $query = TeacherClass::query()->where('teacher_id', $teacherId); + if ($schoolYear !== '') { + $query->where('school_year', $schoolYear); + } + + $allowedIds = $query->pluck('class_section_id') + ->map(fn ($v) => (int) $v) + ->filter(fn ($v) => $v > 0) + ->unique() + ->values() + ->all(); + + if ($allowedIds === []) { + return null; + } + + if ($requestedClassSectionId !== null && $requestedClassSectionId > 0 && in_array($requestedClassSectionId, $allowedIds, true)) { + return $requestedClassSectionId; + } + + return $allowedIds[0]; + } + public function list(int $classSectionId, ?string $semester, ?string $schoolYear): array { $semester = $this->term->semesterLabel($semester); diff --git a/app/Services/Scores/ParticipationScoreService.php b/app/Services/Scores/ParticipationScoreService.php index f8620add..b56faaed 100644 --- a/app/Services/Scores/ParticipationScoreService.php +++ b/app/Services/Scores/ParticipationScoreService.php @@ -7,6 +7,7 @@ use App\Models\MissingScoreOverride; use App\Models\Participation; use App\Models\Student; use App\Models\StudentClass; +use App\Models\TeacherClass; class ParticipationScoreService { @@ -14,6 +15,36 @@ class ParticipationScoreService { } + public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int + { + if ($teacherId <= 0) { + return null; + } + + $schoolYear = $this->term->schoolYear($schoolYearInput); + $query = TeacherClass::query()->where('teacher_id', $teacherId); + if ($schoolYear !== '') { + $query->where('school_year', $schoolYear); + } + + $allowedIds = $query->pluck('class_section_id') + ->map(fn ($v) => (int) $v) + ->filter(fn ($v) => $v > 0) + ->unique() + ->values() + ->all(); + + if ($allowedIds === []) { + return null; + } + + if ($requestedClassSectionId !== null && $requestedClassSectionId > 0 && in_array($requestedClassSectionId, $allowedIds, true)) { + return $requestedClassSectionId; + } + + return $allowedIds[0]; + } + public function list(int $classSectionId, ?string $semester, ?string $schoolYear): array { $semester = $this->term->semesterLabel($semester); diff --git a/app/Services/Scores/ProjectScoreService.php b/app/Services/Scores/ProjectScoreService.php index 4221f775..080305ec 100644 --- a/app/Services/Scores/ProjectScoreService.php +++ b/app/Services/Scores/ProjectScoreService.php @@ -7,6 +7,7 @@ use App\Models\MissingScoreOverride; use App\Models\Project; use App\Models\Student; use App\Models\StudentClass; +use App\Models\TeacherClass; use Illuminate\Support\Facades\DB; class ProjectScoreService @@ -15,6 +16,36 @@ class ProjectScoreService { } + public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int + { + if ($teacherId <= 0) { + return null; + } + + $schoolYear = $this->term->schoolYear($schoolYearInput); + $query = TeacherClass::query()->where('teacher_id', $teacherId); + if ($schoolYear !== '') { + $query->where('school_year', $schoolYear); + } + + $allowedIds = $query->pluck('class_section_id') + ->map(fn ($v) => (int) $v) + ->filter(fn ($v) => $v > 0) + ->unique() + ->values() + ->all(); + + if ($allowedIds === []) { + return null; + } + + if ($requestedClassSectionId !== null && $requestedClassSectionId > 0 && in_array($requestedClassSectionId, $allowedIds, true)) { + return $requestedClassSectionId; + } + + return $allowedIds[0]; + } + public function list(int $classSectionId, ?string $semester, ?string $schoolYear): array { $semester = $this->term->semesterLabel($semester); diff --git a/app/Services/Scores/QuizScoreService.php b/app/Services/Scores/QuizScoreService.php index d5d1a7dc..74a9e8bc 100644 --- a/app/Services/Scores/QuizScoreService.php +++ b/app/Services/Scores/QuizScoreService.php @@ -7,6 +7,7 @@ use App\Models\MissingScoreOverride; use App\Models\Quiz; use App\Models\Student; use App\Models\StudentClass; +use App\Models\TeacherClass; class QuizScoreService { @@ -14,6 +15,36 @@ class QuizScoreService { } + public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int + { + if ($teacherId <= 0) { + return null; + } + + $schoolYear = $this->term->schoolYear($schoolYearInput); + $query = TeacherClass::query()->where('teacher_id', $teacherId); + if ($schoolYear !== '') { + $query->where('school_year', $schoolYear); + } + + $allowedIds = $query->pluck('class_section_id') + ->map(fn ($v) => (int) $v) + ->filter(fn ($v) => $v > 0) + ->unique() + ->values() + ->all(); + + if ($allowedIds === []) { + return null; + } + + if ($requestedClassSectionId !== null && $requestedClassSectionId > 0 && in_array($requestedClassSectionId, $allowedIds, true)) { + return $requestedClassSectionId; + } + + return $allowedIds[0]; + } + public function list(int $classSectionId, ?string $semester, ?string $schoolYear): array { $semester = $this->term->semesterLabel($semester); diff --git a/resources/views/docs/swagger.blade.php b/resources/views/docs/swagger.blade.php old mode 100755 new mode 100644 diff --git a/routes/api.php b/routes/api.php index 7ed7156e..b7d5f121 100644 --- a/routes/api.php +++ b/routes/api.php @@ -118,6 +118,7 @@ use App\Http\Controllers\Api\Documentation\ApiDocsAdminController; use App\Http\Controllers\Api\Documentation\DocsCatalogController; use App\Http\Controllers\Api\Parents\AuthorizedUserInviteController; use App\Http\Controllers\Api\Public\PublicWinnersController; +use App\Http\Controllers\Api\ScannerController; use App\Http\Controllers\Api\Staff\TimeOffNotificationController; use App\Http\Controllers\Api\System\AccessDeniedController; // CodeIgniter also registers POST /api/login and POST /api/register (public, no /v1). @@ -210,6 +211,8 @@ Route::prefix('v1')->group(function () { */ Route::post('badge_scan/scan', [BadgeScanController::class, 'scan']) ->middleware('throttle:120,1'); + Route::post('scanner/process', [ScannerController::class, 'process']) + ->middleware('throttle:120,1'); Route::prefix('frontend')->group(function () { Route::get('/', [FrontendController::class, 'index']); @@ -303,12 +306,108 @@ Route::prefix('v1')->group(function () { Route::get('enrollment-withdrawal', [AdministratorEnrollmentController::class, 'index']); Route::get('enrollment-withdrawal/new-students', [AdministratorEnrollmentController::class, 'newStudents']); Route::post('enrollment-withdrawal/update-statuses', [AdministratorEnrollmentController::class, 'updateStatuses']); + Route::get('enroll-withdrawal', [AdministratorEnrollmentController::class, 'index']); + Route::get('enroll-withdrawal/new-students', [AdministratorEnrollmentController::class, 'newStudents']); + Route::post('enroll-withdrawal/update-statuses', [AdministratorEnrollmentController::class, 'updateStatuses']); Route::get('emergency-contacts', [AdministratorEmergencyContactController::class, 'index']); Route::get('emergency-contacts/data', [AdministratorEmergencyContactController::class, 'index']); Route::get('emergency-contacts/{contactId}', [AdministratorEmergencyContactController::class, 'show']); Route::patch('emergency-contacts/{contactId}', [AdministratorEmergencyContactController::class, 'update']); Route::delete('emergency-contacts/{contactId}', [AdministratorEmergencyContactController::class, 'destroy']); + + Route::prefix('staff')->group(function () { + Route::get('/', [StaffController::class, 'index']); + Route::get('{id}', [StaffController::class, 'show']); + Route::post('/', [StaffController::class, 'store']); + Route::patch('{id}', [StaffController::class, 'update']); + Route::delete('{id}', [StaffController::class, 'destroy']); + }); + + Route::prefix('flags')->group(function () { + Route::get('pending', [IncidentApiController::class, 'current']); + Route::get('form-meta', [IncidentApiController::class, 'formMeta']); + Route::get('students/{gradeId}', [IncidentApiController::class, 'studentsByGrade']); + Route::get('processed', [IncidentApiController::class, 'processed']); + Route::get('analysis', [IncidentApiController::class, 'analysis']); + Route::post('/', [IncidentApiController::class, 'store']); + Route::post('{incidentId}/state', [IncidentApiController::class, 'updateState']); + Route::post('{incidentId}/close', [IncidentApiController::class, 'close']); + Route::post('{incidentId}/cancel', [IncidentApiController::class, 'cancel']); + }); + + Route::prefix('grading')->group(function () { + Route::get('homework-tracking', [HomeworkTrackingController::class, 'index']); + }); + + Route::prefix('expenses')->group(function () { + Route::get('/', [ExpenseController::class, 'index']); + Route::get('options', [ExpenseController::class, 'options']); + Route::get('{id}', [ExpenseController::class, 'show']); + Route::post('/', [ExpenseController::class, 'store']); + Route::put('{id}', [ExpenseController::class, 'update']); + Route::post('{id}/status', [ExpenseController::class, 'updateStatus']); + }); + + Route::prefix('reports')->group(function () { + Route::get('financial-detailed', [FinancialController::class, 'report']); + }); + + Route::prefix('invoices')->group(function () { + Route::get('management', [InvoiceController::class, 'management']); + }); + + Route::prefix('paypal-transactions')->group(function () { + Route::get('/', [PaypalTransactionsController::class, 'index']); + Route::get('csv', [PaypalTransactionsController::class, 'downloadCsv']); + }); + + Route::prefix('reimbursements')->group(function () { + Route::get('under-processing', [ReimbursementController::class, 'underProcessing']); + Route::post('mark-donation', [ReimbursementController::class, 'markDonation']); + Route::post('batches', [ReimbursementController::class, 'createBatch']); + Route::post('batches/assign', [ReimbursementController::class, 'updateBatchAssignment']); + Route::post('batches/lock', [ReimbursementController::class, 'lockBatch']); + Route::post('batches/admin-files', [ReimbursementController::class, 'uploadBatchAdminFile']); + Route::get('batches/admin-files/{name}/{mode?}', [ReimbursementController::class, 'serveAdminCheckFile']); + Route::post('batches/email', [ReimbursementController::class, 'sendBatchEmail']); + Route::get('export', [ReimbursementController::class, 'export']); + Route::get('batches/export', [ReimbursementController::class, 'exportBatch']); + Route::get('reimbursed-expenses', [ReimbursementController::class, 'reimbursedExpenses']); + Route::get('/', [ReimbursementController::class, 'index']); + Route::post('/', [ReimbursementController::class, 'store']); + Route::post('process', [ReimbursementController::class, 'process']); + Route::put('{id}', [ReimbursementController::class, 'update']); + }); + + Route::prefix('printables')->group(function () { + Route::prefix('badges')->group(function () { + Route::get('form-options', [BadgeController::class, 'formData']); + }); + Route::prefix('report-card')->group(function () { + Route::get('meta', [ReportCardsController::class, 'meta']); + }); + }); + + Route::prefix('role-permission')->group(function () { + Route::get('roles', [RolePermissionController::class, 'roles']); + Route::post('roles', [RolePermissionController::class, 'storeRole']); + Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']); + Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']); + Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']); + + Route::get('users', [RolePermissionController::class, 'users']); + Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']); + + Route::get('permissions', [RolePermissionController::class, 'permissions']); + Route::post('permissions', [RolePermissionController::class, 'storePermission']); + Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission']); + Route::patch('permissions/{permissionId}', [RolePermissionController::class, 'updatePermission']); + Route::delete('permissions/{permissionId}', [RolePermissionController::class, 'deletePermission']); + + Route::get('roles/{roleId}/permissions', [RolePermissionController::class, 'rolePermissions']); + Route::put('roles/{roleId}/permissions', [RolePermissionController::class, 'updateRolePermissions']); + }); }); Route::middleware('auth:api')->prefix('attendance')->group(function () { @@ -374,6 +473,71 @@ Route::prefix('v1')->group(function () { }); Route::middleware('auth:api')->group(function () { + // Legacy alias: same handler as GET /api/v1/scores/homework + Route::get('teacher/homework-list', [HomeworkScoreController::class, 'index']); + // Legacy alias: teacher add-homework page and save action + Route::get('teacher/add-homework', [HomeworkScoreController::class, 'index']); + Route::post('teacher/add-homework', [HomeworkScoreController::class, 'update']); + // Legacy alias: teacher add-quiz page and save action + Route::get('teacher/add-quiz', [QuizScoreController::class, 'index']); + Route::post('teacher/add-quiz', [QuizScoreController::class, 'update']); + // Legacy alias: teacher add quiz column + Route::get('teacher/add-quiz-column-form', [QuizScoreController::class, 'index']); + Route::post('teacher/add-quiz-column-form', [QuizScoreController::class, 'addColumn']); + // Legacy alias: teacher add-midterm-exam page and save action + Route::get('teacher/add-midterm-exam', [MidtermScoreController::class, 'index']); + Route::post('teacher/add-midterm-exam', [MidtermScoreController::class, 'update']); + // Legacy alias: teacher add-final-exam page and save action + Route::get('teacher/add-final-exam', [FinalScoreController::class, 'index']); + Route::post('teacher/add-final-exam', [FinalScoreController::class, 'update']); + // Legacy alias: teacher add-participation page and save action + Route::get('teacher/add-participation', [ParticipationScoreController::class, 'index']); + Route::post('teacher/add-participation', [ParticipationScoreController::class, 'update']); + // Legacy alias: teacher add-project page and save action + Route::get('teacher/add-project', [ProjectScoreController::class, 'index']); + Route::post('teacher/add-project', [ProjectScoreController::class, 'update']); + // Legacy alias: teacher assignment management screen + Route::get('teacher/teacher-assignment', [TeacherClassAssignmentController::class, 'index']); + // Legacy alias: teacher exam drafts + Route::get('teacher/exam-drafts', [ExamDraftApiController::class, 'teacherIndex']); + Route::post('teacher/exam-drafts', [ExamDraftApiController::class, 'teacherStore']); + Route::get('teacher/drafts', [ExamDraftApiController::class, 'teacherIndex']); + Route::post('teacher/drafts', [ExamDraftApiController::class, 'teacherStore']); + // Legacy alias: teacher inventory book distribution form/save + Route::get('teacher/inventory/books/distribute', [InventoryController::class, 'teacherDistribution']); + Route::post('teacher/inventory/books/distribute', [InventoryController::class, 'teacherDistributionStore']); + // Legacy alias: teacher print request bootstrap/context + Route::get('teacher/print-requests/context', [PrintRequestsController::class, 'teacher']); + // Legacy alias: same handler as GET /api/v1/teachers/classes (class dashboard / roster) + Route::get('teacher/class-view', [TeacherController::class, 'classes']); + Route::get('teacher/no-classes', [TeacherController::class, 'classes']); + Route::get('teacher/select-semester', function () { + return response()->json([ + 'ok' => true, + 'school_year' => \App\Models\Configuration::getConfig('school_year'), + 'semester' => \App\Models\Configuration::getConfig('semester'), + 'semester_options' => ['Fall', 'Spring'], + ]); + }); + Route::get('teacher/absence-vacation', [TeacherController::class, 'absenceForm']); + Route::post('teacher/absence-vacation', [TeacherController::class, 'submitAbsence']); + // Legacy CI paths: same handler as GET /api/v1/attendance/teacher/form + Route::get('teacher/showupdate-attendance', [TeacherAttendanceApiController::class, 'form']); + Route::get('teacher/showupdate_attendance', [TeacherAttendanceApiController::class, 'form']); + // Legacy alias: same handler as GET /api/v1/scores/overview (teacher score dashboard) + Route::get('teacher/scores', [ScoreDashboardController::class, 'overview']); + // Legacy alias: school calendar events with audience=teacher + Route::get('teacher/calendar', [SchoolCalendarController::class, 'teacherCalendarLegacy']); + // Legacy teacher progress submit page: GET loads form metadata, POST saves reports + Route::post('teacher/class-progress-submit', [ClassProgressController::class, 'store']); + Route::get('teacher/class-progress-submit', [ClassProgressController::class, 'legacySubmitForm']); + // Legacy teacher progress history page + Route::get('teacher/class-progress-history', [ClassProgressController::class, 'index']); + // Legacy teacher progress view page + Route::get('teacher/class-progress-view', [ClassProgressController::class, 'index']); + // Legacy alias: same handler as GET /api/v1/competition-scores + Route::get('teacher/competition-scores', [CompetitionScoresController::class, 'index']); + Route::get('dashboard/route', [DashboardController::class, 'route']); Route::get('emergency-contacts', [AdministratorEmergencyContactController::class, 'index']); @@ -466,6 +630,26 @@ Route::prefix('v1')->group(function () { Route::get('roles/{roleId}/permissions', [RolePermissionController::class, 'rolePermissions']); Route::put('roles/{roleId}/permissions', [RolePermissionController::class, 'updateRolePermissions']); }); + + Route::prefix('role-permission')->group(function () { + Route::get('roles', [RolePermissionController::class, 'roles']); + Route::post('roles', [RolePermissionController::class, 'storeRole']); + Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']); + Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']); + Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']); + + Route::get('users', [RolePermissionController::class, 'users']); + Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']); + + Route::get('permissions', [RolePermissionController::class, 'permissions']); + Route::post('permissions', [RolePermissionController::class, 'storePermission']); + Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission']); + Route::patch('permissions/{permissionId}', [RolePermissionController::class, 'updatePermission']); + Route::delete('permissions/{permissionId}', [RolePermissionController::class, 'deletePermission']); + + Route::get('roles/{roleId}/permissions', [RolePermissionController::class, 'rolePermissions']); + Route::put('roles/{roleId}/permissions', [RolePermissionController::class, 'updateRolePermissions']); + }); Route::prefix('role-switcher')->group(function () { Route::get('/', [RoleSwitcherController::class, 'index']); Route::post('switch', [RoleSwitcherController::class, 'switch']); @@ -512,6 +696,7 @@ Route::prefix('v1')->group(function () { Route::prefix('students')->group(function () { Route::get('/', [StudentApiController::class, 'index']); Route::post('/', [StudentApiController::class, 'store']); + Route::get('score-card/selectable', [StudentApiController::class, 'scoreCardSelectable']); Route::get('assignments', [StudentApiController::class, 'assignments']); Route::post('assign-class', [StudentApiController::class, 'assignClass']); Route::post('remove-class', [StudentApiController::class, 'removeClass']); @@ -579,6 +764,10 @@ Route::prefix('v1')->group(function () { Route::delete('{userId}', [UserController::class, 'destroy']); }); + Route::prefix('administrator')->group(function () { + Route::get('login-activity', [UserController::class, 'loginActivity']); + }); + Route::prefix('broadcast-email')->group(function () { Route::get('options', [BroadcastEmailController::class, 'options']); Route::post('send', [BroadcastEmailController::class, 'send']); @@ -665,6 +854,7 @@ Route::prefix('v1')->group(function () { }); Route::prefix('discounts')->group(function () { + Route::get('/', [DiscountController::class, 'options']); Route::get('options', [DiscountController::class, 'options']); Route::post('apply', [DiscountController::class, 'apply']); Route::get('vouchers', [DiscountController::class, 'listVouchers']);