diff --git a/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php b/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php new file mode 100644 index 00000000..fffae4f7 --- /dev/null +++ b/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php @@ -0,0 +1,226 @@ +query('school_year'); + $semester = $request->query('semester'); + + $query = DB::table('parent_attendance_reports as par') + ->select([ + 'par.id', + 'par.report_date', + 'par.dismiss_time', + 'par.reason', + 'par.status', + 'par.school_year', + 'par.semester', + 'par.class_section_id', + 's.firstname', + 's.lastname', + 'cs.class_section_name', + ]) + ->join('students as s', 's.id', '=', 'par.student_id') + ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id') + ->where('par.type', 'early_dismissal') + ->orderBy('par.report_date', 'desc') + ->orderByRaw("CONCAT(s.firstname, ' ', s.lastname) ASC"); + + if ($schoolYear) { + $query->where('par.school_year', $schoolYear); + } + if ($semester) { + $query->where('par.semester', $semester); + } + + $rows = $query->get(); + + $groups = []; + foreach ($rows as $row) { + $date = $row->report_date ?? 'Unknown'; + $groups[$date][] = [ + 'id' => $row->id, + 'firstname' => $row->firstname, + 'lastname' => $row->lastname, + 'class_section_name' => $row->class_section_name, + 'dismiss_time' => $row->dismiss_time, + 'reason' => $row->reason, + 'status' => $row->status, + 'school_year' => $row->school_year, + 'semester' => $row->semester, + ]; + } + + $dates = array_keys($groups); + $signatures = EarlyDismissalSignature::query() + ->whereIn('report_date', $dates) + ->get(['report_date', 'filename', 'original_name']); + + $signatureByDate = []; + foreach ($signatures as $sig) { + $signatureByDate[(string) $sig->report_date] = [ + 'filename' => $sig->filename, + 'original_name' => $sig->original_name, + ]; + } + + return $this->success([ + 'groups' => $groups, + 'signatureByDate' => $signatureByDate, + ]); + } + + /** + * GET /api/v1/attendance/early-dismissals/student-options + * Returns active students with their current class section. + */ + public function studentOptions(): JsonResponse + { + $students = DB::table('students as s') + ->select([ + 's.id', + 's.firstname', + 's.lastname', + 'cs.class_section_name', + ]) + ->leftJoin('student_class as sc', function ($j) { + $j->on('sc.student_id', '=', 's.id') + ->where('sc.school_year', '=', Configuration::getConfigValueByKey('school_year')); + }) + ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') + ->where('s.is_active', 1) + ->orderByRaw("CONCAT(s.firstname, ' ', s.lastname) ASC") + ->get(); + + return $this->success(['students' => $students]); + } + + /** + * POST /api/v1/attendance/early-dismissals + * Creates a new early dismissal record. + */ + public function store(Request $request): JsonResponse + { + $validator = Validator::make($request->all(), [ + 'student_id' => ['required', 'integer', 'min:1', 'exists:students,id'], + 'date' => ['required', 'date_format:Y-m-d'], + 'dismiss_time' => ['required', 'date_format:H:i'], + 'reason' => ['nullable', 'string', 'max:2000'], + ]); + + if ($validator->fails()) { + return $this->respondValidationError($validator->errors()->toArray()); + } + + $data = $validator->validated(); + + $schoolYear = (string) Configuration::getConfigValueByKey('school_year'); + $semester = (string) Configuration::getConfigValueByKey('semester'); + + $sc = DB::table('student_class') + ->where('student_id', $data['student_id']) + ->where('school_year', $schoolYear) + ->orderBy('id') + ->first(); + + $classSectionId = $sc?->class_section_id ?? null; + + $parentId = DB::table('students') + ->where('id', $data['student_id']) + ->value('parent_id') ?? Auth::id(); + + DB::table('parent_attendance_reports')->insert([ + 'parent_id' => $parentId ?? Auth::id(), + 'student_id' => $data['student_id'], + 'class_section_id' => $classSectionId, + 'report_date' => $data['date'], + 'type' => 'early_dismissal', + 'dismiss_time' => $data['dismiss_time'], + 'reason' => $data['reason'] ?? null, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'status' => 'new', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return $this->success([], 'Early dismissal saved.', Response::HTTP_CREATED); + } + + /** + * POST /api/v1/attendance/early-dismissals/signature + * Uploads (or replaces) the signature file for a report date. + */ + public function uploadSignature(Request $request): JsonResponse + { + $validator = Validator::make($request->all(), [ + 'report_date' => ['required', 'date_format:Y-m-d'], + 'school_year' => ['nullable', 'string', 'max:16'], + 'semester' => ['nullable', 'string', 'max:32'], + 'signature_file' => ['required', 'file', 'mimes:jpg,jpeg,png,webp,gif,pdf', 'max:5120'], + ]); + + if ($validator->fails()) { + return $this->respondValidationError($validator->errors()->toArray()); + } + + $file = $request->file('signature_file'); + $reportDate = $request->input('report_date'); + $schoolYear = $request->input('school_year') ?: (string) Configuration::getConfigValueByKey('school_year'); + $semester = $request->input('semester') ?: (string) Configuration::getConfigValueByKey('semester'); + + $dir = storage_path('uploads/early_dismissal_signatures'); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $existing = EarlyDismissalSignature::query() + ->where('report_date', $reportDate) + ->where('school_year', $schoolYear) + ->where('semester', $semester) + ->first(); + + if ($existing?->filename && file_exists($dir . '/' . $existing->filename)) { + unlink($dir . '/' . $existing->filename); + } + + $filename = uniqid('sig_', true) . '.' . $file->getClientOriginalExtension(); + $file->move($dir, $filename); + + EarlyDismissalSignature::updateOrCreate( + ['report_date' => $reportDate, 'school_year' => $schoolYear, 'semester' => $semester], + [ + 'filename' => $filename, + 'original_name' => $file->getClientOriginalName(), + 'mime_type' => $file->getClientMimeType(), + 'file_size' => $file->getSize(), + 'uploaded_by' => Auth::id(), + ] + ); + + return $this->success(['filename' => $filename], 'Signature uploaded.'); + } +} diff --git a/app/Http/Controllers/Api/Grading/GradingController.php b/app/Http/Controllers/Api/Grading/GradingController.php index 4202936e..9d575a5f 100644 --- a/app/Http/Controllers/Api/Grading/GradingController.php +++ b/app/Http/Controllers/Api/Grading/GradingController.php @@ -72,6 +72,7 @@ class GradingController extends BaseApiController 'scores' => GradingScoreResource::collection($data['scores']), 'type' => $data['type'], 'class_section_id' => $data['class_section_id'], + 'class_section_name' => $data['class_section_name'], 'semester' => $data['semester'], 'scores_locked' => $data['scores_locked'], ]); diff --git a/app/Http/Controllers/Api/Scores/FinalController.php b/app/Http/Controllers/Api/Scores/FinalController.php index d44b3fec..4b9b705c 100644 --- a/app/Http/Controllers/Api/Scores/FinalController.php +++ b/app/Http/Controllers/Api/Scores/FinalController.php @@ -67,6 +67,7 @@ class FinalController extends BaseApiController return response()->json([ 'ok' => true, 'students' => ScoreStudentResource::collection($data['students']), + 'class_section_name' => $data['classSectionName'], 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'scores_locked' => $data['scoresLocked'], diff --git a/app/Http/Controllers/Api/Scores/HomeworkController.php b/app/Http/Controllers/Api/Scores/HomeworkController.php index 67e7b2af..f38e491c 100644 --- a/app/Http/Controllers/Api/Scores/HomeworkController.php +++ b/app/Http/Controllers/Api/Scores/HomeworkController.php @@ -73,6 +73,7 @@ class HomeworkController extends BaseApiController 'ok' => true, 'students' => ScoreStudentResource::collection($data['students']), 'headers' => $data['headers'], + 'class_section_name' => $data['classSectionName'], 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'scores_locked' => $data['scoresLocked'], diff --git a/app/Http/Controllers/Api/Scores/MidtermController.php b/app/Http/Controllers/Api/Scores/MidtermController.php index fbb7d894..8ddcb304 100644 --- a/app/Http/Controllers/Api/Scores/MidtermController.php +++ b/app/Http/Controllers/Api/Scores/MidtermController.php @@ -67,6 +67,7 @@ class MidtermController extends BaseApiController return response()->json([ 'ok' => true, 'students' => ScoreStudentResource::collection($data['students']), + 'class_section_name' => $data['classSectionName'], 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'scores_locked' => $data['scoresLocked'], diff --git a/app/Http/Controllers/Api/Scores/ParticipationController.php b/app/Http/Controllers/Api/Scores/ParticipationController.php index f3db3349..98b18796 100644 --- a/app/Http/Controllers/Api/Scores/ParticipationController.php +++ b/app/Http/Controllers/Api/Scores/ParticipationController.php @@ -71,6 +71,7 @@ class ParticipationController extends BaseApiController return response()->json([ 'ok' => true, 'students' => ScoreStudentResource::collection($data['students']), + 'class_section_name' => $data['classSectionName'], 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'scores_locked' => $data['scoresLocked'], diff --git a/app/Http/Controllers/Api/Scores/ProjectController.php b/app/Http/Controllers/Api/Scores/ProjectController.php index 68fc5312..9954763f 100644 --- a/app/Http/Controllers/Api/Scores/ProjectController.php +++ b/app/Http/Controllers/Api/Scores/ProjectController.php @@ -73,6 +73,7 @@ class ProjectController extends BaseApiController 'ok' => true, 'students' => ScoreStudentResource::collection($data['students']), 'headers' => $data['headers'], + 'class_section_name' => $data['classSectionName'], 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'scores_locked' => $data['scoresLocked'], diff --git a/app/Http/Controllers/Api/Scores/QuizController.php b/app/Http/Controllers/Api/Scores/QuizController.php index 77db8e6e..2a17f95d 100644 --- a/app/Http/Controllers/Api/Scores/QuizController.php +++ b/app/Http/Controllers/Api/Scores/QuizController.php @@ -73,6 +73,7 @@ class QuizController extends BaseApiController 'ok' => true, 'students' => ScoreStudentResource::collection($data['students']), 'headers' => $data['headers'], + 'class_section_name' => $data['classSectionName'], 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'scores_locked' => $data['scoresLocked'], diff --git a/app/Http/Controllers/Api/Scores/ScoreCommentController.php b/app/Http/Controllers/Api/Scores/ScoreCommentController.php index 1943185f..24a1c873 100644 --- a/app/Http/Controllers/Api/Scores/ScoreCommentController.php +++ b/app/Http/Controllers/Api/Scores/ScoreCommentController.php @@ -66,6 +66,7 @@ class ScoreCommentController extends BaseApiController 'semester' => $data['semester'], 'school_year' => $data['schoolYear'], 'class_section_id' => $data['classSectionId'], + 'class_section_name' => $data['classSectionName'], 'scores_locked' => $data['scoresLocked'], ]); } diff --git a/app/Http/Resources/Attendance/DailyAttendanceResource.php b/app/Http/Resources/Attendance/DailyAttendanceResource.php index 697cd09e..ce0a40ad 100644 --- a/app/Http/Resources/Attendance/DailyAttendanceResource.php +++ b/app/Http/Resources/Attendance/DailyAttendanceResource.php @@ -7,6 +7,8 @@ use Illuminate\Http\Resources\Json\JsonResource; class DailyAttendanceResource extends JsonResource { + public bool $preserveKeys = true; + public function toArray(Request $request): array { return [ diff --git a/app/Http/Resources/Scores/ScoreStudentResource.php b/app/Http/Resources/Scores/ScoreStudentResource.php index f1abac16..cc2895dc 100644 --- a/app/Http/Resources/Scores/ScoreStudentResource.php +++ b/app/Http/Resources/Scores/ScoreStudentResource.php @@ -14,6 +14,7 @@ class ScoreStudentResource extends JsonResource 'firstname' => $this->resource['firstname'] ?? null, 'lastname' => $this->resource['lastname'] ?? null, 'school_id' => $this->resource['school_id'] ?? null, + 'score' => $this->resource['score'] ?? null, 'scores' => $this->resource['scores'] ?? [], 'comments' => $this->resource['comments'] ?? null, ]; diff --git a/app/Models/StudentClass.php b/app/Models/StudentClass.php index ccc75363..670cdceb 100644 --- a/app/Models/StudentClass.php +++ b/app/Models/StudentClass.php @@ -16,6 +16,7 @@ class StudentClass extends BaseModel 'school_id', 'class_section_id', 'school_year', + 'semester', 'is_event_only', 'description', 'updated_by', diff --git a/app/Services/Assignment/AssignmentService.php b/app/Services/Assignment/AssignmentService.php index 8e03f5a7..f3d59b0d 100644 --- a/app/Services/Assignment/AssignmentService.php +++ b/app/Services/Assignment/AssignmentService.php @@ -159,8 +159,8 @@ class AssignmentService $currentSemester = $this->getCurrentSemester(); $currentSchoolYear = $this->getCurrentSchoolYear(); - $teacherClasses = $this->dataLoader->loadTeacherClasses(); - $studentClasses = $this->dataLoader->loadStudentClasses(); + $teacherClasses = $this->dataLoader->loadTeacherClasses($currentSchoolYear); + $studentClasses = $this->dataLoader->loadStudentClasses($currentSchoolYear); $teacherBySection = $teacherClasses->groupBy('class_section_id'); $studentBySection = $studentClasses->groupBy('class_section_id'); diff --git a/app/Services/Attendance/AttendanceQueryService.php b/app/Services/Attendance/AttendanceQueryService.php index 778e1729..0ce9ebc1 100644 --- a/app/Services/Attendance/AttendanceQueryService.php +++ b/app/Services/Attendance/AttendanceQueryService.php @@ -515,43 +515,74 @@ class AttendanceQueryService continue; } + // Bulk-load student profiles for the whole section in one query + $studentsArray = is_array($students) ? $students : $students->toArray(); + $studentIds = array_map(fn($sc) => (int)($sc['student_id'] ?? $sc->student_id ?? 0), $studentsArray); + $studentProfiles = $this->student + ->query() + ->select(['id', 'firstname', 'lastname', 'school_id']) + ->whereIn('id', $studentIds) + ->where('is_active', 1) + ->get() + ->keyBy('id') + ->map(fn($row) => $row->toArray()) + ->all(); + + $activeIds = array_keys($studentProfiles); + if (empty($activeIds)) { + unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]); + continue; + } + + // Bulk-load all attendance entries for the section in one query + $allEntries = AttendanceData::query() + ->whereIn('student_id', $activeIds) + ->where(function ($q) use ($secCode, $secPk) { + $q->where('class_section_id', $secCode); + if ($secPk && (string)$secPk !== (string)$secCode) { + $q->orWhere('class_section_id', $secPk); + } + }) + ->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester)) + ->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear)) + ->orderBy('date') + ->get() + ->map(fn($row) => $row->toArray()) + ->groupBy('student_id') + ->map(fn($group) => $group->values()->all()) + ->all(); + + // Bulk-load all attendance records (summaries) for the section in one query + $allRecords = AttendanceRecord::query() + ->whereIn('student_id', $activeIds) + ->where(function ($q) use ($secCode, $secPk) { + $q->where('class_section_id', $secCode); + if ($secPk && (string)$secPk !== (string)$secCode) { + $q->orWhere('class_section_id', $secPk); + } + }) + ->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester)) + ->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear)) + ->get() + ->keyBy('student_id') + ->map(fn($row) => $row->toArray()) + ->all(); + $hasRoster = false; - foreach ($students as $sc) { - $studentId = (int)$sc['student_id']; - - $student = $this->student - ->query() - ->select(['id', 'firstname', 'lastname', 'school_id']) - ->where('id', $studentId) - ->where('is_active', 1) - ->first(); - + foreach ($studentIds as $studentId) { + $student = $studentProfiles[$studentId] ?? null; if (!$student) { continue; } - $student = $student->toArray(); $studentsBySection[$secCode][] = $student; $studentSchoolMap[$studentId] = (string)($student['school_id'] ?? ''); $hasRoster = true; - $entries = AttendanceData::query() - ->where('student_id', $studentId) - ->where(function ($q) use ($secCode, $secPk) { - $q->where('class_section_id', $secCode); - if ($secPk && (string)$secPk !== (string)$secCode) { - $q->orWhere('class_section_id', $secPk); - } - }) - ->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester)) - ->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear)) - ->orderBy('date') - ->get() - ->map(fn($row) => $row->toArray()) - ->all(); - - $entries = $this->attendanceService->normalizeAttendanceEntries($entries); + $entries = $this->attendanceService->normalizeAttendanceEntries( + array_map(fn($r) => (array)$r, $allEntries[$studentId] ?? []) + ); $attendanceData[$secCode][$studentId] = $entries; foreach ($entries as $entry) { @@ -561,19 +592,7 @@ class AttendanceQueryService } } - $summary = AttendanceRecord::query() - ->where('student_id', $studentId) - ->where(function ($q) use ($secCode, $secPk) { - $q->where('class_section_id', $secCode); - if ($secPk && (string)$secPk !== (string)$secCode) { - $q->orWhere('class_section_id', $secPk); - } - }) - ->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester)) - ->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear)) - ->first(); - - $attendanceRecord[$secCode][$studentId] = $summary?->toArray() ?? [ + $attendanceRecord[$secCode][$studentId] = $allRecords[$studentId] ?? [ 'total_presence' => 0, 'total_late' => 0, 'total_absence' => 0, @@ -627,7 +646,7 @@ class AttendanceQueryService foreach ($events as $event) { $d = substr((string)($event['date'] ?? ''), 0, 10); if ($d !== '') { - $noSchoolDays[$d] = true; + $noSchoolDays[$d] = trim((string)($event['title'] ?? 'No School')) ?: 'No School'; } } diff --git a/app/Services/Attendance/SemesterRangeService.php b/app/Services/Attendance/SemesterRangeService.php index e8c43d7c..c4c065f5 100644 --- a/app/Services/Attendance/SemesterRangeService.php +++ b/app/Services/Attendance/SemesterRangeService.php @@ -2,6 +2,7 @@ namespace App\Services\Attendance; +use App\Models\Configuration; use Carbon\Carbon; class SemesterRangeService @@ -19,6 +20,16 @@ class SemesterRangeService public function getSchoolYearRange(string $schoolYear): array { + $fallStart = Configuration::getConfig('fall_semester_start'); + $lastDay = Configuration::getConfig('last_school_day'); + + if ($fallStart && $lastDay) { + return [ + Carbon::parse($fallStart)->toDateString(), + Carbon::parse($lastDay)->toDateString(), + ]; + } + [$startYear, $endYear] = $this->parseSchoolYear($schoolYear); return [ @@ -29,9 +40,29 @@ class SemesterRangeService public function getSemesterRange(string $schoolYear, string $semester): ?array { - [$startYear, $endYear] = $this->parseSchoolYear($schoolYear); $semester = $this->normalizeSemester($semester); + $fallStart = Configuration::getConfig('fall_semester_start'); + $springStart = Configuration::getConfig('spring_semester_start'); + $lastDay = Configuration::getConfig('last_school_day'); + + if ($semester === 'Fall' && $fallStart && $springStart) { + return [ + Carbon::parse($fallStart)->toDateString(), + Carbon::parse($springStart)->subDay()->toDateString(), + ]; + } + + if ($semester === 'Spring' && $springStart && $lastDay) { + return [ + Carbon::parse($springStart)->toDateString(), + Carbon::parse($lastDay)->toDateString(), + ]; + } + + // Fallback to hardcoded logic when config keys are missing + [$startYear, $endYear] = $this->parseSchoolYear($schoolYear); + return match ($semester) { 'Fall' => [ Carbon::create($startYear, 9, 21)->toDateString(), diff --git a/app/Services/Grading/GradingOverviewService.php b/app/Services/Grading/GradingOverviewService.php index 1fe2228a..3bfb14d9 100644 --- a/app/Services/Grading/GradingOverviewService.php +++ b/app/Services/Grading/GradingOverviewService.php @@ -2,27 +2,14 @@ namespace App\Services\Grading; -use App\Models\AttendanceRecord; -use App\Models\CalendarEvent; use App\Models\Configuration; -use App\Models\ClassSection; use App\Models\GradingLock; -use App\Models\Student; -use App\Services\Scores\AttendanceCalculator; -use App\Services\Scores\SemesterScoreService; use Illuminate\Support\Facades\DB; class GradingOverviewService { - private AttendanceCalculator $attendanceCalculator; - - public function __construct(private SemesterScoreService $semesterScoreService) + public function __construct() { - $this->attendanceCalculator = new AttendanceCalculator( - new AttendanceRecord(), - new Configuration(), - new CalendarEvent() - ); } public function overview(?int $classId, ?string $semester, ?string $schoolYear): array @@ -38,62 +25,9 @@ class GradingOverviewService $scoresReleasedFall = $this->getParentScoresReleasedForSemester('Fall'); $scoresReleasedSpring = $this->getParentScoresReleasedForSemester('Spring'); - if ($classId && $this->semesterScoreService) { - $sectionIds = ClassSection::query() - ->select('class_section_id') - ->where('class_id', $classId) - ->pluck('class_section_id') - ->map(fn ($v) => (int) $v) - ->all(); - foreach ($sectionIds as $sectionId) { - $studentTeacherInfo = Student::getStudentInfoByClassSectionId( - $sectionId, - $semester, - $schoolYear - ); - if (empty($studentTeacherInfo)) { - continue; - } - try { - $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); - } catch (\Throwable $e) { - } - } - } - $rows = $this->buildGradingRows($semester, $schoolYear); - $counts = $this->loadScoreCounts($rows, $semester, $schoolYear); - $sectionsNeedingRefresh = []; - foreach ($rows as $row) { - $sectionId = (int) ($row['section_id'] ?? 0); - if ($sectionId <= 0) { - continue; - } - if ($row['ss_ptap_score'] === null || $row['ss_semester_score'] === null) { - $sectionsNeedingRefresh[$sectionId] = true; - } - } - - if (!empty($sectionsNeedingRefresh)) { - foreach (array_keys($sectionsNeedingRefresh) as $sectionId) { - $studentTeacherInfo = Student::getStudentInfoByClassSectionId( - $sectionId, - $semester, - $schoolYear - ); - if (empty($studentTeacherInfo)) { - continue; - } - try { - $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); - } catch (\Throwable $e) { - } - } - $rows = $this->buildGradingRows($semester, $schoolYear); - } - $grades = []; $studentsBySection = []; foreach ($rows as $row) { @@ -126,18 +60,7 @@ class GradingOverviewService continue; } - $attendanceScore = $this->calculateAttendanceScoreForStudent( - $studentId, - $semester, - $schoolYear, - $sectionId - ); - if ($attendanceScore === null) { - $rawAttendance = $row['ss_attendance_score'] ?? null; - if ($rawAttendance !== null && $rawAttendance !== '') { - $attendanceScore = round((float) $rawAttendance, 2); - } - } + $attendanceScore = $this->normalizeScore($row['ss_attendance_score'] ?? null); $studentRow = [ 'id' => $studentId, @@ -320,21 +243,6 @@ class GradingOverviewService return $map; } - private function calculateAttendanceScoreForStudent(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): ?float - { - try { - $result = $this->attendanceCalculator->calculate($studentId, $semester, $schoolYear, $classSectionId); - $score = $result['attendance_score'] ?? null; - if ($score === null || $score === '') { - return null; - } - return round((float) $score, 2); - } catch (\Throwable $e) { - } - - return null; - } - private function normalizeScore($value): ?float { if ($value === null || $value === '') { diff --git a/app/Services/Grading/GradingScoreService.php b/app/Services/Grading/GradingScoreService.php index 8fce936e..52b07c81 100644 --- a/app/Services/Grading/GradingScoreService.php +++ b/app/Services/Grading/GradingScoreService.php @@ -11,6 +11,7 @@ use App\Models\Quiz; use App\Models\ScoreComment; use App\Models\SemesterScore; use App\Models\Student; +use App\Models\ClassSection; use App\Services\Scores\SemesterScoreService; use RuntimeException; @@ -44,6 +45,9 @@ class GradingScoreService 'scores' => $scores, 'type' => $type, 'class_section_id' => $classSectionId, + 'class_section_name' => $classSectionId > 0 + ? (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? '') + : '', 'semester' => $semester, 'scores_locked' => $scoresLocked, ]; diff --git a/app/Services/Scores/ExamScoreService.php b/app/Services/Scores/ExamScoreService.php index 5e5a9a2d..b1b50a66 100644 --- a/app/Services/Scores/ExamScoreService.php +++ b/app/Services/Scores/ExamScoreService.php @@ -6,6 +6,7 @@ use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\Student; use App\Models\TeacherClass; +use App\Models\ClassSection; use Illuminate\Support\Facades\DB; class ExamScoreService @@ -79,6 +80,7 @@ class ExamScoreService return [ 'students' => $rows, + 'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''), 'semester' => $semester, 'schoolYear' => $schoolYear, 'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear), diff --git a/app/Services/Scores/HomeworkScoreService.php b/app/Services/Scores/HomeworkScoreService.php index 8b47f517..d46a45ff 100644 --- a/app/Services/Scores/HomeworkScoreService.php +++ b/app/Services/Scores/HomeworkScoreService.php @@ -8,6 +8,7 @@ use App\Models\MissingScoreOverride; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\ClassSection; class HomeworkScoreService { @@ -84,6 +85,7 @@ class HomeworkScoreService ->get() ->map(fn ($row) => [ 'student_id' => (int) $row->id, + 'school_id' => (string) $row->school_id, 'firstname' => (string) $row->firstname, 'lastname' => (string) $row->lastname, 'scores' => [], @@ -115,6 +117,7 @@ class HomeworkScoreService return [ 'students' => $students, 'headers' => $headers, + 'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''), 'semester' => $semester, 'schoolYear' => $schoolYear, 'scoresLocked' => $this->isLockedForAnySchoolYear($classSectionId, $semester, $schoolYearVariants), diff --git a/app/Services/Scores/ParticipationScoreService.php b/app/Services/Scores/ParticipationScoreService.php index b56faaed..b50a3c2a 100644 --- a/app/Services/Scores/ParticipationScoreService.php +++ b/app/Services/Scores/ParticipationScoreService.php @@ -3,6 +3,7 @@ namespace App\Services\Scores; use App\Models\GradingLock; +use App\Models\ClassSection; use App\Models\MissingScoreOverride; use App\Models\Participation; use App\Models\Student; @@ -95,6 +96,7 @@ class ParticipationScoreService return [ 'students' => $students, + 'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''), 'semester' => $semester, 'schoolYear' => $schoolYear, 'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear), diff --git a/app/Services/Scores/ProjectScoreService.php b/app/Services/Scores/ProjectScoreService.php index 080305ec..45bb8957 100644 --- a/app/Services/Scores/ProjectScoreService.php +++ b/app/Services/Scores/ProjectScoreService.php @@ -8,6 +8,7 @@ use App\Models\Project; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\ClassSection; use Illuminate\Support\Facades\DB; class ProjectScoreService @@ -76,6 +77,7 @@ class ProjectScoreService ->get() ->map(fn ($row) => [ 'student_id' => (int) $row->id, + 'school_id' => (string) $row->school_id, 'firstname' => (string) $row->firstname, 'lastname' => (string) $row->lastname, 'scores' => [], @@ -107,6 +109,7 @@ class ProjectScoreService return [ 'students' => $students, 'headers' => $headers, + 'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''), 'semester' => $semester, 'schoolYear' => $schoolYear, 'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear), diff --git a/app/Services/Scores/QuizScoreService.php b/app/Services/Scores/QuizScoreService.php index 74a9e8bc..072b2427 100644 --- a/app/Services/Scores/QuizScoreService.php +++ b/app/Services/Scores/QuizScoreService.php @@ -8,6 +8,7 @@ use App\Models\Quiz; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\ClassSection; class QuizScoreService { @@ -75,6 +76,7 @@ class QuizScoreService ->get() ->map(fn ($row) => [ 'student_id' => (int) $row->id, + 'school_id' => (string) $row->school_id, 'firstname' => (string) $row->firstname, 'lastname' => (string) $row->lastname, 'scores' => [], @@ -106,6 +108,7 @@ class QuizScoreService return [ 'students' => $students, 'headers' => $headers, + 'classSectionName' => (string) (ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name') ?? ''), 'semester' => $semester, 'schoolYear' => $schoolYear, 'scoresLocked' => GradingLock::isLocked($classSectionId, $semester, $schoolYear), diff --git a/app/Services/Scores/ScoreCommentService.php b/app/Services/Scores/ScoreCommentService.php index 4204be4a..6796f64c 100644 --- a/app/Services/Scores/ScoreCommentService.php +++ b/app/Services/Scores/ScoreCommentService.php @@ -3,6 +3,7 @@ namespace App\Services\Scores; use App\Models\Configuration; +use App\Models\ClassSection; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\ScoreComment; @@ -111,6 +112,9 @@ class ScoreCommentService 'semester' => $activeSemester, 'schoolYear' => $schoolYear, 'classSectionId' => $isAllSections ? 'allsections' : $classSectionInt, + 'classSectionName' => (!$isAllSections && $classSectionInt) + ? (string) (ClassSection::query()->where('class_section_id', $classSectionInt)->value('class_section_name') ?? '') + : '', 'scoresLocked' => (!$isAllSections && $classSectionInt) ? GradingLock::isLocked($classSectionInt, $activeSemester, $schoolYear) : false, diff --git a/app/Services/Scores/ScorePredictorDataService.php b/app/Services/Scores/ScorePredictorDataService.php index 0bb303c6..66da63b8 100644 --- a/app/Services/Scores/ScorePredictorDataService.php +++ b/app/Services/Scores/ScorePredictorDataService.php @@ -9,11 +9,14 @@ class ScorePredictorDataService public function classSections(string $schoolYear): array { return DB::table('classSection as cs') - ->select('cs.*') + ->select([ + 'cs.class_section_id', + 'cs.class_section_name', + ]) ->join('student_class as sc', 'sc.class_section_id', '=', 'cs.class_section_id') ->where('sc.school_year', $schoolYear) ->where('cs.class_section_name', 'not like', 'KG%') - ->groupBy('cs.id') + ->distinct() ->orderBy('cs.class_section_name', 'ASC') ->get() ->map(fn ($row) => (array) $row) @@ -28,9 +31,9 @@ class ScorePredictorDataService 's.school_id', 's.firstname', 's.lastname', - 'fall.semester_score as fall_score', - 'spring.semester_score as spring_score', - 'sc.class_section_id as class_section_id', + DB::raw('MAX(fall.semester_score) as fall_score'), + DB::raw('MAX(spring.semester_score) as spring_score'), + DB::raw('MAX(sc.class_section_id) as class_section_id'), ]) ->leftJoin('student_class as sc', function ($join) use ($schoolYear) { $join->on('sc.student_id', '=', 's.id') @@ -58,7 +61,7 @@ class ScorePredictorDataService } return $builder - ->groupBy('s.id') + ->groupBy('s.id', 's.school_id', 's.firstname', 's.lastname') ->get() ->map(fn ($row) => (array) $row) ->all(); diff --git a/app/Services/Students/StudentAssignmentService.php b/app/Services/Students/StudentAssignmentService.php index 998ff955..c4ff20e2 100644 --- a/app/Services/Students/StudentAssignmentService.php +++ b/app/Services/Students/StudentAssignmentService.php @@ -90,6 +90,7 @@ class StudentAssignmentService 'student_id' => $studentId, 'class_section_id' => $classSectionId, 'school_year' => $schoolYear, + 'semester' => $semester, 'is_event_only' => $eventFlag, 'updated_by' => $userId, 'updated_at' => now(), diff --git a/app/Services/Whatsapp/WhatsappContactService.php b/app/Services/Whatsapp/WhatsappContactService.php index 5947fd6d..5075d8ab 100644 --- a/app/Services/Whatsapp/WhatsappContactService.php +++ b/app/Services/Whatsapp/WhatsappContactService.php @@ -147,19 +147,19 @@ class WhatsappContactService } $rows = DB::table('student_class as sc') - ->select(" - sc.class_section_id, - u.id AS primary_id, - u.firstname AS primary_firstname, - u.lastname AS primary_lastname, - u.email AS primary_email, - u.cellphone AS primary_phone, - sp.id AS second_id, - sp.secondparent_firstname AS second_firstname, - sp.secondparent_lastname AS second_lastname, - sp.secondparent_email AS second_email, - sp.secondparent_phone AS second_phone - ") + ->select([ + 'sc.class_section_id', + 'u.id as primary_id', + 'u.firstname as primary_firstname', + 'u.lastname as primary_lastname', + 'u.email as primary_email', + 'u.cellphone as primary_phone', + 'sp.id as second_id', + 'sp.secondparent_firstname as second_firstname', + 'sp.secondparent_lastname as second_lastname', + 'sp.secondparent_email as second_email', + 'sp.secondparent_phone as second_phone', + ]) ->join('students as s', 's.id', '=', 'sc.student_id') ->join('users as u', 'u.id', '=', 's.parent_id') ->leftJoin('parents as sp', function ($join) { diff --git a/routes/api.php b/routes/api.php index 82fde2a9..b48cca9e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -58,6 +58,7 @@ use App\Http\Controllers\Api\Inventory\InventoryCategoryController; use App\Http\Controllers\Api\Inventory\InventoryMovementController; use App\Http\Controllers\Api\Auth\IpBanController; use App\Http\Controllers\Api\Frontend\LandingPageController; +use App\Http\Controllers\Api\Attendance\EarlyDismissalsController; use App\Http\Controllers\Api\Attendance\LateSlipLogsController; use App\Http\Controllers\Api\Messaging\MessagesController; use App\Http\Controllers\Api\Incidents\IncidentController as IncidentApiController; @@ -433,6 +434,13 @@ Route::prefix('v1')->group(function () { Route::get('{id}', [LateSlipLogsController::class, 'show']); Route::delete('{id}', [LateSlipLogsController::class, 'destroy']); }); + + Route::prefix('early-dismissals')->group(function () { + Route::get('/', [EarlyDismissalsController::class, 'index']); + Route::get('/student-options', [EarlyDismissalsController::class, 'studentOptions']); + Route::post('/', [EarlyDismissalsController::class, 'store']); + Route::post('/signature', [EarlyDismissalsController::class, 'uploadSignature']); + }); }); Route::middleware('auth:api')->prefix('attendance-tracking')->group(function () {