apply the school year concept
Tests / PHPUnit (push) Failing after 1m19s

This commit is contained in:
root
2026-07-14 00:59:00 -04:00
parent 504c3bc9f9
commit feb1b29a32
73 changed files with 4288 additions and 620 deletions
@@ -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
+179 -8
View File
@@ -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;
}
}
+99
View File
@@ -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, [
+160 -61
View File
@@ -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 doesnt 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
+10 -1
View File
@@ -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 = [];
+10 -1
View File
@@ -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) {
+61 -39
View File
@@ -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
+4 -1
View File
@@ -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 = [];
+13 -4
View File
@@ -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 = [];
+2 -3
View File
@@ -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');
+12 -3
View File
@@ -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';
}
}
}
+45 -11
View File
@@ -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')
+27 -16
View File
@@ -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)) {
+3 -3
View File
@@ -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));
+9 -3
View File
@@ -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));
+8 -18
View File
@@ -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.');
}
+15 -1
View File
@@ -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,
]);
}
}