From f18c470f143aaa06539a2a69dc0991840210d554 Mon Sep 17 00:00:00 2001 From: Mahayoga Date: Wed, 29 Apr 2026 02:37:26 +0700 Subject: [PATCH] WIP: halaman ujian (mayan progress) --- .../Controllers/HalamanUjianController.php | 164 ++++++++++++++++- app/Models/DetailTestModel.php | 38 ++++ app/Models/TestSessionsModel.php | 2 + ...836_change_column_in_detail_test_table.php | 34 ++++ ...905_change_column_in_detail_test_table.php | 28 +++ ...3652_add_column_in_test_sessions_table.php | 30 ++++ .../views/user/pages/ujian/index.blade.php | 169 +++++++++++++++--- routes/web.php | 2 + 8 files changed, 439 insertions(+), 28 deletions(-) create mode 100644 app/Models/DetailTestModel.php create mode 100644 database/migrations/2026_04_28_093836_change_column_in_detail_test_table.php create mode 100644 database/migrations/2026_04_28_132905_change_column_in_detail_test_table.php create mode 100644 database/migrations/2026_04_28_183652_add_column_in_test_sessions_table.php diff --git a/app/Http/Controllers/HalamanUjianController.php b/app/Http/Controllers/HalamanUjianController.php index 0bc89dc..41324dd 100644 --- a/app/Http/Controllers/HalamanUjianController.php +++ b/app/Http/Controllers/HalamanUjianController.php @@ -3,11 +3,72 @@ namespace App\Http\Controllers; use App\Models\DataSoalModel; +use App\Models\DetailTestModel; use App\Models\TestSessionsModel; use Illuminate\Http\Request; class HalamanUjianController extends Controller { + + public function getCurrentAnswer(Request $request) { + $data_answer = DetailTestModel::where('id_soal', $request->id_question) + ->where('id_test_sessions', $request->session_id) + ->first(); + + if($data_answer != null) { + return response()->json([ + 'status' => 'success', + 'jawaban' => $data_answer->jawaban + ]); + } else { + return response()->json([ + 'status' => 'error', + 'message' => 'Data jawaban tidak ditemukan.' + ]); + } + } + + public function getTimeLeft(Request $request) { + $data_session = TestSessionsModel::where('id_test_sessions', $request->session_id)->first(); + if($data_session != null) { + if($data_session->status_session != '1') { + return response()->json([ + 'status' => 'error', + 'message' => 'Data sesi tes belum dimulai atau telah usai. Silahkan cek status dengan kode untuk informasi lebih lanjut.' + ]); + } else if($data_session->status_session == '1') { + if($data_session->is_active == '1') { + $time_left = $data_session->unix_end_time - time(); + if($time_left <= 0) { + $data_session->update([ + 'status_session' => '2', + 'is_active' => '0' + ]); + return response()->json([ + 'status' => 'error', + 'message' => 'Waktu sesi tes telah habis. Silahkan cek status dengan kode untuk informasi lebih lanjut.' + ]); + } else { + return response()->json([ + 'status' => 'success', + 'time_left' => $data_session->unix_end_time + ]); + } + } else { + return response()->json([ + 'status' => 'error', + 'message' => 'Sesi tes belum aktif. Silahkan mulai tes terlebih dahulu.' + ]); + } + } + } else { + return response()->json([ + 'status' => 'error', + 'message' => 'Data sesi tes tidak ditemukan. Silakan mulai tes terlebih dahulu.' + ]); + } + } + /** * Display a listing of the resource. */ @@ -27,7 +88,23 @@ public function index(Request $request) if($data_session != null) { if($data_session->status_session != '1') { return redirect()->route('cek-status.index')->with('error', 'Data sesi tes belum dimulai atau telah usai. Silahkan cek status dengan kode untuk informasi lebih lanjut.'); - } + } else if($data_session->status_session == '1') { + if($data_session->is_active == '0') { + $data_session->update([ + 'is_active' => '1', + 'unix_start_time' => time(), + 'unix_end_time' => time() + (20 * 60) + ]); + } else if($data_session->is_active == '1') { + if(time() > $data_session->unix_end_time) { + $data_session->update([ + 'status_session' => '2', + 'is_active' => '0' + ]); + return redirect()->route('cek-status.index')->with('error', 'Waktu sesi tes telah habis. Silahkan cek status dengan kode untuk informasi lebih lanjut.'); + } + } + } } else { return redirect()->route('cek-status.index')->with('error', 'Data sesi tes tidak ditemukan. Silakan mulai tes terlebih dahulu.'); } @@ -43,6 +120,30 @@ public function index(Request $request) } $data_question = DataSoalModel::all(); + $answered_question = []; + $index_question = 1; + foreach($data_question as $detail) { + $detail_answer = DetailTestModel::where('id_soal', $detail->id_soal) + ->where('id_test_sessions', $id_session) + ->first(); + if($detail_answer != null) { + $answered_question[] = [ + 'id_soal' => $detail->id_soal, + 'nomor_soal' => $index_question, + 'answered' => true, + 'jawaban' => $detail_answer->jawaban + ]; + } else { + $answered_question[] = [ + 'id_soal' => $detail->id_soal, + 'nomor_soal' => $index_question, + 'answered' => false, + 'jawaban' => null + ]; + } + $index_question++; + } + $total_question = null; $is_last_question = false; if($question_number <= 0 || $question_number > count($data_question)) { @@ -54,7 +155,41 @@ public function index(Request $request) $total_question = count($data_question); $data_question = $data_question[$question_number - 1]; - return view("user.pages.ujian.index", compact('data_session', 'data_question', 'total_question', 'is_last_question', 'question_number')); + $answer = []; + if($data_question->jawaban_a != null) { + $answer[] = [ + 'key' => 'a', + 'value' => $data_question->jawaban_a + ]; + } + if($data_question->jawaban_b != null) { + $answer[] = [ + 'key' => 'b', + 'value' => $data_question->jawaban_b + ]; + } + if($data_question->jawaban_c != null) { + $answer[] = [ + 'key' => 'c', + 'value' => $data_question->jawaban_c + ]; + } + if($data_question->jawaban_d != null) { + $answer[] = [ + 'key' => 'd', + 'value' => $data_question->jawaban_d + ]; + } + if($data_question->jawaban_e != null) { + $answer[] = [ + 'key' => 'e', + 'value' => $data_question->jawaban_e + ]; + } + + // dd($answer); + + return view("user.pages.ujian.index", compact('data_session', 'data_question', 'total_question', 'is_last_question', 'question_number', 'answer', 'answered_question')); } /** @@ -70,7 +205,30 @@ public function create() */ public function store(Request $request) { - // + $data_session = TestSessionsModel::where('id_test_sessions', $request->session_id)->first(); + if($data_session != null) { + if($data_session->status_session != '1') { + return response()->json([ + 'status' => 'error', + 'message' => 'Data sesi tes belum dimulai atau telah usai. Silahkan cek status dengan kode untuk informasi lebih lanjut.' + ]); + } + DetailTestModel::updateOrCreate( + [ + 'id_soal' => $request->id_question, + 'id_test_sessions' => $request->session_id + ], + [ + 'jawaban' => $request->jawaban_final + ] + ); + } else { + return response()->json([ + 'status' => 'error', + 'message' => 'Data sesi tes tidak ditemukan. Silakan mulai tes terlebih dahulu.' + ]); + } + } /** diff --git a/app/Models/DetailTestModel.php b/app/Models/DetailTestModel.php new file mode 100644 index 0000000..f3b6a9c --- /dev/null +++ b/app/Models/DetailTestModel.php @@ -0,0 +1,38 @@ + 'string', + ]; + + protected static function boot() + { + parent::boot(); + static::creating(function ($model) { + if (empty($model->id_detail_test)) { + $model->id_detail_test = Str::uuid(); + } + }); + } +} diff --git a/app/Models/TestSessionsModel.php b/app/Models/TestSessionsModel.php index a05af2d..d5c0cdf 100644 --- a/app/Models/TestSessionsModel.php +++ b/app/Models/TestSessionsModel.php @@ -18,7 +18,9 @@ class TestSessionsModel extends Model 'status_session', 'token_session', 'id_booking', + 'unix_start_time', 'is_active', + 'unix_end_time', ]; protected $casts = [ diff --git a/database/migrations/2026_04_28_093836_change_column_in_detail_test_table.php b/database/migrations/2026_04_28_093836_change_column_in_detail_test_table.php new file mode 100644 index 0000000..b09699a --- /dev/null +++ b/database/migrations/2026_04_28_093836_change_column_in_detail_test_table.php @@ -0,0 +1,34 @@ +dropForeign(['id_test']); + $table->dropColumn('id_test'); + $table->char('id_test_sessions', 36)->after('id_soal'); + $table->foreign('id_test_sessions')->references('id_test_sessions')->on('test_sessions')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('detail_test', function (Blueprint $table) { + $table->dropForeign(['id_test_sessions']); + $table->dropColumn('id_test_sessions'); + $table->unsignedBigInteger('id_test')->after('id_soal'); + $table->foreign('id_test')->references('id_test')->on('test')->onDelete('cascade'); + }); + } +}; diff --git a/database/migrations/2026_04_28_132905_change_column_in_detail_test_table.php b/database/migrations/2026_04_28_132905_change_column_in_detail_test_table.php new file mode 100644 index 0000000..05b2846 --- /dev/null +++ b/database/migrations/2026_04_28_132905_change_column_in_detail_test_table.php @@ -0,0 +1,28 @@ +foreign('id_soal')->references('id_soal')->on('soal')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('detail_test', function (Blueprint $table) { + $table->dropForeign(['id_soal']); + }); + } +}; diff --git a/database/migrations/2026_04_28_183652_add_column_in_test_sessions_table.php b/database/migrations/2026_04_28_183652_add_column_in_test_sessions_table.php new file mode 100644 index 0000000..274005d --- /dev/null +++ b/database/migrations/2026_04_28_183652_add_column_in_test_sessions_table.php @@ -0,0 +1,30 @@ +char('unix_start_time', 20)->default(0)->after('token_session'); + $table->char('unix_end_time', 20)->default(0)->after('is_active'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('test_sessions', function (Blueprint $table) { + $table->dropColumn('unix_start_time'); + $table->dropColumn('unix_end_time'); + }); + } +}; diff --git a/resources/views/user/pages/ujian/index.blade.php b/resources/views/user/pages/ujian/index.blade.php index 9a5d584..93dac33 100644 --- a/resources/views/user/pages/ujian/index.blade.php +++ b/resources/views/user/pages/ujian/index.blade.php @@ -7,9 +7,16 @@ @@ -19,38 +26,25 @@
Soal
+
{{ $data_question->pertanyaan }}
Jawaban:
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+ @foreach ($answer as $item) +
+ + +
+ @endforeach
Navigasi Soal
- @for ($i = 0; $i < $total_question; $i++) -
{{ $i + 1 }}
- @endfor + @foreach ($answered_question as $question) +
{{ $question['nomor_soal'] }}
+ @endforeach
@if ($question_number > 1) @@ -64,6 +58,28 @@ @endif
+
+
+
+
+
Instruksi
+

Silahkan baca soal dengan teliti dan pilih jawaban yang paling tepat.

+
+ + adalah tanda bahwa jawaban BELUM diisi. +
+
+ + adalah tanda bahwa jawaban SUDAH diisi. +
+
+ + adalah tanda bahwa anda sedang membaca/menjawab soal ini. +
+
+
+
+
@@ -75,10 +91,37 @@ const ws = new WebSocket('{{ env("WEBSOCKET_URL") }}'); let SESSION_ID = '{{ $data_session->id_test_sessions }}'; let TOKEN_SESSION = '{{ $data_session->token_session }}'; + let UNIX_END_TIME = null; function changeQuestion(num) { let url_temp = window.location.href; let current_num = new URLSearchParams(window.location.search).get('question'); + let xhttp = new XMLHttpRequest(); + let the_answer = null; + let token = document.getElementById('_token').getAttribute('value'); + let formData = new FormData(); + + if(current_num == num) { + console.log('same question'); + return; + } + + document.getElementsByName('jawaban_final').forEach(element => { + if (element.checked) { + the_answer = { + key: element.id.split('_')[1], + }; + } + }); + if(the_answer) { + formData.append('jawaban_final', the_answer.key); + formData.append('_token', token); + formData.append('question', num); + formData.append('id_question', $('#id_question').attr('value')); + formData.append('session_id', SESSION_ID); + xhttp.open("POST", "{{ route('ujian.store') }}", false); + xhttp.send(formData); + } stopTobii(); if(!url_temp.includes('question')) { window.location.href = url_temp + '&question=' + num; @@ -87,6 +130,55 @@ function changeQuestion(num) { } } + function getCurrentAnswer() { + let id_question = $('#id_question').attr('value'); + let xhttp = new XMLHttpRequest(); + let current_num = new URLSearchParams(window.location.search).get('question'); + + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + let response = JSON.parse(this.responseText); + document.getElementById('nomor_' + current_num).classList.remove('bg-light-subtle'); + document.getElementById('nomor_' + current_num).classList.remove('bg-secondary-subtle'); + document.getElementById('nomor_' + current_num).classList.add('bg-info'); + document.getElementById('nomor_' + current_num).classList.add('text-white'); + if(response.status == 'success') { + document.getElementsByName('jawaban_final').forEach(element => { + if (element.id.split('_')[1] == response.jawaban) { + element.checked = true; + } + }); + } + } + }; + xhttp.open("GET", "{{ route('ujian.getCurrentAnswer') }}?id_question=" + id_question + "&session_id=" + SESSION_ID, false); + xhttp.send(); + } + + function getTimeLeft() { + let xhttp = new XMLHttpRequest(); + + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + let response = JSON.parse(this.responseText); + UNIX_END_TIME = response.time_left; + } + }; + xhttp.open("GET", "{{ route('ujian.getTimeLeft') }}?session_id=" + SESSION_ID, false); + xhttp.send(); + } + + function updateTimeLeft() { + let time_left = UNIX_END_TIME - Math.floor(Date.now() / 1000); + if(time_left < 0) { + time_left = 0; + finishExam(); + } + let minutes = Math.floor(time_left / 60); + let seconds = time_left % 60; + document.getElementById('time_left').innerText = ' ' + minutes + ':' + (seconds < 10 ? '0' : '') + seconds; + } + function finishExam() { ws.send(JSON.stringify({ type: "command", @@ -139,7 +231,34 @@ function stopTobii() { session_id: SESSION_ID })); + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + + if(data.type === 'reset_start_tobii') { + startTobii(); + } + + if(data.type === 'status_tobii_device') { + if(data.status) { + console.log('Tobii device is connected'); + // document.getElementById('status_tobii_device').innerText = 'Connected'; + document.getElementById('bg_status_tobii').classList.remove('bg-warning'); + document.getElementById('bg_status_tobii').classList.remove('bg-danger'); + document.getElementById('bg_status_tobii').classList.add('bg-success'); + } else { + console.log('Tobii device is not connected'); + // document.getElementById('status_tobii_device').innerText = 'Not Connected'; + document.getElementById('bg_status_tobii').classList.remove('bg-success'); + document.getElementById('bg_status_tobii').classList.remove('bg-warning'); + document.getElementById('bg_status_tobii').classList.add('bg-danger'); + } + } + }; + startTobii(); + getCurrentAnswer(); + getTimeLeft(); + setInterval(updateTimeLeft, 1000); }; }); diff --git a/routes/web.php b/routes/web.php index 414fa24..d935e0a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,6 +38,8 @@ Route::resource('tobii-receiver', TobiiReceiverController::class)->only(['store', 'show']); Route::get('tobii-receiver/fetch/data/{id_test_sessions}', [TobiiReceiverController::class, 'fetchDataTobii'])->name('tobii-receiver.fetchData'); Route::resource('ujian', HalamanUjianController::class); + Route::get('ujian/get/current-answer', [HalamanUjianController::class, 'getCurrentAnswer'])->name('ujian.getCurrentAnswer'); + Route::get('ujian/get/time-left', [HalamanUjianController::class, 'getTimeLeft'])->name('ujian.getTimeLeft'); Route::middleware(['auth', 'role.auth'])->group(function() { Route::resource('dashboard', DashboardController::class); Route::resource('users', UsersController::class);