urls = $urls ?? app(ApplicationUrlService::class); } public function notify(array $event, array $targets): array { if (empty($event) || empty(array_filter($targets))) { return []; } $subject = $this->buildSubject($event); $body = $this->buildBody($event); $results = []; foreach ($targets as $group => $enabled) { if (! $enabled) { continue; } $emails = $this->collectEmailsForGroup($group); if (empty($emails)) { Log::warning("SchoolCalendar: no recipient emails for {$group}."); $results[$group] = null; continue; } $to = $emails[0] ?? ''; $ok = $this->emailService->send($to, $subject, $body, 'general', [], $emails); if (! $ok) { Log::warning("SchoolCalendar: failed to send calendar notification to {$group}."); } $results[$group] = $ok; } return $results; } private function buildSubject(array $event): string { $title = trim((string) ($event['title'] ?? 'School Calendar Update')); $typeSegment = ! empty($event['event_type']) ? ' ('.$event['event_type'].')' : ''; return 'School Calendar: '.$title.$typeSegment; } private function buildBody(array $event): string { $calendarUrl = $this->urls->spaAdministratorCalendarViewUrl(); $title = trim((string) ($event['title'] ?? 'School Calendar Update')); $date = (string) ($event['date'] ?? ''); $description = trim((string) ($event['description'] ?? '')); $body = '
Date: '.e($date).'
'; if ($description !== '') { $body .= ''.nl2br(e($description)).'
'; } $body .= ''; return $body; } private function collectEmailsForGroup(string $group): array { return match ($group) { 'parents' => $this->normalizeEmails(User::getParents()), 'teachers' => $this->normalizeEmails(User::getAllTeachers()), 'admins' => $this->normalizeEmails($this->fetchAdminRows()), default => [], }; } private function normalizeEmails(array $rows): array { $emails = []; foreach ($rows as $row) { $email = trim((string) ($row['email'] ?? '')); if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { continue; } $emails[$email] = true; } return array_values(array_keys($emails)); } private function fetchAdminRows(): array { $excluded = ['guest', 'teacher', 'teacher_assistant', 'parent']; $escaped = array_map(static fn ($value) => "'".addslashes($value)."'", $excluded); $excludedList = implode(', ', $escaped); $builder = DB::table('users as u') ->selectRaw("u.id, u.firstname, u.lastname, u.email, MAX(CASE WHEN LOWER(COALESCE(r.slug, r.name)) NOT IN({$excludedList}) THEN 1 ELSE 0 END) AS is_admin") ->join('user_roles as ur', 'ur.user_id', '=', 'u.id') ->join('roles as r', 'r.id', '=', 'ur.role_id') ->where('r.is_active', 1) ->whereNotNull('u.email') ->where('u.email', '!=', '') ->groupBy('u.id', 'u.firstname', 'u.lastname', 'u.email') ->having('is_admin', 1); return $builder->get()->map(fn ($r) => (array) $r)->all(); } }