Compare commits

...

5 Commits

Author SHA1 Message Date
root 6d543c52cc fix homepage and admin dashboard 2026-02-27 17:15:40 -05:00
root 253be573ac score submit fix for spring semester 2026-02-27 15:26:23 -05:00
root 3cc5546733 AVP-78 Feature to check if parents looked at report card 2026-02-27 01:21:39 -05:00
root fe7b99581d update code 2026-02-26 23:50:24 -05:00
root a6880ea14f update code 2026-02-26 23:27:50 -05:00
71 changed files with 1918 additions and 357 deletions
+134 -113
View File
@@ -14,62 +14,6 @@
# ENVIRONMENT # ENVIRONMENT
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# CI_ENVIRONMENT = production
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
# app.baseURL = ''
# If you have trouble with `.`, you could also use `_`.
# app_baseURL = ''
# app.forceGlobalSecureRequests = false
# app.CSPEnabled = false
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
# database.default.hostname = localhost
# database.default.database = ci4
# database.default.username = root
# database.default.password = root
# database.default.DBDriver = MySQLi
# database.default.DBPrefix =
# database.default.port = 3306
# If you use MySQLi as tests, first update the values of Config\Database::$tests.
# database.tests.hostname = localhost
# database.tests.database = ci4_test
# database.tests.username = root
# database.tests.password = root
# database.tests.DBDriver = MySQLi
# database.tests.DBPrefix =
# database.tests.charset = utf8mb4
# database.tests.DBCollat = utf8mb4_general_ci
# database.tests.port = 3306
#--------------------------------------------------------------------
# ENCRYPTION
#--------------------------------------------------------------------
# encryption.key =
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
# session.savePath = null
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
# logger.threshold = 4
#--------------------------------------------------------------------
# ENVIRONMENT
#--------------------------------------------------------------------
CI_ENVIRONMENT = development CI_ENVIRONMENT = development
# Which profile to use if none is supplied to sendEmail() # Which profile to use if none is supplied to sendEmail()
@@ -77,53 +21,20 @@ CI_ENVIRONMENT = development
MAIL_PROFILE_DEFAULT=default MAIL_PROFILE_DEFAULT=default
# ===== DEFAULT (system) ===== # ===== DEFAULT (system) =====
MAIL_DEFAULT_HOST=smtp.hostinger.com # ---- Legacy fallbacks (kept for compatibility) ----
MAIL_DEFAULT_USER=no-replay@alrahmaisgl.org SMTP_HOST = smtp.gmail.com
MAIL_DEFAULT_PASS="W8;xZ/5g" SMTP_USER = alrahma.sunday.school@gmail.com
MAIL_DEFAULT_PORT=587 SMTP_PASS = "psnp emdq dykw ypul"
MAIL_DEFAULT_ENCRYPTION=ssl SMTP_PORT = 587
MAIL_DEFAULT_FROM_EMAIL=no-replay@alrahmaisgl.org SMTP_ENCRYPTION = tls # was ssl; use tls for port 587
MAIL_DEFAULT_FROM_NAME="Al Rahma Sunday School"
# Optional Reply-To (uncomment if you want a replyable inbox)
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_DEFAULT_REPLY_TO_NAME="No-Reply"
# Optional bounce address (envelope sender)
# MAIL_DEFAULT_RETURN_PATH=bounces@alrahmaisgl.org
# Optional DKIM
# MAIL_DEFAULT_DKIM_DOMAIN=alrahmaisgl.org
# MAIL_DEFAULT_DKIM_SELECTOR=default
# MAIL_DEFAULT_DKIM_PRIVATE=/path/to/private.key
# ===== COMMUNICATION ===== # Reply-To for all mail profiles
MAIL_COMMUNICATION_HOST=smtp.hostinger.com MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_COMMUNICATION_USER=no-replay@alrahmaisgl.org MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
MAIL_COMMUNICATION_PASS="W8;xZ/5g"
MAIL_COMMUNICATION_PORT=587
MAIL_COMMUNICATION_ENCRYPTION=ssl
MAIL_COMMUNICATION_FROM_EMAIL=no-replay@alrahmaisgl.org
MAIL_COMMUNICATION_FROM_NAME="School Communications"
MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications" MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
# MAIL_COMMUNICATION_RETURN_PATH=bounces@alrahmaisgl.org
# ===== PAYMENT =====
MAIL_PAYMENT_HOST=smtp.hostinger.com
MAIL_PAYMENT_USER=no-replay@alrahmaisgl.org
MAIL_PAYMENT_PASS="W8;xZ/5g"
MAIL_PAYMENT_PORT=587
MAIL_PAYMENT_ENCRYPTION=ssl
MAIL_PAYMENT_FROM_EMAIL=no-replay@alrahmaisgl.org
MAIL_PAYMENT_FROM_NAME="School Payments"
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_PAYMENT_REPLY_TO_NAME="School Payments" MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
# MAIL_PAYMENT_RETURN_PATH=bounces@alrahmaisgl.org
# ---- Legacy fallbacks (kept for compatibility) ----
SMTP_HOST = smtp.hostinger.com
SMTP_USER = no-replay@alrahmaosgl.org
SMTP_PASS = "W8;xZ/5g"
SMTP_PORT = 587
SMTP_ENCRYPTION = ssl # was SSL; set to ssl to match 587
# Optional sender directory you already use elsewhere # Optional sender directory you already use elsewhere
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\ MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
@@ -131,11 +42,20 @@ MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al R
\"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\ \"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}" \"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
# Principal notification recipient for TimeOff requests (comma-separated allowed)
PRINCIPAL_EMAIL="melabidi@alrahmaisgl.org"
MAIL_PARENT_REPORT_TO="alrahma.isgl@gmail.com"
MAIL_FROM_ADDRESS = "alrahma.sunday.school@gmail.com"
session.expiration = 43200
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# APP # APP
#-------------------------------------------------------------------- #--------------------------------------------------------------------
app.baseURL = 'http://localhost:8080/'
app.forceGlobalSecureRequests = false # If you have trouble with `.`, you could also use `_`.
app_baseURL = 'http://localhost:8080/'
# app.forceGlobalSecureRequests = true
# app.CSPEnabled = false
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# DATABASE # DATABASE
@@ -143,21 +63,122 @@ app.forceGlobalSecureRequests = false
database.default.hostname = 127.0.0.1 database.default.hostname = 127.0.0.1
database.default.database = school database.default.database = school
database.default.username = root database.default.username = root
database.default.password = database.default.password =
database.default.DBDriver = MySQLi database.default.DBDriver = MySQLi
database.default.DBPrefix = database.default.DBPrefix =
database.default.port = 3306 database.default.port = 3306
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# CODEIGNITER EMAIL (only used if you use CI Email class) # CONTENT SECURITY POLICY
# These don't affect PHPMailer but keep them aligned anyway. #--------------------------------------------------------------------
mail.protocol = smtp
mail.SMTPHost = ${SMTP_HOST} # contentsecuritypolicy.reportOnly = false
mail.SMTPUser = ${SMTP_USER} # contentsecuritypolicy.defaultSrc = 'none'
mail.SMTPPass = ${SMTP_PASS} # contentsecuritypolicy.scriptSrc = 'self'
mail.SMTPPort = ${SMTP_PORT} # contentsecuritypolicy.styleSrc = 'self'
mail.SMTPCrypto = ssl # contentsecuritypolicy.imageSrc = 'self'
mail.fromEmail = ${SMTP_USER} # contentsecuritypolicy.baseURI = null
mail.fromName = "Al Rahma Sunday School" # contentsecuritypolicy.childSrc = null
mail.mailType = html # contentsecuritypolicy.connectSrc = 'self'
mail.charset = UTF-8 # contentsecuritypolicy.fontSrc = null
# contentsecuritypolicy.formAction = null
# contentsecuritypolicy.frameAncestors = null
# contentsecuritypolicy.frameSrc = null
# contentsecuritypolicy.mediaSrc = null
# contentsecuritypolicy.objectSrc = null
# contentsecuritypolicy.pluginTypes = null
# contentsecuritypolicy.reportURI = null
# contentsecuritypolicy.sandbox = false
# contentsecuritypolicy.upgradeInsecureRequests = false
# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
# contentsecuritypolicy.autoNonce = true
#--------------------------------------------------------------------
# COOKIE
#--------------------------------------------------------------------
# cookie.prefix = ''
# cookie.expires = 0
# cookie.path = '/'
# cookie.domain = ''
# cookie.secure = false
# cookie.httponly = false
# cookie.samesite = 'Lax'
# cookie.raw = false
#--------------------------------------------------------------------
# ENCRYPTION
#--------------------------------------------------------------------
# encryption.key =
# encryption.driver = OpenSSL
# encryption.blockSize = 16
# encryption.digest = SHA512
#--------------------------------------------------------------------
# HONEYPOT
#--------------------------------------------------------------------
# honeypot.hidden = 'true'
# honeypot.label = 'Fill This Field'
# honeypot.name = 'honeypot'
# honeypot.template = '<label>{label}</label><input type="text" name="{name}" value=""/>'
# honeypot.container = '<div style="display:none">{template}</div>'
#--------------------------------------------------------------------
# SECURITY
#--------------------------------------------------------------------
# security.csrfProtection = 'cookie'
# security.tokenRandomize = false
# security.tokenName = 'csrf_token_name'
# security.headerName = 'X-CSRF-TOKEN'
# security.cookieName = 'csrf_cookie_name'
# security.expires = 7200
# security.regenerate = true
# security.redirect = false
# security.samesite = 'Lax'
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
# session.cookieName = 'ci_session'
# session.expiration = 7200
# session.savePath = null
# session.matchIP = false
# session.timeToUpdate = 300
# session.regenerateDestroy = false
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
logger.threshold = 4
MAIL_DEBUG = true
MAIL_TIMEOUT = 15
#--------------------------------------------------------------------
# CURLRequest
#--------------------------------------------------------------------
# curlrequest.shareOptions = false
mail.protocol = smtp
mail.SMTPHost = ${SMTP_HOST}
mail.SMTPUser = ${SMTP_USER}
mail.SMTPPass = ${SMTP_PASS}
mail.SMTPPort = ${SMTP_PORT}
mail.SMTPCrypto = tls
mail.fromEmail = ${SMTP_USER}
mail.fromName = "Al Rahma Sunday School"
mail.mailType = text
mail.charset = UTF-8
+2
View File
@@ -0,0 +1,2 @@
*.log
writable/session/
+4
View File
@@ -108,6 +108,7 @@ $routes->get('administrator/student-score-card', 'View\StudentController::scoreC
// API for report card meta (students, class sections, school years) // API for report card meta (students, class sections, school years)
$routes->get('api/printables/report-card/meta', 'View\ReportCardsController::reportCardMeta', ['filter' => 'auth']); $routes->get('api/printables/report-card/meta', 'View\ReportCardsController::reportCardMeta', ['filter' => 'auth']);
$routes->get('api/printables/report-card/completeness', 'View\ReportCardsController::reportCardCompleteness', ['filter' => 'auth']); $routes->get('api/printables/report-card/completeness', 'View\ReportCardsController::reportCardCompleteness', ['filter' => 'auth']);
$routes->get('api/printables/report-card/ack', 'View\ReportCardsController::reportCardAcknowledgement', ['filter' => 'auth']);
//Badges //Badges
$routes->get('printables_reports/badge_form', 'View\BadgesController::badgeForm'); $routes->get('printables_reports/badge_form', 'View\BadgesController::badgeForm');
@@ -330,6 +331,9 @@ $routes->post('/administrator/scores/update/{id}', 'View\ScoreController::update
// Route to delete a score record // Route to delete a score record
$routes->get('/administrator/scores/delete/{id}', 'View\ScoreController::destroy'); $routes->get('/administrator/scores/delete/{id}', 'View\ScoreController::destroy');
$routes->get('/parent/scores', 'View\ScoreController::viewStudentScore'); $routes->get('/parent/scores', 'View\ScoreController::viewStudentScore');
$routes->get('parent/report-cards', 'ParentReportCardController::index', ['filter' => 'auth:parent']);
$routes->get('parent/report-cards/view/(:num)', 'ParentReportCardController::view/$1', ['filter' => 'auth:parent']);
$routes->post('parent/report-cards/sign/(:num)', 'ParentReportCardController::sign/$1', ['filter' => 'auth:parent']);
+370 -7
View File
@@ -5,7 +5,11 @@ namespace App\Controllers;
use App\Models\ClassProgressReportModel; use App\Models\ClassProgressReportModel;
use App\Models\ClassProgressAttachmentModel; use App\Models\ClassProgressAttachmentModel;
use App\Models\ClassSectionModel; use App\Models\ClassSectionModel;
use App\Models\CalendarModel;
use App\Models\ConfigurationModel;
use App\Models\StudentClassModel; use App\Models\StudentClassModel;
use App\Models\SubjectCurriculumModel;
use App\Services\SemesterRangeService;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
@@ -15,6 +19,10 @@ class AdminProgressController extends BaseController
protected ClassProgressAttachmentModel $attachmentModel; protected ClassProgressAttachmentModel $attachmentModel;
protected ClassSectionModel $classSectionModel; protected ClassSectionModel $classSectionModel;
protected StudentClassModel $studentClassModel; protected StudentClassModel $studentClassModel;
protected CalendarModel $calendarModel;
protected ConfigurationModel $configModel;
protected SemesterRangeService $semesterRangeService;
protected SubjectCurriculumModel $curriculumModel;
public function __construct() public function __construct()
{ {
@@ -23,6 +31,10 @@ class AdminProgressController extends BaseController
$this->attachmentModel = new ClassProgressAttachmentModel(); $this->attachmentModel = new ClassProgressAttachmentModel();
$this->classSectionModel = new ClassSectionModel(); $this->classSectionModel = new ClassSectionModel();
$this->studentClassModel = new StudentClassModel(); $this->studentClassModel = new StudentClassModel();
$this->calendarModel = new CalendarModel();
$this->configModel = new ConfigurationModel();
$this->semesterRangeService = new SemesterRangeService($this->configModel);
$this->curriculumModel = new SubjectCurriculumModel();
} }
public function index() public function index()
@@ -53,22 +65,30 @@ class AdminProgressController extends BaseController
} }
$rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray(); $rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray();
$reportGroups = []; $reportGroupsBySection = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
$key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? ''); $sectionId = (int) ($row['class_section_id'] ?? 0);
if ($key === '_') { $weekKey = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $weekKey === '') {
continue; continue;
} }
if (! isset($reportGroups[$key])) { if (! isset($reportGroupsBySection[$sectionId])) {
$reportGroups[$key] = [ $reportGroupsBySection[$sectionId] = [];
}
if (! isset($reportGroupsBySection[$sectionId][$weekKey])) {
$reportGroupsBySection[$sectionId][$weekKey] = [
'week_start' => $row['week_start'], 'week_start' => $row['week_start'],
'week_end' => $row['week_end'], 'week_end' => $row['week_end'],
'class_section_name' => $row['class_section_name'] ?? '', 'class_section_name' => $row['class_section_name'] ?? '',
'reports' => [], 'reports' => [],
]; ];
} }
$reportGroups[$key]['reports'][$row['subject']] = $row; $reportGroupsBySection[$sectionId][$weekKey]['reports'][$row['subject']] = $row;
}
foreach ($reportGroupsBySection as $sectionId => $groups) {
krsort($groups);
$reportGroupsBySection[$sectionId] = $groups;
} }
$classSections = $this->classSectionModel->getClassSections(); $classSections = $this->classSectionModel->getClassSections();
@@ -78,12 +98,29 @@ class AdminProgressController extends BaseController
return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0; return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0;
})); }));
$filterStart = $this->normalizeDate($filters['from'] ?? '');
$filterEnd = $this->normalizeDate($filters['to'] ?? '');
[$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates();
[$expectedDays, $activeDatesSet] = $this->resolveExpectedDays(
$dateList,
$noSchoolDays,
$totalPassedDays,
$passedDatesSet,
$filterStart,
$filterEnd
);
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
$sectionSubjectCounts = $this->buildSectionSubjectCounts($rows);
return view('admin/class_progress_list', [ return view('admin/class_progress_list', [
'reportGroups' => $reportGroups, 'reportGroupsBySection' => $reportGroupsBySection,
'filters' => $filters, 'filters' => $filters,
'classSections' => $filteredSections, 'classSections' => $filteredSections,
'statusOptions' => ClassProgressController::STATUS_OPTIONS, 'statusOptions' => ClassProgressController::STATUS_OPTIONS,
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS, 'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
'sectionStats' => $sectionStats,
'sectionSubjectCounts' => $sectionSubjectCounts,
'expectedDays' => $expectedDays,
]); ]);
} }
@@ -211,4 +248,330 @@ class AdminProgressController extends BaseController
$flags = json_decode($json, true); $flags = json_decode($json, true);
return is_array($flags) ? $flags : []; return is_array($flags) ? $flags : [];
} }
protected function normalizeDate(string $value): string
{
$value = trim($value);
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
return '';
}
[$y, $m, $d] = array_map('intval', explode('-', $value));
return checkdate($m, $d, $y) ? $value : '';
}
protected function buildSemesterDates(): array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string) ($this->configModel->getConfig('school_year') ?? '');
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange);
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
$semRange = $this->semesterRangeService->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 {
$events = $this->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');
$passedDatesSet = [];
if (! empty($dateList) && $anchorSundayYmd !== '') {
foreach ($dateList as $d) {
if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
$passedDatesSet[$d] = true;
}
}
}
$totalPassedDays = count($passedDatesSet);
return [$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet];
}
protected function resolveExpectedDays(
array $dateList,
array $noSchoolDays,
int $totalPassedDays,
array $passedDatesSet,
string $filterStart,
string $filterEnd
): array {
$activeDatesSet = [];
if ($filterStart === '' && $filterEnd === '') {
$expectedDays = $totalPassedDays;
if ($expectedDays > 0) {
$activeDatesSet = $passedDatesSet;
}
return [$expectedDays, $activeDatesSet];
}
$expectedDays = 0;
foreach ($dateList as $d) {
if ($d === '') {
continue;
}
if ($filterStart !== '' && $d < $filterStart) {
continue;
}
if ($filterEnd !== '' && $d > $filterEnd) {
continue;
}
if (! empty($noSchoolDays[$d])) {
continue;
}
$activeDatesSet[$d] = true;
$expectedDays++;
}
return [$expectedDays, $activeDatesSet];
}
protected function buildSectionSubmissionStats(array $rows, array $activeDatesSet, int $expectedDays): array
{
$submittedBySection = [];
foreach ($rows as $row) {
$sectionId = (int) ($row['class_section_id'] ?? 0);
$weekStart = (string) ($row['week_start'] ?? '');
if ($sectionId === 0 || $weekStart === '') {
continue;
}
$submittedBySection[$sectionId][$weekStart] = true;
}
$stats = [];
foreach ($submittedBySection as $sectionId => $weeks) {
$submitted = count($weeks);
$stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays);
}
return $stats;
}
protected function buildSectionSubjectCounts(array $rows): array
{
$allowedSubjects = [];
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
$allowedSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
}
$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 === '') {
continue;
}
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
continue;
}
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)));
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 === '') {
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];
}
$counts[$sectionId] = ($counts[$sectionId] ?? 0) + $this->countChapterSegments(
(string) ($row['unit_title'] ?? ''),
$chapterSet
);
}
return $counts;
}
protected function buildCurriculumChapterMap(array $classIds): array
{
$classIds = array_values(array_filter(array_map('intval', $classIds)));
if (empty($classIds)) {
return [];
}
$rows = $this->curriculumModel
->whereIn('class_id', $classIds)
->findAll();
$map = [];
foreach ($rows as $row) {
$classId = (int) ($row['class_id'] ?? 0);
$subject = (string) ($row['subject'] ?? '');
$chapter = trim((string) ($row['chapter_name'] ?? ''));
if ($classId === 0 || $subject === '' || $chapter === '') {
continue;
}
$map[$classId][$subject][$chapter] = true;
}
return $map;
}
protected function resolveSubjectSlug(string $subject): ?string
{
foreach (ClassProgressController::SUBJECT_SECTIONS as $slug => $section) {
$dbSubject = (string) ($section['db_subject'] ?? '');
$label = (string) ($section['label'] ?? '');
if ($subject === $dbSubject || $subject === $label) {
return $slug;
}
}
return null;
}
protected function countChapterSegments(string $unitTitle, array $chapterSet): int
{
$unitTitle = trim($unitTitle);
if ($unitTitle === '') {
return 1;
}
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
if (! $parts) {
return 1;
}
$count = 0;
$seen = [];
foreach ($parts as $part) {
$chapter = $this->extractChapterFromSegment($part);
$key = $chapter !== '' ? $chapter : $part;
if (! empty($chapterSet) && $chapter !== '' && empty($chapterSet[$chapter])) {
$key = $part;
}
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$count++;
}
return $count > 0 ? $count : 1;
}
protected function extractChapterFromSegment(string $segment): string
{
$segment = trim($segment);
if ($segment === '') {
return '';
}
$pos = strrpos($segment, '/');
if ($pos === false) {
return $segment;
}
return trim(substr($segment, $pos + 1));
}
protected function buildSectionStat(int $submitted, int $expectedDays): array
{
$percent = $expectedDays > 0 ? round(($submitted * 100) / $expectedDays, 1) : 0.0;
if ($expectedDays === 0) {
$badgeClass = 'bg-secondary';
$labelClass = 'text-muted';
} elseif ($percent >= 100) {
$badgeClass = 'bg-success';
$labelClass = 'text-success';
} elseif ($percent >= 60) {
$badgeClass = 'bg-warning text-dark';
$labelClass = 'text-warning';
} elseif ($percent >= 40) {
$badgeClass = 'bg-orange text-dark';
$labelClass = 'text-warning';
} else {
$badgeClass = 'bg-danger';
$labelClass = 'text-danger';
}
return [
'submitted' => $submitted,
'expected' => $expectedDays,
'percent' => $percent,
'badgeClass' => $badgeClass,
'labelClass' => $labelClass,
];
}
} }
+127 -8
View File
@@ -7,6 +7,7 @@ use App\Models\ClassProgressAttachmentModel;
use App\Models\ConfigurationModel; use App\Models\ConfigurationModel;
use App\Models\SubjectCurriculumModel; use App\Models\SubjectCurriculumModel;
use App\Models\TeacherClassModel; use App\Models\TeacherClassModel;
use App\Services\SemesterRangeService;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
class ClassProgressController extends BaseController class ClassProgressController extends BaseController
@@ -56,6 +57,7 @@ class ClassProgressController extends BaseController
$classSectionName = $first['class_section_name'] ?? null; $classSectionName = $first['class_section_name'] ?? null;
$classId = $first['class_id'] ?? null; $classId = $first['class_id'] ?? null;
$sundayOptions = $this->buildSundayOptions(); $sundayOptions = $this->buildSundayOptions();
$defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions);
$subjectCurriculum = []; $subjectCurriculum = [];
if ($classId) { if ($classId) {
foreach (self::SUBJECT_SECTIONS as $slug => $section) { foreach (self::SUBJECT_SECTIONS as $slug => $section) {
@@ -69,7 +71,7 @@ class ClassProgressController extends BaseController
'classSectionName' => $classSectionName, 'classSectionName' => $classSectionName,
'classId' => $classId, 'classId' => $classId,
'sundayOptions' => $sundayOptions, 'sundayOptions' => $sundayOptions,
'defaultWeekStart' => $sundayOptions[0] ?? '', 'defaultWeekStart' => $defaultWeekStart,
]; ];
return view('teacher/class_progress_submit', $data); return view('teacher/class_progress_submit', $data);
} }
@@ -171,10 +173,16 @@ class ClassProgressController extends BaseController
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) { if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
$selectedSectionId = $validSectionIds[0] ?? null; $selectedSectionId = $validSectionIds[0] ?? null;
} }
[$semester, $schoolYear] = $this->resolveCurrentTerm();
$allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear);
if (empty($allowedTeacherIds)) {
$allowedTeacherIds = [$teacherId];
}
$builder = $this->reportModel $builder = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name') ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId); ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->whereIn('teacher_id', $allowedTeacherIds);
if ($selectedSectionId) { if ($selectedSectionId) {
$builder->where('class_progress_reports.class_section_id', $selectedSectionId); $builder->where('class_progress_reports.class_section_id', $selectedSectionId);
} }
@@ -216,20 +224,33 @@ class ClassProgressController extends BaseController
{ {
$teacherId = (int) session()->get('user_id'); $teacherId = (int) session()->get('user_id');
$row = $this->reportModel $row = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name') ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId) ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->find((int) $id); ->where('class_progress_reports.id', (int) $id)
->first();
if (! $row) { if (! $row) {
throw new PageNotFoundException('Progress report not found.'); 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.');
}
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown'; $row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
$weeklyReports = $this->reportModel $weeklyReports = $this->reportModel
->select('class_progress_reports.*, cs.class_section_name') ->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left') ->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
->where('teacher_id', $teacherId) ->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
->whereIn('teacher_id', $allowedTeacherIds)
->where('class_progress_reports.class_section_id', $row['class_section_id']) ->where('class_progress_reports.class_section_id', $row['class_section_id'])
->where('week_start', $row['week_start']) ->where('week_start', $row['week_start'])
->orderBy('subject', 'ASC') ->orderBy('subject', 'ASC')
@@ -427,6 +448,39 @@ class ClassProgressController extends BaseController
} }
protected function buildSundayOptions(int $count = 12): array protected function buildSundayOptions(int $count = 12): array
{
$range = $this->resolveProgressDateRange();
if ($range === null) {
return $this->buildUpcomingSundayOptions($count);
}
[$rangeStart, $rangeEnd] = $range;
try {
$start = new \DateTime($rangeStart);
$end = new \DateTime($rangeEnd);
} catch (\Exception $e) {
return $this->buildUpcomingSundayOptions($count);
}
if ($end < $start) {
[$start, $end] = [$end, $start];
}
$weekday = (int) $start->format('w');
if ($weekday !== 0) {
$start->modify('next sunday');
}
$options = [];
while ($start <= $end) {
$options[] = $start->format('Y-m-d');
$start->modify('+7 days');
}
return $options;
}
protected function buildUpcomingSundayOptions(int $count): array
{ {
$start = new \DateTime('today'); $start = new \DateTime('today');
$weekday = (int) $start->format('w'); $weekday = (int) $start->format('w');
@@ -443,6 +497,48 @@ class ClassProgressController extends BaseController
return $options; return $options;
} }
protected function resolveProgressDateRange(): ?array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
if ($schoolYear === '') {
return null;
}
$semesterResolver = new SemesterRangeService($this->configModel);
$semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? ''));
if ($semester === '') {
$semester = $semesterResolver->getSemesterForDate();
}
if ($semester !== '') {
$range = $semesterResolver->getSemesterRange($schoolYear, $semester);
if ($range !== null) {
return $range;
}
}
return $semesterResolver->getSchoolYearRange($schoolYear);
}
protected function pickDefaultWeekStart(array $options): string
{
if (empty($options)) {
return '';
}
$today = date('Y-m-d');
$default = '';
foreach ($options as $option) {
if ($option <= $today) {
$default = $option;
continue;
}
break;
}
return $default !== '' ? $default : $options[0];
}
protected function buildWeekEndFromStart(string $weekStart): string protected function buildWeekEndFromStart(string $weekStart): string
{ {
try { try {
@@ -468,4 +564,27 @@ class ClassProgressController extends BaseController
mkdir($this->attachmentStoragePath, 0755, true); mkdir($this->attachmentStoragePath, 0755, true);
} }
} }
protected function resolveCurrentTerm(): array
{
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
return [$semester, $schoolYear];
}
protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array
{
if (! $classSectionId || $schoolYear === '') {
return [];
}
$rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$ids = [];
foreach ($rows as $row) {
$id = (int) ($row['teacher_id'] ?? 0);
if ($id > 0) {
$ids[$id] = true;
}
}
return array_keys($ids);
}
} }
@@ -0,0 +1,186 @@
<?php
namespace App\Controllers;
use App\Models\ConfigurationModel;
use App\Models\ReportCardAcknowledgementModel;
use App\Models\StudentModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\Database\BaseConnection;
class ParentReportCardController extends BaseController
{
protected ConfigurationModel $configModel;
protected ReportCardAcknowledgementModel $ackModel;
protected StudentModel $studentModel;
protected BaseConnection $db;
public function __construct()
{
helper(['url', 'form']);
$this->db = \Config\Database::connect();
$this->configModel = new ConfigurationModel();
$this->ackModel = new ReportCardAcknowledgementModel();
$this->studentModel = new StudentModel();
}
public function index()
{
$parentId = $this->resolvePrimaryParentId();
if (! $parentId) {
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
}
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
$builder = $this->db->table('students s')
->select('s.id, s.firstname, s.lastname, cs.class_section_name')
->join('student_class sc', 'sc.student_id = s.id', 'left')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
->where('s.parent_id', $parentId)
->orderBy('s.firstname', 'ASC')
->orderBy('s.lastname', 'ASC');
if ($schoolYear !== '') {
$builder->where('sc.school_year', $schoolYear);
}
if ($semester !== '') {
$builder->where('sc.semester', $semester);
}
$students = $builder->get()->getResultArray();
$studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students)));
$ackMap = [];
if (! empty($studentIds)) {
$rows = $this->ackModel
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->whereIn('student_id', $studentIds)
->findAll();
foreach ($rows as $row) {
$ackMap[(int) $row['student_id']] = $row;
}
}
return view('parent/report_cards', [
'students' => $students,
'ackMap' => $ackMap,
'schoolYear' => $schoolYear,
'semester' => $semester,
]);
}
public function view($studentId)
{
$parentId = $this->resolvePrimaryParentId();
if (! $parentId) {
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
}
$student = $this->studentModel->find((int) $studentId);
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
throw new PageNotFoundException('Student not found.');
}
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
'viewed_at' => date('Y-m-d H:i:s'),
]);
$url = site_url('report-card/student/' . (int) $studentId);
$query = [];
if ($schoolYear !== '') {
$query['school_year'] = $schoolYear;
}
if ($semester !== '') {
$query['semester'] = $semester;
}
if ($query) {
$url .= '?' . http_build_query($query);
}
return redirect()->to($url);
}
public function sign($studentId)
{
$parentId = $this->resolvePrimaryParentId();
if (! $parentId) {
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
}
$student = $this->studentModel->find((int) $studentId);
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
throw new PageNotFoundException('Student not found.');
}
$name = trim((string) $this->request->getPost('signed_name'));
if ($name === '') {
return redirect()->back()->with('error', 'Please type your full name to sign.');
}
$schoolYear = trim((string) ($this->configModel->getConfig('school_year') ?? ''));
$semester = trim((string) ($this->configModel->getConfig('semester') ?? ''));
$now = date('Y-m-d H:i:s');
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
'viewed_at' => $now,
'signed_at' => $now,
'signed_name' => $name,
'signer_ip' => $this->request->getIPAddress(),
]);
return redirect()->to(site_url('parent/report-cards'))->with('success', 'Report card acknowledged.');
}
protected function resolvePrimaryParentId(): ?int
{
$parentId = (int) (session()->get('user_id') ?? 0);
$userType = (string) ($_SESSION['user_type'] ?? '');
if ($userType === 'primary') {
return $parentId ?: null;
}
if ($userType === 'secondary') {
$row = $this->db->table('parents')
->select('parent_id')
->where('secondparent_user_id', $parentId)
->get()
->getRowArray();
return $row ? (int) ($row['parent_id'] ?? 0) : null;
}
if ($userType === 'tertiary') {
$row = $this->db->table('authorized_users')
->select('user_id as parent_id')
->where('authorized_user_id', $parentId)
->get()
->getRowArray();
return $row ? (int) ($row['parent_id'] ?? 0) : null;
}
return $parentId ?: null;
}
protected function touchAcknowledgement(
int $parentId,
int $studentId,
string $schoolYear,
string $semester,
array $values
): void {
$criteria = [
'parent_id' => $parentId,
'student_id' => $studentId,
'school_year' => $schoolYear,
'semester' => $semester,
];
$existing = $this->ackModel->where($criteria)->first();
if ($existing) {
$this->ackModel->update((int) $existing['id'], $values);
return;
}
$this->ackModel->insert(array_merge($criteria, $values));
}
}
@@ -416,8 +416,10 @@ class AdministratorController extends BaseController
$totalStudents = (int) ( $totalStudents = (int) (
$this->db->table('student_class') $this->db->table('student_class')
->select('COUNT(DISTINCT student_class.student_id) AS cnt') ->select('COUNT(DISTINCT student_class.student_id) AS cnt')
->join('students', 'students.id = student_class.student_id', 'inner')
->where('student_class.school_year', $this->schoolYear) ->where('student_class.school_year', $this->schoolYear)
->where('student_class.class_section_id IS NOT NULL', null, false) ->where('student_class.class_section_id IS NOT NULL', null, false)
->where('students.is_active', 1)
->get() ->get()
->getRow('cnt') ->getRow('cnt')
?? 0 ?? 0
+16 -17
View File
@@ -797,26 +797,25 @@ public function showUpdateAttendanceForm()
} }
$noSchoolDays = []; $noSchoolDays = [];
if ($schoolYearForRange !== '') { $events = [];
try {
$events = $this->calendarModel->getEvents();
} catch (\Throwable $e) {
$events = []; $events = [];
try { }
$events = $this->calendarModel->getEventsBySchoolYearAndSemester( foreach ($events as $event) {
$schoolYearForRange, $d = substr((string)($event['date'] ?? ''), 0, 10);
$semesterNorm !== '' ? $semesterNorm : null if ($d === '' || empty($event['no_school'])) {
); continue;
} catch (\Throwable $e) {
$events = [];
} }
foreach ($events as $event) { if ($d < $rangeStart || $d > $rangeEnd) {
$d = substr((string)($event['date'] ?? ''), 0, 10); continue;
if ($d === '' || empty($event['no_school'])) {
continue;
}
if ($d < $rangeStart || $d > $rangeEnd) {
continue;
}
$noSchoolDays[$d] = true;
} }
$eventYear = trim((string)($event['school_year'] ?? ''));
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
continue;
}
$noSchoolDays[$d] = true;
} }
// Total passed attendance days up to this Sunday (inclusive), excluding no-school days. // Total passed attendance days up to this Sunday (inclusive), excluding no-school days.
@@ -60,13 +60,15 @@ class ClassPreparationController extends BaseController
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester); $limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
// 1) Get student count per class-section (distinct students, correct semester) // 1) Get student count per class-section (distinct students, correct semester)
$scQ = $this->studentClassModel $scQ = $this->db->table('student_class sc')
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count') ->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
->where('school_year', $schoolYear); ->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$scQ->where('semester', $semester); $scQ->where('sc.semester', $semester);
} }
$classSections = $scQ->groupBy('class_section_id')->findAll(); $classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
// 2) Inventory availability — prefer good_qty when present, else condition='good' quantity. // 2) Inventory availability — prefer good_qty when present, else condition='good' quantity.
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed); $inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
@@ -298,14 +300,16 @@ class ClassPreparationController extends BaseController
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; $className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
// Distinct student count for this term // Distinct student count for this term
$studentQ = $this->studentClassModel $studentQ = $this->db->table('student_class sc')
->select('COUNT(DISTINCT student_id) AS cnt') ->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
->where('class_section_id', $classSectionId) ->join('students s', 's.id = sc.student_id', 'inner')
->where('school_year', $schoolYear); ->where('s.is_active', 1)
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$studentQ->where('semester', $semester); $studentQ->where('sc.semester', $semester);
} }
$studentRow = $studentQ->first(); $studentRow = $studentQ->get()->getRowArray();
$studentCount = (int)($studentRow['cnt'] ?? 0); $studentCount = (int)($studentRow['cnt'] ?? 0);
// Live calc + adjustments // Live calc + adjustments
@@ -355,13 +359,15 @@ class ClassPreparationController extends BaseController
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester); $limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
// Student counts // Student counts
$scQ = $this->studentClassModel $scQ = $this->db->table('student_class sc')
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count') ->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
->where('school_year', $schoolYear); ->join('students s', 's.id = sc.student_id', 'inner')
->where('s.is_active', 1)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$scQ->where('semester', $semester); $scQ->where('sc.semester', $semester);
} }
$classSections = $scQ->groupBy('class_section_id')->findAll(); $classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
// Build inventory availability maps // Build inventory availability maps
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed); $inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
@@ -448,14 +454,16 @@ class ClassPreparationController extends BaseController
$now = utc_now(); $now = utc_now();
$count = 0; $count = 0;
foreach ($ids as $classSectionId) { foreach ($ids as $classSectionId) {
$studentQ = $this->studentClassModel $studentQ = $this->db->table('student_class sc')
->select('COUNT(DISTINCT student_id) AS cnt') ->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
->where('class_section_id', $classSectionId) ->join('students s', 's.id = sc.student_id', 'inner')
->where('school_year', $schoolYear); ->where('s.is_active', 1)
->where('sc.class_section_id', $classSectionId)
->where('sc.school_year', $schoolYear);
if ($limitToSemester && $semester !== '') { if ($limitToSemester && $semester !== '') {
$studentQ->where('semester', $semester); $studentQ->where('sc.semester', $semester);
} }
$studentRow = $studentQ->first(); $studentRow = $studentQ->get()->getRowArray();
$studentCount = (int)($studentRow['cnt'] ?? 0); $studentCount = (int)($studentRow['cnt'] ?? 0);
$classLevel = $this->getClassLevelBySection((string)$classSectionId); $classLevel = $this->getClassLevelBySection((string)$classSectionId);
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; $className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
@@ -71,12 +71,15 @@ class ParentAttendanceReportController extends BaseController
->orderBy('s.lastname', 'ASC') ->orderBy('s.lastname', 'ASC')
->get()->getResultArray(); ->get()->getResultArray();
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
return view('parent/report_attendance', [ return view('parent/report_attendance', [
'students' => $students, 'students' => $students,
'today' => local_date(utc_now(), 'Y-m-d'), 'today' => local_date(utc_now(), 'Y-m-d'),
'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items 'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items
'defaultDate' => $defaultDate, // preselected date (upcoming Sunday) 'defaultDate' => $defaultDate, // preselected date (upcoming Sunday)
'myReports' => $previewRows, 'myReports' => $previewRows,
'cutoffThreshold' => $cutoffThreshold,
]); ]);
} }
@@ -141,6 +144,17 @@ class ParentAttendanceReportController extends BaseController
// Enforce Sunday-only and future-or-today selection // Enforce Sunday-only and future-or-today selection
$todayCheck = new \DateTime('today'); $todayCheck = new \DateTime('today');
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = null;
$nowTz = null;
try {
$tz = new \DateTimeZone($tzName ?: 'UTC');
$nowTz = new \DateTime('now', $tz);
} catch (\Throwable $e) {
$tz = null;
$nowTz = null;
}
$validDates = []; $validDates = [];
$cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year')); $cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year'));
@@ -150,9 +164,11 @@ class ParentAttendanceReportController extends BaseController
$lateTodayStr = $todayCheck->format('Y-m-d'); $lateTodayStr = $todayCheck->format('Y-m-d');
if ($type === 'late') { if ($type === 'late') {
try { try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); if (!$tz) {
$tz = new \DateTimeZone($tzName ?: 'UTC'); $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$lateNow = new \DateTime('now', $tz); $tz = new \DateTimeZone($tzName ?: 'UTC');
}
$lateNow = $nowTz ?: new \DateTime('now', $tz);
$cm = $this->configModel; $cm = $this->configModel;
$cutoffStr = (string) ($cm->getConfig('late_report_cutoff') $cutoffStr = (string) ($cm->getConfig('late_report_cutoff')
?: $cm->getConfig('late_cutoff_time') ?: $cm->getConfig('late_cutoff_time')
@@ -183,6 +199,17 @@ class ParentAttendanceReportController extends BaseController
if ($dt < $todayCheck) { if ($dt < $todayCheck) {
return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.'); return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.');
} }
if ($nowTz && $tz) {
try {
$cutoff = new \DateTime($entry . ' ' . $cutoffThreshold . ':00', $tz);
if ($nowTz >= $cutoff) {
$abbr = $cutoff->format('T');
return redirect()->back()->withInput()->with('error', 'Reporting closes at ' . $cutoffThreshold . ' ' . $abbr . ' on the report date.');
}
} catch (\Throwable $e) {
// ignore and continue
}
}
if ($dt > $cap) { if ($dt > $cap) {
return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.'); return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.');
} }
@@ -800,6 +827,36 @@ class ParentAttendanceReportController extends BaseController
$update['reported'] = 1; $update['reported'] = 1;
} }
$this->attendanceDataModel->update((int) $existing['id'], $update); $this->attendanceDataModel->update((int) $existing['id'], $update);
return;
}
if ($existingStatus === '' || $existingStatus === 'present') {
$update = [
'status' => $status,
'reason' => $existing['reason'] ?: $reasonText,
'updated_at' => utc_now(),
];
if ($hasIsReported) {
$update['is_reported'] = 'yes';
}
if ($hasLegacyReported) {
$update['reported'] = 1;
}
$this->attendanceDataModel->update((int) $existing['id'], $update);
$classSectionId = (int) ($existing['class_section_id'] ?? $classSectionId ?? 0);
$schoolId = (string) ($existing['school_id'] ?? '');
if ($classSectionId > 0 && $schoolId !== '') {
$this->adjustAttendanceRecordForStatusChange(
studentId: $studentId,
classSectionId: $classSectionId,
schoolId: $schoolId,
semester: $semester,
schoolYear: $schoolYear,
oldStatus: $existingStatus,
newStatus: $status
);
}
} }
} }
@@ -856,6 +913,61 @@ class ParentAttendanceReportController extends BaseController
$this->attendanceRecordModel->insert($insert); $this->attendanceRecordModel->insert($insert);
} }
private function adjustAttendanceRecordForStatusChange(
int $studentId,
int $classSectionId,
string $schoolId,
string $semester,
string $schoolYear,
string $oldStatus,
string $newStatus
): void {
$oldStatus = strtolower($oldStatus);
$newStatus = strtolower($newStatus);
if ($oldStatus === $newStatus) {
return;
}
$record = $this->attendanceRecordModel->where([
'student_id' => $studentId,
'semester' => $semester,
'school_year' => $schoolYear,
])->first();
if (!$record) {
$this->bumpAttendanceRecordFromParent($studentId, $classSectionId, $schoolId, $newStatus, $semester, $schoolYear);
return;
}
$updates = [
'total_presence' => (int) ($record['total_presence'] ?? 0),
'total_absence' => (int) ($record['total_absence'] ?? 0),
'total_late' => (int) ($record['total_late'] ?? 0),
'total_attendance' => (int) ($record['total_attendance'] ?? 0),
'updated_at' => utc_now(),
'modified_by' => (int) (session()->get('user_id') ?? 0),
];
if ($oldStatus === 'present') {
$updates['total_presence'] = max(0, $updates['total_presence'] - 1);
} elseif ($oldStatus === 'absent') {
$updates['total_absence'] = max(0, $updates['total_absence'] - 1);
} elseif ($oldStatus === 'late') {
$updates['total_late'] = max(0, $updates['total_late'] - 1);
}
if ($newStatus === 'present') {
$updates['total_presence']++;
} elseif ($newStatus === 'absent') {
$updates['total_absence']++;
} elseif ($newStatus === 'late') {
$updates['total_late']++;
}
$this->attendanceRecordModel->update((int) $record['id'], $updates);
}
// GET: staff view to list reports (today & upcoming by default) // GET: staff view to list reports (today & upcoming by default)
public function list() public function list()
{ {
@@ -1503,6 +1615,20 @@ class ParentAttendanceReportController extends BaseController
return $userId; // fallback return $userId; // fallback
} }
private function normalizeCutoffTime(string $raw, string $default = '09:00'): string
{
$raw = trim($raw);
if ($raw === '') {
return $default;
}
if (preg_match('/^(\d{1,2}):(\d{2})$/', $raw, $m)) {
$hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT);
$mm = $m[2];
return $hh . ':' . $mm;
}
return $default;
}
/** /**
* Send confirmation/notification emails for parent attendance submissions. * Send confirmation/notification emails for parent attendance submissions.
*/ */
@@ -4,18 +4,21 @@ namespace App\Controllers\View;
use App\Models\CalendarModel; use App\Models\CalendarModel;
use App\Models\AttendanceDataModel; use App\Models\AttendanceDataModel;
use App\Models\ReportCardAcknowledgementModel;
use App\Services\SemesterRangeService; use App\Services\SemesterRangeService;
class ReportCardsController extends PrintablesBaseController class ReportCardsController extends PrintablesBaseController
{ {
protected $calendarModel; protected $calendarModel;
protected $semesterRangeService; protected $semesterRangeService;
protected $ackModel;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->calendarModel = new CalendarModel(); $this->calendarModel = new CalendarModel();
$this->semesterRangeService = new SemesterRangeService($this->configModel); $this->semesterRangeService = new SemesterRangeService($this->configModel);
$this->ackModel = new ReportCardAcknowledgementModel();
} }
public function report_card() public function report_card()
@@ -369,6 +372,24 @@ class ReportCardsController extends PrintablesBaseController
$isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v); $isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v);
$ackMap = [];
try {
$ackRows = $this->ackModel
->whereIn('student_id', $studentIds)
->where('school_year', $year)
->where('semester', $sem)
->orderBy('updated_at', 'DESC')
->findAll();
foreach ($ackRows as $row) {
$sid = (int) ($row['student_id'] ?? 0);
if ($sid > 0 && ! isset($ackMap[$sid])) {
$ackMap[$sid] = $row;
}
}
} catch (\Throwable $e) {
$ackMap = [];
}
$results = []; $results = [];
$completeCount = 0; $completeCount = 0;
$warningCount = 0; $warningCount = 0;
@@ -471,12 +492,16 @@ class ReportCardsController extends PrintablesBaseController
$warningCount++; $warningCount++;
} }
$ack = $sid > 0 ? ($ackMap[$sid] ?? null) : null;
$results[] = [ $results[] = [
'id' => $sid, 'id' => $sid,
'name' => $name !== '' ? $name : 'N/A', 'name' => $name !== '' ? $name : 'N/A',
'missing' => $missing, 'missing' => $missing,
'warnings' => $warnings, 'warnings' => $warnings,
'complete' => $isComplete, 'complete' => $isComplete,
'viewed_at' => $ack['viewed_at'] ?? null,
'signed_at' => $ack['signed_at'] ?? null,
'signed_name' => $ack['signed_name'] ?? null,
]; ];
} }
@@ -501,6 +526,37 @@ class ReportCardsController extends PrintablesBaseController
]); ]);
} }
/**
* API: report card parent acknowledgement status
* GET params: student_id, school_year, semester
*/
public function reportCardAcknowledgement()
{
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
if ($studentId <= 0) {
return $this->response->setJSON(['ok' => false, 'error' => 'Missing student_id'])->setStatusCode(400);
}
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->schoolYear ?? ''));
$semester = trim((string) ($this->request->getGet('semester') ?? $this->semester ?? ''));
$row = $this->ackModel
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->orderBy('updated_at', 'DESC')
->first();
return $this->response->setJSON([
'ok' => true,
'student_id' => $studentId,
'school_year' => $schoolYear,
'semester' => $semester,
'viewed_at' => $row['viewed_at'] ?? null,
'signed_at' => $row['signed_at'] ?? null,
'signed_name' => $row['signed_name'] ?? null,
]);
}
protected function fetchStudentsByClass(int $sectionId, ?string $schoolYear) protected function fetchStudentsByClass(int $sectionId, ?string $schoolYear)
{ {
$b = $this->studentClassModel $b = $this->studentClassModel
+32
View File
@@ -339,6 +339,37 @@ class ScoreController extends Controller
); );
} }
try {
$students = $this->getStudentScores($classSectionId, $semester, $schoolYear, $teacherId);
} catch (\Throwable $e) {
log_message('error', 'ScoreController::submitScoresLock missing check failed: ' . $e->getMessage());
return redirect()->back()->with('error', 'Unable to validate missing scores. Please try again.');
}
$missingRows = [];
foreach ($students as $student) {
$missingItems = $student['missing_items'] ?? [];
if (empty($missingItems)) {
continue;
}
$studentId = (int) ($student['student_id'] ?? 0);
$studentName = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''));
if ($studentName === '') {
$studentName = $studentId > 0 ? ('Student #' . $studentId) : 'Student';
}
$missingRows[] = $studentName . ': ' . implode(', ', $missingItems);
}
if (!empty($missingRows)) {
$sample = array_slice($missingRows, 0, 8);
$message = sprintf(
'Missing entries found for %d student(s). Fill them or mark "Missing ok" before submitting. %s',
count($missingRows),
$sample ? ('Examples: ' . implode(' | ', $sample)) : ''
);
return redirect()->back()->with('error', trim($message));
}
$existing = $this->gradingLockModel->getLock($classSectionId, $semester, $schoolYear); $existing = $this->gradingLockModel->getLock($classSectionId, $semester, $schoolYear);
if (!empty($existing) && !empty($existing['is_locked'])) { if (!empty($existing) && !empty($existing['is_locked'])) {
return redirect()->back()->with('status', 'Scores already submitted and locked.'); return redirect()->back()->with('status', 'Scores already submitted and locked.');
@@ -1378,6 +1409,7 @@ $students = $this->db->table('students')
$releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall $releaseScores = (strcasecmp($semester, 'Fall') === 0) ? $releaseFall
: ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny); : ((strcasecmp($semester, 'Spring') === 0) ? $releaseSpring : $releaseAny);
$scores[$selectedYear][$semester][$studentId] = [ $scores[$selectedYear][$semester][$studentId] = [
'student_id' => $studentId,
'school_id' => $student['school_id'], 'school_id' => $student['school_id'],
'student_firstname' => $student['firstname'], 'student_firstname' => $student['firstname'],
'student_lastname' => $student['lastname'], 'student_lastname' => $student['lastname'],
@@ -13,11 +13,11 @@ class AllowCompetitionWinnerRankTies extends Migration
return; return;
} }
if ($db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) { if ($this->indexExists($db, 'competition_winners', 'competition_id_class_section_id_rank')) {
$db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners'); $db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners');
} }
if (!$db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) { if (! $this->indexExists($db, 'competition_winners', 'competition_id_class_section_id_rank')) {
$db->query( $db->query(
'CREATE INDEX competition_id_class_section_id_rank ON competition_winners (competition_id, class_section_id, rank)' 'CREATE INDEX competition_id_class_section_id_rank ON competition_winners (competition_id, class_section_id, rank)'
); );
@@ -31,7 +31,7 @@ class AllowCompetitionWinnerRankTies extends Migration
return; return;
} }
if ($db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) { if ($this->indexExists($db, 'competition_winners', 'competition_id_class_section_id_rank')) {
$db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners'); $db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners');
} }
@@ -39,4 +39,23 @@ class AllowCompetitionWinnerRankTies extends Migration
'ALTER TABLE competition_winners ADD UNIQUE KEY competition_id_class_section_id_rank (competition_id, class_section_id, rank)' 'ALTER TABLE competition_winners ADD UNIQUE KEY competition_id_class_section_id_rank (competition_id, class_section_id, rank)'
); );
} }
private function indexExists($db, string $table, string $index): bool
{
if (method_exists($db, 'indexExists')) {
return $db->indexExists($table, $index);
}
$dbName = $db->getDatabase();
$builder = $db->table('information_schema.STATISTICS');
$row = $builder
->select('INDEX_NAME')
->where('TABLE_SCHEMA', $dbName)
->where('TABLE_NAME', $table)
->where('INDEX_NAME', $index)
->get()
->getRowArray();
return ! empty($row);
}
} }
@@ -1 +1,37 @@
Print all <?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPageSelectionToPrintRequests extends Migration
{
public function up()
{
if (! $this->db->tableExists('print_requests')) {
return;
}
if (! $this->db->fieldExists('page_selection', 'print_requests')) {
$this->forge->addColumn('print_requests', [
'page_selection' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
'after' => 'num_copies',
],
]);
}
}
public function down()
{
if (! $this->db->tableExists('print_requests')) {
return;
}
if ($this->db->fieldExists('page_selection', 'print_requests')) {
$this->forge->dropColumn('print_requests', 'page_selection');
}
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateReportCardAcknowledgements extends Migration
{
public function up()
{
if ($this->db->tableExists('report_card_acknowledgements')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'parent_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'student_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'viewed_at' => [
'type' => 'DATETIME',
'null' => true,
],
'signed_at' => [
'type' => 'DATETIME',
'null' => true,
],
'signed_name' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
],
'signer_ip' => [
'type' => 'VARCHAR',
'constraint' => 45,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['parent_id', 'student_id', 'school_year', 'semester'], false, true);
$this->forge->createTable('report_card_acknowledgements');
}
public function down()
{
$this->forge->dropTable('report_card_acknowledgements', true);
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class ReportCardAcknowledgementModel extends Model
{
protected $table = 'report_card_acknowledgements';
protected $primaryKey = 'id';
protected $allowedFields = [
'parent_id',
'student_id',
'school_year',
'semester',
'viewed_at',
'signed_at',
'signed_name',
'signer_ip',
'created_at',
'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $returnType = 'array';
}
+141 -71
View File
@@ -1,8 +1,21 @@
<?= $this->extend('layout/management_layout') ?> <?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<style> <style>
#adminProgressTable thead th { .admin-progress-table thead th {
position: static !important; position: static !important;
top: auto !important;
z-index: auto !important;
background: #f8f9fa;
white-space: nowrap;
line-height: 1.2;
padding-top: .75rem;
padding-bottom: .75rem;
}
.admin-progress-table td {
vertical-align: top;
}
.bg-orange {
background-color: #fd7e14 !important;
} }
</style> </style>
<div class="container py-4"> <div class="container py-4">
@@ -56,79 +69,136 @@
</form> </form>
<div class="card shadow-sm mt-5"> <div class="card shadow-sm mt-5">
<div class="card-body pt-4 px-0"> <div class="card-body pt-4">
<div class="table-responsive px-4" style="margin-top: 1.5rem;"> <?php
<table id="adminProgressTable" class="table table-hover align-middle mb-0"> $reportGroupsBySection = $reportGroupsBySection ?? [];
<thead class="table-light"> $subjectSections = $subjectSections ?? [];
<tr> $classSections = $classSections ?? [];
<th>Week</th> $sectionStats = $sectionStats ?? [];
<th>Subjects</th> $sectionSubjectCounts = $sectionSubjectCounts ?? [];
<th>Teacher</th> $expectedDays = (int) ($expectedDays ?? 0);
<th class="text-end">Action</th> ?>
</tr> <?php if (empty($classSections)): ?>
</thead> <div class="text-center text-muted py-4">No class sections found.</div>
<tbody> <?php else: ?>
<div class="accordion" id="adminProgressAccordion">
<?php foreach ($classSections as $index => $cs): ?>
<?php <?php
$reportGroups = $reportGroups ?? []; $sectionId = (int) ($cs['class_section_id'] ?? 0);
$subjectSections = $subjectSections ?? []; $sectionName = $cs['class_section_name'] ?? 'Unknown Section';
$sectionGroups = $reportGroupsBySection[$sectionId] ?? [];
$hasReports = ! empty($sectionGroups);
$collapseId = 'progress-section-' . $sectionId;
$headingId = 'progress-heading-' . $sectionId;
$stat = $sectionStats[$sectionId] ?? null;
$subjectCount = (int) ($sectionSubjectCounts[$sectionId] ?? 0);
if (! $stat) {
if ($expectedDays === 0) {
$stat = [
'submitted' => 0,
'expected' => 0,
'percent' => 0,
'badgeClass' => 'bg-secondary',
'labelClass' => 'text-muted',
];
} else {
$stat = [
'submitted' => 0,
'expected' => $expectedDays,
'percent' => 0,
'badgeClass' => 'bg-danger',
'labelClass' => 'text-danger',
];
}
}
$percentLabel = $expectedDays > 0 ? number_format((float) $stat['percent'], 1) . '%' : 'N/A';
$submissionLabel = $expectedDays > 0
? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')')
: 'Submitted: N/A';
$subjectLabel = 'Subjects: ' . $subjectCount;
?> ?>
<?php if (empty($reportGroups)): ?> <div class="accordion-item mb-2">
<tr><td colspan="4" class="text-center text-muted py-4">No reports found.</td></tr> <h2 class="accordion-header" id="<?= esc($headingId) ?>">
<?php else: ?> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
<?php foreach ($reportGroups as $group): ?> <?= esc($sectionName) ?>
<?php <span class="badge <?= esc($stat['badgeClass']) ?> ms-2"><?= esc($submissionLabel) ?></span>
$weekLabel = $group['week_start'] ? date('M d, Y', strtotime($group['week_start'])) : '-'; <span class="badge bg-info text-dark ms-2"><?= esc($subjectLabel) ?></span>
if (!empty($group['week_end'])) { <?php if ($hasReports): ?>
$weekLabel .= ' ' . date('M d, Y', strtotime($group['week_end'])); <span class="badge bg-secondary ms-2"><?= count($sectionGroups) ?> weeks</span>
} <?php endif; ?>
$reports = $group['reports'] ?? []; </button>
$teacherName = ''; </h2>
foreach ($reports as $report) { <div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= esc($headingId) ?>" data-bs-parent="#adminProgressAccordion">
if (!empty($report['teacher_name'])) { <div class="accordion-body">
$teacherName = $report['teacher_name']; <?php if (! $hasReports): ?>
break; <div class="text-muted">No reports found for this section.</div>
} <?php else: ?>
} <div class="table-responsive">
$exampleId = $reports ? reset($reports)['id'] : null; <table class="table table-hover align-middle mb-0 admin-progress-table no-mgmt-sticky" data-no-mgmt-sticky>
?> <thead class="table-light">
<tr> <tr>
<td> <th>Week</th>
<div class="fw-semibold"><?= esc($weekLabel) ?></div> <th>Subjects</th>
<?php if (!empty($group['class_section_name'])): ?> <th>Teacher</th>
<div class="text-muted small">Class: <?= esc($group['class_section_name']) ?></div> <th class="text-end">Action</th>
<?php endif; ?> </tr>
</td> </thead>
<td> <tbody>
<div class="d-flex flex-column gap-2"> <?php foreach ($sectionGroups as $group): ?>
<?php foreach ($subjectSections as $slug => $section): ?> <?php
<?php $weekLabel = $group['week_start'] ? date('M d, Y', strtotime($group['week_start'])) : '-';
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug; if (! empty($group['week_end'])) {
$report = $reports[$subjectName] ?? null; $weekLabel .= ' ' . date('M d, Y', strtotime($group['week_end']));
$statusTag = $report ? ($report['status_label'] ?? 'Unknown') : 'No entry'; }
$badgeClass = $report ? 'bg-secondary' : 'bg-light text-muted'; $reports = $group['reports'] ?? [];
?> $teacherName = '';
<div class="border rounded-3 p-2"> foreach ($reports as $report) {
<div class="d-flex justify-content-between align-items-center"> if (! empty($report['teacher_name'])) {
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong> $teacherName = $report['teacher_name'];
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span> break;
</div> }
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div> }
</div> $exampleId = $reports ? reset($reports)['id'] : null;
<?php endforeach; ?> ?>
<tr>
<td><div class="fw-semibold"><?= esc($weekLabel) ?></div></td>
<td>
<div class="d-flex flex-column gap-2">
<?php foreach ($subjectSections as $slug => $section): ?>
<?php
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
$report = $reports[$subjectName] ?? null;
$statusTag = $report ? ($report['status_label'] ?? 'Unknown') : 'No entry';
$badgeClass = $report ? 'bg-secondary' : 'bg-light text-muted';
?>
<div class="border rounded-3 p-2">
<div class="d-flex justify-content-between align-items-center">
<strong class="small mb-0"><?= esc($section['label'] ?? $subjectName) ?></strong>
<span class="badge <?= $badgeClass ?>"><?= esc($statusTag) ?></span>
</div>
<div class="small text-muted"><?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?></div>
</div>
<?php endforeach; ?>
</div>
</td>
<td><?= esc($teacherName ?: '-') ?></td>
<td class="text-end">
<?php if ($exampleId): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> </div>
</td> <?php endif; ?>
<td><?= esc($teacherName ?: '-') ?></td> </div>
<td class="text-end"> </div>
<?php if ($exampleId): ?> </div>
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('admin/progress/view/' . $exampleId) ?>">View</a> <?php endforeach; ?>
<?php endif; ?> </div>
</td> <?php endif; ?>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -517,37 +517,28 @@
<section class="mb-5"> <section class="mb-5">
<h2 class="mb-4 text-center">Statistics</h2> <h2 class="mb-4 text-center">Statistics</h2>
<div class="stats-row"> <div class="stats-row">
<!-- Students -->
<div class="stat-circle circle-primary"> <div class="stat-circle circle-primary">
<div class="stat-icon"><i class="bi bi-people-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-graduate"></i></div>
<div class="stat-value" data-stat="students">—</div> <div class="stat-value" data-stat="students">—</div>
<div class="stat-title">Students</div> <div class="stat-title">Students</div>
</div> </div>
<!-- Teachers -->
<div class="stat-circle circle-info"> <div class="stat-circle circle-info">
<div class="stat-icon"><i class="bi bi-person-video2"></i></div> <div class="stat-icon"><i class="fas fa-chalkboard-teacher"></i></div>
<div class="stat-value" data-stat="teachers">—</div> <div class="stat-value" data-stat="teachers">—</div>
<div class="stat-title">Teachers</div> <div class="stat-title">Teachers</div>
</div> </div>
<!-- Teacher Assistants -->
<div class="stat-circle circle-success"> <div class="stat-circle circle-success">
<div class="stat-icon"><i class="bi bi-person-badge-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-cog"></i></div>
<div class="stat-value" data-stat="teacherAssistants">—</div> <div class="stat-value" data-stat="teacherAssistants">—</div>
<div class="stat-title">Teachers' Assistants</div> <div class="stat-title">TAs</div>
</div> </div>
<!-- Admins -->
<div class="stat-circle circle-warning"> <div class="stat-circle circle-warning">
<div class="stat-icon"><i class="bi bi-shield-lock-fill"></i></div> <div class="stat-icon"><i class="fas fa-user-shield"></i></div>
<div class="stat-value" data-stat="admins">—</div> <div class="stat-value" data-stat="admins">—</div>
<div class="stat-title">Admins</div> <div class="stat-title">Admins</div>
</div> </div>
<!-- Parents -->
<div class="stat-circle circle-light"> <div class="stat-circle circle-light">
<div class="stat-icon"><i class="bi bi-people"></i></div> <div class="stat-icon"><i class="fas fa-users"></i></div>
<div class="stat-value" data-stat="parents">—</div> <div class="stat-value" data-stat="parents">—</div>
<div class="stat-title">Parents</div> <div class="stat-title">Parents</div>
</div> </div>
@@ -71,9 +71,20 @@
max-height: none; max-height: none;
overflow: visible; overflow: visible;
} }
.attn-pie-wrap {
width: 320px;
height: 320px;
margin: 0 auto;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
.attn-analysis-half { grid-column: span 12; } .attn-analysis-half { grid-column: span 12; }
} }
@media (max-width: 576px) {
.attn-pie-wrap {
width: 180px;
height: 180px;
}
}
</style> </style>
<?= $this->endSection() ?> <?= $this->endSection() ?>
@@ -291,7 +302,9 @@
<div class="attn-analysis-grid"> <div class="attn-analysis-grid">
<div class="attn-analysis-card attn-analysis-half"> <div class="attn-analysis-card attn-analysis-half">
<h6>Overall Distribution</h6> <h6>Overall Distribution</h6>
<canvas id="attnPieChart" height="50" aria-label="Overall attendance pie chart"></canvas> <div class="attn-pie-wrap">
<canvas id="attnPieChart" aria-label="Overall attendance pie chart"></canvas>
</div>
<table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky> <table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky>
<thead> <thead>
<tr> <tr>
@@ -415,6 +428,8 @@
}] }]
}, },
options: { options: {
responsive: true,
maintainAspectRatio: false,
plugins: { plugins: {
tooltip: { tooltip: {
callbacks: { callbacks: {
+173 -14
View File
@@ -322,6 +322,72 @@
min-height: 400px; min-height: 400px;
} }
.home-stats {
background: #ffffff;
border-radius: 16px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
padding: 2.5rem 1.5rem;
}
.home-stats .stats-row {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: center;
}
.home-stats .stat-circle {
width: 190px;
height: 190px;
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #ffffff;
font-weight: 700;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.12);
}
.home-stats .stat-value {
font-size: 1.8rem;
line-height: 1;
}
.home-stats .stat-icon {
font-size: 2rem;
margin-bottom: .2rem;
opacity: .9;
}
.home-stats .stat-title {
font-size: 1.1rem;
margin-top: .25rem;
font-weight: 500;
text-align: center;
line-height: 1.2;
}
.home-stats .circle-primary {
background: radial-gradient(circle at 30% 30%, #0d6efd, #0044aa);
}
.home-stats .circle-success {
background: radial-gradient(circle at 30% 30%, #198754, #0c4f2c);
}
.home-stats .circle-warning {
background: radial-gradient(circle at 30% 30%, #daa544ff, #a66f00);
}
.home-stats .circle-info {
background: radial-gradient(circle at 30% 30%, #0dcaf0, #047e9c);
}
.home-stats .circle-light {
background: radial-gradient(circle at 30% 30%, #63af4cff, #00eb7dff);
}
@media (max-width: 992px) { @media (max-width: 992px) {
.image-column { .image-column {
min-height: 300px; min-height: 300px;
@@ -332,6 +398,13 @@
border-radius: 0 0 10px 10px !important; border-radius: 0 0 10px 10px !important;
} }
} }
@media (max-width: 768px) {
.home-stats .stat-circle {
width: 160px;
height: 160px;
}
}
</style> </style>
</head> </head>
@@ -448,8 +521,43 @@
<h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4> <h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4>
</a> </a>
</section> </section>
<!-- Statistics Start -->
<div class="container-xxl py-2 content-section">
<div class="container">
<div class="home-stats" data-dashboard-endpoint="<?= site_url('api/administrator/dashboard') ?>">
<h1 class="d-flex justify-content-center p-md-1">Active Participants</h1>
<br>
<div class="stats-row">
<div class="stat-circle circle-primary">
<div class="stat-icon"><i class="fa-solid fa-user-graduate"></i></div>
<div class="stat-value" data-stat="students">—</div>
<div class="stat-title">Students</div>
</div>
<div class="stat-circle circle-info">
<div class="stat-icon"><i class="fa-solid fa-chalkboard-user"></i></div>
<div class="stat-value" data-stat="teachers">—</div>
<div class="stat-title">Teachers</div>
</div>
<div class="stat-circle circle-success">
<div class="stat-icon"><i class="fa-solid fa-user-gear"></i></div>
<div class="stat-value" data-stat="teacherAssistants">—</div>
<div class="stat-title">TAs</div>
</div>
<div class="stat-circle circle-warning">
<div class="stat-icon"><i class="fa-solid fa-user-shield"></i></div>
<div class="stat-value" data-stat="admins">—</div>
<div class="stat-title">Admins</div>
</div>
<div class="stat-circle circle-light">
<div class="stat-icon"><i class="fa-solid fa-people-group"></i></div>
<div class="stat-value" data-stat="parents">—</div>
<div class="stat-title">Parents</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics End -->
<div class="container-xxl py-2 content-section"> <div class="container-xxl py-2 content-section">
<div class="container"> <div class="container">
<div class="row bg-light squared align-items-center"> <div class="row bg-light squared align-items-center">
@@ -461,18 +569,18 @@
<p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p> <p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p>
<p>For more information, click below to visit ISGL official website.</p> <p>For more information, click below to visit ISGL official website.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0"> <table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr> <tr>
<td align="center" bgcolor="#28a745" style="border-radius:6px;"> <td align="center" bgcolor="#28a745" style="border-radius:6px;">
<a href="https://isgl.org" <a href="https://isgl.org"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;"> style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;">
ISGL Official Website ISGL Official Website
</a> </a>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
<!-- Image Column - Fixed with correct path --> <!-- Image Column - Fixed with correct path -->
@@ -677,6 +785,57 @@
} }
}); });
</script> </script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.querySelector('.home-stats[data-dashboard-endpoint]');
if (!container) return;
const endpoint = container.dataset.dashboardEndpoint;
const statElements = container.querySelectorAll('[data-stat]');
const numberFormatter = new Intl.NumberFormat();
let isLoading = false;
const loadStats = function() {
if (isLoading) return;
isLoading = true;
fetch(endpoint, {
headers: {
'Accept': 'application/json'
},
credentials: 'same-origin',
})
.then(function(response) {
if (!response.ok) {
throw new Error('Request failed');
}
return response.json();
})
.then(function(payload) {
const counts = payload && typeof payload === 'object' && payload.counts ?
payload.counts :
{};
statElements.forEach(function(element) {
const key = element.dataset.stat;
const value = counts[key];
element.textContent = Number.isFinite(value) ?
numberFormatter.format(value) :
'—';
});
})
.catch(function() {})
.finally(function() {
isLoading = false;
});
};
loadStats();
setInterval(loadStats, 60000);
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
loadStats();
}
});
});
</script>
</body> </body>
</html> </html>
@@ -121,6 +121,15 @@
</div> </div>
</div> </div>
<!-- Report Cards -->
<div class="col-auto">
<a href="<?= base_url('parent/report-cards') ?>" class="d-inline-block text-decoration-none">
<div class="circle-box bg-dark text-white position-relative" role="button" aria-label="Open report cards">
<h4 class="fw-bold mb-0">Report Cards</h4>
</div>
</a>
</div>
<!-- Enrollment --> <!-- Enrollment -->
<div class="col-auto"> <div class="col-auto">
<div class="circle-box bg-info text-white"> <div class="circle-box bg-info text-white">
+93 -5
View File
@@ -122,15 +122,40 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<?php
$cutoffDate = '';
$cutoffTime = '';
$cutoffThreshold = (string) ($cutoffThreshold ?? '09:00');
$cutoffTzAbbr = '';
try {
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
$tz = new DateTimeZone($tzName ?: 'UTC');
$nowTz = new DateTime('now', $tz);
$cutoffDate = $nowTz->format('Y-m-d');
$cutoffTime = $nowTz->format('H:i');
$cutoffTzAbbr = $nowTz->format('T');
} catch (\Throwable $e) {
$cutoffDate = '';
$cutoffTime = '';
$cutoffTzAbbr = '';
}
?>
<form id="reportForm" <form id="reportForm"
method="post" method="post"
action="<?= site_url('parent/report-attendance') ?>" action="<?= site_url('parent/report-attendance') ?>"
data-csrf-name="<?= esc(csrf_token()) ?>" data-csrf-name="<?= esc(csrf_token()) ?>"
data-csrf-value="<?= esc(csrf_hash()) ?>" data-csrf-value="<?= esc(csrf_hash()) ?>"
data-check-url="<?= site_url('api/parent/report-attendance/check') ?>"> data-check-url="<?= site_url('api/parent/report-attendance/check') ?>"
data-cutoff-date="<?= esc($cutoffDate) ?>"
data-cutoff-time="<?= esc($cutoffTime) ?>"
data-cutoff-threshold="<?= esc($cutoffThreshold) ?>"
data-cutoff-blocked="0">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>"> <input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
<div id="clientCheckAlert" class="alert d-none" role="alert"></div> <div id="clientCheckAlert" class="alert d-none" role="alert"></div>
<div id="cutoffAlert" class="alert alert-warning d-none" role="alert">
Reporting closes at <?= esc($cutoffThreshold) ?><?= $cutoffTzAbbr !== '' ? ' ' . esc($cutoffTzAbbr) : '' ?> on the report date.
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Select Child(ren)</label> <label class="form-label fw-semibold">Select Child(ren)</label>
@@ -352,7 +377,10 @@
const dates = selectedDates(); const dates = selectedDates();
if (!checkUrl || !dateSel || !typeSel) return; if (!checkUrl || !dateSel || !typeSel) return;
if (!ids.length || !dates.length) { if (!ids.length || !dates.length) {
if (submitBtn) submitBtn.disabled = false; if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
return; return;
} }
@@ -395,7 +423,10 @@
} }
if (!data || !data.ok) { if (!data || !data.ok) {
if (submitBtn) submitBtn.disabled = false; if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
return; return;
} }
@@ -454,10 +485,16 @@
setAlert('', ''); setAlert('', '');
} }
if (submitBtn) submitBtn.disabled = (selectedIds().length === 0); if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked || (selectedIds().length === 0);
}
} catch (e) { } catch (e) {
setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.'); setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.');
if (submitBtn) submitBtn.disabled = false; if (submitBtn) {
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
submitBtn.disabled = cutoffBlocked;
}
} }
} }
@@ -472,4 +509,55 @@
runCheck(); runCheck();
}); });
</script> </script>
<script>
// Cutoff enforcement helper (9:00 AM on the selected Sunday).
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('reportForm');
if (!form) return;
const dateSel = form.querySelector('select[name="dates[]"]');
const submitBtn = form.querySelector('button[type="submit"]');
const cutoffAlert = document.getElementById('cutoffAlert');
const cutoffDate = form.getAttribute('data-cutoff-date') || '';
const cutoffTime = form.getAttribute('data-cutoff-time') || '';
const cutoffThreshold = form.getAttribute('data-cutoff-threshold') || '09:00';
function parseTimeToMinutes(val) {
const parts = (val || '').split(':');
if (parts.length < 2) return null;
const hh = parseInt(parts[0], 10);
const mm = parseInt(parts[1], 10);
if (Number.isNaN(hh) || Number.isNaN(mm)) return null;
return (hh * 60) + mm;
}
function updateCutoffState() {
if (!dateSel || !cutoffDate || !cutoffTime) return;
const dates = Array.from(dateSel.selectedOptions || [])
.map(opt => opt.value)
.filter(v => v);
const nowMinutes = parseTimeToMinutes(cutoffTime);
const cutoffMinutes = parseTimeToMinutes(cutoffThreshold);
const isTodaySelected = dates.includes(cutoffDate);
const blocked = isTodaySelected && nowMinutes !== null && cutoffMinutes !== null && nowMinutes >= cutoffMinutes;
form.setAttribute('data-cutoff-blocked', blocked ? '1' : '0');
if (cutoffAlert) {
cutoffAlert.classList.toggle('d-none', !blocked);
}
if (submitBtn && blocked) {
submitBtn.disabled = true;
}
}
form.addEventListener('change', function(e) {
const tgt = e.target;
if (!tgt) return;
if (tgt.matches('select[name="dates[]"], input[name="student_ids[]"], #reportType')) {
updateCutoffState();
}
});
updateCutoffState();
});
</script>
<?= $this->endSection() ?> <?= $this->endSection() ?>
+72
View File
@@ -0,0 +1,72 @@
<?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?>
<div class="container my-5">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<h3 class="text-success mb-1" style="font-family: Arial, sans-serif;">Report Cards</h3>
<div class="text-muted small">
<?= esc($schoolYear ?: 'N/A') ?> <?= $semester ? '• ' . esc($semester) . ' Semester' : '' ?>
</div>
</div>
</div>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (empty($students)): ?>
<div class="alert alert-info">No students available for report cards.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle">
<thead class="table-light">
<tr>
<th>Student</th>
<th>Class Section</th>
<th>Viewed</th>
<th>Signature</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<?php
$sid = (int) ($student['id'] ?? 0);
$ack = $ackMap[$sid] ?? null;
$viewedAt = $ack['viewed_at'] ?? '';
$signedAt = $ack['signed_at'] ?? '';
$signedName = $ack['signed_name'] ?? '';
?>
<tr>
<td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
<td><?= esc($student['class_section_name'] ?? 'N/A') ?></td>
<td><?= $viewedAt ? esc(local_datetime($viewedAt, 'm-d-Y H:i')) : 'Not viewed' ?></td>
<td>
<?php if ($signedAt): ?>
<?= esc($signedName ?: 'Signed') ?><br>
<small class="text-muted"><?= esc(local_datetime($signedAt, 'm-d-Y H:i')) ?></small>
<?php else: ?>
<span class="text-muted">Not signed</span>
<?php endif; ?>
</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('parent/report-cards/view/' . $sid) ?>">View Report</a>
<?php if (! $signedAt): ?>
<form class="d-inline-flex align-items-center gap-2 ms-2" method="post" action="<?= base_url('parent/report-cards/sign/' . $sid) ?>">
<?= csrf_field() ?>
<input type="text" name="signed_name" class="form-control form-control-sm" placeholder="Full name" required>
<button class="btn btn-sm btn-success" type="submit">Sign</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?= $this->endSection() ?>
+30 -1
View File
@@ -1,7 +1,9 @@
<?= $this->extend('layout/main_layout') ?> <?= $this->extend('layout/main_layout') ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<div class="container my-5"> <div class="container my-5">
<h3 class="text-center text-success" style="font-family: Arial, sans-serif;">Scores</h2> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<h3 class="text-success mb-0" style="font-family: Arial, sans-serif;">Scores</h3>
</div>
</div> </div>
<!-- Filter Form --> <!-- Filter Form -->
@@ -73,6 +75,7 @@ $showExamScoresBySemester = $showExamScoresBySemester ?? [];
<?php if ($showExamScoresForSemester): ?><th>Exam</th><?php endif; ?> <?php if ($showExamScoresForSemester): ?><th>Exam</th><?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php if ($showExamScoresForSemester): ?><th>Semester Score</th><?php endif; ?> <?php if ($showExamScoresForSemester): ?><th>Semester Score</th><?php endif; ?>
<?php if ($showExamScoresForSemester): ?><th>Report Card</th><?php endif; ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -96,6 +99,32 @@ $showExamScoresBySemester = $showExamScoresBySemester ?? [];
<?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['midterm_exam']['score'] ?? $student['scores']['final_exam']['score'] ?? '-') ?></td><?php endif; ?> <?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['midterm_exam']['score'] ?? $student['scores']['final_exam']['score'] ?? '-') ?></td><?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['semester']['score'] ?? '-') ?></td><?php endif; ?> <?php if ($showExamScoresForSemester): ?><td><?= esc($student['scores']['semester']['score'] ?? '-') ?></td><?php endif; ?>
<?php if ($showExamScoresForSemester): ?>
<td>
<?php
$reportStudentId = (int) ($student['student_id'] ?? 0);
if ($reportStudentId > 0) {
$reportYear = (string) ($selectedYear ?? '');
$reportSemester = (string) ($semester ?? '');
$reportDate = date('Y-m-d');
$reportUrl = base_url('parent/report-cards/view/' . $reportStudentId);
$query = http_build_query([
'school_year' => $reportYear,
'semester' => $reportSemester,
'report_date' => $reportDate,
]);
if ($query) {
$reportUrl .= '?' . $query;
}
}
?>
<?php if ($reportStudentId > 0): ?>
<a class="btn btn-sm btn-outline-primary" href="<?= esc($reportUrl) ?>" target="_blank" rel="noopener">View</a>
<?php else: ?>
<span class="text-muted">Unavailable</span>
<?php endif; ?>
</td>
<?php endif; ?>
</tr> </tr>
<?php if (!empty($student['comment'])): ?> <?php if (!empty($student['comment'])): ?>
<tr> <tr>
+6
View File
@@ -334,6 +334,12 @@ switch ($role) {
'label' => 'Scores', 'label' => 'Scores',
'title' => 'Review progressive academic score for the year (homeworks, projects and exams).', 'title' => 'Review progressive academic score for the year (homeworks, projects and exams).',
], ],
[
'href' => base_url('/parent/report-cards'),
'icon' => 'bi-file-earmark-text',
'label' => 'Report Cards',
'title' => 'View and acknowledge student report cards.',
],
[ [
'href' => base_url('/parent/progress'), 'href' => base_url('/parent/progress'),
'icon' => 'bi-journal-check', 'icon' => 'bi-journal-check',
+44 -1
View File
@@ -48,6 +48,7 @@
<a id="downloadStudentBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a> <a id="downloadStudentBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a>
</div> </div>
</div> </div>
<div class="text-muted small text-center" id="ackStatus"></div>
</form> </form>
<div class="card shadow-sm mb-3"> <div class="card shadow-sm mb-3">
@@ -67,10 +68,12 @@
<table class="table table-sm table-bordered align-middle mb-0"> <table class="table table-sm table-bordered align-middle mb-0">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th style="width: 35%;">Student</th> <th style="width: 28%;">Student</th>
<th style="width: 15%;">Status</th> <th style="width: 15%;">Status</th>
<th>Missing</th> <th>Missing</th>
<th>Warnings</th> <th>Warnings</th>
<th style="width: 16%;">Viewed</th>
<th style="width: 16%;">Signed</th>
</tr> </tr>
</thead> </thead>
<tbody id="completenessBody"></tbody> <tbody id="completenessBody"></tbody>
@@ -91,6 +94,7 @@
document.addEventListener('DOMContentLoaded', function(){ document.addEventListener('DOMContentLoaded', function(){
const API = <?= json_encode(site_url('api/printables/report-card/meta')) ?>; const API = <?= json_encode(site_url('api/printables/report-card/meta')) ?>;
const COMPLETENESS_API = <?= json_encode(site_url('api/printables/report-card/completeness')) ?>; const COMPLETENESS_API = <?= json_encode(site_url('api/printables/report-card/completeness')) ?>;
const ACK_API = <?= json_encode(site_url('api/printables/report-card/ack')) ?>;
// add cache-busting cb param // add cache-busting cb param
const withCB = (u) => u + (u.includes('?') ? '&' : '?') + 'cb=' + Date.now(); const withCB = (u) => u + (u.includes('?') ? '&' : '?') + 'cb=' + Date.now();
@@ -119,6 +123,7 @@ document.addEventListener('DOMContentLoaded', function(){
const $completenessSummary = document.getElementById('completenessSummary'); const $completenessSummary = document.getElementById('completenessSummary');
const $completenessWrap = document.getElementById('completenessTableWrap'); const $completenessWrap = document.getElementById('completenessTableWrap');
const $completenessBody = document.getElementById('completenessBody'); const $completenessBody = document.getElementById('completenessBody');
const $ackStatus = document.getElementById('ackStatus');
function opt(el, val, label) { function opt(el, val, label) {
const o = document.createElement('option'); o.value = String(val); o.textContent = label; el.appendChild(o); const o = document.createElement('option'); o.value = String(val); o.textContent = label; el.appendChild(o);
@@ -157,10 +162,40 @@ document.addEventListener('DOMContentLoaded', function(){
// Classes // Classes
$class.innerHTML = ''; $class.innerHTML = '';
(data.classSections || []).forEach(c => opt($class, c.class_section_id || c.id, c.class_section_name)); (data.classSections || []).forEach(c => opt($class, c.class_section_id || c.id, c.class_section_name));
updateAckStatus();
}
async function updateAckStatus() {
if (! $ackStatus) return;
const sid = $student.value;
const y = $year.value || '';
const s = $sem.value || '';
if (!sid) {
$ackStatus.textContent = '';
return;
}
try {
const url = `${ACK_API}?student_id=${encodeURIComponent(sid)}&school_year=${encodeURIComponent(y)}&semester=${encodeURIComponent(s)}`;
const res = await fetch(url, { credentials: 'same-origin' });
const data = await res.json();
if (!res.ok || data?.ok === false) {
$ackStatus.textContent = 'Parent acknowledgement: unavailable';
return;
}
const viewed = data.viewed_at ? `Viewed: ${data.viewed_at}` : 'Viewed: not yet';
const signed = data.signed_at
? `Signed: ${data.signed_at} (${data.signed_name || 'name missing'})`
: 'Signed: not yet';
$ackStatus.textContent = `Parent acknowledgement — ${viewed} · ${signed}`;
} catch (err) {
$ackStatus.textContent = 'Parent acknowledgement: unavailable';
}
} }
$year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); }); $year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); });
$sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); }); $sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); });
$student.addEventListener('change', function(){ updateAckStatus(); });
$viewS.addEventListener('click', function(){ $viewS.addEventListener('click', function(){
const sid = $student.value; const y = $year.value; const s=$sem.value; const sid = $student.value; const y = $year.value; const s=$sem.value;
@@ -254,10 +289,18 @@ document.addEventListener('DOMContentLoaded', function(){
tdMissing.textContent = missing.length ? missing.join(', ') : 'None'; tdMissing.textContent = missing.length ? missing.join(', ') : 'None';
const tdWarnings = document.createElement('td'); const tdWarnings = document.createElement('td');
tdWarnings.textContent = warningsList.length ? warningsList.join(', ') : 'None'; tdWarnings.textContent = warningsList.length ? warningsList.join(', ') : 'None';
const tdViewed = document.createElement('td');
tdViewed.textContent = row.viewed_at ? row.viewed_at : 'Not viewed';
const tdSigned = document.createElement('td');
tdSigned.textContent = row.signed_at
? `${row.signed_at}${row.signed_name ? ` (${row.signed_name})` : ''}`
: 'Not signed';
tr.appendChild(tdName); tr.appendChild(tdName);
tr.appendChild(tdStatus); tr.appendChild(tdStatus);
tr.appendChild(tdMissing); tr.appendChild(tdMissing);
tr.appendChild(tdWarnings); tr.appendChild(tdWarnings);
tr.appendChild(tdViewed);
tr.appendChild(tdSigned);
$completenessBody.appendChild(tr); $completenessBody.appendChild(tr);
}); });
$completenessWrap.hidden = false; $completenessWrap.hidden = false;
+6 -1
View File
@@ -3,7 +3,7 @@
<div class="container py-4"> <div class="container py-4">
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<div> <div>
<h3 class="mb-0">My Progress Reports</h3> <h3 class="mb-0">Class Progress Reports</h3>
<div class="text-muted">Review weekly submissions and compare Islamic Studies with Quran/Arabic.</div> <div class="text-muted">Review weekly submissions and compare Islamic Studies with Quran/Arabic.</div>
</div> </div>
<a href="<?= base_url('teacher/progress/submit') ?>" class="btn btn-outline-secondary">Submit New Report</a> <a href="<?= base_url('teacher/progress/submit') ?>" class="btn btn-outline-secondary">Submit New Report</a>
@@ -78,6 +78,11 @@
<div class="small text-muted"> <div class="small text-muted">
<?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?> <?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?>
</div> </div>
<?php if ($report): ?>
<div class="small text-muted">
Submitted by: <?= esc(trim((string) ($report['teacher_name'] ?? '')) ?: 'Unknown') ?>
</div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
+19 -4
View File
@@ -175,10 +175,13 @@
<?php for ($i = 0; $i < $rowsCount; $i++): ?> <?php for ($i = 0; $i < $rowsCount; $i++): ?>
<div class="row g-2 align-items-end mb-2 unit-chapter-entry"> <div class="row g-2 align-items-end mb-2 unit-chapter-entry">
<div class="col"> <div class="col">
<input type="text" name="unit_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Unit name" value="<?= esc($unitValues[$i] ?? '') ?>"> <input type="text" name="unit_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Unit name" value="<?= esc($unitValues[$i] ?? '') ?>" readonly>
</div> </div>
<div class="col"> <div class="col">
<input type="text" name="chapter_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Chapter" value="<?= esc($chapterValues[$i] ?? '') ?>"> <input type="text" name="chapter_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Chapter" value="<?= esc($chapterValues[$i] ?? '') ?>" readonly>
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-unit-chapter aria-label="Remove entry">X</button>
</div> </div>
</div> </div>
<?php endfor; ?> <?php endfor; ?>
@@ -263,10 +266,13 @@
row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry'; row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry';
row.innerHTML = ` row.innerHTML = `
<div class="col"> <div class="col">
<input type="text" name="unit_${slug}[]" class="form-control form-control-sm" placeholder="Unit name" /> <input type="text" name="unit_${slug}[]" class="form-control form-control-sm" placeholder="Unit name" readonly />
</div> </div>
<div class="col"> <div class="col">
<input type="text" name="chapter_${slug}[]" class="form-control form-control-sm" placeholder="Chapter" /> <input type="text" name="chapter_${slug}[]" class="form-control form-control-sm" placeholder="Chapter" readonly />
</div>
<div class="col-auto">
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-unit-chapter aria-label="Remove entry">X</button>
</div> </div>
`; `;
const inputs = row.querySelectorAll('input'); const inputs = row.querySelectorAll('input');
@@ -338,6 +344,15 @@
}); });
}); });
document.addEventListener('click', event => {
const button = event.target.closest('[data-remove-unit-chapter]');
if (!button) return;
const row = button.closest('.unit-chapter-entry');
if (row) {
row.remove();
}
});
document.querySelectorAll('[data-custom-entry]').forEach(button => { document.querySelectorAll('[data-custom-entry]').forEach(button => {
button.addEventListener('click', event => { button.addEventListener('click', event => {
event.stopPropagation(); event.stopPropagation();
@@ -37,6 +37,7 @@
<?php if (! $report): ?> <?php if (! $report): ?>
<div class="text-muted">No entry submitted for this subject this week.</div> <div class="text-muted">No entry submitted for this subject this week.</div>
<?php else: ?> <?php else: ?>
<div class="mb-2"><strong>Submitted by:</strong> <?= esc(trim((string) ($report['teacher_name'] ?? '')) ?: 'Unknown') ?></div>
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div> <div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
<div class="mb-3"><strong>Activities</strong><div class="mt-1"><?= nl2br(esc($report['covered'] ?? '-')) ?></div></div> <div class="mb-3"><strong>Activities</strong><div class="mt-1"><?= nl2br(esc($report['covered'] ?? '-')) ?></div></div>
<div class="mb-3"> <div class="mb-3">
+13 -2
View File
@@ -32,9 +32,20 @@ document.addEventListener('DOMContentLoaded', function () {
toggleCommentCheckbox(wrapper); toggleCommentCheckbox(wrapper);
}); });
let lastSubmitterId = null;
form.addEventListener('click', function (event) {
const target = event.target.closest('button, input[type="submit"]');
if (!target || target.form !== form) return;
lastSubmitterId = target.id || null;
});
form.addEventListener('submit', function (event) { form.addEventListener('submit', function (event) {
const submitter = event.submitter; const activeEl = document.activeElement;
if (!submitter || submitter.id !== 'submitScoresLockBtn') { const submitterId = event.submitter
? event.submitter.id
: (activeEl && activeEl.form === form ? activeEl.id : lastSubmitterId);
lastSubmitterId = null;
if (submitterId !== 'submitScoresLockBtn') {
return; return;
} }
+1 -1
View File
@@ -1 +1 @@
a:3:{s:4:"time";i:1770759428;s:3:"ttl";i:300;s:4:"data";a:3:{i:0;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:1;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:2;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:{}}}}}} a:3:{s:4:"time";i:1772133029;s:3:"ttl";i:300;s:4:"data";a:3:{i:0;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:1;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:2;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:{}}}}}}
File diff suppressed because one or more lines are too long
-66
View File
@@ -1,66 +0,0 @@
ERROR - 2026-02-10 07:23:30 --> mysqli_sql_exception: Column 'school_year' in where clause is ambiguous in /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Database/MySQLi/Connection.php:327
Stack trace:
#0 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Database/MySQLi/Connection.php(327): mysqli->query()
#1 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Database/BaseConnection.php(729): CodeIgniter\Database\MySQLi\Connection->execute()
#2 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Database/BaseConnection.php(646): CodeIgniter\Database\BaseConnection->simpleQuery()
#3 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Database/BaseBuilder.php(1649): CodeIgniter\Database\BaseConnection->query()
#4 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Model.php(286): CodeIgniter\Database\BaseBuilder->get()
#5 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/BaseModel.php(677): CodeIgniter\Model->doFindAll()
#6 /opt/lampp/htdocs/alrahma_school_sunday/app/Models/StudentClassModel.php(55): CodeIgniter\BaseModel->findAll()
#7 /opt/lampp/htdocs/alrahma_school_sunday/app/Controllers/View/AttendanceTrackingController.php(70): App\Models\StudentClassModel->findAll()
#8 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/CodeIgniter.php(933): App\Controllers\View\AttendanceTrackingController->pendingViolationsView()
#9 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/CodeIgniter.php(507): CodeIgniter\CodeIgniter->runController()
#10 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/CodeIgniter.php(354): CodeIgniter\CodeIgniter->handleRequest()
#11 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Boot.php(363): CodeIgniter\CodeIgniter->run()
#12 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/Boot.php(68): CodeIgniter\Boot::runCodeIgniter()
#13 /opt/lampp/htdocs/alrahma_school_sunday/public/index.php(60): CodeIgniter\Boot::bootWeb()
#14 /opt/lampp/htdocs/alrahma_school_sunday/vendor/codeigniter4/framework/system/rewrite.php(44): require_once('...')
#15 {main}
CRITICAL - 2026-02-10 07:23:30 --> CodeIgniter\Database\Exceptions\DatabaseException: Column 'school_year' in where clause is ambiguous
[Method: GET, Route: attendance/violations]
in SYSTEMPATH/Database/BaseConnection.php on line 684.
1 SYSTEMPATH/Database/BaseBuilder.php(1649): CodeIgniter\Database\BaseConnection->query()
2 SYSTEMPATH/Model.php(286): CodeIgniter\Database\BaseBuilder->get()
3 SYSTEMPATH/BaseModel.php(677): CodeIgniter\Model->doFindAll()
4 APPPATH/Models/StudentClassModel.php(55): CodeIgniter\BaseModel->findAll()
5 APPPATH/Controllers/View/AttendanceTrackingController.php(70): App\Models\StudentClassModel->findAll()
6 SYSTEMPATH/CodeIgniter.php(933): App\Controllers\View\AttendanceTrackingController->pendingViolationsView()
7 SYSTEMPATH/CodeIgniter.php(507): CodeIgniter\CodeIgniter->runController()
8 SYSTEMPATH/CodeIgniter.php(354): CodeIgniter\CodeIgniter->handleRequest()
9 SYSTEMPATH/Boot.php(363): CodeIgniter\CodeIgniter->run()
10 SYSTEMPATH/Boot.php(68): CodeIgniter\Boot::runCodeIgniter()
11 FCPATH/index.php(60): CodeIgniter\Boot::bootWeb()
12 SYSTEMPATH/rewrite.php(44): require_once('/opt/lampp/htdocs/alrahma_school_sunday/public/index.php')
CRITICAL - 2026-02-10 07:23:30 --> [Caused by] CodeIgniter\Database\Exceptions\DatabaseException: Column 'school_year' in where clause is ambiguous
in SYSTEMPATH/Database/MySQLi/Connection.php on line 332.
1 SYSTEMPATH/Database/BaseConnection.php(729): CodeIgniter\Database\MySQLi\Connection->execute()
2 SYSTEMPATH/Database/BaseConnection.php(646): CodeIgniter\Database\BaseConnection->simpleQuery()
3 SYSTEMPATH/Database/BaseBuilder.php(1649): CodeIgniter\Database\BaseConnection->query()
4 SYSTEMPATH/Model.php(286): CodeIgniter\Database\BaseBuilder->get()
5 SYSTEMPATH/BaseModel.php(677): CodeIgniter\Model->doFindAll()
6 APPPATH/Models/StudentClassModel.php(55): CodeIgniter\BaseModel->findAll()
7 APPPATH/Controllers/View/AttendanceTrackingController.php(70): App\Models\StudentClassModel->findAll()
8 SYSTEMPATH/CodeIgniter.php(933): App\Controllers\View\AttendanceTrackingController->pendingViolationsView()
9 SYSTEMPATH/CodeIgniter.php(507): CodeIgniter\CodeIgniter->runController()
10 SYSTEMPATH/CodeIgniter.php(354): CodeIgniter\CodeIgniter->handleRequest()
11 SYSTEMPATH/Boot.php(363): CodeIgniter\CodeIgniter->run()
12 SYSTEMPATH/Boot.php(68): CodeIgniter\Boot::runCodeIgniter()
13 FCPATH/index.php(60): CodeIgniter\Boot::bootWeb()
14 SYSTEMPATH/rewrite.php(44): require_once('/opt/lampp/htdocs/alrahma_school_sunday/public/index.php')
CRITICAL - 2026-02-10 07:23:30 --> [Caused by] mysqli_sql_exception: Column 'school_year' in where clause is ambiguous
in SYSTEMPATH/Database/MySQLi/Connection.php on line 327.
1 SYSTEMPATH/Database/MySQLi/Connection.php(327): mysqli->query()
2 SYSTEMPATH/Database/BaseConnection.php(729): CodeIgniter\Database\MySQLi\Connection->execute()
3 SYSTEMPATH/Database/BaseConnection.php(646): CodeIgniter\Database\BaseConnection->simpleQuery()
4 SYSTEMPATH/Database/BaseBuilder.php(1649): CodeIgniter\Database\BaseConnection->query()
5 SYSTEMPATH/Model.php(286): CodeIgniter\Database\BaseBuilder->get()
6 SYSTEMPATH/BaseModel.php(677): CodeIgniter\Model->doFindAll()
7 APPPATH/Models/StudentClassModel.php(55): CodeIgniter\BaseModel->findAll()
8 APPPATH/Controllers/View/AttendanceTrackingController.php(70): App\Models\StudentClassModel->findAll()
9 SYSTEMPATH/CodeIgniter.php(933): App\Controllers\View\AttendanceTrackingController->pendingViolationsView()
10 SYSTEMPATH/CodeIgniter.php(507): CodeIgniter\CodeIgniter->runController()
11 SYSTEMPATH/CodeIgniter.php(354): CodeIgniter\CodeIgniter->handleRequest()
12 SYSTEMPATH/Boot.php(363): CodeIgniter\CodeIgniter->run()
13 SYSTEMPATH/Boot.php(68): CodeIgniter\Boot::runCodeIgniter()
14 FCPATH/index.php(60): CodeIgniter\Boot::bootWeb()
15 SYSTEMPATH/rewrite.php(44): require_once('/opt/lampp/htdocs/alrahma_school_sunday/public/index.php')
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772168049;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772167655;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772151598;_ci_previous_url|s:46:"http://localhost:8080/teacher/progress/history";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133519;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:7:"teacher";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772171635;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772170259;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772173263;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772172575;roles|a:1:{i:0;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772057262;_ci_previous_url|s:52:"http://localhost:8080/administrator/daily_attendance";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054013;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772055817;_ci_previous_url|s:46:"http://localhost:8080/parent/report-attendance";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054696;roles|a:1:{i:0;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772168666;_ci_previous_url|s:42:"http://localhost:8080/admin/print-requests";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772167655;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:21;class_section_ids|a:1:{i:0;i:21;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772171325;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772170259;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772057860;_ci_previous_url|s:99:"http://localhost:8080/administrator/daily_attendance/analysis?semester=Spring&school_year=2025-2026";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772057461;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772139133;_ci_previous_url|s:74:"http://localhost:8080/admin/progress?from=&to=&class_section_id=10&status=";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772057153;_ci_previous_url|s:46:"http://localhost:8080/parent/report-attendance";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054696;roles|a:1:{i:0;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772056359;_ci_previous_url|s:52:"http://localhost:8080/administrator/daily_attendance";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054013;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772056691;_ci_previous_url|s:49:"http://localhost:8080/administrator/calendar_view";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054013;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}__ci_vars|a:0:{}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772169271;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772168936;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772173263;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772172575;roles|a:1:{i:0;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772150686;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772138241;_ci_previous_url|s:45:"http://localhost:8080/teacher/progress/submit";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:7:"teacher";class_section_id|N;__ci_vars|a:0:{}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772054613;_ci_previous_url|s:52:"http://localhost:8080/administrator/removed_students";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054013;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}__ci_vars|a:0:{}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772172343;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772170259;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772055981;_ci_previous_url|s:52:"http://localhost:8080/administrator/daily_attendance";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054013;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772054312;_ci_previous_url|s:45:"http://localhost:8080/teacher/progress/submit";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054013;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:7:"teacher";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}__ci_vars|a:0:{}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772169728;_ci_previous_url|s:43:"http://localhost:8080/admin/progress/view/1";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772168936;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772170251;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772151602;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772172244;_ci_previous_url|s:52:"http://localhost:8080/printables_reports/report_card";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772168936;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772168444;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772151602;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772172244;_ci_previous_url|s:52:"http://localhost:8080/printables_reports/report_card";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772168936;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772151137;_ci_previous_url|s:99:"http://localhost:8080/administrator/daily_attendance/analysis?semester=Spring&school_year=2025-2026";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772170606;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772170259;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772167652;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772150314;_ci_previous_url|s:72:"http://localhost:8080/admin/progress?from=&to=&class_section_id=&status=";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772057153;_ci_previous_url|s:46:"http://localhost:8080/parent/report-attendance";user_id|s:1:"2";user_email|s:24:"moulay.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772054696;roles|a:1:{i:0;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|N;__ci_vars|a:0:{}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772138572;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772133038;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";class_section_id|N;
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772170913;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772170259;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772057860;_ci_previous_url|s:52:"http://localhost:8080/administrator/daily_attendance";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772057461;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772172006;_ci_previous_url|s:35:"http://localhost:8080/parent/scores";user_id|s:1:"3";user_email|s:19:"kelabidi0@gmail.com";user_name|s:15:"Khadija Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772170259;roles|a:2:{i:0;s:7:"teacher";i:1;s:6:"parent";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:6:"parent";class_section_id|i:10;class_section_ids|a:1:{i:0;i:10;}
@@ -0,0 +1 @@
__ci_last_regenerate|i:1772171020;_ci_previous_url|s:36:"http://localhost:8080/admin/progress";user_id|s:1:"1";user_email|s:23:"larbi.elabidi@gmail.com";user_name|s:20:"Moulay Larbi Elabidi";user_type|s:7:"primary";is_logged_in|b:1;login_time|i:1772168936;roles|a:20:{i:0;s:5:"admin";i:1;s:13:"administrator";i:2;s:15:"csm_contributor";i:3;s:15:"fes_contributor";i:4;s:21:"financial_contributor";i:5;s:14:"fs_contributor";i:6;s:11:"head of csm";i:7;s:28:"head of department education";i:8;s:10:"head of fa";i:9;s:11:"head of fes";i:10;s:10:"head of fs";i:11;s:12:"head of itpd";i:12;s:16:"itpd contributor";i:13;s:6:"parent";i:14;s:9:"principal";i:15;s:15:"saf_contributor";i:16;s:10:"substitute";i:17;s:7:"teacher";i:18;s:17:"teacher_assistant";i:19;s:14:"vice principal";}semester|s:6:"Spring";school_year|s:9:"2025-2026";role|s:13:"administrator";