merge prod to main
This commit is contained in:
Regular → Executable
+434
-7
@@ -5,7 +5,11 @@ namespace App\Controllers;
|
||||
use App\Models\ClassProgressReportModel;
|
||||
use App\Models\ClassProgressAttachmentModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\CalendarModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\SubjectCurriculumModel;
|
||||
use App\Services\SemesterRangeService;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
@@ -15,6 +19,10 @@ class AdminProgressController extends BaseController
|
||||
protected ClassProgressAttachmentModel $attachmentModel;
|
||||
protected ClassSectionModel $classSectionModel;
|
||||
protected StudentClassModel $studentClassModel;
|
||||
protected CalendarModel $calendarModel;
|
||||
protected ConfigurationModel $configModel;
|
||||
protected SemesterRangeService $semesterRangeService;
|
||||
protected SubjectCurriculumModel $curriculumModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -23,6 +31,10 @@ class AdminProgressController extends BaseController
|
||||
$this->attachmentModel = new ClassProgressAttachmentModel();
|
||||
$this->classSectionModel = new ClassSectionModel();
|
||||
$this->studentClassModel = new StudentClassModel();
|
||||
$this->calendarModel = new CalendarModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->semesterRangeService = new SemesterRangeService($this->configModel);
|
||||
$this->curriculumModel = new SubjectCurriculumModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -53,22 +65,30 @@ class AdminProgressController extends BaseController
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('week_start', 'DESC')->get()->getResultArray();
|
||||
$reportGroups = [];
|
||||
$reportGroupsBySection = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['status_label'] = ClassProgressController::STATUS_OPTIONS[$row['status']] ?? 'Unknown';
|
||||
$key = ($row['week_start'] ?? '') . '_' . ($row['class_section_id'] ?? '');
|
||||
if ($key === '_') {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$weekKey = (string) ($row['week_start'] ?? '');
|
||||
if ($sectionId === 0 || $weekKey === '') {
|
||||
continue;
|
||||
}
|
||||
if (! isset($reportGroups[$key])) {
|
||||
$reportGroups[$key] = [
|
||||
if (! isset($reportGroupsBySection[$sectionId])) {
|
||||
$reportGroupsBySection[$sectionId] = [];
|
||||
}
|
||||
if (! isset($reportGroupsBySection[$sectionId][$weekKey])) {
|
||||
$reportGroupsBySection[$sectionId][$weekKey] = [
|
||||
'week_start' => $row['week_start'],
|
||||
'week_end' => $row['week_end'],
|
||||
'class_section_name' => $row['class_section_name'] ?? '',
|
||||
'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();
|
||||
@@ -78,12 +98,47 @@ class AdminProgressController extends BaseController
|
||||
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);
|
||||
$sectionSubjectCounts = $this->buildSectionSubjectCounts($rows);
|
||||
$lowProgressSectionIds = [];
|
||||
if ($expectedDays > 0) {
|
||||
foreach ($filteredSections as $section) {
|
||||
$sectionId = (int) ($section['class_section_id'] ?? 0);
|
||||
if ($sectionId === 0) {
|
||||
continue;
|
||||
}
|
||||
$stat = $sectionStats[$sectionId] ?? null;
|
||||
$percent = $stat['percent'] ?? 0;
|
||||
if ($stat === null) {
|
||||
$percent = 0;
|
||||
}
|
||||
if ($percent < 50) {
|
||||
$lowProgressSectionIds[] = $sectionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('admin/class_progress_list', [
|
||||
'reportGroups' => $reportGroups,
|
||||
'reportGroupsBySection' => $reportGroupsBySection,
|
||||
'filters' => $filters,
|
||||
'classSections' => $filteredSections,
|
||||
'statusOptions' => ClassProgressController::STATUS_OPTIONS,
|
||||
'subjectSections' => ClassProgressController::SUBJECT_SECTIONS,
|
||||
'sectionStats' => $sectionStats,
|
||||
'sectionSubjectCounts' => $sectionSubjectCounts,
|
||||
'expectedDays' => $expectedDays,
|
||||
'lowProgressSectionIds' => $lowProgressSectionIds,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -211,4 +266,376 @@ class AdminProgressController extends BaseController
|
||||
$flags = json_decode($json, true);
|
||||
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
|
||||
{
|
||||
$submittedBySection = [];
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$weekStart = (string) ($row['week_start'] ?? '');
|
||||
if ($sectionId === 0 || $weekStart === '') {
|
||||
continue;
|
||||
}
|
||||
$submittedBySection[$sectionId][$weekStart] = true;
|
||||
}
|
||||
|
||||
$stats = [];
|
||||
foreach ($submittedBySection as $sectionId => $weeks) {
|
||||
$submitted = count($weeks);
|
||||
$stats[$sectionId] = $this->buildSectionStat($submitted, $expectedDays);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function buildSectionSubjectCounts(array $rows): array
|
||||
{
|
||||
$allowedSubjects = [];
|
||||
foreach (ClassProgressController::SUBJECT_SECTIONS as $section) {
|
||||
$allowedSubjects[] = $section['db_subject'] ?? $section['label'] ?? '';
|
||||
}
|
||||
$allowedSubjects = array_values(array_filter($allowedSubjects));
|
||||
|
||||
$counts = [];
|
||||
$sectionClassMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||
continue;
|
||||
}
|
||||
if (! isset($sectionClassMap[$sectionId])) {
|
||||
$sectionClassMap[$sectionId] = $this->classSectionModel->getClassId($sectionId);
|
||||
}
|
||||
}
|
||||
|
||||
$curriculumUnits = $this->buildCurriculumUnitMap(array_values(array_filter($sectionClassMap)));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$sectionId = (int) ($row['class_section_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
if ($sectionId === 0 || $subject === '') {
|
||||
continue;
|
||||
}
|
||||
if (! empty($allowedSubjects) && ! in_array($subject, $allowedSubjects, true)) {
|
||||
continue;
|
||||
}
|
||||
$subjectSlug = $this->resolveSubjectSlug($subject);
|
||||
$classId = $sectionClassMap[$sectionId] ?? null;
|
||||
$chapterToUnit = [];
|
||||
if ($classId && $subjectSlug && ! empty($curriculumUnits[$classId][$subjectSlug]['chapter_to_unit'])) {
|
||||
$chapterToUnit = $curriculumUnits[$classId][$subjectSlug]['chapter_to_unit'];
|
||||
}
|
||||
$unitKeys = $this->extractUnitKeys((string) ($row['unit_title'] ?? ''), $chapterToUnit);
|
||||
foreach ($unitKeys as $unitKey) {
|
||||
$key = $subjectSlug . '|' . $unitKey;
|
||||
$counts[$sectionId][$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$totals = [];
|
||||
foreach ($counts as $sectionId => $unitSet) {
|
||||
$totals[$sectionId] = count($unitSet);
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
protected function buildCurriculumUnitMap(array $classIds): array
|
||||
{
|
||||
$classIds = array_values(array_filter(array_map('intval', $classIds)));
|
||||
if (empty($classIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->curriculumModel
|
||||
->whereIn('class_id', $classIds)
|
||||
->findAll();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$classId = (int) ($row['class_id'] ?? 0);
|
||||
$subject = (string) ($row['subject'] ?? '');
|
||||
$chapter = trim((string) ($row['chapter_name'] ?? ''));
|
||||
$unitNumber = $row['unit_number'] ?? null;
|
||||
if ($classId === 0 || $subject === '' || $chapter === '') {
|
||||
continue;
|
||||
}
|
||||
if ($unitNumber === null || $unitNumber === '') {
|
||||
continue;
|
||||
}
|
||||
$unitKey = (string) $unitNumber;
|
||||
$map[$classId][$subject]['chapter_to_unit'][$chapter] = $unitKey;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
protected function resolveSubjectSlug(string $subject): ?string
|
||||
{
|
||||
foreach (ClassProgressController::SUBJECT_SECTIONS as $slug => $section) {
|
||||
$dbSubject = (string) ($section['db_subject'] ?? '');
|
||||
$label = (string) ($section['label'] ?? '');
|
||||
if ($subject === $dbSubject || $subject === $label) {
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function countUnitSegments(string $unitTitle, array $chapterToUnit): int
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return 0;
|
||||
}
|
||||
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
||||
if (! $parts) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$seen = [];
|
||||
foreach ($parts as $part) {
|
||||
[$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part);
|
||||
$key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit);
|
||||
if ($key === '') {
|
||||
$key = $part;
|
||||
}
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
}
|
||||
|
||||
return count($seen);
|
||||
}
|
||||
|
||||
protected function splitUnitChapterSegment(string $segment): array
|
||||
{
|
||||
$segment = trim($segment);
|
||||
if ($segment === '') {
|
||||
return ['', ''];
|
||||
}
|
||||
$pos = strrpos($segment, '/');
|
||||
if ($pos === false) {
|
||||
return [$segment, ''];
|
||||
}
|
||||
$unitPart = trim(substr($segment, 0, $pos));
|
||||
$chapterPart = trim(substr($segment, $pos + 1));
|
||||
return [$unitPart, $chapterPart];
|
||||
}
|
||||
|
||||
protected function extractUnitKeys(string $unitTitle, array $chapterToUnit): array
|
||||
{
|
||||
$unitTitle = trim($unitTitle);
|
||||
if ($unitTitle === '') {
|
||||
return [];
|
||||
}
|
||||
$parts = array_filter(array_map('trim', explode(';', $unitTitle)), static fn ($part) => $part !== '');
|
||||
if (! $parts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
foreach ($parts as $part) {
|
||||
[$unitPart, $chapterPart] = $this->splitUnitChapterSegment($part);
|
||||
$key = $this->resolveUnitKey($unitPart, $chapterPart, $chapterToUnit);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$keys[$key] = true;
|
||||
}
|
||||
|
||||
return array_keys($keys);
|
||||
}
|
||||
|
||||
protected function resolveUnitKey(string $unitPart, string $chapterPart, array $chapterToUnit): string
|
||||
{
|
||||
if ($chapterPart !== '' && ! empty($chapterToUnit[$chapterPart])) {
|
||||
return (string) $chapterToUnit[$chapterPart];
|
||||
}
|
||||
if (empty($chapterToUnit)) {
|
||||
if (preg_match('/\bunit\s*(\d+)\b/i', $unitPart, $matches)) {
|
||||
return (string) $matches[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (preg_match('/\bunit\s*(\d+)\b/i', $unitPart, $matches)) {
|
||||
return (string) $matches[1];
|
||||
}
|
||||
if ($unitPart !== '') {
|
||||
return $unitPart;
|
||||
}
|
||||
if ($chapterPart !== '') {
|
||||
return $chapterPart;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user