add controllers, servoices
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\CurrentFlag;
|
||||
use App\Models\ParentMeetingSchedule;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\Student;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use RuntimeException;
|
||||
|
||||
class GradingBelowSixtyService
|
||||
{
|
||||
public function listRows(string $schoolYear, string $semester): array
|
||||
{
|
||||
$semesterKey = strtolower(trim($semester));
|
||||
$rows = DB::table('semester_scores as ss')
|
||||
->select([
|
||||
's.id as student_id',
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'ss.homework_avg',
|
||||
'ss.project_avg',
|
||||
'ss.participation_score',
|
||||
DB::raw('COALESCE(ss.test_avg, ss.quiz_avg) as test_avg'),
|
||||
'ss.ptap_score',
|
||||
'ss.attendance_score',
|
||||
'ss.midterm_exam_score',
|
||||
'ss.final_exam_score',
|
||||
'ss.semester_score',
|
||||
])
|
||||
->join('students as s', 's.id', '=', 'ss.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
|
||||
->where('s.is_active', 1)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(ss.semester)) = ?', [$semesterKey])
|
||||
->whereNotNull('ss.semester_score')
|
||||
->where('ss.semester_score', '<', 60)
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_unique(array_map(
|
||||
static fn ($r) => (int) ($r['student_id'] ?? 0),
|
||||
$rows
|
||||
)));
|
||||
$studentIds = array_values(array_filter($studentIds, static fn ($id) => $id > 0));
|
||||
|
||||
$commentMap = [];
|
||||
if (!empty($studentIds)) {
|
||||
$commentRows = ScoreComment::query()
|
||||
->select('student_id', 'comment', 'created_at')
|
||||
->where('score_type', 'general')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey])
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->get();
|
||||
foreach ($commentRows as $row) {
|
||||
$sid = (int) $row->student_id;
|
||||
if ($sid > 0 && !isset($commentMap[$sid])) {
|
||||
$commentMap[$sid] = (string) ($row->comment ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$statusMap = [];
|
||||
if (!empty($studentIds)) {
|
||||
$flagRows = CurrentFlag::query()
|
||||
->select('student_id', 'flag_state')
|
||||
->where('flag', 'grade')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey])
|
||||
->whereIn('student_id', $studentIds)
|
||||
->get();
|
||||
foreach ($flagRows as $row) {
|
||||
$sid = (int) $row->student_id;
|
||||
if ($sid > 0) {
|
||||
$statusMap[$sid] = (string) ($row->flag_state ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
$row['comment'] = $commentMap[$sid] ?? '';
|
||||
$flagState = strtolower(trim((string) ($statusMap[$sid] ?? '')));
|
||||
$row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open';
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function emailContext(int $studentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
if (empty($row)) {
|
||||
throw new RuntimeException('Student record not found for the selected term.');
|
||||
}
|
||||
|
||||
$studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
|
||||
$subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear);
|
||||
$parentName = $this->fetchBelowSixtyParentName($studentId);
|
||||
|
||||
$scores = [
|
||||
'homework_avg' => $row['homework_avg'] ?? null,
|
||||
'project_avg' => $row['project_avg'] ?? null,
|
||||
'participation_score' => $row['participation_score'] ?? null,
|
||||
'test_avg' => $row['test_avg'] ?? null,
|
||||
'ptap_score' => $row['ptap_score'] ?? null,
|
||||
'attendance_score' => $row['attendance_score'] ?? null,
|
||||
'midterm_exam_score' => $row['midterm_exam_score'] ?? null,
|
||||
'semester_score' => $row['semester_score'] ?? null,
|
||||
];
|
||||
|
||||
return [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $studentName !== '' ? $studentName : 'Student',
|
||||
'parent_name' => $parentName,
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'scores' => $scores,
|
||||
'comment' => $row['comment'] ?? '',
|
||||
'subject' => $subject,
|
||||
];
|
||||
}
|
||||
|
||||
public function sendEmail(array $payload): void
|
||||
{
|
||||
Event::dispatch('below60.email', $payload);
|
||||
}
|
||||
|
||||
public function updateStatus(int $studentId, string $semester, string $schoolYear, string $status, string $note, ?int $userId): void
|
||||
{
|
||||
$status = ucfirst(strtolower($status));
|
||||
if (!in_array($status, ['Open', 'Closed'], true)) {
|
||||
throw new RuntimeException('Invalid status.');
|
||||
}
|
||||
|
||||
$semesterKey = strtolower(trim($semester));
|
||||
$existing = CurrentFlag::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('flag', 'grade')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey])
|
||||
->first();
|
||||
|
||||
$now = now();
|
||||
|
||||
if ($existing) {
|
||||
$data = [
|
||||
'flag_state' => $status,
|
||||
'flag_datetime' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($status === 'Open') {
|
||||
$data['updated_by_open'] = $userId;
|
||||
if ($note !== '') {
|
||||
$prev = (string) ($existing->open_description ?? '');
|
||||
$data['open_description'] = trim($prev . PHP_EOL . $note);
|
||||
}
|
||||
} else {
|
||||
$data['updated_by_closed'] = $userId;
|
||||
if ($note !== '') {
|
||||
$prev = (string) ($existing->close_description ?? '');
|
||||
$data['close_description'] = trim($prev . PHP_EOL . $note);
|
||||
}
|
||||
}
|
||||
$existing->update($data);
|
||||
return;
|
||||
}
|
||||
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
$studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
|
||||
$grade = (string) ($row['class_section_name'] ?? '');
|
||||
$data = [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => $studentName !== '' ? $studentName : 'Student',
|
||||
'grade' => $grade,
|
||||
'flag' => 'grade',
|
||||
'flag_datetime' => $now,
|
||||
'flag_state' => $status,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
if ($status === 'Open') {
|
||||
$data['updated_by_open'] = $userId;
|
||||
if ($note !== '') {
|
||||
$data['open_description'] = $note;
|
||||
}
|
||||
} else {
|
||||
$data['updated_by_closed'] = $userId;
|
||||
if ($note !== '') {
|
||||
$data['close_description'] = $note;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentFlag::query()->create($data);
|
||||
}
|
||||
|
||||
public function meetingContext(int $studentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$context = $this->fetchBelowSixtyMeetingContext($studentId, $schoolYear, $semester);
|
||||
if (empty($context)) {
|
||||
throw new RuntimeException('Student record not found for the selected term.');
|
||||
}
|
||||
|
||||
return $context + [
|
||||
'student_id' => $studentId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
];
|
||||
}
|
||||
|
||||
public function saveMeeting(array $payload, ?int $userId): void
|
||||
{
|
||||
$studentId = (int) ($payload['student_id'] ?? 0);
|
||||
$semester = (string) ($payload['semester'] ?? '');
|
||||
$schoolYear = (string) ($payload['school_year'] ?? '');
|
||||
$date = trim((string) ($payload['date'] ?? ''));
|
||||
$time = trim((string) ($payload['time'] ?? ''));
|
||||
$notes = trim((string) ($payload['notes'] ?? ''));
|
||||
|
||||
if ($studentId <= 0 || $semester === '' || $schoolYear === '' || $date === '') {
|
||||
throw new RuntimeException('Missing required fields.');
|
||||
}
|
||||
|
||||
$context = $this->fetchBelowSixtyMeetingContext($studentId, $schoolYear, $semester);
|
||||
if (empty($context)) {
|
||||
throw new RuntimeException('Student record not found for the selected term.');
|
||||
}
|
||||
|
||||
ParentMeetingSchedule::query()->create([
|
||||
'student_id' => $studentId,
|
||||
'parent_user_id' => $context['parent_user_id'] ?? null,
|
||||
'parent_name' => $context['parent_name'],
|
||||
'student_name' => $context['student_name'],
|
||||
'class_section_name' => $context['class_section_name'],
|
||||
'date' => $date,
|
||||
'time' => $time !== '' ? $time : null,
|
||||
'notes' => $notes !== '' ? $notes : null,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'status' => 'scheduled',
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyEmailRow(int $studentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$semesterKey = strtolower(trim($semester));
|
||||
$row = DB::table('semester_scores as ss')
|
||||
->select([
|
||||
's.id as student_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
'ss.homework_avg',
|
||||
'ss.project_avg',
|
||||
'ss.participation_score',
|
||||
DB::raw('COALESCE(ss.test_avg, ss.quiz_avg) as test_avg'),
|
||||
'ss.ptap_score',
|
||||
'ss.attendance_score',
|
||||
'ss.midterm_exam_score',
|
||||
'ss.semester_score',
|
||||
])
|
||||
->join('students as s', 's.id', '=', 'ss.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->where('ss.student_id', $studentId)
|
||||
->where('s.is_active', 1)
|
||||
->whereRaw('LOWER(TRIM(ss.semester)) = ?', [$semesterKey])
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$commentRow = ScoreComment::query()
|
||||
->select('comment')
|
||||
->where('score_type', 'general')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(semester)) = ?', [$semesterKey])
|
||||
->where('student_id', $studentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->first();
|
||||
|
||||
$row = (array) $row;
|
||||
$row['comment'] = (string) ($commentRow?->comment ?? '');
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyParentName(int $studentId): string
|
||||
{
|
||||
$parentName = 'Parent/Guardian';
|
||||
try {
|
||||
$rows = DB::select(
|
||||
"SELECT u.firstname, u.lastname
|
||||
FROM family_students fs
|
||||
JOIN family_guardians fg ON fg.family_id = fs.family_id
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
WHERE fs.student_id = ?
|
||||
ORDER BY fg.is_primary DESC, u.lastname, u.firstname
|
||||
LIMIT 1",
|
||||
[$studentId]
|
||||
);
|
||||
if (!empty($rows[0])) {
|
||||
$candidate = trim((string) ($rows[0]->firstname ?? '') . ' ' . (string) ($rows[0]->lastname ?? ''));
|
||||
if ($candidate !== '') {
|
||||
$parentName = $candidate;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
return $parentName;
|
||||
}
|
||||
|
||||
private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string
|
||||
{
|
||||
$subject = 'Student Performance Alert';
|
||||
if ($studentName !== '') {
|
||||
$subject .= ' — ' . $studentName;
|
||||
}
|
||||
if ($semester !== '' || $schoolYear !== '') {
|
||||
$subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')';
|
||||
}
|
||||
return $subject;
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyMeetingContext(int $studentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester);
|
||||
if (empty($row)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parentName = 'Parent/Guardian';
|
||||
$parentUserId = null;
|
||||
try {
|
||||
$pRows = DB::select(
|
||||
"SELECT u.id AS user_id, u.firstname, u.lastname
|
||||
FROM family_students fs
|
||||
JOIN family_guardians fg ON fg.family_id = fs.family_id
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
WHERE fs.student_id = ?
|
||||
ORDER BY fg.is_primary DESC, u.lastname, u.firstname
|
||||
LIMIT 1",
|
||||
[$studentId]
|
||||
);
|
||||
if (!empty($pRows[0])) {
|
||||
$parentUserId = (int) ($pRows[0]->user_id ?? 0) ?: null;
|
||||
$parentName = trim((string) ($pRows[0]->firstname ?? '') . ' ' . (string) ($pRows[0]->lastname ?? ''));
|
||||
if ($parentName === '') {
|
||||
$parentName = 'Parent/Guardian';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
if ($parentUserId === null) {
|
||||
try {
|
||||
$srow = DB::selectOne(
|
||||
"SELECT s.parent_id, u.firstname, u.lastname
|
||||
FROM students s
|
||||
LEFT JOIN users u ON u.id = s.parent_id
|
||||
WHERE s.id = ?
|
||||
LIMIT 1",
|
||||
[$studentId]
|
||||
);
|
||||
if (!empty($srow)) {
|
||||
$parentUserId = (int) ($srow->parent_id ?? 0) ?: null;
|
||||
$fallbackName = trim((string) ($srow->firstname ?? '') . ' ' . (string) ($srow->lastname ?? ''));
|
||||
if ($fallbackName !== '') {
|
||||
$parentName = $fallbackName;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? ''));
|
||||
|
||||
return [
|
||||
'student_name' => $studentName !== '' ? $studentName : 'Student',
|
||||
'parent_name' => $parentName,
|
||||
'parent_user_id' => $parentUserId,
|
||||
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\GradingLock;
|
||||
use App\Models\ClassSection;
|
||||
use RuntimeException;
|
||||
|
||||
class GradingLockService
|
||||
{
|
||||
public function toggle(int $classSectionId, string $semester, string $schoolYear, ?int $userId): bool
|
||||
{
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') {
|
||||
throw new RuntimeException('Missing class section or term.');
|
||||
}
|
||||
|
||||
$existing = GradingLock::getLock($classSectionId, $semester, $schoolYear);
|
||||
|
||||
if ($existing && $existing->is_locked) {
|
||||
$existing->update([
|
||||
'is_locked' => 0,
|
||||
'locked_by' => null,
|
||||
'locked_at' => null,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
'is_locked' => 1,
|
||||
'locked_by' => $userId ?: null,
|
||||
'locked_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
GradingLock::query()->create([
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'is_locked' => 1,
|
||||
'locked_by' => $userId ?: null,
|
||||
'locked_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function lockAll(string $semester, string $schoolYear, ?int $userId): int
|
||||
{
|
||||
if ($semester === '' || $schoolYear === '') {
|
||||
throw new RuntimeException('Missing semester or school year.');
|
||||
}
|
||||
|
||||
$sectionIds = ClassSection::query()
|
||||
->select('class_section_id')
|
||||
->groupBy('class_section_id')
|
||||
->pluck('class_section_id')
|
||||
->map(fn ($v) => (int) $v)
|
||||
->filter(fn ($v) => $v > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (empty($sectionIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$existingLocks = GradingLock::query()
|
||||
->whereIn('class_section_id', $sectionIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
$existingBySection = [];
|
||||
foreach ($existingLocks as $row) {
|
||||
$sid = (int) $row->class_section_id;
|
||||
if ($sid > 0) {
|
||||
$existingBySection[$sid] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$insertRows = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($sectionIds as $sid) {
|
||||
if (!empty($existingBySection[$sid])) {
|
||||
if ($existingBySection[$sid]->is_locked) {
|
||||
continue;
|
||||
}
|
||||
$existingBySection[$sid]->update([
|
||||
'is_locked' => 1,
|
||||
'locked_by' => $userId ?: null,
|
||||
'locked_at' => $now,
|
||||
]);
|
||||
$count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertRows[] = [
|
||||
'class_section_id' => $sid,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'is_locked' => 1,
|
||||
'locked_by' => $userId ?: null,
|
||||
'locked_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$count++;
|
||||
}
|
||||
|
||||
if (!empty($insertRows)) {
|
||||
GradingLock::query()->insert($insertRows);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
<?php
|
||||
|
||||
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)
|
||||
{
|
||||
$this->attendanceCalculator = new AttendanceCalculator(
|
||||
new AttendanceRecord(),
|
||||
new Configuration(),
|
||||
new CalendarEvent()
|
||||
);
|
||||
}
|
||||
|
||||
public function overview(?int $classId, ?string $semester, ?string $schoolYear): array
|
||||
{
|
||||
$schoolYear = $schoolYear ?: (string) (Configuration::getConfig('school_year') ?? '');
|
||||
$configuredSemester = (string) (Configuration::getConfig('semester') ?? '');
|
||||
$semesterOptions = $this->getSemestersForSchoolYear($schoolYear, $configuredSemester);
|
||||
$semester = $this->resolveSemesterSelection($semester, $semesterOptions, $configuredSemester);
|
||||
|
||||
$this->ensureParentReleaseKeyExists('Fall');
|
||||
$this->ensureParentReleaseKeyExists('Spring');
|
||||
$scoresReleased = $this->getParentScoresReleasedForSemester($semester);
|
||||
$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) {
|
||||
$sectionId = (int) ($row['section_id'] ?? 0);
|
||||
$classIdValue = (int) ($row['class_id'] ?? 0);
|
||||
$sectionName = (string) ($row['class_section_name'] ?? '');
|
||||
if ($sectionId <= 0 || $classIdValue <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($grades[$classIdValue])) {
|
||||
$grades[$classIdValue] = [];
|
||||
}
|
||||
$exists = false;
|
||||
foreach ($grades[$classIdValue] as $section) {
|
||||
if ((int) $section['class_section_id'] === $sectionId) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$exists) {
|
||||
$grades[$classIdValue][] = [
|
||||
'class_section_id' => $sectionId,
|
||||
'class_section_name' => $sectionName,
|
||||
];
|
||||
}
|
||||
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$studentRow = [
|
||||
'id' => $studentId,
|
||||
'school_id' => $row['school_id'] ?? null,
|
||||
'firstname' => $row['firstname'] ?? null,
|
||||
'lastname' => $row['lastname'] ?? null,
|
||||
'class_id' => $classIdValue,
|
||||
'ptap' => $this->normalizeScore($row['ss_ptap_score'] ?? null),
|
||||
'semester_score' => $this->normalizeScore($row['ss_semester_score'] ?? null),
|
||||
'attendance' => $attendanceScore,
|
||||
'homework_avg' => $this->normalizeCountedScore($row['ss_homework_avg'] ?? null, $counts['homework'] ?? [], $sectionId, $studentId),
|
||||
'project_avg' => $this->normalizeCountedScore($row['ss_project_avg'] ?? null, $counts['project'] ?? [], $sectionId, $studentId),
|
||||
'quiz_avg' => $this->normalizeCountedScore($row['ss_quiz_avg'] ?? null, $counts['quiz'] ?? [], $sectionId, $studentId),
|
||||
'participation' => $this->normalizeCountedScore($row['ss_participation_score'] ?? null, $counts['participation'] ?? [], $sectionId, $studentId),
|
||||
'midterm_exam' => $this->normalizeCountedScore($row['ss_midterm_exam_score'] ?? null, $counts['midterm'] ?? [], $sectionId, $studentId),
|
||||
'final_exam' => $this->normalizeScore($row['ss_final_exam_score'] ?? null),
|
||||
'matched_biz_csid' => $row['matched_biz_csid'] ?? null,
|
||||
'matched_pk_csid' => $row['matched_pk_csid'] ?? null,
|
||||
'placement_level' => $row['placement_level'] ?? null,
|
||||
];
|
||||
|
||||
$studentsBySection[$sectionId][] = $studentRow;
|
||||
}
|
||||
|
||||
$scoreLocks = $this->loadScoreLocks($grades, $semester, $schoolYear);
|
||||
|
||||
return [
|
||||
'grades' => $grades,
|
||||
'students_by_section' => $studentsBySection,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'requested_class_id' => $classId,
|
||||
'semester_options' => $semesterOptions,
|
||||
'scores_released' => $scoresReleased,
|
||||
'scores_released_fall' => $scoresReleasedFall,
|
||||
'scores_released_spring' => $scoresReleasedSpring,
|
||||
'score_locks' => $scoreLocks,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildGradingRows(string $semester, string $schoolYear): array
|
||||
{
|
||||
$semLower = strtolower(trim($semester));
|
||||
|
||||
return DB::table('student_class as sc')
|
||||
->select([
|
||||
'cs.id as section_pk',
|
||||
'cs.class_section_id as section_id',
|
||||
'cs.class_id as class_id',
|
||||
'cs.class_section_name',
|
||||
's.id as student_id',
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'pl.level as placement_level',
|
||||
DB::raw('COALESCE(ss_b.ptap_score, ss_p.ptap_score) as ss_ptap_score'),
|
||||
DB::raw('COALESCE(ss_b.semester_score, ss_p.semester_score) as ss_semester_score'),
|
||||
DB::raw('COALESCE(ss_b.attendance_score, ss_p.attendance_score) as ss_attendance_score'),
|
||||
DB::raw('COALESCE(ss_b.homework_avg, ss_p.homework_avg) as ss_homework_avg'),
|
||||
DB::raw('COALESCE(ss_b.project_avg, ss_p.project_avg) as ss_project_avg'),
|
||||
DB::raw('COALESCE(ss_b.quiz_avg, ss_p.quiz_avg) as ss_quiz_avg'),
|
||||
DB::raw('COALESCE(ss_b.participation_score, ss_p.participation_score) as ss_participation_score'),
|
||||
DB::raw('COALESCE(ss_b.midterm_exam_score, ss_p.midterm_exam_score) as ss_midterm_exam_score'),
|
||||
DB::raw('COALESCE(ss_b.final_exam_score, ss_p.final_exam_score) as ss_final_exam_score'),
|
||||
'ss_b.class_section_id as matched_biz_csid',
|
||||
'ss_p.class_section_id as matched_pk_csid',
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->leftJoin('placement_levels as pl', function ($join) use ($schoolYear) {
|
||||
$join->on('pl.student_id', '=', 's.id')
|
||||
->where('pl.school_year', '=', $schoolYear);
|
||||
})
|
||||
->leftJoin('semester_scores as ss_b', function ($join) use ($semLower, $schoolYear) {
|
||||
$join->on('ss_b.student_id', '=', 's.id')
|
||||
->on('ss_b.class_section_id', '=', 'sc.class_section_id')
|
||||
->whereRaw('LOWER(TRIM(ss_b.semester)) = ?', [$semLower])
|
||||
->where('ss_b.school_year', '=', $schoolYear);
|
||||
})
|
||||
->leftJoin('semester_scores as ss_p', function ($join) use ($semLower, $schoolYear) {
|
||||
$join->on('ss_p.student_id', '=', 's.id')
|
||||
->on('ss_p.class_section_id', '=', 'cs.id')
|
||||
->whereRaw('LOWER(TRIM(ss_p.semester)) = ?', [$semLower])
|
||||
->where('ss_p.school_year', '=', $schoolYear);
|
||||
})
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('s.is_active', 1)
|
||||
->orderBy('cs.class_id', 'ASC')
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
private function loadScoreCounts(array $rows, string $semester, string $schoolYear): array
|
||||
{
|
||||
$sectionIds = [];
|
||||
$studentIds = [];
|
||||
foreach ($rows as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$sectionId = (int) ($row['section_id'] ?? 0);
|
||||
if ($studentId > 0) {
|
||||
$studentIds[$studentId] = true;
|
||||
}
|
||||
if ($sectionId > 0) {
|
||||
$sectionIds[$sectionId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$sectionIds = array_keys($sectionIds);
|
||||
$studentIds = array_keys($studentIds);
|
||||
if (empty($sectionIds) || empty($studentIds)) {
|
||||
return [
|
||||
'quiz' => [],
|
||||
'homework' => [],
|
||||
'project' => [],
|
||||
'participation' => [],
|
||||
'midterm' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'quiz' => $this->countScores('quiz', $sectionIds, $studentIds, $semester, $schoolYear),
|
||||
'homework' => $this->countScores('homework', $sectionIds, $studentIds, $semester, $schoolYear),
|
||||
'project' => $this->countScores('project', $sectionIds, $studentIds, $semester, $schoolYear),
|
||||
'participation' => $this->countScores('participation', $sectionIds, $studentIds, $semester, $schoolYear),
|
||||
'midterm' => $this->countScores('midterm_exam', $sectionIds, $studentIds, $semester, $schoolYear),
|
||||
];
|
||||
}
|
||||
|
||||
private function countScores(string $table, array $sectionIds, array $studentIds, string $semester, string $schoolYear): array
|
||||
{
|
||||
$rows = DB::table($table)
|
||||
->select('student_id', 'class_section_id', DB::raw('COUNT(*) as cnt'))
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('class_section_id', $sectionIds)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->whereNotNull('score')
|
||||
->groupBy('student_id', 'class_section_id')
|
||||
->get();
|
||||
|
||||
$counts = [];
|
||||
foreach ($rows as $row) {
|
||||
$counts[(int) $row->class_section_id][(int) $row->student_id] = (int) $row->cnt;
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
private function loadScoreLocks(array $grades, string $semester, string $schoolYear): array
|
||||
{
|
||||
$sectionIds = [];
|
||||
foreach ($grades as $sections) {
|
||||
foreach ($sections as $section) {
|
||||
$sid = (int) ($section['class_section_id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$sectionIds[$sid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$sectionIds = array_keys($sectionIds);
|
||||
if (empty($sectionIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$locks = GradingLock::query()
|
||||
->whereIn('class_section_id', $sectionIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
$map = [];
|
||||
foreach ($locks as $lock) {
|
||||
$map[(int) $lock->class_section_id] = (bool) $lock->is_locked;
|
||||
}
|
||||
|
||||
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 === '') {
|
||||
return null;
|
||||
}
|
||||
return round((float) $value, 2);
|
||||
}
|
||||
|
||||
private function normalizeCountedScore($value, array $countMap, int $sectionId, int $studentId): ?float
|
||||
{
|
||||
$score = $this->normalizeScore($value);
|
||||
if ($score !== null && (float) $score === 0.0) {
|
||||
$count = (int) ($countMap[$sectionId][$studentId] ?? 0);
|
||||
if ($count === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $score;
|
||||
}
|
||||
|
||||
private function resolveSemesterSelection(?string $requestedSemester, array $semesterOptions, ?string $fallbackSemester): string
|
||||
{
|
||||
if ($requestedSemester !== null && $requestedSemester !== '') {
|
||||
foreach ($semesterOptions as $option) {
|
||||
if (strcasecmp($option, $requestedSemester) === 0) {
|
||||
return $option;
|
||||
}
|
||||
}
|
||||
return $requestedSemester;
|
||||
}
|
||||
if ($fallbackSemester !== null && $fallbackSemester !== '') {
|
||||
foreach ($semesterOptions as $option) {
|
||||
if (strcasecmp($option, $fallbackSemester) === 0) {
|
||||
return $option;
|
||||
}
|
||||
}
|
||||
return $fallbackSemester;
|
||||
}
|
||||
return $semesterOptions[0] ?? '';
|
||||
}
|
||||
|
||||
private function getSemestersForSchoolYear(string $schoolYear, ?string $fallbackSemester = null): array
|
||||
{
|
||||
$rows = DB::table('semester_scores')
|
||||
->select(DB::raw('DISTINCT semester'))
|
||||
->whereNotNull('semester')
|
||||
->where('semester', '!=', '')
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('semester', 'ASC')
|
||||
->get();
|
||||
|
||||
$semesters = [];
|
||||
foreach ($rows as $row) {
|
||||
$value = trim((string) $row->semester);
|
||||
if ($value !== '') {
|
||||
$semesters[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($semesters)) {
|
||||
$semesters = ['Fall', 'Spring'];
|
||||
}
|
||||
|
||||
if ($fallbackSemester !== null && $fallbackSemester !== '' && !in_array($fallbackSemester, $semesters, true)) {
|
||||
array_unshift($semesters, $fallbackSemester);
|
||||
}
|
||||
|
||||
return array_values(array_unique($semesters));
|
||||
}
|
||||
|
||||
private function getParentReleaseKey(string $semester): ?string
|
||||
{
|
||||
$norm = strtolower(trim($semester));
|
||||
if ($norm === 'fall') {
|
||||
return 'parent_scores_released_fall';
|
||||
}
|
||||
if ($norm === 'spring') {
|
||||
return 'parent_scores_released_spring';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getParentScoresReleasedForSemester(string $semester): bool
|
||||
{
|
||||
$key = $this->getParentReleaseKey($semester);
|
||||
$raw = $key ? Configuration::getConfig($key) : null;
|
||||
$raw = (string) ($raw ?? '');
|
||||
return in_array(strtolower(trim($raw)), ['1', 'true', 'yes', 'y', 'on'], true);
|
||||
}
|
||||
|
||||
private function ensureParentReleaseKeyExists(string $semester): void
|
||||
{
|
||||
$key = $this->getParentReleaseKey($semester);
|
||||
if (!$key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Configuration::getConfigValueByKey($key) === null) {
|
||||
Configuration::setConfigValueByKey($key, '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\PlacementBatch;
|
||||
use App\Models\PlacementLevel;
|
||||
use App\Models\PlacementScore;
|
||||
use App\Models\Student;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class GradingPlacementService
|
||||
{
|
||||
public function placementContext(?int $classSectionId, string $schoolYear, ?string $placementTest, ?string $open): array
|
||||
{
|
||||
if (!$classSectionId) {
|
||||
$showStudents = ($placementTest !== '' && $open === '1');
|
||||
$students = $showStudents ? $this->fetchActiveStudentsWithSection($schoolYear) : [];
|
||||
$batches = $this->fetchPlacementBatches($schoolYear);
|
||||
$batchDetails = $this->fetchPlacementBatchDetails($batches, $schoolYear);
|
||||
|
||||
return [
|
||||
'school_year' => $schoolYear,
|
||||
'students' => $students,
|
||||
'batches' => $batches,
|
||||
'batch_details' => $batchDetails,
|
||||
'placement_test' => $placementTest,
|
||||
'show_students' => $showStudents,
|
||||
];
|
||||
}
|
||||
|
||||
$sectionName = ClassSection::query()->where('class_section_id', $classSectionId)->value('class_section_name');
|
||||
$classId = ClassSection::query()->where('class_section_id', $classSectionId)->value('class_id');
|
||||
|
||||
$students = Student::getStudentInfoByClassSectionId($classSectionId, null, $schoolYear);
|
||||
$studentIds = array_values(array_filter(array_map(
|
||||
static fn ($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn ($id) => $id > 0));
|
||||
|
||||
$levels = [];
|
||||
if (!empty($studentIds)) {
|
||||
$rows = PlacementLevel::query()
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
foreach ($rows as $row) {
|
||||
$levels[(int) $row->student_id] = $row->level ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class_section_id' => $classSectionId,
|
||||
'class_section_name' => $sectionName ?? '',
|
||||
'class_id' => $classId,
|
||||
'school_year' => $schoolYear,
|
||||
'students' => $students,
|
||||
'levels' => $levels,
|
||||
];
|
||||
}
|
||||
|
||||
public function updatePlacementLevel(int $studentId, string $schoolYear, $level, ?int $userId): void
|
||||
{
|
||||
if ($studentId <= 0 || $schoolYear === '') {
|
||||
throw new RuntimeException('Missing student or school year.');
|
||||
}
|
||||
|
||||
$level = $level === '' || $level === null ? null : (int) $level;
|
||||
if ($level !== null && !in_array($level, [1, 2, 3], true)) {
|
||||
throw new RuntimeException('Invalid placement level.');
|
||||
}
|
||||
|
||||
$existing = PlacementLevel::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if ($level === null) {
|
||||
if ($existing) {
|
||||
$existing->delete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'level' => $level,
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$existing->update($payload);
|
||||
} else {
|
||||
$payload['created_by'] = $userId;
|
||||
PlacementLevel::query()->create($payload);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatePlacementLevels(int $classSectionId, string $schoolYear, array $levels, ?int $userId): void
|
||||
{
|
||||
if ($classSectionId <= 0 || $schoolYear === '') {
|
||||
throw new RuntimeException('Missing class section or school year.');
|
||||
}
|
||||
|
||||
$students = Student::getStudentInfoByClassSectionId($classSectionId, null, $schoolYear);
|
||||
$validIds = array_values(array_filter(array_map(
|
||||
static fn ($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn ($id) => $id > 0));
|
||||
|
||||
$validSet = array_flip($validIds);
|
||||
$existingRows = [];
|
||||
if (!empty($validIds)) {
|
||||
$rows = PlacementLevel::query()
|
||||
->whereIn('student_id', $validIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
foreach ($rows as $row) {
|
||||
$existingRows[(int) $row->student_id] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($levels as $studentIdRaw => $levelRaw) {
|
||||
$studentId = (int) $studentIdRaw;
|
||||
if (!isset($validSet[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$levelRaw = trim((string) $levelRaw);
|
||||
$level = $levelRaw === '' ? null : (int) $levelRaw;
|
||||
if ($level !== null && !in_array($level, [1, 2, 3], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($level === null) {
|
||||
if (isset($existingRows[$studentId])) {
|
||||
$existingRows[$studentId]->delete();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'level' => $level,
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
|
||||
if (isset($existingRows[$studentId])) {
|
||||
$existingRows[$studentId]->update($payload);
|
||||
} else {
|
||||
$payload['created_by'] = $userId;
|
||||
PlacementLevel::query()->create($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function createPlacementBatch(string $schoolYear, string $placementTest, array $levels, ?int $userId): int
|
||||
{
|
||||
if ($schoolYear === '' || $placementTest === '') {
|
||||
throw new RuntimeException('Missing placement test or school year.');
|
||||
}
|
||||
|
||||
$students = $this->fetchActiveStudentsWithSection($schoolYear);
|
||||
$validIds = array_values(array_filter(array_map(
|
||||
static fn ($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn ($id) => $id > 0));
|
||||
$validSet = array_flip($validIds);
|
||||
|
||||
$batch = PlacementBatch::query()->create([
|
||||
'placement_test' => $placementTest,
|
||||
'school_year' => $schoolYear,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$savedCount = 0;
|
||||
foreach ($levels as $studentIdRaw => $levelRaw) {
|
||||
$studentId = (int) $studentIdRaw;
|
||||
if (!isset($validSet[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$levelRaw = trim((string) $levelRaw);
|
||||
if ($levelRaw === '') {
|
||||
continue;
|
||||
}
|
||||
$score = (int) $levelRaw;
|
||||
if ($score < 0 || $score > 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlacementScore::query()->create([
|
||||
'batch_id' => (int) $batch->id,
|
||||
'student_id' => $studentId,
|
||||
'score' => $score,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
if ($savedCount === 0) {
|
||||
$batch->delete();
|
||||
throw new RuntimeException('No scores entered. Batch not saved.');
|
||||
}
|
||||
|
||||
return (int) $batch->id;
|
||||
}
|
||||
|
||||
public function getPlacementBatch(int $batchId): array
|
||||
{
|
||||
$batch = PlacementBatch::query()->find($batchId);
|
||||
if (!$batch) {
|
||||
throw new RuntimeException('Placement batch not found.');
|
||||
}
|
||||
|
||||
$schoolYear = (string) ($batch->school_year ?? '');
|
||||
$students = $this->fetchActiveStudentsWithSection($schoolYear);
|
||||
$scores = $this->fetchPlacementScoresForBatch($batchId);
|
||||
|
||||
return [
|
||||
'batch' => $batch,
|
||||
'students' => $students,
|
||||
'scores' => $scores,
|
||||
];
|
||||
}
|
||||
|
||||
public function updatePlacementBatch(int $batchId, string $schoolYear, array $levels, ?int $userId): void
|
||||
{
|
||||
$batch = PlacementBatch::query()->find($batchId);
|
||||
if (!$batch) {
|
||||
throw new RuntimeException('Placement batch not found.');
|
||||
}
|
||||
|
||||
$students = $this->fetchActiveStudentsWithSection($schoolYear);
|
||||
$validIds = array_values(array_filter(array_map(
|
||||
static fn ($row) => (int) ($row['student_id'] ?? 0),
|
||||
$students
|
||||
), static fn ($id) => $id > 0));
|
||||
|
||||
$validSet = array_flip($validIds);
|
||||
$existing = $this->fetchPlacementScoresForBatch($batchId);
|
||||
|
||||
foreach ($levels as $studentIdRaw => $scoreRaw) {
|
||||
$studentId = (int) $studentIdRaw;
|
||||
if (!isset($validSet[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$scoreRaw = trim((string) $scoreRaw);
|
||||
if ($scoreRaw === '') {
|
||||
if (isset($existing[$studentId])) {
|
||||
PlacementScore::query()->whereKey($existing[$studentId]['id'])->delete();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$score = (int) $scoreRaw;
|
||||
if ($score < 0 || $score > 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($existing[$studentId])) {
|
||||
PlacementScore::query()->whereKey($existing[$studentId]['id'])->update([
|
||||
'score' => $score,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
} else {
|
||||
PlacementScore::query()->create([
|
||||
'batch_id' => $batchId,
|
||||
'student_id' => $studentId,
|
||||
'score' => $score,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$batch->update([
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function fetchActiveStudentsWithSection(string $schoolYear): array
|
||||
{
|
||||
return DB::table('students as s')
|
||||
->select('s.id as student_id', 's.school_id', 's.firstname', 's.lastname', 'sc.class_section_id', 'cs.class_section_name', 'c.class_name')
|
||||
->leftJoin('student_class as sc', function ($join) use ($schoolYear) {
|
||||
$join->on('sc.student_id', '=', 's.id')
|
||||
->where('sc.school_year', '=', $schoolYear);
|
||||
})
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
|
||||
->where('s.is_active', 1)
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
private function fetchPlacementBatches(string $schoolYear): array
|
||||
{
|
||||
return PlacementBatch::query()
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
private function fetchPlacementBatchDetails(array $batches, string $schoolYear): array
|
||||
{
|
||||
if (empty($batches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$batchIds = array_values(array_filter(array_map(
|
||||
static fn ($row) => (int) ($row['id'] ?? 0),
|
||||
$batches
|
||||
), static fn ($id) => $id > 0));
|
||||
|
||||
if (empty($batchIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = DB::table('placement_scores as ps')
|
||||
->select('ps.batch_id', 'ps.score', 's.school_id', 's.firstname', 's.lastname', 'cs.class_section_name', 'c.class_name')
|
||||
->join('students as s', 's.id', '=', 'ps.student_id')
|
||||
->leftJoin('student_class as sc', function ($join) use ($schoolYear) {
|
||||
$join->on('sc.student_id', '=', 's.id')
|
||||
->where('sc.school_year', '=', $schoolYear);
|
||||
})
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
|
||||
->whereIn('ps.batch_id', $batchIds)
|
||||
->where('s.is_active', 1)
|
||||
->orderBy('ps.batch_id', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$details = [];
|
||||
foreach ($rows as $row) {
|
||||
$bid = (int) ($row['batch_id'] ?? 0);
|
||||
if ($bid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$details[$bid][] = $row;
|
||||
}
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
private function fetchPlacementScoresForBatch(int $batchId): array
|
||||
{
|
||||
$rows = PlacementScore::query()
|
||||
->where('batch_id', $batchId)
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$scores = [];
|
||||
foreach ($rows as $row) {
|
||||
$scores[(int) $row['student_id']] = $row;
|
||||
}
|
||||
|
||||
return $scores;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\SemesterScore;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use RuntimeException;
|
||||
|
||||
class GradingRefreshService
|
||||
{
|
||||
public function __construct(private SemesterScoreService $semesterScoreService)
|
||||
{
|
||||
}
|
||||
|
||||
public function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
|
||||
{
|
||||
if ($classSectionId <= 0) {
|
||||
throw new RuntimeException('Missing class section.');
|
||||
}
|
||||
|
||||
$studentTeacherInfo = Student::getStudentInfoByClassSectionId(
|
||||
$classSectionId,
|
||||
$semester,
|
||||
$schoolYear
|
||||
);
|
||||
if (empty($studentTeacherInfo)) {
|
||||
throw new RuntimeException('No students found for this class/term.');
|
||||
}
|
||||
|
||||
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
||||
$this->refreshAttendanceComments($classSectionId, $semester, $schoolYear);
|
||||
}
|
||||
|
||||
private function refreshAttendanceComments(int $classSectionId, string $semester, string $schoolYear): void
|
||||
{
|
||||
if (!function_exists('attendance_comment_from_score')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scoreRows = SemesterScore::query()
|
||||
->select(['student_id', 'attendance_score'])
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
if ($scoreRows->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$studentIds = $scoreRows->pluck('student_id')->map(fn ($v) => (int) $v)->unique()->values()->all();
|
||||
if (empty($studentIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$students = Student::query()
|
||||
->select(['id', 'firstname'])
|
||||
->whereIn('id', $studentIds)
|
||||
->get();
|
||||
$nameMap = [];
|
||||
foreach ($students as $student) {
|
||||
$nameMap[(int) $student->id] = (string) ($student->firstname ?? '');
|
||||
}
|
||||
|
||||
$existing = ScoreComment::query()
|
||||
->where('score_type', 'attendance')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->get();
|
||||
$existingByStudent = [];
|
||||
foreach ($existing as $row) {
|
||||
$existingByStudent[(int) $row->student_id] = $row;
|
||||
}
|
||||
|
||||
foreach ($scoreRows as $row) {
|
||||
$studentId = (int) $row->student_id;
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$score = $row->attendance_score !== null ? (float) $row->attendance_score : null;
|
||||
if ($score === null) {
|
||||
continue;
|
||||
}
|
||||
$auto = attendance_comment_from_score($score, $nameMap[$studentId] ?? '');
|
||||
if ($auto === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($existingByStudent[$studentId])) {
|
||||
$existingByStudent[$studentId]->update([
|
||||
'comment' => $auto,
|
||||
]);
|
||||
} else {
|
||||
ScoreComment::query()->create([
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'score_type' => 'attendance',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'comment' => $auto,
|
||||
'commented_by' => null,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\FinalExam;
|
||||
use App\Models\GradingLock;
|
||||
use App\Models\Homework;
|
||||
use App\Models\MidtermExam;
|
||||
use App\Models\Project;
|
||||
use App\Models\Quiz;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\SemesterScore;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use RuntimeException;
|
||||
|
||||
class GradingScoreService
|
||||
{
|
||||
public function __construct(private SemesterScoreService $semesterScoreService)
|
||||
{
|
||||
}
|
||||
|
||||
public function show(string $type, int $classSectionId, int $studentId, string $semester, string $schoolYear): array
|
||||
{
|
||||
$model = $this->resolveModel($type);
|
||||
$student = Student::query()->find($studentId);
|
||||
|
||||
if (!$student) {
|
||||
throw new RuntimeException('Student not found.');
|
||||
}
|
||||
|
||||
$scores = $model->newQuery()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
$scoresLocked = $classSectionId > 0
|
||||
? GradingLock::isLocked($classSectionId, $semester, $schoolYear)
|
||||
: false;
|
||||
|
||||
return [
|
||||
'student' => $student,
|
||||
'scores' => $scores,
|
||||
'type' => $type,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'scores_locked' => $scoresLocked,
|
||||
];
|
||||
}
|
||||
|
||||
public function update(array $payload, int $updatedBy): void
|
||||
{
|
||||
$type = (string) ($payload['type'] ?? '');
|
||||
$studentId = (int) ($payload['student_id'] ?? 0);
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
$semester = (string) ($payload['semester'] ?? '');
|
||||
$schoolYear = (string) ($payload['school_year'] ?? '');
|
||||
|
||||
if ($classSectionId > 0 && GradingLock::isLocked($classSectionId, $semester, $schoolYear)) {
|
||||
throw new RuntimeException('Scores are locked for this class. Unlock to edit.');
|
||||
}
|
||||
|
||||
$model = $this->resolveModel($type);
|
||||
|
||||
if (in_array($type, ['homework', 'quiz', 'project'], true)) {
|
||||
$scoreIds = $payload['score_ids'] ?? [];
|
||||
$scores = $payload['scores'] ?? [];
|
||||
$comments = $payload['comments'] ?? [];
|
||||
|
||||
foreach ($scoreIds as $i => $id) {
|
||||
$model->newQuery()->whereKey($id)->update([
|
||||
'score' => $scores[$i] ?? null,
|
||||
'comment' => $comments[$i] ?? null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
} elseif (in_array($type, ['midterm', 'final', 'test'], true)) {
|
||||
$score = $payload['score'] ?? null;
|
||||
|
||||
$data = [
|
||||
'score' => $score,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
$existing = $model->newQuery()
|
||||
->where('student_id', $studentId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->update($data);
|
||||
} else {
|
||||
$model->newQuery()->create($data + [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
} elseif ($type === 'comments') {
|
||||
$comment = (string) ($payload['comment'] ?? '');
|
||||
|
||||
$model->newQuery()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->delete();
|
||||
|
||||
$model->newQuery()->create([
|
||||
'student_id' => $studentId,
|
||||
'score_type' => 'general',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'comment' => $comment,
|
||||
'commented_by' => $updatedBy ?: null,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$studentTeacherInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, $updatedBy);
|
||||
if (!empty($studentTeacherInfo)) {
|
||||
try {
|
||||
$this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear);
|
||||
} catch (RuntimeException $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getScoreComments(string $semester, string $schoolYear): array
|
||||
{
|
||||
$studentIds = Student::query()
|
||||
->select('students.id')
|
||||
->join('student_class', 'student_class.student_id', '=', 'students.id')
|
||||
->where('student_class.semester', $semester)
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->pluck('students.id')
|
||||
->map(fn ($v) => (int) $v)
|
||||
->all();
|
||||
|
||||
if (empty($studentIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$scores = [];
|
||||
$students = Student::query()
|
||||
->whereIn('id', $studentIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
foreach ($students as $student) {
|
||||
$scores[$student->id] = [
|
||||
'school_id' => $student->school_id,
|
||||
'firstname' => $student->firstname,
|
||||
'lastname' => $student->lastname,
|
||||
'comments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$commentRows = ScoreComment::query()
|
||||
->whereIn('student_id', $studentIds)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
foreach ($commentRows as $comment) {
|
||||
$scores[$comment->student_id]['comments'][] = $comment;
|
||||
}
|
||||
|
||||
return $scores;
|
||||
}
|
||||
|
||||
private function resolveModel(string $type)
|
||||
{
|
||||
return match ($type) {
|
||||
'homework' => new Homework(),
|
||||
'quiz' => new Quiz(),
|
||||
'project' => new Project(),
|
||||
'midterm' => new MidtermExam(),
|
||||
'final' => new FinalExam(),
|
||||
'test' => new SemesterScore(),
|
||||
'comments' => new ScoreComment(),
|
||||
default => throw new RuntimeException("Invalid type: {$type}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\CalendarEvent;
|
||||
use DateTime;
|
||||
|
||||
class HomeworkTrackingCalendarService
|
||||
{
|
||||
public function buildDateRange(string $schoolYear, string $semester): array
|
||||
{
|
||||
$startYear = null;
|
||||
if (preg_match('/\b(20\d{2})\b/', $schoolYear, $m)) {
|
||||
$startYear = (int) $m[1];
|
||||
}
|
||||
if ($startYear === null) {
|
||||
$today = new DateTime('today');
|
||||
return [$today, $today];
|
||||
}
|
||||
|
||||
$nextYear = $startYear + 1;
|
||||
$sem = strtolower(trim($semester));
|
||||
|
||||
try {
|
||||
if ($sem === 'spring') {
|
||||
$start = new DateTime("{$nextYear}-01-25");
|
||||
$end = new DateTime("{$nextYear}-05-31");
|
||||
} else {
|
||||
$start = new DateTime("{$startYear}-09-21");
|
||||
$end = new DateTime("{$nextYear}-01-18");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$today = new DateTime('today');
|
||||
return [$today, $today];
|
||||
}
|
||||
|
||||
return [$start, $end];
|
||||
}
|
||||
|
||||
public function buildSundays(DateTime $start, DateTime $end): array
|
||||
{
|
||||
if ((int) $start->format('w') !== 0) {
|
||||
$start = (clone $start)->modify('next sunday');
|
||||
}
|
||||
|
||||
$sundays = [];
|
||||
$d = clone $start;
|
||||
while ($d <= $end) {
|
||||
$sundays[] = $d->format('Y-m-d');
|
||||
$d->modify('+7 days');
|
||||
}
|
||||
|
||||
return $sundays;
|
||||
}
|
||||
|
||||
public function eventDays(string $schoolYear): array
|
||||
{
|
||||
$events = CalendarEvent::getEventsBySchoolYear($schoolYear) ?? [];
|
||||
$eventDays = [];
|
||||
foreach ($events as $event) {
|
||||
$ymd = substr((string) ($event['date'] ?? ''), 0, 10);
|
||||
$no = (int) ($event['no_school'] ?? 0);
|
||||
if ($ymd && $no === 1) {
|
||||
$eventDays[$ymd] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $eventDays;
|
||||
}
|
||||
|
||||
public function dateToIndex(array $sundays, array $eventDays): array
|
||||
{
|
||||
$dateToIndex = [];
|
||||
$idx = 0;
|
||||
foreach ($sundays as $ymd) {
|
||||
if (!isset($eventDays[$ymd])) {
|
||||
$idx++;
|
||||
$dateToIndex[$ymd] = $idx;
|
||||
} else {
|
||||
$dateToIndex[$ymd] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $dateToIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class HomeworkTrackingService
|
||||
{
|
||||
private array $teacherAssignmentCache = [];
|
||||
|
||||
public function __construct(private HomeworkTrackingCalendarService $calendarService)
|
||||
{
|
||||
}
|
||||
|
||||
public function report(?string $semester = null, ?string $schoolYear = null, int $page = 1): array
|
||||
{
|
||||
$semester = $semester ?: (string) (Configuration::getConfig('semester') ?? '');
|
||||
$schoolYear = $schoolYear ?: (string) (Configuration::getConfig('school_year') ?? '');
|
||||
|
||||
[$start, $end] = $this->calendarService->buildDateRange($schoolYear, $semester);
|
||||
$sundays = $this->calendarService->buildSundays($start, $end);
|
||||
$eventDays = $this->calendarService->eventDays($schoolYear);
|
||||
$dateToIndex = $this->calendarService->dateToIndex($sundays, $eventDays);
|
||||
|
||||
$limitToSemester = $this->hasTeacherAssignments($schoolYear);
|
||||
|
||||
$homeworkByIndex = $this->loadHomeworkByIndex($schoolYear, $semester, $limitToSemester);
|
||||
$homeworkByDate = $this->loadHomeworkByDate($schoolYear, $semester, $limitToSemester, $sundays, $eventDays);
|
||||
$teachers = $this->loadTeachers($schoolYear);
|
||||
|
||||
$perPage = 8;
|
||||
$totalRows = count($teachers);
|
||||
$totalPages = max(1, (int) ceil($totalRows / $perPage));
|
||||
$page = max(1, min($page, $totalPages));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$teachersPage = array_slice($teachers, $offset, $perPage);
|
||||
|
||||
return [
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'sundays' => $sundays,
|
||||
'event_days' => $eventDays,
|
||||
'date_to_index' => $dateToIndex,
|
||||
'teachers' => $teachersPage,
|
||||
'has_homework' => $homeworkByIndex['has_homework'],
|
||||
'hw_entered_at' => $homeworkByIndex['entered_at'],
|
||||
'has_homework_by_date' => $homeworkByDate['has_homework'],
|
||||
'hw_entered_at_by_date' => $homeworkByDate['entered_at'],
|
||||
'page' => $page,
|
||||
'total_pages' => $totalPages,
|
||||
'per_page' => $perPage,
|
||||
'total_rows' => $totalRows,
|
||||
];
|
||||
}
|
||||
|
||||
private function hasTeacherAssignments(string $schoolYear): bool
|
||||
{
|
||||
$year = trim((string) $schoolYear);
|
||||
if ($year === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array_key_exists($year, $this->teacherAssignmentCache)) {
|
||||
return $this->teacherAssignmentCache[$year];
|
||||
}
|
||||
|
||||
$cnt = DB::table('teacher_class')->where('school_year', $year)->count();
|
||||
$this->teacherAssignmentCache[$year] = $cnt > 0;
|
||||
|
||||
return $this->teacherAssignmentCache[$year];
|
||||
}
|
||||
|
||||
private function loadHomeworkByIndex(string $schoolYear, string $semester, bool $limitToSemester): array
|
||||
{
|
||||
$query = DB::table('homework')
|
||||
->select('class_section_id', 'homework_index', DB::raw('MIN(created_at) as first_created'), DB::raw('COUNT(*) as cnt'))
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($limitToSemester && $semester !== '') {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
|
||||
$rows = $query->groupBy('class_section_id', 'homework_index')->get();
|
||||
|
||||
$hasHomework = [];
|
||||
$enteredAt = [];
|
||||
foreach ($rows as $row) {
|
||||
$csid = (int) $row->class_section_id;
|
||||
$hi = (int) $row->homework_index;
|
||||
$cnt = (int) $row->cnt;
|
||||
if ($csid > 0 && $hi > 0 && $cnt > 0) {
|
||||
$hasHomework[$csid][$hi] = true;
|
||||
$dateStr = substr((string) ($row->first_created ?? ''), 0, 10);
|
||||
$enteredAt[$csid][$hi] = $dateStr ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
return ['has_homework' => $hasHomework, 'entered_at' => $enteredAt];
|
||||
}
|
||||
|
||||
private function loadHomeworkByDate(string $schoolYear, string $semester, bool $limitToSemester, array $sundays, array $eventDays): array
|
||||
{
|
||||
$query = DB::table('homework')
|
||||
->select(DB::raw('class_section_id, DATE(created_at) as hw_date, MIN(created_at) as first_created, COUNT(*) as cnt'))
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($limitToSemester && $semester !== '') {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
|
||||
$rows = $query
|
||||
->groupBy('class_section_id', DB::raw('DATE(created_at)'))
|
||||
->orderBy('hw_date', 'ASC')
|
||||
->get();
|
||||
|
||||
$hasHomework = [];
|
||||
$enteredAt = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$csid = (int) $row->class_section_id;
|
||||
$date = substr((string) ($row->hw_date ?? ''), 0, 10);
|
||||
$cnt = (int) $row->cnt;
|
||||
$firstCreated = substr((string) ($row->first_created ?? ''), 0, 10);
|
||||
if ($csid <= 0 || !$date || $cnt <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$baseIndex = -1;
|
||||
for ($i = count($sundays) - 1; $i >= 0; $i--) {
|
||||
if ($sundays[$i] <= $date) {
|
||||
$baseIndex = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($baseIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$j = $baseIndex;
|
||||
while ($j >= 0 && !empty($eventDays[$sundays[$j]])) {
|
||||
$j--;
|
||||
}
|
||||
if ($j < 0) {
|
||||
continue;
|
||||
}
|
||||
$sunday = $sundays[$j];
|
||||
|
||||
$hasHomework[$csid][$sunday] = true;
|
||||
$candidate = $firstCreated ?: $date;
|
||||
if (empty($enteredAt[$csid][$sunday]) || ($candidate && $candidate < $enteredAt[$csid][$sunday])) {
|
||||
$enteredAt[$csid][$sunday] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return ['has_homework' => $hasHomework, 'entered_at' => $enteredAt];
|
||||
}
|
||||
|
||||
private function loadTeachers(string $schoolYear): array
|
||||
{
|
||||
$rows = DB::table('teacher_class as tc')
|
||||
->select('tc.class_section_id', 'tc.position', 'cs.class_section_name', 'cs.class_id', 'u.firstname', 'u.lastname')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'tc.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'tc.teacher_id')
|
||||
->where('tc.school_year', $schoolYear)
|
||||
->whereNotNull('tc.class_section_id')
|
||||
->whereIn('tc.position', ['main', 'ta'])
|
||||
->orderBy('cs.class_id', 'ASC')
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderByRaw("FIELD(tc.position, 'main','ta')")
|
||||
->orderBy('u.firstname', 'ASC')
|
||||
->get();
|
||||
|
||||
$bySection = [];
|
||||
foreach ($rows as $row) {
|
||||
$sid = (int) $row->class_section_id;
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($bySection[$sid])) {
|
||||
$bySection[$sid] = [
|
||||
'class_section_id' => $sid,
|
||||
'class_id' => (int) ($row->class_id ?? 0),
|
||||
'class_section_name' => (string) ($row->class_section_name ?? ''),
|
||||
'teachers' => [],
|
||||
'tas' => [],
|
||||
];
|
||||
}
|
||||
$name = trim((string) ($row->firstname ?? '') . ' ' . (string) ($row->lastname ?? ''));
|
||||
$pos = strtolower((string) ($row->position ?? ''));
|
||||
if ($name !== '') {
|
||||
if ($pos === 'main') {
|
||||
$bySection[$sid]['teachers'][] = $name;
|
||||
} elseif ($pos === 'ta') {
|
||||
$bySection[$sid]['tas'][] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$teachers = array_values($bySection);
|
||||
usort($teachers, function ($a, $b) {
|
||||
$ai = (int) ($a['class_id'] ?? 0);
|
||||
$bi = (int) ($b['class_id'] ?? 0);
|
||||
$order = function (int $cid): int {
|
||||
if ($cid === 13) return 0;
|
||||
if ($cid >= 1 && $cid <= 11) return $cid;
|
||||
if ($cid === 12) return 99;
|
||||
return 200 + $cid;
|
||||
};
|
||||
$cmp = $order($ai) <=> $order($bi);
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
return strnatcasecmp($a['class_section_name'] ?? '', $b['class_section_name'] ?? '');
|
||||
});
|
||||
|
||||
return $teachers;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user