success([ 'event_types' => $this->contextService->eventTypes(), 'defaults' => [ 'school_year' => $this->contextService->defaultSchoolYear(), 'semester' => $this->contextService->defaultSemester(), ], ]); } public function index(SchoolCalendarIndexRequest $request): JsonResponse { $filters = $request->validated(); if (! $this->canListEventsForAudience($filters['audience'] ?? null)) { return $this->error('Forbidden.', Response::HTTP_FORBIDDEN); } return $this->respondWithEvents($filters); } /** * Legacy teacher portal URL: same response as * GET /api/v1/settings/school-calendar/events?audience=teacher */ public function teacherCalendarLegacy(SchoolCalendarIndexRequest $request): JsonResponse { if (! $this->canListEventsForAudience('teacher')) { return $this->error('Forbidden.', Response::HTTP_FORBIDDEN); } return $this->respondWithEvents([ ...$request->validated(), 'audience' => 'teacher', ]); } private function respondWithEvents(array $filters): JsonResponse { $audience = $filters['audience'] ?? null; $includeMeetings = array_key_exists('include_meetings', $filters) ? (bool) $filters['include_meetings'] : true; if (empty($filters['school_year'])) { $filters['school_year'] = $this->contextService->defaultSchoolYear(); } $events = $this->queryService->listEvents($filters); $events = $this->queryService->filterEventsForAudience($events, $audience); $formatted = $events->map(function ($event) use ($audience) { return $this->formatterService->formatEvent($event, $audience); })->all(); if ($includeMeetings) { $parentUserId = null; if ($audience === 'parent') { $actor = $this->authenticatedUserIdOrUnauthorized(); if ($actor instanceof JsonResponse) { return $actor; } $parentUserId = $actor; } $meetingRows = $this->meetingService->listMeetings($filters['school_year'] ?? '', $audience, $parentUserId); foreach ($meetingRows as $row) { $formatted[] = $this->formatterService->formatMeetingEvent($row, $audience); } } return $this->success([ 'events' => SchoolCalendarEventResource::collection($formatted), ]); } public function show(int $eventId): JsonResponse { $event = $this->queryService->findEvent($eventId); if (! $event) { return $this->error('Event not found.', Response::HTTP_NOT_FOUND); } return $this->success([ 'event' => new SchoolCalendarEventDetailResource($event), ]); } public function store(SchoolCalendarStoreRequest $request): JsonResponse { $payload = $request->validated(); $targets = $this->extractEmailTargets($payload); try { $result = $this->mutationService->create($payload, $targets); } catch (\Throwable $e) { Log::error('SchoolCalendar store failed: '.$e->getMessage()); return $this->error('Failed to save event.', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success([ 'event' => new SchoolCalendarEventDetailResource($result['event']), 'email_status' => $result['email_status'] ?? [], ], 'Event added successfully.', Response::HTTP_CREATED); } public function update(SchoolCalendarUpdateRequest $request, int $eventId): JsonResponse { $event = $this->queryService->findEvent($eventId); if (! $event) { return $this->error('Event not found.', Response::HTTP_NOT_FOUND); } $payload = $request->validated(); $targets = $this->extractEmailTargets($payload); try { $result = $this->mutationService->update($event, $payload, $targets); } catch (\Throwable $e) { Log::error('SchoolCalendar update failed: '.$e->getMessage()); return $this->error('Failed to update event.', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success([ 'event' => new SchoolCalendarEventDetailResource($result['event']), 'email_status' => $result['email_status'] ?? [], ], 'Event updated successfully.'); } public function destroy(int $eventId): JsonResponse { $event = $this->queryService->findEvent($eventId); if (! $event) { return $this->error('Event not found.', Response::HTTP_NOT_FOUND); } try { $this->mutationService->delete($event); } catch (\Throwable $e) { Log::error('SchoolCalendar delete failed: '.$e->getMessage()); return $this->error('Failed to delete event.', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success(null, 'Event deleted successfully.'); } private function extractEmailTargets(array $payload): array { return [ 'parents' => ! empty($payload['send_email_parent']), 'teachers' => ! empty($payload['send_email_teacher']), 'admins' => ! empty($payload['send_email_admin']), ]; } private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); if ($userId <= 0) { return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); } return $userId; } private function canListEventsForAudience(?string $audience): bool { if ($this->hasAnyRole(['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'])) { return true; } $audience = strtolower(trim((string) $audience)); if ($audience === 'parent') { return $this->hasAnyRole(['parent']) || $this->hasUserType(['secondary', 'tertiary']); } if ($audience === 'teacher') { return $this->hasAnyRole(['teacher']); } return false; } /** * @param list $roles */ private function hasAnyRole(array $roles): bool { $user = auth()->user(); if (! $user) { return false; } $actual = $user->roles() ->pluck('roles.name') ->map(fn ($name) => strtolower((string) $name)) ->toArray(); foreach ($roles as $role) { if (in_array(strtolower($role), $actual, true)) { return true; } } return false; } /** * @param list $types */ private function hasUserType(array $types): bool { $type = strtolower(trim((string) (auth()->user()?->user_type ?? ''))); return in_array($type, array_map('strtolower', $types), true); } }