add grading, attendnace management

This commit is contained in:
root
2026-04-29 17:39:33 -04:00
parent 8beeed883f
commit df5266c5b5
27 changed files with 386 additions and 157 deletions
@@ -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');
@@ -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';
}
}
@@ -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(),
@@ -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 === '') {
@@ -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,
];
+2
View File
@@ -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),
@@ -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),
@@ -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),
@@ -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),
+3
View File
@@ -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),
@@ -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,
@@ -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();
@@ -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(),
@@ -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) {