reconstruction of the project
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class AttendancePolicyService
|
||||
{
|
||||
public function canEditDay(array $dayRow, array $currentUser): bool
|
||||
{
|
||||
$status = strtolower((string)($dayRow['status'] ?? 'draft'));
|
||||
$roles = array_map([$this, 'normalizeRole'], (array)($currentUser['roles'] ?? []));
|
||||
$permissions = array_map('strtolower', (array)($currentUser['permissions'] ?? []));
|
||||
|
||||
$isAdmin = $this->isAdminLike($roles, $permissions);
|
||||
$isTeacher = in_array('teacher', $roles, true) || in_array('ta', $roles, true) || in_array('teacher_assistant', $roles, true);
|
||||
|
||||
if ($status === 'finalized' || $status === 'published') {
|
||||
return $isAdmin;
|
||||
}
|
||||
|
||||
if ($status === 'submitted') {
|
||||
return $isAdmin;
|
||||
}
|
||||
|
||||
if ($status === 'draft') {
|
||||
return $isAdmin || $isTeacher;
|
||||
}
|
||||
|
||||
return $isAdmin;
|
||||
}
|
||||
|
||||
public function isTeacher(array $roles): bool
|
||||
{
|
||||
$roles = array_map([$this, 'normalizeRole'], $roles);
|
||||
return in_array('teacher', $roles, true)
|
||||
|| in_array('ta', $roles, true)
|
||||
|| in_array('teacher_assistant', $roles, true)
|
||||
|| in_array('assistant_teacher', $roles, true);
|
||||
}
|
||||
|
||||
public function isAdminLike(array $roles, array $permissions = []): bool
|
||||
{
|
||||
$roles = array_map([$this, 'normalizeRole'], $roles);
|
||||
|
||||
$excluded = [
|
||||
'parent',
|
||||
'guest',
|
||||
'teacher',
|
||||
'teacher_assistant',
|
||||
'assistant_teacher',
|
||||
'ta',
|
||||
];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if ($role !== '' && !in_array($role, $excluded, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = strtolower(trim((string)$permission));
|
||||
if (str_contains($permission, 'attendance.manage') || str_contains($permission, 'attendance.admin')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function normalizeRole(?string $role): string
|
||||
{
|
||||
return str_replace([' ', '-'], '_', strtolower(trim((string)$role)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
<?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 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 = [];
|
||||
|
||||
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) {
|
||||
$q->where('class_section_id', $classSectionId)
|
||||
->orWhere('class_section_id', (int)$classSectionId);
|
||||
})
|
||||
->whereIn('date', $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) {
|
||||
$q->where('class_section_id', $classSectionId)
|
||||
->orWhere('class_section_id', (int)$classSectionId);
|
||||
})
|
||||
->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 = (string)($entry['date'] ?? '');
|
||||
$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');
|
||||
}
|
||||
|
||||
$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,
|
||||
];
|
||||
}
|
||||
|
||||
$assigned = $this->teacherClass->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
|
||||
$teacherRows = [];
|
||||
|
||||
if (!empty($assigned)) {
|
||||
$teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned);
|
||||
|
||||
$statusMap = [];
|
||||
if (!empty($teacherIds)) {
|
||||
$rows = 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)
|
||||
->whereIn('sa.date', $sundayDates)
|
||||
->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statusMap[(int)$row->user_id][(string)$row->date] = [
|
||||
'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');
|
||||
}
|
||||
|
||||
$teacherRows[] = [
|
||||
'teacher_id' => $teacherId,
|
||||
'position' => $position,
|
||||
'name' => $name !== '' ? $name : 'User#' . $teacherId,
|
||||
'sunday_statuses' => $history,
|
||||
'today' => $history[$currentSunday] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$noSchoolDays = [];
|
||||
$calendarRows = $this->calendar->query()
|
||||
->where('no_school', 1)
|
||||
->whereIn('date', $sundayDates)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
foreach ($calendarRows as $row) {
|
||||
$d = (string)($row['date'] ?? '');
|
||||
$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
|
||||
{
|
||||
$classSections = $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', $termYear)
|
||||
->groupBy('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name')
|
||||
->get()
|
||||
->map(fn($row) => (array)$row)
|
||||
->all();
|
||||
|
||||
$attendanceData = [];
|
||||
$attendanceRecord = [];
|
||||
$studentsBySection = [];
|
||||
$grades = [];
|
||||
$datesBySection = [];
|
||||
$studentSchoolMap = [];
|
||||
|
||||
$sectionCounts = $this->studentClass->getStudentCountsBySection($termYear);
|
||||
|
||||
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, $termYear);
|
||||
if (!$students && $secPk && (string)$secPk !== (string)$secCode) {
|
||||
$students = $this->studentClass->getClassStudents($secPk, $termYear);
|
||||
}
|
||||
|
||||
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($termYear !== '', fn($q) => $q->where('school_year', $termYear))
|
||||
->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($termYear !== '', fn($q) => $q->where('school_year', $termYear))
|
||||
->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($termYear);
|
||||
$semesterNorm = $this->semesterRangeService->normalizeSemester($termSemester);
|
||||
|
||||
if ($semesterNorm !== '') {
|
||||
$semRange = $this->semesterRangeService->getSemesterRange($termYear, $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 && $termYear !== '' && $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' => $termYear,
|
||||
],
|
||||
[
|
||||
'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' => $termYear,
|
||||
'currentAdminName' => $adminName,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceRecord;
|
||||
|
||||
class AttendanceRecordSyncService
|
||||
{
|
||||
public function __construct(
|
||||
protected AttendanceRecord $attendanceRecord
|
||||
) {}
|
||||
|
||||
public function updateAttendanceRecord(
|
||||
int $studentId,
|
||||
string $schoolId,
|
||||
string $newStatus,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?string $oldStatus,
|
||||
?int $modifiedBy = null
|
||||
): void {
|
||||
$record = AttendanceRecord::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newStatus = strtolower($newStatus);
|
||||
$oldStatus = strtolower((string)$oldStatus);
|
||||
|
||||
$presence = (int)($record->total_presence ?? 0);
|
||||
$absence = (int)($record->total_absence ?? 0);
|
||||
$late = (int)($record->total_late ?? 0);
|
||||
|
||||
if ($oldStatus === 'present') {
|
||||
$presence--;
|
||||
} elseif ($oldStatus === 'absent') {
|
||||
$absence--;
|
||||
} elseif ($oldStatus === 'late') {
|
||||
$late--;
|
||||
}
|
||||
|
||||
if ($newStatus === 'present') {
|
||||
$presence++;
|
||||
} elseif ($newStatus === 'absent') {
|
||||
$absence++;
|
||||
} elseif ($newStatus === 'late') {
|
||||
$late++;
|
||||
}
|
||||
|
||||
$record->update([
|
||||
'total_presence' => max(0, $presence),
|
||||
'total_absence' => max(0, $absence),
|
||||
'total_late' => max(0, $late),
|
||||
'updated_at' => now(),
|
||||
'modified_by' => $modifiedBy,
|
||||
]);
|
||||
}
|
||||
|
||||
public function addNewAttendanceRecord(
|
||||
int $classSectionId,
|
||||
int $studentId,
|
||||
string $schoolId,
|
||||
string $status,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $modifiedBy = null
|
||||
): void {
|
||||
$status = strtolower($status);
|
||||
|
||||
$record = AttendanceRecord::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if ($record) {
|
||||
$record->update([
|
||||
'total_presence' => (int)($record->total_presence ?? 0) + ($status === 'present' ? 1 : 0),
|
||||
'total_absence' => (int)($record->total_absence ?? 0) + ($status === 'absent' ? 1 : 0),
|
||||
'total_late' => (int)($record->total_late ?? 0) + ($status === 'late' ? 1 : 0),
|
||||
'total_attendance' => (int)($record->total_attendance ?? 0) + 1,
|
||||
'updated_at' => now(),
|
||||
'modified_by' => $modifiedBy,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
AttendanceRecord::query()->create([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $schoolId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
'modified_by' => $modifiedBy,
|
||||
'total_presence' => $status === 'present' ? 1 : 0,
|
||||
'total_absence' => $status === 'absent' ? 1 : 0,
|
||||
'total_late' => $status === 'late' ? 1 : 0,
|
||||
'total_attendance' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function upsertAttendanceRecordTotals(
|
||||
int $studentId,
|
||||
string $classSectionId,
|
||||
string $schoolId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
int $totalPresence,
|
||||
int $totalLate,
|
||||
int $totalAbsence,
|
||||
int $totalAttendance,
|
||||
?int $modifiedBy = null
|
||||
): bool {
|
||||
if ($semester === '' || $schoolYear === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$record = AttendanceRecord::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$payload = [
|
||||
'total_presence' => max(0, $totalPresence),
|
||||
'total_late' => max(0, $totalLate),
|
||||
'total_absence' => max(0, $totalAbsence),
|
||||
'total_attendance' => max(0, $totalAttendance),
|
||||
'updated_at' => now(),
|
||||
'modified_by' => $modifiedBy,
|
||||
];
|
||||
|
||||
if ($record) {
|
||||
return $record->update($payload);
|
||||
}
|
||||
|
||||
$payload['class_section_id'] = (int)$classSectionId;
|
||||
$payload['student_id'] = $studentId;
|
||||
$payload['school_id'] = $schoolId;
|
||||
$payload['semester'] = $semester;
|
||||
$payload['school_year'] = $schoolYear;
|
||||
$payload['created_at'] = now();
|
||||
|
||||
return (bool)AttendanceRecord::query()->create($payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\UserRole;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class AttendanceService
|
||||
{
|
||||
public function __construct(
|
||||
protected Configuration $configuration,
|
||||
protected AttendanceData $attendanceData,
|
||||
protected AttendanceDay $attendanceDay,
|
||||
protected ClassSection $classSection,
|
||||
protected UserRole $userRole,
|
||||
protected AttendancePolicyService $attendancePolicyService,
|
||||
protected StudentAttendanceWriterService $studentAttendanceWriterService,
|
||||
protected TeacherAttendanceSubmissionService $teacherAttendanceSubmissionService,
|
||||
protected AttendanceRecordSyncService $attendanceRecordSyncService,
|
||||
) {}
|
||||
|
||||
public function currentSemester(): string
|
||||
{
|
||||
return (string)($this->configuration->getConfig('semester') ?? 'Fall');
|
||||
}
|
||||
|
||||
public function currentSchoolYear(): string
|
||||
{
|
||||
return (string)($this->configuration->getConfig('school_year') ?? now()->year . '-' . (now()->year + 1));
|
||||
}
|
||||
|
||||
public function updateAttendanceManagement(Authenticatable $user, array $data): array
|
||||
{
|
||||
$classSectionId = (int)$data['class_section_id'];
|
||||
$studentId = (int)$data['student_id'];
|
||||
$status = strtolower((string)$data['status']);
|
||||
$date = (string)$data['date'];
|
||||
$reason = $data['reason'] ?? null;
|
||||
$classId = $data['class_id'] ?? null;
|
||||
$semester = (string)($data['semester'] ?? $this->currentSemester());
|
||||
$schoolYear = (string)($data['school_year'] ?? $this->currentSchoolYear());
|
||||
|
||||
$dayRow = AttendanceDay::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->whereDate('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$dayPolicyRow = $dayRow?->toArray() ?? [
|
||||
'status' => 'draft',
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
];
|
||||
|
||||
$currentUser = [
|
||||
'id' => $user->id,
|
||||
'roles' => method_exists($user, 'roles') ? $user->roles->pluck('name')->all() : [],
|
||||
'permissions' => method_exists($user, 'permissions') ? $user->permissions->pluck('name')->all() : [],
|
||||
];
|
||||
|
||||
if (!$this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) {
|
||||
throw new RuntimeException('Attendance is locked for your role.');
|
||||
}
|
||||
|
||||
$reportedRaw = strtolower((string)($data['is_reported'] ?? ''));
|
||||
$isReported = $status === 'present'
|
||||
? 'no'
|
||||
: (in_array($reportedRaw, ['1', 'yes', 'y', 'true', 'on'], true) ? 'yes' : 'no');
|
||||
|
||||
DB::transaction(function () use (
|
||||
$user,
|
||||
$classSectionId,
|
||||
$studentId,
|
||||
$status,
|
||||
$date,
|
||||
$reason,
|
||||
$classId,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$isReported
|
||||
) {
|
||||
$this->studentAttendanceWriterService->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => null,
|
||||
'status' => $status,
|
||||
'reason' => $reason,
|
||||
'is_reported' => $isReported,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'date' => $date,
|
||||
'class_id' => (int)$classId,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
AttendanceDay::query()->firstOrCreate(
|
||||
[
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
],
|
||||
[
|
||||
'status' => 'draft',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'Attendance updated successfully.',
|
||||
];
|
||||
}
|
||||
|
||||
public function submitTeacherAttendance(Authenticatable $user, array $data): array
|
||||
{
|
||||
return $this->teacherAttendanceSubmissionService->submit(
|
||||
$user,
|
||||
$data,
|
||||
fn (?array $row) => $this->detectExternalSubmission($row),
|
||||
fn () => $this->currentSemester(),
|
||||
fn () => $this->currentSchoolYear(),
|
||||
);
|
||||
}
|
||||
|
||||
public function adminAddEntry(Authenticatable $user, array $data): array
|
||||
{
|
||||
$classSectionId = (int)$data['class_section_id'];
|
||||
$studentId = (int)$data['student_id'];
|
||||
$status = strtolower((string)$data['status']);
|
||||
$date = (string)($data['date'] ?? now()->toDateString());
|
||||
$reason = $data['reason'] ?? null;
|
||||
$isReported = in_array(strtolower((string)($data['is_reported'] ?? 'no')), ['yes', 'no'], true)
|
||||
? strtolower((string)($data['is_reported'] ?? 'no'))
|
||||
: 'no';
|
||||
|
||||
$section = $this->classSection
|
||||
->query()
|
||||
->select('class_id', 'id', 'class_section_id')
|
||||
->where('id', $classSectionId)
|
||||
->orWhere('class_section_id', $classSectionId)
|
||||
->first();
|
||||
|
||||
$resolvedClassId = (int)($section->class_id ?? 0);
|
||||
|
||||
if ($resolvedClassId <= 0) {
|
||||
throw new RuntimeException('Cannot resolve class_id for this section.');
|
||||
}
|
||||
|
||||
DB::transaction(function () use (
|
||||
$user,
|
||||
$classSectionId,
|
||||
$studentId,
|
||||
$status,
|
||||
$date,
|
||||
$reason,
|
||||
$resolvedClassId,
|
||||
$isReported
|
||||
) {
|
||||
AttendanceDay::query()->firstOrCreate(
|
||||
[
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'semester' => $this->currentSemester(),
|
||||
'school_year' => $this->currentSchoolYear(),
|
||||
],
|
||||
[
|
||||
'status' => 'draft',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->studentAttendanceWriterService->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $data['school_id'] ?? null,
|
||||
'status' => $status,
|
||||
'reason' => $reason,
|
||||
'is_reported' => $isReported,
|
||||
'semester' => $this->currentSemester(),
|
||||
'school_year' => $this->currentSchoolYear(),
|
||||
'date' => $date,
|
||||
'class_id' => $resolvedClassId,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'Entry saved successfully.',
|
||||
];
|
||||
}
|
||||
|
||||
public function normalizeAttendanceEntries(array $entries): array
|
||||
{
|
||||
foreach ($entries as &$row) {
|
||||
$row['status'] = $this->normalizeStatus($row['status'] ?? '');
|
||||
$reportedRaw = $row['is_reported'] ?? ($row['reported'] ?? '');
|
||||
$row['is_reported'] = $this->normalizeReportedFlag($reportedRaw);
|
||||
}
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function normalizeStatus(?string $status): string
|
||||
{
|
||||
$status = strtolower(trim((string)$status));
|
||||
return in_array($status, ['present', 'absent', 'late'], true) ? $status : $status;
|
||||
}
|
||||
|
||||
public function normalizeReportedFlag(mixed $flag): string
|
||||
{
|
||||
$v = strtolower(trim((string)$flag));
|
||||
return in_array($v, ['yes', '1', 'true', 'y', 'on'], true) ? 'yes' : 'no';
|
||||
}
|
||||
|
||||
public function detectExternalSubmission(?array $row): array
|
||||
{
|
||||
if (empty($row) || !is_array($row)) {
|
||||
return ['locked' => false, 'source' => null];
|
||||
}
|
||||
|
||||
$reportedRaw = strtolower((string)($row['is_reported'] ?? ''));
|
||||
$reason = strtolower((string)($row['reason'] ?? ''));
|
||||
|
||||
$isParent = in_array($reportedRaw, ['yes', '1', 'true', 'y'], true)
|
||||
|| str_contains($reason, 'parent-reported')
|
||||
|| str_contains($reason, 'parent reported');
|
||||
|
||||
if ($isParent) {
|
||||
return ['locked' => true, 'source' => 'parent'];
|
||||
}
|
||||
|
||||
$modifierId = (int)($row['modified_by'] ?? 0);
|
||||
if ($modifierId > 0 && $this->isAdminLikeUserId($modifierId)) {
|
||||
return ['locked' => true, 'source' => 'admin'];
|
||||
}
|
||||
|
||||
return ['locked' => false, 'source' => null];
|
||||
}
|
||||
|
||||
public function isAdminLikeUserId(int $userId): bool
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roles = $this->userRole->getRolesByUserId($userId);
|
||||
if (empty($roles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$excluded = ['parent', 'guest', 'teacher', 'teacher_assistant', 'assistant_teacher', 'ta'];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$name = strtolower(str_replace([' ', '-'], '_', (string)($role['role_name'] ?? '')));
|
||||
if ($name !== '' && !in_array($name, $excluded, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SemesterRangeService
|
||||
{
|
||||
public function normalizeSemester(?string $semester): string
|
||||
{
|
||||
$value = strtolower(trim((string)$semester));
|
||||
|
||||
return match ($value) {
|
||||
'fall', 'autumn' => 'Fall',
|
||||
'spring' => 'Spring',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
public function getSchoolYearRange(string $schoolYear): array
|
||||
{
|
||||
[$startYear, $endYear] = $this->parseSchoolYear($schoolYear);
|
||||
|
||||
return [
|
||||
Carbon::create($startYear, 9, 1)->toDateString(),
|
||||
Carbon::create($endYear, 5, 31)->toDateString(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSemesterRange(string $schoolYear, string $semester): ?array
|
||||
{
|
||||
[$startYear, $endYear] = $this->parseSchoolYear($schoolYear);
|
||||
$semester = $this->normalizeSemester($semester);
|
||||
|
||||
return match ($semester) {
|
||||
'Fall' => [
|
||||
Carbon::create($startYear, 9, 21)->toDateString(),
|
||||
Carbon::create($endYear, 1, 18)->toDateString(),
|
||||
],
|
||||
'Spring' => [
|
||||
Carbon::create($endYear, 1, 25)->toDateString(),
|
||||
Carbon::create($endYear, 5, 31)->toDateString(),
|
||||
],
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function buildSundayList(string $startDate, string $endDate): array
|
||||
{
|
||||
$start = Carbon::parse($startDate)->startOfDay();
|
||||
$end = Carbon::parse($endDate)->startOfDay();
|
||||
|
||||
if ($start->dayOfWeek !== Carbon::SUNDAY) {
|
||||
$start = $start->next(Carbon::SUNDAY);
|
||||
}
|
||||
|
||||
$dates = [];
|
||||
$cursor = $start->copy();
|
||||
|
||||
while ($cursor->lte($end)) {
|
||||
$dates[] = $cursor->toDateString();
|
||||
$cursor->addWeek();
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
protected function parseSchoolYear(string $schoolYear): array
|
||||
{
|
||||
if (preg_match('/^\s*(\d{4})\s*-\s*(\d{4})\s*$/', $schoolYear, $m)) {
|
||||
return [(int)$m[1], (int)$m[2]];
|
||||
}
|
||||
|
||||
$year = (int) date('Y');
|
||||
return [$year, $year + 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Models\StaffAttendance;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class StaffAttendanceService
|
||||
{
|
||||
public function __construct(
|
||||
protected Configuration $configuration,
|
||||
protected StaffAttendance $staffAttendance,
|
||||
protected TeacherClass $teacherClass,
|
||||
protected ClassSection $classSection,
|
||||
protected SemesterRangeService $semesterRangeService,
|
||||
) {}
|
||||
|
||||
public function monthData(?string $semester, ?string $schoolYear): array
|
||||
{
|
||||
$semester = $semester ?: (string)$this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year');
|
||||
|
||||
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
|
||||
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
|
||||
|
||||
if ($semesterNorm !== '') {
|
||||
$semesterRange = $this->semesterRangeService->getSemesterRange($schoolYear, $semesterNorm);
|
||||
if ($semesterRange) {
|
||||
[$rangeStart, $rangeEnd] = $semesterRange;
|
||||
}
|
||||
}
|
||||
|
||||
$days = $this->semesterRangeService->buildSundayList($rangeStart, $rangeEnd);
|
||||
|
||||
$assignedBySection = $semesterNorm !== ''
|
||||
? $this->teacherClass->assignedBySectionForTerm($semesterNorm, $schoolYear)
|
||||
: $this->teacherClass->assignedBySectionForTerm($semester, $schoolYear);
|
||||
|
||||
$sectionCodes = array_keys($assignedBySection);
|
||||
$labels = [];
|
||||
|
||||
if (!empty($sectionCodes)) {
|
||||
$rows = $this->classSection->query()
|
||||
->select(['class_section_id', 'class_section_name'])
|
||||
->whereIn('class_section_id', $sectionCodes)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$labels[(int)$row['class_section_id']] = trim((string)$row['class_section_name']) ?: 'Section #' . (int)$row['class_section_id'];
|
||||
}
|
||||
}
|
||||
|
||||
$teacherIds = [];
|
||||
foreach ($assignedBySection as $rows) {
|
||||
foreach ($rows as $row) {
|
||||
$teacherIds[(int)$row['teacher_id']] = true;
|
||||
}
|
||||
}
|
||||
$teacherIds = array_keys($teacherIds);
|
||||
|
||||
$statusByTeacher = [];
|
||||
if (!empty($teacherIds) && !empty($days)) {
|
||||
$query = DB::table('staff_attendance')
|
||||
->select('user_id', 'date', 'status', 'reason')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('user_id', $teacherIds)
|
||||
->whereIn('date', $days);
|
||||
|
||||
if ($semesterNorm !== '') {
|
||||
$query->where('semester', $semesterNorm);
|
||||
}
|
||||
|
||||
$rows = $query->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statusByTeacher[(int)$row->user_id][(string)$row->date] = [
|
||||
'status' => strtolower((string)$row->status),
|
||||
'reason' => $row->reason,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$noSchoolRows = DB::table('calendar_events')
|
||||
->select('date')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('no_school', 1)
|
||||
->where('date', '>=', $rangeStart)
|
||||
->where('date', '<=', $rangeEnd)
|
||||
->get();
|
||||
|
||||
$noSchoolDays = [];
|
||||
foreach ($noSchoolRows as $row) {
|
||||
$noSchoolDays[] = (string)$row->date;
|
||||
}
|
||||
|
||||
$sections = [];
|
||||
foreach ($sectionCodes as $code) {
|
||||
$teachersPayload = [];
|
||||
|
||||
foreach (($assignedBySection[$code] ?? []) as $row) {
|
||||
$teacherId = (int)$row['teacher_id'];
|
||||
$name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')) ?: ('User#' . $teacherId);
|
||||
$position = strtolower((string)($row['position'] ?? 'main'));
|
||||
$position = in_array($position, ['main', 'ta'], true) ? $position : 'main';
|
||||
|
||||
$cells = [];
|
||||
$totals = ['p' => 0, 'a' => 0, 'l' => 0];
|
||||
|
||||
foreach ($days as $date) {
|
||||
$cell = $statusByTeacher[$teacherId][$date] ?? null;
|
||||
if (!$cell || empty($cell['status'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cells[$date] = $cell;
|
||||
|
||||
if ($cell['status'] === 'present') {
|
||||
$totals['p']++;
|
||||
} elseif ($cell['status'] === 'absent') {
|
||||
$totals['a']++;
|
||||
} elseif ($cell['status'] === 'late') {
|
||||
$totals['l']++;
|
||||
}
|
||||
}
|
||||
|
||||
$teachersPayload[] = [
|
||||
'teacher_id' => $teacherId,
|
||||
'name' => $name,
|
||||
'position' => $position,
|
||||
'cells' => $cells,
|
||||
'totals' => $totals,
|
||||
];
|
||||
}
|
||||
|
||||
$sections[] = [
|
||||
'section_code' => (int)$code,
|
||||
'label' => $labels[$code] ?? ('Section #' . (int)$code),
|
||||
'teachers' => $teachersPayload,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'filters' => [
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'range_start' => $rangeStart,
|
||||
'range_end' => $rangeEnd,
|
||||
],
|
||||
'sundays' => $days,
|
||||
'noSchoolDays' => $noSchoolDays,
|
||||
'sections' => $sections,
|
||||
'admins' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function adminsAttendanceData(?string $date, ?string $semester, ?string $schoolYear): array
|
||||
{
|
||||
$date = $date ?: now()->toDateString();
|
||||
$semester = $semester ?: (string)$this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year');
|
||||
|
||||
$allStaff = $this->staffAttendance->staffWithStatusForDay($date, $semester, $schoolYear, []);
|
||||
$userIds = [];
|
||||
|
||||
foreach ($allStaff as $row) {
|
||||
$uid = (int)($row['user_id'] ?? 0);
|
||||
if ($uid > 0) {
|
||||
$userIds[$uid] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$userIds = array_keys($userIds);
|
||||
|
||||
$rolesRows = DB::table('user_roles as ur')
|
||||
->select('ur.user_id', 'r.id as role_id', 'r.name as role_name', 'r.slug as role_slug', 'r.priority')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
->where('r.is_active', 1)
|
||||
->whereIn('ur.user_id', $userIds)
|
||||
->get();
|
||||
|
||||
$rolesByUser = [];
|
||||
foreach ($rolesRows as $row) {
|
||||
$rolesByUser[(int)$row->user_id][] = (array)$row;
|
||||
}
|
||||
|
||||
foreach ($rolesByUser as &$list) {
|
||||
usort($list, fn($a, $b) => ((int)($a['priority'] ?? 100)) <=> ((int)($b['priority'] ?? 100)));
|
||||
}
|
||||
|
||||
$excludeSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant', 'ta', 'assistant_teacher'];
|
||||
|
||||
$staffByUser = [];
|
||||
foreach ($allStaff as $row) {
|
||||
$uid = (int)($row['user_id'] ?? 0);
|
||||
if ($uid > 0 && !isset($staffByUser[$uid])) {
|
||||
$staffByUser[$uid] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$adminsGrid = [];
|
||||
foreach ($userIds as $uid) {
|
||||
$roles = $rolesByUser[$uid] ?? [];
|
||||
$picked = null;
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$slug = strtolower((string)($role['role_slug'] ?? $role['role_name'] ?? ''));
|
||||
if (!in_array($slug, $excludeSlugs, true)) {
|
||||
$picked = $role;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$picked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$base = $staffByUser[$uid] ?? ['user_id' => $uid];
|
||||
$base['role_name'] = $picked['role_name'] ?? 'Admin';
|
||||
$base['role_slug'] = $picked['role_slug'] ?? null;
|
||||
$adminsGrid[] = $base;
|
||||
}
|
||||
|
||||
usort($adminsGrid, function ($a, $b) {
|
||||
$an = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? ''));
|
||||
$bn = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? ''));
|
||||
return strcasecmp($an, $bn);
|
||||
});
|
||||
|
||||
return [
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'date' => $date,
|
||||
'admins_grid' => $adminsGrid,
|
||||
];
|
||||
}
|
||||
|
||||
public function saveAdmins(Authenticatable $user, array $data): array
|
||||
{
|
||||
$date = (string)$data['date'];
|
||||
$semester = (string)$data['semester'];
|
||||
$schoolYear = (string)$data['school_year'];
|
||||
$admins = (array)($data['admins'] ?? $data['staff'] ?? []);
|
||||
$allowedStatuses = ['present', 'absent', 'late'];
|
||||
$excludedRoleSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant'];
|
||||
|
||||
DB::transaction(function () use ($admins, $date, $semester, $schoolYear, $allowedStatuses, $excludedRoleSlugs) {
|
||||
foreach ($admins as $uid => $row) {
|
||||
$uid = (int)$uid;
|
||||
$status = strtolower(trim((string)($row['status'] ?? '')));
|
||||
$reason = trim((string)($row['reason'] ?? ''));
|
||||
$roleName = (string)($row['role_name'] ?? '');
|
||||
$roleSlug = strtolower(str_replace(['-', ' '], '_', (string)($row['role_slug'] ?? $roleName)));
|
||||
|
||||
if (in_array($roleSlug, $excludedRoleSlugs, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($status === '') {
|
||||
DB::table('staff_attendance')
|
||||
->where('user_id', $uid)
|
||||
->whereDate('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($status, $allowedStatuses, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('staff_attendance')->updateOrInsert(
|
||||
[
|
||||
'user_id' => $uid,
|
||||
'date' => $date,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
],
|
||||
[
|
||||
'role_name' => $roleName !== '' ? $roleName : 'Admin',
|
||||
'status' => $status,
|
||||
'reason' => $reason !== '' ? $reason : null,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'Admins attendance saved successfully.',
|
||||
];
|
||||
}
|
||||
|
||||
public function saveCell(Authenticatable $user, array $data): array
|
||||
{
|
||||
$date = (string)$data['date'];
|
||||
$status = (string)($data['status'] ?? '');
|
||||
$targetId = (int)$data['user_id'];
|
||||
$roleName = trim((string)($data['role_name'] ?? ''));
|
||||
$semester = (string)$data['semester'];
|
||||
$schoolYear = (string)$data['school_year'];
|
||||
$reason = trim((string)($data['reason'] ?? ''));
|
||||
|
||||
if ($status === '') {
|
||||
DB::table('staff_attendance')
|
||||
->where('user_id', $targetId)
|
||||
->whereDate('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->delete();
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
DB::table('staff_attendance')->updateOrInsert(
|
||||
[
|
||||
'user_id' => $targetId,
|
||||
'date' => $date,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
],
|
||||
[
|
||||
'role_name' => $roleName !== '' ? $roleName : 'Admin',
|
||||
'status' => $status,
|
||||
'reason' => $reason !== '' ? $reason : null,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
public function monthCsv(?string $semester, ?string $schoolYear, ?string $scope): StreamedResponse
|
||||
{
|
||||
$semester = $semester ?: (string)$this->configuration->getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year');
|
||||
$scope = strtolower((string)$scope);
|
||||
|
||||
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
|
||||
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
|
||||
if ($semesterNorm !== '') {
|
||||
$semesterRange = $this->semesterRangeService->getSemesterRange($schoolYear, $semesterNorm);
|
||||
if ($semesterRange) {
|
||||
[$rangeStart, $rangeEnd] = $semesterRange;
|
||||
}
|
||||
}
|
||||
|
||||
$daysList = $this->semesterRangeService->buildSundayList($rangeStart, $rangeEnd);
|
||||
|
||||
$filename = ($scope === 'admins' ? 'admin_attendance_' : 'teacher_attendance_')
|
||||
. $semester . '_' . str_replace('/', '-', $schoolYear) . '.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($semester, $schoolYear, $scope, $daysList, $semesterNorm, $rangeStart, $rangeEnd) {
|
||||
$out = fopen('php://output', 'w');
|
||||
|
||||
if ($scope === 'admins') {
|
||||
fputcsv($out, ['User ID', 'Date', 'Status', 'Reason']);
|
||||
|
||||
$query = DB::table('staff_attendance')
|
||||
->select('user_id', 'date', 'status', 'reason')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $rangeStart)
|
||||
->where('date', '<=', $rangeEnd);
|
||||
|
||||
if ($semesterNorm !== '') {
|
||||
$query->where('semester', $semesterNorm);
|
||||
}
|
||||
|
||||
foreach ($query->orderBy('date')->get() as $row) {
|
||||
fputcsv($out, [
|
||||
$row->user_id,
|
||||
$row->date,
|
||||
strtoupper(substr((string)$row->status, 0, 1)),
|
||||
$row->reason,
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($out);
|
||||
return;
|
||||
}
|
||||
|
||||
fputcsv($out, ['Teacher ID', 'Date', 'Status', 'Reason']);
|
||||
|
||||
$query = DB::table('staff_attendance')
|
||||
->select('user_id', 'date', 'status', 'reason')
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $rangeStart)
|
||||
->where('date', '<=', $rangeEnd);
|
||||
|
||||
if ($semesterNorm !== '') {
|
||||
$query->where('semester', $semesterNorm);
|
||||
}
|
||||
|
||||
foreach ($query->orderBy('date')->get() as $row) {
|
||||
fputcsv($out, [
|
||||
$row->user_id,
|
||||
$row->date,
|
||||
strtoupper(substr((string)$row->status, 0, 1)),
|
||||
$row->reason,
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($out);
|
||||
}, $filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\Student;
|
||||
use RuntimeException;
|
||||
|
||||
class StudentAttendanceWriterService
|
||||
{
|
||||
public function __construct(
|
||||
protected AttendanceData $attendanceData,
|
||||
protected Student $student,
|
||||
protected AttendanceRecordSyncService $attendanceRecordSyncService,
|
||||
) {}
|
||||
|
||||
public function updateAttendanceData(
|
||||
int $attendanceId,
|
||||
string $newStatus,
|
||||
?string $newReason = null,
|
||||
?string $reported = null,
|
||||
?int $userId = null
|
||||
): void {
|
||||
$payload = [
|
||||
'status' => strtolower($newStatus),
|
||||
'reason' => $newReason ?: null,
|
||||
'modified_by' => $userId,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
if ($reported !== null) {
|
||||
$payload['is_reported'] = in_array(strtolower($reported), ['yes', 'no'], true)
|
||||
? strtolower($reported)
|
||||
: 'no';
|
||||
}
|
||||
|
||||
AttendanceData::query()->whereKey($attendanceId)->update($payload);
|
||||
}
|
||||
|
||||
public function insertAttendanceData(
|
||||
int $classSectionId,
|
||||
int $studentId,
|
||||
string $studentSchoolId,
|
||||
string $status,
|
||||
?string $reason,
|
||||
string $reported,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
string $date,
|
||||
int $classId,
|
||||
?int $userId = null
|
||||
): void {
|
||||
AttendanceData::query()->create([
|
||||
'class_id' => $classId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $studentSchoolId,
|
||||
'status' => strtolower($status),
|
||||
'reason' => $reason ?: null,
|
||||
'is_reported' => $reported,
|
||||
'is_notified' => 'no',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'date' => $date,
|
||||
'modified_by' => $userId,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function resolveStudentSchoolId(int $studentId, ?string $studentSchoolId = null): string
|
||||
{
|
||||
$studentSchoolId = trim((string)$studentSchoolId);
|
||||
|
||||
if ($studentSchoolId !== '') {
|
||||
return $studentSchoolId;
|
||||
}
|
||||
|
||||
$studentRow = $this->student->query()->select('school_id')->find($studentId);
|
||||
|
||||
if (!$studentRow || empty($studentRow->school_id)) {
|
||||
throw new RuntimeException('Student school ID not found.');
|
||||
}
|
||||
|
||||
return (string)$studentRow->school_id;
|
||||
}
|
||||
|
||||
public function saveOrUpdateStudentAttendance(array $payload): array
|
||||
{
|
||||
$classSectionId = (int)$payload['class_section_id'];
|
||||
$studentId = (int)$payload['student_id'];
|
||||
$studentSchoolId = $this->resolveStudentSchoolId($studentId, $payload['school_id'] ?? null);
|
||||
$status = strtolower((string)$payload['status']);
|
||||
$reason = $payload['reason'] ?? null;
|
||||
$reported = $payload['is_reported'] ?? null;
|
||||
$semester = (string)$payload['semester'];
|
||||
$schoolYear = (string)$payload['school_year'];
|
||||
$date = (string)$payload['date'];
|
||||
$classId = (int)$payload['class_id'];
|
||||
$userId = $payload['user_id'] ?? null;
|
||||
|
||||
$existing = AttendanceData::query()
|
||||
->where('student_id', $studentId)
|
||||
->whereDate('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$oldStatus = $existing->status;
|
||||
|
||||
$this->updateAttendanceData(
|
||||
(int)$existing->id,
|
||||
$status,
|
||||
$reason,
|
||||
$reported,
|
||||
$userId
|
||||
);
|
||||
|
||||
if ($status !== $oldStatus) {
|
||||
$this->attendanceRecordSyncService->updateAttendanceRecord(
|
||||
$studentId,
|
||||
$studentSchoolId,
|
||||
$status,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$oldStatus,
|
||||
$userId
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => 'updated',
|
||||
'attendance_id' => (int)$existing->id,
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => $status,
|
||||
'school_id' => $studentSchoolId,
|
||||
];
|
||||
}
|
||||
|
||||
$this->insertAttendanceData(
|
||||
$classSectionId,
|
||||
$studentId,
|
||||
$studentSchoolId,
|
||||
$status,
|
||||
$reason,
|
||||
$reported ?? 'no',
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$date,
|
||||
$classId,
|
||||
$userId
|
||||
);
|
||||
|
||||
$this->attendanceRecordSyncService->addNewAttendanceRecord(
|
||||
$classSectionId,
|
||||
$studentId,
|
||||
$studentSchoolId,
|
||||
$status,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$userId
|
||||
);
|
||||
|
||||
$created = AttendanceData::query()
|
||||
->where('student_id', $studentId)
|
||||
->whereDate('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'mode' => 'created',
|
||||
'attendance_id' => (int)($created->id ?? 0),
|
||||
'old_status' => null,
|
||||
'new_status' => $status,
|
||||
'school_id' => $studentSchoolId,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Attendance;
|
||||
|
||||
use App\Libraries\AttendanceAutoPublish;
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceDay;
|
||||
use App\Models\Student;
|
||||
use App\Models\TeacherClass;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class TeacherAttendanceSubmissionService
|
||||
{
|
||||
public function __construct(
|
||||
protected AttendanceDay $attendanceDay,
|
||||
protected AttendanceData $attendanceData,
|
||||
protected TeacherClass $teacherClass,
|
||||
protected Student $student,
|
||||
protected AttendancePolicyService $attendancePolicyService,
|
||||
protected StudentAttendanceWriterService $studentAttendanceWriterService,
|
||||
) {}
|
||||
|
||||
public function submit(
|
||||
Authenticatable $user,
|
||||
array $data,
|
||||
callable $detectExternalSubmission,
|
||||
callable $currentSemester,
|
||||
callable $currentSchoolYear
|
||||
): array {
|
||||
$rawSection = trim((string)$data['class_section_id']);
|
||||
|
||||
if ($rawSection === '') {
|
||||
throw new RuntimeException('Missing or invalid class section.');
|
||||
}
|
||||
|
||||
$sectionRow = DB::table('classSection')
|
||||
->select('id', 'class_id', 'class_section_id')
|
||||
->where('class_section_id', $rawSection)
|
||||
->first();
|
||||
|
||||
if (!$sectionRow && ctype_digit($rawSection)) {
|
||||
$sectionRow = DB::table('classSection')
|
||||
->select('id', 'class_id', 'class_section_id')
|
||||
->where('id', (int)$rawSection)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$sectionRow) {
|
||||
throw new RuntimeException('Invalid class section.');
|
||||
}
|
||||
|
||||
$classSectionId = (int)$sectionRow->class_section_id;
|
||||
$classId = (int)$sectionRow->class_id;
|
||||
$attendanceData = $data['attendance'] ?? [];
|
||||
$teachersData = $data['teachers'] ?? [];
|
||||
|
||||
if (empty($attendanceData) || !is_array($attendanceData)) {
|
||||
throw new RuntimeException('No student attendance data provided.');
|
||||
}
|
||||
|
||||
if (empty($teachersData) || !is_array($teachersData)) {
|
||||
throw new RuntimeException('No teacher attendance data provided.');
|
||||
}
|
||||
|
||||
$rawDate = $data['date'] ?? now()->toDateString();
|
||||
$attendDate = Carbon::parse($rawDate)->toDateString();
|
||||
$semester = (string)$currentSemester();
|
||||
$schoolYear = (string)$currentSchoolYear();
|
||||
|
||||
$dayRow = AttendanceDay::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->whereDate('date', $attendDate)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$currentUser = [
|
||||
'id' => $user->id,
|
||||
'roles' => method_exists($user, 'roles') ? $user->roles->pluck('name')->all() : [],
|
||||
'permissions' => method_exists($user, 'permissions') ? $user->permissions->pluck('name')->all() : [],
|
||||
];
|
||||
|
||||
$dayPolicyRow = $dayRow?->toArray() ?? [
|
||||
'status' => 'draft',
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $attendDate,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
];
|
||||
|
||||
if (!$this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) {
|
||||
throw new RuntimeException('Attendance is locked for your role.');
|
||||
}
|
||||
|
||||
$existingRows = AttendanceData::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->whereDate('date', $attendDate)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
|
||||
$existingByStudent = [];
|
||||
foreach ($existingRows as $row) {
|
||||
$existingByStudent[(int)$row->student_id] = $row->toArray();
|
||||
}
|
||||
|
||||
$allowedStatuses = ['present', 'absent', 'late'];
|
||||
$assignedTeachers = $this->teacherClass->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
|
||||
|
||||
foreach ($assignedTeachers as $teacher) {
|
||||
$teacherId = (int)$teacher['teacher_id'];
|
||||
$status = strtolower((string)($teachersData[$teacherId]['status'] ?? ''));
|
||||
|
||||
if (!in_array($status, $allowedStatuses, true)) {
|
||||
throw new RuntimeException('Please fill teacher attendance for all assigned teachers.');
|
||||
}
|
||||
}
|
||||
|
||||
$isTeacher = $this->attendancePolicyService->isTeacher($currentUser['roles']);
|
||||
|
||||
DB::transaction(function () use (
|
||||
$user,
|
||||
$assignedTeachers,
|
||||
$teachersData,
|
||||
$attendanceData,
|
||||
$existingByStudent,
|
||||
$classSectionId,
|
||||
$classId,
|
||||
$attendDate,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$isTeacher,
|
||||
$detectExternalSubmission
|
||||
) {
|
||||
$dayRow = AttendanceDay::query()->firstOrCreate(
|
||||
[
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $attendDate,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
],
|
||||
[
|
||||
'status' => 'draft',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($assignedTeachers as $teacher) {
|
||||
$teacherId = (int)$teacher['teacher_id'];
|
||||
$position = strtolower((string)($teacher['position'] ?? 'main'));
|
||||
$position = $position === 'ta' ? 'ta' : 'main';
|
||||
|
||||
DB::table('staff_attendance')->updateOrInsert(
|
||||
[
|
||||
'user_id' => $teacherId,
|
||||
'date' => $attendDate,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
],
|
||||
[
|
||||
'role_name' => 'Teacher',
|
||||
'class_section_id' => $classSectionId,
|
||||
'position' => $position,
|
||||
'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')),
|
||||
'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($attendanceData as $row) {
|
||||
$studentId = (int)($row['student_id'] ?? 0);
|
||||
$status = strtolower(trim((string)($row['status'] ?? '')));
|
||||
$reason = trim((string)($row['reason'] ?? ''));
|
||||
$existing = $existingByStudent[$studentId] ?? null;
|
||||
|
||||
if ($isTeacher && $existing) {
|
||||
$lockInfo = $detectExternalSubmission($existing);
|
||||
if (($lockInfo['locked'] ?? false) === true) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->studentAttendanceWriterService->saveOrUpdateStudentAttendance([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $row['school_id'] ?? null,
|
||||
'status' => $status,
|
||||
'reason' => $reason,
|
||||
'is_reported' => 'no',
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'date' => $attendDate,
|
||||
'class_id' => $classId,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$autoPublishAt = AttendanceAutoPublish::secondSundayAfterEndOfDay($attendDate);
|
||||
|
||||
$dayRow->update([
|
||||
'status' => 'submitted',
|
||||
'submitted_by' => $user->id,
|
||||
'submitted_at' => now(),
|
||||
'auto_publish_at' => $autoPublishAt,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'Attendance submitted successfully.',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user