calendar = model(Calendar::class); $this->config = model(Configuration::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); } /** * GET /api/v1/school-calendar * Get calendar events filtered by school_year and optional semester */ public function index() { $schoolYear = (string) ($this->request->getGet('school_year') ?? $this->schoolYear); $semester = $this->request->getGet('semester'); try { $query = $this->calendar->newQuery(); if ($schoolYear !== '') { $query->where('school_year', $schoolYear); } if ($semester !== null && $semester !== '') { $events = $query->where('semester', $semester)->orderBy('date', 'ASC')->get()->toArray(); } else { $events = $query->orderBy('date', 'ASC')->get()->toArray(); } return $this->success([ 'events' => $events, 'school_year' => $schoolYear, 'semester' => $semester, ], 'Calendar events retrieved successfully'); } catch (\Throwable $e) { log_message('error', 'School calendar error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve calendar events', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/school-calendar/{id} * Get a single calendar event */ public function show($id = null) { $event = $this->calendar->find($id); if (!$event) { return $this->error('Event not found', Response::HTTP_NOT_FOUND); } return $this->success($event, 'Event retrieved successfully'); } /** * POST /api/v1/school-calendar * Create a new calendar event */ public function store() { $payload = $this->payloadData(); if (empty($payload)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $rules = [ 'title' => 'required|min_length[3]', 'start' => 'required|valid_date[Y-m-d]', ]; $errors = $this->validateRequest($payload, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $data = [ 'title' => trim((string) ($payload['title'] ?? '')), 'description' => trim((string) ($payload['description'] ?? '')), 'date' => trim((string) ($payload['start'] ?? '')), 'notify_parent' => isset($payload['notify_parent']) && $payload['notify_parent'] ? 1 : 0, 'notify_teacher' => isset($payload['notify_teacher']) && $payload['notify_teacher'] ? 1 : 0, 'notify_admin' => isset($payload['notify_admin']) && $payload['notify_admin'] ? 1 : 0, 'no_school' => isset($payload['no_school']) && $payload['no_school'] ? 1 : 0, 'school_year' => trim((string) ($payload['school_year'] ?? $this->schoolYear)), 'semester' => trim((string) ($payload['semester'] ?? $this->semester)), ]; try { $event = $this->calendar->create($data); log_message('info', 'Event saved successfully: ' . print_r($data, true)); return $this->success($event, 'Event added successfully', Response::HTTP_CREATED); } catch (\Throwable $e) { log_message('error', 'Failed to save event: ' . $e->getMessage()); return $this->respondError('Failed to save the event', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * PATCH /api/v1/school-calendar/{id} * Update a calendar event */ public function update($id = null) { $event = $this->calendar->find($id); if (!$event) { return $this->error('Event not found', Response::HTTP_NOT_FOUND); } $payload = $this->payloadData(); if (empty($payload)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $rules = [ 'title' => 'permit_empty|min_length[3]', 'start' => 'permit_empty|valid_date[Y-m-d]', ]; $errors = $this->validateRequest($payload, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $data = []; if (isset($payload['title'])) { $data['title'] = trim((string) $payload['title']); } if (isset($payload['description'])) { $data['description'] = trim((string) $payload['description']); } if (isset($payload['start'])) { $data['date'] = trim((string) $payload['start']); } if (isset($payload['notify_parent'])) { $data['notify_parent'] = $payload['notify_parent'] ? 1 : 0; } if (isset($payload['notify_teacher'])) { $data['notify_teacher'] = $payload['notify_teacher'] ? 1 : 0; } if (isset($payload['notify_admin'])) { $data['notify_admin'] = $payload['notify_admin'] ? 1 : 0; } if (isset($payload['no_school'])) { $data['no_school'] = $payload['no_school'] ? 1 : 0; } if (isset($payload['school_year'])) { $data['school_year'] = trim((string) $payload['school_year']); } if (isset($payload['semester'])) { $data['semester'] = trim((string) $payload['semester']); } try { $this->calendar->update($id, $data); $updatedEvent = $this->calendar->find($id); return $this->success($updatedEvent, 'Event updated successfully'); } catch (\Throwable $e) { log_message('error', 'Failed to update event: ' . $e->getMessage()); return $this->respondError('Failed to update the event', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * DELETE /api/v1/school-calendar/{id} * Delete a calendar event */ public function delete($id = null) { $event = $this->calendar->find($id); if (!$event) { return $this->error('Event not found', Response::HTTP_NOT_FOUND); } try { $this->calendar->delete($id); return $this->success(null, 'Event deleted successfully'); } catch (\Throwable $e) { log_message('error', 'Failed to delete event: ' . $e->getMessage()); return $this->respondError('Failed to delete the event', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/school-calendar/view/parent * Get calendar events formatted for parent view */ public function calendarParentView() { $formattedEvents = $this->calendarView('parent'); return $this->success(['events' => $formattedEvents], 'Parent calendar view retrieved successfully'); } /** * GET /api/v1/school-calendar/view/teacher * Get calendar events formatted for teacher view */ public function calendarTeacherView() { $formattedEvents = $this->calendarView('teacher'); return $this->success(['events' => $formattedEvents], 'Teacher calendar view retrieved successfully'); } /** * GET /api/v1/school-calendar/view/admin * Get calendar events formatted for admin view */ public function calendarAdminView() { $formattedEvents = $this->calendarView('admin'); return $this->success(['events' => $formattedEvents], 'Admin calendar view retrieved successfully'); } /** * Private method to format calendar events based on audience visibility rules */ private function calendarView(?string $audience = null): array { // Filter events by school year $query = $this->calendar->newQuery(); if ($this->schoolYear !== '') { $query->where('school_year', $this->schoolYear); } $events = $query->orderBy('date', 'ASC')->get()->toArray(); // 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); return $formattedEvents; } }