calendarModel = new CalendarModel(); $this->configModel = new ConfigurationModel(); $this->userModel = new UserModel(); $this->teacherModel = new TeacherModel(); $this->emailService = new EmailService(); $this->meetingModel = new ParentMeetingScheduleModel(); $this->db = \Config\Database::connect(); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->semester = $this->configModel->getConfig('semester'); // Load helpers helper(['form', 'url']); } public function index() { // Get school_year / semester from query string or defaults $syParam = $this->request->getGet('school_year'); $semParam = $this->request->getGet('semester'); $schoolYear = $syParam ?: $this->schoolYear; $semester = $semParam ?: ''; if ($semester !== '') { $events = $this->calendarModel->getEventsBySchoolYearAndSemester($schoolYear, $semester); } else { $events = $this->calendarModel->getEventsBySchoolYear($schoolYear); } return view('administrator/calendar_view', [ 'events' => $events, 'schoolYear' => $schoolYear, 'semester' => $semester, ]); } public function edit($id) { $event = $this->calendarModel->find($id); if (!$event) { return redirect()->to('/administrator/calendar_view')->with('error', 'Event not found'); } return view('administrator/calendar_edit', [ 'event' => $event, 'eventTypes' => $this->eventTypes, ]); } public function update($id) { // Fetch the event by ID $event = $this->calendarModel->find($id); if (!$event) { return redirect()->to('/administrator/calendar_view')->with('error', 'Event not found.'); } if (strtolower($this->request->getMethod()) === 'post') { $validation = $this->validate([ 'title' => 'required|min_length[3]', 'start' => 'required|valid_date[Y-m-d]', ]); if (!$validation) { return view('administrator/calendar_edit', [ 'validation' => $this->validator, 'event' => $event, 'eventTypes' => $this->eventTypes, ]); } $emailTargets = $this->emailNotificationTargetsFromRequest(); $data = [ 'title' => $this->request->getPost('title'), 'description' => $this->request->getPost('description'), 'event_type' => $this->request->getPost('event_type') ?: null, 'date' => $this->request->getPost('start'), 'notify_parent' => $this->request->getPost('notify_parent') ? 1 : 0, 'notify_teacher' => $this->request->getPost('notify_teacher') ? 1 : 0, 'notify_admin' => $this->request->getPost('notify_admin') ? 1 : 0, 'no_school' => $this->request->getPost('no_school') ? 1 : 0, 'school_year' => $this->request->getPost('school_year') ?: $this->schoolYear, 'semester' => $this->request->getPost('semester') ?: $this->semester, ]; if (!$this->calendarModel->supportsEventType()) { unset($data['event_type']); } if ($this->calendarModel->update($id, $data)) { $updatedEvent = $this->calendarModel->find($id); $emailStatus = $this->dispatchCalendarNotificationEmails($updatedEvent, $emailTargets); $filters = [ 'school_year' => $data['school_year'], 'semester' => $data['semester'], ]; $redirect = redirect()->to($this->buildCalendarViewUrl($filters))->with('success', 'Event updated successfully'); if ($emailFlash = $this->buildEmailFlash($emailStatus)) { $redirect = $redirect ->with('email_status', $emailFlash['message']) ->with('email_status_class', $emailFlash['class']); } return $redirect; } else { return redirect()->back()->with('error', 'Failed to update the event.'); } } return view('administrator/calendar_edit', [ 'event' => $event, 'eventTypes' => $this->eventTypes, ]); } public function addEvent() { if (strtolower($this->request->getMethod()) === 'post') { $validation = $this->validate([ 'title' => 'required|min_length[3]', 'start' => 'required|valid_date[Y-m-d]', ]); if (!$validation) { return view('administrator/calendar_add_event', [ 'validation' => $this->validator, 'eventTypes' => $this->eventTypes, ]); } $emailTargets = $this->emailNotificationTargetsFromRequest(); // Prepare data $data = [ 'title' => $this->request->getPost('title'), 'description' => $this->request->getPost('description'), 'event_type' => $this->request->getPost('event_type') ?: null, 'date' => $this->request->getPost('start'), 'notify_parent' => $this->request->getPost('notify_parent') ? 1 : 0, 'notify_teacher' => $this->request->getPost('notify_teacher') ? 1 : 0, 'notify_admin' => $this->request->getPost('notify_admin') ? 1 : 0, 'no_school' => $this->request->getPost('no_school') ? 1 : 0, 'school_year' => $this->request->getPost('school_year') ?: $this->schoolYear, 'semester' => $this->request->getPost('semester') ?: $this->semester, ]; if (!$this->calendarModel->supportsEventType()) { unset($data['event_type']); } if ($this->calendarModel->save($data)) { $insertedId = $this->calendarModel->getInsertID(); $savedEvent = $this->calendarModel->find($insertedId); $emailStatus = $this->dispatchCalendarNotificationEmails($savedEvent, $emailTargets); log_message('info', 'Event saved successfully: ' . print_r($data, true)); $filters = [ 'school_year' => $data['school_year'], 'semester' => $data['semester'], ]; $redirect = redirect()->to($this->buildCalendarViewUrl($filters))->with('success', 'Event added successfully'); if ($emailFlash = $this->buildEmailFlash($emailStatus)) { $redirect = $redirect ->with('email_status', $emailFlash['message']) ->with('email_status_class', $emailFlash['class']); } return $redirect; } else { log_message('error', 'Failed to save event: ' . print_r($data, true)); return redirect()->back()->with('error', 'Failed to save the event.'); } } return view('administrator/calendar_add_event', [ 'eventTypes' => $this->eventTypes, ]); } protected function emailNotificationTargetsFromRequest(): array { return [ 'parents' => (bool) $this->request->getPost('send_email_parent'), 'teachers' => (bool) $this->request->getPost('send_email_teacher'), 'admins' => (bool) $this->request->getPost('send_email_admin'), ]; } protected function dispatchCalendarNotificationEmails(array $event = null, array $targets = []): array { if (empty($event) || empty(array_filter($targets))) { return []; } $subject = $this->buildCalendarEmailSubject($event); $innerBody = view('emails/calendar_notification', [ 'event' => $event, 'calendarUrl' => base_url('administrator/calendar_view'), ]); $body = view('emails/_wrap_layout', [ 'title' => $subject, 'subject' => $subject, 'body_html' => $innerBody, ]); $results = []; foreach ($targets as $group => $enabled) { if (!$enabled) { continue; } $emails = $this->collectEmailsForGroup($group); if (empty($emails)) { log_message('warning', "SchoolCalendarController: no recipient emails for {$group}."); $results[$group] = null; continue; } $ok = $this->emailService->sendBcc($emails, $subject, $body, 'general'); if (!$ok) { log_message('warning', "SchoolCalendarController: failed to send calendar notification to {$group}."); } $results[$group] = $ok; } return $results; } protected function collectEmailsForGroup(string $group): array { switch ($group) { case 'parents': return $this->normalizeEmailAddresses($this->userModel->getParents()); case 'teachers': return $this->normalizeEmailAddresses($this->teacherModel->getAllTeachers()); case 'admins': return $this->normalizeEmailAddresses($this->fetchAdminRows()); default: return []; } } protected function normalizeEmailAddresses(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)); } protected function fetchAdminRows(): array { $excluded = ['guest', 'teacher', 'teacher_assistant', 'parent']; $escaped = array_map(static fn($value) => "'" . addslashes($value) . "'", $excluded); $excludedList = implode(', ', $escaped); $builder = $this->db->table('users u') ->select("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", false) ->join('user_roles ur', 'ur.user_id = u.id') ->join('roles r', 'r.id = ur.role_id') ->where('r.is_active', 1) ->where('u.email IS NOT NULL') ->where('u.email !=', '') ->groupBy('u.id, u.firstname, u.lastname, u.email') ->having('is_admin', 1); return $builder->get()->getResultArray(); } protected function buildCalendarEmailSubject(array $event): string { $title = trim((string) ($event['title'] ?? 'School Calendar Update')); $typeSegment = !empty($event['event_type']) ? ' (' . $event['event_type'] . ')' : ''; return 'School Calendar: ' . $title . $typeSegment; } protected function buildEmailFlash(array $status): ?array { if (empty($status)) { return null; } $sent = []; $failed = []; $missing = []; foreach ($status as $group => $result) { if ($result === true) { $sent[] = $group; } elseif ($result === false) { $failed[] = $group; } elseif ($result === null) { $missing[] = $group; } } $parts = []; if (!empty($sent)) { $parts[] = 'Email sent to ' . $this->humanizeGroupList($sent) . '.'; } if (!empty($failed)) { $parts[] = 'Sending failed for ' . $this->humanizeGroupList($failed) . '.'; } if (!empty($missing)) { $parts[] = 'No recipient emails for ' . $this->humanizeGroupList($missing) . '.'; } if (empty($parts)) { return null; } return [ 'message' => implode(' ', $parts), 'class' => empty($failed) && empty($missing) ? 'info' : 'warning', ]; } protected function humanizeGroupList(array $groups): string { $groups = array_map('strval', $groups); if (empty($groups)) { return ''; } if (count($groups) === 1) { return $groups[0]; } $last = array_pop($groups); return implode(', ', $groups) . ' and ' . $last; } protected function buildCalendarViewUrl(array $filters = []): string { $allowed = ['school_year', 'semester']; $params = []; foreach ($allowed as $key) { $value = $filters[$key] ?? ''; if ($value !== '') { $params[$key] = $value; } } $query = http_build_query($params); return '/administrator/calendar_view' . ($query !== '' ? ('?' . $query) : ''); } public function delete($id) { // Check if the event exists $event = $this->calendarModel->find($id); if (!$event) { return redirect()->to('/administrator/calendar_view')->with('error', 'Event not found.'); } // Delete the event if ($this->calendarModel->delete($id)) { return redirect()->to('/administrator/calendar_view')->with('success', 'Event deleted successfully'); } else { return redirect()->to('/administrator/calendar_view')->with('error', 'Failed to delete the event.'); } } public function calendarParentView() { // Transform the events into the format FullCalendar expects $formattedEvents = $this->calendarView('parent'); // Pass the formatted events to the view return view('/parent/calendar', ['events' => $formattedEvents]); } public function calendarTeacherView() { // Transform the events into the format FullCalendar expects $formattedEvents = $this->calendarView('teacher'); // Pass the formatted events to the view return view('/teacher/calendar', ['events' => $formattedEvents]); } public function calendarAdminView() { // Transform the events into the format FullCalendar expects $formattedEvents = $this->calendarView('admin'); // Pass the formatted events to the view return view('administrator/calendar', ['events' => $formattedEvents]); } private function calendarView(?string $audience = null) { // Filter events by school year $events = $this->calendarModel->getEventsBySchoolYear($this->schoolYear); // Apply audience visibility rules: // - If no checkboxes selected (all notify_* = 0) => visible to everyone // - Otherwise visible only to the selected audience if (in_array($audience, ['parent', 'teacher', 'admin'], true)) { $events = array_values(array_filter($events, function ($event) use ($audience) { $p = (int) ($event['notify_parent'] ?? 0); $t = (int) ($event['notify_teacher'] ?? 0); $a = (int) ($event['notify_admin'] ?? 0); $ns = (int) ($event['no_school'] ?? 0); $hasAny = ($p + $t + $a) > 0; // If marked as no_school, it should be visible to all audiences if ($ns === 1) { return true; } if (!$hasAny) { // No audience explicitly selected: show to everyone return true; } // Audience explicitly selected: restrict to matching audience if ($audience === 'parent') { return $p === 1; } if ($audience === 'teacher') { return $t === 1; } if ($audience === 'admin') { return $a === 1; } return true; })); } // Format for FullCalendar $aud = $audience; // capture for closure $formattedEvents = array_map(function ($event) use ($aud) { $p = (int) ($event['notify_parent'] ?? 0); $t = (int) ($event['notify_teacher'] ?? 0); $a = (int) ($event['notify_admin'] ?? 0); $ns = (int) ($event['no_school'] ?? 0); $backgroundColor = null; // Color no-school events yellow on teacher and parent calendars if (in_array($aud, ['teacher', 'parent'], true) && $ns === 1) { $backgroundColor = '#ffc107'; // yellow } // Highlight events dedicated to staff (teachers/admins) only in green // Definition: visible to staff but not to parents, and not a no-school marker if (in_array($aud, ['teacher', 'admin'], true) && $backgroundColor === null) { $isStaffOnly = ($p === 0 && ($t === 1 || $a === 1) && $ns === 0); if ($isStaffOnly) { // Use a distinct green to differentiate from the default blue $backgroundColor = '#28a745'; } } // Title-casing for "no school" phrase when marked as no_school $title = $event['title'] ?? 'Untitled'; if ($ns === 1 && is_string($title)) { // Replace common variants with Title Case $title = preg_replace('/\bno\s*[- ]\s*school\b/i', 'No School', $title); } return [ 'id' => $event['id'], 'title' => $title, 'start' => $event['date'] ?? '', // FullCalendar expects 'start' 'description' => $event['description'] ?? '', 'extendedProps' => [ 'semester' => $event['semester'] ?? '', 'school_year' => $event['school_year'] ?? '', 'notify_parent' => $p, 'notify_teacher' => $t, 'notify_admin' => $a, 'no_school' => $ns, 'backgroundColor' => $backgroundColor, ] ]; }, $events); // Append parent meeting schedules $meetingEvents = $this->formatMeetingEvents($audience); if (!empty($meetingEvents)) { $formattedEvents = array_values(array_merge($formattedEvents, $meetingEvents)); } return $formattedEvents; } private function formatMeetingEvents(?string $audience = null): array { $aud = $audience ?? ''; $builder = $this->meetingModel ->where('school_year', $this->schoolYear) ->groupStart() ->where('status', 'scheduled') ->orWhere('status', 'Scheduled') ->orWhere('status', '') ->orWhere('status', null) ->groupEnd(); if ($aud === 'parent') { $parentUserId = (int) (session()->get('user_id') ?? 0); if ($parentUserId <= 0) { return []; } $studentIds = $this->getStudentIdsForParent($parentUserId); if (!empty($studentIds)) { $builder = $builder->groupStart() ->where('parent_user_id', $parentUserId) ->orWhereIn('student_id', $studentIds) ->groupEnd(); } else { $builder = $builder->where('parent_user_id', $parentUserId); } } $rows = $builder->orderBy('date', 'ASC')->findAll(); if (empty($rows)) { // Fallback: show any meetings if school_year was missing/mismatched $fallback = $this->meetingModel ->groupStart() ->where('status', 'scheduled') ->orWhere('status', 'Scheduled') ->orWhere('status', '') ->orWhere('status', null) ->groupEnd(); if ($aud === 'parent') { $parentUserId = (int) (session()->get('user_id') ?? 0); if ($parentUserId > 0) { $studentIds = $this->getStudentIdsForParent($parentUserId); if (!empty($studentIds)) { $fallback = $fallback->groupStart() ->where('parent_user_id', $parentUserId) ->orWhereIn('student_id', $studentIds) ->groupEnd(); } else { $fallback = $fallback->where('parent_user_id', $parentUserId); } } } $rows = $fallback->orderBy('date', 'ASC')->findAll(); } if (empty($rows)) { return []; } return array_map(function ($row) use ($aud) { $parentName = (string)($row['parent_name'] ?? 'Parent'); $studentName = (string)($row['student_name'] ?? 'Student'); $time = trim((string)($row['time'] ?? '')); $timeLabel = $time !== '' ? (' ' . $time) : ''; if ($aud === 'admin') { $title = 'Admin Meeting'; } else { $title = 'Parent Meeting'; } $descParts = []; $descParts[] = 'Student: ' . $studentName; $descParts[] = 'Parent: ' . $parentName; if (!empty($row['class_section_name'])) { $descParts[] = 'Section: ' . $row['class_section_name']; } $descParts[] = 'Date/Time: ' . ($row['date'] ?? '') . $timeLabel; if (!empty($row['notes'])) { $descParts[] = 'Notes: ' . $row['notes']; } $start = (string)($row['date'] ?? ''); if ($start !== '' && $time !== '') { $start = $start . 'T' . $time; } return [ 'id' => 'pm-' . (int)($row['id'] ?? 0), 'title' => $title, 'start' => $start, 'description' => implode("\n", $descParts), 'extendedProps' => [ 'semester' => $row['semester'] ?? '', 'school_year' => $row['school_year'] ?? '', 'notify_parent' => 1, 'notify_admin' => 1, 'notify_teacher' => 0, 'no_school' => 0, 'backgroundColor' => '#6f42c1', ], ]; }, $rows); } private function getStudentIdsForParent(int $parentUserId): array { try { $rows = $this->db->query( "SELECT DISTINCT fs.student_id FROM family_guardians fg JOIN family_students fs ON fs.family_id = fg.family_id WHERE fg.user_id = ?", [$parentUserId] )->getResultArray(); $ids = array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows); // Legacy fallback: students.parent_id $rows2 = $this->db->query( "SELECT id AS student_id FROM students WHERE parent_id = ?", [$parentUserId] )->getResultArray(); foreach ($rows2 as $r) { $ids[] = (int)($r['student_id'] ?? 0); } return array_values(array_filter($ids, static fn($id) => $id > 0)); } catch (\Throwable $e) { return []; } } }