normalizeInt($this->request->getGet('target_school_year_id')); $preview = service('schoolYearClosing')->preview($id, $targetId); $promotionTable = $this->promotionTablePayload($preview['promotion']['rows'] ?? []); return view('school_years/closing_preview', [ 'preview' => $preview, 'promotionTable' => $promotionTable, 'latestBatch' => service('schoolYearClosing')->latestBatch($id), 'schoolYears' => (new SchoolYearModel())->orderBy('name', 'DESC')->findAll(), ]); } catch (Throwable $e) { return redirect()->to('/administrator/school-years')->with('error', $e->getMessage()); } } public function start(int $id) { try { $targetId = $this->normalizeInt($this->request->getPost('target_school_year_id')); if ($targetId === null) { return redirect()->back()->with('error', 'Select a target school year.'); } service('schoolYearClosing')->start($id, $targetId, $this->userId()); return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'End-year process started.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } public function execute(int $id) { try { service('schoolYearClosing')->execute($id, $this->userId()); return redirect()->to('/administrator/school-years/' . $id . '/closing/preview')->with('success', 'Carry-forward confirmed.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } public function complete(int $id) { try { service('schoolYearClosing')->complete($id, $this->userId()); return redirect()->to('/administrator/school-years')->with('success', 'School year ended and closed.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } public function cancel(int $id) { try { service('schoolYearClosing')->cancel($id, $this->userId()); return redirect()->to('/administrator/school-years')->with('success', 'End-year process cancelled.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } private function normalizeInt(mixed $value): ?int { return is_numeric($value) && (int) $value > 0 ? (int) $value : null; } private function promotionTablePayload(array $rows): array { $allowedSorts = ['student', 'school_id', 'class', 'year_score', 'decision', 'source', 'queue', 'target', 'status']; $sort = (string) ($this->request->getGet('sort') ?? 'class'); $sort = in_array($sort, $allowedSorts, true) ? $sort : 'class'; $order = strtolower((string) ($this->request->getGet('order') ?? 'asc')) === 'desc' ? 'desc' : 'asc'; $page = max(1, (int) ($this->request->getGet('page') ?? 1)); $perPage = (int) ($this->request->getGet('per_page') ?? 25); $allowedPerPage = [10, 25, 50, 100]; $perPage = in_array($perPage, $allowedPerPage, true) ? $perPage : 25; usort($rows, function (array $a, array $b) use ($sort, $order): int { $comparison = $this->comparePromotionRows($a, $b, $sort); if ($comparison === 0 && $sort !== 'class') { $comparison = $this->comparePromotionRows($a, $b, 'class'); } if ($comparison === 0 && $sort !== 'student') { $comparison = $this->comparePromotionRows($a, $b, 'student'); } if ($comparison === 0) { $comparison = ((int) ($a['student_id'] ?? 0)) <=> ((int) ($b['student_id'] ?? 0)); } return $order === 'desc' ? -$comparison : $comparison; }); $total = count($rows); $pageCount = max(1, (int) ceil($total / $perPage)); $page = min($page, $pageCount); $offset = ($page - 1) * $perPage; return [ 'rows' => array_slice($rows, $offset, $perPage), 'sort' => $sort, 'order' => $order, 'page' => $page, 'perPage' => $perPage, 'total' => $total, 'pageCount' => $pageCount, 'allowedPerPage' => $allowedPerPage, 'from' => $total === 0 ? 0 : $offset + 1, 'to' => min($offset + $perPage, $total), ]; } private function comparePromotionRows(array $a, array $b, string $sort): int { $aValue = $this->promotionSortValue($a, $sort); $bValue = $this->promotionSortValue($b, $sort); if ($sort === 'year_score') { if ($aValue === null && $bValue === null) { return 0; } if ($aValue === null) { return 1; } if ($bValue === null) { return -1; } return $aValue <=> $bValue; } return strnatcasecmp((string) $aValue, (string) $bValue); } private function promotionSortValue(array $row, string $sort): mixed { return match ($sort) { 'student' => (string) ($row['student_name'] ?? ''), 'school_id' => (string) ($row['school_id'] ?? ''), 'class' => (string) ($row['class_section_name'] ?? ''), 'year_score' => is_numeric($row['year_score'] ?? null) ? (float) $row['year_score'] : null, 'decision' => (string) ($row['decision'] ?? ''), 'source' => (string) ($row['source'] ?? ''), 'queue' => (string) ($row['queue_status'] ?? ''), 'target' => trim((string) ($row['target_section'] ?? '') . ' ' . (string) ($row['target_class'] ?? '')), 'status' => (string) ($row['status'] ?? ''), default => '', }; } private function userId(): ?int { $id = session('user_id') ?? session('id'); return is_numeric($id) ? (int) $id : null; } }