903 lines
35 KiB
PHP
903 lines
35 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Attendance;
|
|
|
|
use App\Models\AttendanceData;
|
|
use App\Models\AttendanceDay;
|
|
use App\Models\AttendanceRecord;
|
|
use App\Models\Calendar;
|
|
use App\Models\ClassSection;
|
|
use App\Models\Configuration;
|
|
use App\Models\Student;
|
|
use App\Models\StudentClass;
|
|
use App\Models\TeacherClass;
|
|
use App\Models\User;
|
|
use App\Models\UserRole;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use RuntimeException;
|
|
|
|
class AttendanceQueryService
|
|
{
|
|
public function __construct(
|
|
protected Configuration $configuration,
|
|
protected AttendanceData $attendanceData,
|
|
protected AttendanceDay $attendanceDay,
|
|
protected AttendanceRecord $attendanceRecord,
|
|
protected Student $student,
|
|
protected StudentClass $studentClass,
|
|
protected ClassSection $classSection,
|
|
protected TeacherClass $teacherClass,
|
|
protected Calendar $calendar,
|
|
protected User $user,
|
|
protected UserRole $userRole,
|
|
protected AttendanceService $attendanceService,
|
|
protected SemesterRangeService $semesterRangeService,
|
|
) {}
|
|
|
|
public function teacherGrid(string $semester, string $schoolYear, string $date, int $sectionCode): array
|
|
{
|
|
$bySection = $this->teacherClass->assignedBySectionForTerm($semester, $schoolYear);
|
|
$sectionCodes = array_keys($bySection);
|
|
|
|
$labels = [];
|
|
if (!empty($sectionCodes)) {
|
|
$rows = $this->classSection
|
|
->query()
|
|
->select(['class_section_id', 'class_section_name'])
|
|
->whereIn('class_section_id', $sectionCodes)
|
|
->orderBy('class_section_id')
|
|
->get()
|
|
->toArray();
|
|
|
|
foreach ($rows as $row) {
|
|
$code = (int)$row['class_section_id'];
|
|
$name = trim((string)($row['class_section_name'] ?? ''));
|
|
$labels[$code] = $name !== '' ? $name : 'Section #' . $code;
|
|
}
|
|
|
|
foreach ($sectionCodes as $code) {
|
|
$labels[$code] ??= 'Section #' . $code;
|
|
}
|
|
}
|
|
|
|
$grid = [];
|
|
$locked = false;
|
|
|
|
if ($sectionCode > 0) {
|
|
$assigned = $this->teacherClass->assignedForSectionTerm($sectionCode, $semester, $schoolYear);
|
|
$teacherIds = array_map(fn($r) => (int)$r['teacher_id'], $assigned);
|
|
|
|
$statusMap = [];
|
|
if (!empty($teacherIds)) {
|
|
$rows = DB::table('staff_attendance as sa')
|
|
->select('sa.user_id', 'sa.status', 'sa.reason')
|
|
->where('sa.semester', $semester)
|
|
->where('sa.school_year', $schoolYear)
|
|
->whereDate('sa.date', $date)
|
|
->whereIn('sa.user_id', $teacherIds)
|
|
->get();
|
|
|
|
foreach ($rows as $row) {
|
|
$statusMap[(int)$row->user_id] = [
|
|
'status' => $row->status,
|
|
'reason' => $row->reason,
|
|
];
|
|
}
|
|
}
|
|
|
|
foreach ($assigned as $teacher) {
|
|
$teacherId = (int)$teacher['teacher_id'];
|
|
$position = strtolower((string)($teacher['position'] ?? 'main'));
|
|
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
|
|
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
|
$cell = $statusMap[$teacherId] ?? ['status' => null, 'reason' => null];
|
|
|
|
$grid[] = [
|
|
'teacher_id' => $teacherId,
|
|
'name' => $name !== '' ? $name : 'User#' . $teacherId,
|
|
'position' => $position,
|
|
'status' => $cell['status'],
|
|
'reason' => $cell['reason'],
|
|
];
|
|
}
|
|
|
|
try {
|
|
$locked = method_exists($this->attendanceDay, 'isFinalized')
|
|
? $this->attendanceDay->isFinalized($sectionCode, $date, $semester, $schoolYear)
|
|
: false;
|
|
} catch (\Throwable) {
|
|
$locked = false;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'semester' => $semester,
|
|
'school_year' => $schoolYear,
|
|
'date' => $date,
|
|
'class_section_id' => $sectionCode,
|
|
'sections' => $labels,
|
|
'grid' => $grid,
|
|
'locked' => $locked,
|
|
];
|
|
}
|
|
|
|
public function teacherAttendanceFormData(?int $userId, int $requestedSectionId = 0): array
|
|
{
|
|
if (!$userId) {
|
|
throw new RuntimeException('User not logged in.');
|
|
}
|
|
|
|
$teacher = $this->user->find($userId);
|
|
$teacherName = $teacher
|
|
? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? ''))
|
|
: 'Unknown Teacher';
|
|
|
|
$semester = $this->attendanceService->currentSemester();
|
|
$schoolYear = $this->attendanceService->currentSchoolYear();
|
|
|
|
$assignments = $this->teacherClass->getClassAssignmentsByUserId($userId, $schoolYear, $semester);
|
|
|
|
if (empty($assignments)) {
|
|
throw new RuntimeException('You do not have an assigned class yet.');
|
|
}
|
|
|
|
$activeSection = null;
|
|
foreach ($assignments as $assignment) {
|
|
$cid = (int)($assignment['class_section_id'] ?? 0);
|
|
$pk = (int)($assignment['class_section_pk'] ?? 0);
|
|
|
|
if ($requestedSectionId > 0 && ($requestedSectionId === $cid || $requestedSectionId === $pk)) {
|
|
$activeSection = $assignment;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$activeSection ??= $assignments[0];
|
|
|
|
$classSectionCode = (int)($activeSection['class_section_id'] ?? 0);
|
|
$classSectionPk = (int)($activeSection['class_section_pk'] ?? 0);
|
|
$classSectionId = $classSectionCode > 0 ? $classSectionCode : $classSectionPk;
|
|
|
|
if ($classSectionId <= 0) {
|
|
throw new RuntimeException('Unable to determine your class section.');
|
|
}
|
|
|
|
$today = Carbon::today();
|
|
$anchorSunday = $today->dayOfWeek === Carbon::SUNDAY ? $today->copy() : $today->copy()->next(Carbon::SUNDAY);
|
|
|
|
$sundayDates = [
|
|
$anchorSunday->copy()->subWeeks(2)->toDateString(),
|
|
$anchorSunday->copy()->subWeek()->toDateString(),
|
|
$anchorSunday->toDateString(),
|
|
];
|
|
$currentSunday = $sundayDates[2];
|
|
|
|
$students = $this->student->getByClassAndYear($classSectionId, $schoolYear, $semester);
|
|
if (empty($students) && $classSectionPk > 0 && $classSectionPk !== $classSectionId) {
|
|
$students = $this->student->getByClassAndYear($classSectionPk, $schoolYear, $semester);
|
|
}
|
|
|
|
$attendanceRows = [];
|
|
$lockedByStudent = [];
|
|
|
|
$attendanceTable = $this->attendanceData->getTable();
|
|
$dateInSundaysSql = 'DATE(`' . $attendanceTable . '`.`date`) IN (' .
|
|
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
|
|
|
foreach ($students as $student) {
|
|
$studentId = (int)$student['id'];
|
|
|
|
$rows = AttendanceData::query()
|
|
->select(['date', 'status', 'is_reported', 'reason', 'modified_by'])
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $schoolYear)
|
|
->where('semester', $semester)
|
|
->where(function ($q) use ($classSectionId, $classSectionPk) {
|
|
$q->where('class_section_id', $classSectionId);
|
|
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
|
|
$q->orWhere('class_section_id', $classSectionPk);
|
|
}
|
|
})
|
|
->whereRaw($dateInSundaysSql, $sundayDates)
|
|
->get()
|
|
->map(fn($row) => (array)$row)
|
|
->all();
|
|
|
|
$rows = $this->attendanceService->normalizeAttendanceEntries($rows);
|
|
|
|
$recentRows = AttendanceData::query()
|
|
->select(['date', 'status'])
|
|
->where('student_id', $studentId)
|
|
->where('school_year', $schoolYear)
|
|
->where('semester', $semester)
|
|
->where(function ($q) use ($classSectionId, $classSectionPk) {
|
|
$q->where('class_section_id', $classSectionId);
|
|
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
|
|
$q->orWhere('class_section_id', $classSectionPk);
|
|
}
|
|
})
|
|
->orderByDesc('date')
|
|
->limit(3)
|
|
->get()
|
|
->toArray();
|
|
|
|
$recentHistory = [];
|
|
foreach ($recentRows as $row) {
|
|
$recentHistory[] = [
|
|
'date' => substr((string)($row['date'] ?? ''), 0, 10),
|
|
'status' => strtolower((string)($row['status'] ?? '')),
|
|
];
|
|
}
|
|
|
|
$snapshot = [];
|
|
$rowsByDate = [];
|
|
foreach ($rows as $entry) {
|
|
$d = $this->calendarDateKey($entry['date'] ?? null);
|
|
if ($d === '') {
|
|
continue;
|
|
}
|
|
$snapshot[$d] = $entry['status'] ?? null;
|
|
$rowsByDate[$d] = $entry;
|
|
}
|
|
|
|
$statusHistory = [];
|
|
foreach ($sundayDates as $date) {
|
|
$statusHistory[$date] = array_key_exists($date, $snapshot)
|
|
? $snapshot[$date]
|
|
: ($date === $currentSunday ? null : 'N/A');
|
|
}
|
|
|
|
// Recent list uses the same section filters but no Sunday window; if the windowed query
|
|
// missed rows (SQL/driver quirks), still show statuses that match the visible columns.
|
|
foreach ($recentRows as $row) {
|
|
$d = $this->calendarDateKey($row['date'] ?? null);
|
|
if ($d === '' || !in_array($d, $sundayDates, true)) {
|
|
continue;
|
|
}
|
|
$rowStatus = $row['status'] ?? null;
|
|
if ($rowStatus === null || $rowStatus === '') {
|
|
continue;
|
|
}
|
|
$current = $statusHistory[$d] ?? null;
|
|
if ($current === 'N/A' || $current === null) {
|
|
$statusHistory[$d] = $rowStatus;
|
|
}
|
|
}
|
|
|
|
$todayRow = $rowsByDate[$currentSunday] ?? null;
|
|
$lockInfo = $this->attendanceService->detectExternalSubmission($todayRow);
|
|
$todayReason = $todayRow['reason'] ?? null;
|
|
|
|
$attendanceRows[] = [
|
|
'student' => $student,
|
|
'sunday_statuses' => $statusHistory,
|
|
'today' => $statusHistory[$currentSunday] ?? null,
|
|
'today_reason' => $todayReason,
|
|
'locked' => $lockInfo['locked'],
|
|
'locked_source' => $lockInfo['source'],
|
|
'recent_three' => $recentHistory,
|
|
];
|
|
|
|
$lockedByStudent[$studentId] = [
|
|
'locked' => $lockInfo['locked'],
|
|
'source' => $lockInfo['source'],
|
|
'reason' => $todayReason,
|
|
'status' => $statusHistory[$currentSunday] ?? null,
|
|
];
|
|
}
|
|
|
|
$candidates = array_values(array_unique(array_filter(
|
|
[$classSectionCode, $classSectionPk],
|
|
static fn (int $v): bool => $v > 0,
|
|
)));
|
|
$alsoSectionId = null;
|
|
foreach ($candidates as $cid) {
|
|
if ($cid !== $classSectionId) {
|
|
$alsoSectionId = $cid;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$assigned = $this->teacherClass->assignedForSectionTerm(
|
|
$classSectionId,
|
|
$semester,
|
|
$schoolYear,
|
|
$alsoSectionId,
|
|
true,
|
|
);
|
|
if ($assigned === []) {
|
|
$assigned = $this->teacherClass->assignedForSectionTerm(
|
|
$classSectionId,
|
|
$semester,
|
|
$schoolYear,
|
|
$alsoSectionId,
|
|
false,
|
|
);
|
|
}
|
|
if ($assigned === []) {
|
|
$assigned = $this->teacherRosterFromStaffAttendance(
|
|
$sundayDates,
|
|
$semester,
|
|
$schoolYear,
|
|
$classSectionId,
|
|
$classSectionPk,
|
|
);
|
|
}
|
|
if ($assigned === [] && $userId > 0) {
|
|
$self = $this->user->query()->select(['id', 'firstname', 'lastname'])->find($userId);
|
|
if ($self) {
|
|
$assigned = [[
|
|
'class_section_id' => $classSectionId,
|
|
'teacher_id' => (int) $self->id,
|
|
'position' => 'main',
|
|
'firstname' => $self->firstname,
|
|
'lastname' => $self->lastname,
|
|
]];
|
|
}
|
|
}
|
|
|
|
$teacherRows = [];
|
|
|
|
if (!empty($assigned)) {
|
|
$teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned);
|
|
|
|
$statusMap = [];
|
|
if (!empty($teacherIds)) {
|
|
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
|
|
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
|
$staffQ = DB::table('staff_attendance as sa')
|
|
->select('sa.user_id', 'sa.date', 'sa.status', 'sa.reason')
|
|
->where('sa.semester', $semester)
|
|
->where('sa.school_year', $schoolYear)
|
|
->whereIn('sa.user_id', $teacherIds)
|
|
->whereRaw($staffDateInSql, $sundayDates);
|
|
|
|
if (Schema::hasColumn('staff_attendance', 'class_section_id')) {
|
|
$staffQ->where(function ($w) use ($classSectionId, $classSectionPk) {
|
|
$w->whereNull('sa.class_section_id')
|
|
->orWhere('sa.class_section_id', 0)
|
|
->orWhere('sa.class_section_id', $classSectionId);
|
|
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
|
|
$w->orWhere('sa.class_section_id', $classSectionPk);
|
|
}
|
|
});
|
|
}
|
|
|
|
$rows = $staffQ->get();
|
|
|
|
foreach ($rows as $row) {
|
|
$dk = $this->calendarDateKey($row->date ?? null);
|
|
if ($dk === '') {
|
|
continue;
|
|
}
|
|
$statusMap[(int)$row->user_id][$dk] = [
|
|
'status' => $row->status,
|
|
'reason' => $row->reason,
|
|
];
|
|
}
|
|
}
|
|
|
|
foreach ($assigned as $teacher) {
|
|
$teacherId = (int)$teacher['teacher_id'];
|
|
$position = strtolower((string)($teacher['position'] ?? 'main'));
|
|
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
|
|
$name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
|
|
|
|
$history = [];
|
|
foreach ($sundayDates as $date) {
|
|
$history[$date] = isset($statusMap[$teacherId][$date]['status'])
|
|
? $statusMap[$teacherId][$date]['status']
|
|
: ($date === $currentSunday ? null : 'N/A');
|
|
}
|
|
|
|
$todayReason = null;
|
|
if (isset($statusMap[$teacherId][$currentSunday]) && is_array($statusMap[$teacherId][$currentSunday])) {
|
|
$todayReason = $statusMap[$teacherId][$currentSunday]['reason'] ?? null;
|
|
}
|
|
|
|
$teacherRows[] = [
|
|
'teacher_id' => $teacherId,
|
|
'position' => $position,
|
|
'name' => $name !== '' ? $name : 'User#' . $teacherId,
|
|
'sunday_statuses' => $history,
|
|
'today' => $history[$currentSunday] ?? null,
|
|
'today_reason' => $todayReason,
|
|
];
|
|
}
|
|
}
|
|
|
|
$noSchoolDays = [];
|
|
$calendarTable = $this->calendar->getTable();
|
|
$calDateInSql = 'DATE(`' . $calendarTable . '`.`date`) IN (' .
|
|
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
|
$calendarRows = $this->calendar->query()
|
|
->where('no_school', 1)
|
|
->whereRaw($calDateInSql, $sundayDates)
|
|
->where('semester', $semester)
|
|
->where('school_year', $schoolYear)
|
|
->get()
|
|
->toArray();
|
|
|
|
foreach ($calendarRows as $row) {
|
|
$d = $this->calendarDateKey($row['date'] ?? null);
|
|
if ($d === '') {
|
|
continue;
|
|
}
|
|
$noSchoolDays[$d] = [
|
|
'title' => $row['title'] ?? 'No School',
|
|
'description' => $row['description'] ?? '',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'teacher_name' => $teacherName,
|
|
'students' => $attendanceRows,
|
|
'teachers' => $teacherRows,
|
|
'class_section_id' => $classSectionId,
|
|
'sunday_dates' => $sundayDates,
|
|
'current_sunday' => $currentSunday,
|
|
'enable_attendance' => (int)$this->configuration->getConfig('enable_attendance'),
|
|
'can_edit' => ((int)$this->configuration->getConfig('enable_attendance') === 1),
|
|
'class_id' => (int)$this->classSection->getClassId($classSectionId),
|
|
'no_school_days' => $noSchoolDays,
|
|
'today' => $currentSunday,
|
|
'locked_by_student' => $lockedByStudent,
|
|
];
|
|
}
|
|
|
|
public function buildDailyAttendanceData(string $termSemester, string $termYear): array
|
|
{
|
|
$effectiveTermYear = $termYear;
|
|
|
|
$loadClassSections = function (string $schoolYear): array {
|
|
if ($schoolYear === '') {
|
|
return [];
|
|
}
|
|
|
|
return $this->classSection
|
|
->query()
|
|
->select('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name')
|
|
->join('student_class as sc', 'sc.class_section_id', '=', 'classSection.class_section_id')
|
|
->where('sc.school_year', $schoolYear)
|
|
->groupBy('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name')
|
|
->get()
|
|
->map(fn($row) => $row->toArray())
|
|
->all();
|
|
};
|
|
|
|
$classSections = $loadClassSections($effectiveTermYear);
|
|
|
|
if ($classSections === []) {
|
|
$fallbackYear = (string) DB::table('student_class')
|
|
->whereNotNull('school_year')
|
|
->where('school_year', '<>', '')
|
|
->orderByDesc('school_year')
|
|
->value('school_year');
|
|
|
|
if ($fallbackYear !== '' && $fallbackYear !== $effectiveTermYear) {
|
|
$effectiveTermYear = $fallbackYear;
|
|
$classSections = $loadClassSections($effectiveTermYear);
|
|
}
|
|
}
|
|
|
|
$attendanceData = [];
|
|
$attendanceRecord = [];
|
|
$studentsBySection = [];
|
|
$grades = [];
|
|
$datesBySection = [];
|
|
$studentSchoolMap = [];
|
|
|
|
$sectionCounts = $this->studentClass->getStudentCountsBySection($effectiveTermYear);
|
|
|
|
foreach ($classSections as $classSection) {
|
|
$secPk = (int)($classSection['id'] ?? 0);
|
|
$secCodeRaw = (string)($classSection['class_section_id'] ?? '');
|
|
$secCode = $secCodeRaw !== '' ? $secCodeRaw : (string)$secPk;
|
|
$classId = (int)($classSection['class_id'] ?? 0);
|
|
|
|
$studentsBySection[$secCode] = [];
|
|
$datesBySection[$secCode] = [];
|
|
$attendanceData[$secCode] = [];
|
|
$attendanceRecord[$secCode] = [];
|
|
|
|
$countForCode = (int)($sectionCounts[$secCode] ?? 0);
|
|
$countForPk = (int)($sectionCounts[$secPk] ?? 0);
|
|
$sectionHasStudents = ($countForCode + $countForPk) > 0;
|
|
|
|
$students = $this->studentClass->getClassStudents($secCode, $effectiveTermYear);
|
|
if (!$students && $secPk && (string)$secPk !== (string)$secCode) {
|
|
$students = $this->studentClass->getClassStudents($secPk, $effectiveTermYear);
|
|
}
|
|
|
|
if (!$sectionHasStudents || !$students) {
|
|
continue;
|
|
}
|
|
|
|
$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();
|
|
|
|
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);
|
|
$attendanceData[$secCode][$studentId] = $entries;
|
|
|
|
foreach ($entries as $entry) {
|
|
$d = (string)($entry['date'] ?? '');
|
|
if ($d !== '') {
|
|
$datesBySection[$secCode][$d] = true;
|
|
}
|
|
}
|
|
|
|
$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() ?? [
|
|
'total_presence' => 0,
|
|
'total_late' => 0,
|
|
'total_absence' => 0,
|
|
'total_attendance' => 0,
|
|
];
|
|
}
|
|
|
|
if (!$hasRoster) {
|
|
unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]);
|
|
continue;
|
|
}
|
|
|
|
$grades[$classId][] = $classSection;
|
|
}
|
|
|
|
foreach ($attendanceData as $secId => &$studentEntries) {
|
|
foreach ($studentEntries as &$entries) {
|
|
usort($entries, static fn($a, $b) => strcmp((string)($a['date'] ?? ''), (string)($b['date'] ?? '')));
|
|
}
|
|
}
|
|
unset($studentEntries, $entries);
|
|
|
|
foreach ($datesBySection as $sec => $set) {
|
|
$arr = array_keys($set);
|
|
sort($arr, SORT_STRING);
|
|
$datesBySection[$sec] = $arr;
|
|
}
|
|
|
|
ksort($grades, SORT_NUMERIC);
|
|
|
|
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($effectiveTermYear);
|
|
$semesterNorm = $this->semesterRangeService->normalizeSemester($termSemester);
|
|
|
|
if ($semesterNorm !== '') {
|
|
$semRange = $this->semesterRangeService->getSemesterRange($effectiveTermYear, $semesterNorm);
|
|
if ($semRange) {
|
|
[$rangeStart, $rangeEnd] = $semRange;
|
|
}
|
|
}
|
|
|
|
$dateList = $this->semesterRangeService->buildSundayList($rangeStart, $rangeEnd);
|
|
|
|
$noSchoolDays = [];
|
|
$events = $this->calendar->query()
|
|
->where('no_school', 1)
|
|
->where('date', '>=', $rangeStart)
|
|
->where('date', '<=', $rangeEnd)
|
|
->get()
|
|
->toArray();
|
|
|
|
foreach ($events as $event) {
|
|
$d = substr((string)($event['date'] ?? ''), 0, 10);
|
|
if ($d !== '') {
|
|
$noSchoolDays[$d] = true;
|
|
}
|
|
}
|
|
|
|
$anchorSunday = now()->dayOfWeek === Carbon::SUNDAY
|
|
? now()->toDateString()
|
|
: now()->next(Carbon::SUNDAY)->toDateString();
|
|
|
|
$passedDatesSet = [];
|
|
foreach ($dateList as $d) {
|
|
if ($d <= $anchorSunday && empty($noSchoolDays[$d])) {
|
|
$passedDatesSet[$d] = true;
|
|
}
|
|
}
|
|
|
|
$totalPassedDays = count($passedDatesSet);
|
|
|
|
if ($totalPassedDays > 0 && $effectiveTermYear !== '' && $termSemester !== '') {
|
|
foreach ($attendanceData as $secCode => $studentEntries) {
|
|
foreach ($studentEntries as $studentId => $entries) {
|
|
$statusByDate = [];
|
|
foreach ($entries as $entry) {
|
|
$d = substr((string)($entry['date'] ?? ''), 0, 10);
|
|
if ($d === '' || empty($passedDatesSet[$d])) {
|
|
continue;
|
|
}
|
|
$st = strtolower(trim((string)($entry['status'] ?? '')));
|
|
if (!in_array($st, ['present', 'absent', 'late'], true)) {
|
|
continue;
|
|
}
|
|
$statusByDate[$d] = $st;
|
|
}
|
|
|
|
$p = 0;
|
|
$l = 0;
|
|
$a = 0;
|
|
|
|
foreach ($statusByDate as $st) {
|
|
if ($st === 'present') {
|
|
$p++;
|
|
} elseif ($st === 'late') {
|
|
$l++;
|
|
} elseif ($st === 'absent') {
|
|
$a++;
|
|
}
|
|
}
|
|
|
|
$sum = $p + $l + $a;
|
|
$record = $attendanceRecord[$secCode][$studentId] ?? [];
|
|
$storedSum = (int)($record['total_presence'] ?? 0)
|
|
+ (int)($record['total_late'] ?? 0)
|
|
+ (int)($record['total_absence'] ?? 0);
|
|
|
|
if ($storedSum !== $totalPassedDays && $storedSum !== $sum) {
|
|
DB::table('attendance_record')
|
|
->updateOrInsert(
|
|
[
|
|
'student_id' => $studentId,
|
|
'semester' => $termSemester,
|
|
'school_year' => $effectiveTermYear,
|
|
],
|
|
[
|
|
'class_section_id' => $secCode,
|
|
'school_id' => $studentSchoolMap[$studentId] ?? '',
|
|
'total_presence' => $p,
|
|
'total_late' => $l,
|
|
'total_absence' => $a,
|
|
'total_attendance' => $sum,
|
|
'modified_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
'created_at' => now(),
|
|
]
|
|
);
|
|
|
|
$attendanceRecord[$secCode][$studentId]['total_presence'] = $p;
|
|
$attendanceRecord[$secCode][$studentId]['total_late'] = $l;
|
|
$attendanceRecord[$secCode][$studentId]['total_absence'] = $a;
|
|
$attendanceRecord[$secCode][$studentId]['total_attendance'] = $sum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$adminName = '';
|
|
if (auth()->id()) {
|
|
$u = $this->user->query()->select('firstname', 'lastname')->find(auth()->id());
|
|
if ($u) {
|
|
$adminName = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? ''));
|
|
}
|
|
}
|
|
|
|
return [
|
|
'attendanceData' => $attendanceData,
|
|
'attendanceRecord' => $attendanceRecord,
|
|
'studentsBySection' => $studentsBySection,
|
|
'grades' => $grades,
|
|
'datesBySection' => $datesBySection,
|
|
'dateList' => $dateList,
|
|
'noSchoolDays' => $noSchoolDays,
|
|
'totalPassedDays' => $totalPassedDays,
|
|
'semester' => $termSemester,
|
|
'schoolYear' => $effectiveTermYear,
|
|
'currentAdminName' => $adminName,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Distinct user_ids with staff rows on the given Sundays, scoped by teacher_class section assignment
|
|
* (works when staff_attendance has no class_section_id column).
|
|
*
|
|
* @return list<int>
|
|
*/
|
|
private function distinctStaffAttendeeIdsViaTeacherClassJoin(
|
|
array $sundayDates,
|
|
string $semester,
|
|
string $schoolYear,
|
|
int $classSectionId,
|
|
int $classSectionPk,
|
|
): array {
|
|
$sectionIds = array_values(array_unique(array_filter(
|
|
[$classSectionId, $classSectionPk],
|
|
static fn (int $v): bool => $v > 0,
|
|
)));
|
|
if ($sundayDates === [] || $sectionIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
|
|
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
|
|
|
$run = static function (
|
|
bool $requireTcSemester,
|
|
bool $requireSaSemester,
|
|
) use ($sundayDates, $schoolYear, $semester, $staffDateInSql, $sectionIds): array {
|
|
$q = DB::table('staff_attendance as sa')
|
|
->join('teacher_class as tc', function ($j) use ($sectionIds, $schoolYear, $requireTcSemester, $semester) {
|
|
$j->on('tc.teacher_id', '=', 'sa.user_id')
|
|
->whereIn('tc.class_section_id', $sectionIds)
|
|
->where('tc.school_year', '=', $schoolYear);
|
|
if ($requireTcSemester && trim($semester) !== '') {
|
|
$j->whereRaw('TRIM(tc.semester) = ?', [trim($semester)]);
|
|
}
|
|
})
|
|
->where('sa.school_year', $schoolYear)
|
|
->whereRaw($staffDateInSql, $sundayDates);
|
|
if ($requireSaSemester && trim($semester) !== '') {
|
|
$q->whereRaw('TRIM(sa.semester) = ?', [trim($semester)]);
|
|
}
|
|
|
|
return $q->distinct()
|
|
->pluck('sa.user_id')
|
|
->map(static fn ($v): int => (int) $v)
|
|
->filter(static fn (int $v): bool => $v > 0)
|
|
->values()
|
|
->all();
|
|
};
|
|
|
|
$ids = $run(true, true);
|
|
if ($ids === []) {
|
|
$ids = $run(false, true);
|
|
}
|
|
if ($ids === []) {
|
|
$ids = $run(false, false);
|
|
}
|
|
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* When teacher_class has no rows, build a roster from staff_attendance for this section/window.
|
|
*
|
|
* @return array<int, array{class_section_id:int, teacher_id:int, position:string, firstname:?string, lastname:?string}>
|
|
*/
|
|
private function teacherRosterFromStaffAttendance(
|
|
array $sundayDates,
|
|
string $semester,
|
|
string $schoolYear,
|
|
int $classSectionId,
|
|
int $classSectionPk,
|
|
): array {
|
|
if ($sundayDates === []) {
|
|
return [];
|
|
}
|
|
|
|
$staffDateInSql = 'DATE(`sa`.`date`) IN (' .
|
|
implode(',', array_fill(0, count($sundayDates), '?')) . ')';
|
|
|
|
$ids = [];
|
|
|
|
if (Schema::hasColumn('staff_attendance', 'class_section_id')) {
|
|
$applySectionScope = function ($query) use ($classSectionId, $classSectionPk): void {
|
|
$query->where(function ($w) use ($classSectionId, $classSectionPk) {
|
|
$w->whereNull('sa.class_section_id')
|
|
->orWhere('sa.class_section_id', 0)
|
|
->orWhere('sa.class_section_id', $classSectionId);
|
|
if ($classSectionPk > 0 && $classSectionPk !== $classSectionId) {
|
|
$w->orWhere('sa.class_section_id', $classSectionPk);
|
|
}
|
|
});
|
|
};
|
|
|
|
$base = function (bool $useSemester) use ($sundayDates, $schoolYear, $semester, $staffDateInSql, $applySectionScope) {
|
|
$q = DB::table('staff_attendance as sa')
|
|
->where('sa.school_year', $schoolYear)
|
|
->whereRaw($staffDateInSql, $sundayDates);
|
|
if ($useSemester && trim($semester) !== '') {
|
|
$q->whereRaw('TRIM(sa.semester) = ?', [trim($semester)]);
|
|
}
|
|
$applySectionScope($q);
|
|
|
|
return $q->distinct()->pluck('sa.user_id')->map(static fn ($v) => (int) $v)->filter(static fn (int $v) => $v > 0)->values()->all();
|
|
};
|
|
|
|
$ids = $base(true);
|
|
if ($ids === []) {
|
|
$ids = $base(false);
|
|
}
|
|
}
|
|
|
|
if ($ids === []) {
|
|
$ids = $this->distinctStaffAttendeeIdsViaTeacherClassJoin(
|
|
$sundayDates,
|
|
$semester,
|
|
$schoolYear,
|
|
$classSectionId,
|
|
$classSectionPk,
|
|
);
|
|
}
|
|
|
|
if ($ids === []) {
|
|
return [];
|
|
}
|
|
|
|
$users = $this->user->query()
|
|
->select(['id', 'firstname', 'lastname'])
|
|
->whereIn('id', $ids)
|
|
->orderBy('firstname')
|
|
->orderBy('lastname')
|
|
->get();
|
|
|
|
$out = [];
|
|
foreach ($users as $u) {
|
|
$out[] = [
|
|
'class_section_id' => $classSectionId,
|
|
'teacher_id' => (int) $u->id,
|
|
'position' => 'main',
|
|
'firstname' => $u->firstname,
|
|
'lastname' => $u->lastname,
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Normalize DB datetimes / date strings to Y-m-d so they match {@see $sundayDates} keys.
|
|
*/
|
|
private function calendarDateKey(mixed $value): string
|
|
{
|
|
$s = trim((string)($value ?? ''));
|
|
if ($s === '') {
|
|
return '';
|
|
}
|
|
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $s, $m)) {
|
|
return $m[1];
|
|
}
|
|
try {
|
|
return Carbon::parse($s)->toDateString();
|
|
} catch (\Throwable) {
|
|
return '';
|
|
}
|
|
}
|
|
}
|