This commit is contained in:
@@ -5,7 +5,9 @@ namespace App\Controllers;
|
||||
use App\Controllers\ClassProgressController;
|
||||
use App\Models\ClassProgressAttachmentModel;
|
||||
use App\Models\ClassProgressReportModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\EnrollmentModel;
|
||||
use App\Services\SemesterRangeService;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use Config\Database;
|
||||
@@ -15,8 +17,10 @@ class ParentProgressController extends BaseController
|
||||
protected ClassProgressReportModel $reportModel;
|
||||
protected ClassProgressAttachmentModel $attachmentModel;
|
||||
protected EnrollmentModel $enrollmentModel;
|
||||
protected ConfigurationModel $configModel;
|
||||
protected BaseConnection $db;
|
||||
private ?array $parentSectionIds = null;
|
||||
private array $parentSectionIdsByYear = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -24,6 +28,7 @@ class ParentProgressController extends BaseController
|
||||
$this->reportModel = new ClassProgressReportModel();
|
||||
$this->attachmentModel = new ClassProgressAttachmentModel();
|
||||
$this->enrollmentModel = new EnrollmentModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->db = Database::connect();
|
||||
}
|
||||
|
||||
@@ -43,8 +48,8 @@ class ParentProgressController extends BaseController
|
||||
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
|
||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||
->whereIn('class_progress_reports.class_section_id', $sectionIds)
|
||||
->where('class_progress_reports.school_year', $schoolYear);
|
||||
->whereIn('class_progress_reports.class_section_id', $sectionIds);
|
||||
$this->applyProgressSchoolYearScope($builder, $schoolYear);
|
||||
|
||||
$rows = $builder
|
||||
->orderBy('week_start', 'DESC')
|
||||
@@ -82,21 +87,29 @@ class ParentProgressController extends BaseController
|
||||
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
|
||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||
->where('class_progress_reports.school_year', $schoolYear)
|
||||
->find((int) $id);
|
||||
->where('class_progress_reports.id', (int) $id)
|
||||
->first();
|
||||
|
||||
if (! $row || ! $this->isSectionAccessible($row['class_section_id'] ?? null)) {
|
||||
if (! $row) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
|
||||
$reportSchoolYear = trim((string) ($row['school_year'] ?? ''));
|
||||
$effectiveSchoolYear = $reportSchoolYear !== '' ? $reportSchoolYear : $schoolYear;
|
||||
|
||||
if (! $this->isSectionAccessible($row['class_section_id'] ?? null, $effectiveSchoolYear)) {
|
||||
throw new PageNotFoundException('Progress report not found.');
|
||||
}
|
||||
|
||||
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
||||
$row['flags'] = $this->decodeFlags($row['flags_json']);
|
||||
|
||||
$weeklyReports = $this->reportModel
|
||||
$weeklyReportsQuery = $this->reportModel
|
||||
->select('class_progress_reports.*')
|
||||
->where('class_section_id', $row['class_section_id'])
|
||||
->where('week_start', $row['week_start'])
|
||||
->where('school_year', $schoolYear)
|
||||
->where('week_start', $row['week_start']);
|
||||
$this->applyProgressSchoolYearScope($weeklyReportsQuery, $effectiveSchoolYear);
|
||||
$weeklyReports = $weeklyReportsQuery
|
||||
->orderBy('subject', 'ASC')
|
||||
->findAll();
|
||||
|
||||
@@ -122,7 +135,14 @@ class ParentProgressController extends BaseController
|
||||
public function attachment($id)
|
||||
{
|
||||
$row = $this->reportModel->find((int) $id);
|
||||
if (! $row || ! $this->isSectionAccessible($row['class_section_id'] ?? null) || empty($row['attachment_path'])) {
|
||||
if (! $row) {
|
||||
throw new PageNotFoundException('Attachment not found.');
|
||||
}
|
||||
|
||||
$reportSchoolYear = trim((string) ($row['school_year'] ?? ''));
|
||||
$effectiveSchoolYear = $reportSchoolYear !== '' ? $reportSchoolYear : $this->currentSchoolYearName();
|
||||
|
||||
if (! $this->isSectionAccessible($row['class_section_id'] ?? null, $effectiveSchoolYear) || empty($row['attachment_path'])) {
|
||||
throw new PageNotFoundException('Attachment not found.');
|
||||
}
|
||||
|
||||
@@ -142,7 +162,14 @@ class ParentProgressController extends BaseController
|
||||
}
|
||||
|
||||
$report = $this->reportModel->find((int) ($attachment['report_id'] ?? 0));
|
||||
if (! $report || ! $this->isSectionAccessible($report['class_section_id'] ?? null)) {
|
||||
if (! $report) {
|
||||
throw new PageNotFoundException('Attachment not found.');
|
||||
}
|
||||
|
||||
$reportSchoolYear = trim((string) ($report['school_year'] ?? ''));
|
||||
$effectiveSchoolYear = $reportSchoolYear !== '' ? $reportSchoolYear : $this->currentSchoolYearName();
|
||||
|
||||
if (! $this->isSectionAccessible($report['class_section_id'] ?? null, $effectiveSchoolYear)) {
|
||||
throw new PageNotFoundException('Attachment not found.');
|
||||
}
|
||||
|
||||
@@ -155,22 +182,30 @@ class ParentProgressController extends BaseController
|
||||
return $this->response->download($file, null)->setFileName($downloadName);
|
||||
}
|
||||
|
||||
protected function getParentSectionIds(): array
|
||||
protected function getParentSectionIds(?string $schoolYear = null): array
|
||||
{
|
||||
if ($this->parentSectionIds !== null) {
|
||||
$schoolYear = trim((string) ($schoolYear ?? $this->currentSchoolYearName()));
|
||||
if ($schoolYear === $this->currentSchoolYearName() && $this->parentSectionIds !== null) {
|
||||
return $this->parentSectionIds;
|
||||
}
|
||||
|
||||
if (isset($this->parentSectionIdsByYear[$schoolYear])) {
|
||||
return $this->parentSectionIdsByYear[$schoolYear];
|
||||
}
|
||||
|
||||
$parentId = (int) session()->get('user_id');
|
||||
if ($parentId === 0) {
|
||||
$this->parentSectionIds = [];
|
||||
return $this->parentSectionIds;
|
||||
$this->parentSectionIdsByYear[$schoolYear] = [];
|
||||
if ($schoolYear === $this->currentSchoolYearName()) {
|
||||
$this->parentSectionIds = [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->enrollmentModel
|
||||
->select('class_section_id')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $this->currentSchoolYearName())
|
||||
->where('school_year', $schoolYear)
|
||||
->where('is_withdrawn', 0)
|
||||
->groupBy('class_section_id')
|
||||
->findAll();
|
||||
@@ -183,8 +218,12 @@ class ParentProgressController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$this->parentSectionIds = array_values(array_unique($ids));
|
||||
return $this->parentSectionIds;
|
||||
$ids = array_values(array_unique($ids));
|
||||
$this->parentSectionIdsByYear[$schoolYear] = $ids;
|
||||
if ($schoolYear === $this->currentSchoolYearName()) {
|
||||
$this->parentSectionIds = $ids;
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
protected function buildSectionOptions(array $sectionIds): array
|
||||
@@ -263,12 +302,50 @@ class ParentProgressController extends BaseController
|
||||
return $reportGroups;
|
||||
}
|
||||
|
||||
protected function isSectionAccessible(?int $classSectionId): bool
|
||||
protected function isSectionAccessible(?int $classSectionId, ?string $schoolYear = null): bool
|
||||
{
|
||||
if (! $classSectionId) {
|
||||
return false;
|
||||
}
|
||||
return in_array((int) $classSectionId, $this->getParentSectionIds(), true);
|
||||
return in_array((int) $classSectionId, $this->getParentSectionIds($schoolYear), true);
|
||||
}
|
||||
|
||||
protected function applyProgressSchoolYearScope($builder, string $schoolYear): void
|
||||
{
|
||||
if ($schoolYear === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$hasReportYear = $this->db->fieldExists('school_year', 'class_progress_reports');
|
||||
$semesterResolver = new SemesterRangeService($this->configModel);
|
||||
[$rangeStart, $rangeEnd] = $semesterResolver->getSchoolYearRange($schoolYear);
|
||||
|
||||
if ($hasReportYear) {
|
||||
if ($rangeStart !== '' && $rangeEnd !== '') {
|
||||
$builder
|
||||
->groupStart()
|
||||
->where('class_progress_reports.school_year', $schoolYear)
|
||||
->orGroupStart()
|
||||
->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()
|
||||
->groupEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
$builder->where('class_progress_reports.school_year', $schoolYear);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($rangeStart !== '' && $rangeEnd !== '') {
|
||||
$builder
|
||||
->where('class_progress_reports.week_start >=', $rangeStart)
|
||||
->where('class_progress_reports.week_start <=', $rangeEnd);
|
||||
}
|
||||
}
|
||||
|
||||
protected function resolveAttachmentFile(array $row): ?string
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Exceptions\SchoolYear\SchoolYearWriteConflictException;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\ReportCardAcknowledgementModel;
|
||||
use App\Models\StudentModel;
|
||||
@@ -31,7 +32,8 @@ class ParentReportCardController extends BaseController
|
||||
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) $this->currentSchoolYearName());
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$schoolYear = trim($schoolYearContext->yearName());
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
|
||||
|
||||
$builder = $this->db->table('students s')
|
||||
@@ -67,6 +69,7 @@ class ParentReportCardController extends BaseController
|
||||
'ackMap' => $ackMap,
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'isEditable' => ! $schoolYearContext->isReadonly(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -82,12 +85,15 @@ class ParentReportCardController extends BaseController
|
||||
throw new PageNotFoundException('Student not found.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) $this->currentSchoolYearName());
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$schoolYear = trim($schoolYearContext->yearName());
|
||||
$semester = trim((string) ($this->request->getGet('semester') ?? $this->configModel->getConfig('semester') ?? ''));
|
||||
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
'viewed_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
if (! $schoolYearContext->isReadonly()) {
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
'viewed_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
$url = site_url('report-card/student/' . (int) $studentId);
|
||||
$query = [];
|
||||
@@ -111,6 +117,13 @@ class ParentReportCardController extends BaseController
|
||||
return redirect()->back()->with('error', 'Unable to retrieve student data. Please contact support.');
|
||||
}
|
||||
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
try {
|
||||
$this->assertSchoolYearWritable($schoolYearContext);
|
||||
} catch (SchoolYearWriteConflictException $e) {
|
||||
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$student = $this->studentModel->find((int) $studentId);
|
||||
if (! $student || (int) ($student['parent_id'] ?? 0) !== $parentId) {
|
||||
throw new PageNotFoundException('Student not found.');
|
||||
@@ -121,7 +134,7 @@ class ParentReportCardController extends BaseController
|
||||
return redirect()->back()->with('error', 'Please type your full name to sign.');
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) $this->currentSchoolYearName());
|
||||
$schoolYear = trim($schoolYearContext->yearName());
|
||||
$semester = trim((string) ($this->configModel->getConfig('semester') ?? ''));
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->touchAcknowledgement($parentId, (int) $studentId, $schoolYear, $semester, [
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\StudentClassModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\AttendanceDataModel;
|
||||
use App\Models\AttendanceRecordModel;
|
||||
use App\Exceptions\SchoolYear\SchoolYearWriteConflictException;
|
||||
use App\Services\SemesterRangeService;
|
||||
|
||||
class ParentAttendanceReportController extends BaseController
|
||||
@@ -58,7 +59,8 @@ class ParentAttendanceReportController extends BaseController
|
||||
[$sundays, $defaultDate] = $this->computeSundays();
|
||||
|
||||
// Load upcoming reports for preview/edit (today and forward within this school year)
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$schoolYear = $schoolYearContext->yearName();
|
||||
$todayYmd = local_date(utc_now(), 'Y-m-d');
|
||||
$previewRows = $this->reportModel->builder()
|
||||
->select('parent_attendance_reports.*, s.firstname, s.lastname')
|
||||
@@ -80,6 +82,8 @@ class ParentAttendanceReportController extends BaseController
|
||||
'defaultDate' => $defaultDate, // preselected date (upcoming Sunday)
|
||||
'myReports' => $previewRows,
|
||||
'cutoffThreshold' => $cutoffThreshold,
|
||||
'selectedYear' => $schoolYear,
|
||||
'isEditable' => ! $schoolYearContext->isReadonly(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -91,6 +95,13 @@ class ParentAttendanceReportController extends BaseController
|
||||
return redirect()->to('/login');
|
||||
}
|
||||
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
try {
|
||||
$this->assertSchoolYearWritable($schoolYearContext);
|
||||
} catch (SchoolYearWriteConflictException $e) {
|
||||
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$post = $this->request->getPost();
|
||||
|
||||
$rules = [
|
||||
@@ -141,7 +152,7 @@ class ParentAttendanceReportController extends BaseController
|
||||
$dismissTime = $post['dismiss_time'] ?? null;
|
||||
$reasonRaw = $post['reason'] ?? null;
|
||||
$reason = is_string($reasonRaw) ? trim($reasonRaw) : null;
|
||||
$schoolYear = $this->currentSchoolYearName((string) ($this->configModel->getConfig('school_year') ?? ''));
|
||||
$schoolYear = $schoolYearContext->yearName();
|
||||
|
||||
// Enforce Sunday-only and future-or-today selection
|
||||
$todayCheck = new \DateTime('today');
|
||||
@@ -1508,6 +1519,13 @@ class ParentAttendanceReportController extends BaseController
|
||||
return redirect()->to('/login');
|
||||
}
|
||||
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
try {
|
||||
$this->assertSchoolYearWritable($schoolYearContext);
|
||||
} catch (SchoolYearWriteConflictException $e) {
|
||||
return redirect()->back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$id = (int) ($this->request->getPost('id') ?? 0);
|
||||
if ($id <= 0) {
|
||||
return redirect()->back()->with('error', 'Invalid report.');
|
||||
@@ -1517,6 +1535,9 @@ class ParentAttendanceReportController extends BaseController
|
||||
if (!$row || (int)($row['parent_id'] ?? 0) !== (int)$parentId) {
|
||||
return redirect()->back()->with('error', 'Report not found.');
|
||||
}
|
||||
if ((string) ($row['school_year'] ?? '') !== $schoolYearContext->yearName()) {
|
||||
return redirect()->back()->with('error', 'Report not found for the selected school year.');
|
||||
}
|
||||
|
||||
// Only allow editing for 'new' status entries
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
|
||||
@@ -1064,6 +1064,10 @@ class ParentController extends BaseController
|
||||
|
||||
$schoolIdService = new SchoolIdService();
|
||||
$parentId = session()->get('user_id');
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$this->assertSchoolYearWritable($schoolYearContext);
|
||||
$selectedSchoolYear = $schoolYearContext->yearName();
|
||||
$this->schoolYear = $selectedSchoolYear;
|
||||
|
||||
if (!$this->lastDayOfRegistration || !strtotime($this->lastDayOfRegistration)) {
|
||||
throw new \Exception('Invalid enrollment deadline date.');
|
||||
@@ -1270,7 +1274,9 @@ class ParentController extends BaseController
|
||||
|
||||
private function getRegistrationData(int $parentId): array
|
||||
{
|
||||
$enrollments = $this->getEnrollmentsByParent($parentId, $this->schoolYear);
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$selectedSchoolYear = $schoolYearContext->yearName();
|
||||
$enrollments = $this->getEnrollmentsByParent($parentId, $selectedSchoolYear);
|
||||
|
||||
$enrollmentMap = [];
|
||||
if (!empty($enrollments)) {
|
||||
@@ -1306,6 +1312,8 @@ class ParentController extends BaseController
|
||||
'maxChilds' => $this->maxChilds,
|
||||
'maxEmergency' => $this->maxEmergency,
|
||||
'enrollments' => $enrollments,
|
||||
'selectedYear' => $selectedSchoolYear,
|
||||
'isEditable' => ! $schoolYearContext->isReadonly(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1636,6 +1644,9 @@ $existing = $this->studentModel
|
||||
|
||||
public function editEmergencyContact($id = null)
|
||||
{
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$this->assertSchoolYearWritable($schoolYearContext);
|
||||
|
||||
if ($id === null) {
|
||||
$parentId = session()->get('user_id');
|
||||
$contacts = $this->emergencyContactModel->where('parent_id', $parentId)->findAll();
|
||||
@@ -1681,8 +1692,8 @@ $existing = $this->studentModel
|
||||
}
|
||||
|
||||
$parentId = session()->get('user_id');
|
||||
$semester = session()->get('active_semester');
|
||||
$schoolYear = session()->get('active_school_year');
|
||||
$semester = $this->semester;
|
||||
$schoolYear = $schoolYearContext->yearName();
|
||||
|
||||
$this->saveEmergencyContact($parentId, $semester, $schoolYear, [
|
||||
'first_name' => $this->request->getPost('emergency_first_name'),
|
||||
@@ -1699,6 +1710,9 @@ $existing = $this->studentModel
|
||||
|
||||
public function editStudent($id)
|
||||
{
|
||||
$schoolYearContext = $this->resolveSchoolYearContext();
|
||||
$this->assertSchoolYearWritable($schoolYearContext);
|
||||
$this->schoolYear = $schoolYearContext->yearName();
|
||||
$schoolIdService = new \App\Services\SchoolIdService();
|
||||
|
||||
if (strtolower($this->request->getMethod()) !== 'post') {
|
||||
@@ -1752,6 +1766,8 @@ $existing = $this->studentModel
|
||||
|
||||
public function deleteStudent($id)
|
||||
{
|
||||
$this->assertSchoolYearWritable($this->resolveSchoolYearContext());
|
||||
|
||||
if (strtolower($this->request->getMethod()) !== 'post') {
|
||||
return redirect()->back()->with('error', 'Invalid request method.');
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ final class SchoolYearSelectionController extends BaseController
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$path = $this->normalizeSchoolYearReturnPath($path);
|
||||
|
||||
$query = [];
|
||||
if (! empty($parts['query'])) {
|
||||
parse_str((string) $parts['query'], $query);
|
||||
@@ -73,6 +75,15 @@ final class SchoolYearSelectionController extends BaseController
|
||||
return $path . ($cleanQuery !== '' ? '?' . $cleanQuery : '');
|
||||
}
|
||||
|
||||
private function normalizeSchoolYearReturnPath(string $path): string
|
||||
{
|
||||
if (preg_match('#^/(parent|teacher|admin)/progress/view/\d+$#', $path, $matches) === 1) {
|
||||
return '/' . $matches[1] . '/progress';
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function dashboardRoute(): string
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user