Files
2026-06-11 11:46:12 -04:00

279 lines
9.3 KiB
PHP

<?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;
}
}