Files
alrahma_sunday_school_api/app/Http/Controllers/Api/TeacherController.php
T
2026-03-05 12:29:37 -05:00

749 lines
29 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Api;
use App\Models\Configuration;
use App\Models\TeacherClass;
use App\Models\User;
use App\Models\Teacher;
use App\Models\Student;
use App\Models\ClassSection;
use App\Models\StudentClass;
use App\Models\StudentMedicalCondition;
use App\Models\StudentAllergy;
use App\Models\StaffAttendance;
use App\Services\EmailService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class TeacherController extends BaseApiController
{
protected User $user;
protected TeacherClass $teacherClass;
protected Configuration $config;
protected Teacher $teacher;
protected Student $student;
protected ClassSection $classSection;
protected StudentClass $studentClass;
protected StudentMedicalCondition $medical;
protected StudentAllergy $allergy;
protected StaffAttendance $staffAttendance;
protected EmailService $emailService;
protected string $semester;
protected string $schoolYear;
public function __construct()
{
parent::__construct();
$this->user = model(User::class);
$this->teacherClass = model(TeacherClass::class);
$this->config = model(Configuration::class);
$this->teacher = model(Teacher::class);
$this->student = model(Student::class);
$this->classSection = model(ClassSection::class);
$this->studentClass = model(StudentClass::class);
$this->medical = model(StudentMedicalCondition::class);
$this->allergy = model(StudentAllergy::class);
$this->staffAttendance = model(StaffAttendance::class);
$this->emailService = app(EmailService::class);
$this->semester = (string) ($this->config->getConfig('semester') ?? '');
$this->schoolYear = (string) ($this->config->getConfig('school_year') ?? '');
}
public function index()
{
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
$perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20)));
$query = $this->user->newQuery()
->select('users.*')
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->whereIn('LOWER(roles.name)', ['teacher', 'teacher_assistant'])
->groupBy('users.id')
->orderBy('users.lastname', 'ASC');
$result = $this->paginate($query, $page, $perPage);
foreach ($result['data'] as &$teacher) {
unset($teacher['password'], $teacher['token']);
$assignments = $this->teacherClass->newQuery()
->where('teacher_id', $teacher['id'])
->where('school_year', $this->schoolYear)
->where('semester', $this->semester)
->get()
->toArray();
$teacher['class_assignments'] = $assignments;
}
return $this->success($result, 'Teachers retrieved successfully');
}
public function show($id = null)
{
$teacher = $this->user->newQuery()
->select('users.*')
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->whereIn('LOWER(roles.name)', ['teacher', 'teacher_assistant'])
->where('users.id', $id)
->first();
if (!$teacher) {
return $this->respondError('Teacher not found', Response::HTTP_NOT_FOUND);
}
$teacherArray = $teacher->toArray();
unset($teacherArray['password'], $teacherArray['token']);
$assignments = $this->teacherClass->newQuery()
->where('teacher_id', $id)
->where('school_year', $this->schoolYear)
->where('semester', $this->semester)
->get()
->toArray();
$teacherArray['class_assignments'] = $assignments;
return $this->success($teacherArray, 'Teacher retrieved successfully');
}
public function getClasses($id = null)
{
$assignments = $this->teacherClass->newQuery()
->select([
'tc.*',
'cs.class_section_name',
'c.name as class_name',
])
->from('teacher_class as tc')
->leftJoin('class_sections as cs', 'cs.class_section_id', '=', 'tc.class_section_id')
->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
->where('tc.teacher_id', $id)
->where('tc.school_year', $this->schoolYear)
->where('tc.semester', $this->semester)
->orderBy('cs.class_section_name', 'ASC')
->get()
->toArray();
return $this->success($assignments, 'Teacher classes retrieved successfully');
}
/**
* GET /api/v1/teachers/class-view
* Get class view data for the logged-in teacher
*/
public function getClassView()
{
try {
$userId = $this->getCurrentUserId();
if (!$userId) {
return $this->respondError('User not logged in', Response::HTTP_UNAUTHORIZED);
}
// Fetch all assignments (as main or TA)
$classes = $this->teacherClass->getClassAssignmentsByUserId($userId, $this->schoolYear);
if (empty($classes)) {
return $this->success([
'has_classes' => false,
'message' => 'You do not have an assigned class yet. Please contact the administration.',
], 'No classes found');
}
// Identify user and their role
$user = $this->user->find($userId);
if (!$user) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
$roles = DB::table('user_roles ur')
->select('r.name')
->join('roles r', 'r.id', '=', 'ur.role_id')
->where('ur.user_id', $userId)
->get()
->map(fn($r) => strtolower($r->name))
->toArray();
$roleName = 'teacher';
if (in_array('teacher', $roles)) {
$roleName = 'teacher';
} elseif (in_array('teacher_assistant', $roles)) {
$roleName = 'teacher_assistant';
} else {
return $this->respondError('Access denied', Response::HTTP_FORBIDDEN);
}
// Collect class section IDs
$classSectionIds = array_column($classes, 'class_section_id');
$studentsData = [];
if (!empty($classSectionIds)) {
// Fetch all students for the selected class sections
$students = $this->studentClass->getStudentsByClassSectionIds($classSectionIds);
if (!empty($students)) {
// Collect unique student IDs
$studentIds = array_values(array_unique(array_map(
static fn(array $row) => (int) ($row['student_id'] ?? 0),
$students
)));
$studentIds = array_values(array_filter($studentIds));
// Bulk fetch medical conditions & allergies grouped by student_id
$conditionsByStudent = !empty($studentIds)
? $this->medical->getMedicalByStudentIds($studentIds)
: [];
$allergiesByStudent = !empty($studentIds)
? $this->allergy->getAllergiesByStudentIds($studentIds)
: [];
// Attach medical/allergy data to each student
foreach ($students as $student) {
$sid = (int) ($student['student_id'] ?? 0);
$cid = (int) ($student['class_section_id'] ?? 0);
$medList = array_column($conditionsByStudent[$sid] ?? [], 'condition_name');
$algList = array_column($allergiesByStudent[$sid] ?? [], 'allergy');
$student['medical_conditions'] = $medList;
$student['allergies'] = $algList;
$student['medical_conditions_text'] = implode(', ', $medList);
$student['allergies_text'] = implode(', ', $algList);
$studentsData[$cid][] = $student;
}
}
}
// Build assigned staff lists (all Main teachers and all TAs) per class section
$assignedNames = [];
if (!empty($classSectionIds)) {
$rows = DB::table('teacher_class tc')
->select('tc.class_section_id', 'tc.position', 'u.firstname', 'u.lastname')
->join('users u', 'u.id', '=', 'tc.teacher_id')
->whereIn('tc.class_section_id', $classSectionIds)
->where('tc.school_year', $this->schoolYear)
->where('tc.semester', $this->semester)
->orderBy('tc.class_section_id', 'ASC')
->orderByRaw("FIELD(tc.position,'main','ta')")
->orderBy('u.firstname', 'ASC')
->get();
foreach ($rows as $r) {
$sid = (int) ($r->class_section_id ?? 0);
$pos = strtolower((string) ($r->position ?? ''));
$name = trim(($r->firstname ?? '') . ' ' . ($r->lastname ?? ''));
if ($sid === 0 || $name === '') continue;
if (in_array($pos, ['main', 'teacher'], true)) {
$assignedNames[$sid]['mains'][] = $name;
} elseif (in_array($pos, ['ta', 'assistant', 'teacher_assistant'], true)) {
$assignedNames[$sid]['tas'][] = $name;
} else {
$assignedNames[$sid]['others'][] = $name;
}
}
}
$classSectionId = $classSectionIds[0] ?? null;
return $this->success([
'teacher_name' => trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')),
'role_name' => $roleName,
'classes' => $classes,
'students' => $studentsData[$classSectionId] ?? [],
'studentsData' => $studentsData,
'class_section_id' => $classSectionId,
'assignedNames' => $assignedNames,
], 'Class view data retrieved successfully');
} catch (\Exception $e) {
Log::error('Error in TeacherController::getClassView(): ' . $e->getMessage());
return $this->respondError('An error occurred. Please try again later.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/teachers/class-assignments
* Get teacher class assignment data for administration
*/
public function getTeacherClassAssignments()
{
$forYear = trim((string) ($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? ''));
$payload = $this->buildTeacherClassAssignmentPayload($forYear !== '' ? $forYear : null);
$payload['ok'] = true;
return $this->success($payload, 'Teacher class assignments retrieved successfully');
}
/**
* POST /api/v1/teachers/class-assignments/assign
* Assign a teacher to a class
*/
public function assignClassTeacher()
{
$data = $this->payloadData();
$result = $this->handleAssignClassTeacher($data);
$statusCode = $result['ok'] ? Response::HTTP_OK : Response::HTTP_UNPROCESSABLE_ENTITY;
return response()->json($result, $statusCode);
}
/**
* DELETE /api/v1/teachers/class-assignments
* Delete a teacher class assignment
*/
public function deleteAssignment()
{
$data = $this->payloadData();
$result = $this->handleDeleteAssignment($data);
$statusCode = $result['ok'] ? Response::HTTP_OK : Response::HTTP_UNPROCESSABLE_ENTITY;
return response()->json($result, $statusCode);
}
/**
* GET /api/v1/teachers/absence-form
* Get absence form data for the logged-in teacher
*/
public function getAbsenceForm()
{
$userId = $this->getCurrentUserId();
if (!$userId) {
return $this->respondError('Please log in first.', Response::HTTP_UNAUTHORIZED);
}
$teacher = $this->user->find($userId);
$displayName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : 'Unknown';
// Fetch existing self-reported staff attendance rows for this user (current term)
$existing = $this->staffAttendance
->where('user_id', $userId)
->where('semester', $this->semester)
->where('school_year', $this->schoolYear)
->orderBy('date', 'DESC')
->findAll();
return $this->success([
'teacher_name' => $displayName,
'semester' => $this->semester,
'schoolYear' => $this->schoolYear,
'existing' => $existing,
'availableDates' => $this->allowedAbsenceDates(),
], 'Absence form data retrieved successfully');
}
/**
* POST /api/v1/teachers/absence
* Submit absence/vacation request
*/
public function submitAbsence()
{
$userId = $this->getCurrentUserId();
if (!$userId) {
return $this->respondError('Please log in first.', Response::HTTP_UNAUTHORIZED);
}
if ($this->semester === '' || $this->schoolYear === '') {
return $this->respondError('Semester or school year not configured.', Response::HTTP_BAD_REQUEST);
}
$data = $this->payloadData();
$dates = (array) ($data['dates'] ?? []);
$reasonType = trim((string) ($data['reason_type'] ?? ''));
$reasonText = trim((string) ($data['reason'] ?? ''));
if ($reasonText === '') {
return $this->respondValidationError(['reason' => ['Reason is required.']]);
}
// Build reason string
$reasonBase = ($reasonType !== '') ? strtolower($reasonType) : '';
$reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;
// Enforce allowed Sundays list
$allowedDates = $this->allowedAbsenceDates();
$allowedSet = array_fill_keys($allowedDates, true);
$roleName = $this->user->getUserRole($userId) ?: null;
// Validate dates and save
$saved = 0;
$invalid = [];
$savedDates = [];
$dates = array_values(array_unique(array_map('strval', $dates)));
foreach ($dates as $d) {
$d = trim((string) $d);
if ($d === '') continue;
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', $d);
if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) {
$invalid[] = $d;
continue;
}
$ok = $this->staffAttendance->upsertOne(
userId: $userId,
roleName: $roleName,
date: $d,
semester: $this->semester,
schoolYear: $this->schoolYear,
status: StaffAttendance::STATUS_ABSENT,
reason: $reason,
editorId: $userId
);
if ($ok) {
$saved++;
$savedDates[] = $d;
}
}
if (!empty($invalid)) {
return $this->respondError('Invalid dates: ' . implode(', ', $invalid), Response::HTTP_BAD_REQUEST);
}
// Send notification email to principal
try {
$user = $this->user->find($userId) ?: [];
$fullName = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')) ?: 'Unknown';
$userEmail = $user['email'] ?? '';
$role = $roleName ?: 'staff';
$dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);
// If teacher/TA, include assigned class(es) for current term
$assignedText = '-';
if (in_array(strtolower((string) $roleName), ['teacher', 'teacher_assistant'], true)) {
try {
$assignments = $this->teacherClass->getClassAssignmentsByUserId($userId, $this->schoolYear, $this->semester);
if (!empty($assignments)) {
$parts = [];
foreach ($assignments as $as) {
$name = (string) ($as['class_section_name'] ?? '');
$r = (string) ($as['teacher_role'] ?? '');
if ($name !== '') {
$parts[] = $r ? ($name . ' (' . $r . ')') : $name;
}
}
if (!empty($parts)) {
$assignedText = implode(', ', $parts);
} else {
$assignedText = 'No assigned class';
}
} else {
$assignedText = 'No assigned class';
}
} catch (\Throwable $e) {
$assignedText = 'N/A';
}
}
$subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string) $role), $dateList ?: 'No dates');
$submittedAt = utc_now();
$body = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;">'
. '<h3 style="margin:0 0 8px;">New TimeOff Request</h3>'
. '<p style="margin:0 0 12px;">A staff time-off request was submitted from the teacher portal.</p>'
. '<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
. '<tr><td><strong>Name</strong></td><td>' . esc($fullName) . '</td></tr>'
. '<tr><td><strong>Email</strong></td><td>' . esc($userEmail) . '</td></tr>'
. '<tr><td><strong>Role</strong></td><td>' . esc((string) $role) . '</td></tr>'
. '<tr><td><strong>Semester</strong></td><td>' . esc($this->semester) . '</td></tr>'
. '<tr><td><strong>School Year</strong></td><td>' . esc($this->schoolYear) . '</td></tr>'
. '<tr><td><strong>Reason Type</strong></td><td>' . esc($reasonType ?: '-') . '</td></tr>'
. '<tr><td><strong>Reason</strong></td><td>' . esc($reasonText) . '</td></tr>'
. '<tr><td><strong>Dates</strong></td><td>' . esc($dateList ?: '-') . '</td></tr>'
. (in_array(strtolower((string) $roleName), ['teacher', 'teacher_assistant'], true)
? '<tr><td><strong>Assigned Class(es)</strong></td><td>' . esc($assignedText) . '</td></tr>'
: '')
. '<tr><td><strong>Submitted At</strong></td><td>' . esc($submittedAt) . '</td></tr>'
. '</table>'
. '</div>';
// Note: StaffTimeOffLinkService not found, skipping link generation
$principalEmail = env('PRINCIPAL_EMAIL') ?: 'principal@alrahmaisgl.org';
$this->emailService->send($principalEmail, $subject, $body);
} catch (\Throwable $e) {
Log::error('Failed to send TimeOff email: ' . $e->getMessage());
}
return $this->success([
'saved' => $saved,
'savedDates' => $savedDates,
], $saved . ' day(s) saved as absent.');
}
/**
* Build teacher class assignment payload
*/
private function buildTeacherClassAssignmentPayload(?string $forYear = null): array
{
$schoolYear = $forYear ?: $this->schoolYear;
$teachers = $this->teacher->getTeachersAndTAs();
// Prefer class sections for the selected year, fallback to all if none
$classSections = $this->classSection
->where('school_year', $schoolYear)
->orderBy('class_section_name', 'ASC')
->findAll();
if (empty($classSections)) {
$classSections = $this->classSection->orderBy('class_section_name', 'ASC')->findAll();
}
$classNames = [];
foreach ($classSections as $section) {
$sectionId = (int) ($section['class_section_id'] ?? $section['id'] ?? 0);
if ($sectionId <= 0) continue;
$classNames[$sectionId] = $section['class_section_name'] ?? ($section['name'] ?? 'N/A');
}
$assignments = $this->teacherClass
->where('school_year', $schoolYear)
->findAll();
$assignmentMap = [];
foreach ($assignments as $assign) {
$teacherId = (int) ($assign['teacher_id'] ?? 0);
$sectionId = (int) ($assign['class_section_id'] ?? 0);
if ($teacherId <= 0 || $sectionId <= 0) continue;
$role = strtolower((string) ($assign['position'] ?? ''));
if (!in_array($role, ['main', 'ta'], true)) continue;
if (!isset($assignmentMap[$teacherId])) {
$assignmentMap[$teacherId] = ['main' => [], 'ta' => []];
}
$assignmentMap[$teacherId][$role][] = [
'section_id' => $sectionId,
'class_section_name' => $classNames[$sectionId] ?? 'N/A',
'role' => $role,
'semester' => (string) ($assign['semester'] ?? $this->semester ?? ''),
'school_year' => (string) ($assign['school_year'] ?? $schoolYear),
];
}
$teacherData = [];
foreach ($teachers as $teacher) {
$tid = (int) ($teacher['id'] ?? 0);
if ($tid <= 0) continue;
$assigned = $assignmentMap[$tid] ?? ['main' => [], 'ta' => []];
$firstName = $teacher['firstname'] ?? '';
$lastName = $teacher['lastname'] ?? '';
$name = trim($firstName . ' ' . $lastName);
if ($name === '') {
$name = 'User#' . $tid;
}
$teacherData[] = [
'teacher_id' => $tid,
'firstname' => $firstName,
'lastname' => $lastName,
'name' => $name,
'email' => $teacher['email'] ?? '',
'cellphone' => $teacher['cellphone'] ?? '',
'role' => $teacher['role'] ?? '',
'school_year' => $this->schoolYear,
'main_assignments' => $assigned['main'],
'ta_assignments' => $assigned['ta'],
];
}
$classesPayload = [];
foreach ($classSections as $section) {
$sectionId = (int) ($section['class_section_id'] ?? $section['id'] ?? 0);
if ($sectionId <= 0) continue;
$classesPayload[] = [
'class_section_id' => $sectionId,
'class_section_name' => $section['class_section_name'] ?? ($section['name'] ?? 'N/A'),
];
}
return [
'school_year' => $schoolYear,
'teachers' => $teacherData,
'classes' => $classesPayload,
];
}
/**
* Handle assign class teacher logic
*/
private function handleAssignClassTeacher(array $input): array
{
$teacherId = (int) ($input['teacher_id'] ?? 0);
$classSectionId = (int) ($input['class_section_id'] ?? 0);
$teacherRole = $input['teacher_role'] ?? null;
$loggedInUserId = $this->getCurrentUserId() ?? 0;
$currentDateTime = utc_now();
if ($teacherId <= 0 || $classSectionId <= 0) {
return ['ok' => false, 'status' => 'error', 'message' => 'Missing required parameters for assignment.'];
}
if (empty($this->semester) || empty($this->schoolYear)) {
return ['ok' => false, 'status' => 'error', 'message' => 'Semester or school year not configured.'];
}
if ($teacherRole === null || $teacherRole === '') {
$teacherRole = $this->user->getUserRole($teacherId);
}
$isAssistant = strtolower((string) $teacherRole) === 'teacher_assistant';
$position = $isAssistant ? 'ta' : 'main';
$alreadyAssigned = $this->teacherClass->where([
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'position' => $position,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
])->first();
if ($alreadyAssigned) {
return ['ok' => false, 'status' => 'info', 'message' => 'This teacher is already assigned to this class with the same role.'];
}
$insertData = [
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'position' => $position,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
'created_at' => $currentDateTime,
'updated_at' => $currentDateTime,
'updated_by' => $loggedInUserId,
];
try {
$this->teacherClass->insert($insertData);
} catch (\Throwable $e) {
Log::error('assignClassTeacher failed: ' . $e->getMessage());
return ['ok' => false, 'status' => 'error', 'message' => 'Unable to assign class. Please try again.'];
}
return [
'ok' => true,
'status' => 'success',
'message' => 'Class assigned to ' . ($isAssistant ? 'Teacher Assistant' : 'Main Teacher') . ' successfully.',
'position' => $position,
];
}
/**
* Handle delete assignment logic
*/
private function handleDeleteAssignment(array $input): array
{
$teacherId = (int) ($input['teacher_id'] ?? 0);
$classSectionId = (int) ($input['class_section_id'] ?? 0);
$position = strtolower((string) ($input['position'] ?? ''));
if ($teacherId <= 0 || $classSectionId <= 0 || !in_array($position, ['main', 'ta'], true)) {
return ['ok' => false, 'status' => 'error', 'message' => 'Missing or invalid data for deletion.'];
}
if (empty($this->semester) || empty($this->schoolYear)) {
return ['ok' => false, 'status' => 'error', 'message' => 'Semester or school year not configured.'];
}
$requestedSemester = trim((string) ($input['semester'] ?? ''));
$requestedSchoolYear = trim((string) ($input['school_year'] ?? ''));
$semester = $requestedSemester !== '' ? $requestedSemester : $this->semester;
$schoolYear = $requestedSchoolYear !== '' ? $requestedSchoolYear : $this->schoolYear;
if ($semester === '' || $schoolYear === '') {
return ['ok' => false, 'status' => 'error', 'message' => 'Semester or school year not configured.'];
}
$assignment = $this->teacherClass
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->where('position', $position)
->first();
if (!$assignment) {
return ['ok' => false, 'status' => 'error', 'message' => 'Assignment not found.'];
}
try {
$this->teacherClass->delete($assignment['id']);
} catch (\Throwable $e) {
Log::error('deleteAssignment failed: ' . $e->getMessage());
return ['ok' => false, 'status' => 'error', 'message' => 'Unable to remove assignment. Please try again.'];
}
return [
'ok' => true,
'status' => 'success',
'message' => ucfirst($position) . ' assignment removed successfully.',
];
}
/**
* Compute all allowed absence dates (Sundays) for the configured school year
*/
private function allowedAbsenceDates(): array
{
$todayStr = local_date(utc_now(), 'Y-m-d');
try {
$today = new \DateTimeImmutable($todayStr);
} catch (\Throwable $e) {
$today = new \DateTimeImmutable();
}
$sy = $this->schoolYear;
$startYear = null;
$endYear = null;
if (preg_match('/^(\d{4})\D+(\d{4})$/', $sy, $m)) {
$startYear = (int) $m[1];
$endYear = (int) $m[2];
} else {
// Fallback based on current date
$cy = (int) date('Y');
$cm = (int) date('n');
if ($cm >= 9) { // Sep-Dec
$startYear = $cy;
$endYear = $cy + 1;
} else { // Jan-Aug
$startYear = $cy - 1;
$endYear = $cy;
}
}
try {
$start = new \DateTimeImmutable(sprintf('%04d-09-01', $startYear));
$end = new \DateTimeImmutable(sprintf('%04d-05-31', $endYear));
} catch (\Throwable $e) {
return [];
}
// Clamp start to today if in range
if ($start < $today) {
$start = $today;
}
// Iterate and collect Sundays in range [start..end]
$dates = [];
for ($cursor = $start; $cursor <= $end; $cursor = $cursor->modify('+1 day')) {
if ((int) $cursor->format('w') === 0) { // Sunday
$dates[] = $cursor->format('Y-m-d');
}
}
return $dates;
}
}