update code
This commit is contained in:
@@ -5,7 +5,10 @@ namespace App\Controllers;
|
|||||||
use App\Models\ClassProgressReportModel;
|
use App\Models\ClassProgressReportModel;
|
||||||
use App\Models\ClassProgressAttachmentModel;
|
use App\Models\ClassProgressAttachmentModel;
|
||||||
use App\Models\ClassSectionModel;
|
use App\Models\ClassSectionModel;
|
||||||
|
use App\Models\CalendarModel;
|
||||||
|
use App\Models\ConfigurationModel;
|
||||||
use App\Models\StudentClassModel;
|
use App\Models\StudentClassModel;
|
||||||
|
use App\Services\SemesterRangeService;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
|
||||||
@@ -15,6 +18,9 @@ class AdminProgressController extends BaseController
|
|||||||
protected ClassProgressAttachmentModel $attachmentModel;
|
protected ClassProgressAttachmentModel $attachmentModel;
|
||||||
protected ClassSectionModel $classSectionModel;
|
protected ClassSectionModel $classSectionModel;
|
||||||
protected StudentClassModel $studentClassModel;
|
protected StudentClassModel $studentClassModel;
|
||||||
|
protected CalendarModel $calendarModel;
|
||||||
|
protected ConfigurationModel $configModel;
|
||||||
|
protected SemesterRangeService $semesterRangeService;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -23,6 +29,9 @@ class AdminProgressController extends BaseController
|
|||||||
$this->attachmentModel = new ClassProgressAttachmentModel();
|
$this->attachmentModel = new ClassProgressAttachmentModel();
|
||||||
$this->classSectionModel = new ClassSectionModel();
|
$this->classSectionModel = new ClassSectionModel();
|
||||||
$this->studentClassModel = new StudentClassModel();
|
$this->studentClassModel = new StudentClassModel();
|
||||||
|
$this->calendarModel = new CalendarModel();
|
||||||
|
$this->configModel = new ConfigurationModel();
|
||||||
|
$this->semesterRangeService = new SemesterRangeService($this->configModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
@@ -53,22 +62,30 @@ class AdminProgressController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray();
|
$rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray();
|
||||||
$reportGroups = [];
|
$reportGroupsBySection = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
||||||
$key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? '');
|
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||||
if ($key === '_') {
|
$weekKey = (string) ($row['week_start'] ?? '');
|
||||||
|
if ($sectionId === 0 || $weekKey === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (! isset($reportGroups[$key])) {
|
if (! isset($reportGroupsBySection[$sectionId])) {
|
||||||
$reportGroups[$key] = [
|
$reportGroupsBySection[$sectionId] = [];
|
||||||
|
}
|
||||||
|
if (! isset($reportGroupsBySection[$sectionId][$weekKey])) {
|
||||||
|
$reportGroupsBySection[$sectionId][$weekKey] = [
|
||||||
'week_start' => $row['week_start'],
|
'week_start' => $row['week_start'],
|
||||||
'week_end' => $row['week_end'],
|
'week_end' => $row['week_end'],
|
||||||
'class_section_name' => $row['class_section_name'] ?? '',
|
'class_section_name' => $row['class_section_name'] ?? '',
|
||||||
'reports' => [],
|
'reports' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$reportGroups[$key]['reports'][$row['subject']] = $row;
|
$reportGroupsBySection[$sectionId][$weekKey]['reports'][$row['subject']] = $row;
|
||||||
|
}
|
||||||
|
foreach ($reportGroupsBySection as $sectionId => $groups) {
|
||||||
|
krsort($groups);
|
||||||
|
$reportGroupsBySection[$sectionId] = $groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
$classSections = $this->classSectionModel->getClassSections();
|
$classSections = $this->classSectionModel->getClassSections();
|
||||||
@@ -78,12 +95,27 @@ class AdminProgressController extends BaseController
|
|||||||
return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0;
|
return isset($studentCounts[$sectionId]) && $studentCounts[$sectionId] > 0;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
$filterStart = $this->normalizeDate($filters['from'] ?? '');
|
||||||
|
$filterEnd = $this->normalizeDate($filters['to'] ?? '');
|
||||||
|
[$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet] = $this->buildSemesterDates();
|
||||||
|
[$expectedDays, $activeDatesSet] = $this->resolveExpectedDays(
|
||||||
|
$dateList,
|
||||||
|
$noSchoolDays,
|
||||||
|
$totalPassedDays,
|
||||||
|
$passedDatesSet,
|
||||||
|
$filterStart,
|
||||||
|
$filterEnd
|
||||||
|
);
|
||||||
|
$sectionStats = $this->buildSectionSubmissionStats($rows, $activeDatesSet, $expectedDays);
|
||||||
|
|
||||||
return view('admin/class_progress_list', [
|
return view('admin/class_progress_list', [
|
||||||
'reportGroups' => $reportGroups,
|
'reportGroupsBySection' => $reportGroupsBySection,
|
||||||
'filters' => $filters,
|
'filters' => $filters,
|
||||||
'classSections' => $filteredSections,
|
'classSections' => $filteredSections,
|
||||||
'statusOptions' => ClassProgressController::STATUS_OPTIONS,
|
'statusOptions' => ClassProgressController::STATUS_OPTIONS,
|
||||||
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
|
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
|
||||||
|
'sectionStats' => $sectionStats,
|
||||||
|
'expectedDays' => $expectedDays,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,4 +243,214 @@ class AdminProgressController extends BaseController
|
|||||||
$flags = json_decode($json, true);
|
$flags = json_decode($json, true);
|
||||||
return is_array($flags) ? $flags : [];
|
return is_array($flags) ? $flags : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizeDate(string $value): string
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
[$y, $m, $d] = array_map('intval', explode('-', $value));
|
||||||
|
return checkdate($m, $d, $y) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSemesterDates(): array
|
||||||
|
{
|
||||||
|
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||||
|
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||||
|
$schoolYearForRange = $schoolYear !== '' ? $schoolYear : (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||||
|
[$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange);
|
||||||
|
$semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
|
||||||
|
if ($semesterNorm !== '' && $schoolYearForRange !== '') {
|
||||||
|
$semRange = $this->semesterRangeService->getSemesterRange($schoolYearForRange, $semesterNorm);
|
||||||
|
if ($semRange) {
|
||||||
|
[$rangeStart, $rangeEnd] = $semRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dateList = [];
|
||||||
|
try {
|
||||||
|
$start = new \DateTimeImmutable($rangeStart);
|
||||||
|
$end = new \DateTimeImmutable($rangeEnd);
|
||||||
|
$cursor = $start;
|
||||||
|
$w = (int) $cursor->format('w');
|
||||||
|
if ($w !== 0) {
|
||||||
|
$cursor = $cursor->modify('next sunday');
|
||||||
|
}
|
||||||
|
while ($cursor <= $end) {
|
||||||
|
$dateList[] = $cursor->format('Y-m-d');
|
||||||
|
$cursor = $cursor->modify('+7 days');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$dateList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$noSchoolDays = [];
|
||||||
|
$events = [];
|
||||||
|
try {
|
||||||
|
$events = $this->calendarModel->getEvents();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$events = [];
|
||||||
|
}
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$d = substr((string) ($event['date'] ?? ''), 0, 10);
|
||||||
|
if ($d === '' || empty($event['no_school'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($d < $rangeStart || $d > $rangeEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$eventYear = trim((string) ($event['school_year'] ?? ''));
|
||||||
|
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$noSchoolDays[$d] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anchorSundayYmd = '';
|
||||||
|
try {
|
||||||
|
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||||
|
$tzObj = new \DateTimeZone($tzName ?: 'UTC');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
try {
|
||||||
|
$tzObj = new \DateTimeZone(user_timezone() ?: 'UTC');
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
$tzObj = new \DateTimeZone('UTC');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$nowDate = new \DateTime('now', $tzObj);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$nowDate = new \DateTime('now');
|
||||||
|
}
|
||||||
|
$weekday = (int) $nowDate->format('w');
|
||||||
|
$anchorSundayYmd = $weekday === 0
|
||||||
|
? $nowDate->format('Y-m-d')
|
||||||
|
: $nowDate->modify('next sunday')->format('Y-m-d');
|
||||||
|
|
||||||
|
$passedDatesSet = [];
|
||||||
|
if (! empty($dateList) && $anchorSundayYmd !== '') {
|
||||||
|
foreach ($dateList as $d) {
|
||||||
|
if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
|
||||||
|
$passedDatesSet[$d] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$totalPassedDays = count($passedDatesSet);
|
||||||
|
|
||||||
|
return [$dateList, $noSchoolDays, $totalPassedDays, $passedDatesSet];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveExpectedDays(
|
||||||
|
array $dateList,
|
||||||
|
array $noSchoolDays,
|
||||||
|
int $totalPassedDays,
|
||||||
|
array $passedDatesSet,
|
||||||
|
string $filterStart,
|
||||||
|
string $filterEnd
|
||||||
|
): array {
|
||||||
|
$activeDatesSet = [];
|
||||||
|
if ($filterStart === '' && $filterEnd === '') {
|
||||||
|
$expectedDays = $totalPassedDays;
|
||||||
|
if ($expectedDays > 0) {
|
||||||
|
$activeDatesSet = $passedDatesSet;
|
||||||
|
}
|
||||||
|
return [$expectedDays, $activeDatesSet];
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedDays = 0;
|
||||||
|
foreach ($dateList as $d) {
|
||||||
|
if ($d === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($filterStart !== '' && $d < $filterStart) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($filterEnd !== '' && $d > $filterEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! empty($noSchoolDays[$d])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$activeDatesSet[$d] = true;
|
||||||
|
$expectedDays++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$expectedDays, $activeDatesSet];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSectionSubmissionStats(array $rows, array $activeDatesSet, int $expectedDays): array
|
||||||
|
{
|
||||||
|
$requiredSubjects = [];
|
||||||
|
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
|
||||||
|
$requiredSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
|
||||||
|
}
|
||||||
|
$requiredSubjects = array_values(array_filter($requiredSubjects));
|
||||||
|
|
||||||
|
$submittedBySection = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||||
|
$weekStart = (string) ($row['week_start'] ?? '');
|
||||||
|
$subject = (string) ($row['subject'] ?? '');
|
||||||
|
if ($sectionId === 0 || $weekStart === '' || $subject === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! empty($activeDatesSet) && empty($activeDatesSet[$weekStart])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! in_array($subject, $requiredSubjects, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$submittedBySection[$sectionId][$weekStart][$subject] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = [];
|
||||||
|
foreach ($submittedBySection as $sectionId => $weeks) {
|
||||||
|
$submitted = 0;
|
||||||
|
foreach ($weeks as $subjects) {
|
||||||
|
$all = true;
|
||||||
|
foreach ($requiredSubjects as $subject) {
|
||||||
|
if (empty($subjects[$subject])) {
|
||||||
|
$all = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($all) {
|
||||||
|
$submitted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSectionStat(int $submitted, int $expectedDays): array
|
||||||
|
{
|
||||||
|
$percent = $expectedDays > 0 ? round(($submitted * 100) / $expectedDays, 1) : 0.0;
|
||||||
|
if ($expectedDays === 0) {
|
||||||
|
$badgeClass = 'bg-secondary';
|
||||||
|
$labelClass = 'text-muted';
|
||||||
|
} elseif ($percent >= 100) {
|
||||||
|
$badgeClass = 'bg-success';
|
||||||
|
$labelClass = 'text-success';
|
||||||
|
} elseif ($percent >= 60) {
|
||||||
|
$badgeClass = 'bg-warning text-dark';
|
||||||
|
$labelClass = 'text-warning';
|
||||||
|
} elseif ($percent >= 40) {
|
||||||
|
$badgeClass = 'bg-orange text-dark';
|
||||||
|
$labelClass = 'text-warning';
|
||||||
|
} else {
|
||||||
|
$badgeClass = 'bg-danger';
|
||||||
|
$labelClass = 'text-danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'submitted' => $submitted,
|
||||||
|
'expected' => $expectedDays,
|
||||||
|
'percent' => $percent,
|
||||||
|
'badgeClass' => $badgeClass,
|
||||||
|
'labelClass' => $labelClass,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Models\ClassProgressAttachmentModel;
|
|||||||
use App\Models\ConfigurationModel;
|
use App\Models\ConfigurationModel;
|
||||||
use App\Models\SubjectCurriculumModel;
|
use App\Models\SubjectCurriculumModel;
|
||||||
use App\Models\TeacherClassModel;
|
use App\Models\TeacherClassModel;
|
||||||
|
use App\Services\SemesterRangeService;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
|
|
||||||
class ClassProgressController extends BaseController
|
class ClassProgressController extends BaseController
|
||||||
@@ -56,6 +57,7 @@ class ClassProgressController extends BaseController
|
|||||||
$classSectionName = $first['class_section_name'] ?? null;
|
$classSectionName = $first['class_section_name'] ?? null;
|
||||||
$classId = $first['class_id'] ?? null;
|
$classId = $first['class_id'] ?? null;
|
||||||
$sundayOptions = $this->buildSundayOptions();
|
$sundayOptions = $this->buildSundayOptions();
|
||||||
|
$defaultWeekStart = $this->pickDefaultWeekStart($sundayOptions);
|
||||||
$subjectCurriculum = [];
|
$subjectCurriculum = [];
|
||||||
if ($classId) {
|
if ($classId) {
|
||||||
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
foreach (self::SUBJECT_SECTIONS as $slug => $section) {
|
||||||
@@ -69,7 +71,7 @@ class ClassProgressController extends BaseController
|
|||||||
'classSectionName' => $classSectionName,
|
'classSectionName' => $classSectionName,
|
||||||
'classId' => $classId,
|
'classId' => $classId,
|
||||||
'sundayOptions' => $sundayOptions,
|
'sundayOptions' => $sundayOptions,
|
||||||
'defaultWeekStart' => $sundayOptions[0] ?? '',
|
'defaultWeekStart' => $defaultWeekStart,
|
||||||
];
|
];
|
||||||
return view('teacher/class_progress_submit', $data);
|
return view('teacher/class_progress_submit', $data);
|
||||||
}
|
}
|
||||||
@@ -171,10 +173,16 @@ class ClassProgressController extends BaseController
|
|||||||
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
|
if ($selectedSectionId && ! in_array($selectedSectionId, $validSectionIds, true)) {
|
||||||
$selectedSectionId = $validSectionIds[0] ?? null;
|
$selectedSectionId = $validSectionIds[0] ?? null;
|
||||||
}
|
}
|
||||||
|
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
||||||
|
$allowedTeacherIds = $this->resolveAssignedTeacherIds($selectedSectionId, $semester, $schoolYear);
|
||||||
|
if (empty($allowedTeacherIds)) {
|
||||||
|
$allowedTeacherIds = [$teacherId];
|
||||||
|
}
|
||||||
$builder = $this->reportModel
|
$builder = $this->reportModel
|
||||||
->select('class_progress_reports.*, cs.class_section_name')
|
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
|
||||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||||
->where('teacher_id', $teacherId);
|
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||||
|
->whereIn('teacher_id', $allowedTeacherIds);
|
||||||
if ($selectedSectionId) {
|
if ($selectedSectionId) {
|
||||||
$builder->where('class_progress_reports.class_section_id', $selectedSectionId);
|
$builder->where('class_progress_reports.class_section_id', $selectedSectionId);
|
||||||
}
|
}
|
||||||
@@ -216,20 +224,33 @@ class ClassProgressController extends BaseController
|
|||||||
{
|
{
|
||||||
$teacherId = (int) session()->get('user_id');
|
$teacherId = (int) session()->get('user_id');
|
||||||
$row = $this->reportModel
|
$row = $this->reportModel
|
||||||
->select('class_progress_reports.*, cs.class_section_name')
|
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
|
||||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||||
->where('teacher_id', $teacherId)
|
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||||
->find((int) $id);
|
->where('class_progress_reports.id', (int) $id)
|
||||||
|
->first();
|
||||||
|
|
||||||
if (! $row) {
|
if (! $row) {
|
||||||
throw new PageNotFoundException('Progress report not found.');
|
throw new PageNotFoundException('Progress report not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$semester, $schoolYear] = $this->resolveCurrentTerm();
|
||||||
|
$allowedTeacherIds = $this->resolveAssignedTeacherIds((int) $row['class_section_id'], $semester, $schoolYear);
|
||||||
|
if (empty($allowedTeacherIds)) {
|
||||||
|
if ($teacherId !== (int) $row['teacher_id']) {
|
||||||
|
throw new PageNotFoundException('Progress report not found.');
|
||||||
|
}
|
||||||
|
$allowedTeacherIds = [(int) $row['teacher_id']];
|
||||||
|
} elseif (! in_array($teacherId, $allowedTeacherIds, true)) {
|
||||||
|
throw new PageNotFoundException('Progress report not found.');
|
||||||
|
}
|
||||||
|
|
||||||
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
$row['status_label'] = self::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
||||||
$weeklyReports = $this->reportModel
|
$weeklyReports = $this->reportModel
|
||||||
->select('class_progress_reports.*, cs.class_section_name')
|
->select('class_progress_reports.*, cs.class_section_name, CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name')
|
||||||
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
->join('classSection cs', 'cs.class_section_id = class_progress_reports.class_section_id', 'left')
|
||||||
->where('teacher_id', $teacherId)
|
->join('users u', 'u.id = class_progress_reports.teacher_id', 'left')
|
||||||
|
->whereIn('teacher_id', $allowedTeacherIds)
|
||||||
->where('class_progress_reports.class_section_id', $row['class_section_id'])
|
->where('class_progress_reports.class_section_id', $row['class_section_id'])
|
||||||
->where('week_start', $row['week_start'])
|
->where('week_start', $row['week_start'])
|
||||||
->orderBy('subject', 'ASC')
|
->orderBy('subject', 'ASC')
|
||||||
@@ -427,6 +448,39 @@ class ClassProgressController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function buildSundayOptions(int $count = 12): array
|
protected function buildSundayOptions(int $count = 12): array
|
||||||
|
{
|
||||||
|
$range = $this->resolveProgressDateRange();
|
||||||
|
if ($range === null) {
|
||||||
|
return $this->buildUpcomingSundayOptions($count);
|
||||||
|
}
|
||||||
|
[$rangeStart, $rangeEnd] = $range;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$start = new \DateTime($rangeStart);
|
||||||
|
$end = new \DateTime($rangeEnd);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->buildUpcomingSundayOptions($count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end < $start) {
|
||||||
|
[$start, $end] = [$end, $start];
|
||||||
|
}
|
||||||
|
|
||||||
|
$weekday = (int) $start->format('w');
|
||||||
|
if ($weekday !== 0) {
|
||||||
|
$start->modify('next sunday');
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
while ($start <= $end) {
|
||||||
|
$options[] = $start->format('Y-m-d');
|
||||||
|
$start->modify('+7 days');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildUpcomingSundayOptions(int $count): array
|
||||||
{
|
{
|
||||||
$start = new \DateTime('today');
|
$start = new \DateTime('today');
|
||||||
$weekday = (int) $start->format('w');
|
$weekday = (int) $start->format('w');
|
||||||
@@ -443,6 +497,48 @@ class ClassProgressController extends BaseController
|
|||||||
return $options;
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function resolveProgressDateRange(): ?array
|
||||||
|
{
|
||||||
|
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||||
|
if ($schoolYear === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$semesterResolver = new SemesterRangeService($this->configModel);
|
||||||
|
$semester = $semesterResolver->normalizeSemester((string) ($this->configModel->getConfig('semester') ?? ''));
|
||||||
|
if ($semester === '') {
|
||||||
|
$semester = $semesterResolver->getSemesterForDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semester !== '') {
|
||||||
|
$range = $semesterResolver->getSemesterRange($schoolYear, $semester);
|
||||||
|
if ($range !== null) {
|
||||||
|
return $range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $semesterResolver->getSchoolYearRange($schoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function pickDefaultWeekStart(array $options): string
|
||||||
|
{
|
||||||
|
if (empty($options)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$default = '';
|
||||||
|
foreach ($options as $option) {
|
||||||
|
if ($option <= $today) {
|
||||||
|
$default = $option;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default !== '' ? $default : $options[0];
|
||||||
|
}
|
||||||
|
|
||||||
protected function buildWeekEndFromStart(string $weekStart): string
|
protected function buildWeekEndFromStart(string $weekStart): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -468,4 +564,27 @@ class ClassProgressController extends BaseController
|
|||||||
mkdir($this->attachmentStoragePath, 0755, true);
|
mkdir($this->attachmentStoragePath, 0755, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function resolveCurrentTerm(): array
|
||||||
|
{
|
||||||
|
$schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||||
|
$semester = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||||
|
return [$semester, $schoolYear];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveAssignedTeacherIds(?int $classSectionId, string $semester, string $schoolYear): array
|
||||||
|
{
|
||||||
|
if (! $classSectionId || $schoolYear === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$rows = $this->teacherClassModel->assignedForSectionTerm($classSectionId, $semester, $schoolYear);
|
||||||
|
$ids = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$id = (int) ($row['teacher_id'] ?? 0);
|
||||||
|
if ($id > 0) {
|
||||||
|
$ids[$id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_keys($ids);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -797,13 +797,9 @@ public function showUpdateAttendanceForm()
|
|||||||
}
|
}
|
||||||
|
|
||||||
$noSchoolDays = [];
|
$noSchoolDays = [];
|
||||||
if ($schoolYearForRange !== '') {
|
|
||||||
$events = [];
|
$events = [];
|
||||||
try {
|
try {
|
||||||
$events = $this->calendarModel->getEventsBySchoolYearAndSemester(
|
$events = $this->calendarModel->getEvents();
|
||||||
$schoolYearForRange,
|
|
||||||
$semesterNorm !== '' ? $semesterNorm : null
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$events = [];
|
$events = [];
|
||||||
}
|
}
|
||||||
@@ -815,8 +811,11 @@ public function showUpdateAttendanceForm()
|
|||||||
if ($d < $rangeStart || $d > $rangeEnd) {
|
if ($d < $rangeStart || $d > $rangeEnd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$noSchoolDays[$d] = true;
|
$eventYear = trim((string)($event['school_year'] ?? ''));
|
||||||
|
if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
$noSchoolDays[$d] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Total passed attendance days up to this Sunday (inclusive), excluding no-school days.
|
// Total passed attendance days up to this Sunday (inclusive), excluding no-school days.
|
||||||
|
|||||||
@@ -60,13 +60,15 @@ class ClassPreparationController extends BaseController
|
|||||||
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
|
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
|
||||||
|
|
||||||
// 1) Get student count per class-section (distinct students, correct semester)
|
// 1) Get student count per class-section (distinct students, correct semester)
|
||||||
$scQ = $this->studentClassModel
|
$scQ = $this->db->table('student_class sc')
|
||||||
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count')
|
->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
|
||||||
->where('school_year', $schoolYear);
|
->join('students s', 's.id = sc.student_id', 'inner')
|
||||||
|
->where('s.is_active', 1)
|
||||||
|
->where('sc.school_year', $schoolYear);
|
||||||
if ($limitToSemester && $semester !== '') {
|
if ($limitToSemester && $semester !== '') {
|
||||||
$scQ->where('semester', $semester);
|
$scQ->where('sc.semester', $semester);
|
||||||
}
|
}
|
||||||
$classSections = $scQ->groupBy('class_section_id')->findAll();
|
$classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
|
||||||
|
|
||||||
// 2) Inventory availability — prefer good_qty when present, else condition='good' quantity.
|
// 2) Inventory availability — prefer good_qty when present, else condition='good' quantity.
|
||||||
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
|
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
|
||||||
@@ -298,14 +300,16 @@ class ClassPreparationController extends BaseController
|
|||||||
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
|
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
|
||||||
|
|
||||||
// Distinct student count for this term
|
// Distinct student count for this term
|
||||||
$studentQ = $this->studentClassModel
|
$studentQ = $this->db->table('student_class sc')
|
||||||
->select('COUNT(DISTINCT student_id) AS cnt')
|
->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
|
||||||
->where('class_section_id', $classSectionId)
|
->join('students s', 's.id = sc.student_id', 'inner')
|
||||||
->where('school_year', $schoolYear);
|
->where('s.is_active', 1)
|
||||||
|
->where('sc.class_section_id', $classSectionId)
|
||||||
|
->where('sc.school_year', $schoolYear);
|
||||||
if ($limitToSemester && $semester !== '') {
|
if ($limitToSemester && $semester !== '') {
|
||||||
$studentQ->where('semester', $semester);
|
$studentQ->where('sc.semester', $semester);
|
||||||
}
|
}
|
||||||
$studentRow = $studentQ->first();
|
$studentRow = $studentQ->get()->getRowArray();
|
||||||
$studentCount = (int)($studentRow['cnt'] ?? 0);
|
$studentCount = (int)($studentRow['cnt'] ?? 0);
|
||||||
|
|
||||||
// Live calc + adjustments
|
// Live calc + adjustments
|
||||||
@@ -355,13 +359,15 @@ class ClassPreparationController extends BaseController
|
|||||||
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
|
$limitToSemester = $this->hasRosterForSemester($schoolYear, $semester);
|
||||||
|
|
||||||
// Student counts
|
// Student counts
|
||||||
$scQ = $this->studentClassModel
|
$scQ = $this->db->table('student_class sc')
|
||||||
->select('class_section_id, COUNT(DISTINCT student_id) AS student_count')
|
->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
|
||||||
->where('school_year', $schoolYear);
|
->join('students s', 's.id = sc.student_id', 'inner')
|
||||||
|
->where('s.is_active', 1)
|
||||||
|
->where('sc.school_year', $schoolYear);
|
||||||
if ($limitToSemester && $semester !== '') {
|
if ($limitToSemester && $semester !== '') {
|
||||||
$scQ->where('semester', $semester);
|
$scQ->where('sc.semester', $semester);
|
||||||
}
|
}
|
||||||
$classSections = $scQ->groupBy('class_section_id')->findAll();
|
$classSections = $scQ->groupBy('sc.class_section_id')->get()->getResultArray();
|
||||||
|
|
||||||
// Build inventory availability maps
|
// Build inventory availability maps
|
||||||
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
|
$inventoryMap = $this->buildInventoryAvailability($schoolYear, $semester, $limitToSemester, $allowed);
|
||||||
@@ -448,14 +454,16 @@ class ClassPreparationController extends BaseController
|
|||||||
$now = utc_now();
|
$now = utc_now();
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($ids as $classSectionId) {
|
foreach ($ids as $classSectionId) {
|
||||||
$studentQ = $this->studentClassModel
|
$studentQ = $this->db->table('student_class sc')
|
||||||
->select('COUNT(DISTINCT student_id) AS cnt')
|
->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
|
||||||
->where('class_section_id', $classSectionId)
|
->join('students s', 's.id = sc.student_id', 'inner')
|
||||||
->where('school_year', $schoolYear);
|
->where('s.is_active', 1)
|
||||||
|
->where('sc.class_section_id', $classSectionId)
|
||||||
|
->where('sc.school_year', $schoolYear);
|
||||||
if ($limitToSemester && $semester !== '') {
|
if ($limitToSemester && $semester !== '') {
|
||||||
$studentQ->where('semester', $semester);
|
$studentQ->where('sc.semester', $semester);
|
||||||
}
|
}
|
||||||
$studentRow = $studentQ->first();
|
$studentRow = $studentQ->get()->getRowArray();
|
||||||
$studentCount = (int)($studentRow['cnt'] ?? 0);
|
$studentCount = (int)($studentRow['cnt'] ?? 0);
|
||||||
$classLevel = $this->getClassLevelBySection((string)$classSectionId);
|
$classLevel = $this->getClassLevelBySection((string)$classSectionId);
|
||||||
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
|
$className = $this->classSectionModel->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
|
||||||
|
|||||||
@@ -71,12 +71,15 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
->orderBy('s.lastname', 'ASC')
|
->orderBy('s.lastname', 'ASC')
|
||||||
->get()->getResultArray();
|
->get()->getResultArray();
|
||||||
|
|
||||||
|
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
|
||||||
|
|
||||||
return view('parent/report_attendance', [
|
return view('parent/report_attendance', [
|
||||||
'students' => $students,
|
'students' => $students,
|
||||||
'today' => local_date(utc_now(), 'Y-m-d'),
|
'today' => local_date(utc_now(), 'Y-m-d'),
|
||||||
'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items
|
'sundays' => $sundays, // array of ['value'=>Y-m-d, 'label'=>...] items
|
||||||
'defaultDate' => $defaultDate, // preselected date (upcoming Sunday)
|
'defaultDate' => $defaultDate, // preselected date (upcoming Sunday)
|
||||||
'myReports' => $previewRows,
|
'myReports' => $previewRows,
|
||||||
|
'cutoffThreshold' => $cutoffThreshold,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +144,17 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
|
|
||||||
// Enforce Sunday-only and future-or-today selection
|
// Enforce Sunday-only and future-or-today selection
|
||||||
$todayCheck = new \DateTime('today');
|
$todayCheck = new \DateTime('today');
|
||||||
|
$cutoffThreshold = $this->normalizeCutoffTime((string) $this->configModel->getConfig('parent_report_cutoff'));
|
||||||
|
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||||
|
$tz = null;
|
||||||
|
$nowTz = null;
|
||||||
|
try {
|
||||||
|
$tz = new \DateTimeZone($tzName ?: 'UTC');
|
||||||
|
$nowTz = new \DateTime('now', $tz);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$tz = null;
|
||||||
|
$nowTz = null;
|
||||||
|
}
|
||||||
$validDates = [];
|
$validDates = [];
|
||||||
|
|
||||||
$cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year'));
|
$cap = $this->firstSundayOfJune((string) $this->configModel->getConfig('school_year'));
|
||||||
@@ -150,9 +164,11 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
$lateTodayStr = $todayCheck->format('Y-m-d');
|
$lateTodayStr = $todayCheck->format('Y-m-d');
|
||||||
if ($type === 'late') {
|
if ($type === 'late') {
|
||||||
try {
|
try {
|
||||||
|
if (!$tz) {
|
||||||
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||||
$tz = new \DateTimeZone($tzName ?: 'UTC');
|
$tz = new \DateTimeZone($tzName ?: 'UTC');
|
||||||
$lateNow = new \DateTime('now', $tz);
|
}
|
||||||
|
$lateNow = $nowTz ?: new \DateTime('now', $tz);
|
||||||
$cm = $this->configModel;
|
$cm = $this->configModel;
|
||||||
$cutoffStr = (string) ($cm->getConfig('late_report_cutoff')
|
$cutoffStr = (string) ($cm->getConfig('late_report_cutoff')
|
||||||
?: $cm->getConfig('late_cutoff_time')
|
?: $cm->getConfig('late_cutoff_time')
|
||||||
@@ -183,6 +199,17 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
if ($dt < $todayCheck) {
|
if ($dt < $todayCheck) {
|
||||||
return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.');
|
return redirect()->back()->withInput()->with('error', 'Past dates are not allowed. Please select today or a future Sunday.');
|
||||||
}
|
}
|
||||||
|
if ($nowTz && $tz) {
|
||||||
|
try {
|
||||||
|
$cutoff = new \DateTime($entry . ' ' . $cutoffThreshold . ':00', $tz);
|
||||||
|
if ($nowTz >= $cutoff) {
|
||||||
|
$abbr = $cutoff->format('T');
|
||||||
|
return redirect()->back()->withInput()->with('error', 'Reporting closes at ' . $cutoffThreshold . ' ' . $abbr . ' on the report date.');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// ignore and continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($dt > $cap) {
|
if ($dt > $cap) {
|
||||||
return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.');
|
return redirect()->back()->withInput()->with('error', 'The last available date is the first Sunday of June.');
|
||||||
}
|
}
|
||||||
@@ -800,6 +827,36 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
$update['reported'] = 1;
|
$update['reported'] = 1;
|
||||||
}
|
}
|
||||||
$this->attendanceDataModel->update((int) $existing['id'], $update);
|
$this->attendanceDataModel->update((int) $existing['id'], $update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existingStatus === '' || $existingStatus === 'present') {
|
||||||
|
$update = [
|
||||||
|
'status' => $status,
|
||||||
|
'reason' => $existing['reason'] ?: $reasonText,
|
||||||
|
'updated_at' => utc_now(),
|
||||||
|
];
|
||||||
|
if ($hasIsReported) {
|
||||||
|
$update['is_reported'] = 'yes';
|
||||||
|
}
|
||||||
|
if ($hasLegacyReported) {
|
||||||
|
$update['reported'] = 1;
|
||||||
|
}
|
||||||
|
$this->attendanceDataModel->update((int) $existing['id'], $update);
|
||||||
|
|
||||||
|
$classSectionId = (int) ($existing['class_section_id'] ?? $classSectionId ?? 0);
|
||||||
|
$schoolId = (string) ($existing['school_id'] ?? '');
|
||||||
|
if ($classSectionId > 0 && $schoolId !== '') {
|
||||||
|
$this->adjustAttendanceRecordForStatusChange(
|
||||||
|
studentId: $studentId,
|
||||||
|
classSectionId: $classSectionId,
|
||||||
|
schoolId: $schoolId,
|
||||||
|
semester: $semester,
|
||||||
|
schoolYear: $schoolYear,
|
||||||
|
oldStatus: $existingStatus,
|
||||||
|
newStatus: $status
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -856,6 +913,61 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
$this->attendanceRecordModel->insert($insert);
|
$this->attendanceRecordModel->insert($insert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function adjustAttendanceRecordForStatusChange(
|
||||||
|
int $studentId,
|
||||||
|
int $classSectionId,
|
||||||
|
string $schoolId,
|
||||||
|
string $semester,
|
||||||
|
string $schoolYear,
|
||||||
|
string $oldStatus,
|
||||||
|
string $newStatus
|
||||||
|
): void {
|
||||||
|
$oldStatus = strtolower($oldStatus);
|
||||||
|
$newStatus = strtolower($newStatus);
|
||||||
|
|
||||||
|
if ($oldStatus === $newStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = $this->attendanceRecordModel->where([
|
||||||
|
'student_id' => $studentId,
|
||||||
|
'semester' => $semester,
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
])->first();
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
$this->bumpAttendanceRecordFromParent($studentId, $classSectionId, $schoolId, $newStatus, $semester, $schoolYear);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updates = [
|
||||||
|
'total_presence' => (int) ($record['total_presence'] ?? 0),
|
||||||
|
'total_absence' => (int) ($record['total_absence'] ?? 0),
|
||||||
|
'total_late' => (int) ($record['total_late'] ?? 0),
|
||||||
|
'total_attendance' => (int) ($record['total_attendance'] ?? 0),
|
||||||
|
'updated_at' => utc_now(),
|
||||||
|
'modified_by' => (int) (session()->get('user_id') ?? 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($oldStatus === 'present') {
|
||||||
|
$updates['total_presence'] = max(0, $updates['total_presence'] - 1);
|
||||||
|
} elseif ($oldStatus === 'absent') {
|
||||||
|
$updates['total_absence'] = max(0, $updates['total_absence'] - 1);
|
||||||
|
} elseif ($oldStatus === 'late') {
|
||||||
|
$updates['total_late'] = max(0, $updates['total_late'] - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newStatus === 'present') {
|
||||||
|
$updates['total_presence']++;
|
||||||
|
} elseif ($newStatus === 'absent') {
|
||||||
|
$updates['total_absence']++;
|
||||||
|
} elseif ($newStatus === 'late') {
|
||||||
|
$updates['total_late']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attendanceRecordModel->update((int) $record['id'], $updates);
|
||||||
|
}
|
||||||
|
|
||||||
// GET: staff view to list reports (today & upcoming by default)
|
// GET: staff view to list reports (today & upcoming by default)
|
||||||
public function list()
|
public function list()
|
||||||
{
|
{
|
||||||
@@ -1503,6 +1615,20 @@ class ParentAttendanceReportController extends BaseController
|
|||||||
return $userId; // fallback
|
return $userId; // fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeCutoffTime(string $raw, string $default = '09:00'): string
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
if (preg_match('/^(\d{1,2}):(\d{2})$/', $raw, $m)) {
|
||||||
|
$hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT);
|
||||||
|
$mm = $m[2];
|
||||||
|
return $hh . ':' . $mm;
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send confirmation/notification emails for parent attendance submissions.
|
* Send confirmation/notification emails for parent attendance submissions.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
<?= $this->extend('layout/management_layout') ?>
|
<?= $this->extend('layout/management_layout') ?>
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<style>
|
<style>
|
||||||
#adminProgressTable thead th {
|
.admin-progress-table thead th {
|
||||||
position: static !important;
|
position: static !important;
|
||||||
|
top: auto !important;
|
||||||
|
z-index: auto !important;
|
||||||
|
background: #f8f9fa;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-top: .75rem;
|
||||||
|
padding-bottom: .75rem;
|
||||||
|
}
|
||||||
|
.admin-progress-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.bg-orange {
|
||||||
|
background-color: #fd7e14 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
@@ -56,9 +69,68 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="card shadow-sm mt-5">
|
<div class="card shadow-sm mt-5">
|
||||||
<div class="card-body pt-4 px-0">
|
<div class="card-body pt-4">
|
||||||
<div class="table-responsive px-4" style="margin-top: 1.5rem;">
|
<?php
|
||||||
<table id="adminProgressTable" class="table table-hover align-middle mb-0">
|
$reportGroupsBySection = $reportGroupsBySection ?? [];
|
||||||
|
$subjectSections = $subjectSections ?? [];
|
||||||
|
$classSections = $classSections ?? [];
|
||||||
|
$sectionStats = $sectionStats ?? [];
|
||||||
|
$expectedDays = (int) ($expectedDays ?? 0);
|
||||||
|
?>
|
||||||
|
<?php if (empty($classSections)): ?>
|
||||||
|
<div class="text-center text-muted py-4">No class sections found.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="accordion" id="adminProgressAccordion">
|
||||||
|
<?php foreach ($classSections as $index => $cs): ?>
|
||||||
|
<?php
|
||||||
|
$sectionId = (int) ($cs['class_section_id'] ?? 0);
|
||||||
|
$sectionName = $cs['class_section_name'] ?? 'Unknown Section';
|
||||||
|
$sectionGroups = $reportGroupsBySection[$sectionId] ?? [];
|
||||||
|
$hasReports = ! empty($sectionGroups);
|
||||||
|
$collapseId = 'progress-section-' . $sectionId;
|
||||||
|
$headingId = 'progress-heading-' . $sectionId;
|
||||||
|
$stat = $sectionStats[$sectionId] ?? null;
|
||||||
|
if (! $stat) {
|
||||||
|
if ($expectedDays === 0) {
|
||||||
|
$stat = [
|
||||||
|
'submitted' => 0,
|
||||||
|
'expected' => 0,
|
||||||
|
'percent' => 0,
|
||||||
|
'badgeClass' => 'bg-secondary',
|
||||||
|
'labelClass' => 'text-muted',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$stat = [
|
||||||
|
'submitted' => 0,
|
||||||
|
'expected' => $expectedDays,
|
||||||
|
'percent' => 0,
|
||||||
|
'badgeClass' => 'bg-danger',
|
||||||
|
'labelClass' => 'text-danger',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$percentLabel = $expectedDays > 0 ? number_format((float) $stat['percent'], 1) . '%' : 'N/A';
|
||||||
|
$submissionLabel = $expectedDays > 0
|
||||||
|
? ('Submitted: ' . (int) $stat['submitted'] . ' / ' . (int) $expectedDays . ' (' . $percentLabel . ')')
|
||||||
|
: 'Submitted: N/A';
|
||||||
|
?>
|
||||||
|
<div class="accordion-item mb-2">
|
||||||
|
<h2 class="accordion-header" id="<?= esc($headingId) ?>">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||||
|
<?= esc($sectionName) ?>
|
||||||
|
<span class="badge <?= esc($stat['badgeClass']) ?> ms-2"><?= esc($submissionLabel) ?></span>
|
||||||
|
<?php if ($hasReports): ?>
|
||||||
|
<span class="badge bg-secondary ms-2"><?= count($sectionGroups) ?> weeks</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="<?= esc($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= esc($headingId) ?>" data-bs-parent="#adminProgressAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<?php if (! $hasReports): ?>
|
||||||
|
<div class="text-muted">No reports found for this section.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0 admin-progress-table no-mgmt-sticky" data-no-mgmt-sticky>
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Week</th>
|
<th>Week</th>
|
||||||
@@ -68,14 +140,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
<?php foreach ($sectionGroups as $group): ?>
|
||||||
$reportGroups = $reportGroups ?? [];
|
|
||||||
$subjectSections = $subjectSections ?? [];
|
|
||||||
?>
|
|
||||||
<?php if (empty($reportGroups)): ?>
|
|
||||||
<tr><td colspan="4" class="text-center text-muted py-4">No reports found.</td></tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($reportGroups as $group): ?>
|
|
||||||
<?php
|
<?php
|
||||||
$weekLabel = $group['week_start'] ? date('M d, Y', strtotime($group['week_start'])) : '-';
|
$weekLabel = $group['week_start'] ? date('M d, Y', strtotime($group['week_start'])) : '-';
|
||||||
if (! empty($group['week_end'])) {
|
if (! empty($group['week_end'])) {
|
||||||
@@ -92,12 +157,7 @@
|
|||||||
$exampleId = $reports ? reset($reports)['id'] : null;
|
$exampleId = $reports ? reset($reports)['id'] : null;
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td><div class="fw-semibold"><?= esc($weekLabel) ?></div></td>
|
||||||
<div class="fw-semibold"><?= esc($weekLabel) ?></div>
|
|
||||||
<?php if (!empty($group['class_section_name'])): ?>
|
|
||||||
<div class="text-muted small">Class: <?= esc($group['class_section_name']) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-column gap-2">
|
<div class="d-flex flex-column gap-2">
|
||||||
<?php foreach ($subjectSections as $slug => $section): ?>
|
<?php foreach ($subjectSections as $slug => $section): ?>
|
||||||
@@ -125,10 +185,16 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -122,15 +122,40 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<?php
|
||||||
|
$cutoffDate = '';
|
||||||
|
$cutoffTime = '';
|
||||||
|
$cutoffThreshold = (string) ($cutoffThreshold ?? '09:00');
|
||||||
|
$cutoffTzAbbr = '';
|
||||||
|
try {
|
||||||
|
$tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
|
||||||
|
$tz = new DateTimeZone($tzName ?: 'UTC');
|
||||||
|
$nowTz = new DateTime('now', $tz);
|
||||||
|
$cutoffDate = $nowTz->format('Y-m-d');
|
||||||
|
$cutoffTime = $nowTz->format('H:i');
|
||||||
|
$cutoffTzAbbr = $nowTz->format('T');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$cutoffDate = '';
|
||||||
|
$cutoffTime = '';
|
||||||
|
$cutoffTzAbbr = '';
|
||||||
|
}
|
||||||
|
?>
|
||||||
<form id="reportForm"
|
<form id="reportForm"
|
||||||
method="post"
|
method="post"
|
||||||
action="<?= site_url('parent/report-attendance') ?>"
|
action="<?= site_url('parent/report-attendance') ?>"
|
||||||
data-csrf-name="<?= esc(csrf_token()) ?>"
|
data-csrf-name="<?= esc(csrf_token()) ?>"
|
||||||
data-csrf-value="<?= esc(csrf_hash()) ?>"
|
data-csrf-value="<?= esc(csrf_hash()) ?>"
|
||||||
data-check-url="<?= site_url('api/parent/report-attendance/check') ?>">
|
data-check-url="<?= site_url('api/parent/report-attendance/check') ?>"
|
||||||
|
data-cutoff-date="<?= esc($cutoffDate) ?>"
|
||||||
|
data-cutoff-time="<?= esc($cutoffTime) ?>"
|
||||||
|
data-cutoff-threshold="<?= esc($cutoffThreshold) ?>"
|
||||||
|
data-cutoff-blocked="0">
|
||||||
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
|
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>">
|
||||||
|
|
||||||
<div id="clientCheckAlert" class="alert d-none" role="alert"></div>
|
<div id="clientCheckAlert" class="alert d-none" role="alert"></div>
|
||||||
|
<div id="cutoffAlert" class="alert alert-warning d-none" role="alert">
|
||||||
|
Reporting closes at <?= esc($cutoffThreshold) ?><?= $cutoffTzAbbr !== '' ? ' ' . esc($cutoffTzAbbr) : '' ?> on the report date.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Select Child(ren)</label>
|
<label class="form-label fw-semibold">Select Child(ren)</label>
|
||||||
@@ -352,7 +377,10 @@
|
|||||||
const dates = selectedDates();
|
const dates = selectedDates();
|
||||||
if (!checkUrl || !dateSel || !typeSel) return;
|
if (!checkUrl || !dateSel || !typeSel) return;
|
||||||
if (!ids.length || !dates.length) {
|
if (!ids.length || !dates.length) {
|
||||||
if (submitBtn) submitBtn.disabled = false;
|
if (submitBtn) {
|
||||||
|
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
|
||||||
|
submitBtn.disabled = cutoffBlocked;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +423,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !data.ok) {
|
if (!data || !data.ok) {
|
||||||
if (submitBtn) submitBtn.disabled = false;
|
if (submitBtn) {
|
||||||
|
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
|
||||||
|
submitBtn.disabled = cutoffBlocked;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,10 +485,16 @@
|
|||||||
setAlert('', '');
|
setAlert('', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submitBtn) submitBtn.disabled = (selectedIds().length === 0);
|
if (submitBtn) {
|
||||||
|
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
|
||||||
|
submitBtn.disabled = cutoffBlocked || (selectedIds().length === 0);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.');
|
setAlert('warn', 'Could not verify submissions right now. You may proceed; server will validate.');
|
||||||
if (submitBtn) submitBtn.disabled = false;
|
if (submitBtn) {
|
||||||
|
const cutoffBlocked = form.getAttribute('data-cutoff-blocked') === '1';
|
||||||
|
submitBtn.disabled = cutoffBlocked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,4 +509,55 @@
|
|||||||
runCheck();
|
runCheck();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// Cutoff enforcement helper (9:00 AM on the selected Sunday).
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.getElementById('reportForm');
|
||||||
|
if (!form) return;
|
||||||
|
const dateSel = form.querySelector('select[name="dates[]"]');
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
const cutoffAlert = document.getElementById('cutoffAlert');
|
||||||
|
const cutoffDate = form.getAttribute('data-cutoff-date') || '';
|
||||||
|
const cutoffTime = form.getAttribute('data-cutoff-time') || '';
|
||||||
|
const cutoffThreshold = form.getAttribute('data-cutoff-threshold') || '09:00';
|
||||||
|
|
||||||
|
function parseTimeToMinutes(val) {
|
||||||
|
const parts = (val || '').split(':');
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
const hh = parseInt(parts[0], 10);
|
||||||
|
const mm = parseInt(parts[1], 10);
|
||||||
|
if (Number.isNaN(hh) || Number.isNaN(mm)) return null;
|
||||||
|
return (hh * 60) + mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCutoffState() {
|
||||||
|
if (!dateSel || !cutoffDate || !cutoffTime) return;
|
||||||
|
const dates = Array.from(dateSel.selectedOptions || [])
|
||||||
|
.map(opt => opt.value)
|
||||||
|
.filter(v => v);
|
||||||
|
const nowMinutes = parseTimeToMinutes(cutoffTime);
|
||||||
|
const cutoffMinutes = parseTimeToMinutes(cutoffThreshold);
|
||||||
|
const isTodaySelected = dates.includes(cutoffDate);
|
||||||
|
const blocked = isTodaySelected && nowMinutes !== null && cutoffMinutes !== null && nowMinutes >= cutoffMinutes;
|
||||||
|
|
||||||
|
form.setAttribute('data-cutoff-blocked', blocked ? '1' : '0');
|
||||||
|
if (cutoffAlert) {
|
||||||
|
cutoffAlert.classList.toggle('d-none', !blocked);
|
||||||
|
}
|
||||||
|
if (submitBtn && blocked) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('change', function(e) {
|
||||||
|
const tgt = e.target;
|
||||||
|
if (!tgt) return;
|
||||||
|
if (tgt.matches('select[name="dates[]"], input[name="student_ids[]"], #reportType')) {
|
||||||
|
updateCutoffState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCutoffState();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mb-0">My Progress Reports</h3>
|
<h3 class="mb-0">Class Progress Reports</h3>
|
||||||
<div class="text-muted">Review weekly submissions and compare Islamic Studies with Quran/Arabic.</div>
|
<div class="text-muted">Review weekly submissions and compare Islamic Studies with Quran/Arabic.</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="<?= base_url('teacher/progress/submit') ?>" class="btn btn-outline-secondary">Submit New Report</a>
|
<a href="<?= base_url('teacher/progress/submit') ?>" class="btn btn-outline-secondary">Submit New Report</a>
|
||||||
@@ -78,6 +78,11 @@
|
|||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
<?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?>
|
<?= $report ? esc($report['unit_title'] ?: '-') : 'No submission' ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if ($report): ?>
|
||||||
|
<div class="small text-muted">
|
||||||
|
Submitted by: <?= esc(trim((string) ($report['teacher_name'] ?? '')) ?: 'Unknown') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -175,10 +175,13 @@
|
|||||||
<?php for ($i = 0; $i < $rowsCount; $i++): ?>
|
<?php for ($i = 0; $i < $rowsCount; $i++): ?>
|
||||||
<div class="row g-2 align-items-end mb-2 unit-chapter-entry">
|
<div class="row g-2 align-items-end mb-2 unit-chapter-entry">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<input type="text" name="unit_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Unit name" value="<?= esc($unitValues[$i] ?? '') ?>">
|
<input type="text" name="unit_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Unit name" value="<?= esc($unitValues[$i] ?? '') ?>" readonly>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<input type="text" name="chapter_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Chapter" value="<?= esc($chapterValues[$i] ?? '') ?>">
|
<input type="text" name="chapter_<?= esc($slug) ?>[]" class="form-control form-control-sm" placeholder="Chapter" value="<?= esc($chapterValues[$i] ?? '') ?>" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-unit-chapter aria-label="Remove entry">X</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endfor; ?>
|
<?php endfor; ?>
|
||||||
@@ -263,10 +266,13 @@
|
|||||||
row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry';
|
row.className = 'row g-2 align-items-end mb-2 unit-chapter-entry';
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<input type="text" name="unit_${slug}[]" class="form-control form-control-sm" placeholder="Unit name" />
|
<input type="text" name="unit_${slug}[]" class="form-control form-control-sm" placeholder="Unit name" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<input type="text" name="chapter_${slug}[]" class="form-control form-control-sm" placeholder="Chapter" />
|
<input type="text" name="chapter_${slug}[]" class="form-control form-control-sm" placeholder="Chapter" readonly />
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-unit-chapter aria-label="Remove entry">X</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const inputs = row.querySelectorAll('input');
|
const inputs = row.querySelectorAll('input');
|
||||||
@@ -338,6 +344,15 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', event => {
|
||||||
|
const button = event.target.closest('[data-remove-unit-chapter]');
|
||||||
|
if (!button) return;
|
||||||
|
const row = button.closest('.unit-chapter-entry');
|
||||||
|
if (row) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('[data-custom-entry]').forEach(button => {
|
document.querySelectorAll('[data-custom-entry]').forEach(button => {
|
||||||
button.addEventListener('click', event => {
|
button.addEventListener('click', event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<?php if (! $report): ?>
|
<?php if (! $report): ?>
|
||||||
<div class="text-muted">No entry submitted for this subject this week.</div>
|
<div class="text-muted">No entry submitted for this subject this week.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<div class="mb-2"><strong>Submitted by:</strong> <?= esc(trim((string) ($report['teacher_name'] ?? '')) ?: 'Unknown') ?></div>
|
||||||
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
|
<div class="mb-2"><strong><?= $unitLabel ?>:</strong> <?= esc($report['unit_title'] ?: '-') ?></div>
|
||||||
<div class="mb-3"><strong>Activities</strong><div class="mt-1"><?= nl2br(esc($report['covered'] ?? '-')) ?></div></div>
|
<div class="mb-3"><strong>Activities</strong><div class="mt-1"><?= nl2br(esc($report['covered'] ?? '-')) ?></div></div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user