From b4af0244a7236f243573a8f6ba3c0eae05776754 Mon Sep 17 00:00:00 2001 From: Ammar Fathin Date: Mon, 31 Aug 2026 13:47:01 +0800 Subject: [PATCH] fix: fill thesis grade only in last semester and fix finish() redirect Targets the thesis/skripsi row in the last semester, only while that row is still ungraded (no backfill when the last-semester thesis already has a grade). Also fixes Graduation::finish() declared ': string' while returning a RedirectResponse (TypeError on submit) and lifts the 60s PHP time limit for the long synchronous batch submission. --- CHANGELOG.md | 4 ++ app/Controllers/Graduation.php | 102 +++++++++++++++++++++++++++----- app/Views/graduation/wizard.php | 5 +- 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea7a06..bc0effd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,10 @@ _None yet._ - **PISN graduation wizard validation errors not shown:** `step()` never read the `graduation_error` flash set by `stepPost()`, so a failed verification (missing checkbox, bad graduation input) silently re-rendered the step with no message; API-error assignments also overwrote it. `step()` now initializes `$error` from the `graduation_error` flash and the API-error branches guard with `empty($error)`, so validation failures now display their aggregated messages on the step — ENH-008, #102 +- **Graduation wizard thesis grade fill targeted the wrong semester (ENH-010):** when a student had several thesis/skripsi rows across semesters (e.g. retakes), card 3 initially tagged *every* missing-grade thesis row "(akan diisi)" and `finish()` could push the Excel grade into an *older* row (e.g. filling `20251` when `20252` was the real last thesis). Now the grade targets a single row — the thesis/skripsi in the **last semester** (`id_smt` largest) — and only while that last-semester row is **still ungraded**; if the last-semester thesis already has a grade, no value is filled and no older row is backfilled. Card 3 tags "(akan diisi)" only on that one row; every other ungraded row renders "Belum ada nilai" — #113 + +- **Graduation wizard `finish()` fatal TypeError on submit:** `Graduation::finish()` was declared `: string` but returns a `RedirectResponse` (`redirect()->to('graduation/guidance')`), throwing `TypeError: Return value must be of type string` the moment the wizard is actually submitted to Neo Feeder. Changed the return type to `CodeIgniter\HTTP\RedirectResponse`. Also, the full submission iterates the whole batch with a synchronous call chain per student (InsertMahasiswaLulusDO + academic edits + thesis grade), which could exceed PHP's default 60 s limit on large batches → added `set_time_limit(0)` at the top of `finish()` — #113 + - **Graduation wizard transcript completeness (BUG-001, #89):** the "Kelengkapan Transkrip" check is now sourced from the Neo Feeder "Cek Transkrip Mahasiswa" menu data (cloud REST `/ws/transkrip/nilai_mahasiswa`, using BASE's existing WS `act` token as Bearer — no separate cloud login) instead of `GetTranskripMahasiswa`. Completeness now requires the thesis/skripsi course to exist with a non-empty grade; the per-course `choosed` transcript-inclusion marker is shown as status detail (badge "Nilai ada · masuk transkrip"/"belum masuk transkrip"/"Tanpa nilai") but no longer gates the result. New `NeoFeeder::getCekTranskripMahasiswa()` + `cloudGet()` helper; card 2b renders the detected thesis course (MK, kode, semester, nilai, status). - **FA6 icon consistency:** `header.php` logout button and `DESIGN.md` reference still used FA5 deprecated names (`fa-sign-out-alt` → `fa-right-from-bracket`, `fa-tachometer-alt` → `fa-gauge-high`) — aligned to canonical FA6 names to match `sidebar.php` - **Sidebar active state:** fixed route detection — `getPath()` returned `index.php/dashboard` instead of `dashboard`; now uses `current_url()` with `basename()` which handles both URL formats diff --git a/app/Controllers/Graduation.php b/app/Controllers/Graduation.php index 89843e7..99533c9 100644 --- a/app/Controllers/Graduation.php +++ b/app/Controllers/Graduation.php @@ -5,6 +5,7 @@ use App\Libraries\NeoFeeder; use App\Libraries\PisnService; use App\Libraries\WizardProgress; +use CodeIgniter\HTTP\RedirectResponse; /** * PISN graduation wizard. @@ -336,6 +337,10 @@ public function step() } } + // ENH-010 fix: only the ungraded thesis/skripsi row in the LAST semester + // (largest id_smt) is the one that will have its grade filled. + $targetThesisSemester = $this->lastUngradedThesisSemester($completeness['thesis'] ?? []); + $pisn = $this->pisn->checkEligibility($student); $total = count($progress['students']); @@ -356,6 +361,7 @@ public function step() 'identityOk' => $identityOk, 'academicOk' => $academicOk, 'transcriptOk' => $transcriptOk, + 'targetThesisSemester' => $targetThesisSemester, 'activeCode' => $activeCode, 'transcript' => $transcript, 'completeness' => $completeness, @@ -442,13 +448,21 @@ private function checkTranscriptCompleteness(array $transcript): array /** * Finds the thesis/skripsi course row that is registered but still carries - * no grade, returning its composite key for UpdateNilaiPerkuliahanKelas. + * no grade and sits in the LAST semester (largest id_smt), returning its + * composite key for UpdateNilaiPerkuliahanKelas. + * + * A student may have several thesis/skripsi rows across semesters (e.g. + * repeating the course). Only the row in the last semester may receive the + * grade, and only while that row is still ungraded — if the last semester's + * thesis row already has a grade, no row is returned (we never backfill an + * older, ungraded row). * * @param array $transcript Rows from getCekTranskripMahasiswa() * (cloud nilai_mahasiswa rows carrying id_kls / id_reg_pd). * - * @return array|null The row's key as ['id_reg_pd', 'id_kls'], or null when - * no graded-missing thesis row exists. + * @return array|null The row's key as ['id_reg_pd', 'id_kls', 'id_smt'], or + * null when the last-semester thesis row is already graded + * or no thesis row exists. */ private function findMissingGradeThesis(array $transcript): ?array { @@ -457,26 +471,79 @@ private function findMissingGradeThesis(array $transcript): ?array } $thesisPattern = '/skripsi|tugas\s*akhir|thesis|disertasi/i'; + $thesisRows = []; foreach ($transcript as $row) { $name = $row['nm_mk'] ?? ($row['nama_mata_kuliah'] ?? ''); - if (! preg_match($thesisPattern, $name)) { + if (! is_string($name) || ! preg_match($thesisPattern, $name)) { continue; } - $nilai = $row['nilai_huruf'] ?? ($row['nilai_angka'] ?? null); - $hasGrade = ($nilai !== null && $nilai !== ''); - if ($hasGrade) { + $thesisRows[] = $row; + } + + if (empty($thesisRows)) { + return null; + } + + // The candidate is the thesis row in the LAST semester. + $smtKey = fn (array $r) => (string) ($r['id_smt'] ?? ($r['id_semester'] ?? '')); + usort($thesisRows, fn ($a, $b) => strcmp($smtKey($b), $smtKey($a))); + $last = $thesisRows[0]; + + // If the last-semester thesis row already has a grade, do nothing. + $grade = trim((string) ($last['nilai_huruf'] ?? '')); + if ($grade === '') { + $grade = trim((string) ($last['nilai_angka'] ?? '')); + } + if ($grade !== '') { + return null; + } + + $idReg = (string) ($last['id_reg_pd'] ?? ($last['id_registrasi_mahasiswa'] ?? '')); + $idKls = (string) ($last['id_kls'] ?? ($last['id_kelas_kuliah'] ?? '')); + if ($idReg === '' || $idKls === '') { + return null; + } + + return [ + 'id_reg_pd' => $idReg, + 'id_kls' => $idKls, + 'id_smt' => $smtKey($last), + ]; + } + + /** + * Returns the semester (id_smt) of the thesis/skripsi course row that sits in + * the LAST semester, but only when that row is still ungraded. Returns null + * when the last-semester thesis row already has a grade (so nothing is tagged + * "(akan diisi)"). + * + * @param list $thesisRows Rows from checkTranscriptCompleteness(). + */ + private function lastUngradedThesisSemester(array $thesisRows): ?string + { + if (empty($thesisRows)) { + return null; + } + + $last = null; + $lastRow = null; + foreach ($thesisRows as $t) { + $smt = (string) ($t['semester'] ?? ''); + if ($smt === '') { continue; } - $idReg = $row['id_reg_pd'] ?? ($row['id_registrasi_mahasiswa'] ?? ''); - $idKls = $row['id_kls'] ?? ($row['id_kelas_kuliah'] ?? ''); - if ($idReg === '' || $idKls === '') { - continue; + if ($last === null || strcmp($smt, $last) > 0) { + $last = $smt; + $lastRow = $t; } - return ['id_reg_pd' => (string) $idReg, 'id_kls' => (string) $idKls]; } - return null; + if ($lastRow === null || ! empty($lastRow['hasGrade'])) { + return null; + } + + return $last; } /** @@ -610,8 +677,15 @@ public function stepPost() /** * Submits every verified student to Neo Feeder, then shows guidance. */ - public function finish(): string + public function finish(): RedirectResponse { + // Full submission iterates the whole batch with one synchronous Neo Feeder + // call chain per student (InsertMahasiswaLulusDO + academic edits + thesis + // grade), which can exceed PHP's default 60s time limit on large batches. + // Allow this long-running server-side job to finish. ponytail: unbounded; a + // progress/queue pipeline is the upgrade if batches keep growing. + set_time_limit(0); + $token = $this->currentToken(); $progress = $this->loadProgress(); if ($progress === null) { diff --git a/app/Views/graduation/wizard.php b/app/Views/graduation/wizard.php index e49e8b7..c81ba31 100644 --- a/app/Views/graduation/wizard.php +++ b/app/Views/graduation/wizard.php @@ -187,7 +187,10 @@