add more controller
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceRecord;
|
||||
use App\Models\ParentAttendanceReport;
|
||||
use App\Models\Student;
|
||||
use App\Models\ClassSection;
|
||||
use App\Services\SemesterRangeService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ParentAttendanceReportService
|
||||
{
|
||||
public function __construct(
|
||||
private ParentConfigService $configService,
|
||||
private ParentAttendanceReportCalendarService $calendarService,
|
||||
private SemesterRangeService $semesterRangeService
|
||||
) {
|
||||
}
|
||||
|
||||
public function formData(int $parentId): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$students = Student::query()
|
||||
->where('parent_id', $parentId)
|
||||
->orderBy('firstname')
|
||||
->orderBy('lastname')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
[$sundays, $defaultDate] = $this->calendarService->computeSundays();
|
||||
|
||||
$today = now()->toDateString();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$previewRows = DB::table('parent_attendance_reports as par')
|
||||
->select('par.*', 's.firstname', 's.lastname')
|
||||
->leftJoin('students as s', 's.id', '=', 'par.student_id')
|
||||
->where('par.parent_id', $parentId)
|
||||
->where('par.school_year', $schoolYear)
|
||||
->where('par.report_date', '>=', $today)
|
||||
->orderBy('par.report_date')
|
||||
->orderBy('s.firstname')
|
||||
->orderBy('s.lastname')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$cutoffThreshold = $this->calendarService->normalizeCutoffTime(
|
||||
(string) ($context['parent_report_cutoff'] ?? ''),
|
||||
'09:00'
|
||||
);
|
||||
|
||||
return [
|
||||
'students' => $students,
|
||||
'today' => $today,
|
||||
'sundays' => $sundays,
|
||||
'defaultDate' => $defaultDate,
|
||||
'myReports' => $previewRows,
|
||||
'cutoffThreshold' => $cutoffThreshold,
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(int $parentId, array $payload): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$semester = (string) ($context['semester'] ?? '');
|
||||
|
||||
$studentIds = array_values(array_filter(array_map('intval', (array) ($payload['student_ids'] ?? []))));
|
||||
if (empty($studentIds)) {
|
||||
throw new \RuntimeException('At least select one student.');
|
||||
}
|
||||
|
||||
$datesInput = $payload['dates'] ?? ($payload['date'] ?? null);
|
||||
$rawDates = [];
|
||||
if (is_array($datesInput)) {
|
||||
$rawDates = $datesInput;
|
||||
} elseif ($datesInput !== null) {
|
||||
$rawDates = [$datesInput];
|
||||
}
|
||||
$rawDates = array_values(array_unique(array_filter(array_map(static function ($val) {
|
||||
return substr((string) $val, 0, 10);
|
||||
}, $rawDates))));
|
||||
|
||||
if (empty($rawDates)) {
|
||||
throw new \RuntimeException('Please select at least one Sunday date.');
|
||||
}
|
||||
|
||||
$type = (string) ($payload['type'] ?? '');
|
||||
if (!in_array($type, ['absent', 'late', 'early_dismissal'], true)) {
|
||||
throw new \RuntimeException('Invalid report type.');
|
||||
}
|
||||
|
||||
$arrivalTime = $payload['arrival_time'] ?? null;
|
||||
$dismissTime = $payload['dismiss_time'] ?? null;
|
||||
$reason = isset($payload['reason']) ? trim((string) $payload['reason']) : null;
|
||||
|
||||
$todayCheck = new \DateTime('today');
|
||||
$cap = $this->calendarService->firstSundayOfJune($schoolYear);
|
||||
$validDates = [];
|
||||
|
||||
foreach ($rawDates as $entry) {
|
||||
$dt = \DateTime::createFromFormat('Y-m-d', $entry);
|
||||
if (!$dt) {
|
||||
throw new \RuntimeException('Invalid date selected: ' . $entry);
|
||||
}
|
||||
if ((int) $dt->format('w') !== 0) {
|
||||
throw new \RuntimeException('Please select Sunday dates only.');
|
||||
}
|
||||
if ($dt < $todayCheck) {
|
||||
throw new \RuntimeException('Past dates are not allowed. Please select today or a future Sunday.');
|
||||
}
|
||||
if ($dt > $cap) {
|
||||
throw new \RuntimeException('The last available date is the first Sunday of June.');
|
||||
}
|
||||
$validDates[$entry] = $dt;
|
||||
}
|
||||
|
||||
if ($type === 'late' && (!is_string($arrivalTime) || $arrivalTime === '')) {
|
||||
throw new \RuntimeException('Please provide an expected arrival time for late reporting.');
|
||||
}
|
||||
if ($type === 'early_dismissal' && (!is_string($dismissTime) || $dismissTime === '')) {
|
||||
throw new \RuntimeException('Please provide a dismissal time for early dismissal.');
|
||||
}
|
||||
if ($type !== 'early_dismissal') {
|
||||
if ($reason === null || $reason === '') {
|
||||
throw new \RuntimeException('Please provide a reason for absence or late arrival.');
|
||||
}
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
$blocked = [];
|
||||
$successDetails = [];
|
||||
|
||||
foreach ($studentIds as $sid) {
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$sectionRow = DB::table('student_class')
|
||||
->select('class_section_id')
|
||||
->where('student_id', $sid)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('updated_at')
|
||||
->first();
|
||||
$classSectionId = $sectionRow->class_section_id ?? null;
|
||||
$student = Student::query()->select('firstname', 'lastname')->find($sid);
|
||||
$studentName = $student ? trim($student->firstname . ' ' . $student->lastname) : ('Student #' . $sid);
|
||||
$classLabel = $this->resolveClassSectionLabel($classSectionId);
|
||||
|
||||
foreach ($validDates as $reportDate => $_dt) {
|
||||
$semesterForDate = $this->semesterRangeService->getSemesterForDate($reportDate) ?: $semester;
|
||||
$saved = false;
|
||||
|
||||
$qb = ParentAttendanceReport::query()
|
||||
->where('student_id', $sid)
|
||||
->where('report_date', $reportDate);
|
||||
|
||||
if ($type === 'early_dismissal') {
|
||||
$existingAL = (clone $qb)
|
||||
->whereIn('type', ['absent', 'late'])
|
||||
->first();
|
||||
if ($existingAL) {
|
||||
$blocked[] = $studentName . ' (' . $reportDate . ')';
|
||||
} else {
|
||||
$existingED = (clone $qb)->where('type', 'early_dismissal')->first();
|
||||
if ($existingED) {
|
||||
$existingED->update([
|
||||
'dismiss_time' => $dismissTime ?: $existingED->dismiss_time,
|
||||
'status' => 'new',
|
||||
]);
|
||||
$saved = true;
|
||||
$inserted++;
|
||||
} else {
|
||||
ParentAttendanceReport::query()->create([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $sid,
|
||||
'class_section_id' => $classSectionId,
|
||||
'report_date' => $reportDate,
|
||||
'type' => 'early_dismissal',
|
||||
'dismiss_time' => $dismissTime ?: null,
|
||||
'reason' => $reason ?: null,
|
||||
'semester' => $semesterForDate,
|
||||
'school_year' => $schoolYear,
|
||||
'status' => 'new',
|
||||
]);
|
||||
$saved = true;
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$existingAny = (clone $qb)
|
||||
->whereIn('type', ['absent', 'late', 'early_dismissal'])
|
||||
->first();
|
||||
if ($existingAny) {
|
||||
$blocked[] = $studentName . ' (' . $reportDate . ')';
|
||||
} else {
|
||||
ParentAttendanceReport::query()->create([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $sid,
|
||||
'class_section_id' => $classSectionId,
|
||||
'report_date' => $reportDate,
|
||||
'type' => $type,
|
||||
'arrival_time' => ($type === 'late') ? ($arrivalTime ?: null) : null,
|
||||
'reason' => $reason ?: null,
|
||||
'semester' => $semesterForDate,
|
||||
'school_year' => $schoolYear,
|
||||
'status' => 'new',
|
||||
]);
|
||||
$saved = true;
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($saved && in_array($type, ['absent', 'late'], true)) {
|
||||
$this->reflectToAttendanceData(
|
||||
studentId: $sid,
|
||||
reportDate: $reportDate,
|
||||
type: $type,
|
||||
semester: $semesterForDate,
|
||||
schoolYear: $schoolYear,
|
||||
classSectionId: $classSectionId,
|
||||
reason: $reason,
|
||||
arrivalTime: $arrivalTime
|
||||
);
|
||||
}
|
||||
|
||||
if ($saved) {
|
||||
$successDetails[] = [
|
||||
'student_id' => $sid,
|
||||
'name' => $studentName,
|
||||
'class_label' => $classLabel,
|
||||
'type' => $type,
|
||||
'arrival_time' => $arrivalTime,
|
||||
'dismiss_time' => $dismissTime,
|
||||
'date' => $reportDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($inserted <= 0) {
|
||||
$err = !empty($blocked)
|
||||
? ('Already submitted and cannot be changed for: ' . implode(', ', array_slice($blocked, 0, 5)) . (count($blocked) > 5 ? '…' : ''))
|
||||
: 'Could not save your report. Please try again.';
|
||||
throw new \RuntimeException($err);
|
||||
}
|
||||
|
||||
return [
|
||||
'inserted' => $inserted,
|
||||
'blocked' => $blocked,
|
||||
'students' => $successDetails,
|
||||
'dates' => array_keys($validDates),
|
||||
];
|
||||
}
|
||||
|
||||
public function listReports(string $start, ?string $end, ?string $schoolYear, ?string $semester): array
|
||||
{
|
||||
return ParentAttendanceReport::listForDateRange($start, $end, $schoolYear, $semester);
|
||||
}
|
||||
|
||||
public function checkExisting(array $payload): array
|
||||
{
|
||||
$datesPost = $payload['dates'] ?? null;
|
||||
$dateSingle = $payload['date'] ?? null;
|
||||
$dateValues = [];
|
||||
if (is_array($datesPost)) {
|
||||
foreach ($datesPost as $d) {
|
||||
$dateValues[] = substr((string) $d, 0, 10);
|
||||
}
|
||||
} elseif ($dateSingle !== null) {
|
||||
$dateValues[] = substr((string) $dateSingle, 0, 10);
|
||||
}
|
||||
$dateValues = array_values(array_unique(array_filter($dateValues, static function ($d) {
|
||||
return preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $d);
|
||||
})));
|
||||
|
||||
$type = (string) ($payload['type'] ?? '');
|
||||
if (empty($dateValues) || !in_array($type, ['absent', 'late', 'early_dismissal'], true)) {
|
||||
throw new \RuntimeException('Invalid date or type.');
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_unique(array_map('intval', (array) ($payload['student_ids'] ?? []))));
|
||||
$studentIds = array_filter($studentIds, static fn ($v) => $v > 0);
|
||||
if (empty($studentIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = DB::table('parent_attendance_reports as par')
|
||||
->select('par.student_id', 'par.type', 'par.report_date', 's.firstname', 's.lastname')
|
||||
->leftJoin('students as s', 's.id', '=', 'par.student_id')
|
||||
->whereIn('par.report_date', $dateValues)
|
||||
->whereIn('par.student_id', $studentIds)
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$sid = (int) ($row['student_id'] ?? 0);
|
||||
if ($sid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($map[$sid])) {
|
||||
$map[$sid] = [
|
||||
'student_id' => $sid,
|
||||
'firstname' => (string) ($row['firstname'] ?? ''),
|
||||
'lastname' => (string) ($row['lastname'] ?? ''),
|
||||
'dates' => [],
|
||||
];
|
||||
}
|
||||
$dVal = substr((string) ($row['report_date'] ?? ''), 0, 10);
|
||||
if ($dVal === '') {
|
||||
continue;
|
||||
}
|
||||
$map[$sid]['dates'][$dVal] = $map[$sid]['dates'][$dVal] ?? [];
|
||||
$t = (string) ($row['type'] ?? '');
|
||||
if ($t !== '' && !in_array($t, $map[$sid]['dates'][$dVal], true)) {
|
||||
$map[$sid]['dates'][$dVal][] = $t;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($map);
|
||||
}
|
||||
|
||||
public function updateReport(int $parentId, int $reportId, array $payload): void
|
||||
{
|
||||
$row = ParentAttendanceReport::query()->find($reportId);
|
||||
if (!$row || (int) $row->parent_id !== $parentId) {
|
||||
throw new \RuntimeException('Report not found.');
|
||||
}
|
||||
|
||||
if ((string) ($row->status ?? '') !== 'new') {
|
||||
throw new \RuntimeException('This report can no longer be edited.');
|
||||
}
|
||||
|
||||
$tzName = (string) (config('School')->attendance['timezone'] ?? 'UTC');
|
||||
$tz = new \DateTimeZone($tzName ?: 'UTC');
|
||||
$cutoff = new \DateTime($row->report_date . ' 09:00:00', $tz);
|
||||
$now = new \DateTime('now', $tz);
|
||||
if ($now >= $cutoff) {
|
||||
throw new \RuntimeException('Editing window closed at 9:00 AM on the report date.');
|
||||
}
|
||||
|
||||
$type = (string) $row->type;
|
||||
$reason = trim((string) ($payload['reason'] ?? ''));
|
||||
$arrival = (string) ($payload['arrival_time'] ?? '');
|
||||
$dismiss = (string) ($payload['dismiss_time'] ?? '');
|
||||
|
||||
$update = [
|
||||
'reason' => $reason !== '' ? $reason : null,
|
||||
];
|
||||
|
||||
if ($type === 'late' && $arrival !== '') {
|
||||
if (!preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $arrival)) {
|
||||
throw new \RuntimeException('Invalid arrival time format.');
|
||||
}
|
||||
$update['arrival_time'] = $arrival;
|
||||
}
|
||||
if ($type === 'early_dismissal' && $dismiss !== '') {
|
||||
if (!preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $dismiss)) {
|
||||
throw new \RuntimeException('Invalid dismissal time format.');
|
||||
}
|
||||
$update['dismiss_time'] = $dismiss;
|
||||
}
|
||||
|
||||
if (empty($update)) {
|
||||
throw new \RuntimeException('Nothing to update.');
|
||||
}
|
||||
|
||||
$row->update($update);
|
||||
}
|
||||
|
||||
private function resolveClassSectionLabel(?int $classSectionId): string
|
||||
{
|
||||
$cid = (int) ($classSectionId ?? 0);
|
||||
if ($cid <= 0) {
|
||||
return 'Not Assigned';
|
||||
}
|
||||
|
||||
$name = ClassSection::query()
|
||||
->where('class_section_id', $cid)
|
||||
->value('class_section_name');
|
||||
|
||||
return $name ?: 'Not Assigned';
|
||||
}
|
||||
|
||||
private function reflectToAttendanceData(
|
||||
int $studentId,
|
||||
string $reportDate,
|
||||
string $type,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId,
|
||||
?string $reason = null,
|
||||
?string $arrivalTime = null
|
||||
): void {
|
||||
$status = ($type === 'late') ? 'late' : (($type === 'absent') ? 'absent' : null);
|
||||
if ($status === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$classSectionId) {
|
||||
$row = DB::table('student_class')
|
||||
->select('class_section_id')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('updated_at')
|
||||
->first();
|
||||
$classSectionId = (int) ($row->class_section_id ?? 0);
|
||||
}
|
||||
if ($classSectionId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = AttendanceData::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('date', $reportDate)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
$reasonParts = ['Parent-reported'];
|
||||
if ($type === 'late' && $arrivalTime) {
|
||||
$reasonParts[] = 'ETA ' . substr($arrivalTime, 0, 5);
|
||||
}
|
||||
if (!empty($reason)) {
|
||||
$reasonParts[] = trim($reason);
|
||||
}
|
||||
$reasonText = implode(' — ', $reasonParts);
|
||||
|
||||
if (!$existing) {
|
||||
$classId = ClassSection::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->value('class_id');
|
||||
$schoolId = Student::query()->whereKey($studentId)->value('school_id');
|
||||
if (!$classId || !$schoolId) {
|
||||
return;
|
||||
}
|
||||
AttendanceData::query()->create([
|
||||
'class_id' => $classId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $schoolId,
|
||||
'date' => $reportDate,
|
||||
'status' => $status,
|
||||
'reason' => $reasonText,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
$this->bumpAttendanceRecord($studentId, $classSectionId, $schoolId, $status, $semester, $schoolYear);
|
||||
return;
|
||||
}
|
||||
|
||||
$existingStatus = strtolower((string) ($existing->status ?? ''));
|
||||
if ($existingStatus === 'present' || $existingStatus === '') {
|
||||
$existing->update([
|
||||
'status' => $status,
|
||||
'reason' => $existing->reason ?: $reasonText,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function bumpAttendanceRecord(
|
||||
int $studentId,
|
||||
int $classSectionId,
|
||||
string $schoolId,
|
||||
string $status,
|
||||
string $semester,
|
||||
string $schoolYear
|
||||
): void {
|
||||
$record = AttendanceRecord::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
if ($record) {
|
||||
$updates = [
|
||||
'total_presence' => (int) ($record->total_presence ?? 0),
|
||||
'total_absence' => (int) ($record->total_absence ?? 0),
|
||||
'total_late' => (int) ($record->total_late ?? 0),
|
||||
'total_attendance' => (int) ($record->total_attendance ?? 0) + 1,
|
||||
];
|
||||
if ($status === 'present') {
|
||||
$updates['total_presence']++;
|
||||
} elseif ($status === 'absent') {
|
||||
$updates['total_absence']++;
|
||||
} elseif ($status === 'late') {
|
||||
$updates['total_late']++;
|
||||
}
|
||||
$record->update($updates);
|
||||
return;
|
||||
}
|
||||
|
||||
AttendanceRecord::query()->create([
|
||||
'class_section_id' => $classSectionId,
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $schoolId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'total_presence' => ($status === 'present') ? 1 : 0,
|
||||
'total_absence' => ($status === 'absent') ? 1 : 0,
|
||||
'total_late' => ($status === 'late') ? 1 : 0,
|
||||
'total_attendance' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user