This commit is contained in:
@@ -54,6 +54,11 @@ class CompetitionWinnersController extends BaseController
|
||||
public function index()
|
||||
{
|
||||
$competitionModel = new CompetitionModel();
|
||||
$schoolYear = $this->currentCompetitionSchoolYear();
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$competitionModel->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$competitions = $competitionModel
|
||||
->orderBy('id', 'DESC')
|
||||
@@ -62,12 +67,13 @@ class CompetitionWinnersController extends BaseController
|
||||
return view('admin/competition_winners/index', [
|
||||
'competitions' => $competitions,
|
||||
'sectionMap' => $this->getClassSectionMap(),
|
||||
'schoolYear' => $schoolYear,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$schoolYear = $this->configModel->getConfig('school_year');
|
||||
$schoolYear = $this->currentCompetitionSchoolYear();
|
||||
$classCounts = $this->getClassStudentCounts($schoolYear);
|
||||
$classSections = $this->classSectionModel
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
@@ -1088,6 +1094,11 @@ class CompetitionWinnersController extends BaseController
|
||||
return $questionCounts;
|
||||
}
|
||||
|
||||
private function currentCompetitionSchoolYear(): string
|
||||
{
|
||||
return $this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
}
|
||||
|
||||
private function getClassSectionMap(): array
|
||||
{
|
||||
$sections = $this->classSectionModel
|
||||
|
||||
@@ -39,7 +39,13 @@ class AdminProgressController extends BaseController
|
||||
|
||||
public function index()
|
||||
{
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'school_year' => $schoolYear,
|
||||
'from' => (string) $this->request->getGet('from'),
|
||||
'to' => (string) $this->request->getGet('to'),
|
||||
'class_section_id' => (string) $this->request->getGet('class_section_id'),
|
||||
@@ -51,6 +57,8 @@ class AdminProgressController extends BaseController
|
||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left');
|
||||
|
||||
$this->applyProgressSchoolYearScope($builder, $schoolYear);
|
||||
|
||||
if ($filters['from']) {
|
||||
$builder->where('week_start >=', $filters['from']);
|
||||
}
|
||||
@@ -91,16 +99,17 @@ class AdminProgressController extends BaseController
|
||||
$reportGroupsBySection[$sectionId] = $groups;
|
||||
}
|
||||
|
||||
$classSections = $this->classSectionModel->getClassSections();
|
||||
$studentCounts = $this->studentClassModel->getStudentCountsBySection();
|
||||
$filteredSections = array_values(array_filter($classSections, function ($section) use ($studentCounts) {
|
||||
$classSections = $this->classSectionsForYear($schoolYear, $rows);
|
||||
$studentCounts = $this->studentClassModel->getStudentCountsBySection($schoolYear !== '' ? $schoolYear : null);
|
||||
$filteredSections = array_values(array_filter($classSections, function ($section) use ($studentCounts, $reportGroupsBySection) {
|
||||
$sectionId = (int) ($section['class_section_id'] ?? 0);
|
||||
return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0;
|
||||
return (isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0)
|
||||
|| isset($reportGroupsBySection[$sectionId]);
|
||||
}));
|
||||
|
||||
$filterStart = $this->normalizeDate($filters['from'] ?? '');
|
||||
$filterEnd = $this->normalizeDate($filters['to'] ?? '');
|
||||
[$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates();
|
||||
[$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates($schoolYear);
|
||||
[$expectedDays, $activeDatesSet] = $this->resolveExpectedDays(
|
||||
$dateList,
|
||||
$noSchoolDays,
|
||||
@@ -132,6 +141,8 @@ class AdminProgressController extends BaseController
|
||||
return view('admin/class_progress_list', [
|
||||
'reportGroupsBySection' => $reportGroupsBySection,
|
||||
'filters' => $filters,
|
||||
'schoolYear' => $schoolYear,
|
||||
'schoolYears' => $this->availableSchoolYears($schoolYear),
|
||||
'classSections' => $filteredSections,
|
||||
'statusOptions' => ClassProgressController::STATUS_OPTIONS,
|
||||
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
|
||||
@@ -142,6 +153,162 @@ class AdminProgressController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
private function classSectionsForYear(string $schoolYear, array $reportRows = []): array
|
||||
{
|
||||
$db = db_connect();
|
||||
|
||||
if ($schoolYear !== '' && $db->fieldExists('school_year', 'classSection')) {
|
||||
$sections = $db->table('classSection')
|
||||
->select('classSection.class_section_id, classSection.class_section_name, classes.class_name')
|
||||
->join('classes', 'classSection.class_id = classes.id', 'left')
|
||||
->where('classSection.school_year', $schoolYear)
|
||||
->orderBy('classSection.class_section_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
} else {
|
||||
$sections = $this->classSectionModel->getClassSections();
|
||||
}
|
||||
|
||||
$sectionsById = [];
|
||||
foreach ($sections as $section) {
|
||||
$sectionId = (int) ($section['class_section_id'] ?? 0);
|
||||
if ($sectionId > 0) {
|
||||
$sectionsById[$sectionId] = $section;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($reportRows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
if ($sectionId <= 0 || isset($sectionsById[$sectionId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sectionsById[$sectionId] = [
|
||||
'class_section_id' => $sectionId,
|
||||
'class_section_name' => (string) ($row['class_section_name'] ?? ('Section #' . $sectionId)),
|
||||
'class_name' => '',
|
||||
];
|
||||
}
|
||||
|
||||
uasort($sectionsById, static function (array $a, array $b): int {
|
||||
return strnatcasecmp((string) ($a['class_section_name'] ?? ''), (string) ($b['class_section_name'] ?? ''));
|
||||
});
|
||||
|
||||
return array_values($sectionsById);
|
||||
}
|
||||
|
||||
private function applyProgressSchoolYearScope($builder, string $schoolYear): void
|
||||
{
|
||||
if ($schoolYear === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$db = db_connect();
|
||||
$hasReportYear = $db->fieldExists('school_year', 'class_progress_reports');
|
||||
$hasSectionYear = $db->fieldExists('school_year', 'classSection');
|
||||
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
|
||||
|
||||
if ($hasReportYear) {
|
||||
if ($this->hasAnyExplicitProgressSchoolYearRows()) {
|
||||
$builder->where('class_progress_reports.school_year', $schoolYear);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($rangeStart !== '' && $rangeEnd !== '') {
|
||||
$builder
|
||||
->groupStart()
|
||||
->groupStart()
|
||||
->where('class_progress_reports.school_year IS NULL', null, false)
|
||||
->orWhere('class_progress_reports.school_year', '')
|
||||
->groupEnd()
|
||||
->where('class_progress_reports.week_start >=', $rangeStart)
|
||||
->where('class_progress_reports.week_start <=', $rangeEnd)
|
||||
->groupEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
$builder->where('class_progress_reports.school_year', $schoolYear);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($hasSectionYear) {
|
||||
$builder->where('cs.school_year', $schoolYear);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($rangeStart !== '' && $rangeEnd !== '') {
|
||||
$builder
|
||||
->where('class_progress_reports.week_start >=', $rangeStart)
|
||||
->where('class_progress_reports.week_start <=', $rangeEnd);
|
||||
}
|
||||
}
|
||||
|
||||
private function hasAnyExplicitProgressSchoolYearRows(): bool
|
||||
{
|
||||
try {
|
||||
return db_connect()
|
||||
->table('class_progress_reports')
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->where('school_year !=', '')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray() !== null;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to check explicit progress school-year rows: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function availableSchoolYears(string $selectedYear): array
|
||||
{
|
||||
$years = [];
|
||||
$addYear = static function (mixed $value) use (&$years): void {
|
||||
$year = trim((string) $value);
|
||||
if ($year !== '' && ! in_array($year, $years, true)) {
|
||||
$years[] = $year;
|
||||
}
|
||||
};
|
||||
|
||||
$addYear($selectedYear);
|
||||
$addYear($this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? '')));
|
||||
|
||||
$db = db_connect();
|
||||
foreach ([
|
||||
['table' => 'school_years', 'column' => 'name'],
|
||||
['table' => 'class_progress_reports', 'column' => 'school_year'],
|
||||
['table' => 'classSection', 'column' => 'school_year'],
|
||||
['table' => 'student_class', 'column' => 'school_year'],
|
||||
] as $source) {
|
||||
try {
|
||||
if (! $db->tableExists($source['table']) || ! $db->fieldExists($source['column'], $source['table'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows = $db->table($source['table'])
|
||||
->select($source['column'])
|
||||
->distinct()
|
||||
->where($source['column'] . ' IS NOT NULL', null, false)
|
||||
->orderBy($source['column'], 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$addYear($row[$source['column']] ?? '');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'Unable to load school-year options for admin progress: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
rsort($years, SORT_NATURAL);
|
||||
|
||||
return $years;
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
{
|
||||
$row = $this->reportModel
|
||||
@@ -277,11 +444,15 @@ class AdminProgressController extends BaseController
|
||||
return checkdate($m, $d, $y) ? $value : '';
|
||||
}
|
||||
|
||||
protected function buildSemesterDates(): array
|
||||
protected function buildSemesterDates(?string $schoolYear = null): array
|
||||
{
|
||||
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
$schoolYear = trim((string) ($schoolYear ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
}
|
||||
|
||||
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : $this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange);
|
||||
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
|
||||
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
|
||||
|
||||
@@ -12,9 +12,12 @@ class SchoolYearClosingController extends BaseController
|
||||
{
|
||||
try {
|
||||
$targetId = $this->normalizeInt($this->request->getGet('target_school_year_id'));
|
||||
$preview = service('schoolYearClosing')->preview($id, $targetId);
|
||||
$promotionTable = $this->promotionTablePayload($preview['promotion']['rows'] ?? []);
|
||||
|
||||
return view('school_years/closing_preview', [
|
||||
'preview' => service('schoolYearClosing')->preview($id, $targetId),
|
||||
'preview' => $preview,
|
||||
'promotionTable' => $promotionTable,
|
||||
'latestBatch' => service('schoolYearClosing')->latestBatch($id),
|
||||
'schoolYears' => (new SchoolYearModel())->orderBy('name', 'DESC')->findAll(),
|
||||
]);
|
||||
@@ -32,7 +35,7 @@ class SchoolYearClosingController extends BaseController
|
||||
}
|
||||
|
||||
service('schoolYearClosing')->start($id, $targetId, $this->userId());
|
||||
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'Closing started.');
|
||||
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'End-year process started.');
|
||||
} catch (Throwable $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
@@ -42,7 +45,7 @@ class SchoolYearClosingController extends BaseController
|
||||
{
|
||||
try {
|
||||
service('schoolYearClosing')->execute($id, $this->userId());
|
||||
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'Carry-forward batch executed.');
|
||||
return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'Carry-forward confirmed.');
|
||||
} catch (Throwable $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
@@ -52,7 +55,7 @@ class SchoolYearClosingController extends BaseController
|
||||
{
|
||||
try {
|
||||
service('schoolYearClosing')->complete($id, $this->userId());
|
||||
return redirect()->to('/administrator/school-years')->with('success', 'School year closed.');
|
||||
return redirect()->to('/administrator/school-years')->with('success', 'School year ended and closed.');
|
||||
} catch (Throwable $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
@@ -62,7 +65,7 @@ class SchoolYearClosingController extends BaseController
|
||||
{
|
||||
try {
|
||||
service('schoolYearClosing')->cancel($id, $this->userId());
|
||||
return redirect()->to('/administrator/school-years')->with('success', 'Closing cancelled.');
|
||||
return redirect()->to('/administrator/school-years')->with('success', 'End-year process cancelled.');
|
||||
} catch (Throwable $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
@@ -73,6 +76,89 @@ class SchoolYearClosingController extends BaseController
|
||||
return is_numeric($value) && (int) $value > 0 ? (int) $value : null;
|
||||
}
|
||||
|
||||
private function promotionTablePayload(array $rows): array
|
||||
{
|
||||
$allowedSorts = ['student', 'school_id', 'class', 'year_score', 'decision', 'source', 'queue', 'target', 'status'];
|
||||
$sort = (string) ($this->request->getGet('sort') ?? 'class');
|
||||
$sort = in_array($sort, $allowedSorts, true) ? $sort : 'class';
|
||||
$order = strtolower((string) ($this->request->getGet('order') ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
||||
$page = max(1, (int) ($this->request->getGet('page') ?? 1));
|
||||
$perPage = (int) ($this->request->getGet('per_page') ?? 25);
|
||||
$allowedPerPage = [10, 25, 50, 100];
|
||||
$perPage = in_array($perPage, $allowedPerPage, true) ? $perPage : 25;
|
||||
|
||||
usort($rows, function (array $a, array $b) use ($sort, $order): int {
|
||||
$comparison = $this->comparePromotionRows($a, $b, $sort);
|
||||
if ($comparison === 0 && $sort !== 'class') {
|
||||
$comparison = $this->comparePromotionRows($a, $b, 'class');
|
||||
}
|
||||
if ($comparison === 0 && $sort !== 'student') {
|
||||
$comparison = $this->comparePromotionRows($a, $b, 'student');
|
||||
}
|
||||
if ($comparison === 0) {
|
||||
$comparison = ((int) ($a['student_id'] ?? 0)) <=> ((int) ($b['student_id'] ?? 0));
|
||||
}
|
||||
|
||||
return $order === 'desc' ? -$comparison : $comparison;
|
||||
});
|
||||
|
||||
$total = count($rows);
|
||||
$pageCount = max(1, (int) ceil($total / $perPage));
|
||||
$page = min($page, $pageCount);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
return [
|
||||
'rows' => array_slice($rows, $offset, $perPage),
|
||||
'sort' => $sort,
|
||||
'order' => $order,
|
||||
'page' => $page,
|
||||
'perPage' => $perPage,
|
||||
'total' => $total,
|
||||
'pageCount' => $pageCount,
|
||||
'allowedPerPage' => $allowedPerPage,
|
||||
'from' => $total === 0 ? 0 : $offset + 1,
|
||||
'to' => min($offset + $perPage, $total),
|
||||
];
|
||||
}
|
||||
|
||||
private function comparePromotionRows(array $a, array $b, string $sort): int
|
||||
{
|
||||
$aValue = $this->promotionSortValue($a, $sort);
|
||||
$bValue = $this->promotionSortValue($b, $sort);
|
||||
|
||||
if ($sort === 'year_score') {
|
||||
if ($aValue === null && $bValue === null) {
|
||||
return 0;
|
||||
}
|
||||
if ($aValue === null) {
|
||||
return 1;
|
||||
}
|
||||
if ($bValue === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $aValue <=> $bValue;
|
||||
}
|
||||
|
||||
return strnatcasecmp((string) $aValue, (string) $bValue);
|
||||
}
|
||||
|
||||
private function promotionSortValue(array $row, string $sort): mixed
|
||||
{
|
||||
return match ($sort) {
|
||||
'student' => (string) ($row['student_name'] ?? ''),
|
||||
'school_id' => (string) ($row['school_id'] ?? ''),
|
||||
'class' => (string) ($row['class_section_name'] ?? ''),
|
||||
'year_score' => is_numeric($row['year_score'] ?? null) ? (float) $row['year_score'] : null,
|
||||
'decision' => (string) ($row['decision'] ?? ''),
|
||||
'source' => (string) ($row['source'] ?? ''),
|
||||
'queue' => (string) ($row['queue_status'] ?? ''),
|
||||
'target' => trim((string) ($row['target_section'] ?? '') . ' ' . (string) ($row['target_class'] ?? '')),
|
||||
'status' => (string) ($row['status'] ?? ''),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private function userId(): ?int
|
||||
{
|
||||
$id = session('user_id') ?? session('id');
|
||||
|
||||
@@ -51,6 +51,8 @@ class SchoolYearController extends BaseController
|
||||
'closingYear' => $closingYear,
|
||||
'archivedCount' => $archivedCount,
|
||||
'latestTransitions' => service('schoolYearManagement')->latestTransitionByYear(),
|
||||
'yearVerification' => $this->verificationByYear($schoolYears),
|
||||
'schoolYearNamesById' => array_column($schoolYears, 'name', 'id'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -124,4 +126,91 @@ class SchoolYearController extends BaseController
|
||||
|
||||
return is_numeric($id) ? (int) $id : null;
|
||||
}
|
||||
|
||||
private function verificationByYear(array $schoolYears): array
|
||||
{
|
||||
$db = db_connect();
|
||||
$verification = [];
|
||||
|
||||
$hasStudentClass = $db->tableExists('student_class');
|
||||
$hasStudentDecisions = $db->tableExists('student_decisions');
|
||||
$hasPromotionQueue = $db->tableExists('promotion_queue');
|
||||
$hasInvoices = $db->tableExists('invoices');
|
||||
$hasClosingBatches = $db->tableExists('school_year_closing_batches');
|
||||
|
||||
foreach ($schoolYears as $year) {
|
||||
$id = (int) ($year['id'] ?? 0);
|
||||
$name = (string) ($year['name'] ?? '');
|
||||
if ($id <= 0 || $name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = [
|
||||
'students' => 0,
|
||||
'decisions' => 0,
|
||||
'promoted' => 0,
|
||||
'queued' => 0,
|
||||
'carry_forward_balance' => 0.0,
|
||||
'positive_balance' => 0.0,
|
||||
'credit_balance' => 0.0,
|
||||
'closing_status' => '',
|
||||
];
|
||||
|
||||
if ($hasStudentClass && $db->fieldExists('school_year', 'student_class')) {
|
||||
$row = $db->table('student_class')
|
||||
->select('COUNT(DISTINCT student_id) AS total', false)
|
||||
->where('school_year', $name)
|
||||
->get()
|
||||
->getRowArray();
|
||||
$summary['students'] = (int) ($row['total'] ?? 0);
|
||||
}
|
||||
|
||||
if ($hasStudentDecisions && $db->fieldExists('school_year', 'student_decisions')) {
|
||||
$row = $db->table('student_decisions')
|
||||
->select('COUNT(DISTINCT student_id) AS decisions', false)
|
||||
->select("COUNT(DISTINCT CASE WHEN LOWER(decision) = 'pass' THEN student_id END) AS promoted", false)
|
||||
->where('school_year', $name)
|
||||
->get()
|
||||
->getRowArray();
|
||||
$summary['decisions'] = (int) ($row['decisions'] ?? 0);
|
||||
$summary['promoted'] = (int) ($row['promoted'] ?? 0);
|
||||
}
|
||||
|
||||
if ($hasPromotionQueue && $db->fieldExists('school_year_from', 'promotion_queue')) {
|
||||
$row = $db->table('promotion_queue')
|
||||
->select('COUNT(DISTINCT student_id) AS total', false)
|
||||
->where('school_year_from', $name)
|
||||
->get()
|
||||
->getRowArray();
|
||||
$summary['queued'] = (int) ($row['total'] ?? 0);
|
||||
}
|
||||
|
||||
if ($hasInvoices && $db->fieldExists('school_year', 'invoices')) {
|
||||
$row = $db->table('invoices')
|
||||
->select('COALESCE(SUM(balance), 0) AS carry_forward_balance')
|
||||
->select('COALESCE(SUM(CASE WHEN balance > 0 THEN balance ELSE 0 END), 0) AS positive_balance', false)
|
||||
->select('COALESCE(SUM(CASE WHEN balance < 0 THEN ABS(balance) ELSE 0 END), 0) AS credit_balance', false)
|
||||
->where('school_year', $name)
|
||||
->get()
|
||||
->getRowArray();
|
||||
$summary['carry_forward_balance'] = round((float) ($row['carry_forward_balance'] ?? 0), 2);
|
||||
$summary['positive_balance'] = round((float) ($row['positive_balance'] ?? 0), 2);
|
||||
$summary['credit_balance'] = round((float) ($row['credit_balance'] ?? 0), 2);
|
||||
}
|
||||
|
||||
if ($hasClosingBatches) {
|
||||
$batch = $db->table('school_year_closing_batches')
|
||||
->select('status')
|
||||
->where('source_school_year_id', $id)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
$summary['closing_status'] = (string) ($batch['status'] ?? '');
|
||||
}
|
||||
|
||||
$verification[$id] = $summary;
|
||||
}
|
||||
|
||||
return $verification;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use App\Services\ApiClient;
|
||||
use App\Exceptions\SchoolYear\SchoolYearConfigurationException;
|
||||
use App\Support\SchoolYear\SchoolYearContext;
|
||||
use Throwable;
|
||||
|
||||
@@ -60,6 +61,8 @@ abstract class BaseController extends Controller
|
||||
// Preload any models, libraries, etc, here.
|
||||
// E.g.: $this->session = \Config\Services::session();
|
||||
session();
|
||||
$this->syncSchoolYearPropertyFromContext();
|
||||
$this->injectSchoolYearViewData();
|
||||
|
||||
// Shared API client available to all controllers
|
||||
$this->api = service('apiClient');
|
||||
@@ -104,6 +107,23 @@ abstract class BaseController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
protected function currentSchoolYearName(?string $fallback = null): string
|
||||
{
|
||||
try {
|
||||
return $this->resolveSchoolYearContext()->yearName();
|
||||
} catch (Throwable $e) {
|
||||
if ($fallback !== null && $fallback !== '') {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return (string) ((new \App\Models\ConfigurationModel())->getConfig('school_year') ?? '');
|
||||
} catch (Throwable) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertSchoolYearWritable(
|
||||
SchoolYearContext $context,
|
||||
bool $allowDraftForAdmin = false,
|
||||
@@ -111,6 +131,85 @@ abstract class BaseController extends Controller
|
||||
): void {
|
||||
service('schoolYearWriteGuard')->assertWritable($context, $allowDraftForAdmin, $isAdmin);
|
||||
}
|
||||
|
||||
private function syncSchoolYearPropertyFromContext(): void
|
||||
{
|
||||
if (! property_exists($this, 'schoolYear')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->request instanceof IncomingRequest || is_cli()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! session()->get('user_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
} catch (Throwable $e) {
|
||||
log_message('warning', 'Unable to sync controller school-year property: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function injectSchoolYearViewData(): void
|
||||
{
|
||||
if (! config('Feature')->globalSchoolYearSelector) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->request instanceof IncomingRequest || is_cli()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! session()->get('user_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->shouldShowSchoolYearSelector()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accept = strtolower($this->request->getHeaderLine('Accept'));
|
||||
if ($this->request->isAJAX() || str_contains($accept, 'application/json')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
service('renderer')->setData(service('schoolYearViewData')->forCurrentRequest($this->request));
|
||||
} catch (SchoolYearConfigurationException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
log_message('warning', 'Unable to inject school-year view data: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldShowSchoolYearSelector(): bool
|
||||
{
|
||||
$roles = (array) session()->get('roles');
|
||||
$singleRole = session()->get('role');
|
||||
if ($singleRole !== null && $singleRole !== '') {
|
||||
$roles[] = $singleRole;
|
||||
}
|
||||
|
||||
$roles = array_map(
|
||||
static fn ($role): string => strtolower(trim((string) $role)),
|
||||
$roles
|
||||
);
|
||||
|
||||
return (bool) array_intersect($roles, [
|
||||
'admin',
|
||||
'administrator',
|
||||
'administrative staff',
|
||||
'principal',
|
||||
'vice principal',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -31,7 +31,7 @@ class ParentReportCardController extends BaseController
|
||||
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') ?? ''));
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->currentSchoolYearName()));
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
|
||||
|
||||
$builder = $this->db->table('students s')
|
||||
@@ -82,7 +82,7 @@ class ParentReportCardController extends BaseController
|
||||
throw new PageNotFoundException('Student not found.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->configModel->getConfig('school_year') ?? ''));
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->currentSchoolYearName()));
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
|
||||
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
@@ -121,7 +121,7 @@ class ParentReportCardController extends BaseController
|
||||
return redirect()->back()->with('error', 'Please type your full name to sign.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
$schoolYear = trim((string) $this->currentSchoolYearName());
|
||||
$semester = trim((string) ($this->configModel->getConfig('semester') ?? ''));
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
|
||||
@@ -699,7 +699,10 @@ class AdministratorController extends BaseController
|
||||
public function teacherSubmissionsReport()
|
||||
{
|
||||
$semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? '');
|
||||
$schoolYear = (string)($this->configModel->getConfig('school_year') ?? $this->schoolYear ?? '');
|
||||
$schoolYear = trim((string) ($this->request->getGet('school_year') ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
}
|
||||
$semesterResolver = new SemesterRangeService($this->configModel);
|
||||
$semesterNorm = $semesterResolver->normalizeSemester($semester);
|
||||
$semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester;
|
||||
@@ -1099,9 +1102,9 @@ class AdministratorController extends BaseController
|
||||
}
|
||||
|
||||
$semesterResolver = new SemesterRangeService($this->configModel);
|
||||
$schoolYear = (string)($this->configModel->getConfig('school_year') ?? '');
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
$semester = (string)($this->configModel->getConfig('semester') ?? '');
|
||||
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string)($this->configModel->getConfig('school_year') ?? '');
|
||||
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
[$rangeStart, $rangeEnd] = $semesterResolver->getSchoolYearRange($schoolYearForRange);
|
||||
$semesterNorm = $semesterResolver->normalizeSemester($semester);
|
||||
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
|
||||
@@ -2295,22 +2298,10 @@ class AdministratorController extends BaseController
|
||||
public function showEnrollmentWithdrawalPage()
|
||||
{
|
||||
try {
|
||||
$selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
|
||||
if ($selectedYear === '') {
|
||||
$selectedYear = (string)$this->schoolYear;
|
||||
}
|
||||
$schoolYears = $this->availableSchoolYears();
|
||||
$selectedYear = $this->selectedEnrollmentSchoolYear($schoolYears);
|
||||
|
||||
// Distinct school years from enrollments (for selector)
|
||||
$yearsRows = $this->db->table('enrollments')
|
||||
->distinct()
|
||||
->select('school_year')
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()->getResultArray();
|
||||
$schoolYears = array_values(array_filter(array_map(static function ($r) {
|
||||
return isset($r['school_year']) ? (string)$r['school_year'] : null;
|
||||
}, $yearsRows)));
|
||||
|
||||
$students = $this->studentModel->getStudentsWithClassAndEnrollment();
|
||||
$students = $this->studentModel->getStudentsWithClassAndEnrollment($selectedYear);
|
||||
|
||||
$selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear);
|
||||
$removedPriorIds = [];
|
||||
@@ -2326,6 +2317,7 @@ class AdministratorController extends BaseController
|
||||
}
|
||||
}
|
||||
}
|
||||
$returningStudentIds = $this->priorYearStudentIds($selectedYear);
|
||||
|
||||
foreach ($students as &$s) {
|
||||
// ===== Ensure IDs needed by the modal =====
|
||||
@@ -2360,6 +2352,9 @@ class AdministratorController extends BaseController
|
||||
|
||||
// ===== New-student flags =====
|
||||
$s['is_new'] = (int) ($s['is_new'] ?? 0);
|
||||
if (isset($returningStudentIds[$s['student_id']])) {
|
||||
$s['is_new'] = 0;
|
||||
}
|
||||
$s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';
|
||||
|
||||
// ===== Admission override =====
|
||||
@@ -2369,6 +2364,9 @@ class AdministratorController extends BaseController
|
||||
$s['enrollment_status'] = $statusForYear;
|
||||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||||
$s['enrollment_status'] = 'denied';
|
||||
} else {
|
||||
$s['enrollment_status'] = 'admission under review';
|
||||
$s['admission_status'] = 'pending';
|
||||
}
|
||||
|
||||
// ===== Class section name for the selected year =====
|
||||
@@ -2396,22 +2394,7 @@ class AdministratorController extends BaseController
|
||||
return strcasecmp($pa, $pb);
|
||||
});
|
||||
|
||||
// ===== Provide classes for the modal select (like studentClassAssignment) =====
|
||||
// Filter to current term; loosen if your table doesn’t store these fields.
|
||||
$classes = $this->classSectionModel
|
||||
->select('id, class_section_id, class_section_name, school_year, semester')
|
||||
->where('school_year', (string)$selectedYear)
|
||||
->where('semester', (string)$this->semester)
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// Fallback so the dropdown never shows empty if filters are too strict
|
||||
if (empty($classes)) {
|
||||
$classes = $this->classSectionModel
|
||||
->select('id, class_section_id, class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
$classes = $this->enrollmentClassOptions((string)$selectedYear);
|
||||
|
||||
return view('enroll_withdraw/enrollment_withdrawal', [
|
||||
'students' => $students,
|
||||
@@ -2432,22 +2415,10 @@ class AdministratorController extends BaseController
|
||||
public function enrollmentWithdrawalData()
|
||||
{
|
||||
try {
|
||||
$selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
|
||||
if ($selectedYear === '') {
|
||||
$selectedYear = (string)$this->schoolYear;
|
||||
}
|
||||
$schoolYears = $this->availableSchoolYears();
|
||||
$selectedYear = $this->selectedEnrollmentSchoolYear($schoolYears);
|
||||
|
||||
// Distinct school years
|
||||
$yearsRows = $this->db->table('enrollments')
|
||||
->distinct()
|
||||
->select('school_year')
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()->getResultArray();
|
||||
$schoolYears = array_values(array_filter(array_map(static function ($r) {
|
||||
return isset($r['school_year']) ? (string)$r['school_year'] : null;
|
||||
}, $yearsRows)));
|
||||
|
||||
$students = $this->studentModel->getStudentsWithClassAndEnrollment();
|
||||
$students = $this->studentModel->getStudentsWithClassAndEnrollment($selectedYear);
|
||||
|
||||
$selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear);
|
||||
$removedPriorIds = [];
|
||||
@@ -2463,6 +2434,7 @@ class AdministratorController extends BaseController
|
||||
}
|
||||
}
|
||||
}
|
||||
$returningStudentIds = $this->priorYearStudentIds($selectedYear);
|
||||
|
||||
foreach ($students as &$s) {
|
||||
$s['student_id'] = (int)($s['id'] ?? 0);
|
||||
@@ -2485,6 +2457,9 @@ class AdministratorController extends BaseController
|
||||
$s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf) ?: 'ZZZ Unknown Parent';
|
||||
|
||||
$s['is_new'] = (int) ($s['is_new'] ?? 0);
|
||||
if (isset($returningStudentIds[$s['student_id']])) {
|
||||
$s['is_new'] = 0;
|
||||
}
|
||||
$s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';
|
||||
|
||||
$statusForYear = $this->enrollmentModel->getEnrollmentStatus((int)$s['student_id'], $selectedYear);
|
||||
@@ -2492,6 +2467,9 @@ class AdministratorController extends BaseController
|
||||
$s['enrollment_status'] = $statusForYear;
|
||||
} elseif (($s['admission_status'] ?? null) === 'denied') {
|
||||
$s['enrollment_status'] = 'denied';
|
||||
} else {
|
||||
$s['enrollment_status'] = 'admission under review';
|
||||
$s['admission_status'] = 'pending';
|
||||
}
|
||||
|
||||
$className = $this->studentClassModel->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear);
|
||||
@@ -2516,18 +2494,7 @@ class AdministratorController extends BaseController
|
||||
return strcasecmp($pa, $pb);
|
||||
});
|
||||
|
||||
$classes = $this->classSectionModel
|
||||
->select('id, class_section_id, class_section_name, school_year, semester')
|
||||
->where('school_year', (string)$selectedYear)
|
||||
->where('semester', (string)$this->semester)
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
if (empty($classes)) {
|
||||
$classes = $this->classSectionModel
|
||||
->select('id, class_section_id, class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
$classes = $this->enrollmentClassOptions((string)$selectedYear);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'students' => $students,
|
||||
@@ -2543,6 +2510,138 @@ class AdministratorController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
private function availableSchoolYears(): array
|
||||
{
|
||||
$years = [];
|
||||
$addYear = static function (mixed $value) use (&$years): void {
|
||||
$year = trim((string) $value);
|
||||
if ($year !== '' && !in_array($year, $years, true)) {
|
||||
$years[] = $year;
|
||||
}
|
||||
};
|
||||
|
||||
$addYear($this->schoolYear ?? null);
|
||||
|
||||
if ($this->db->tableExists('school_years')) {
|
||||
$rows = $this->db->table('school_years')
|
||||
->select('name')
|
||||
->orderBy('name', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$addYear($row['name'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('enrollments')) {
|
||||
$rows = $this->db->table('enrollments')
|
||||
->distinct()
|
||||
->select('school_year')
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$addYear($row['school_year'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
usort($years, static fn(string $a, string $b): int => strnatcasecmp($b, $a));
|
||||
|
||||
$activeYear = trim((string) ($this->schoolYear ?? ''));
|
||||
if ($activeYear !== '') {
|
||||
$years = array_values(array_filter($years, static fn(string $year): bool => $year !== $activeYear));
|
||||
array_unshift($years, $activeYear);
|
||||
}
|
||||
|
||||
return $years;
|
||||
}
|
||||
|
||||
private function selectedEnrollmentSchoolYear(array $schoolYears): string
|
||||
{
|
||||
$requestedYear = trim((string) ($this->request->getGet('schoolYear') ?? ''));
|
||||
if ($requestedYear !== '' && in_array($requestedYear, $schoolYears, true)) {
|
||||
return $requestedYear;
|
||||
}
|
||||
|
||||
$activeYear = trim((string) ($this->schoolYear ?? ''));
|
||||
if ($activeYear !== '') {
|
||||
return $activeYear;
|
||||
}
|
||||
|
||||
return (string) ($schoolYears[0] ?? '');
|
||||
}
|
||||
|
||||
private function enrollmentClassOptions(string $selectedYear): array
|
||||
{
|
||||
$select = ['id', 'class_section_id', 'class_section_name'];
|
||||
$hasSchoolYear = $this->db->fieldExists('school_year', 'classSection');
|
||||
$hasSemester = $this->db->fieldExists('semester', 'classSection');
|
||||
|
||||
if ($hasSchoolYear) {
|
||||
$select[] = 'school_year';
|
||||
}
|
||||
if ($hasSemester) {
|
||||
$select[] = 'semester';
|
||||
}
|
||||
|
||||
$query = $this->classSectionModel
|
||||
->select(implode(', ', $select))
|
||||
->orderBy('class_section_name', 'ASC');
|
||||
|
||||
if ($hasSchoolYear && $selectedYear !== '') {
|
||||
$query->where('school_year', $selectedYear);
|
||||
}
|
||||
if ($hasSemester) {
|
||||
$query->where('semester', (string)$this->semester);
|
||||
}
|
||||
|
||||
$classes = $query->findAll();
|
||||
|
||||
if (! empty($classes) || (! $hasSchoolYear && ! $hasSemester)) {
|
||||
return $classes;
|
||||
}
|
||||
|
||||
return $this->classSectionModel
|
||||
->select('id, class_section_id, class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
private function priorYearStudentIds(string $selectedYear): array
|
||||
{
|
||||
$selectedStartYear = $this->getSchoolYearStartYear($selectedYear);
|
||||
if ($selectedStartYear === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$studentIds = [];
|
||||
foreach (['enrollments', 'student_class'] as $table) {
|
||||
if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows = $this->db->table($table)
|
||||
->select('student_id, school_year')
|
||||
->where('student_id IS NOT NULL', null, false)
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$rowStartYear = $this->getSchoolYearStartYear((string) ($row['school_year'] ?? ''));
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
if ($studentId > 0 && $rowStartYear !== null && $rowStartYear < $selectedStartYear) {
|
||||
$studentIds[$studentId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $studentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show only newly registered students, with contact modal support.
|
||||
*
|
||||
|
||||
@@ -46,7 +46,7 @@ class AssignmentController extends BaseController
|
||||
|
||||
// Apply school year filter (default to current config) but avoid semester filtering so the full year is visible
|
||||
$selectedSemester = (string)($this->request->getGet('semester') ?? $this->semester ?? '');
|
||||
$year = (string)($this->request->getGet('school_year') ?? $this->schoolYear ?? '');
|
||||
$year = (string)($this->schoolYear ?? '');
|
||||
|
||||
$tcQ = $this->teacherClassModel;
|
||||
if ($year !== '') {
|
||||
@@ -172,11 +172,11 @@ class AssignmentController extends BaseController
|
||||
}
|
||||
|
||||
$schoolYearsList = [];
|
||||
try {
|
||||
$db = Database::connect();
|
||||
$yearsQuery = $db->table('teacher_class')
|
||||
->select('DISTINCT school_year', false)
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
try {
|
||||
$db = Database::connect();
|
||||
$yearsQuery = $db->table('teacher_class')
|
||||
->select('DISTINCT school_year', false)
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
@@ -189,8 +189,8 @@ class AssignmentController extends BaseController
|
||||
} catch (\Throwable $e) {
|
||||
// ignore fallback below
|
||||
}
|
||||
if (empty($schoolYearsList) && $this->schoolYear !== null && $this->schoolYear !== '') {
|
||||
$schoolYearsList[] = (string)$this->schoolYear;
|
||||
if ($this->schoolYear !== null && $this->schoolYear !== '' && !in_array((string)$this->schoolYear, $schoolYearsList, true)) {
|
||||
array_unshift($schoolYearsList, (string)$this->schoolYear);
|
||||
}
|
||||
|
||||
// Sort sections
|
||||
|
||||
@@ -64,11 +64,20 @@ class AttendanceController extends Controller
|
||||
$this->userRoleModel = new UserRoleModel();
|
||||
$this->semesterScoreService = service('semesterScoreService');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->schoolYear = $this->currentSchoolYearName();
|
||||
$this->enableAttendance = $this->configModel->getConfig('enable_attendance');
|
||||
$this->semesterRangeService = new SemesterRangeService($this->configModel);
|
||||
}
|
||||
|
||||
private function currentSchoolYearName(): string
|
||||
{
|
||||
try {
|
||||
return service('schoolYearContext')->resolve(service('request'))->yearName();
|
||||
} catch (\Throwable $e) {
|
||||
return (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET: filter + grid for a single date/section.
|
||||
|
||||
@@ -70,28 +70,6 @@ class AttendanceTrackingController extends BaseController
|
||||
->findAll();
|
||||
$debugInfo['class_students'] = count($classStudents);
|
||||
|
||||
// Fallback: if the configured school_year has no class assignments, look up the latest term
|
||||
if (empty($classStudents)) {
|
||||
$latestTerm = $this->db->table('student_class')
|
||||
->select('school_year, semester')
|
||||
->orderBy('id', 'DESC')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($latestTerm && !empty($latestTerm['school_year'])) {
|
||||
$schoolYear = (string)$latestTerm['school_year'];
|
||||
if (!empty($latestTerm['semester'])) {
|
||||
$semester = (string)$latestTerm['semester'];
|
||||
}
|
||||
|
||||
$classStudents = $this->studentClassModel
|
||||
->active()
|
||||
->where('student_class.school_year', $schoolYear)
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Gather candidate student identifiers (numeric IDs and code-like IDs)
|
||||
$studentIds = [];
|
||||
$studentCodes = [];
|
||||
|
||||
@@ -62,7 +62,7 @@ class EventController extends ResourceController
|
||||
$this->parentModel = new ParentModel();
|
||||
$this->emailService = new EmailService();
|
||||
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->schoolYear = $this->currentSchoolYearName();
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->categories = [
|
||||
'workshops',
|
||||
@@ -88,6 +88,15 @@ class EventController extends ResourceController
|
||||
return $this->eventChargesHasCreatedBy;
|
||||
}
|
||||
|
||||
private function currentSchoolYearName(): string
|
||||
{
|
||||
try {
|
||||
return service('schoolYearContext')->resolve(service('request'))->yearName();
|
||||
} catch (\Throwable $e) {
|
||||
return (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
private function eventChargesSupportsWaiverSigned(): bool
|
||||
{
|
||||
if ($this->eventChargesHasWaiverSigned !== null) {
|
||||
|
||||
@@ -20,6 +20,60 @@ class FinancialController extends BaseController
|
||||
$accept = strtolower((string)($this->request->getHeaderLine('Accept') ?? ''));
|
||||
return $this->request->isAJAX() || str_contains($accept, 'application/json');
|
||||
}
|
||||
|
||||
private function selectedSchoolYear(): string
|
||||
{
|
||||
$requested = trim((string) ($this->request->getGet('school_year') ?? ''));
|
||||
if ($requested !== '') {
|
||||
return $requested;
|
||||
}
|
||||
|
||||
return $this->currentSchoolYearName();
|
||||
}
|
||||
|
||||
private function financialSchoolYearOptions(?string $selectedYear = null): array
|
||||
{
|
||||
$schoolYears = [];
|
||||
$addYear = static function (mixed $value) use (&$schoolYears): void {
|
||||
$year = trim((string) $value);
|
||||
if ($year !== '' && ! in_array($year, $schoolYears, true)) {
|
||||
$schoolYears[] = $year;
|
||||
}
|
||||
};
|
||||
|
||||
$addYear($selectedYear);
|
||||
$addYear($this->currentSchoolYearName());
|
||||
|
||||
try {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
if ($db->tableExists('school_years')) {
|
||||
$rows = $db->table('school_years')
|
||||
->select('name')
|
||||
->orderBy('name', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($rows as $row) {
|
||||
$addYear($row['name'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if ($db->tableExists('invoices') && $db->fieldExists('school_year', 'invoices')) {
|
||||
$rows = $db->table('invoices')
|
||||
->select('DISTINCT school_year', false)
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($rows as $row) {
|
||||
$addYear($row['school_year'] ?? '');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return $schoolYears;
|
||||
}
|
||||
|
||||
public function financialReport()
|
||||
{
|
||||
@@ -32,7 +86,7 @@ public function financialReport()
|
||||
|
||||
$dateFrom = $this->request->getGet('date_from');
|
||||
$dateTo = $this->request->getGet('date_to');
|
||||
$schoolYear = $this->request->getGet('school_year');
|
||||
$schoolYear = $this->selectedSchoolYear();
|
||||
|
||||
// === Apply filters to models (invoice/refund/discount/expense/reimb) ===
|
||||
if ($schoolYear) {
|
||||
@@ -235,22 +289,7 @@ public function financialReport()
|
||||
->findAll();
|
||||
|
||||
// --- School year options (for detailed view & JSON) ---
|
||||
$schoolYears = [];
|
||||
try {
|
||||
$db = \Config\Database::connect();
|
||||
$rows = $db->table('invoices')
|
||||
->select('DISTINCT school_year', false)
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$val = (string)($r['school_year'] ?? '');
|
||||
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
if (empty($schoolYears) && !empty($schoolYear)) {
|
||||
$schoolYears[] = (string)$schoolYear;
|
||||
}
|
||||
$schoolYears = $this->financialSchoolYearOptions($schoolYear);
|
||||
|
||||
$eventFeesTotal = $this->getEventFeesTotal($schoolYear, $dateFrom, $dateTo);
|
||||
|
||||
@@ -298,28 +337,11 @@ public function financialReport()
|
||||
{
|
||||
$dateFrom = $this->request->getGet('date_from');
|
||||
$dateTo = $this->request->getGet('date_to');
|
||||
$schoolYear = $this->request->getGet('school_year');
|
||||
$schoolYear = $this->selectedSchoolYear();
|
||||
|
||||
$data = $this->getFinancialSummary($dateFrom, $dateTo, $schoolYear);
|
||||
|
||||
// Build school year options from invoices table (fallback to configured year)
|
||||
$schoolYears = [];
|
||||
try {
|
||||
$db = \Config\Database::connect();
|
||||
$rows = $db->table('invoices')
|
||||
->select('DISTINCT school_year', false)
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()->getResultArray();
|
||||
foreach ($rows as $r) {
|
||||
$val = (string)($r['school_year'] ?? '');
|
||||
if ($val !== '' && !in_array($val, $schoolYears, true)) $schoolYears[] = $val;
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
if (empty($schoolYears) && !empty($data['schoolYear'])) {
|
||||
$schoolYears[] = (string)$data['schoolYear'];
|
||||
}
|
||||
$data['schoolYears'] = $schoolYears;
|
||||
$data['schoolYears'] = $this->financialSchoolYearOptions((string)($data['schoolYear'] ?? $schoolYear));
|
||||
if ($this->wantsJson() || strtolower((string)($this->request->getGet('format') ?? '')) === 'json') {
|
||||
return $this->response->setJSON(['ok' => true] + $data + [
|
||||
'csrf_token' => csrf_token(),
|
||||
@@ -524,7 +546,7 @@ public function financialReport()
|
||||
private function getFinancialSummaryDetailSections(?string $dateFrom = null, ?string $dateTo = null, ?string $schoolYear = null): array
|
||||
{
|
||||
$configModel = new \App\Models\ConfigurationModel();
|
||||
$schoolYear = $schoolYear ?: $configModel->getConfig('school_year');
|
||||
$schoolYear = $schoolYear ?: $this->currentSchoolYearName();
|
||||
|
||||
$derivedDateFrom = null;
|
||||
$derivedDateTo = null;
|
||||
@@ -1612,7 +1634,7 @@ public function financialReport()
|
||||
$additionalModel = new \App\Models\AdditionalChargeModel();
|
||||
|
||||
// Allow override via parameter; fallback to configured year
|
||||
$schoolYear = $schoolYear ?: $configModel->getConfig('school_year');
|
||||
$schoolYear = $schoolYear ?: $this->currentSchoolYearName();
|
||||
$derivedDateFrom = null;
|
||||
$derivedDateTo = null;
|
||||
if (!empty($schoolYear) && empty($dateFrom) && empty($dateTo)) {
|
||||
@@ -2068,7 +2090,7 @@ public function financialReport()
|
||||
$configModel = new \App\Models\ConfigurationModel();
|
||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = (string) ($configModel->getConfig('school_year') ?? date('Y'));
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($configModel->getConfig('school_year') ?? date('Y')));
|
||||
}
|
||||
|
||||
// Build school year options from invoices
|
||||
|
||||
@@ -35,8 +35,11 @@ class FlagController extends Controller
|
||||
$grades = [];
|
||||
|
||||
// Retrieve flags and class sections that currently have active students
|
||||
$flags = $currentFlagModel->findAll();
|
||||
$schoolYear = $configModel->getConfig('school_year');
|
||||
if ((string)$schoolYear !== '' && $this->db->fieldExists('school_year', 'current_flag')) {
|
||||
$currentFlagModel->where('school_year', (string)$schoolYear);
|
||||
}
|
||||
$flags = $currentFlagModel->findAll();
|
||||
$studentCounts = $studentClassModel->getStudentCountsBySection($schoolYear);
|
||||
$classSections = [];
|
||||
|
||||
|
||||
@@ -67,17 +67,17 @@ class InvoiceController extends ResourceController
|
||||
$this->chargesModel = new EventChargesModel();
|
||||
$this->discountUsageModel = new DiscountUsageModel();
|
||||
$this->refundModel = new RefundModel();
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->request = \Config\Services::request();
|
||||
|
||||
$this->gradeFee = $this->configModel->getConfig('grade_fee');
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->schoolYear = $this->currentSchoolYearName();
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->dueDate = $this->configModel->getConfig('due_date');
|
||||
$this->firstStudentFee = (float) ($this->configModel->getConfig('first_student_fee') ?? 350);
|
||||
$this->secondStudentFee = (float) ($this->configModel->getConfig('second_student_fee') ?? 200);
|
||||
$this->youthFee = (float) ($this->configModel->getConfig('youth_fee') ?? 200);
|
||||
$this->refundDeadline = date('Y-m-d', strtotime($this->configModel->getConfig('refund_deadline')));
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->request = \Config\Services::request();
|
||||
}
|
||||
|
||||
public function index($schoolYear = null)
|
||||
@@ -213,6 +213,15 @@ class InvoiceController extends ResourceController
|
||||
]);
|
||||
}
|
||||
|
||||
private function currentSchoolYearName(): string
|
||||
{
|
||||
try {
|
||||
return service('schoolYearContext')->resolve($this->request)->yearName();
|
||||
} catch (\Throwable $e) {
|
||||
return (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API: Invoice management composite data (used by invoice_management view)
|
||||
* Returns the same structure previously rendered server-side in index().
|
||||
@@ -221,7 +230,7 @@ class InvoiceController extends ResourceController
|
||||
{
|
||||
$schoolYear = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('year') ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = $this->schoolYear;
|
||||
$schoolYear = $this->currentSchoolYearName();
|
||||
}
|
||||
|
||||
$invoiceData = [];
|
||||
|
||||
@@ -21,7 +21,7 @@ class LateSlipLogsController extends BaseController
|
||||
{
|
||||
$req = $this->request;
|
||||
|
||||
$defaultYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
$defaultYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
$defaultSem = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$schoolYear = trim((string) ($req->getGet('school_year') ?? $defaultYear));
|
||||
$semester = trim((string) ($req->getGet('semester') ?? $defaultSem));
|
||||
|
||||
@@ -149,6 +149,11 @@ class NotificationsController extends BaseController
|
||||
$targetGroup = null;
|
||||
}
|
||||
|
||||
$schoolYear = (string) ((new \App\Models\ConfigurationModel())->getConfig('school_year') ?? '');
|
||||
if ($schoolYear !== '' && db_connect()->fieldExists('school_year', 'notifications')) {
|
||||
$this->notificationModel->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$rows = $this->notificationModel->getActiveNotifications($targetGroup);
|
||||
if (!is_array($rows)) {
|
||||
$rows = [];
|
||||
|
||||
@@ -164,8 +164,7 @@ class ParentController extends BaseController
|
||||
return redirect()->back()->with('error', 'Parent session not found.');
|
||||
}
|
||||
|
||||
// Get current school year from config
|
||||
$currentSchoolYear = $this->configModel->getConfig('school_year');
|
||||
$currentSchoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
|
||||
// Get selected school year (no semester filter on parent view)
|
||||
$selectedYear = $this->request->getVar('school_year') ?? $currentSchoolYear;
|
||||
@@ -1784,7 +1783,7 @@ $existing = $this->studentModel
|
||||
|
||||
public function parentEventPage()
|
||||
{
|
||||
$schoolYear = session()->get('school_year');
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
$semester = session()->get('semester');
|
||||
$parentId = session()->get('user_id');
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class PaymentController extends ResourceController
|
||||
$this->enrollmentModel = new EnrollmentModel();
|
||||
$this->studentModel = new StudentModel();
|
||||
$this->studentClassModel = new StudentClassModel();
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->schoolYear = $this->currentSchoolYearName();
|
||||
$this->semester = $this->configModel->getConfig('semester'); //installment_date
|
||||
$this->installmentDate = $this->configModel->getConfig('installment_date');
|
||||
$this->discountUsageModel = new DiscountUsageModel();
|
||||
@@ -137,11 +137,20 @@ class PaymentController extends ResourceController
|
||||
|
||||
private function getSelectedPaymentHistoryYear(): ?string
|
||||
{
|
||||
$selectedYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
$selectedYear = $this->request->getGet('school_year') ?? $this->currentSchoolYearName();
|
||||
|
||||
return $selectedYear !== '' ? $selectedYear : null;
|
||||
}
|
||||
|
||||
private function currentSchoolYearName(): string
|
||||
{
|
||||
try {
|
||||
return service('schoolYearContext')->resolve($this->request)->yearName();
|
||||
} catch (\Throwable $e) {
|
||||
return (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
// View: Create a new payment plan
|
||||
public function create()
|
||||
{
|
||||
@@ -204,7 +213,7 @@ class PaymentController extends ResourceController
|
||||
public function eventChargesShow()
|
||||
{
|
||||
$parents = $this->userModel->getParents();
|
||||
$school_Year = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
$school_Year = $this->request->getGet('school_year') ?? $this->currentSchoolYearName();
|
||||
$seme_ster = $this->request->getGet('semester') ?? $this->semester;
|
||||
|
||||
// Join with users and students for full info
|
||||
|
||||
@@ -1235,6 +1235,10 @@ public function updateBatchAssignment()
|
||||
$status = $this->request->getGet('status') ?: null;
|
||||
$filterYear = $this->request->getGet('school_year') ?: $this->schoolYear;
|
||||
$userId = $this->request->getGet('user_id') ?: null;
|
||||
$hasBatchSchoolYear = $this->db->fieldExists('school_year', 'reimbursement_batches');
|
||||
$hasExpenseSchoolYear = $this->db->fieldExists('school_year', 'expenses');
|
||||
$hasExpenseSemester = $this->db->fieldExists('semester', 'expenses');
|
||||
$hasReimbursementSchoolYear = $this->db->fieldExists('school_year', 'reimbursements');
|
||||
|
||||
$filters = [
|
||||
'school_year' => $filterYear,
|
||||
@@ -1251,13 +1255,11 @@ public function updateBatchAssignment()
|
||||
// Build closed batch summaries/details (all closed batches)
|
||||
$batchSummaries = [];
|
||||
$batchDetails = [];
|
||||
$batchQuery = $this->db->table('reimbursement_batches b')
|
||||
->select("
|
||||
$batchSelect = "
|
||||
b.id AS batch_id,
|
||||
b.title AS batch_title,
|
||||
b.yearly_batch_number,
|
||||
b.closed_at,
|
||||
b.school_year AS batch_school_year,
|
||||
e.id AS expense_id,
|
||||
e.amount AS expense_amount,
|
||||
e.description,
|
||||
@@ -1268,7 +1270,13 @@ public function updateBatchAssignment()
|
||||
u.firstname AS purchaser_firstname,
|
||||
u.lastname AS purchaser_lastname,
|
||||
r.amount AS reimb_amount
|
||||
")
|
||||
";
|
||||
if ($hasBatchSchoolYear) {
|
||||
$batchSelect .= ', b.school_year AS batch_school_year';
|
||||
}
|
||||
|
||||
$batchQuery = $this->db->table('reimbursement_batches b')
|
||||
->select($batchSelect)
|
||||
->join('reimbursement_batch_items bi', 'bi.batch_id = b.id', 'inner')
|
||||
->join('expenses e', 'e.id = bi.expense_id', 'inner')
|
||||
->join('users u', 'u.id = e.purchased_by', 'left')
|
||||
@@ -1277,10 +1285,16 @@ public function updateBatchAssignment()
|
||||
->where('bi.unassigned_at IS NULL', null, false);
|
||||
|
||||
if (!empty($filterYear)) {
|
||||
$batchQuery->groupStart()
|
||||
->where('b.school_year', $filterYear)
|
||||
->orWhere('e.school_year', $filterYear)
|
||||
->groupEnd();
|
||||
if ($hasBatchSchoolYear && $hasExpenseSchoolYear) {
|
||||
$batchQuery->groupStart()
|
||||
->where('b.school_year', $filterYear)
|
||||
->orWhere('e.school_year', $filterYear)
|
||||
->groupEnd();
|
||||
} elseif ($hasBatchSchoolYear) {
|
||||
$batchQuery->where('b.school_year', $filterYear);
|
||||
} elseif ($hasExpenseSchoolYear) {
|
||||
$batchQuery->where('e.school_year', $filterYear);
|
||||
}
|
||||
}
|
||||
if (!empty($userId)) {
|
||||
$batchQuery->where('e.purchased_by', $userId);
|
||||
@@ -1401,27 +1415,33 @@ public function updateBatchAssignment()
|
||||
'items' => [],
|
||||
'checks' => [],
|
||||
];
|
||||
$donationQuery = $this->db->table('expenses e')
|
||||
->select('
|
||||
$donationSelect = '
|
||||
e.id AS expense_id,
|
||||
e.amount AS expense_amount,
|
||||
e.description,
|
||||
e.retailor,
|
||||
e.receipt_path AS expense_receipt,
|
||||
e.purchased_by,
|
||||
e.school_year,
|
||||
e.semester,
|
||||
e.status,
|
||||
u.firstname AS purchaser_firstname,
|
||||
u.lastname AS purchaser_lastname
|
||||
')
|
||||
';
|
||||
if ($hasExpenseSchoolYear) {
|
||||
$donationSelect .= ', e.school_year';
|
||||
}
|
||||
if ($hasExpenseSemester) {
|
||||
$donationSelect .= ', e.semester';
|
||||
}
|
||||
|
||||
$donationQuery = $this->db->table('expenses e')
|
||||
->select($donationSelect)
|
||||
->join('users u', 'u.id = e.purchased_by', 'left')
|
||||
->where('e.category', 'Donation');
|
||||
|
||||
if (!empty($filterYear)) {
|
||||
if (!empty($filterYear) && $hasExpenseSchoolYear) {
|
||||
$donationQuery->where('e.school_year', $filterYear);
|
||||
}
|
||||
if (!empty($semester)) {
|
||||
if (!empty($semester) && $hasExpenseSemester) {
|
||||
$donationQuery->where('e.semester', $semester);
|
||||
}
|
||||
if (!empty($userId)) {
|
||||
@@ -1488,13 +1508,25 @@ public function updateBatchAssignment()
|
||||
|
||||
$users = $this->recipientOptions();
|
||||
|
||||
$years = $this->db->table('reimbursements')
|
||||
->select('school_year')
|
||||
->distinct()
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
$schoolYears = array_column($years, 'school_year');
|
||||
$schoolYears = [];
|
||||
if ($hasReimbursementSchoolYear) {
|
||||
$years = $this->db->table('reimbursements')
|
||||
->select('school_year')
|
||||
->distinct()
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
$schoolYears = array_column($years, 'school_year');
|
||||
} elseif ($hasExpenseSchoolYear) {
|
||||
$years = $this->db->table('expenses')
|
||||
->select('school_year')
|
||||
->distinct()
|
||||
->where('school_year IS NOT NULL', null, false)
|
||||
->orderBy('school_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
$schoolYears = array_column($years, 'school_year');
|
||||
}
|
||||
|
||||
// Add school years from fallback expenses (closed batches without reimbursements)
|
||||
if (!empty($fallbackRows)) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
|
||||
final class SchoolYearSelectionController extends BaseController
|
||||
{
|
||||
public function select(): RedirectResponse
|
||||
{
|
||||
$schoolYearId = (int) ($this->request->getPost('school_year_id') ?? 0);
|
||||
$returnTo = (string) ($this->request->getPost('return_to') ?? '');
|
||||
|
||||
if ($schoolYearId <= 0) {
|
||||
return redirect()->to($this->safeReturnTo($returnTo))
|
||||
->with('error', 'Please select a valid school year.');
|
||||
}
|
||||
|
||||
try {
|
||||
service('schoolYearContext')->select($schoolYearId);
|
||||
return redirect()->to($this->safeReturnTo($returnTo));
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', 'School-year selection failed: {message}', [
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return redirect()->to($this->safeReturnTo($returnTo))
|
||||
->with('error', 'The selected school year is not available.');
|
||||
}
|
||||
}
|
||||
|
||||
public function reset(): RedirectResponse
|
||||
{
|
||||
service('schoolYearContext')->clearSelection();
|
||||
|
||||
return redirect()->to($this->safeReturnTo((string) ($this->request->getPost('return_to') ?? '')));
|
||||
}
|
||||
|
||||
private function safeReturnTo(string $returnTo): string
|
||||
{
|
||||
$fallback = $this->dashboardRoute();
|
||||
$returnTo = trim($returnTo);
|
||||
|
||||
if ($returnTo === '') {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$parts = parse_url($returnTo);
|
||||
if ($parts === false) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if (isset($parts['host']) && strcasecmp((string) $parts['host'], (string) $this->request->getUri()->getHost()) !== 0) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$path = '/' . ltrim((string) ($parts['path'] ?? ''), '/');
|
||||
if ($path === '/' || str_starts_with($path, '//')) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (! empty($parts['query'])) {
|
||||
parse_str((string) $parts['query'], $query);
|
||||
foreach (['school_year_id', 'school_year', 'schoolYear', 'year'] as $key) {
|
||||
unset($query[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$cleanQuery = http_build_query($query);
|
||||
|
||||
return $path . ($cleanQuery !== '' ? '?' . $cleanQuery : '');
|
||||
}
|
||||
|
||||
private function dashboardRoute(): string
|
||||
{
|
||||
try {
|
||||
return service('roleService')->bestDashboardRouteFor((array) session()->get('roles'));
|
||||
} catch (\Throwable) {
|
||||
return '/landing_page/guest_dashboard';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class ScorePredictor extends Controller
|
||||
class ScorePredictor extends BaseController
|
||||
{
|
||||
protected $db;
|
||||
protected $configModel;
|
||||
@@ -60,17 +59,30 @@ class ScorePredictor extends Controller
|
||||
public function combinedReport()
|
||||
{
|
||||
$request = service('request');
|
||||
$currentSchoolYear = $this->schoolYear;
|
||||
$currentSchoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
|
||||
$selectedYear = $request->getVar('school_year') ?? $currentSchoolYear;
|
||||
$classSectionId = $request->getVar('class_section_id');
|
||||
$yearEsc = $this->db->escape($selectedYear);
|
||||
$acceptedEnrollmentStatuses = ['payment pending', 'enrolled'];
|
||||
// Only show class sections that have at least one student in the selected year
|
||||
$classSections = $this->db->table('classSection cs')
|
||||
->select('cs.*')
|
||||
->distinct()
|
||||
->join('student_class sc', 'sc.class_section_id = cs.class_section_id', 'inner')
|
||||
->join('enrollments e', 'e.student_id = sc.student_id AND e.school_year = ' . $yearEsc, 'inner')
|
||||
->where('sc.school_year', $selectedYear)
|
||||
->whereIn('e.enrollment_status', $acceptedEnrollmentStatuses)
|
||||
->where('e.admission_status', 'accepted')
|
||||
->groupStart()
|
||||
->where('e.is_withdrawn', 0)
|
||||
->orWhere('e.is_withdrawn', null)
|
||||
->groupEnd()
|
||||
->groupStart()
|
||||
->where('sc.is_event_only', 0)
|
||||
->orWhere('sc.is_event_only', null)
|
||||
->groupEnd()
|
||||
->notLike('cs.class_section_name', 'KG', 'after')
|
||||
->groupBy('cs.id')
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
@@ -85,12 +97,23 @@ class ScorePredictor extends Controller
|
||||
MAX(spring.semester_score) as spring_score');
|
||||
// Reduce duplication from restored students while keeping a stable class section.
|
||||
$builder->select('MAX(sc.class_section_id) as class_section_id');
|
||||
$yearEsc = $this->db->escape($selectedYear);
|
||||
$builder->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $yearEsc, 'left');
|
||||
$builder->join('student_class sc', 'sc.student_id = s.id AND sc.school_year = ' . $yearEsc, 'inner');
|
||||
$builder->join('enrollments e', 'e.student_id = s.id AND e.school_year = ' . $yearEsc, 'inner');
|
||||
$builder->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
|
||||
$builder->join('semester_scores fall', 'fall.student_id = s.id AND fall.semester = "fall" AND fall.school_year = "' . $selectedYear . '"', 'left');
|
||||
$builder->join('semester_scores spring', 'spring.student_id = s.id AND spring.semester = "spring" AND spring.school_year = "' . $selectedYear . '"', 'left');
|
||||
$builder->join('semester_scores fall', 'fall.student_id = s.id AND fall.semester = "fall" AND fall.school_year = ' . $yearEsc, 'left');
|
||||
$builder->join('semester_scores spring', 'spring.student_id = s.id AND spring.semester = "spring" AND spring.school_year = ' . $yearEsc, 'left');
|
||||
$builder->where('s.is_active', 1);
|
||||
$builder->where('sc.class_section_id IS NOT NULL', null, false);
|
||||
$builder->whereIn('e.enrollment_status', $acceptedEnrollmentStatuses);
|
||||
$builder->where('e.admission_status', 'accepted');
|
||||
$builder->groupStart()
|
||||
->where('e.is_withdrawn', 0)
|
||||
->orWhere('e.is_withdrawn', null)
|
||||
->groupEnd();
|
||||
$builder->groupStart()
|
||||
->where('sc.is_event_only', 0)
|
||||
->orWhere('sc.is_event_only', null)
|
||||
->groupEnd();
|
||||
$builder->groupStart()
|
||||
->where('cs.class_section_name IS NULL')
|
||||
->orNotLike('cs.class_section_name', 'KG', 'after')
|
||||
@@ -106,11 +129,22 @@ class ScorePredictor extends Controller
|
||||
// Stats for spring
|
||||
$springStatsQuery = $this->db->table('semester_scores')
|
||||
->select('AVG(semester_scores.semester_score) as mean, STDDEV(semester_scores.semester_score) as std')
|
||||
->join('student_class', 'student_class.student_id = semester_scores.student_id')
|
||||
->join('student_class', 'student_class.student_id = semester_scores.student_id AND student_class.school_year = ' . $yearEsc)
|
||||
->join('enrollments e', 'e.student_id = semester_scores.student_id AND e.school_year = ' . $yearEsc, 'inner')
|
||||
->join('classSection cs', 'cs.class_section_id = student_class.class_section_id', 'left')
|
||||
->where('semester_scores.semester', 'spring')
|
||||
->where('semester_scores.school_year', $selectedYear)
|
||||
->where('student_class.school_year', $selectedYear)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->whereIn('e.enrollment_status', $acceptedEnrollmentStatuses)
|
||||
->where('e.admission_status', 'accepted')
|
||||
->groupStart()
|
||||
->where('e.is_withdrawn', 0)
|
||||
->orWhere('e.is_withdrawn', null)
|
||||
->groupEnd()
|
||||
->groupStart()
|
||||
->where('student_class.is_event_only', 0)
|
||||
->orWhere('student_class.is_event_only', null)
|
||||
->groupEnd()
|
||||
->groupStart()
|
||||
->where('cs.class_section_name IS NULL')
|
||||
->orNotLike('cs.class_section_name', 'KG', 'after')
|
||||
|
||||
@@ -386,21 +386,8 @@ class StudentController extends BaseController
|
||||
{
|
||||
$schoolYear = (string)($this->schoolYear ?? '');
|
||||
|
||||
$activeStudents = $this->studentModel
|
||||
->select('id, school_id, firstname, lastname, gender, age')
|
||||
->where('is_active', 1)
|
||||
->orderBy('lastname', 'ASC')
|
||||
->orderBy('firstname', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$removedStudents = $this->studentModel
|
||||
->select('id, school_id, firstname, lastname, gender, age')
|
||||
->where('is_active', 0)
|
||||
->orderBy('lastname', 'ASC')
|
||||
->orderBy('firstname', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$classMap = [];
|
||||
$activeYearStudentIds = [];
|
||||
$classQuery = $this->db->table('student_class sc')
|
||||
->select('sc.student_id, cs.class_section_name')
|
||||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
|
||||
@@ -411,11 +398,35 @@ class StudentController extends BaseController
|
||||
|
||||
foreach ($classRows as $row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$activeYearStudentIds[$sid] = true;
|
||||
}
|
||||
|
||||
$name = trim((string)($row['class_section_name'] ?? ''));
|
||||
if ($sid <= 0 || $name === '') continue;
|
||||
$classMap[$sid][] = $name;
|
||||
}
|
||||
|
||||
$students = $this->studentModel
|
||||
->select('id, school_id, firstname, lastname, gender, age, is_active')
|
||||
->orderBy('lastname', 'ASC')
|
||||
->orderBy('firstname', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$activeStudents = [];
|
||||
$removedStudents = [];
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int)($student['id'] ?? 0);
|
||||
$isGloballyActive = (int)($student['is_active'] ?? 0) === 1;
|
||||
$hasSelectedYearClass = $studentId > 0 && isset($activeYearStudentIds[$studentId]);
|
||||
|
||||
if ($isGloballyActive && $hasSelectedYearClass) {
|
||||
$activeStudents[] = $student;
|
||||
} else {
|
||||
$removedStudents[] = $student;
|
||||
}
|
||||
}
|
||||
|
||||
$attachClassNames = static function (array $students) use ($classMap): array {
|
||||
foreach ($students as &$student) {
|
||||
$sid = (int)($student['id'] ?? 0);
|
||||
@@ -432,6 +443,7 @@ class StudentController extends BaseController
|
||||
'active_students' => $attachClassNames($activeStudents),
|
||||
'removed_students' => $attachClassNames($removedStudents),
|
||||
'school_year' => $schoolYear,
|
||||
'active_school_year' => (string)($this->schoolYear ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1671,8 +1683,7 @@ class StudentController extends BaseController
|
||||
->findAll();
|
||||
}
|
||||
} elseif (in_array($role, ['teacher', 'teacher_assistant', 'teacher_dashboard'], true)) {
|
||||
$configModel = new \App\Models\ConfigurationModel();
|
||||
$schoolYear = session()->get('school_year') ?? $configModel->getConfig('school_year');
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
|
||||
$classSectionId = (int)(session()->get('class_section_id') ?? 0);
|
||||
|
||||
if ($classSectionId > 0 && !empty($schoolYear)) {
|
||||
|
||||
@@ -20,7 +20,7 @@ class TrophyController extends BaseController
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$currentYear = $this->configModel->getConfig('school_year') ?? '';
|
||||
$currentYear = $this->currentSchoolYearName();
|
||||
$selectedYear = $this->request->getGet('school_year') ?? $currentYear;
|
||||
|
||||
$percentile = (float) ($this->request->getGet('percentile') ?? 75);
|
||||
@@ -166,7 +166,7 @@ class TrophyController extends BaseController
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$currentYear = $this->configModel->getConfig('school_year') ?? '';
|
||||
$currentYear = $this->currentSchoolYearName();
|
||||
$selectedYear = $this->request->getGet('school_year') ?? $currentYear;
|
||||
$percentile = (float) ($this->request->getGet('percentile') ?? 75);
|
||||
$percentile = max(1.0, min(99.0, $percentile));
|
||||
@@ -283,7 +283,7 @@ class TrophyController extends BaseController
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$currentYear = $this->configModel->getConfig('school_year') ?? '';
|
||||
$currentYear = $this->currentSchoolYearName();
|
||||
$selectedYear = $this->request->getGet('school_year') ?? $currentYear;
|
||||
$percentile = (float) ($this->request->getGet('percentile') ?? 75);
|
||||
$percentile = max(1.0, min(99.0, $percentile));
|
||||
|
||||
@@ -961,12 +961,18 @@ class UserController extends BaseController
|
||||
$perPage = max(1, min($perPage, 200));
|
||||
$page = max(1, $page);
|
||||
|
||||
$totalActivities = (int) $this->loginActivityModel->countAll();
|
||||
$activities = $this->loginActivityModel
|
||||
$schoolYear = (string) ((new \App\Models\ConfigurationModel())->getConfig('school_year') ?? '');
|
||||
$activityModel = $this->loginActivityModel;
|
||||
if ($schoolYear !== '' && db_connect()->fieldExists('school_year', 'login_activity')) {
|
||||
$activityModel->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$totalActivities = (int) $activityModel->countAllResults(false);
|
||||
$activities = $activityModel
|
||||
->orderBy('login_time', 'DESC')
|
||||
->paginate($perPage, 'loginActivity', $page) ?? [];
|
||||
|
||||
$pager = $this->loginActivityModel->pager;
|
||||
$pager = $activityModel->pager;
|
||||
$currentPage = $pager ? (int) $pager->getCurrentPage('loginActivity') : $page;
|
||||
$pageCount = $pager ? (int) $pager->getPageCount('loginActivity') : (int) max(1, ceil($totalActivities / $perPage));
|
||||
|
||||
|
||||
@@ -103,24 +103,14 @@ class WhatsappController extends BaseController
|
||||
$sectionName = $row['class_section_name'] ?? ('Section ' . $sectionId);
|
||||
}
|
||||
|
||||
$existing = $this->linkModel->where([
|
||||
'class_section_id' => $sectionId,
|
||||
'school_year' => $this->schoolYear,
|
||||
])->first();
|
||||
|
||||
$payload = [
|
||||
'class_section_id' => $sectionId,
|
||||
'class_section_name' => $sectionName,
|
||||
'school_year' => $this->schoolYear,
|
||||
'invite_link' => trim($inviteLink),
|
||||
'active' => $active,
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
$this->linkModel->update($existing['id'], $payload);
|
||||
} else {
|
||||
$this->linkModel->insert($payload);
|
||||
}
|
||||
$this->linkModel->upsertLinkForSection(
|
||||
$sectionId,
|
||||
$sectionName,
|
||||
(string) $this->schoolYear,
|
||||
'',
|
||||
$inviteLink,
|
||||
$active === 1
|
||||
);
|
||||
|
||||
return redirect()->back()->with('success', 'WhatsApp group link saved.');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ class WinnersController extends BaseController
|
||||
public function competitionIndex()
|
||||
{
|
||||
$competitionModel = new CompetitionModel();
|
||||
$schoolYear = $this->currentSchoolYearName();
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$competitionModel->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$competitions = $competitionModel
|
||||
->where('is_published', 1)
|
||||
@@ -21,6 +26,7 @@ class WinnersController extends BaseController
|
||||
|
||||
return view('winners/competitions/index', [
|
||||
'competitions' => $competitions,
|
||||
'schoolYear' => $schoolYear,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -31,7 +37,14 @@ class WinnersController extends BaseController
|
||||
$classSectionModel = new ClassSectionModel();
|
||||
$classWinnerModel = new CompetitionClassWinnerModel();
|
||||
|
||||
$competition = $competitionModel->where('is_published', 1)->find($id);
|
||||
$schoolYear = $this->currentSchoolYearName();
|
||||
|
||||
$competitionModel->where('is_published', 1);
|
||||
if ($schoolYear !== '') {
|
||||
$competitionModel->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$competition = $competitionModel->find($id);
|
||||
if (!$competition) {
|
||||
return redirect()->to('/winners/competitions');
|
||||
}
|
||||
@@ -81,6 +94,7 @@ class WinnersController extends BaseController
|
||||
'winnersByClass' => $winnersByClass,
|
||||
'sectionMap' => $sectionMap,
|
||||
'questionCounts' => $questionCounts,
|
||||
'schoolYear' => $schoolYear,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user