add school calendar logic
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settings\SchoolCalendar;
|
||||
|
||||
use App\Models\Configuration;
|
||||
|
||||
class SchoolCalendarContextService
|
||||
{
|
||||
public function defaultSchoolYear(): string
|
||||
{
|
||||
return trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
}
|
||||
|
||||
public function defaultSemester(): string
|
||||
{
|
||||
return trim((string) (Configuration::getConfig('semester') ?? ''));
|
||||
}
|
||||
|
||||
public function eventTypes(): array
|
||||
{
|
||||
return [
|
||||
'1st Semester Scores',
|
||||
'2nd Semester Scores',
|
||||
'Break',
|
||||
'Event',
|
||||
'Final',
|
||||
'Final Exam Draft',
|
||||
'Midterm',
|
||||
'Midterm Exam Draft',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settings\SchoolCalendar;
|
||||
|
||||
use App\Models\CalendarEvent;
|
||||
|
||||
class SchoolCalendarFormatterService
|
||||
{
|
||||
public function formatEvent(CalendarEvent|array $event, ?string $audience = null): array
|
||||
{
|
||||
$event = $event instanceof CalendarEvent ? $event->toArray() : $event;
|
||||
$audience = $audience !== null ? strtolower(trim($audience)) : null;
|
||||
|
||||
$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;
|
||||
if (in_array($audience, ['teacher', 'parent'], true) && $ns === 1) {
|
||||
$backgroundColor = '#ffc107';
|
||||
}
|
||||
|
||||
if (in_array($audience, ['teacher', 'admin'], true) && $backgroundColor === null) {
|
||||
$isStaffOnly = ($p === 0 && ($t === 1 || $a === 1) && $ns === 0);
|
||||
if ($isStaffOnly) {
|
||||
$backgroundColor = '#28a745';
|
||||
}
|
||||
}
|
||||
|
||||
$title = (string) ($event['title'] ?? 'Untitled');
|
||||
if ($ns === 1) {
|
||||
$title = preg_replace('/\bno\s*[- ]\s*school\b/i', 'No School', $title);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($event['id'] ?? 0),
|
||||
'title' => $title,
|
||||
'start' => (string) ($event['date'] ?? ''),
|
||||
'description' => (string) ($event['description'] ?? ''),
|
||||
'extendedProps' => [
|
||||
'semester' => (string) ($event['semester'] ?? ''),
|
||||
'school_year' => (string) ($event['school_year'] ?? ''),
|
||||
'event_type' => $event['event_type'] ?? null,
|
||||
'notify_parent' => $p,
|
||||
'notify_teacher' => $t,
|
||||
'notify_admin' => $a,
|
||||
'no_school' => $ns,
|
||||
'backgroundColor' => $backgroundColor,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function formatMeetingEvent(array $row, ?string $audience = null): array
|
||||
{
|
||||
$audience = $audience !== null ? strtolower(trim($audience)) : null;
|
||||
|
||||
$parentName = (string) ($row['parent_name'] ?? 'Parent');
|
||||
$studentName = (string) ($row['student_name'] ?? 'Student');
|
||||
$time = trim((string) ($row['time'] ?? ''));
|
||||
$timeLabel = $time !== '' ? (' ' . $time) : '';
|
||||
|
||||
$title = $audience === 'admin' ? 'Admin Meeting' : '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' => (string) ($row['semester'] ?? ''),
|
||||
'school_year' => (string) ($row['school_year'] ?? ''),
|
||||
'notify_parent' => 1,
|
||||
'notify_admin' => 1,
|
||||
'notify_teacher' => 0,
|
||||
'no_school' => 0,
|
||||
'backgroundColor' => '#6f42c1',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settings\SchoolCalendar;
|
||||
|
||||
use App\Models\ParentMeetingSchedule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SchoolCalendarMeetingService
|
||||
{
|
||||
public function listMeetings(string $schoolYear, ?string $audience, ?int $parentUserId = null): array
|
||||
{
|
||||
$audience = $audience !== null ? strtolower(trim($audience)) : null;
|
||||
|
||||
$builder = ParentMeetingSchedule::query()
|
||||
->where('school_year', $schoolYear)
|
||||
->where(function (Builder $q) {
|
||||
$q->where('status', 'scheduled')
|
||||
->orWhere('status', 'Scheduled')
|
||||
->orWhere('status', '')
|
||||
->orWhereNull('status');
|
||||
});
|
||||
|
||||
if ($audience === 'parent' && $parentUserId) {
|
||||
$studentIds = $this->getStudentIdsForParent($parentUserId);
|
||||
if (!empty($studentIds)) {
|
||||
$builder->where(function (Builder $q) use ($parentUserId, $studentIds) {
|
||||
$q->where('parent_user_id', $parentUserId)
|
||||
->orWhereIn('student_id', $studentIds);
|
||||
});
|
||||
} else {
|
||||
$builder->where('parent_user_id', $parentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('date', 'ASC')->get()->toArray();
|
||||
if (!empty($rows)) {
|
||||
return $rows;
|
||||
}
|
||||
|
||||
$fallback = ParentMeetingSchedule::query()
|
||||
->where(function (Builder $q) {
|
||||
$q->where('status', 'scheduled')
|
||||
->orWhere('status', 'Scheduled')
|
||||
->orWhere('status', '')
|
||||
->orWhereNull('status');
|
||||
});
|
||||
|
||||
if ($audience === 'parent' && $parentUserId) {
|
||||
$studentIds = $this->getStudentIdsForParent($parentUserId);
|
||||
if (!empty($studentIds)) {
|
||||
$fallback->where(function (Builder $q) use ($parentUserId, $studentIds) {
|
||||
$q->where('parent_user_id', $parentUserId)
|
||||
->orWhereIn('student_id', $studentIds);
|
||||
});
|
||||
} else {
|
||||
$fallback->where('parent_user_id', $parentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
return $fallback->orderBy('date', 'ASC')->get()->toArray();
|
||||
}
|
||||
|
||||
private function getStudentIdsForParent(int $parentUserId): array
|
||||
{
|
||||
try {
|
||||
$rows = DB::table('family_guardians as fg')
|
||||
->select('fs.student_id')
|
||||
->join('family_students as fs', 'fs.family_id', '=', 'fg.family_id')
|
||||
->where('fg.user_id', $parentUserId)
|
||||
->distinct()
|
||||
->get()
|
||||
->map(fn ($r) => (int) $r->student_id)
|
||||
->all();
|
||||
|
||||
$legacyRows = DB::table('students')
|
||||
->select('id as student_id')
|
||||
->where('parent_id', $parentUserId)
|
||||
->get()
|
||||
->map(fn ($r) => (int) $r->student_id)
|
||||
->all();
|
||||
|
||||
return array_values(array_filter(array_unique(array_merge($rows, $legacyRows))));
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settings\SchoolCalendar;
|
||||
|
||||
use App\Models\CalendarEvent;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SchoolCalendarMutationService
|
||||
{
|
||||
public function __construct(
|
||||
private SchoolCalendarContextService $context,
|
||||
private SchoolCalendarNotificationService $notificationService
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(array $payload, array $targets = []): array
|
||||
{
|
||||
$data = $this->normalizePayload($payload);
|
||||
|
||||
try {
|
||||
$event = DB::transaction(function () use ($data) {
|
||||
return CalendarEvent::query()->create($data);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('SchoolCalendar create failed: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$emailStatus = $this->notificationService->notify($event->toArray(), $targets);
|
||||
|
||||
return [
|
||||
'event' => $event,
|
||||
'email_status' => $emailStatus,
|
||||
];
|
||||
}
|
||||
|
||||
public function update(CalendarEvent $event, array $payload, array $targets = []): array
|
||||
{
|
||||
$data = $this->normalizePayload($payload);
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($event, $data) {
|
||||
$event->fill($data);
|
||||
$event->save();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('SchoolCalendar update failed: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$event->refresh();
|
||||
$emailStatus = $this->notificationService->notify($event->toArray(), $targets);
|
||||
|
||||
return [
|
||||
'event' => $event,
|
||||
'email_status' => $emailStatus,
|
||||
];
|
||||
}
|
||||
|
||||
public function delete(CalendarEvent $event): void
|
||||
{
|
||||
try {
|
||||
DB::transaction(function () use ($event) {
|
||||
$event->delete();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('SchoolCalendar delete failed: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizePayload(array $payload): array
|
||||
{
|
||||
$data = [
|
||||
'title' => (string) ($payload['title'] ?? ''),
|
||||
'description' => $payload['description'] ?? null,
|
||||
'event_type' => $payload['event_type'] ?? null,
|
||||
'date' => $payload['date'] ?? $payload['start'] ?? null,
|
||||
'notify_parent' => !empty($payload['notify_parent']) ? 1 : 0,
|
||||
'notify_teacher' => !empty($payload['notify_teacher']) ? 1 : 0,
|
||||
'notify_admin' => !empty($payload['notify_admin']) ? 1 : 0,
|
||||
'no_school' => !empty($payload['no_school']) ? 1 : 0,
|
||||
'school_year' => $payload['school_year'] ?? $this->context->defaultSchoolYear(),
|
||||
'semester' => $payload['semester'] ?? $this->context->defaultSemester(),
|
||||
];
|
||||
|
||||
if (!CalendarEvent::supportsEventType()) {
|
||||
unset($data['event_type']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settings\SchoolCalendar;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\EmailService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class SchoolCalendarNotificationService
|
||||
{
|
||||
public function __construct(private EmailService $emailService)
|
||||
{
|
||||
}
|
||||
|
||||
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 = URL::to('/administrator/calendar_view');
|
||||
$title = trim((string) ($event['title'] ?? 'School Calendar Update'));
|
||||
$date = (string) ($event['date'] ?? '');
|
||||
$description = trim((string) ($event['description'] ?? ''));
|
||||
|
||||
$body = '<h3>' . e($title) . '</h3>';
|
||||
$body .= '<p><strong>Date:</strong> ' . e($date) . '</p>';
|
||||
if ($description !== '') {
|
||||
$body .= '<p>' . nl2br(e($description)) . '</p>';
|
||||
}
|
||||
$body .= '<p><a href="' . e($calendarUrl) . '">View calendar</a></p>';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settings\SchoolCalendar;
|
||||
|
||||
use App\Models\CalendarEvent;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class SchoolCalendarQueryService
|
||||
{
|
||||
public function listEvents(array $filters): Collection
|
||||
{
|
||||
$query = CalendarEvent::query();
|
||||
|
||||
$schoolYear = trim((string) ($filters['school_year'] ?? ''));
|
||||
if ($schoolYear !== '') {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$semester = trim((string) ($filters['semester'] ?? ''));
|
||||
if ($semester !== '') {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
|
||||
$sortDir = strtolower((string) ($filters['sort_dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
||||
$query->orderBy('date', $sortDir)->orderBy('id', 'asc');
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function findEvent(int $id): ?CalendarEvent
|
||||
{
|
||||
return CalendarEvent::query()->find($id);
|
||||
}
|
||||
|
||||
public function filterEventsForAudience(Collection $events, ?string $audience): Collection
|
||||
{
|
||||
$audience = $audience !== null ? strtolower(trim($audience)) : null;
|
||||
if (!in_array($audience, ['parent', 'teacher', 'admin'], true)) {
|
||||
return $events;
|
||||
}
|
||||
|
||||
return $events->filter(function (CalendarEvent $event) use ($audience) {
|
||||
$p = (int) $event->notify_parent;
|
||||
$t = (int) $event->notify_teacher;
|
||||
$a = (int) $event->notify_admin;
|
||||
$ns = (int) $event->no_school;
|
||||
$hasAny = ($p + $t + $a) > 0;
|
||||
|
||||
if ($ns === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$hasAny) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($audience === 'parent') {
|
||||
return $p === 1;
|
||||
}
|
||||
if ($audience === 'teacher') {
|
||||
return $t === 1;
|
||||
}
|
||||
if ($audience === 'admin') {
|
||||
return $a === 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->values();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user