'Fase tidak valid.'],400); cgo('home.php'); } cdb()->begin_transaction(); try { $current=cq('SELECT level FROM in_daper WHERE id=? FOR UPDATE','i',[(int)$p['id']])->get_result()->fetch_assoc(); $attempt=exam_find((int)$p['id'],$phase); if(!$attempt || $attempt['status']!=='active' || (int)$current['level']!==$phase-1) { cdb()->rollback(); if($json) exam_reply(['closed'=>true,'redirect'=>'home.php'],409); cgo('home.php'); } if(!hash_equals($attempt['active_token'],(string)($_POST['attempt_token']??''))) { exam_event($attempt,'stale_tab'); cdb()->commit(); if($json) exam_reply(['stale'=>true,'error'=>'Halaman ini sudah digantikan tab atau sesi lain. Muat ulang untuk melanjutkan.'],409); $_SESSION['error']='Halaman tes sudah digantikan tab atau sesi lain. Buka kembali fase aktif.'; cgo('home.php'); } if(time()>=(int)$attempt['deadline']) { exam_finalize($attempt,json_decode($attempt['answers_json'],true),true); cdb()->commit(); $_SESSION['success']='Waktu fase '.$phase.' habis. Hanya draft yang diterima sebelum deadline yang disimpan.'; if($json) exam_reply(['closed'=>true,'redirect'=>'home.php']); cgo('home.php'); } if($action==='event') { exam_event($attempt,(string)($_POST['event']??'')); cdb()->commit(); exam_reply(['ok'=>true,'remaining'=>max(0,(int)$attempt['deadline']-time())]); } if(!in_array($action,['save','submit'],true)) throw new InvalidArgumentException('Aksi tidak valid.'); $answers=exam_answers($attempt,$_POST['answers']??[]); if($action==='save') { cq('UPDATE rec_exam_attempt SET answers_json=? WHERE id=?','si',[json_encode($answers,JSON_UNESCAPED_UNICODE),(int)$attempt['id']]); cdb()->commit(); exam_reply(['ok'=>true,'remaining'=>max(0,(int)$attempt['deadline']-time())]); } exam_finalize($attempt,$answers); cdb()->commit(); $_SESSION['level']=$phase; $_SESSION['success']=$phase===4?'Seluruh fase berhasil dikirim. Terima kasih telah mengikuti psikotest Nurinda.':'Jawaban fase '.$phase.' tersimpan. Silakan lanjutkan ke fase berikutnya.'; } catch(InvalidArgumentException $error) { cdb()->rollback(); if($json) exam_reply(['error'=>$error->getMessage()],422); $_SESSION['error']=$error->getMessage(); } catch(Throwable $error) { cdb()->rollback(); error_log($error->getMessage()); if($json) exam_reply(['error'=>'Sinkronisasi gagal. Coba kembali.'],503); $_SESSION['error']='Jawaban belum tersimpan. Buka kembali fase aktif untuk melanjutkan.'; } cgo('home.php');