156 lines
5.8 KiB
PHP
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;
|
|
}
|
|
}
|