recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
@@ -0,0 +1,658 @@
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\CalendarModel;
use App\Models\ConfigurationModel;
use App\Models\TeacherModel;
use App\Models\UserModel;
use App\Models\ParentMeetingScheduleModel;
use App\Services\EmailService;
class SchoolCalendarController extends BaseController
{
protected $calendarModel;
protected $configModel;
protected $semester;
protected $schoolYear;
protected $db;
protected $eventTypes = [
'1st Semester Scores',
'2nd Semester Scores',
'Break',
'Event',
'Final',
'Final Exam Draft',
'Midterm',
'Midterm Exam Draft',
];
protected EmailService $emailService;
protected UserModel $userModel;
protected TeacherModel $teacherModel;
protected ParentMeetingScheduleModel $meetingModel;
public function __construct()
{
// Load the models
$this->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 [];
}
}
}