Files
alrahma_sunday_school/app/Services/Calculators/AttendanceCalculator.php
T
2026-02-10 22:11:06 -05:00

156 lines
5.8 KiB
PHP

<?php
namespace App\Services\Calculators;
use App\Interfaces\ScoreCalculatorInterface;
use App\Models\AttendanceRecordModel;
use App\Models\ConfigurationModel;
use App\Models\CalendarModel;
use RuntimeException;
class AttendanceCalculator implements ScoreCalculatorInterface
{
private $attendanceModel;
private $configModel;
private $calendarModel;
public function __construct(
AttendanceRecordModel $attendanceModel,
ConfigurationModel $configModel,
CalendarModel $calendarModel
) {
$this->attendanceModel = $attendanceModel;
$this->configModel = $configModel;
$this->calendarModel = $calendarModel;
}
public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array
{
$absences = $this->attendanceModel->getTotalAbsences($studentId, $semester, $schoolYear);
$totalDays = null;
$normSemester = $this->normalizeSemester($semester);
if ($schoolYear !== '' && $normSemester !== '') {
$semRange = $this->getSemesterRange($schoolYear, $normSemester);
if ($semRange) {
try {
$events = $this->calendarModel->getEventsBySchoolYearAndSemester($schoolYear, $normSemester);
$noSchoolSundays = 0;
foreach ($events as $event) {
if (!empty($event['no_school'])) {
$date = new \DateTime($event['date']);
if ($date->format('N') == 7) {
if ($date->format('Y-m-d') >= $semRange[0] && $date->format('Y-m-d') <= $semRange[1]) {
$noSchoolSundays++;
}
}
}
}
$totalDays = $this->countSundays($semRange[0], $semRange[1]) - $noSchoolSundays;
} catch (\Throwable $e) {
log_message('error', 'Failed to count sundays for attendance score: ' . $e->getMessage());
$totalDays = null;
}
}
}
if ($totalDays === null) {
$totalDays = $this->getConfiguredSemesterLength($semester);
} else {
$configuredDays = $this->getConfiguredSemesterLength($semester);
if ($configuredDays !== null) {
$totalDays = $configuredDays;
}
}
if ($absences === null || $totalDays === null || $totalDays < 0) {
throw new RuntimeException("Invalid attendance data: could not determine total days or absences for student {$studentId}.");
}
if ($totalDays == 0) {
return ['attendance_score' => 100.0];
}
$attendanceRatio = ($totalDays + 1 - $absences) / $totalDays;
$score = min(100, max(0, $attendanceRatio * 100));
return ['attendance_score' => round($score, 2)];
}
protected function normalizeSemester(?string $input): string
{
$s = strtolower(trim((string)$input));
if ($s === 'fall' || str_contains($s, '1')) return 'fall';
if ($s === 'spring' || str_contains($s, '2')) return 'spring';
return '';
}
protected function getSemesterRange(string $schoolYear, string $semester): ?array
{
$norm = $this->normalizeSemester($semester);
if ($norm === '' || !preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $m)) {
// Fallback for single year format
if ($norm === '' || !preg_match('/^(\d{4})/', trim($schoolYear), $m)) {
return null;
}
$y1 = (int)$m[1];
$y2 = $y1 + 1;
} else {
$y1 = (int)$m[1];
$y2 = (int)$m[2];
}
$fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? '');
$fallEndCfg = (string)($this->configModel->getConfig('fall_end_date') ?? '');
$springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? '');
$springEndCfg = (string)($this->configModel->getConfig('last_school_day') ?? '');
if ($norm === 'fall') {
$start = ($fallStartCfg !== '') ? sprintf('%04d-%s', $y1, date('m-d', strtotime($fallStartCfg))) : "{$y1}-09-01";
$end = ($fallEndCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($fallEndCfg))) : "{$y2}-01-15";
return [$start, $end];
}
if ($norm === 'spring') {
$start = ($springStartCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($springStartCfg))) : "{$y2}-01-16";
$end = ($springEndCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($springEndCfg))) : "{$y2}-06-30";
return [$start, $end];
}
return null;
}
private function countSundays(string $startDate, string $endDate): int
{
$start = new \DateTime($startDate);
$end = new \DateTime($endDate);
$sundays = 0;
$interval = new \DateInterval('P1D');
$period = new \DatePeriod($start, $interval, $end->modify('+1 day')); // include end date
foreach ($period as $day) {
if ($day->format('N') == 7) {
$sundays++;
}
}
return $sundays;
}
private function getConfiguredSemesterLength(string $semester): ?int
{
$norm = $this->normalizeSemester($semester);
if ($norm === '') {
return null;
}
$configKey = $norm === 'fall' ? 'total_semester1_days' : 'total_semester2_days';
$value = $this->configModel->getConfig($configKey);
if (!is_numeric($value)) {
return null;
}
$length = (int) $value;
return $length > 0 ? $length : null;
}
}