866 lines
33 KiB
PHP
866 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Models\ClassProgressReportModel;
|
|
use App\Models\ClassProgressAttachmentModel;
|
|
use App\Models\ConfigurationModel;
|
|
use App\Models\SubjectCurriculumModel;
|
|
use App\Models\TeacherClassModel;
|
|
use App\Services\SemesterRangeService;
|
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
|
|
|
class ClassProgressController extends BaseController
|
|
{
|
|
public const STATUS_OPTIONS = [
|
|
'on_track' => 'On track',
|
|
'slightly_behind' => 'Slightly behind',
|
|
'behind' => 'Behind',
|
|
];
|
|
private const DEFAULT_STATUS = 'on_track';
|
|
public const SUBJECT_SECTIONS = [
|
|
'islamic' => [
|
|
'label' => 'Islamic Studies',
|
|
'db_subject' => 'Islamic Studies',
|
|
],
|
|
'quran' => [
|
|
'label' => 'Quran/Arabic',
|
|
'db_subject' => 'Quran/Arabic',
|
|
],
|
|
];
|
|
protected ClassProgressReportModel $reportModel;
|
|
protected ClassProgressAttachmentModel $attachmentModel;
|
|
protected TeacherClassModel $teacherClassModel;
|
|
protected ConfigurationModel $configModel;
|
|
protected string $attachmentStoragePath;
|
|
protected string $attachmentPublicBase;
|
|
protected SubjectCurriculumModel $curriculumModel;
|
|
|
|
public function __construct()
|
|
{
|
|
helper(['form', 'url']);
|
|
$this->reportModel = new ClassProgressReportModel();
|
|
$this->attachmentModel = new ClassProgressAttachmentModel();
|
|
$this->teacherClassModel = new TeacherClassModel();
|
|
$this->configModel = new ConfigurationModel();
|
|
$this->curriculumModel = new SubjectCurriculumModel();
|
|
$this->attachmentStoragePath = WRITEPATH . 'uploads/class_material/';
|
|
$this->attachmentPublicBase = 'writable/uploads/class_material/';
|
|
}
|
|
|
|
public function create()
|
|
{
|
|
$teacherId = (int) session()->get('user_id');
|
|
$assignments = $this->loadTeacherSections($teacherId);
|
|
$first = $assignments[0] ?? null;
|
|
$classSectionId = $first['class_section_id'] ?? null;
|
|
$classSectionName = $first['class_section_name'] ?? null;
|
|
$classId = $first['class_id'] ?? null;
|
|
$sundayOptions = $this->buildSundayOptions();
|
|
$defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions);
|
|
$subjectCurriculum = [];
|
|
if ($classId) {
|
|
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
|
$subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug);
|
|
}
|
|
}
|
|
$data = [
|
|
'subjectSections' => self::SUBJECT_SECTIONS,
|
|
'subjectCurriculum' => $subjectCurriculum,
|
|
'classSectionId' => $classSectionId,
|
|
'classSectionName' => $classSectionName,
|
|
'classId' => $classId,
|
|
'sundayOptions' => $sundayOptions,
|
|
'defaultWeekStart' => $defaultWeekStart,
|
|
];
|
|
return view('teacher/class_progress_submit', $data);
|
|
}
|
|
|
|
public function store()
|
|
{
|
|
$subjectSections = self::SUBJECT_SECTIONS;
|
|
$rules = [
|
|
'class_section_id' => 'required|integer',
|
|
'week_start' => 'required|valid_date[Y-m-d]',
|
|
'week_end' => 'required|valid_date[Y-m-d]',
|
|
'support_needed' => 'permit_empty|string',
|
|
'flags' => 'permit_empty',
|
|
];
|
|
|
|
foreach ($subjectSections as $slug => $section) {
|
|
$rules["covered_$slug"] = 'required|string';
|
|
$rules["homework_$slug"] = 'permit_empty|string';
|
|
$rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]';
|
|
$rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]';
|
|
}
|
|
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
|
if (! empty($attachmentErrors)) {
|
|
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
|
}
|
|
|
|
$weekStart = (string) $this->request->getPost('week_start');
|
|
$weekEnd = (string) $this->request->getPost('week_end');
|
|
if ($weekStart && ! $weekEnd) {
|
|
$weekEnd = $this->buildWeekEndFromStart($weekStart);
|
|
}
|
|
if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) {
|
|
return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.');
|
|
}
|
|
|
|
$teacherId = (int) session()->get('user_id');
|
|
$classSectionId = $this->request->getPost('class_section_id');
|
|
$classSectionId = $classSectionId ? (int) $classSectionId : null;
|
|
if (! $classSectionId) {
|
|
return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.');
|
|
}
|
|
|
|
$confirmOverwrite = (bool) $this->request->getPost('confirm_overwrite');
|
|
$existingReports = $this->reportModel
|
|
->select('id')
|
|
->where('class_section_id', $classSectionId)
|
|
->where('week_start', $weekStart)
|
|
->where('teacher_id', $teacherId)
|
|
->findAll();
|
|
|
|
if (! $confirmOverwrite && ! empty($existingReports)) {
|
|
return redirect()->back()
|
|
->withInput()
|
|
->with('warning', 'A progress report already exists for this week, are you sure you want to override it?')
|
|
->with('confirm_overwrite', true);
|
|
}
|
|
|
|
if ($confirmOverwrite && ! empty($existingReports)) {
|
|
$existingIds = array_values(array_filter(array_map(
|
|
static fn (array $row): int => (int) ($row['id'] ?? 0),
|
|
$existingReports
|
|
)));
|
|
if (! empty($existingIds)) {
|
|
$this->attachmentModel->whereIn('report_id', $existingIds)->delete();
|
|
$this->reportModel->whereIn('id', $existingIds)->delete();
|
|
}
|
|
}
|
|
|
|
$status = self::DEFAULT_STATUS;
|
|
|
|
$reportsCreated = 0;
|
|
foreach ($subjectSections as $slug => $section) {
|
|
$covered = trim((string) $this->request->getPost("covered_$slug"));
|
|
if ($covered === '') {
|
|
continue;
|
|
}
|
|
$homework = trim((string) $this->request->getPost("homework_$slug"));
|
|
$unitTitle = $this->buildUnitChapterSummary($slug);
|
|
|
|
$data = [
|
|
'teacher_id' => $teacherId,
|
|
'class_section_id' => $classSectionId,
|
|
'week_start' => $weekStart,
|
|
'week_end' => $weekEnd,
|
|
'subject' => $section['db_subject'] ?? $section['label'] ?? $slug,
|
|
'unit_title' => $unitTitle,
|
|
'covered' => $covered,
|
|
'homework' => $homework ?: null,
|
|
'status' => $status,
|
|
'flags_json' => $this->normalizeFlags($this->request->getPost('flags')),
|
|
];
|
|
|
|
$reportId = $this->reportModel->insert($data, true);
|
|
$attachmentField = "attachment_$slug";
|
|
$attachments = $this->request->getFileMultiple($attachmentField) ?? [];
|
|
$storedAttachments = $this->storeAttachments($reportId, $attachments);
|
|
if (! empty($storedAttachments)) {
|
|
$this->attachmentModel->insertBatch($storedAttachments);
|
|
$this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]);
|
|
}
|
|
$reportsCreated++;
|
|
}
|
|
|
|
if ($reportsCreated === 0) {
|
|
return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.');
|
|
}
|
|
|
|
return redirect()->to('teacher/progress/submit')->with('success', 'Progress reports saved.');
|
|
}
|
|
|
|
public function history()
|
|
{
|
|
$teacherId = (int) session()->get('user_id');
|
|
$assignments = $this->loadTeacherSections($teacherId);
|
|
$selectedSectionId = (int) $this->request->getGet('class_section_id');
|
|
$validSectionIds = array_column($assignments, 'class_section_id');
|
|
if ($selectedSectionId === 0 && ! empty($validSectionIds)) {
|
|
$selectedSectionId = $validSectionIds[0];
|
|
}
|
|
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
|
|
$selectedSectionId = $validSectionIds[0] ?? null;
|
|
}
|
|
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
|
$allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear);
|
|
if (empty($allowedTeacherIds)) {
|
|
$allowedTeacherIds = [$teacherId];
|
|
}
|
|
$builder = $this->reportModel
|
|
->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('teacher_id', $allowedTeacherIds);
|
|
if ($selectedSectionId) {
|
|
$builder->where('class_progress_reports.class_section_id', $selectedSectionId);
|
|
}
|
|
$rows = $builder
|
|
->orderBy('week_start', 'DESC')
|
|
->findAll();
|
|
|
|
$reportGroups = [];
|
|
foreach ($rows as $row) {
|
|
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
|
$key = $row['week_start'] ?? '';
|
|
if ($key === '') {
|
|
continue;
|
|
}
|
|
if (! isset($reportGroups[$key])) {
|
|
$reportGroups[$key] = [
|
|
'week_start' => $row['week_start'],
|
|
'week_end' => $row['week_end'],
|
|
'class_section_name' => $row['class_section_name'] ?? '',
|
|
'reports' => [],
|
|
];
|
|
}
|
|
$reportGroups[$key]['reports'][$row['subject']] = $row;
|
|
}
|
|
|
|
$sectionOptions = [];
|
|
foreach ($assignments as $assignment) {
|
|
$sectionOptions[$assignment['class_section_id']] = $assignment['class_section_name'] ?? '';
|
|
}
|
|
return view('teacher/class_progress_history', [
|
|
'reportGroups' => $reportGroups,
|
|
'subjectSections' => self::SUBJECT_SECTIONS,
|
|
'classSectionOptions' => $sectionOptions,
|
|
'selectedSectionId' => $selectedSectionId,
|
|
]);
|
|
}
|
|
|
|
public function view($id)
|
|
{
|
|
$teacherId = (int) session()->get('user_id');
|
|
$row = $this->reportModel
|
|
->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.id', (int) $id)
|
|
->first();
|
|
|
|
if (! $row) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
|
|
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
|
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
|
|
if (empty($allowedTeacherIds)) {
|
|
if ($teacherId !== (int) $row['teacher_id']) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
$allowedTeacherIds = [(int) $row['teacher_id']];
|
|
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
|
|
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
|
$weeklyReports = $this->reportModel
|
|
->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('teacher_id', $allowedTeacherIds)
|
|
->where('class_progress_reports.class_section_id', $row['class_section_id'])
|
|
->where('week_start', $row['week_start'])
|
|
->orderBy('subject', 'ASC')
|
|
->findAll();
|
|
|
|
$attachmentMap = $this->loadAttachmentsForReports(array_column($weeklyReports, 'id'));
|
|
foreach ($weeklyReports as &$report) {
|
|
$report['status_label'] = self::STATUS_OPTIONS[$report['status']] ?? 'Unknown';
|
|
$report['attachments'] = $attachmentMap[$report['id']] ?? [];
|
|
if (empty($report['attachments']) && ! empty($report['attachment_path'])) {
|
|
$report['attachments'][] = [
|
|
'id' => $report['id'],
|
|
'name' => basename((string) $report['attachment_path']),
|
|
'legacy' => true,
|
|
];
|
|
}
|
|
}
|
|
|
|
return view('teacher/class_progress_view', [
|
|
'row' => $row,
|
|
'weeklyReports' => $weeklyReports,
|
|
'subjectSections' => self::SUBJECT_SECTIONS,
|
|
]);
|
|
}
|
|
|
|
public function edit($id)
|
|
{
|
|
$teacherId = (int) session()->get('user_id');
|
|
$row = $this->reportModel
|
|
->select('class_progress_reports.*, cs.class_section_name')
|
|
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
|
->where('class_progress_reports.id', (int) $id)
|
|
->first();
|
|
|
|
if (! $row) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
|
|
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
|
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
|
|
if (empty($allowedTeacherIds)) {
|
|
if ($teacherId !== (int) $row['teacher_id']) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
$allowedTeacherIds = [(int) $row['teacher_id']];
|
|
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
|
|
$weeklyReports = $this->reportModel
|
|
->select('class_progress_reports.*')
|
|
->whereIn('teacher_id', $allowedTeacherIds)
|
|
->where('class_section_id', $row['class_section_id'])
|
|
->where('week_start', $row['week_start'])
|
|
->orderBy('subject', 'ASC')
|
|
->findAll();
|
|
|
|
$reportMap = [];
|
|
foreach ($weeklyReports as $report) {
|
|
$subject = (string) ($report['subject'] ?? '');
|
|
if ($subject === '') {
|
|
continue;
|
|
}
|
|
$reportMap[$subject] = $report;
|
|
}
|
|
|
|
$subjectReports = [];
|
|
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
|
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
|
$report = $reportMap[$subjectName] ?? null;
|
|
if (! $report) {
|
|
continue;
|
|
}
|
|
$parsed = $this->parseUnitChapterSummary((string) ($report['unit_title'] ?? ''));
|
|
$subjectReports[$slug] = [
|
|
'report_id' => (int) ($report['id'] ?? 0),
|
|
'covered' => $report['covered'] ?? '',
|
|
'homework' => $report['homework'] ?? '',
|
|
'unit_title' => $report['unit_title'] ?? '',
|
|
'unit_values' => $parsed['units'],
|
|
'chapter_values' => $parsed['chapters'],
|
|
];
|
|
}
|
|
|
|
$assignments = $this->loadTeacherSections($teacherId);
|
|
$classId = null;
|
|
$classSectionName = $row['class_section_name'] ?? '';
|
|
foreach ($assignments as $assignment) {
|
|
if ((int) ($assignment['class_section_id'] ?? 0) === (int) $row['class_section_id']) {
|
|
$classId = $assignment['class_id'] ?? null;
|
|
$classSectionName = $assignment['class_section_name'] ?? $classSectionName;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$subjectCurriculum = [];
|
|
if ($classId) {
|
|
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
|
$subjectCurriculum[$slug] = $this->curriculumModel->getOptionsForClass((int) $classId, $slug);
|
|
}
|
|
}
|
|
|
|
return view('teacher/class_progress_submit', [
|
|
'subjectSections' => self::SUBJECT_SECTIONS,
|
|
'subjectCurriculum' => $subjectCurriculum,
|
|
'classSectionId' => $row['class_section_id'],
|
|
'classSectionName' => $classSectionName,
|
|
'classId' => $classId,
|
|
'sundayOptions' => [$row['week_start']],
|
|
'defaultWeekStart' => $row['week_start'],
|
|
'existingWeekEnd' => $row['week_end'],
|
|
'existingReports' => $subjectReports,
|
|
'isEdit' => true,
|
|
'formAction' => base_url('teacher/progress/update/' . (int) $row['id']),
|
|
'submitLabel' => 'Update Progress',
|
|
]);
|
|
}
|
|
|
|
public function update($id)
|
|
{
|
|
$teacherId = (int) session()->get('user_id');
|
|
$row = $this->reportModel->find((int) $id);
|
|
if (! $row) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
|
|
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
|
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
|
|
if (empty($allowedTeacherIds)) {
|
|
if ($teacherId !== (int) $row['teacher_id']) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
$allowedTeacherIds = [(int) $row['teacher_id']];
|
|
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
|
|
throw new PageNotFoundException('Progress report not found.');
|
|
}
|
|
|
|
$subjectSections = self::SUBJECT_SECTIONS;
|
|
$rules = [
|
|
'class_section_id' => 'required|integer',
|
|
'week_start' => 'required|valid_date[Y-m-d]',
|
|
'week_end' => 'required|valid_date[Y-m-d]',
|
|
];
|
|
foreach ($subjectSections as $slug => $section) {
|
|
$rules["covered_$slug"] = 'required|string';
|
|
$rules["homework_$slug"] = 'permit_empty|string';
|
|
$rules["unit_{$slug}.*"] = 'permit_empty|string|max_length[120]';
|
|
$rules["chapter_{$slug}.*"] = 'permit_empty|string|max_length[120]';
|
|
}
|
|
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$attachmentErrors = $this->validateAttachmentFiles($subjectSections);
|
|
if (! empty($attachmentErrors)) {
|
|
return redirect()->back()->withInput()->with('errors', $attachmentErrors);
|
|
}
|
|
|
|
$weekStart = (string) $this->request->getPost('week_start');
|
|
$weekEnd = (string) $this->request->getPost('week_end');
|
|
if ($weekStart && ! $weekEnd) {
|
|
$weekEnd = $this->buildWeekEndFromStart($weekStart);
|
|
}
|
|
if ($weekStart && $weekEnd && strtotime($weekEnd) < strtotime($weekStart)) {
|
|
return redirect()->back()->withInput()->with('error', 'Week end must be the same as or after the week start.');
|
|
}
|
|
|
|
$classSectionId = (int) ($row['class_section_id'] ?? 0);
|
|
if ($classSectionId === 0) {
|
|
return redirect()->back()->withInput()->with('error', 'No class assignment found for this report.');
|
|
}
|
|
|
|
$weeklyReports = $this->reportModel
|
|
->select('class_progress_reports.*')
|
|
->whereIn('teacher_id', $allowedTeacherIds)
|
|
->where('class_section_id', $classSectionId)
|
|
->where('week_start', $row['week_start'])
|
|
->orderBy('subject', 'ASC')
|
|
->findAll();
|
|
|
|
$reportMap = [];
|
|
foreach ($weeklyReports as $report) {
|
|
$subject = (string) ($report['subject'] ?? '');
|
|
if ($subject === '') {
|
|
continue;
|
|
}
|
|
$reportMap[$subject] = $report;
|
|
}
|
|
|
|
$reportsUpdated = 0;
|
|
$flagsInput = $this->request->getPost('flags');
|
|
foreach ($subjectSections as $slug => $section) {
|
|
$covered = trim((string) $this->request->getPost("covered_$slug"));
|
|
if ($covered === '') {
|
|
continue;
|
|
}
|
|
$homework = trim((string) $this->request->getPost("homework_$slug"));
|
|
$unitTitle = $this->buildUnitChapterSummary($slug);
|
|
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
|
$existing = $reportMap[$subjectName] ?? null;
|
|
if ($unitTitle === null && $existing) {
|
|
$unitTitle = $existing['unit_title'] ?? null;
|
|
}
|
|
|
|
$data = [
|
|
'class_section_id' => $classSectionId,
|
|
'week_start' => $weekStart,
|
|
'week_end' => $weekEnd,
|
|
'subject' => $subjectName,
|
|
'unit_title' => $unitTitle,
|
|
'covered' => $covered,
|
|
'homework' => $homework ?: null,
|
|
];
|
|
if ($flagsInput !== null) {
|
|
$data['flags_json'] = $this->normalizeFlags($flagsInput);
|
|
}
|
|
|
|
if ($existing) {
|
|
$this->reportModel->update((int) $existing['id'], $data);
|
|
$reportId = (int) $existing['id'];
|
|
} else {
|
|
$data['teacher_id'] = $teacherId;
|
|
$data['status'] = self::DEFAULT_STATUS;
|
|
$reportId = (int) $this->reportModel->insert($data, true);
|
|
}
|
|
|
|
$attachmentField = "attachment_$slug";
|
|
$attachments = $this->request->getFileMultiple($attachmentField) ?? [];
|
|
$storedAttachments = $this->storeAttachments($reportId, $attachments);
|
|
if (! empty($storedAttachments)) {
|
|
$this->attachmentModel->insertBatch($storedAttachments);
|
|
if (empty($existing['attachment_path'] ?? '')) {
|
|
$this->reportModel->update($reportId, ['attachment_path' => $storedAttachments[0]['file_path']]);
|
|
}
|
|
}
|
|
$reportsUpdated++;
|
|
}
|
|
|
|
if ($reportsUpdated === 0) {
|
|
return redirect()->back()->withInput()->with('error', 'Please provide progress for at least one subject.');
|
|
}
|
|
|
|
return redirect()->to('teacher/progress/history')->with('success', 'Progress reports updated.');
|
|
}
|
|
|
|
public function attachment($id)
|
|
{
|
|
$row = $this->reportModel->find((int)$id);
|
|
if (! $row || empty($row['attachment_path'])) {
|
|
throw new PageNotFoundException('Attachment not found.');
|
|
}
|
|
|
|
$file = $this->resolveAttachmentFile($row);
|
|
if (! $file) {
|
|
throw new PageNotFoundException('Attachment missing.');
|
|
}
|
|
|
|
return $this->response->download($file, null)->setFileName(basename($file));
|
|
}
|
|
|
|
public function attachmentFile($id)
|
|
{
|
|
$attachment = $this->attachmentModel->find((int) $id);
|
|
if (! $attachment || empty($attachment['file_path'])) {
|
|
throw new PageNotFoundException('Attachment not found.');
|
|
}
|
|
|
|
$file = $this->resolveAttachmentPath($attachment['file_path']);
|
|
if (! $file) {
|
|
throw new PageNotFoundException('Attachment missing.');
|
|
}
|
|
|
|
$downloadName = $attachment['original_name'] ?: basename($file);
|
|
return $this->response->download($file, null)->setFileName($downloadName);
|
|
}
|
|
|
|
protected function resolveAttachmentFile(array $row): ?string
|
|
{
|
|
$path = trim((string) ($row['attachment_path'] ?? ''));
|
|
if ($path === '') {
|
|
return null;
|
|
}
|
|
return $this->resolveAttachmentPath($path);
|
|
}
|
|
|
|
protected function resolveAttachmentPath(string $path): ?string
|
|
{
|
|
$relative = preg_replace('#^writable/uploads/#', '', $path);
|
|
$absolute = WRITEPATH . 'uploads/' . ltrim($relative, '/');
|
|
return is_file($absolute) ? $absolute : null;
|
|
}
|
|
|
|
protected function validateAttachmentFiles(array $subjectSections): array
|
|
{
|
|
$errors = [];
|
|
foreach ($subjectSections as $slug => $section) {
|
|
$label = $section['label'] ?? $slug;
|
|
$field = "attachment_$slug";
|
|
$files = $this->request->getFileMultiple($field) ?? [];
|
|
foreach ($files as $file) {
|
|
if (! $file || $file->getError() === UPLOAD_ERR_NO_FILE) {
|
|
continue;
|
|
}
|
|
if (! $file->isValid()) {
|
|
$errors[] = "Invalid attachment uploaded for {$label}.";
|
|
continue;
|
|
}
|
|
if ($file->getSize() > 5 * 1024 * 1024) {
|
|
$errors[] = "Each attachment for {$label} must be 5MB or smaller.";
|
|
}
|
|
$ext = strtolower((string) $file->getClientExtension());
|
|
if ($ext === '' || ! in_array($ext, ['pdf', 'jpg', 'jpeg', 'png'], true)) {
|
|
$errors[] = "Only PDF, JPG, JPEG, or PNG files are allowed for {$label}.";
|
|
}
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
protected function storeAttachments(int $reportId, array $attachments): array
|
|
{
|
|
if (empty($attachments)) {
|
|
return [];
|
|
}
|
|
|
|
$stored = [];
|
|
$now = date('Y-m-d H:i:s');
|
|
foreach ($attachments as $file) {
|
|
if (! $file || $file->getError() === UPLOAD_ERR_NO_FILE) {
|
|
continue;
|
|
}
|
|
if (! $file->isValid() || $file->hasMoved()) {
|
|
continue;
|
|
}
|
|
$stored[] = [
|
|
'report_id' => $reportId,
|
|
'file_path' => $this->storeAttachment($file),
|
|
'original_name' => $file->getClientName(),
|
|
'mime_type' => $file->getClientMimeType(),
|
|
'file_size' => $file->getSize(),
|
|
'created_at' => $now,
|
|
];
|
|
}
|
|
|
|
return $stored;
|
|
}
|
|
|
|
protected function loadAttachmentsForReports(array $reportIds): array
|
|
{
|
|
$reportIds = array_values(array_filter(array_map('intval', $reportIds)));
|
|
if (empty($reportIds)) {
|
|
return [];
|
|
}
|
|
|
|
$rows = $this->attachmentModel
|
|
->whereIn('report_id', $reportIds)
|
|
->orderBy('id', 'ASC')
|
|
->findAll();
|
|
|
|
$map = [];
|
|
foreach ($rows as $row) {
|
|
$reportId = (int) ($row['report_id'] ?? 0);
|
|
if ($reportId === 0) {
|
|
continue;
|
|
}
|
|
$map[$reportId][] = [
|
|
'id' => (int) $row['id'],
|
|
'name' => $row['original_name'] ?: basename((string) $row['file_path']),
|
|
];
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
protected function loadTeacherSections(int $teacherId): array
|
|
{
|
|
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
|
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
|
return $this->teacherClassModel->getClassAssignmentsByUserId($teacherId, $schoolYear, $semester);
|
|
}
|
|
|
|
protected function normalizeFlags($flags): ?string
|
|
{
|
|
$flags = array_values(array_filter((array) $flags, static fn ($item) => $item !== '' && $item !== null));
|
|
return $flags ? json_encode($flags) : null;
|
|
}
|
|
|
|
protected function buildUnitChapterSummary(string $slug): ?string
|
|
{
|
|
$unitValues = array_map('trim', (array) $this->request->getPost("unit_$slug"));
|
|
$chapterValues = array_map('trim', (array) $this->request->getPost("chapter_$slug"));
|
|
$parts = [];
|
|
$count = max(count($unitValues), count($chapterValues));
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$unit = $unitValues[$i] ?? '';
|
|
$chapter = $chapterValues[$i] ?? '';
|
|
if ($unit === '' && $chapter === '') {
|
|
continue;
|
|
}
|
|
$segment = $unit;
|
|
if ($chapter !== '') {
|
|
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
|
|
}
|
|
if ($segment === '') {
|
|
continue;
|
|
}
|
|
$parts[] = $segment;
|
|
}
|
|
if (! $parts) {
|
|
return null;
|
|
}
|
|
$summary = implode(' ; ', $parts);
|
|
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
|
}
|
|
|
|
protected function parseUnitChapterSummary(string $summary): array
|
|
{
|
|
$summary = trim($summary);
|
|
if ($summary === '') {
|
|
return ['units' => [], 'chapters' => []];
|
|
}
|
|
|
|
$units = [];
|
|
$chapters = [];
|
|
$segments = preg_split('/\s*;\s*/', $summary, -1, PREG_SPLIT_NO_EMPTY);
|
|
foreach ($segments as $segment) {
|
|
$segment = trim($segment);
|
|
if ($segment === '') {
|
|
continue;
|
|
}
|
|
$parts = preg_split('/\s*\/\s*/', $segment, 2);
|
|
if (count($parts) === 2) {
|
|
$units[] = trim($parts[0]);
|
|
$chapters[] = trim($parts[1]);
|
|
} else {
|
|
$units[] = $segment;
|
|
$chapters[] = '';
|
|
}
|
|
}
|
|
|
|
return ['units' => $units, 'chapters' => $chapters];
|
|
}
|
|
|
|
protected function buildSundayOptions(int $count = 12): array
|
|
{
|
|
$range = $this->resolveProgressDateRange();
|
|
if ($range === null) {
|
|
return $this->buildUpcomingSundayOptions($count);
|
|
}
|
|
[$rangeStart, $rangeEnd] = $range;
|
|
|
|
try {
|
|
$start = new \DateTime($rangeStart);
|
|
$end = new \DateTime($rangeEnd);
|
|
} catch (\Exception $e) {
|
|
return $this->buildUpcomingSundayOptions($count);
|
|
}
|
|
|
|
if ($end < $start) {
|
|
[$start, $end] = [$end, $start];
|
|
}
|
|
|
|
$weekday = (int) $start->format('w');
|
|
if ($weekday !== 0) {
|
|
$start->modify('next sunday');
|
|
}
|
|
|
|
$options = [];
|
|
while ($start <= $end) {
|
|
$options[] = $start->format('Y-m-d');
|
|
$start->modify('+7 days');
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
protected function buildUpcomingSundayOptions(int $count): array
|
|
{
|
|
$start = new \DateTime('today');
|
|
$weekday = (int) $start->format('w');
|
|
if ($weekday !== 0) {
|
|
$start->modify('next sunday');
|
|
}
|
|
|
|
$options = [];
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$options[] = $start->format('Y-m-d');
|
|
$start->modify('+7 days');
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
protected function resolveProgressDateRange(): ?array
|
|
{
|
|
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
|
if ($schoolYear === '') {
|
|
return null;
|
|
}
|
|
|
|
$semesterResolver = new SemesterRangeService($this->configModel);
|
|
$semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? ''));
|
|
if ($semester === '') {
|
|
$semester = $semesterResolver->getSemesterForDate();
|
|
}
|
|
|
|
if ($semester !== '') {
|
|
$range = $semesterResolver->getSemesterRange($schoolYear, $semester);
|
|
if ($range !== null) {
|
|
return $range;
|
|
}
|
|
}
|
|
|
|
return $semesterResolver->getSchoolYearRange($schoolYear);
|
|
}
|
|
|
|
protected function pickDefaultWeekStart(array $options): string
|
|
{
|
|
if (empty($options)) {
|
|
return '';
|
|
}
|
|
|
|
$today = date('Y-m-d');
|
|
$default = '';
|
|
foreach ($options as $option) {
|
|
if ($option <= $today) {
|
|
$default = $option;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $default !== '' ? $default : $options[0];
|
|
}
|
|
|
|
protected function buildWeekEndFromStart(string $weekStart): string
|
|
{
|
|
try {
|
|
$dt = new \DateTime($weekStart);
|
|
$dt->modify('+6 days');
|
|
return $dt->format('Y-m-d');
|
|
} catch (\Exception $e) {
|
|
return $weekStart;
|
|
}
|
|
}
|
|
|
|
protected function storeAttachment($file): string
|
|
{
|
|
$this->ensureAttachmentPath();
|
|
$name = $file->getRandomName();
|
|
$file->move($this->attachmentStoragePath, $name);
|
|
return $this->attachmentPublicBase . $name;
|
|
}
|
|
|
|
protected function ensureAttachmentPath(): void
|
|
{
|
|
if (! is_dir($this->attachmentStoragePath)) {
|
|
mkdir($this->attachmentStoragePath, 0755, true);
|
|
}
|
|
}
|
|
|
|
protected function resolveCurrentTerm(): array
|
|
{
|
|
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
|
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
|
return [$semester, $schoolYear];
|
|
}
|
|
|
|
protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array
|
|
{
|
|
if (! $classSectionId || $schoolYear === '') {
|
|
return [];
|
|
}
|
|
$rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
|
|
$ids = [];
|
|
foreach ($rows as $row) {
|
|
$id = (int) ($row['teacher_id'] ?? 0);
|
|
if ($id > 0) {
|
|
$ids[$id] = true;
|
|
}
|
|
}
|
|
return array_keys($ids);
|
|
}
|
|
}
|