diff --git a/.env b/.env index 6443556..1e48905 100644 --- a/.env +++ b/.env @@ -63,7 +63,7 @@ session.expiration = 43200 database.default.hostname = 127.0.0.1 database.default.database = school database.default.username = root -database.default.password = +database.default.password = rootpassword database.default.DBDriver = MySQLi database.default.DBPrefix = database.default.port = 3306 diff --git a/app/Config/Database.php b/app/Config/Database.php index 78561d0..0af3e90 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -9,43 +9,51 @@ class Database extends Config public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR; public string $defaultGroup = 'default'; - public array $default = [ - 'DSN' => '', - 'hostname' => 'localhost', - 'username' => 'u280815660_melabidi', - 'password' => '>tNxlRzP/W8', - 'database' => 'u280815660_school', - 'DBDriver' => 'MySQLi', - 'DBPrefix' => '', - 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'development'), - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - 'failover' => [], - 'port' => 3306, - ]; + public array $default = []; + public array $tests = []; - public array $tests = [ - 'DSN' => '', - 'hostname' => 'localhost', - 'username' => 'u280815660_melabidi', - 'password' => '>tNxlRzP/W8', - 'database' => 'u280815660_school', - 'DBDriver' => 'MySQLi', - 'DBPrefix' => 'db_', - 'pConnect' => false, - 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - 'failover' => [], - 'port' => 3306, - ]; + public function __construct() + { + parent::__construct(); + + $this->default = [ + 'DSN' => '', + 'hostname' => env('database.default.hostname'), + 'username' => env('database.default.username'), + 'password' => env('database.default.password'), + 'database' => env('database.default.database'), + 'DBDriver' => env('database.default.DBDriver', 'MySQLi'), + 'DBPrefix' => '', + 'pConnect' => false, + 'DBDebug' => (ENVIRONMENT !== 'development'), + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + 'port' => (int) env('database.default.port', 3306), + ]; + + $this->tests = [ + 'DSN' => '', + 'hostname' => env('database.tests.hostname', env('database.default.hostname')), + 'username' => env('database.tests.username', env('database.default.username')), + 'password' => env('database.tests.password', env('database.default.password')), + 'database' => env('database.tests.database', env('database.default.database')), + 'DBDriver' => env('database.tests.DBDriver', env('database.default.DBDriver', 'MySQLi')), + 'DBPrefix' => env('database.tests.DBPrefix', 'db_'), + 'pConnect' => false, + 'DBDebug' => true, + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + 'port' => (int) env('database.tests.port', env('database.default.port', 3306)), + ]; + } } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 113b887..57a2926 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -230,6 +230,10 @@ $routes->get('reset_password', 'View\UserController::resetPassword'); //$routes->get('/blocked', 'View\UserController::blocked'); +$routes->get('confirm_authorized_user', 'View\AuthorizedUsersController::confirm'); +$routes->get('set_authorized_user_password/(:num)', 'View\AuthorizedUsersController::setPassword/$1'); +$routes->post('set_authorized_user_password/(:num)', 'View\AuthorizedUsersController::savePassword/$1'); + $routes->post('assign_class_student', 'View\StudentController::assignClassStudent'); $routes->post('remove_class_student', 'View\StudentController::removeClassStudent'); $routes->post('administrator/remove_class_student', 'View\StudentController::removeClassStudent'); // alias to avoid 404s @@ -401,6 +405,8 @@ $routes->get('teacher/progress/submit', 'ClassProgressController::create', ['fil $routes->post('teacher/progress/store', 'ClassProgressController::store', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/history', 'ClassProgressController::history', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/view/(:num)', 'ClassProgressController::view/$1', ['filter' => 'auth:teacher,teacher_assistant']); +$routes->get('teacher/progress/edit/(:num)', 'ClassProgressController::edit/$1', ['filter' => 'auth:teacher,teacher_assistant']); +$routes->post('teacher/progress/update/(:num)', 'ClassProgressController::update/$1', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/attachment/(:num)', 'ClassProgressController::attachment/$1', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('teacher/progress/attachment-file/(:num)', 'ClassProgressController::attachmentFile/$1', ['filter' => 'auth:teacher,teacher_assistant']); $routes->get('parent/progress', 'ParentProgressController::index', ['filter' => 'auth:parent']); diff --git a/app/Controllers/AdminProgressController.php b/app/Controllers/AdminProgressController.php index 965d9fe..3ef8c63 100644 --- a/app/Controllers/AdminProgressController.php +++ b/app/Controllers/AdminProgressController.php @@ -111,6 +111,23 @@ class AdminProgressController extends BaseController ); $sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays); $sectionSubjectCounts = $this->buildSectionSubjectCounts($rows); + $lowProgressSectionIds = []; + if ($expectedDays > 0) { + foreach ($filteredSections as $section) { + $sectionId = (int) ($section['class_section_id'] ?? 0); + if ($sectionId === 0) { + continue; + } + $stat = $sectionStats[$sectionId] ?? null; + $percent = $stat['percent'] ?? 0; + if ($stat === null) { + $percent = 0; + } + if ($percent < 50) { + $lowProgressSectionIds[] = $sectionId; + } + } + } return view('admin/class_progress_list', [ 'reportGroupsBySection' => $reportGroupsBySection, @@ -121,6 +138,7 @@ class AdminProgressController extends BaseController 'sectionStats' => $sectionStats, 'sectionSubjectCounts' => $sectionSubjectCounts, 'expectedDays' => $expectedDays, + 'lowProgressSectionIds' => $lowProgressSectionIds, ]); } @@ -414,13 +432,11 @@ class AdminProgressController extends BaseController $allowedSubjects = array_values(array_filter($allowedSubjects)); $counts = []; - $latestWeekBySection = []; $sectionClassMap = []; foreach ($rows as $row) { $sectionId = (int) ($row['class_section_id'] ?? 0); $subject = (string) ($row['subject'] ?? ''); - $weekStart = (string) ($row['week_start'] ?? ''); - if ($sectionId === 0 || $subject === '' || $weekStart === '') { + if ($sectionId === 0 || $subject === '') { continue; } if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) { @@ -429,45 +445,41 @@ class AdminProgressController extends BaseController if (! isset($sectionClassMap[$sectionId])) { $sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId); } - if ( - ! isset($latestWeekBySection[$sectionId]) - || $weekStart > $latestWeekBySection[$sectionId] - ) { - $latestWeekBySection[$sectionId] = $weekStart; - } } - $curriculumChapters = $this->buildCurriculumChapterMap(array_values(array_filter($sectionClassMap))); + $curriculumUnits = $this->buildCurriculumUnitMap(array_values(array_filter($sectionClassMap))); foreach ($rows as $row) { $sectionId = (int) ($row['class_section_id'] ?? 0); $subject = (string) ($row['subject'] ?? ''); - $weekStart = (string) ($row['week_start'] ?? ''); - if ($sectionId === 0 || $subject === '' || $weekStart === '') { + if ($sectionId === 0 || $subject === '') { continue; } if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) { continue; } - if (empty($latestWeekBySection[$sectionId]) || $weekStart !== $latestWeekBySection[$sectionId]) { - continue; - } $subjectSlug = $this->resolveSubjectSlug($subject); $classId = $sectionClassMap[$sectionId] ?? null; - $chapterSet = []; - if ($classId && $subjectSlug && ! empty($curriculumChapters[$classId][$subjectSlug])) { - $chapterSet = $curriculumChapters[$classId][$subjectSlug]; + $chapterToUnit = []; + if ($classId && $subjectSlug && ! empty($curriculumUnits[$classId][$subjectSlug]['chapter_to_unit'])) { + $chapterToUnit = $curriculumUnits[$classId][$subjectSlug]['chapter_to_unit']; + } + $unitKeys = $this->extractUnitKeys((string) ($row['unit_title'] ?? ''), $chapterToUnit); + foreach ($unitKeys as $unitKey) { + $key = $subjectSlug . '|' . $unitKey; + $counts[$sectionId][$key] = true; } - $counts[$sectionId] = ($counts[$sectionId] ?? 0) + $this->countChapterSegments( - (string) ($row['unit_title'] ?? ''), - $chapterSet - ); } - return $counts; + $totals = []; + foreach ($counts as $sectionId => $unitSet) { + $totals[$sectionId] = count($unitSet); + } + + return $totals; } - protected function buildCurriculumChapterMap(array $classIds): array + protected function buildCurriculumUnitMap(array $classIds): array { $classIds = array_values(array_filter(array_map('intval', $classIds))); if (empty($classIds)) { @@ -483,10 +495,15 @@ class AdminProgressController extends BaseController $classId = (int) ($row['class_id'] ?? 0); $subject = (string) ($row['subject'] ?? ''); $chapter = trim((string) ($row['chapter_name'] ?? '')); + $unitNumber = $row['unit_number'] ?? null; if ($classId === 0 || $subject === '' || $chapter === '') { continue; } - $map[$classId][$subject][$chapter] = true; + if ($unitNumber === null || $unitNumber === '') { + continue; + } + $unitKey = (string) $unitNumber; + $map[$classId][$subject]['chapter_to_unit'][$chapter] = $unitKey; } return $map; @@ -504,46 +521,93 @@ class AdminProgressController extends BaseController return null; } - protected function countChapterSegments(string $unitTitle, array $chapterSet): int + protected function countUnitSegments(string $unitTitle, array $chapterToUnit): int { $unitTitle = trim($unitTitle); if ($unitTitle === '') { - return 1; + return 0; } $parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== ''); if (! $parts) { - return 1; + return 0; } - $count = 0; $seen = []; foreach ($parts as $part) { - $chapter = $this->extractChapterFromSegment($part); - $key = $chapter !== '' ? $chapter : $part; - if (! empty($chapterSet) && $chapter !== '' && empty($chapterSet[$chapter])) { + [$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part); + $key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit); + if ($key === '') { $key = $part; } if (isset($seen[$key])) { continue; } $seen[$key] = true; - $count++; } - return $count > 0 ? $count : 1; + return count($seen); } - protected function extractChapterFromSegment(string $segment): string + protected function splitUnitChapterSegment(string $segment): array { $segment = trim($segment); if ($segment === '') { - return ''; + return ['', '']; } $pos = strrpos($segment, '/'); if ($pos === false) { - return $segment; + return [$segment, '']; } - return trim(substr($segment, $pos + 1)); + $unitPart = trim(substr($segment, 0, $pos)); + $chapterPart = trim(substr($segment, $pos + 1)); + return [$unitPart, $chapterPart]; + } + + protected function extractUnitKeys(string $unitTitle, array $chapterToUnit): array + { + $unitTitle = trim($unitTitle); + if ($unitTitle === '') { + return []; + } + $parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== ''); + if (! $parts) { + return []; + } + + $keys = []; + foreach ($parts as $part) { + [$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part); + $key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit); + if ($key === '') { + continue; + } + $keys[$key] = true; + } + + return array_keys($keys); + } + + protected function resolveUnitKey(string $unitPart, string $chapterPart, array $chapterToUnit): string + { + if ($chapterPart !== '' && ! empty($chapterToUnit[$chapterPart])) { + return (string) $chapterToUnit[$chapterPart]; + } + if (empty($chapterToUnit)) { + if (preg_match('/\bunit\s*(\d+)\b/i', $unitPart, $matches)) { + return (string) $matches[1]; + } + return ''; + } + if (preg_match('/\bunit\s*(\d+)\b/i', $unitPart, $matches)) { + return (string) $matches[1]; + } + if ($unitPart !== '') { + return $unitPart; + } + if ($chapterPart !== '') { + return $chapterPart; + } + return ''; } protected function buildSectionStat(int $submitted, int $expectedDays): array diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php index 96aa341..aee212d 100644 --- a/app/Controllers/AuthController.php +++ b/app/Controllers/AuthController.php @@ -469,6 +469,7 @@ class AuthController extends Controller // Generate a secure token for the password reset helper('text'); $token = bin2hex(random_bytes(48)); + $tokenHash = hash('sha256', $token); // Calculate the expiration time for the token (1 hour from now) $expires_at = Time::now()->addHours(1); @@ -477,7 +478,7 @@ class AuthController extends Controller $passwordResetModel = new PasswordResetModel(); $passwordResetModel->insert([ 'email' => $email, - 'token' => $token, + 'token' => $tokenHash, 'created_at' => Time::now(), 'expires_at' => $expires_at, ]); diff --git a/app/Controllers/ClassProgressController.php b/app/Controllers/ClassProgressController.php index b25f03c..c40ba30 100644 --- a/app/Controllers/ClassProgressController.php +++ b/app/Controllers/ClassProgressController.php @@ -98,6 +98,10 @@ class ClassProgressController extends BaseController return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } + if (! $this->hasIslamicUnitSelection()) { + return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.'); + } + $attachmentErrors = $this->validateAttachmentFiles($subjectSections); if (! empty($attachmentErrors)) { return redirect()->back()->withInput()->with('errors', $attachmentErrors); @@ -119,6 +123,32 @@ class ClassProgressController extends BaseController return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.'); } + $confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite'); + $existingReports = $this->reportModel + ->select('id') + ->where('class_section_id', $classSectionId) + ->where('week_start', $weekStart) + ->where('teacher_id', $teacherId) + ->findAll(); + + if (! $confirmOverwrite && ! empty($existingReports)) { + return redirect()->back() + ->withInput() + ->with('warning', 'A progress report already exists for this week, are you sure you want to override it?') + ->with('confirm_overwrite', true); + } + + if ($confirmOverwrite && ! empty($existingReports)) { + $existingIds = array_values(array_filter(array_map( + static fn (array $row): int => (int) ($row['id'] ?? 0), + $existingReports + ))); + if (! empty($existingIds)) { + $this->attachmentModel->whereIn('report_id', $existingIds)->delete(); + $this->reportModel->whereIn('id', $existingIds)->delete(); + } + } + $status = self::DEFAULT_STATUS; $reportsCreated = 0; @@ -276,6 +306,231 @@ class ClassProgressController extends BaseController ]); } + public function edit($id) + { + $teacherId = (int) session()->get('user_id'); + $row = $this->reportModel + ->select('class_progress_reports.*, cs.class_section_name') + ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') + ->where('class_progress_reports.id', (int) $id) + ->first(); + + if (! $row) { + throw new PageNotFoundException('Progress report not found.'); + } + + [$semester, $schoolYear] = $this->resolveCurrentTerm(); + $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); + if (empty($allowedTeacherIds)) { + if ($teacherId !== (int) $row['teacher_id']) { + throw new PageNotFoundException('Progress report not found.'); + } + $allowedTeacherIds = [(int) $row['teacher_id']]; + } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { + throw new PageNotFoundException('Progress report not found.'); + } + + $weeklyReports = $this->reportModel + ->select('class_progress_reports.*') + ->whereIn('teacher_id', $allowedTeacherIds) + ->where('class_section_id', $row['class_section_id']) + ->where('week_start', $row['week_start']) + ->orderBy('subject', 'ASC') + ->findAll(); + + $reportMap = []; + foreach ($weeklyReports as $report) { + $subject = (string) ($report['subject'] ?? ''); + if ($subject === '') { + continue; + } + $reportMap[$subject] = $report; + } + + $subjectReports = []; + foreach (self::SUBJECT_SECTIONS as $slug => $section) { + $subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; + $report = $reportMap[$subjectName] ?? null; + if (! $report) { + continue; + } + $parsed = $this->parseUnitChapterSummary((string) ($report['unit_title'] ?? '')); + $subjectReports[$slug] = [ + 'report_id' => (int) ($report['id'] ?? 0), + 'covered' => $report['covered'] ?? '', + 'homework' => $report['homework'] ?? '', + 'unit_title' => $report['unit_title'] ?? '', + 'unit_values' => $parsed['units'], + 'chapter_values' => $parsed['chapters'], + ]; + } + + $assignments = $this->loadTeacherSections($teacherId); + $classId = null; + $classSectionName = $row['class_section_name'] ?? ''; + foreach ($assignments as $assignment) { + if ((int) ($assignment['class_section_id'] ?? 0) === (int) $row['class_section_id']) { + $classId = $assignment['class_id'] ?? null; + $classSectionName = $assignment['class_section_name'] ?? $classSectionName; + break; + } + } + + $subjectCurriculum = []; + if ($classId) { + foreach (self::SUBJECT_SECTIONS as $slug => $section) { + $subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug); + } + } + + return view('teacher/class_progress_submit', [ + 'subjectSections' => self::SUBJECT_SECTIONS, + 'subjectCurriculum' => $subjectCurriculum, + 'classSectionId' => $row['class_section_id'], + 'classSectionName' => $classSectionName, + 'classId' => $classId, + 'sundayOptions' => [$row['week_start']], + 'defaultWeekStart' => $row['week_start'], + 'existingWeekEnd' => $row['week_end'], + 'existingReports' => $subjectReports, + 'isEdit' => true, + 'formAction' => base_url('teacher/progress/update/' . (int) $row['id']), + 'submitLabel' => 'Update Progress', + ]); + } + + public function update($id) + { + $teacherId = (int) session()->get('user_id'); + $row = $this->reportModel->find((int) $id); + if (! $row) { + throw new PageNotFoundException('Progress report not found.'); + } + + [$semester, $schoolYear] = $this->resolveCurrentTerm(); + $allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear); + if (empty($allowedTeacherIds)) { + if ($teacherId !== (int) $row['teacher_id']) { + throw new PageNotFoundException('Progress report not found.'); + } + $allowedTeacherIds = [(int) $row['teacher_id']]; + } elseif (! in_array($teacherId, $allowedTeacherIds, true)) { + throw new PageNotFoundException('Progress report not found.'); + } + + $subjectSections = self::SUBJECT_SECTIONS; + $rules = [ + 'class_section_id' => 'required|integer', + 'week_start' => 'required|valid_date[Y-m-d]', + 'week_end' => 'required|valid_date[Y-m-d]', + ]; + foreach ($subjectSections as $slug => $section) { + $rules["covered_$slug"] = 'required|string'; + $rules["homework_$slug"] = 'permit_empty|string'; + $rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]'; + $rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]'; + } + + if (! $this->validate($rules)) { + return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); + } + + if (! $this->hasIslamicUnitSelection()) { + return redirect()->back()->withInput()->with('error', 'Please select at least one Islamic Studies unit.'); + } + + $attachmentErrors = $this->validateAttachmentFiles($subjectSections); + if (! empty($attachmentErrors)) { + return redirect()->back()->withInput()->with('errors', $attachmentErrors); + } + + $weekStart = (string) $this->request->getPost('week_start'); + $weekEnd = (string) $this->request->getPost('week_end'); + if ($weekStart && ! $weekEnd) { + $weekEnd = $this->buildWeekEndFromStart($weekStart); + } + if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) { + return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.'); + } + + $classSectionId = (int) ($row['class_section_id'] ?? 0); + if ($classSectionId === 0) { + return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.'); + } + + $weeklyReports = $this->reportModel + ->select('class_progress_reports.*') + ->whereIn('teacher_id', $allowedTeacherIds) + ->where('class_section_id', $classSectionId) + ->where('week_start', $row['week_start']) + ->orderBy('subject', 'ASC') + ->findAll(); + + $reportMap = []; + foreach ($weeklyReports as $report) { + $subject = (string) ($report['subject'] ?? ''); + if ($subject === '') { + continue; + } + $reportMap[$subject] = $report; + } + + $reportsUpdated = 0; + $flagsInput = $this->request->getPost('flags'); + foreach ($subjectSections as $slug => $section) { + $covered = trim((string) $this->request->getPost("covered_$slug")); + if ($covered === '') { + continue; + } + $homework = trim((string) $this->request->getPost("homework_$slug")); + $unitTitle = $this->buildUnitChapterSummary($slug); + $subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; + $existing = $reportMap[$subjectName] ?? null; + if ($unitTitle === null && $existing) { + $unitTitle = $existing['unit_title'] ?? null; + } + + $data = [ + 'class_section_id' => $classSectionId, + 'week_start' => $weekStart, + 'week_end' => $weekEnd, + 'subject' => $subjectName, + 'unit_title' => $unitTitle, + 'covered' => $covered, + 'homework' => $homework ?: null, + ]; + if ($flagsInput !== null) { + $data['flags_json'] = $this->normalizeFlags($flagsInput); + } + + if ($existing) { + $this->reportModel->update((int) $existing['id'], $data); + $reportId = (int) $existing['id']; + } else { + $data['teacher_id'] = $teacherId; + $data['status'] = self::DEFAULT_STATUS; + $reportId = (int) $this->reportModel->insert($data, true); + } + + $attachmentField = "attachment_$slug"; + $attachments = $this->request->getFileMultiple($attachmentField) ?? []; + $storedAttachments = $this->storeAttachments($reportId, $attachments); + if (! empty($storedAttachments)) { + $this->attachmentModel->insertBatch($storedAttachments); + if (empty($existing['attachment_path'] ?? '')) { + $this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]); + } + } + $reportsUpdated++; + } + + if ($reportsUpdated === 0) { + return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.'); + } + + return redirect()->to('teacher/progress/history')->with('success', 'Progress reports updated.'); + } + public function attachment($id) { $row = $this->reportModel->find((int)$id); @@ -447,6 +702,45 @@ class ClassProgressController extends BaseController return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary; } + protected function hasIslamicUnitSelection(): bool + { + $unitValues = array_map('trim', (array) $this->request->getPost('unit_islamic')); + foreach ($unitValues as $value) { + if ($value !== '') { + return true; + } + } + return false; + } + + protected function parseUnitChapterSummary(string $summary): array + { + $summary = trim($summary); + if ($summary === '') { + return ['units' => [], 'chapters' => []]; + } + + $units = []; + $chapters = []; + $segments = preg_split('/\s*;\s*/', $summary, -1, PREG_SPLIT_NO_EMPTY); + foreach ($segments as $segment) { + $segment = trim($segment); + if ($segment === '') { + continue; + } + $parts = preg_split('/\s*\/\s*/', $segment, 2); + if (count($parts) === 2) { + $units[] = trim($parts[0]); + $chapters[] = trim($parts[1]); + } else { + $units[] = $segment; + $chapters[] = ''; + } + } + + return ['units' => $units, 'chapters' => $chapters]; + } + protected function buildSundayOptions(int $count = 12): array { $range = $this->resolveProgressDateRange(); diff --git a/app/Controllers/ParentProgressController.php b/app/Controllers/ParentProgressController.php index e32aede..de6f577 100644 --- a/app/Controllers/ParentProgressController.php +++ b/app/Controllers/ParentProgressController.php @@ -29,18 +29,12 @@ class ParentProgressController extends BaseController public function index() { - $sectionIds = $this->getParentSectionIds(); - $sectionOptions = $this->buildSectionOptions($sectionIds); + $students = $this->getParentStudents(); + $sectionIds = array_values(array_unique(array_filter(array_map( + static fn (array $student): int => (int) ($student['class_section_id'] ?? 0), + $students + )))); $subjectSections = ClassProgressController::SUBJECT_SECTIONS; - $selectedSectionId = (int) $this->request->getGet('class_section_id'); - $validSectionIds = array_keys($sectionOptions); - - if ($selectedSectionId === 0 && ! empty($validSectionIds)) { - $selectedSectionId = $validSectionIds[0]; - } - if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) { - $selectedSectionId = $validSectionIds[0] ?? null; - } $rows = []; if (! empty($sectionIds)) { @@ -50,23 +44,32 @@ class ParentProgressController extends BaseController ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left') ->whereIn('class_progress_reports.class_section_id', $sectionIds); - if ($selectedSectionId) { - $builder->where('class_progress_reports.class_section_id', $selectedSectionId); - } - $rows = $builder ->orderBy('week_start', 'DESC') ->findAll(); } - $reportGroups = $this->groupReportsByWeek($rows); + $studentReportGroups = []; + foreach ($students as $student) { + $studentId = (int) ($student['student_id'] ?? 0); + if ($studentId === 0) { + continue; + } + $classSectionId = (int) ($student['class_section_id'] ?? 0); + $studentRows = $classSectionId + ? array_values(array_filter( + $rows, + static fn (array $row): bool => (int) ($row['class_section_id'] ?? 0) === $classSectionId + )) + : []; + $studentReportGroups[$studentId] = $this->groupReportsByWeek($studentRows); + } return view('parent/class_progress_list', [ - 'reportGroups' => $reportGroups, + 'students' => $students, + 'studentReportGroups' => $studentReportGroups, 'subjectSections' => $subjectSections, - 'classSectionOptions' => $sectionOptions, - 'selectedSectionId' => $selectedSectionId, - 'hasSections' => ! empty($sectionIds), + 'hasStudents' => ! empty($students), ]); } @@ -198,20 +201,53 @@ class ParentProgressController extends BaseController return $options; } + protected function getParentStudents(): array + { + $parentId = (int) session()->get('user_id'); + if ($parentId === 0) { + return []; + } + + $rows = $this->db->table('enrollments e') + ->select('e.student_id, e.class_section_id, e.updated_at, e.created_at, s.firstname, s.lastname, cs.class_section_name') + ->join('students s', 's.id = e.student_id') + ->join('classSection cs', 'cs.class_section_id = e.class_section_id', 'left') + ->where('e.parent_id', $parentId) + ->where('e.is_withdrawn', 0) + ->orderBy('e.updated_at', 'DESC') + ->orderBy('e.created_at', 'DESC') + ->get() + ->getResultArray(); + + $students = []; + foreach ($rows as $row) { + $studentId = (int) ($row['student_id'] ?? 0); + if ($studentId === 0 || isset($students[$studentId])) { + continue; + } + $students[$studentId] = $row; + } + + return array_values($students); + } + protected function groupReportsByWeek(array $rows): array { $reportGroups = []; foreach ($rows as $row) { $row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; - $key = $row['week_start'] ?? ''; - if ($key === '') { + $weekStart = $row['week_start'] ?? ''; + $sectionId = (int) ($row['class_section_id'] ?? 0); + if ($weekStart === '' || $sectionId === 0) { continue; } + $key = $weekStart . ':' . $sectionId; if (! isset($reportGroups[$key])) { $reportGroups[$key] = [ 'week_start' => $row['week_start'] ?? '', 'week_end' => $row['week_end'] ?? '', 'class_section_name' => $row['class_section_name'] ?? '', + 'class_section_id' => $sectionId, 'reports' => [], ]; } diff --git a/app/Controllers/View/AdministratorController.php b/app/Controllers/View/AdministratorController.php index 6991201..61fb1be 100644 --- a/app/Controllers/View/AdministratorController.php +++ b/app/Controllers/View/AdministratorController.php @@ -28,6 +28,8 @@ use App\Models\ScoreCommentModel; use App\Models\SemesterScoreModel; use App\Models\TeacherClassModel; use App\Models\TeacherSubmissionNotificationHistoryModel; +use App\Models\ExamDraftModel; +use App\Models\HomeworkModel; use App\Services\SemesterRangeService; use CodeIgniter\Events\Events; @@ -696,15 +698,26 @@ class AdministratorController extends BaseController public function teacherSubmissionsReport() { - $semester = (string)($this->semester ?? ''); - $schoolYear = (string)($this->schoolYear ?? ''); + $semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? ''); + $schoolYear = (string)($this->configModel->getConfig('school_year') ?? $this->schoolYear ?? ''); + $semesterResolver = new SemesterRangeService($this->configModel); + $semesterNorm = $semesterResolver->normalizeSemester($semester); + $semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester; + $semesterCandidates = $this->buildSemesterCandidates($semesterFilter); + $lowProgressRaw = (string) $this->request->getGet('low_progress_sections'); + $lowProgressSectionIds = array_values(array_unique(array_filter(array_map( + 'intval', + preg_split('/\s*,\s*/', $lowProgressRaw, -1, PREG_SPLIT_NO_EMPTY) + )))); $scoreComments = new ScoreCommentModel(); $semesterScores = new SemesterScoreModel(); $attendanceDays = new AttendanceDayModel(); + $examDrafts = new ExamDraftModel(); + $homeworkModel = new HomeworkModel(); $historyModel = new TeacherSubmissionNotificationHistoryModel(); - $assignmentRows = $this->db->table('teacher_class tc') + $assignmentQuery = $this->db->table('teacher_class tc') ->select([ 'tc.class_section_id', 'cs.class_section_name', @@ -715,11 +728,91 @@ class AdministratorController extends BaseController ]) ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left') ->join('users u', 'u.id = tc.teacher_id', 'left') - ->where('tc.school_year', $schoolYear) - ->where('tc.semester', $semester) - ->orderBy('cs.class_section_name', 'ASC') - ->get() - ->getResultArray(); + ->orderBy('cs.class_section_name', 'ASC'); + + $filteredQuery = clone $assignmentQuery; + if ($schoolYear !== '') { + $filteredQuery = $filteredQuery->where('tc.school_year', $schoolYear); + } + if (!empty($semesterCandidates)) { + $filteredQuery = $filteredQuery->whereIn('tc.semester', $semesterCandidates); + } + + $assignmentRows = $filteredQuery->get()->getResultArray(); + if (empty($assignmentRows) && ($schoolYear !== '' || $semester !== '')) { + $assignmentRows = $assignmentQuery->get()->getResultArray(); + } + + $studentCounts = $this->studentClassModel->getStudentCountsBySection($schoolYear !== '' ? $schoolYear : null); + $sectionRows = $this->classSectionModel + ->select('class_section_id, class_section_name') + ->orderBy('class_section_name', 'ASC') + ->findAll(); + $sectionMap = []; + foreach ($sectionRows as $sectionRow) { + $sectionId = (int) ($sectionRow['class_section_id'] ?? 0); + if ($sectionId <= 0) { + continue; + } + if (empty($studentCounts[$sectionId])) { + continue; + } + $sectionMap[$sectionId] = $sectionRow['class_section_name'] ?? "Section {$sectionId}"; + } + $sectionIds = array_keys($sectionMap); + + [$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds); + $examDraftCounts = []; + $examDraftDeadline = $this->resolveExamDraftDeadline($semester, $schoolYear); + $homeworkCounts = []; + if (! empty($sectionIds)) { + $draftBuilder = $examDrafts + ->select('class_section_id') + ->whereIn('class_section_id', $sectionIds); + if ($schoolYear !== '') { + $draftBuilder->where('school_year', $schoolYear); + } + if (!empty($semesterCandidates)) { + $draftBuilder->whereIn('semester', $semesterCandidates); + } + if ($this->db->fieldExists('is_legacy', 'exam_drafts')) { + $draftBuilder->where('is_legacy', 0); + } + $draftRows = $draftBuilder->findAll(); + foreach ($draftRows as $draft) { + $sectionId = (int) ($draft['class_section_id'] ?? 0); + if ($sectionId <= 0) { + continue; + } + $examDraftCounts[$sectionId] = ($examDraftCounts[$sectionId] ?? 0) + 1; + } + + $homeworkBuilder = $homeworkModel + ->select('class_section_id, homework_index') + ->whereIn('class_section_id', $sectionIds); + if ($schoolYear !== '') { + $homeworkBuilder->where('school_year', $schoolYear); + } + if (!empty($semesterCandidates)) { + $homeworkBuilder->whereIn('semester', $semesterCandidates); + } + $homeworkRows = $homeworkBuilder + ->where('score IS NOT NULL', null, false) + ->where('score !=', '') + ->groupBy('class_section_id, homework_index') + ->findAll(); + foreach ($homeworkRows as $row) { + $sectionId = (int) ($row['class_section_id'] ?? 0); + if ($sectionId <= 0) { + continue; + } + $homeworkCounts[$sectionId] = ($homeworkCounts[$sectionId] ?? 0) + 1; + } + } + + if (empty($lowProgressSectionIds)) { + $lowProgressSectionIds = $this->resolveLowProgressSectionIds($sectionIds); + } $teachersBySection = []; foreach ($assignmentRows as $assignment) { @@ -745,7 +838,7 @@ class AdministratorController extends BaseController $entry = &$teachersBySection[$sectionId]; if (!isset($entry)) { $entry = [ - 'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}", + 'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"), 'teachers' => [], ]; } @@ -765,35 +858,49 @@ class AdministratorController extends BaseController $missingItemCount = 0; $allTeacherIds = []; $allClassSectionIds = []; - foreach ($teachersBySection as $classSectionId => $section) { + $examTerm = $this->resolveExamTermLabel($semester); + $examScoreField = $examTerm === 'final' ? 'final_exam_score' : 'midterm_exam_score'; + + foreach ($sectionMap as $classSectionId => $sectionName) { $classSectionId = (int)$classSectionId; if ($classSectionId <= 0) { continue; } - $studentEntries = $this->studentClassModel + $studentQuery = $this->studentClassModel ->select('student_id') ->where('class_section_id', $classSectionId) - ->where('semester', $semester) - ->where('school_year', $schoolYear) - ->findAll(); + ->where('school_year', $schoolYear); + if (!empty($semesterCandidates)) { + $studentQuery->whereIn('semester', $semesterCandidates); + } + $studentEntries = $studentQuery->findAll(); + if (empty($studentEntries)) { + $studentEntries = $this->studentClassModel + ->select('student_id') + ->where('class_section_id', $classSectionId) + ->where('school_year', $schoolYear) + ->findAll(); + } $studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries)); $expected = count($studentIds); $midtermStudents = []; $participationStudents = []; if ($classSectionId > 0) { - $scoreRecords = $semesterScores + $scoreQuery = $semesterScores ->where('class_section_id', $classSectionId) - ->where('semester', $semester) - ->where('school_year', $schoolYear) - ->findAll(); + ->where('school_year', $schoolYear); + if (!empty($semesterCandidates)) { + $scoreQuery->whereIn('semester', $semesterCandidates); + } + $scoreRecords = $scoreQuery->findAll(); foreach ($scoreRecords as $score) { $sid = (int)($score['student_id'] ?? 0); if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) { continue; } - $midtermValue = trim((string)($score['midterm_exam_score'] ?? '')); + $midtermValue = trim((string)($score[$examScoreField] ?? '')); if ($midtermValue !== '') { $midtermStudents[$sid] = true; } @@ -807,13 +914,15 @@ class AdministratorController extends BaseController $midtermCommentStudents = []; $ptapCommentStudents = []; if (!empty($studentIds)) { - $comments = $scoreComments + $commentQuery = $scoreComments ->select('student_id, score_type, comment') ->whereIn('student_id', $studentIds) - ->where('semester', $semester) ->where('school_year', $schoolYear) - ->whereIn('score_type', ['midterm', 'ptap']) - ->findAll(); + ->whereIn('score_type', [$examTerm, 'ptap']); + if (!empty($semesterCandidates)) { + $commentQuery->whereIn('semester', $semesterCandidates); + } + $comments = $commentQuery->findAll(); foreach ($comments as $comment) { $sid = (int)($comment['student_id'] ?? 0); if ($sid <= 0) { @@ -824,7 +933,7 @@ class AdministratorController extends BaseController continue; } $type = strtolower(trim((string)($comment['score_type'] ?? ''))); - if ($type === 'midterm') { + if ($type === $examTerm) { $midtermCommentStudents[$sid] = true; } if ($type === 'ptap') { @@ -833,14 +942,17 @@ class AdministratorController extends BaseController } } - $attendanceRow = $attendanceDays + $attendanceQuery = $attendanceDays ->where('class_section_id', $classSectionId) - ->where('semester', $semester) ->where('school_year', $schoolYear) - ->where('date', $today) - ->first(); + ->where('date', $today); + if (!empty($semesterCandidates)) { + $attendanceQuery->whereIn('semester', $semesterCandidates); + } + $attendanceRow = $attendanceQuery->first(); $attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true); + $section = $teachersBySection[$classSectionId] ?? ['teachers' => []]; $teacherList = $section['teachers'] ?? []; if (!empty($teacherList)) { usort($teacherList, function ($a, $b) { @@ -861,18 +973,27 @@ class AdministratorController extends BaseController $participationStatus = $this->submissionStatus(count($participationStudents), $expected); $ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected); $attendanceStatus = $this->attendanceStatus($attendanceSubmitted); + $progressSubmitted = (int) ($progressSubmittedBySection[$classSectionId] ?? 0); + $classProgressStatus = $this->progressStatus($progressSubmitted, $progressExpectedWeeks); + $draftSubmitted = (int) ($examDraftCounts[$classSectionId] ?? 0); + $examDraftStatus = $this->draftStatus($draftSubmitted, $examDraftDeadline); + $homeworkSubmitted = (int) ($homeworkCounts[$classSectionId] ?? 0); + $homeworkStatus = $this->homeworkStatus($homeworkSubmitted); $statusDetails = [ 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, + 'class_progress_status' => $classProgressStatus, + 'exam_draft_status' => $examDraftStatus, + 'homework_status' => $homeworkStatus, ]; - $missingItemsForSection = $this->buildMissingItems($statusDetails); + $missingItemsForSection = $this->buildMissingItems($statusDetails, $semester); $missingItemCount += count($missingItemsForSection); $totalStatuses += count($statusDetails); $rows[] = [ - 'class_section' => $section['class_section'] ?? "Section {$classSectionId}", + 'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"), 'class_section_id' => $classSectionId, 'teachers' => $teacherList, 'midterm_score_status' => $midtermScoreStatus, @@ -880,6 +1001,9 @@ class AdministratorController extends BaseController 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, 'attendance_status' => $attendanceStatus, + 'class_progress_status' => $classProgressStatus, + 'exam_draft_status' => $examDraftStatus, + 'homework_status' => $homeworkStatus, 'missing_items' => $missingItemsForSection, 'student_count' => $expected, ]; @@ -941,15 +1065,181 @@ class AdministratorController extends BaseController 'schoolYear' => $schoolYear, 'notificationHistory' => $historyMap, 'summary' => $summary, + 'lowProgressSectionIds' => $lowProgressSectionIds, ]); } + private function resolveLowProgressSectionIds(array $sectionIds): array + { + [$expectedWeeks, $submittedBySection] = $this->buildClassProgressStats($sectionIds); + if ($expectedWeeks <= 0) { + return []; + } + + $lowProgressSectionIds = []; + foreach ($sectionIds as $sectionId) { + $submitted = (int) ($submittedBySection[$sectionId] ?? 0); + $percent = ($submitted / $expectedWeeks) * 100; + if ($percent < 50) { + $lowProgressSectionIds[] = $sectionId; + } + } + + return $lowProgressSectionIds; + } + + private function buildClassProgressStats(array $sectionIds): array + { + $sectionIds = array_values(array_unique(array_filter(array_map('intval', $sectionIds)))); + if (empty($sectionIds)) { + return [0, []]; + } + + $semesterResolver = new SemesterRangeService($this->configModel); + $schoolYear = (string)($this->configModel->getConfig('school_year') ?? ''); + $semester = (string)($this->configModel->getConfig('semester') ?? ''); + $schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string)($this->configModel->getConfig('school_year') ?? ''); + [$rangeStart, $rangeEnd] = $semesterResolver->getSchoolYearRange($schoolYearForRange); + $semesterNorm = $semesterResolver->normalizeSemester($semester); + if ($semesterNorm !== '' && $schoolYearForRange !== '') { + $semRange = $semesterResolver->getSemesterRange($schoolYearForRange, $semesterNorm); + if ($semRange) { + [$rangeStart, $rangeEnd] = $semRange; + } + } + + $dateList = []; + try { + $start = new \DateTimeImmutable($rangeStart); + $end = new \DateTimeImmutable($rangeEnd); + $cursor = $start; + $w = (int) $cursor->format('w'); + if ($w !== 0) { + $cursor = $cursor->modify('next sunday'); + } + while ($cursor <= $end) { + $dateList[] = $cursor->format('Y-m-d'); + $cursor = $cursor->modify('+7 days'); + } + } catch (\Throwable $e) { + $dateList = []; + } + + $noSchoolDays = []; + $events = []; + try { + $calendarModel = new \App\Models\CalendarModel(); + $events = $calendarModel->getEvents(); + } catch (\Throwable $e) { + $events = []; + } + foreach ($events as $event) { + $d = substr((string) ($event['date'] ?? ''), 0, 10); + if ($d === '' || empty($event['no_school'])) { + continue; + } + if ($d < $rangeStart || $d > $rangeEnd) { + continue; + } + $eventYear = trim((string) ($event['school_year'] ?? '')); + if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) { + continue; + } + $noSchoolDays[$d] = true; + } + + $anchorSundayYmd = ''; + try { + $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); + $tzObj = new \DateTimeZone($tzName ?: 'UTC'); + } catch (\Throwable $e) { + try { + $tzObj = new \DateTimeZone(user_timezone() ?: 'UTC'); + } catch (\Throwable $e2) { + $tzObj = new \DateTimeZone('UTC'); + } + } + try { + $nowDate = new \DateTime('now', $tzObj); + } catch (\Throwable $e) { + $nowDate = new \DateTime('now'); + } + $weekday = (int) $nowDate->format('w'); + $anchorSundayYmd = $weekday === 0 + ? $nowDate->format('Y-m-d') + : $nowDate->modify('next sunday')->format('Y-m-d'); + + $activeDatesSet = []; + if (! empty($dateList) && $anchorSundayYmd !== '') { + foreach ($dateList as $d) { + if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) { + $activeDatesSet[$d] = true; + } + } + } + $expectedWeeks = count($activeDatesSet); + if ($expectedWeeks === 0) { + return [0, []]; + } + + $builder = $this->db->table('class_progress_reports') + ->select('class_section_id, week_start') + ->whereIn('class_section_id', $sectionIds); + if (! empty($activeDatesSet)) { + $builder->whereIn('week_start', array_keys($activeDatesSet)); + } + $rows = $builder->get()->getResultArray(); + + $submittedBySection = []; + foreach ($rows as $row) { + $sectionId = (int) ($row['class_section_id'] ?? 0); + $weekStart = (string) ($row['week_start'] ?? ''); + if ($sectionId === 0 || $weekStart === '' || empty($activeDatesSet[$weekStart])) { + continue; + } + $submittedBySection[$sectionId][$weekStart] = true; + } + + $counts = []; + foreach ($sectionIds as $sectionId) { + $counts[$sectionId] = isset($submittedBySection[$sectionId]) + ? count($submittedBySection[$sectionId]) + : 0; + } + + return [$expectedWeeks, $counts]; + } + public function sendTeacherSubmissionNotifications() {$notify = $this->request->getPost('notify'); if (!is_array($notify)) { return redirect()->back()->with('info', 'Select at least one teacher to notify.'); } + $semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? ''); $missingItemsPayload = $this->request->getPost('missing_items') ?? []; + $homeworkNotifyAll = (bool) $this->request->getPost('homework_notify_all'); + $examTerm = $this->resolveExamTermLabel($semester); + $examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores'; + $examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments'; + $forcedItems = []; + if ($this->request->getPost('notify_midterm_score')) { + $forcedItems[] = $examScoreLabel; + } + if ($this->request->getPost('notify_midterm_comment')) { + $forcedItems[] = $examCommentLabel; + } + if ($this->request->getPost('notify_participation')) { + $forcedItems[] = 'participation'; + } + if ($this->request->getPost('notify_ptap_comment')) { + $forcedItems[] = 'PTAP comments'; + } + if ($this->request->getPost('notify_class_progress')) { + $forcedItems[] = 'class progress'; + } + if ($this->request->getPost('notify_exam_draft')) { + $forcedItems[] = 'exam draft'; + } $targets = []; foreach ($notify as $sectionIdRaw => $teachers) { @@ -1006,6 +1296,9 @@ class AdministratorController extends BaseController $historyModel = new TeacherSubmissionNotificationHistoryModel(); $scoreUrl = site_url('/'); + $progressUrl = site_url('teacher/progress/history'); + $examDraftUrl = site_url('teacher/exam-drafts'); + $homeworkUrl = site_url('teacher/addHomework'); $sentCount = 0; $failCount = 0; @@ -1021,6 +1314,13 @@ class AdministratorController extends BaseController $subject = "Reminder: Complete submissions for {$sectionName}"; $missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? ''; $missingItems = $this->parseMissingItemsPayload((string)$missingPayload); + $selectedItems = $forcedItems; + if ($homeworkNotifyAll && !in_array('homework', $selectedItems, true)) { + $selectedItems[] = 'homework'; + } + if (!empty($selectedItems)) { + $missingItems = array_values(array_unique($selectedItems)); + } if (!empty($missingItems)) { $missingText = htmlspecialchars( $this->formatMissingItemsText($missingItems), @@ -1033,10 +1333,45 @@ class AdministratorController extends BaseController } $subject = "Reminder: Complete submissions for {$sectionName}"; + $progressNote = ''; + if (in_array('class progress', $missingItems, true)) { + $progressNote = "

Class progress submissions can be updated at Teacher Progress History.

"; + } + $examDraftNote = ''; + if (in_array('exam draft', $missingItems, true)) { + $semesterLabel = strtolower(trim((string) $semester)); + if ($semesterLabel === 'fall') { + $draftLabel = 'midterm exam draft'; + } elseif ($semesterLabel === 'spring') { + $draftLabel = 'final exam draft'; + } else { + $draftLabel = 'exam draft'; + } + $examDraftNote = "

" . ucfirst($draftLabel) . " submissions can be updated at Teacher Exam Drafts.

"; + } + $homeworkNote = ''; + if (in_array('homework', $missingItems, true)) { + $homeworkNote = "

Homework scores can be submitted at Teacher Homework.

"; + } + $hasScoreItems = (bool) array_intersect($missingItems, [ + 'midterm scores', + 'midterm comments', + 'final scores', + 'final comments', + 'participation', + 'PTAP comments', + 'homework', + ]); + $nonScoreOnly = ! empty($missingItems) && ! $hasScoreItems; $body = "

Dear {$teacherName},

" - . "

Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.

" + . "

Administration is gently reminding you to wrap up any remaining " + . ($nonScoreOnly ? "submissions for {$sectionName}." : "score submissions, comments, and related items for {$sectionName}.") + . "

" . $missingNote - . "

Visit Teacher Score Submission to address any remaining items.

" + . $progressNote + . $examDraftNote + . $homeworkNote + . ($nonScoreOnly ? '' : "

Visit Teacher Score Submission to address any remaining items.

") . "

Thank you,
Al Rahma Administration

"; $email = $teacher['email'] ?? ''; @@ -1098,6 +1433,116 @@ class AdministratorController extends BaseController ]; } + private function progressStatus(int $submitted, int $expected): array + { + if ($expected <= 0) { + return [ + 'label' => 'N/A', + 'badge' => 'bg-secondary', + 'detail' => '', + 'completed' => true, + ]; + } + $completed = $submitted >= $expected; + return [ + 'label' => $completed ? 'Submitted' : 'Missing', + 'badge' => $completed ? 'bg-success' : 'bg-danger', + 'detail' => "{$submitted}/{$expected}", + 'completed' => $completed, + ]; + } + + private function homeworkStatus(int $submitted): array + { + $completed = $submitted > 0; + return [ + 'label' => $completed ? 'Submitted' : 'Missing', + 'badge' => $completed ? 'bg-success' : 'bg-danger', + 'detail' => $completed ? (string) $submitted : '0', + 'completed' => $completed, + ]; + } + + private function draftStatus(int $submitted, ?\DateTimeImmutable $deadline): array + { + if ($deadline !== null) { + $today = new \DateTimeImmutable('today'); + if ($today < $deadline) { + return [ + 'label' => 'Pending', + 'badge' => 'bg-secondary', + 'detail' => 'Not due', + 'completed' => true, + ]; + } + } + $completed = $submitted > 0; + return [ + 'label' => $completed ? 'Submitted' : 'Missing', + 'badge' => $completed ? 'bg-success' : 'bg-danger', + 'detail' => $completed ? (string) $submitted : '0', + 'completed' => $completed, + ]; + } + + private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable + { + $semesterKey = strtolower(trim($semester)); + if ($semesterKey === 'fall') { + $deadlineValue = (string)($this->configModel->getConfig('fall_exam_deadline') ?? ''); + } elseif ($semesterKey === 'spring') { + $deadlineValue = (string)($this->configModel->getConfig('spring_exam_deadline') ?? ''); + } else { + return null; + } + $deadlineValue = trim($deadlineValue); + if ($deadlineValue === '') { + return null; + } + try { + $deadline = new \DateTimeImmutable($deadlineValue); + } catch (\Throwable $e) { + return null; + } + if ($schoolYear !== '' && preg_match('/^\d{4}-\d{4}$/', $schoolYear)) { + $deadlineYear = $deadline->format('Y'); + if ($deadlineYear === '1970') { + return null; + } + } + return $deadline->setTime(0, 0, 0); + } + + private function resolveExamTermLabel(string $semester): string + { + $semesterKey = strtolower(trim($semester)); + if ($semesterKey === '') { + return 'midterm'; + } + if (str_contains($semesterKey, 'spring')) { + return 'final'; + } + if (str_contains($semesterKey, 'fall')) { + return 'midterm'; + } + return 'midterm'; + } + + private function buildSemesterCandidates(string $semester): array + { + $semester = trim((string) $semester); + if ($semester === '') { + return []; + } + $candidates = [ + $semester, + strtolower($semester), + strtoupper($semester), + ucfirst(strtolower($semester)), + ]; + $candidates = array_values(array_unique(array_filter($candidates, static fn ($v) => $v !== ''))); + return $candidates; + } private function attendanceStatus(bool $submitted): array { return [ @@ -1107,14 +1552,20 @@ class AdministratorController extends BaseController ]; } - private function buildMissingItems(array $statusMap): array + private function buildMissingItems(array $statusMap, string $semester): array { + $examTerm = $this->resolveExamTermLabel($semester); + $examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores'; + $examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments'; $labels = [ - 'midterm_score_status' => 'midterm scores', - 'midterm_comment_status' => 'midterm comments', + 'midterm_score_status' => $examScoreLabel, + 'midterm_comment_status' => $examCommentLabel, 'participation_status' => 'participation', 'ptap_comment_status' => 'PTAP comments', 'attendance_status' => 'attendance', + 'class_progress_status' => 'class progress', + 'exam_draft_status' => 'exam draft', + 'homework_status' => 'homework', ]; $items = []; diff --git a/app/Controllers/View/AssignmentController.php b/app/Controllers/View/AssignmentController.php index 491da9c..1a44497 100644 --- a/app/Controllers/View/AssignmentController.php +++ b/app/Controllers/View/AssignmentController.php @@ -119,7 +119,12 @@ class AssignmentController extends BaseController } $students = []; + $seenStudentIds = []; foreach ($studentClasses as $studentClass) { + $sid = (int)($studentClass['student_id'] ?? 0); + if ($sid <= 0 || isset($seenStudentIds[$sid])) { + continue; + } if ($sectionSemester === '' && !empty($studentClass['semester'])) { $sectionSemester = (string)$studentClass['semester']; } @@ -149,6 +154,7 @@ class AssignmentController extends BaseController 'tuition_paid' => esc($student['tuition_paid'] ? 'Yes' : 'No'), 'school_id' => esc($student['school_id']), ]; + $seenStudentIds[$sid] = true; } $sectionSemesterDisplay = $sectionSemester !== '' ? $sectionSemester : ((string)($this->semester ?? '')); diff --git a/app/Controllers/View/AttendanceController.php b/app/Controllers/View/AttendanceController.php index f3b0962..3fdb1d3 100644 --- a/app/Controllers/View/AttendanceController.php +++ b/app/Controllers/View/AttendanceController.php @@ -1004,9 +1004,13 @@ public function showUpdateAttendanceForm() } $hasRoster = false; + $seenStudents = []; foreach ($students as $sc) { $studentId = (int)$sc['student_id']; + if ($studentId <= 0 || isset($seenStudents[$studentId])) { + continue; + } $student = $this->studentModel ->select('id, firstname, lastname, school_id') ->find($studentId); @@ -1014,6 +1018,7 @@ public function showUpdateAttendanceForm() $studentsBySection[$secCode][] = $student; $hasRoster = true; + $seenStudents[$studentId] = true; // Attendance history $qb = $this->attendanceDataModel diff --git a/app/Controllers/View/AuthorizedUsersController.php b/app/Controllers/View/AuthorizedUsersController.php index 5e9ae4a..a6cc46c 100644 --- a/app/Controllers/View/AuthorizedUsersController.php +++ b/app/Controllers/View/AuthorizedUsersController.php @@ -10,6 +10,8 @@ use CodeIgniter\I18n\Time; class AuthorizedUsersController extends ResourceController { + private const TOKEN_TTL_HOURS = 24; + protected $userModel; protected $authorizedUserModel; @@ -18,6 +20,30 @@ class AuthorizedUsersController extends ResourceController $this->userModel = new UserModel(); $this->authorizedUserModel = new AuthorizedUserModel(); } + + private function requireLogin() + { + if (!session()->get('is_logged_in')) { + return $this->failUnauthorized('Authentication required.'); + } + + return null; + } + + private function requireOwnership(array $authorizedUser) + { + $userId = (int) session()->get('user_id'); + if ($userId <= 0 || (int) ($authorizedUser['user_id'] ?? 0) !== $userId) { + return $this->failForbidden('You do not have access to this resource.'); + } + + return null; + } + + private function hashToken(string $token): string + { + return hash('sha256', $token); + } /** * Return a list of authorized users for the logged-in main user. * @@ -25,7 +51,10 @@ class AuthorizedUsersController extends ResourceController */ public function index() { - + if ($resp = $this->requireLogin()) { + return $resp; + } + $userId = session()->get('user_id'); $authorizedUsers = $this->authorizedUserModel->where('user_id', $userId)->findAll(); @@ -40,12 +69,20 @@ class AuthorizedUsersController extends ResourceController */ public function show($id = null) { + if ($resp = $this->requireLogin()) { + return $resp; + } + $authorizedUser = $this->authorizedUserModel->find($id); if (!$authorizedUser) { return $this->failNotFound('Authorized user not found.'); } + if ($resp = $this->requireOwnership($authorizedUser)) { + return $resp; + } + return $this->respond($authorizedUser); } @@ -56,6 +93,10 @@ class AuthorizedUsersController extends ResourceController */ public function create() { + if ($resp = $this->requireLogin()) { + return $resp; + } + $email = strtolower($this->request->getPost('email')); // Validate email @@ -66,19 +107,20 @@ class AuthorizedUsersController extends ResourceController $user = $this->userModel->where('email', $email)->first(); if (!$user) { - return $this->failNotFound('No user found with this email.'); + return $this->respondCreated(['message' => 'Authorized user added. A confirmation email has been sent.']); } // Generate a token for confirmation helper('text'); $token = bin2hex(random_bytes(48)); + $tokenHash = $this->hashToken($token); // Add entry to the authorized_users table $this->authorizedUserModel->insert([ 'user_id' => session()->get('user_id'), // Main user ID 'authorized_user_id' => $user['id'], 'email' => $email, - 'token' => $token, + 'token' => $tokenHash, 'status' => 'Pending' ]); @@ -96,6 +138,10 @@ class AuthorizedUsersController extends ResourceController */ public function update($id = null) { + if ($resp = $this->requireLogin()) { + return $resp; + } + // Fetch the authorized user $authorizedUser = $this->authorizedUserModel->find($id); @@ -103,6 +149,10 @@ class AuthorizedUsersController extends ResourceController return $this->failNotFound('Authorized user not found.'); } + if ($resp = $this->requireOwnership($authorizedUser)) { + return $resp; + } + // Update the authorized user’s information (e.g., email) $email = strtolower($this->request->getPost('email')); if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) { @@ -122,12 +172,20 @@ class AuthorizedUsersController extends ResourceController */ public function delete($id = null) { + if ($resp = $this->requireLogin()) { + return $resp; + } + $authorizedUser = $this->authorizedUserModel->find($id); if (!$authorizedUser) { return $this->failNotFound('Authorized user not found.'); } + if ($resp = $this->requireOwnership($authorizedUser)) { + return $resp; + } + // Delete the authorized user record $this->authorizedUserModel->delete($id); @@ -147,16 +205,28 @@ class AuthorizedUsersController extends ResourceController return $this->fail('Invalid confirmation link.'); } - $authorizedUser = $this->authorizedUserModel->where('token', $token)->first(); + $tokenHash = $this->hashToken($token); + $authorizedUser = $this->authorizedUserModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->where('created_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString()) + ->first(); if (!$authorizedUser) { return $this->fail('Invalid or expired confirmation link.'); } - // Mark the authorized user as active - $this->authorizedUserModel->update($authorizedUser['id'], ['status' => 'Active', 'token' => null]); + // Mark the authorized user as active and rotate token for password setup + $nextToken = bin2hex(random_bytes(48)); + $nextTokenHash = $this->hashToken($nextToken); + $this->authorizedUserModel->update($authorizedUser['id'], [ + 'status' => 'Active', + 'token' => $nextTokenHash, + ]); - return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id']); + return redirect()->to('/set_authorized_user_password/' . $authorizedUser['authorized_user_id'] . '?token=' . $nextToken); } /** @@ -167,13 +237,36 @@ class AuthorizedUsersController extends ResourceController */ public function setPassword($authorizedUserId) { + $token = (string) $this->request->getGet('token'); + if ($token === '') { + return $this->fail('Invalid confirmation link.'); + } + + $tokenHash = $this->hashToken($token); + $authorizedUser = $this->authorizedUserModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->where('authorized_user_id', $authorizedUserId) + ->where('status', 'Active') + ->where('updated_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString()) + ->first(); + + if (!$authorizedUser) { + return $this->fail('Invalid or expired confirmation link.'); + } + $user = $this->userModel->find($authorizedUserId); if (!$user) { return $this->failNotFound('User not found.'); } - return view('user/set_authorized_user_password', ['userId' => $authorizedUserId]); + return view('user/set_authorized_user_password', [ + 'userId' => $authorizedUserId, + 'token' => $token, + ]); } /** @@ -181,38 +274,59 @@ class AuthorizedUsersController extends ResourceController * * @return ResponseInterface */ - /* - public function savePassword() + public function savePassword($authorizedUserId = null) { - // Validate the request $validation = \Config\Services::validation(); $validation->setRules([ - 'password' => 'required|min_length[6]', + 'password' => [ + 'label' => 'Password', + 'rules' => 'required|min_length[8]|regex_match[/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\\-=\\+*#$%&!?])[A-Za-z\\d@\\-=\\+*#$%&!?]{8,}$/]', + ], 'password_confirm' => 'required|matches[password]', - 'user_id' => 'required|integer' + 'user_id' => 'required|integer', + 'token' => 'required', ]); if (!$this->validate($validation->getRules())) { return $this->failValidationErrors($validation->getErrors()); } - // Get the validated input - $userId = $this->request->getPost('user_id'); - $password = $this->request->getPost('password'); + $userId = (int) $this->request->getPost('user_id'); + $token = (string) $this->request->getPost('token'); + $authorizedUserId = $authorizedUserId !== null ? (int) $authorizedUserId : $userId; - $model = new UserModel(); - $user = $model->find($userId); + if ($userId <= 0 || $authorizedUserId <= 0 || $userId !== $authorizedUserId) { + return $this->fail('Invalid request.'); + } + $tokenHash = $this->hashToken($token); + $authorizedUser = $this->authorizedUserModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->where('authorized_user_id', $authorizedUserId) + ->where('status', 'Active') + ->where('updated_at >=', Time::now()->subHours(self::TOKEN_TTL_HOURS)->toDateTimeString()) + ->first(); + + if (!$authorizedUser) { + return $this->fail('Invalid or expired confirmation link.'); + } + + $user = $this->userModel->find($authorizedUserId); if (!$user) { return $this->failNotFound('User not found.'); } - // Save the password - $model->update($userId, ['password' => password_hash($password, PASSWORD_DEFAULT)]); + $password = (string) $this->request->getPost('password'); + $hashedPassword = pbkdf2_hash($password); + + $this->userModel->update($authorizedUserId, ['password' => $hashedPassword]); + $this->authorizedUserModel->update($authorizedUser['id'], ['token' => null]); return $this->respond(['message' => 'Password has been successfully set.']); } -*/ /** * Sends a confirmation email to the authorized user. * @@ -242,4 +356,4 @@ class AuthorizedUsersController extends ResourceController log_message('error', 'Failed to send authorized user confirmation email to ' . $email); } } -} \ No newline at end of file +} diff --git a/app/Controllers/View/ExamDraftController.php b/app/Controllers/View/ExamDraftController.php index f95e7a7..43f167a 100644 --- a/app/Controllers/View/ExamDraftController.php +++ b/app/Controllers/View/ExamDraftController.php @@ -240,23 +240,23 @@ class ExamDraftController extends BaseController // Group legacy uploads (admin-uploaded finalized exams) by class_section for separate tab $legacyByClass = []; if ($this->hasIsLegacyColumn) { - // Keep all submissions visible; additionally surface legacy items in a separate tab - $drafts = $allDrafts; - + // Keep legacy items out of the main submissions list; show them in the legacy tab only. + $drafts = []; foreach ($allDrafts as $d) { $isLegacy = !empty($d['is_legacy']); - if (!$isLegacy) { + if ($isLegacy) { + $cid = (int)($d['class_section_id'] ?? 0); + if (!isset($legacyByClass[$cid])) { + $legacyByClass[$cid] = [ + 'class_section_id' => $cid, + 'class_section_name' => $d['class_section_name'] ?? 'Class ' . $cid, + 'items' => [], + ]; + } + $legacyByClass[$cid]['items'][] = $d; continue; } - $cid = (int)($d['class_section_id'] ?? 0); - if (!isset($legacyByClass[$cid])) { - $legacyByClass[$cid] = [ - 'class_section_id' => $cid, - 'class_section_name' => $d['class_section_name'] ?? 'Class ' . $cid, - 'items' => [], - ]; - } - $legacyByClass[$cid]['items'][] = $d; + $drafts[] = $d; } } else { // Column missing: keep behavior simple and avoid legacy tab @@ -284,13 +284,17 @@ class ExamDraftController extends BaseController return redirect()->to('/login'); } - $classSectionId = (int) ($this->request->getPost('class_section_id') ?? 0); + $classSectionIds = $this->request->getPost('class_section_ids'); + if (!is_array($classSectionIds)) { + $classSectionIds = [$classSectionIds]; + } + $classSectionIds = array_values(array_filter(array_map('intval', $classSectionIds))); $schoolYear = trim((string) ($this->request->getPost('school_year') ?? $this->schoolYear)); $semester = trim((string) ($this->request->getPost('semester') ?? $this->semester)); $examType = trim((string) $this->request->getPost('exam_type')); - if ($classSectionId <= 0) { - return redirect()->back()->withInput()->with('error', 'Select a class section.'); + if (empty($classSectionIds)) { + return redirect()->back()->withInput()->with('error', 'Select at least one class section.'); } if ($schoolYear === '') { return redirect()->back()->withInput()->with('error', 'School year is required.'); @@ -309,9 +313,17 @@ class ExamDraftController extends BaseController return redirect()->back()->withInput()->with('error', 'File type not allowed or upload failed.'); } - $payload = [ + $pdfName = null; + if (strtolower($file->getClientExtension()) === 'pdf') { + $pdfName = $stored; + } else { + $pdfName = $this->convertDocToPdf( + $this->fullUploadPath(self::FINAL_UPLOAD_DIR, $stored), + self::FINAL_UPLOAD_DIR + ); + } + $basePayload = [ 'teacher_id' => $adminId, // store under admin user since legacy uploads are admin-only - 'class_section_id' => $classSectionId, 'semester' => ucfirst(strtolower($semester)), 'school_year' => $schoolYear, 'exam_type' => $examType === '' ? null : $examType, @@ -325,23 +337,22 @@ class ExamDraftController extends BaseController 'version' => 1, ]; if ($this->hasIsLegacyColumn) { - $payload['is_legacy'] = 1; - } - - $pdfName = null; - if (strtolower($file->getClientExtension()) === 'pdf') { - $pdfName = $stored; - } else { - $pdfName = $this->convertDocToPdf( - $this->fullUploadPath(self::FINAL_UPLOAD_DIR, $stored), - self::FINAL_UPLOAD_DIR - ); + $basePayload['is_legacy'] = 1; } if ($pdfName !== null && $this->hasFinalPdfColumn) { - $payload['final_pdf_file'] = $pdfName; + $basePayload['final_pdf_file'] = $pdfName; } - if ($this->examDraftModel->insert($payload)) { + $saved = 0; + foreach ($classSectionIds as $classSectionId) { + $payload = $basePayload; + $payload['class_section_id'] = $classSectionId; + if ($this->examDraftModel->insert($payload)) { + $saved++; + } + } + + if ($saved > 0) { return redirect()->to('/administrator/exam-drafts')->with('success', 'Old exam uploaded successfully.'); } diff --git a/app/Controllers/View/FlagController.php b/app/Controllers/View/FlagController.php index 6e6e7ef..6a62edc 100644 --- a/app/Controllers/View/FlagController.php +++ b/app/Controllers/View/FlagController.php @@ -421,17 +421,45 @@ class FlagController extends Controller log_message('debug', 'Flag state: ' . $this->request->getPost('flag_state')); $currentFlagModel = new CurrentFlagModel(); + $userId = session()->get('user_id'); // Get the new flag state from the form $newState = $this->request->getPost('flag_state'); + $stateDescription = (string) ($this->request->getPost('state_description') ?? ''); + $actionTaken = (string) ($this->request->getPost('action_taken') ?? ''); if (!$newState) { session()->setFlashdata('error', 'incident state not provided.'); return $this->index(); } + $update = ['flag_state' => $newState]; + if ($newState === 'Closed') { + $update['updated_by_closed'] = $userId; + if ($stateDescription !== '') { + $update['close_description'] = $stateDescription; + } + if ($actionTaken !== '') { + $update['action_taken'] = $actionTaken; + } + } elseif ($newState === 'Canceled') { + $update['updated_by_canceled'] = $userId; + if ($stateDescription !== '') { + $update['cancel_description'] = $stateDescription; + } + if ($actionTaken !== '') { + $update['action_taken'] = $actionTaken; + } + } + // Update the flag state in the database - if ($currentFlagModel->update($id, ['flag_state' => $newState])) { + if ($currentFlagModel->update($id, $update)) { + if ($newState === 'Closed' || $newState === 'Canceled') { + $flagData = $currentFlagModel->find($id); + if ($flagData) { + return $this->moveToHistory($flagData); + } + } session()->setFlashdata('success', 'Incident state updated successfully!'); } else { $errors = $currentFlagModel->errors(); diff --git a/app/Controllers/View/GradingController.php b/app/Controllers/View/GradingController.php index f323d6e..9d385c0 100644 --- a/app/Controllers/View/GradingController.php +++ b/app/Controllers/View/GradingController.php @@ -278,7 +278,7 @@ class GradingController extends Controller $semEsc = $this->db->escape($semester); $yrEsc = $this->db->escape($schoolYear); - $rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear); + $rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester); // Preload quiz/homework/project/participation/midterm score counts to distinguish true zeros from empty scores $quizCounts = []; @@ -424,7 +424,7 @@ class GradingController extends Controller } } // Reload rows after refresh - $rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear); + $rows = $this->buildGradingRows($semEsc, $yrEsc, $schoolYear, $semester); } // Build structures keyed by BUSINESS section id @@ -1216,6 +1216,14 @@ class GradingController extends Controller $flagModel = new CurrentFlagModel(); $semKey = strtolower(trim($semester)); + $redirectUrl = base_url('grading/below-60'); + $query = http_build_query([ + 'semester' => $semester, + 'school_year' => $schoolYear, + ]); + if ($query !== '') { + $redirectUrl .= '?' . $query; + } $existing = $flagModel ->where('student_id', $studentId) @@ -1226,11 +1234,14 @@ class GradingController extends Controller $userId = (int)(session()->get('user_id') ?? 0) ?: null; $now = utc_now(); + $ok = true; if ($existing) { $data = [ 'flag_state' => $status, 'flag_datetime' => $now, + 'semester' => $semester, + 'school_year' => $schoolYear, 'updated_at' => $now, ]; if ($status === 'Open') { @@ -1246,7 +1257,7 @@ class GradingController extends Controller $data['close_description'] = trim($prev . PHP_EOL . $note); } } - $flagModel->update((int)$existing['id'], $data); + $ok = (bool) $flagModel->update((int)$existing['id'], $data); } else { $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); $studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? '')); @@ -1269,10 +1280,21 @@ class GradingController extends Controller $data['updated_by_closed'] = $userId; if ($note !== '') $data['close_description'] = $note; } - $flagModel->insert($data); + $ok = (bool) $flagModel->insert($data); } - return redirect()->back()->with('status', 'Status updated.'); + if (!$ok) { + log_message('error', 'updateBelowSixtyStatus failed', [ + 'student_id' => $studentId, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'status' => $status, + 'errors' => $flagModel->errors(), + ]); + return redirect()->to($redirectUrl)->with('error', 'Failed to update status.'); + } + + return redirect()->to($redirectUrl)->with('status', 'Status updated.'); } public function scheduleBelowSixty() @@ -1555,7 +1577,7 @@ class GradingController extends Controller * @param string $schoolYear Raw school year value for filtering student_class * @return array */ - private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear): array + private function buildGradingRows(string $semEsc, string $yrEsc, string $schoolYear, string $semesterRaw): array { $builder = $this->db->table('student_class sc') ->select([ @@ -1583,6 +1605,7 @@ class GradingController extends Controller 'ss_b.class_section_id AS matched_biz_csid', 'ss_p.class_section_id AS matched_pk_csid' ]) + ->distinct() ->join('`classSection` cs', 'cs.class_section_id = sc.class_section_id', 'left') ->join('students s', 's.id = sc.student_id', 'inner') ->join( @@ -1797,9 +1820,10 @@ class GradingController extends Controller } $statusMap = []; + $noteMap = []; if (!empty($studentIds)) { $flagRows = $this->db->table('current_flag') - ->select('student_id, flag_state') + ->select('student_id, flag_state, open_description, close_description') ->where('flag', 'grade') ->where('school_year', $schoolYear) ->where("LOWER(TRIM(semester))", $semesterKey) @@ -1810,6 +1834,12 @@ class GradingController extends Controller $sid = (int)($row['student_id'] ?? 0); if ($sid <= 0) continue; $statusMap[$sid] = (string)($row['flag_state'] ?? ''); + $openNote = trim((string)($row['open_description'] ?? '')); + $closeNote = trim((string)($row['close_description'] ?? '')); + $noteMap[$sid] = [ + 'open' => $openNote, + 'closed' => $closeNote, + ]; } } @@ -1818,6 +1848,15 @@ class GradingController extends Controller $row['comment'] = $commentMap[$sid] ?? ''; $flagState = strtolower(trim((string)($statusMap[$sid] ?? ''))); $row['status'] = ($flagState === 'closed' || $flagState === 'canceled') ? 'Closed' : 'Open'; + $noteBag = $noteMap[$sid] ?? ['open' => '', 'closed' => '']; + $rawNote = $row['status'] === 'Closed' ? (string)$noteBag['closed'] : (string)$noteBag['open']; + if ($rawNote !== '') { + $lines = preg_split('/\R/', $rawNote); + $lines = array_values(array_filter(array_map('trim', $lines), static fn($val) => $val !== '')); + $row['note'] = $lines ? end($lines) : ''; + } else { + $row['note'] = ''; + } } unset($row); diff --git a/app/Controllers/View/HomeworkController.php b/app/Controllers/View/HomeworkController.php index 47594e7..b0a238d 100644 --- a/app/Controllers/View/HomeworkController.php +++ b/app/Controllers/View/HomeworkController.php @@ -482,6 +482,7 @@ class HomeworkController extends Controller // Step 1: Get student IDs from student_class table $studentClassRows = $this->studentClassModel ->select('student_id') + ->distinct() ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->findAll(); @@ -534,10 +535,7 @@ class HomeworkController extends Controller private function getStudentsWithHomeworkScores($classSectionId, $homeworkHeaders, $semester, $schoolYear) { $semVariants = $this->getSemesterVariants($semester); - $studentClasses = $this->studentClassModel - ->active() - ->where('student_class.class_section_id', $classSectionId) - ->findAll(); + $studentClasses = $this->studentClassModel->getClassStudents($classSectionId, $schoolYear, null); $students = []; foreach ($studentClasses as $sc) { diff --git a/app/Controllers/View/ParentController.php b/app/Controllers/View/ParentController.php index d563aa7..027d08f 100644 --- a/app/Controllers/View/ParentController.php +++ b/app/Controllers/View/ParentController.php @@ -717,6 +717,7 @@ class ParentController extends BaseController // Step 1: Generate a secure token for email verification $token = bin2hex(random_bytes(48)); + $tokenHash = hash('sha256', $token); // Step 2: Determine user type based on relationship $userType = in_array(strtolower($relationToStudent), ['wife', 'husband']) ? 'Secondary' : 'Tertiary'; @@ -776,7 +777,7 @@ class ParentController extends BaseController 'state' => strtoupper($userData['state']), 'zip' => $userData['zip'], 'accept_school_policy' => $userData['accept_school_policy'] ?? 0, - 'token' => $token, + 'token' => $tokenHash, 'is_verified' => 0, 'status' => 'Inactive', 'user_type' => $userType, diff --git a/app/Controllers/View/ProjectController.php b/app/Controllers/View/ProjectController.php index 21eea66..69c2901 100644 --- a/app/Controllers/View/ProjectController.php +++ b/app/Controllers/View/ProjectController.php @@ -438,6 +438,7 @@ class ProjectController extends Controller // Step 1: Get student IDs from student_class table $studentClassRows = $studentClassModel ->select('student_id') + ->distinct() ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->findAll(); @@ -494,10 +495,7 @@ class ProjectController extends Controller $studentModel = new StudentModel(); $projectModel = new ProjectModel(); - $studentClasses = $studentClassModel - ->active() - ->where('student_class.class_section_id', $classSectionId) - ->findAll(); + $studentClasses = $studentClassModel->getClassStudents($classSectionId, $this->schoolYear, null); $students = []; foreach ($studentClasses as $sc) { diff --git a/app/Controllers/View/RegisterController.php b/app/Controllers/View/RegisterController.php index fe22344..4970690 100644 --- a/app/Controllers/View/RegisterController.php +++ b/app/Controllers/View/RegisterController.php @@ -173,16 +173,8 @@ class RegisterController extends Controller $existingUser = $this->userModel->where('email', $post['email'])->first(); if ($existingUser) { - // Step 2: Check if the user has a token (i.e., not verified yet) - if (!empty($existingUser['token']) && $existingUser['is_verified'] == 0) { - // User exists and is unverified - return redirect()->back()->withInput()->with('error', - 'This email address is already registered and is pending activation. Please check your email to activate your account.'); - } else { - // User exists and is already active or has no token - return redirect()->back()->withInput()->with('error', - 'The email address you entered is already in use. Please try a different one.'); - } + return redirect()->back()->withInput()->with('error', + 'This email address is already registered. Please check your email or log in.'); } /* ───────────── 6. Determine role ───────────── */ @@ -194,6 +186,7 @@ class RegisterController extends Controller /* ───────────── 7. Build & insert user ───────────── */ $token = bin2hex(random_bytes(48)); + $tokenHash = hash('sha256', $token); $userData = [ 'firstname' => $post['firstname'], 'lastname' => $post['lastname'], @@ -205,7 +198,7 @@ class RegisterController extends Controller 'city' => $post['city'], 'state' => $post['state'], 'zip' => $post['zip'], - 'token' => $token, + 'token' => $tokenHash, 'is_verified'=> 0, 'accept_school_policy' => (int) $post['accept_school_policy'], 'status' => 'Inactive', @@ -357,4 +350,4 @@ class RegisterController extends Controller -} \ No newline at end of file +} diff --git a/app/Controllers/View/ScorePredictor.php b/app/Controllers/View/ScorePredictor.php index a17d1ef..80d54f5 100644 --- a/app/Controllers/View/ScorePredictor.php +++ b/app/Controllers/View/ScorePredictor.php @@ -81,10 +81,10 @@ class ScorePredictor extends Controller s.school_id, s.firstname, s.lastname, - fall.semester_score as fall_score, - spring.semester_score as spring_score'); - // Also select class section for per-class trophy decision - $builder->select('sc.class_section_id as class_section_id'); + MAX(fall.semester_score) as fall_score, + MAX(spring.semester_score) as spring_score'); + // Reduce duplication from restored students while keeping a stable class section. + $builder->select('MAX(sc.class_section_id) as class_section_id'); $yearEsc = $this->db->escape($selectedYear); $builder->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $yearEsc, 'left'); $builder->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left'); diff --git a/app/Controllers/View/SubjectCurriculumController.php b/app/Controllers/View/SubjectCurriculumController.php index adcb695..2273b72 100644 --- a/app/Controllers/View/SubjectCurriculumController.php +++ b/app/Controllers/View/SubjectCurriculumController.php @@ -99,6 +99,7 @@ class SubjectCurriculumController extends BaseController ->orderBy('classes.class_name', 'ASC') ->orderBy('subject', 'ASC') ->orderBy('unit_number', 'ASC') + ->orderBy("CAST(SUBSTRING_INDEX(subject_curriculum_items.chapter_name, '.', 1) AS UNSIGNED)", 'ASC', false) ->orderBy('chapter_name', 'ASC') ->get() ->getResultArray(); diff --git a/app/Controllers/View/UserController.php b/app/Controllers/View/UserController.php index f8d1c6f..2f7e64e 100644 --- a/app/Controllers/View/UserController.php +++ b/app/Controllers/View/UserController.php @@ -21,6 +21,7 @@ require_once APPPATH . 'Helpers/pbkdf2_helper.php'; class UserController extends BaseController { + private const ACTIVATION_TTL_HOURS = 48; protected $userModel; protected $roleModel; protected $userRoleModel; @@ -49,6 +50,37 @@ class UserController extends BaseController $this->resetRequestModel = new PasswordResetRequestModel(); } + private function denyAccess(string $message) + { + if ($this->request->isAJAX() || $this->request->getHeaderLine('Accept') === 'application/json') { + return service('response') + ->setStatusCode(403) + ->setJSON(['status' => 'error', 'message' => $message]); + } + + session()->setFlashdata('error', $message); + return redirect()->to('/access_denied'); + } + + private function requirePermission(string $permission) + { + if (!session()->get('is_logged_in')) { + return redirect()->to('/login'); + } + + $userId = (int) session()->get('user_id'); + if ($userId <= 0 || !has_permission($userId, $permission)) { + return $this->denyAccess("You don't have permission to use this feature."); + } + + return null; + } + + private function hashToken(string $token): string + { + return hash('sha256', $token); + } + // Method to show the home page public function home() { @@ -75,6 +107,10 @@ class UserController extends BaseController public function userList() { + if ($resp = $this->requirePermission('read_user')) { + return $resp; + } + helper('url'); return view('user/user_list', [ @@ -85,6 +121,10 @@ class UserController extends BaseController // Method to show the list of users public function index() { + if ($resp = $this->requirePermission('read_user')) { + return $resp; + } + // Fetch users along with their assigned roles $builder = $this->db->table('users'); $builder->select('users.id, users.firstname, users.lastname, users.email, user_roles.role_id, roles.name as role, users.status, users.updated_at'); @@ -122,6 +162,10 @@ class UserController extends BaseController public function userListData() { + if ($resp = $this->requirePermission('read_user')) { + return $resp; + } + return $this->response->setJSON([ 'users' => $this->buildUsersWithRoles(), ]); @@ -253,6 +297,10 @@ class UserController extends BaseController // Method to store a new user public function store() { + if ($resp = $this->requirePermission('edit_user')) { + return $resp; + } + // Validate input data $validation = \Config\Services::validation(); $validation->setRules([ @@ -315,6 +363,10 @@ class UserController extends BaseController // Method to show the form for editing an existing user public function edit($id) { + if ($resp = $this->requirePermission('edit_user')) { + return $resp; + } + $data['user'] = $this->userModel->find($id); $data['roles'] = $this->roleModel->findAll(); $userRoles = $this->userRoleModel->where('user_id', $id)->findAll(); @@ -327,6 +379,10 @@ class UserController extends BaseController // Method to delete an existing user public function delete($id) { + if ($resp = $this->requirePermission('edit_user')) { + return $resp; + } + $this->userModel->delete($id); // Delete the user's roles from the user_roles table @@ -369,27 +425,21 @@ class UserController extends BaseController $email = strtolower($this->request->getPost('email')); $user = $this->userModel->where('email', $email)->first(); - // --- Handle unknown email --- - if (!$user) { - session()->setFlashdata('error', 'If this email is registered, you will receive a reset link.'); - log_message('info', "Password reset requested for non-existing user {$email}"); - return redirect()->back(); - } - - // --- Handle unverified accounts --- - if ((int) $user['is_verified'] === 0) { - session()->setFlashdata('error', 'Please check your email and complete the account activation process before resetting your password.'); - log_message('info', "Password reset blocked for unverified user {$email}"); + // --- Handle unknown or unverified email --- + if (!$user || (int) $user['is_verified'] === 0) { + session()->setFlashdata('success', 'If this email is registered, you will receive a reset link.'); + log_message('info', "Password reset requested for {$email} (user missing or unverified)."); return redirect()->back(); } // --- Verified user: continue with reset --- $token = bin2hex(random_bytes(48)); + $tokenHash = $this->hashToken($token); $expires_at = Time::now()->addHours(1); $this->passwordResetModel->insert([ 'email' => $email, - 'token' => $token, + 'token' => $tokenHash, 'created_at' => Time::now(), 'expires_at' => $expires_at, ]); @@ -447,7 +497,12 @@ class UserController extends BaseController } // You may want to validate the token here - $resetEntry = $this->passwordResetModel->where('token', $token) + $tokenHash = $this->hashToken($token); + $resetEntry = $this->passwordResetModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() ->where('expires_at >=', Time::now()) ->first(); @@ -462,6 +517,10 @@ class UserController extends BaseController //This function processes the new password submission, validating the token, updating the user's password, and cleaning up the reset entry. public function processResetPassword() { + if (strtolower($this->request->getMethod()) !== 'post') { + return redirect()->to('/')->with('error', 'Invalid request.'); + } + $token = $this->request->getPost('token'); $newPassword = $this->request->getPost('password'); $passConfirm = $this->request->getPost('pass_confirm'); @@ -490,7 +549,12 @@ class UserController extends BaseController } // Find the password reset entry - $resetEntry = $this->passwordResetModel->where('token', $token) + $tokenHash = $this->hashToken($token); + $resetEntry = $this->passwordResetModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() ->where('expires_at >=', Time::now()) ->first(); @@ -519,7 +583,12 @@ class UserController extends BaseController ]); // Delete the used token from the password reset table - $this->passwordResetModel->where('token', $token)->delete(); + $this->passwordResetModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->delete(); // Retrieve the user's IP address from the request $ipAddress = $this->request->getIPAddress(); @@ -546,10 +615,16 @@ class UserController extends BaseController public function confirm($token) { - log_message('info', 'Processing email confirmation with token: ' . $token); + log_message('info', 'Processing email confirmation.'); - - $user = $this->userModel->where('token', $token)->first(); + $tokenHash = $this->hashToken($token); + $user = $this->userModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString()) + ->first(); if (!$user || $user['is_verified'] == 1) { return redirect()->to('/invalid_token'); @@ -570,7 +645,14 @@ class UserController extends BaseController { //echo "Reached setPassword with token: " . esc($token); //echo "Token received: " . $token; - $user = $this->userModel->where('token', $token)->first(); + $tokenHash = $this->hashToken($token); + $user = $this->userModel + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString()) + ->first(); if (!$user || $user['is_verified'] == 1) { return redirect()->to('/invalid_token'); @@ -584,6 +666,10 @@ class UserController extends BaseController public function savePassword() { + if (strtolower($this->request->getMethod()) !== 'post') { + return redirect()->to('/')->with('error', 'Invalid request.'); + } + $validation = \Config\Services::validation(); $validation->setRules([ 'password' => [ @@ -615,9 +701,17 @@ class UserController extends BaseController $token = $this->request->getPost('token'); $password = $this->request->getPost('password'); - $user = $this->userModel->where('id', $userId)->where('token', $token)->first(); + $tokenHash = $this->hashToken($token); + $user = $this->userModel + ->where('id', $userId) + ->groupStart() + ->where('token', $tokenHash) + ->orWhere('token', $token) + ->groupEnd() + ->where('created_at >=', Time::now()->subHours(self::ACTIVATION_TTL_HOURS)->toDateTimeString()) + ->first(); - log_message('debug', "Attempting to set password for user $userId with token $token"); + log_message('debug', "Attempting to set password for user $userId"); if (!$user || $user['is_verified'] == 1) { return redirect()->to('/invalid_token'); @@ -670,20 +764,39 @@ class UserController extends BaseController $roleKey = (string) $this->request->getPost('role'); log_message('info', 'Role selected: ' . $roleKey); - $roleModel = new RoleModel(); - $route = $roleModel->getRouteByNameOrSlug($roleKey); - - if ($route === null) { - log_message('error', 'Invalid or inactive role selected: ' . $roleKey); - return redirect()->back()->with('error', 'Invalid role selected.'); - } - $userId = (int) session()->get('user_id'); log_message('info', 'User ID: ' . $userId); + if ($userId <= 0) { + return $this->denyAccess("You don't have permission to use this feature."); + } + + $roleRow = $this->db->table('user_roles ur') + ->join('roles r', 'r.id = ur.role_id', 'inner') + ->select('r.name, r.slug, r.dashboard_route') + ->where('ur.user_id', $userId) + ->where('r.is_active', 1) + ->groupStart() + ->where('LOWER(r.name)', strtolower($roleKey)) + ->orWhere('LOWER(r.slug)', strtolower($roleKey)) + ->groupEnd() + ->get() + ->getRowArray(); + + if (empty($roleRow)) { + log_message('error', 'Invalid or unassigned role selected: ' . $roleKey); + return redirect()->back()->with('error', 'Invalid role selected.'); + } + + $route = $roleRow['dashboard_route'] ?? null; + if ($route === null) { + log_message('error', 'No dashboard route configured for role: ' . $roleKey); + return redirect()->back()->with('error', 'Invalid role selected.'); + } + // Persist the *exact* role name or slug—choose your convention. - // If you want to store the canonical name, fetch the row and use $row['name']. - $this->userModel->update($userId, ['role' => $roleKey]); + // Store the canonical name to avoid arbitrary role strings. + $this->userModel->update($userId, ['role' => $roleRow['name']]); log_message('info', 'Role updated in database.'); log_message('info', 'Redirecting to role dashboard: ' . $route); @@ -702,6 +815,10 @@ class UserController extends BaseController public function delete_role($roleId) { + if ($resp = $this->requirePermission('edit_user')) { + return $resp; + } + // Fetch the role to be deleted $role = $this->roleModel->find($roleId); if (!$role) { @@ -731,6 +848,10 @@ class UserController extends BaseController public function loginActivity() { + if ($resp = $this->requirePermission('view_login_activity')) { + return $resp; + } + helper('url'); $perPage = (int) ($this->request->getGet('per_page') ?? 25); @@ -743,6 +864,10 @@ class UserController extends BaseController public function loginActivityData() { + if ($resp = $this->requirePermission('view_login_activity')) { + return $resp; + } + $perPage = (int) ($this->request->getGet('per_page') ?? 25); $page = (int) ($this->request->getGet('page') ?? 1); @@ -752,6 +877,10 @@ class UserController extends BaseController // Method to update an existing user public function updateUser() { + if ($resp = $this->requirePermission('edit_user')) { + return $resp; + } + if (strtolower($this->request->getMethod()) !== 'post') { return redirect()->to(site_url('user/user_list'))->with('error', 'Invalid request.'); } @@ -800,9 +929,6 @@ class UserController extends BaseController 'status' => trim((string)$this->request->getPost('status')), 'is_suspended' => $toBool('is_suspended'), 'is_verified' => $toBool('is_verified'), - 'token' => trim((string)$this->request->getPost('token')), - 'updated_at' => $toDT('updated_at'), - 'created_at' => $toDT('created_at'), ]; // Validation diff --git a/app/Models/ConfigurationModel.php b/app/Models/ConfigurationModel.php index 86ca89b..0a6463d 100644 --- a/app/Models/ConfigurationModel.php +++ b/app/Models/ConfigurationModel.php @@ -22,11 +22,13 @@ class ConfigurationModel extends Model */ public function getConfigValueByKey(string $key) { - // Deterministic read in case historical duplicates exist - $result = $this->where('config_key', $key) + // Use a fresh builder to avoid stale state from shared model builder. + $builder = $this->db->table($this->table); + $result = $builder->where('config_key', $key) ->orderBy('id', 'DESC') - ->first(); - return $result ? $result['config_value'] : null; + ->get(1) + ->getRowArray(); + return $result['config_value'] ?? null; } /** diff --git a/app/Models/SubjectCurriculumModel.php b/app/Models/SubjectCurriculumModel.php index 24a5196..d83e308 100644 --- a/app/Models/SubjectCurriculumModel.php +++ b/app/Models/SubjectCurriculumModel.php @@ -27,6 +27,7 @@ class SubjectCurriculumModel extends Model return $this->where('class_id', $classId) ->where('subject', $subject) ->orderBy('unit_number', 'ASC') + ->orderBy("CAST(SUBSTRING_INDEX(chapter_name, '.', 1) AS UNSIGNED)", 'ASC', false) ->orderBy('chapter_name', 'ASC') ->findAll(); } diff --git a/app/Services/TimeService.php b/app/Services/TimeService.php index d805bbb..15cd02c 100644 --- a/app/Services/TimeService.php +++ b/app/Services/TimeService.php @@ -167,8 +167,13 @@ class TimeService return null; } - $sourceTz = $sourceTz ?: $this->serverTimezone; $targetTz = $targetTz ?: $this->userTimezone(); + if ($sourceTz === null && $this->isDateOnlyString($value)) { + // Date-only strings should not shift across timezones. + $sourceTz = $targetTz; + } else { + $sourceTz = $sourceTz ?: $this->serverTimezone; + } try { if ($value instanceof Time) { @@ -204,4 +209,14 @@ class TimeService { return (string) ($this->toUTC($value, $fromTz, $format) ?? ''); } + + private function isDateOnlyString($value): bool + { + if (!is_string($value)) { + return false; + } + + $value = trim($value); + return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value); + } } diff --git a/app/Views/admin/class_progress_list.php b/app/Views/admin/class_progress_list.php index fcaea57..2fa881a 100644 --- a/app/Views/admin/class_progress_list.php +++ b/app/Views/admin/class_progress_list.php @@ -25,6 +25,23 @@

Class Progress Reports

Filter by week, class, and status
+
+ + + > + Teachers < 50% + +
@@ -115,7 +132,7 @@ $submissionLabel = $expectedDays > 0 ? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')') : 'Submitted: N/A'; - $subjectLabel = 'Subjects: ' . $subjectCount; + $subjectLabel = 'Units: ' . $subjectCount; ?>

@@ -151,12 +168,33 @@ $weekLabel .= ' – ' . date('M d, Y', strtotime($group['week_end'])); } $reports = $group['reports'] ?? []; - $teacherName = ''; + $teacherCounts = []; + $teacherLatest = []; foreach ($reports as $report) { - if (! empty($report['teacher_name'])) { - $teacherName = $report['teacher_name']; - break; + $name = trim((string) ($report['teacher_name'] ?? '')); + if ($name === '') { + continue; } + $teacherCounts[$name] = ($teacherCounts[$name] ?? 0) + 1; + $stamp = (string) ($report['updated_at'] ?? $report['created_at'] ?? ''); + if ($stamp !== '' && (!isset($teacherLatest[$name]) || $stamp > $teacherLatest[$name])) { + $teacherLatest[$name] = $stamp; + } + } + $teacherLabel = '-'; + if (!empty($teacherCounts)) { + $bestName = ''; + $bestCount = -1; + $bestStamp = ''; + foreach ($teacherCounts as $name => $count) { + $stamp = $teacherLatest[$name] ?? ''; + if ($count > $bestCount || ($count === $bestCount && $stamp > $bestStamp)) { + $bestName = $name; + $bestCount = $count; + $bestStamp = $stamp; + } + } + $teacherLabel = $bestName ?: '-'; } $exampleId = $reports ? reset($reports)['id'] : null; ?> @@ -181,7 +219,7 @@

- + View diff --git a/app/Views/administrator/daily_attendance.php b/app/Views/administrator/daily_attendance.php index c80d250..97e1485 100644 --- a/app/Views/administrator/daily_attendance.php +++ b/app/Views/administrator/daily_attendance.php @@ -696,7 +696,17 @@ - + + $sections) { + foreach ($sections as $section) { + $sectionKey = (string)($section['class_section_id'] ?? ($section['id'] ?? '')); + if ($sectionKey === '') continue; + $secNameRaw = trim((string)($section['class_section_name'] ?? '')); + $sectionLabelByKey[$sectionKey] = $secNameRaw !== '' ? $secNameRaw : ('Section ' . $sectionKey); + } + } + + $studentIssueRows = []; + foreach ($studentsBySection as $sectionKey => $students) { + foreach ($students as $stu) { + $sid = (int)($stu['id'] ?? 0); + if ($sid <= 0) continue; + $entries = $attendanceData[$sectionKey][$sid] ?? []; + if (!is_array($entries)) continue; + $abs = 0; + $late = 0; + foreach ($entries as $e) { + $d = substr((string)($e['date'] ?? ''), 0, 10); + if ($d === '') continue; + if ($filterStart !== '' && $d < $filterStart) continue; + if ($filterEnd !== '' && $d > $filterEnd) continue; + $st = strtolower(trim((string)($e['status'] ?? ''))); + if ($st === 'absent') { + $abs++; + } elseif ($st === 'late') { + $late++; + } + } + if (($abs + $late) <= 0) continue; + $studentIssueRows[] = [ + 'name' => trim((string)($stu['firstname'] ?? '') . ' ' . (string)($stu['lastname'] ?? '')), + 'section' => $sectionLabelByKey[(string)$sectionKey] ?? ('Section ' . $sectionKey), + 'absent' => $abs, + 'late' => $late, + ]; + } + } + + usort($studentIssueRows, static function ($a, $b) { + $sec = strcmp($a['section'], $b['section']); + if ($sec !== 0) return $sec; + return strcmp($a['name'], $b['name']); + }); + $totalDaysForPercent = 0; if ($filterStart === '' && $filterEnd === '' && !empty($totalPassedDays)) { $totalDaysForPercent = (int)$totalPassedDays; @@ -373,6 +421,37 @@ +
+
Students With Absences / Late
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Student NameClass SectionNbr of ABSNbr of LATE
No absences or late records in the selected range.
+
+
@@ -480,6 +559,13 @@ info: false, order: [[0, 'asc']] }); + $('#studentIssueTable').DataTable({ + paging: true, + searching: true, + info: true, + order: [[1, 'asc'], [0, 'asc']], + pageLength: 25 + }); } }); diff --git a/app/Views/administrator/exam_drafts.php b/app/Views/administrator/exam_drafts.php index 7450dec..fc03e51 100644 --- a/app/Views/administrator/exam_drafts.php +++ b/app/Views/administrator/exam_drafts.php @@ -182,13 +182,13 @@
- - +
Hold Ctrl (Windows) or Command (Mac) to select multiple sections.
diff --git a/app/Views/administrator/teacher_submissions.php b/app/Views/administrator/teacher_submissions.php index f0f87e0..5caee4a 100644 --- a/app/Views/administrator/teacher_submissions.php +++ b/app/Views/administrator/teacher_submissions.php @@ -21,6 +21,28 @@ $totalItems = max(0, (int)($summary['total_items'] ?? 0)); ?>
+ + getFlashdata('success')): ?> +
+ getFlashdata('success')) ?> +
+ getFlashdata('warning')): ?> +
+ getFlashdata('warning')) ?> +
+ getFlashdata('info')): ?> +
+ getFlashdata('info')) ?> +
+ + + +
+ Showing teachers for class sections with progress submissions below 50%. +
+
@@ -56,17 +78,63 @@ > - Class Section - Teacher - Midterm Score - Midterm Comment - Participation - PTAP Comment + Class-Section + Teachers Name + + Score +
+ + +
+ + + Comment +
+ + +
+ + + Participation +
+ + +
+ + + PTAP Comment +
+ + +
+ + + Class Progress +
+ + +
+ + + Exam Draft +
+ + +
+ + + Homework +
+ + +
+ Notifications + @@ -83,7 +151,9 @@ 'midterm_score_status', 'midterm_comment_status', 'participation_status', - 'ptap_comment_status' + 'ptap_comment_status', + 'class_progress_status', + 'exam_draft_status', ] as $statusKey): ?> 'N/A', 'badge' => 'bg-secondary']; ?> @@ -95,6 +165,15 @@ + 'N/A', 'badge' => 'bg-secondary']; ?> + + + + + +
+ + diff --git a/app/Views/flags/flags_management.php b/app/Views/flags/flags_management.php index 6911fdc..3c5877a 100644 --- a/app/Views/flags/flags_management.php +++ b/app/Views/flags/flags_management.php @@ -107,7 +107,10 @@ - + - + diff --git a/app/Views/parent/class_progress_list.php b/app/Views/parent/class_progress_list.php index 4baba69..de73821 100644 --- a/app/Views/parent/class_progress_list.php +++ b/app/Views/parent/class_progress_list.php @@ -7,79 +7,105 @@
Review the weekly reports your child’s teachers submit.
- Reports are grouped by Sunday; click any row to read the full details. + Reports are grouped by student; click a name to expand weekly details.
- +
We couldn’t find any current enrollment for your account. Once your child is assigned to a Sunday class, their teacher’s progress reports will appear here.
- -
No reports submitted yet.
- -
-
- - - - - - - - - - - - - - - - - - -
WeekSubjectsDetails
-
- -
Class:
- -
-
- $section): ?> - -
-
- - -
-
- -
-
- -
-
- - View Weekly Details - -
-
+ +
+ $student): ?> + +
+

+ +

+
+
+ +
No reports submitted yet.
+ +
+ + + + + + + + + + + + + + + + + + +
WeekSubjectsDetails
+
+
+
+ $section): ?> + +
+
+ + +
+
+ +
+
+ +
+
+ + View Weekly Details + +
+
+ +
+
+
+
diff --git a/app/Views/rolepermission/assign_role.php b/app/Views/rolepermission/assign_role.php index 9c295fa..af8e1a9 100644 --- a/app/Views/rolepermission/assign_role.php +++ b/app/Views/rolepermission/assign_role.php @@ -250,6 +250,30 @@ modalInstance.show(); }; + const renderNameCell = (user) => { + const td = document.createElement('td'); + const fullName = `${user?.firstname ?? ''} ${user?.lastname ?? ''}`.trim(); + const label = fullName !== '' ? fullName : '—'; + const roleList = Array.isArray(user?.roles) + ? user.roles.map((role) => (role || '').toString().toLowerCase()) + : []; + const isParent = roleList.includes('parent'); + const uid = Number(user?.id || 0); + + if (isParent && uid > 0) { + const link = document.createElement('a'); + link.href = '#'; + link.className = 'text-decoration-none'; + link.setAttribute('data-family-guardian-id', String(uid)); + link.textContent = label; + td.appendChild(link); + return td; + } + + td.textContent = label; + return td; + }; + const renderTable = () => { if (!tableBody) return; @@ -280,9 +304,7 @@ accountCell.textContent = user.account_id ?? ''; row.appendChild(accountCell); - const nameCell = document.createElement('td'); - nameCell.textContent = `${user.firstname ?? ''} ${user.lastname ?? ''}`.trim(); - row.appendChild(nameCell); + row.appendChild(renderNameCell(user)); const emailCell = document.createElement('td'); emailCell.textContent = user.email ?? ''; diff --git a/app/Views/teacher/class_progress_history.php b/app/Views/teacher/class_progress_history.php index 25ae052..3630835 100644 --- a/app/Views/teacher/class_progress_history.php +++ b/app/Views/teacher/class_progress_history.php @@ -90,6 +90,7 @@ View Weekly Details + Edit diff --git a/app/Views/teacher/class_progress_submit.php b/app/Views/teacher/class_progress_submit.php index 2a82a4b..510dab9 100644 --- a/app/Views/teacher/class_progress_submit.php +++ b/app/Views/teacher/class_progress_submit.php @@ -1,12 +1,16 @@ extend('layout/main_layout') ?> section('content') ?>

- + + + + +

-
Submit weekly progress for a single subject
+
+ +
My Submissions
@@ -41,6 +51,10 @@ getFlashdata('success')): ?>
getFlashdata('success')) ?>
+ getFlashdata('confirm_overwrite'); ?> + getFlashdata('warning') && ! $overwritePrompt): ?> +
getFlashdata('warning')) ?>
+ getFlashdata('error')): ?>
getFlashdata('error')) ?>
@@ -54,33 +68,48 @@
-
+ +
Date Selection -
- -
Week start is required.
-
+ +
+ format('M d, Y'); + } catch (\Exception $e) { + $displayStart = $weekStartSelected; + } + ?> + Week of +
+ + +
+ +
Week start is required.
+
+
@@ -96,8 +125,10 @@ ?> $section): ?>
@@ -188,11 +219,11 @@
- +
- +
@@ -207,7 +238,7 @@
- +
You are not assigned to a class. Contact the administrator to submit progress. @@ -224,12 +255,65 @@ endSection() ?> section('scripts') ?> +getFlashdata('success'); + $submitError = session()->getFlashdata('error'); + $overwriteWarning = session()->getFlashdata('warning'); + $overwritePrompt = session()->getFlashdata('confirm_overwrite'); +?> + + + + + + endSection() ?> diff --git a/app/Views/teacher/exam_drafts.php b/app/Views/teacher/exam_drafts.php index 95b5b13..cddb642 100644 --- a/app/Views/teacher/exam_drafts.php +++ b/app/Views/teacher/exam_drafts.php @@ -137,6 +137,9 @@ + + View +
No file uploaded
@@ -152,6 +155,9 @@ + + View +
@@ -199,6 +205,9 @@ Download + + View + File missing diff --git a/app/Views/user/set_authorized_user_password.php b/app/Views/user/set_authorized_user_password.php new file mode 100644 index 0000000..9fddda8 --- /dev/null +++ b/app/Views/user/set_authorized_user_password.php @@ -0,0 +1,142 @@ +extend('layout/register_layout') ?> +section('content') ?> + +
+ + +
+ + + +
+ +

Create Your Password

+
+ + + +
+
+ + + + +
+ + Password must be at least 8 characters long, contain a number, an uppercase letter, a lowercase letter, and one special character: @, -, =, +, *, #, $, %, &, ! + + + Copy and paste are disabled for security reasons. + +
+ +
+
+ + + + +
+ + Passwords do not match. + + + Copy and paste are disabled for security reasons. + +
+ +
+ +
+ +
+ +endSection() ?> + +section('scripts') ?> + +endSection() ?> diff --git a/quran_surahs_by_grade.csv b/quran_surahs_by_grade.csv deleted file mode 100644 index 7adf2d8..0000000 --- a/quran_surahs_by_grade.csv +++ /dev/null @@ -1,45 +0,0 @@ -Grade,Surah -1,Al-Fatihah -1,An-Nas -1,Al-Falaq -1,Al-Ikhlas -2,Al-Masad -2,An-Nasr -2,Al-Kafirun -2,Al-Kawthar -2,Al-Ma'un -3,Quraysh -3,Al-Fil -3,Al-Humazah -3,Al-'Asr -3,At-Takathur -4,Al-Qari'ah -4,Al-'Adiyat -4,Az-Zalzalah -4,Al-Bayyinah -4,Al-Qadr -5,Al-'Alaq -5,At-Tin -5,Ash-Sharh -5,Ad-Duhaa -5,Al-Layl -6,Ash-Shams -6,Al-Balad -6,Al-Fajr -6,Al-Ghashiyah -6,Al-A'la -7,At-Tariq -7,Al-Buruj -7,Al-Inshiqaq -7,Al-Mutaffifin -7,Al-Infitar -8,At-Takwir -8,Abasa -8,Al-Mursalat -8,An-Naba -9,Al-Mulk -9,Al-Qalam -9,Al-Haqqah -9,Al-Ma'arij -9,Nuh -9,Al-Jinn diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index 543cd4a..0000000 Binary files a/test_output.txt and /dev/null differ diff --git a/writable/cache/navbar_cd1336c7da8c3f064f0b3b9e0c0a2232 b/writable/cache/navbar_cd1336c7da8c3f064f0b3b9e0c0a2232 index 4a9c297..b447566 100644 --- a/writable/cache/navbar_cd1336c7da8c3f064f0b3b9e0c0a2232 +++ b/writable/cache/navbar_cd1336c7da8c3f064f0b3b9e0c0a2232 @@ -1 +1 @@ -a:3:{s:4:"time";i:1772401871;s:3:"ttl";i:300;s:4:"data";a:19:{i:0;a:12:{s:2:"id";s:2:"63";s:14:"menu_parent_id";N;s:5:"label";s:6:"Family";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-09 15:22:56";s:10:"updated_at";s:19:"2025-10-26 06:33:11";s:10:"deleted_at";N;s:8:"children";a:2:{i:0;a:12:{s:2:"id";s:2:"64";s:14:"menu_parent_id";s:2:"63";s:5:"label";s:16:"Family Managment";s:3:"url";s:12:"family/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-09 15:23:49";s:10:"updated_at";s:19:"2025-10-26 06:28:34";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"74";s:14:"menu_parent_id";s:2:"63";s:5:"label";s:13:"Family Update";s:3:"url";s:22:"families/import-legacy";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:29:11";s:10:"updated_at";s:19:"2025-10-26 06:29:11";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:1;a:12:{s:2:"id";s:2:"67";s:14:"menu_parent_id";N;s:5:"label";s:9:"Inventory";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-11 06:24:37";s:10:"updated_at";s:19:"2025-09-16 16:48:58";s:10:"deleted_at";N;s:8:"children";a:6:{i:0;a:12:{s:2:"id";s:2:"56";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:5:"Books";s:3:"url";s:14:"inventory/book";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 19:13:48";s:10:"updated_at";s:19:"2025-10-05 03:43:38";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"72";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:26:"Update Inventory Movements";s:3:"url";s:19:"inventory/movements";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-02 06:07:53";s:10:"updated_at";s:19:"2025-10-05 03:42:08";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"55";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:19:"Classroom Equipment";s:3:"url";s:9:"inventory";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 18:56:08";s:10:"updated_at";s:19:"2025-09-16 16:47:32";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"57";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:16:"Kitchen Supplies";s:3:"url";s:17:"inventory/kitchen";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 19:14:24";s:10:"updated_at";s:19:"2025-09-16 16:48:23";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"58";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:15:"Office Supplies";s:3:"url";s:16:"inventory/office";s:10:"icon_class";N;s:6:"target";s:6:"_blank";s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 19:14:53";s:10:"updated_at";s:19:"2025-09-10 22:17:05";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"66";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:7:"Summary";s:3:"url";s:21:"inventory/summary-all";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-11 05:56:10";s:10:"updated_at";s:19:"2025-09-11 06:28:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:2;a:12:{s:2:"id";s:2:"83";s:14:"menu_parent_id";N;s:5:"label";s:15:"TimeOff Request";s:3:"url";s:22:"/administrator/absence";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 20:58:55";s:10:"updated_at";s:19:"2025-11-01 20:58:55";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"86";s:14:"menu_parent_id";N;s:5:"label";s:11:"Competition";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:40:32";s:10:"updated_at";s:19:"2026-01-11 07:40:32";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"87";s:14:"menu_parent_id";s:2:"86";s:5:"label";s:17:"Competition Setup";s:3:"url";s:19:"competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:41:19";s:10:"updated_at";s:19:"2026-01-11 07:41:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:4;a:12:{s:2:"id";s:2:"88";s:14:"menu_parent_id";N;s:5:"label";s:23:"Notification Management";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-13 19:48:58";s:10:"updated_at";s:19:"2026-01-13 19:48:58";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"89";s:14:"menu_parent_id";s:2:"88";s:5:"label";s:18:"Notification Setup";s:3:"url";s:35:"/administrator/notifications_alerts";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-13 19:49:35";s:10:"updated_at";s:19:"2026-01-13 19:49:35";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:5;a:12:{s:2:"id";s:2:"92";s:14:"menu_parent_id";N;s:5:"label";s:10:"Curriculum";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-18 07:23:26";s:10:"updated_at";s:19:"2026-01-18 07:23:26";s:10:"deleted_at";N;s:8:"children";a:2:{i:0;a:12:{s:2:"id";s:2:"91";s:14:"menu_parent_id";s:2:"92";s:5:"label";s:14:"Class Progress";s:3:"url";s:14:"admin/progress";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-15 04:38:48";s:10:"updated_at";s:19:"2026-01-18 07:24:37";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"93";s:14:"menu_parent_id";s:2:"92";s:5:"label";s:18:"Curriculum Subject";s:3:"url";s:32:"administrator/subject-curriculum";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-18 07:25:26";s:10:"updated_at";s:19:"2026-01-18 07:25:26";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:6;a:12:{s:2:"id";s:2:"94";s:14:"menu_parent_id";N;s:5:"label";s:19:"Teacher Submissions";s:3:"url";s:33:"administrator/teacher-submissions";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-25 22:20:24";s:10:"updated_at";s:19:"2026-01-25 22:20:24";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:7;a:12:{s:2:"id";s:2:"95";s:14:"menu_parent_id";N;s:5:"label";s:5:"Exams";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-29 01:51:29";s:10:"updated_at";s:19:"2026-01-29 01:51:29";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"96";s:14:"menu_parent_id";s:2:"95";s:5:"label";s:12:"Exams Drafts";s:3:"url";s:26:"/administrator/exam-drafts";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-29 01:52:20";s:10:"updated_at";s:19:"2026-01-29 01:52:20";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:8;a:12:{s:2:"id";s:1:"1";s:14:"menu_parent_id";N;s:5:"label";s:5:"Users";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"10";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:6:{i:0;a:12:{s:2:"id";s:2:"12";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:20:"Active Notifications";s:3:"url";s:20:"notifications/active";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"13";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:21:"Deleted Notifications";s:3:"url";s:21:"notifications/deleted";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"14";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:14:"Login Activity";s:3:"url";s:19:"user/login_activity";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:55:24";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"15";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:9:"User List";s:3:"url";s:14:"user/user_list";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"52";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:14:"Navbar Builder";s:3:"url";s:11:"nav-builder";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:34:01";s:10:"updated_at";s:19:"2025-09-05 21:57:09";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"76";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:13:"IP Management";s:3:"url";s:21:"administrator/ip_bans";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:34:35";s:10:"updated_at";s:19:"2025-10-26 06:34:35";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:9;a:12:{s:2:"id";s:1:"2";s:14:"menu_parent_id";N;s:5:"label";s:5:"Roles";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"20";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:56:34";s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"16";s:14:"menu_parent_id";s:1:"2";s:5:"label";s:16:"Assign User Role";s:3:"url";s:26:"rolepermission/assign_role";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:11:09";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"17";s:14:"menu_parent_id";s:1:"2";s:5:"label";s:15:"Role Management";s:3:"url";s:20:"rolepermission/roles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:55:43";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"53";s:14:"menu_parent_id";s:1:"2";s:5:"label";s:21:"Permission Management";s:3:"url";s:31:"rolepermission/list_permissions";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 05:25:02";s:10:"updated_at";s:19:"2025-09-12 07:24:13";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:10;a:12:{s:2:"id";s:1:"3";s:14:"menu_parent_id";N;s:5:"label";s:13:"Configuration";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"30";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:54:57";s:10:"deleted_at";N;s:8:"children";a:2:{i:0;a:12:{s:2:"id";s:2:"84";s:14:"menu_parent_id";s:1:"3";s:5:"label";s:24:"Edit attendance Comments";s:3:"url";s:34:"administrator/attendance-templates";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-04 04:17:02";s:10:"updated_at";s:19:"2026-01-04 04:17:02";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"18";s:14:"menu_parent_id";s:1:"3";s:5:"label";s:22:"Add/Edit Configuration";s:3:"url";s:32:"configuration/configuration_view";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:52:20";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:11;a:12:{s:2:"id";s:1:"4";s:14:"menu_parent_id";N;s:5:"label";s:8:"Staffing";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"40";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"71";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:16:"Staff Attendance";s:3:"url";s:30:"admin/teacher-attendance/month";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-27 04:29:24";s:10:"updated_at";s:19:"2025-09-28 05:39:08";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"19";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:13:"Staff Profile";s:3:"url";s:11:"staff/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"20";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:24:"Teacher Class Assignment";s:3:"url";s:38:"administrator/teacher_class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:12;a:12:{s:2:"id";s:1:"5";s:14:"menu_parent_id";N;s:5:"label";s:7:"Parents";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"50";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"21";s:14:"menu_parent_id";s:1:"5";s:5:"label";s:14:"Parent Profile";s:3:"url";s:29:"administrator/parent_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:13;a:12:{s:2:"id";s:1:"6";s:14:"menu_parent_id";N;s:5:"label";s:15:"Student-Affairs";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"60";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:16:{i:0;a:12:{s:2:"id";s:2:"90";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:14:"Remove Student";s:3:"url";s:31:"/administrator/removed_students";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-14 03:53:43";s:10:"updated_at";s:19:"2026-01-14 03:53:43";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"97";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:14:"Placement Test";s:3:"url";s:17:"grading/placement";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-02-06 03:08:29";s:10:"updated_at";s:19:"2026-02-06 03:08:29";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"22";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:21:"Attendance Management";s:3:"url";s:30:"administrator/daily_attendance";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-27 05:10:47";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"23";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:26:"Attendance Tracking System";s:3:"url";s:21:"attendance/violations";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-10-12 15:13:51";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"24";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Attendance Scans";s:3:"url";s:16:"rfid_coming_soon";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"25";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:12:"Classes List";s:3:"url";s:30:"administrator/class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:6;a:12:{s:2:"id";s:2:"78";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Early Dismissals";s:3:"url";s:27:"attendance/early-dismissals";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:40:01";s:10:"updated_at";s:19:"2025-10-26 14:57:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:7;a:12:{s:2:"id";s:2:"26";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:17:"Emergency Contact";s:3:"url";s:31:"administrator/emergency_contact";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:56:05";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:8;a:12:{s:2:"id";s:2:"27";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:21:"Enrollment-Withdrawal";s:3:"url";s:37:"enroll_withdraw/enrollment_withdrawal";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"6";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-19 06:23:25";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:9;a:12:{s:2:"id";s:2:"28";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:20:"Incidents Management";s:3:"url";s:22:"flags/flags_management";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"7";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-29 04:26:10";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:10;a:12:{s:2:"id";s:2:"29";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:15:"School Calendar";s:3:"url";s:27:"administrator/calendar_view";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:11;a:12:{s:2:"id";s:2:"77";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:18:"Late Slips History";s:3:"url";s:13:"slips/preview";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:38:42";s:10:"updated_at";s:19:"2025-10-26 06:38:42";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:12;a:12:{s:2:"id";s:2:"30";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:14:"Score Analysis";s:3:"url";s:15:"report/combined";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"9";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-26 05:12:17";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:13;a:12:{s:2:"id";s:2:"31";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Score Management";s:3:"url";s:7:"grading";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"10";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-26 05:11:48";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:14;a:12:{s:2:"id";s:2:"32";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:24:"Student Class Assignment";s:3:"url";s:38:"administrator/student_class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"11";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:15;a:12:{s:2:"id";s:2:"33";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:15:"Student Profile";s:3:"url";s:30:"administrator/student_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"12";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:14;a:12:{s:2:"id";s:1:"7";s:14:"menu_parent_id";N;s:5:"label";s:7:"Classes";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"70";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"79";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:16:"Class Distribute";s:3:"url";s:38:"administrator/sections/auto-distribute";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:41:13";s:10:"updated_at";s:19:"2025-10-26 06:41:13";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"34";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:12:"Classes List";s:3:"url";s:30:"administrator/class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:08:21";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"80";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:22:"Section Auto Generator";s:3:"url";s:39:"administrator/sections/promotion-totals";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:42:02";s:10:"updated_at";s:19:"2025-10-26 06:42:02";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:15;a:12:{s:2:"id";s:1:"8";s:14:"menu_parent_id";N;s:5:"label";s:13:"Communication";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"80";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:46:08";s:10:"deleted_at";N;s:8:"children";a:7:{i:0;a:12:{s:2:"id";s:2:"65";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:12:"New Students";s:3:"url";s:29:"admin/enrollment/new-students";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-11 03:44:37";s:10:"updated_at";s:19:"2025-09-16 16:45:27";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"73";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:24:"Whatsapp Link Management";s:3:"url";s:9:"whatsapp/";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-11 22:58:29";s:10:"updated_at";s:19:"2025-10-11 22:58:29";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"35";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:22:"Parent Email Extractor";s:3:"url";s:29:"emails/parent_email_extractor";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:08:44";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"36";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:14:"Parent Profile";s:3:"url";s:29:"administrator/parent_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:44:58";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"37";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:15:"Student Profile";s:3:"url";s:30:"administrator/student_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:09:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"81";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:16:"Grading Tracking";s:3:"url";s:25:"grading/homework-tracking";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:43:11";s:10:"updated_at";s:19:"2025-10-26 14:35:52";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:6;a:12:{s:2:"id";s:2:"99";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:16:"Below 60 Summary";s:3:"url";s:16:"grading/below-60";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-03-01 21:44:34";s:10:"updated_at";s:19:"2026-03-01 21:44:34";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:16;a:12:{s:2:"id";s:1:"9";s:14:"menu_parent_id";N;s:5:"label";s:9:"Financial";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"90";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:56:16";s:10:"deleted_at";N;s:8:"children";a:11:{i:0;a:12:{s:2:"id";s:2:"68";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:17:"Add Extra Charges";s:3:"url";s:13:"admin/charges";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-21 05:05:12";s:10:"updated_at";s:19:"2025-09-21 05:05:12";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"69";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:14:"Manual Payment";s:3:"url";s:18:"payment/manual_pay";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-21 05:06:53";s:10:"updated_at";s:19:"2025-09-21 05:06:53";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"82";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:16:"Payment Tracking";s:3:"url";s:22:"payment/unpaid-parents";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 16:23:39";s:10:"updated_at";s:19:"2025-12-07 16:19:30";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"38";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:19:"Discount Management";s:3:"url";s:14:"discounts/list";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:52:38";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"39";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:19:"Expenses Management";s:3:"url";s:14:"expenses/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:57:45";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"40";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:16:"Financial Report";s:3:"url";s:24:"payment/financial_report";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:58:15";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:6;a:12:{s:2:"id";s:2:"41";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:19:"Invoices Management";s:3:"url";s:34:"invoice_payment/invoice_management";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:57:07";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:7;a:12:{s:2:"id";s:2:"42";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:14:"Manual Payment";s:3:"url";s:18:"payment/manual_pay";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:58:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:8;a:12:{s:2:"id";s:2:"43";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:18:"PaypalTransactions";s:3:"url";s:33:"administrator/paypal_transactions";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"6";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:9;a:12:{s:2:"id";s:2:"44";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:17:"Refund Management";s:3:"url";s:12:"refunds/list";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"7";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 17:02:17";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:10;a:12:{s:2:"id";s:2:"45";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:24:"Reimbursement Management";s:3:"url";s:20:"reimbursements/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 17:01:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:17;a:12:{s:2:"id";s:2:"10";s:14:"menu_parent_id";N;s:5:"label";s:10:"Printables";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:3:"100";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:5:{i:0;a:12:{s:2:"id";s:2:"85";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:17:"Print for teacher";s:3:"url";s:20:"admin/print-requests";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-07 04:29:16";s:10:"updated_at";s:19:"2026-01-07 04:29:16";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"46";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:6:"Badges";s:3:"url";s:29:"printables_reports/badge_form";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:18:24";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"47";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:10:"Class Prep";s:3:"url";s:10:"class-prep";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-27 04:31:03";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"48";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:12:"Report Cards";s:3:"url";s:30:"printables_reports/report_card";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"49";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:8:"Stickers";s:3:"url";s:27:"printables_reports/stickers";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:18;a:12:{s:2:"id";s:2:"11";s:14:"menu_parent_id";N;s:5:"label";s:16:"Event Management";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:3:"110";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"50";s:14:"menu_parent_id";s:2:"11";s:5:"label";s:8:"Calendar";s:3:"url";s:22:"administrator/calendar";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:20:09";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"51";s:14:"menu_parent_id";s:2:"11";s:5:"label";s:6:"Events";s:3:"url";s:20:"administrator/events";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"98";s:14:"menu_parent_id";s:2:"11";s:5:"label";s:19:"Competition Winners";s:3:"url";s:25:"admin/competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-03-01 21:44:34";s:10:"updated_at";s:19:"2026-03-01 21:44:34";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}}} \ No newline at end of file +a:3:{s:4:"time";i:1774928767;s:3:"ttl";i:300;s:4:"data";a:18:{i:0;a:12:{s:2:"id";s:2:"63";s:14:"menu_parent_id";N;s:5:"label";s:6:"Family";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-09 15:22:56";s:10:"updated_at";s:19:"2025-10-26 06:33:11";s:10:"deleted_at";N;s:8:"children";a:2:{i:0;a:12:{s:2:"id";s:2:"64";s:14:"menu_parent_id";s:2:"63";s:5:"label";s:16:"Family Managment";s:3:"url";s:12:"family/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-09 15:23:49";s:10:"updated_at";s:19:"2025-10-26 06:28:34";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"74";s:14:"menu_parent_id";s:2:"63";s:5:"label";s:13:"Family Update";s:3:"url";s:22:"families/import-legacy";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:29:11";s:10:"updated_at";s:19:"2025-10-26 06:29:11";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:1;a:12:{s:2:"id";s:2:"67";s:14:"menu_parent_id";N;s:5:"label";s:9:"Inventory";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-11 06:24:37";s:10:"updated_at";s:19:"2025-09-16 16:48:58";s:10:"deleted_at";N;s:8:"children";a:6:{i:0;a:12:{s:2:"id";s:2:"56";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:5:"Books";s:3:"url";s:14:"inventory/book";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 19:13:48";s:10:"updated_at";s:19:"2025-10-05 03:43:38";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"72";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:26:"Update Inventory Movements";s:3:"url";s:19:"inventory/movements";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-02 06:07:53";s:10:"updated_at";s:19:"2025-10-05 03:42:08";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"55";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:19:"Classroom Equipment";s:3:"url";s:9:"inventory";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 18:56:08";s:10:"updated_at";s:19:"2025-09-16 16:47:32";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"57";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:16:"Kitchen Supplies";s:3:"url";s:17:"inventory/kitchen";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 19:14:24";s:10:"updated_at";s:19:"2025-09-16 16:48:23";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"58";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:15:"Office Supplies";s:3:"url";s:16:"inventory/office";s:10:"icon_class";N;s:6:"target";s:6:"_blank";s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 19:14:53";s:10:"updated_at";s:19:"2025-09-10 22:17:05";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"66";s:14:"menu_parent_id";s:2:"67";s:5:"label";s:7:"Summary";s:3:"url";s:21:"inventory/summary-all";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-11 05:56:10";s:10:"updated_at";s:19:"2025-09-11 06:28:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:2;a:12:{s:2:"id";s:2:"83";s:14:"menu_parent_id";N;s:5:"label";s:15:"TimeOff Request";s:3:"url";s:22:"/administrator/absence";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 20:58:55";s:10:"updated_at";s:19:"2025-11-01 20:58:55";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"88";s:14:"menu_parent_id";N;s:5:"label";s:23:"Notification Management";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-13 19:48:58";s:10:"updated_at";s:19:"2026-01-13 19:48:58";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"89";s:14:"menu_parent_id";s:2:"88";s:5:"label";s:18:"Notification Setup";s:3:"url";s:35:"/administrator/notifications_alerts";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-13 19:49:35";s:10:"updated_at";s:19:"2026-01-13 19:49:35";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:4;a:12:{s:2:"id";s:2:"92";s:14:"menu_parent_id";N;s:5:"label";s:10:"Curriculum";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-18 07:23:26";s:10:"updated_at";s:19:"2026-01-18 07:23:26";s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"94";s:14:"menu_parent_id";s:2:"92";s:5:"label";s:19:"Teacher Submissions";s:3:"url";s:33:"administrator/teacher-submissions";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-25 22:20:24";s:10:"updated_at";s:19:"2026-03-29 18:12:27";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"93";s:14:"menu_parent_id";s:2:"92";s:5:"label";s:18:"Curriculum Subject";s:3:"url";s:32:"administrator/subject-curriculum";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-18 07:25:26";s:10:"updated_at";s:19:"2026-01-18 07:25:26";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"91";s:14:"menu_parent_id";s:2:"92";s:5:"label";s:14:"Class Progress";s:3:"url";s:14:"admin/progress";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-15 04:38:48";s:10:"updated_at";s:19:"2026-01-18 07:24:37";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:5;a:12:{s:2:"id";s:2:"95";s:14:"menu_parent_id";N;s:5:"label";s:5:"Exams";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-29 01:51:29";s:10:"updated_at";s:19:"2026-01-29 01:51:29";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"96";s:14:"menu_parent_id";s:2:"95";s:5:"label";s:12:"Exams Drafts";s:3:"url";s:26:"/administrator/exam-drafts";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-29 01:52:20";s:10:"updated_at";s:19:"2026-01-29 01:52:20";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:6;a:12:{s:2:"id";s:2:"86";s:14:"menu_parent_id";N;s:5:"label";s:11:"Competition";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:40:32";s:10:"updated_at";s:19:"2026-01-11 07:40:32";s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"87";s:14:"menu_parent_id";s:2:"86";s:5:"label";s:17:"Competition Setup";s:3:"url";s:19:"competition-winners";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-11 07:41:19";s:10:"updated_at";s:19:"2026-01-11 07:41:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:7;a:12:{s:2:"id";s:1:"1";s:14:"menu_parent_id";N;s:5:"label";s:5:"Users";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"10";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:6:{i:0;a:12:{s:2:"id";s:2:"12";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:20:"Active Notifications";s:3:"url";s:20:"notifications/active";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"13";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:21:"Deleted Notifications";s:3:"url";s:21:"notifications/deleted";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"14";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:14:"Login Activity";s:3:"url";s:19:"user/login_activity";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:55:24";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"15";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:9:"User List";s:3:"url";s:14:"user/user_list";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"52";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:14:"Navbar Builder";s:3:"url";s:11:"nav-builder";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:34:01";s:10:"updated_at";s:19:"2025-09-05 21:57:09";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"76";s:14:"menu_parent_id";s:1:"1";s:5:"label";s:13:"IP Management";s:3:"url";s:21:"administrator/ip_bans";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:34:35";s:10:"updated_at";s:19:"2025-10-26 06:34:35";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:8;a:12:{s:2:"id";s:1:"2";s:14:"menu_parent_id";N;s:5:"label";s:5:"Roles";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"20";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:56:34";s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"16";s:14:"menu_parent_id";s:1:"2";s:5:"label";s:16:"Assign User Role";s:3:"url";s:26:"rolepermission/assign_role";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:11:09";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"17";s:14:"menu_parent_id";s:1:"2";s:5:"label";s:15:"Role Management";s:3:"url";s:20:"rolepermission/roles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:55:43";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"53";s:14:"menu_parent_id";s:1:"2";s:5:"label";s:21:"Permission Management";s:3:"url";s:31:"rolepermission/list_permissions";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-08 05:25:02";s:10:"updated_at";s:19:"2025-09-12 07:24:13";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:9;a:12:{s:2:"id";s:1:"3";s:14:"menu_parent_id";N;s:5:"label";s:13:"Configuration";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"30";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:54:57";s:10:"deleted_at";N;s:8:"children";a:2:{i:0;a:12:{s:2:"id";s:2:"84";s:14:"menu_parent_id";s:1:"3";s:5:"label";s:24:"Edit attendance Comments";s:3:"url";s:34:"administrator/attendance-templates";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-04 04:17:02";s:10:"updated_at";s:19:"2026-01-04 04:17:02";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"18";s:14:"menu_parent_id";s:1:"3";s:5:"label";s:22:"Add/Edit Configuration";s:3:"url";s:32:"configuration/configuration_view";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:52:20";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:10;a:12:{s:2:"id";s:1:"4";s:14:"menu_parent_id";N;s:5:"label";s:8:"Staffing";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"40";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"71";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:16:"Staff Attendance";s:3:"url";s:30:"admin/teacher-attendance/month";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-27 04:29:24";s:10:"updated_at";s:19:"2025-09-28 05:39:08";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"19";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:13:"Staff Profile";s:3:"url";s:11:"staff/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"20";s:14:"menu_parent_id";s:1:"4";s:5:"label";s:24:"Teacher Class Assignment";s:3:"url";s:38:"administrator/teacher_class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:11;a:12:{s:2:"id";s:1:"5";s:14:"menu_parent_id";N;s:5:"label";s:7:"Parents";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"50";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:1:{i:0;a:12:{s:2:"id";s:2:"21";s:14:"menu_parent_id";s:1:"5";s:5:"label";s:14:"Parent Profile";s:3:"url";s:29:"administrator/parent_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:12;a:12:{s:2:"id";s:1:"6";s:14:"menu_parent_id";N;s:5:"label";s:15:"Student-Affairs";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"60";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:16:{i:0;a:12:{s:2:"id";s:2:"90";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:14:"Remove Student";s:3:"url";s:31:"/administrator/removed_students";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-14 03:53:43";s:10:"updated_at";s:19:"2026-01-14 03:53:43";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"97";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:14:"Placement Test";s:3:"url";s:17:"grading/placement";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-02-06 03:08:29";s:10:"updated_at";s:19:"2026-02-06 03:08:29";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"22";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:21:"Attendance Management";s:3:"url";s:30:"administrator/daily_attendance";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-27 05:10:47";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"23";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:26:"Attendance Tracking System";s:3:"url";s:21:"attendance/violations";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-10-12 15:13:51";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"24";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Attendance Scans";s:3:"url";s:16:"rfid_coming_soon";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"25";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:12:"Classes List";s:3:"url";s:30:"administrator/class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:6;a:12:{s:2:"id";s:2:"78";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Early Dismissals";s:3:"url";s:27:"attendance/early-dismissals";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:40:01";s:10:"updated_at";s:19:"2025-10-26 14:57:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:7;a:12:{s:2:"id";s:2:"26";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:17:"Emergency Contact";s:3:"url";s:31:"administrator/emergency_contact";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:56:05";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:8;a:12:{s:2:"id";s:2:"27";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:21:"Enrollment-Withdrawal";s:3:"url";s:37:"enroll_withdraw/enrollment_withdrawal";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"6";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-19 06:23:25";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:9;a:12:{s:2:"id";s:2:"28";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:20:"Incidents Management";s:3:"url";s:22:"flags/flags_management";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"7";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-29 04:26:10";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:10;a:12:{s:2:"id";s:2:"29";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:15:"School Calendar";s:3:"url";s:27:"administrator/calendar_view";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:11;a:12:{s:2:"id";s:2:"77";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:18:"Late Slips History";s:3:"url";s:13:"slips/preview";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:38:42";s:10:"updated_at";s:19:"2025-10-26 06:38:42";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:12;a:12:{s:2:"id";s:2:"30";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:14:"Score Analysis";s:3:"url";s:15:"report/combined";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"9";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-26 05:12:17";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:13;a:12:{s:2:"id";s:2:"31";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:16:"Score Management";s:3:"url";s:7:"grading";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"10";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2026-01-26 05:11:48";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:14;a:12:{s:2:"id";s:2:"32";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:24:"Student Class Assignment";s:3:"url";s:38:"administrator/student_class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"11";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:15;a:12:{s:2:"id";s:2:"33";s:14:"menu_parent_id";s:1:"6";s:5:"label";s:15:"Student Profile";s:3:"url";s:30:"administrator/student_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"12";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:13;a:12:{s:2:"id";s:1:"7";s:14:"menu_parent_id";N;s:5:"label";s:7:"Classes";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"70";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:3:{i:0;a:12:{s:2:"id";s:2:"79";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:16:"Class Distribute";s:3:"url";s:38:"administrator/sections/auto-distribute";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:41:13";s:10:"updated_at";s:19:"2025-10-26 06:41:13";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"34";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:12:"Classes List";s:3:"url";s:30:"administrator/class_assignment";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:08:21";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"80";s:14:"menu_parent_id";s:1:"7";s:5:"label";s:22:"Section Auto Generator";s:3:"url";s:39:"administrator/sections/promotion-totals";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:42:02";s:10:"updated_at";s:19:"2025-10-26 06:42:02";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:14;a:12:{s:2:"id";s:1:"8";s:14:"menu_parent_id";N;s:5:"label";s:13:"Communication";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"80";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:46:08";s:10:"deleted_at";N;s:8:"children";a:7:{i:0;a:12:{s:2:"id";s:2:"98";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:16:"Below 60 Summary";s:3:"url";s:16:"grading/below-60";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-03-01 21:50:13";s:10:"updated_at";s:19:"2026-03-01 21:50:13";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"65";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:12:"New Students";s:3:"url";s:29:"admin/enrollment/new-students";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-11 03:44:37";s:10:"updated_at";s:19:"2025-09-16 16:45:27";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"73";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:24:"Whatsapp Link Management";s:3:"url";s:9:"whatsapp/";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-11 22:58:29";s:10:"updated_at";s:19:"2025-10-11 22:58:29";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"35";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:22:"Parent Email Extractor";s:3:"url";s:29:"emails/parent_email_extractor";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:08:44";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"36";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:14:"Parent Profile";s:3:"url";s:29:"administrator/parent_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:44:58";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"37";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:15:"Student Profile";s:3:"url";s:30:"administrator/student_profiles";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:09:19";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:6;a:12:{s:2:"id";s:2:"81";s:14:"menu_parent_id";s:1:"8";s:5:"label";s:16:"Grading Tracking";s:3:"url";s:25:"grading/homework-tracking";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-10-26 06:43:11";s:10:"updated_at";s:19:"2025-10-26 14:35:52";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:15;a:12:{s:2:"id";s:1:"9";s:14:"menu_parent_id";N;s:5:"label";s:9:"Financial";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:2:"90";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:56:16";s:10:"deleted_at";N;s:8:"children";a:11:{i:0;a:12:{s:2:"id";s:2:"68";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:17:"Add Extra Charges";s:3:"url";s:13:"admin/charges";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-21 05:05:12";s:10:"updated_at";s:19:"2025-09-21 05:05:12";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"69";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:14:"Manual Payment";s:3:"url";s:18:"payment/manual_pay";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-21 05:06:53";s:10:"updated_at";s:19:"2025-09-21 05:06:53";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"82";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:16:"Payment Tracking";s:3:"url";s:22:"payment/unpaid-parents";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-11-01 16:23:39";s:10:"updated_at";s:19:"2025-12-07 16:19:30";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"38";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:19:"Discount Management";s:3:"url";s:14:"discounts/list";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 02:52:38";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"39";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:19:"Expenses Management";s:3:"url";s:14:"expenses/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:57:45";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:5;a:12:{s:2:"id";s:2:"40";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:16:"Financial Report";s:3:"url";s:24:"payment/financial_report";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:58:15";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:6;a:12:{s:2:"id";s:2:"41";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:19:"Invoices Management";s:3:"url";s:34:"invoice_payment/invoice_management";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:57:07";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:7;a:12:{s:2:"id";s:2:"42";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:14:"Manual Payment";s:3:"url";s:18:"payment/manual_pay";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"5";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 16:58:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:8;a:12:{s:2:"id";s:2:"43";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:18:"PaypalTransactions";s:3:"url";s:33:"administrator/paypal_transactions";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"6";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:9;a:12:{s:2:"id";s:2:"44";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:17:"Refund Management";s:3:"url";s:12:"refunds/list";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"7";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 17:02:17";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:10;a:12:{s:2:"id";s:2:"45";s:14:"menu_parent_id";s:1:"9";s:5:"label";s:24:"Reimbursement Management";s:3:"url";s:20:"reimbursements/index";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"8";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-16 17:01:41";s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:16;a:12:{s:2:"id";s:2:"10";s:14:"menu_parent_id";N;s:5:"label";s:10:"Printables";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:3:"100";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:5:{i:0;a:12:{s:2:"id";s:2:"85";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:17:"Print for teacher";s:3:"url";s:20:"admin/print-requests";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"0";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2026-01-07 04:29:16";s:10:"updated_at";s:19:"2026-01-07 04:29:16";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"46";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:6:"Badges";s:3:"url";s:29:"printables_reports/badge_form";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:18:24";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:2;a:12:{s:2:"id";s:2:"47";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:10:"Class Prep";s:3:"url";s:10:"class-prep";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-27 04:31:03";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:3;a:12:{s:2:"id";s:2:"48";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:12:"Report Cards";s:3:"url";s:30:"printables_reports/report_card";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"3";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}i:4;a:12:{s:2:"id";s:2:"49";s:14:"menu_parent_id";s:2:"10";s:5:"label";s:8:"Stickers";s:3:"url";s:27:"printables_reports/stickers";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"4";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}i:17;a:12:{s:2:"id";s:2:"11";s:14:"menu_parent_id";N;s:5:"label";s:16:"Event Management";s:3:"url";N;s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:3:"110";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:2:{i:0;a:12:{s:2:"id";s:2:"50";s:14:"menu_parent_id";s:2:"11";s:5:"label";s:8:"Calendar";s:3:"url";s:22:"administrator/calendar";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"1";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";s:19:"2025-09-09 03:20:09";s:10:"deleted_at";N;s:8:"children";a:0:{}}i:1;a:12:{s:2:"id";s:2:"51";s:14:"menu_parent_id";s:2:"11";s:5:"label";s:6:"Events";s:3:"url";s:20:"administrator/events";s:10:"icon_class";N;s:6:"target";N;s:10:"sort_order";s:1:"2";s:10:"is_enabled";s:1:"1";s:10:"created_at";s:19:"2025-09-05 17:23:56";s:10:"updated_at";N;s:10:"deleted_at";N;s:8:"children";a:0:{}}}}}} \ No newline at end of file