Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 88 additions & 14 deletions app/Controllers/Graduation.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Libraries\NeoFeeder;
use App\Libraries\PisnService;
use App\Libraries\WizardProgress;
use CodeIgniter\HTTP\RedirectResponse;

/**
* PISN graduation wizard.
Expand Down Expand Up @@ -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']);

Expand All @@ -356,6 +361,7 @@ public function step()
'identityOk' => $identityOk,
'academicOk' => $academicOk,
'transcriptOk' => $transcriptOk,
'targetThesisSemester' => $targetThesisSemester,
'activeCode' => $activeCode,
'transcript' => $transcript,
'completeness' => $completeness,
Expand Down Expand Up @@ -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
{
Expand All @@ -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<array> $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;
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion app/Views/graduation/wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@
<?php foreach ($completeness['thesis'] as $t): ?>
<?php
$excelGrade = trim((string) ($student['nilai_skripsi'] ?? ''));
$willFill = ! $t['hasGrade'] && $excelGrade !== '';
$targetSmt = $targetThesisSemester ?? null;
$willFill = ! $t['hasGrade']
&& $excelGrade !== ''
&& (string) ($t['semester'] ?? '') === (string) $targetSmt;
?>
<tr>
<td><?= esc($t['nama']) ?></td>
Expand Down