Compare commits

..

7 Commits

19 changed files with 1047 additions and 77 deletions

View File

@ -81,7 +81,7 @@ public function store(Request $request)
'no_telp' => $request->no_telp,
'tgl_booking' => $request->tgl_booking,
'deskripsi_tambahan' => $request->deskripsi_tambahan,
'kode_unik' => $kode_unik
'kode_unik' => $kode_unik,
]);
return response()->json([

View File

@ -38,6 +38,11 @@ public function show(string $id)
{
$data = BookingModel::where('kode_unik', $id)->first();
if($data != null) {
if($data->status_tes == '1' && $data->unix_expired_time < time()) {
$data->update([
'status_tes' => '0'
]);
}
return response()->json([
'status' => true,
'msg' => 'Data ditemukan (' . $id . ')',
@ -46,6 +51,7 @@ public function show(string $id)
'nama_lengkap' => $data->nama_lengkap,
'kode_unik' => $data->kode_unik,
'tgl_booking' => $data->tgl_booking,
'jam_booking' => date('H:i:s', strtotime($data->created_at)),
'deskripsi_tambahan' => $data->deskripsi_tambahan,
]);
} else {

View File

@ -0,0 +1,137 @@
<?php
namespace App\Http\Controllers;
use App\Models\DataSoalModel;
use App\Models\DetailTestModel;
use App\Models\TestSessionsModel;
use Illuminate\Http\Request;
class FinishUjianController extends Controller
{
public function getAllAnswer(Request $request) {
$data_session = TestSessionsModel::where('id_test_sessions', $request->id_test_sessions)->first();
if($data_session != null) {
if($data_session->status_session != '1') {
return response()->json([
'status' => false,
'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') {
$detail_test = DetailTestModel::where('id_test_sessions', $request->id_test_sessions)->get();
$data_ujian = [];
foreach($detail_test as $detail) {
$data_soal = DataSoalModel::where('id_soal', $detail->id_soal)->first();
$data_jawaban = $detail->jawaban;
if($data_jawaban == 'a') {
$data_jawaban = $data_soal->jawaban_a;
} else if($data_jawaban == 'b') {
$data_jawaban = $data_soal->jawaban_b;
} else if($data_jawaban == 'c') {
$data_jawaban = $data_soal->jawaban_c;
} else if($data_jawaban == 'd') {
$data_jawaban = $data_soal->jawaban_d;
} else if($data_jawaban == 'e') {
$data_jawaban = $data_soal->jawaban_e;
} else {
$data_jawaban = 'Tidak menjawab';
}
$data_ujian[] = [
'id_soal' => $detail->id_soal,
'jawaban' => $data_jawaban,
'jawaban_key' => $detail->jawaban,
'soal' => $data_soal->pertanyaan
];
}
return response()->json([
'status' => true,
'data' => $data_ujian
]);
}
} else {
return response()->json([
'status' => false,
'message' => 'Data sesi tes tidak ditemukan.'
]);
}
}
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$id_session = null;
if(session('id_test_sessions') != null) {
$id_session = session('id_test_sessions');
} else if($request->id_test_sessions != null) {
$id_session = $request->id_test_sessions;
} else {
return redirect()->route('cek-status.index')->with('error', 'Data sesi tes tidak ditemukan. Silakan mulai tes terlebih dahulu.');
}
$data_session = TestSessionsModel::where('id_test_sessions', $id_session)->first();
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') {
// $data_session->update([
// 'status_session' => '2',
// 'is_active' => '0'
// ]);
return view('user.pages.finish_ujian.index', compact('id_session'));
}
} else {
return redirect()->route('cek-status.index')->with('error', 'Data sesi tes tidak ditemukan.');
}
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@ -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,6 +88,22 @@ 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.'
]);
}
}
/**

View File

@ -22,7 +22,8 @@ class BookingModel extends Model
'kode_unik',
'tgl_booking',
'deskripsi_tambahan',
'status_tes'
'status_tes',
'unix_expired_time'
];
protected $casts = [
@ -38,7 +39,11 @@ protected static function boot()
}
if(empty($model->status_tes)) {
$model->status_tes = '0';
$model->status_tes = '1';
}
if(empty($model->unix_expired_time)) {
$model->unix_expired_time = (string) (time() + 86400); // Set expiration time to 24 hour from now
}
});
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class DetailTestModel extends Model
{
protected $table = 'detail_test';
protected $primaryKey = 'id_detail_test';
protected $keyType = 'string';
public $incrementing = false;
protected $fillable = [
'id_detail_test',
'id_soal',
'id_test_sessions',
'jawaban'
];
protected $casts = [
'id_detail_test' => 'string',
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->id_detail_test)) {
$model->id_detail_test = Str::uuid();
}
});
}
}

View File

@ -18,7 +18,9 @@ class TestSessionsModel extends Model
'status_session',
'token_session',
'id_booking',
'unix_start_time',
'is_active',
'unix_end_time',
];
protected $casts = [

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('detail_test', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('detail_test', function (Blueprint $table) {
$table->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']);
});
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('test_sessions', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('soal', function (Blueprint $table) {
$table->string('jawaban_a')->nullable()->change();
$table->string('jawaban_b')->nullable()->change();
$table->string('jawaban_c')->nullable()->change();
$table->string('jawaban_d')->nullable()->change();
$table->string('jawaban_e')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('soal', function (Blueprint $table) {
$table->string('jawaban_a')->change();
$table->string('jawaban_b')->change();
$table->string('jawaban_c')->change();
$table->string('jawaban_d')->change();
$table->string('jawaban_e')->change();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('booking', function (Blueprint $table) {
$table->char('unix_expired_time', 20)->after('kode_unik');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('booking', function (Blueprint $table) {
$table->dropColumn('unix_expired_time');
});
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -28,6 +28,8 @@
<link href="{{ asset('assets/users/assets/vendor/fontawesome-free/css/all.min.css') }}" rel="stylesheet">
<link href="{{ asset('assets/users/assets/vendor/swiper/swiper-bundle.min.css') }}" rel="stylesheet">
<link href="https://cdn.datatables.net/2.3.7/css/dataTables.dataTables.min.css" rel="stylesheet" />
<!-- Main CSS File -->
<link href="{{ asset('assets/users/assets/css/main.css') }}" rel="stylesheet">
@ -60,6 +62,7 @@ class="bi bi-arrow-up-short"></i></a>
<div id="preloader"></div>
<!-- Vendor JS Files -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="{{ asset('assets/users/assets/vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
@ -69,6 +72,9 @@ class="bi bi-arrow-up-short"></i></a>
<script src="{{ asset('assets/users/assets/vendor/purecounter/purecounter_vanilla.js') }}"></script>
<script src="{{ asset('assets/users/assets/vendor/swiper/swiper-bundle.min.js') }}"></script>
<script src="https://cdn.datatables.net/2.3.7/js/dataTables.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/heatmap.js/2.0.1/heatmap.min.js"></script>
<!-- Main JS File -->
<script src="{{ asset('assets/users/assets/js/main.js') }}"></script>

View File

@ -58,7 +58,7 @@
<ul class="list-group list-group-flush">
<li class="list-group-item">Nama Lengkap: <span id="nama_lengkap"></span></li>
<li class="list-group-item">Kode Unik: <span id="kode_unik"></span></li>
<li class="list-group-item">Tgl Booking: <span id="tgl_booking"></span></li>
<li class="list-group-item">Tgl dan Jam Booking: <span id="tgl_booking"></span> <span id="jam_booking"></span></li>
<li class="list-group-item">Deskripsi Tambahan: <span id="deskripsi"></span></li>
<li class="list-group-item">Status Tes: <span id="status"></span></li>
</ul>
@ -137,28 +137,29 @@ function searchUserStatus() {
xhttp.onreadystatechange = function() {
if(this.readyState == 4 && this.status == 200) {
let response = JSON.parse(this.responseText);
let status_dict = ['Belum dikonfirmasi (cek email anda)', 'Sudah dikonfirmasi, belum melakukan/menyelesaikan tes', 'Sudah melakukan tes'];
let status_dict = ['Sesi anda dinonaktifkan karena telah kedaluwarsa', 'Belum melakukan/menyelesaikan tes', 'Sudah melakukan tes'];
if(response.status) {
$('#result_data').show();
$('#nama_lengkap').html(response.nama_lengkap);
$('#kode_unik').html(response.kode_unik);
$('#tgl_booking').html(response.tgl_booking);
$('#jam_booking').html(response.jam_booking + " (waktu sistem)");
$('#deskripsi').html(response.deskripsi_tambahan);
$('#status').html(status_dict[response.status_data]);
Swal.close();
if(response.status_data == 0) {
$('#alert_status').removeClass();
$('#alert_status').addClass('alert alert-primary');
$('#alert_status').html('Kamu belum dikonfirmasi dengan psikolog, cek waktu konsultasi pada email anda untuk melakukan aktivasi!');
$('#alert_status').addClass('alert alert-warning');
$('#alert_status').html('Session kamu dinonaktifkan!');
} else if(response.status_data == 1) {
$('#alert_status').removeClass();
$('#alert_status').addClass('alert alert-info');
$('#alert_status').html('Kamu sudah dikonfirmasi oleh psikolog!');
$('#alert_status').html('Kamu bisa memulai tes sekarang!');
let test_session = getTestSession(response.id_booking);
$('#result_test').show();
$('#result_test_title').html('Kamu sudah dikonfirmasi oleh psikolog! Kalau kamu sudah siap menjalankan tes, klik tombol di bawah untuk mulai tes kecemasan dari sistem kami!');
$('#result_test_title').html('Kalau kamu sudah siap menjalankan tes, klik tombol di bawah untuk mulai tes kecemasan dari sistem kami!');
if(test_session.status) {
if(test_session.status_session == 0) {
$('#id_booking_input').val(response.id_booking);
@ -183,7 +184,7 @@ function searchUserStatus() {
} else if(!test_session.status) {
$('#id_booking_input').val(response.id_booking);
$('#result_test').show();
$('#result_test_title').html('Kamu sudah dikonfirmasi oleh psikolog! Kalau kamu sudah siap menjalankan tes, klik tombol di bawah untuk mulai tes kecemasan dari sistem kami!');
$('#result_test_title').html('Kalau kamu sudah siap menjalankan tes, klik tombol di bawah untuk mulai tes kecemasan dari sistem kami!');
$('#result_test_status').html('Belum Dimulai');
$('#result_test_status').removeClass();

View File

@ -0,0 +1,231 @@
@section('title', 'Hasil Tes')
@extends('user.layouts.index')
@section('content')
<!-- Page Title -->
<div class="page-title">
<div class="heading">
<div class="container">
<div class="row d-flex justify-content-center text-center">
<div class="col-lg-8">
<h1 class="heading-title">Hasil Tes Anda</h1>
<p class="mb-0">
Cek hasil tes kamu dari sini ya, jika kamu sudah mendapatkan kode dari email setelah melakukan booking, kamu bisa melihat hasil tes disini dengan kode tadi.
</p>
</div>
</div>
</div>
</div>
<nav class="breadcrumbs">
<div class="container">
<ol>
<li><a href="index.html">Home</a></li>
<li class="current">Hasil Tes</li>
</ol>
</div>
</nav>
</div><!-- End Page Title -->
<!-- About Section -->
<section id="about" class="about section">
<div class="container" data-aos="fade-up" data-aos-delay="100">
<table class="table" id="table-data-hasil-ujian">
<thead>
<tr>
<th>No</th>
<th>Pertanyaan</th>
<th>Jawaban</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h2 class="title">Grafik Kecepatan Mata</h2>
<canvas id="velocityChart"></canvas>
</div>
<div class="col-lg-12 mt-5">
<h2 class="title">Heatmap Pergerakan Mata</h2>
<div style="position: relative; aspect-ratio: 16/9;">
<img src="{{ asset('assets/users/assets/img/overlay/overlay.png') }}" style="width:100%; height:100%; object-fit: cover;">
<div id="heatmapChart" style="aspect-ratio: 16/9;"></div>
</div>
</div>
</div>
</div>
</section><!-- /About Section -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto"><i class="bi bi-exclamation-triangle"></i> Kesalahan</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ session('error') }}
</div>
</div>
</div>
@endsection
@section('script')
<script>
const ws = new WebSocket('{{ env("WEBSOCKET_URL") }}');
let table = null;
let data_tobii = null;
let heatmapContainer = null;
let clientWidth = null;
let clientHeight = null;
let mappedHeatmapData = null;
let scaleX = null;
let scaleY = null;
let heatmapInstance = null;
function renderVelocityChart(timeseries) {
const base = timeseries.timestamp[0];
const labels = timeseries.timestamp.map(t => (t - base) / 1000);
const ctx = document.getElementById('velocityChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Velocity',
data: timeseries.velocity,
borderWidth: 2,
tension: 0.3
}]
},
options: {
responsive: true,
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return "Velocity: " + context.raw.toFixed(4);
}
}
}
},
scales: {
x: {
display: false
}
}
}
});
}
async function loadData(data) {
const res = await fetch("{{ env('PYTHON_API_URL') }}/extract", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
const result = await res.json();
if(result != null) {
renderVelocityChart(result.timeseries);
mappedHeatmapData = result.heatmap.map(point => ({
x: point.x * scaleX,
y: point.y * scaleY,
value: point.value
}));
heatmapInstance.setData({
max: 10,
data: mappedHeatmapData
});
heatmapContainer.style.removeProperty('position');
}
}
$(document).ready(function() {
table = new DataTable("#table-data-hasil-ujian", {
searching: false,
paging: false,
ajax: {
url: '{{ route('hasil-tes.getAllAnswer', ['id_test_sessions' => $id_session]) }}',
dataSrc: 'data' // Specify the key containing the data array
},
columns: [
{
data: null,
render: function(data, type, row, meta) {
return meta.row + 1;
}
},
{
data: 'soal'
},
{
data: null,
render: function(data, type, row) {
let renderedJawaban = null;
if(row.jawaban_key == 'a') {
renderedJawaban = `<span class="badge text-bg-primary">${row.jawaban}</span>`;
} else if(row.jawaban_key == 'b') {
renderedJawaban = `<span class="badge" style="background-color: #fc7938; color: white;">${row.jawaban}</span>`;
} else if(row.jawaban_key == 'c') {
renderedJawaban = `<span class="badge text-bg-danger">${row.jawaban}</span>`;
} else if(row.jawaban_key == 'd') {
renderedJawaban = `<span class="badge text-bg-warning">${row.jawaban}</span>`;
} else if(row.jawaban_key == 'e') {
renderedJawaban = `<span class="badge text-bg-success">${row.jawaban}</span>`;
} else {
renderedJawaban = `<span class="badge text-bg-secondary">${row.jawaban}</span>`;
}
return renderedJawaban;
}
}
]
});
ws.onopen = () => {
console.log("Connected to WebSocket");
ws.send(JSON.stringify({
type: 'get_json_file',
session_id: '{{ $id_session }}'
}));
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if(data.type === 'json_file_data') {
if(data.data != null) {
data_tobii = data.data;
heatmapContainer = document.getElementById('heatmapChart');
clientWidth = heatmapContainer.clientWidth;
clientHeight = heatmapContainer.clientHeight;
scaleX = clientWidth / 1366;
scaleY = clientHeight / 768;
heatmapInstance = h337.create({
container: heatmapContainer,
radius: 40,
maxOpacity: 0.6,
blur: 0.85
});
loadData(data_tobii);
}
}
};
};
});
</script>
@if (session('error'))
<script>
$(document).ready(function() {
var toast = new bootstrap.Toast(document.getElementById('liveToast'));
toast.show();
});
</script>
@endif
@endsection

View File

@ -140,7 +140,7 @@
<script>
let getDataInterval = false;
let checkTobiiInterval = null;
let tobiiConnectedStatus = true;
let tobiiConnectedStatus = false;
const ws = new WebSocket('{{ env("WEBSOCKET_URL") }}');
let SESSION_ID = '{{ $data_session->id_test_sessions }}';
let TOKEN_SESSION = '{{ $data_session->token_session }}';
@ -151,9 +151,10 @@ function showTutorial(step, tutorialId) {
if(step === 2 || step === 3) {
startTobii();
checkTobiiInterval = setInterval(() => {
checkTobiiConnection();
}, 2000);
// checkTobiiInterval = setInterval(() => {
// checkTobiiConnection();
// }, 2000);
} else {
if(checkTobiiInterval) {
clearInterval(checkTobiiInterval);
@ -282,10 +283,10 @@ function copyLinkTokenTobii() {
}
function checkTobiiConnection() {
ws.send(JSON.stringify({
type: "check_tobii_connection",
session_id: SESSION_ID,
}));
// ws.send(JSON.stringify({
// type: "check_tobii_connection",
// session_id: SESSION_ID,
// }));
if(tobiiConnectedStatus) {
$('#status_tobii').text('Status Tobii: Terhubung').removeClass('text-danger').addClass('text-success');
@ -353,15 +354,6 @@ function stopTobii() {
const data = JSON.parse(event.data);
console.log('Received data from WebSocket:', data);
// if(data.type === 'temp_data') {
// console.log('Received temp_data:', data);
// if((Date.now() - data.data[0].timestamp) < 5000) {
// tobiiConnectedStatus = true;
// } else {
// tobiiConnectedStatus = false;
// }
// }
if(data.type === 'tobii_data') {
try {
moveTheEye(data.gaze_x, data.gaze_y);
@ -370,13 +362,16 @@ function stopTobii() {
}
}
// if(data.type === 'status_tobii') {
// if(data.status) {
// tobiiConnectedStatus = true;
// } else if(!data.status) {
// tobiiConnectedStatus = false;
// }
// }
if(data.type === 'status_tobii_device') {
if(data.status) {
console.log('Tobii device is connected');
tobiiConnectedStatus = true;
} else {
console.log('Tobii device is not connected');
tobiiConnectedStatus = false;
}
checkTobiiConnection();
}
if(data.type === 'reset_start_tobii') {
startTobii();

View File

@ -7,9 +7,19 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary px-2">
<div class="container-fluid">
<a class="navbar-brand" href="#">Smart Anxiety</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<div class="collapse navbar-collapse justify-content-between" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
<div class="d-flex fw-bold">Time Left: <span id="time_left"></span></div>
<div class="d-flex col-md-4">
<div class="col-md-4 d-flex justify-content-center align-items-center">
Time Left: <span id="time_left"></span>
</div>
<div class="col-md-4 d-flex justify-content-center align-items-center">
<span id="bg_status_tobii" class="rounded bg-warning d-block mx-2" style="width: 10px; aspect-ratio: 1/1;"></span>Status Tobii
</div>
<div class="col-md-4 d-flex justify-content-center align-items-center">
<button id="fullscreen_btn" data-status="fullscreen" class="btn btn-sm btn-outline-secondary" onclick="openFullscreen()">Fullscreen</button>
</div>
</div>
</div>
</div>
</nav>
@ -19,38 +29,25 @@
<div class="col-md-9 border-end">
<div class="row justify-content-center flex-column p-4">
<div class="col-md-12 border-bottom fs-4 fw-bold p-2">Soal</div>
<div class="col-md-12 d-none" id="id_question" value="{{ $data_question->id_soal }}"></div>
<div class="col-md-12 p-2" style="text-align: justify">{{ $data_question->pertanyaan }}</div>
<div class="col-md-12">
<div class="fs-4 fw-bold p-2">Jawaban:</div>
@foreach ($answer as $item)
<div class="form-check m-2">
<input class="form-check-input" type="radio" name="jawaban_final" id="jawaban_a">
<label class="form-check-label" for="jawaban_a">{{ $data_question->jawaban_a }}</label>
</div>
<div class="form-check m-2">
<input class="form-check-input" type="radio" name="jawaban_final" id="jawaban_b">
<label class="form-check-label" for="jawaban_b">{{ $data_question->jawaban_b }}</label>
</div>
<div class="form-check m-2">
<input class="form-check-input" type="radio" name="jawaban_final" id="jawaban_c">
<label class="form-check-label" for="jawaban_c">{{ $data_question->jawaban_c }}</label>
</div>
<div class="form-check m-2">
<input class="form-check-input" type="radio" name="jawaban_final" id="jawaban_d">
<label class="form-check-label" for="jawaban_d">{{ $data_question->jawaban_d }}</label>
</div>
<div class="form-check m-2">
<input class="form-check-input" type="radio" name="jawaban_final" id="jawaban_e">
<label class="form-check-label" for="jawaban_e">{{ $data_question->jawaban_e }}</label>
<input class="form-check-input" type="radio" name="jawaban_final" id="jawaban_{{ $item['key'] }}">
<label class="form-check-label" for="jawaban_{{ $item['key'] }}">{{ $item['value'] }}</label>
</div>
@endforeach
</div>
</div>
</div>
<div class="col-md-3">
<div class="row p-3">
<div class="col-md-12 border-bottom fs-4 fw-bold p-2 mb-3">Navigasi Soal</div>
@for ($i = 0; $i < $total_question; $i++)
<div onclick="changeQuestion({{ $i + 1 }})" class="col-md-1 btn d-flex justify-content-center align-items-center bg-light-subtle border rounded p-0 m-1" style="aspect-ratio: 1/1.5;">{{ $i + 1 }}</div>
@endfor
@foreach ($answered_question as $question)
<div onclick="changeQuestion({{ $question['nomor_soal'] }})" id="nomor_{{ $question['nomor_soal'] }}" class="col-md-1 btn d-flex justify-content-center align-items-center {{ $question['answered'] ? 'bg-secondary-subtle' : 'bg-light-subtle' }} border rounded p-0 m-1" style="aspect-ratio: 1/1.5;">{{ $question['nomor_soal'] }}</div>
@endforeach
<div class="col-md-12 p-0 mt-3">
@if ($question_number > 1)
@ -64,6 +61,28 @@
@endif
</div>
</div>
<div class="row p-3">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Instruksi</h5>
<p class="card-text">Silahkan baca soal dengan teliti dan pilih jawaban yang paling tepat.</p>
<div class="card-text d-flex align-items-center">
<span class="col-md-1 bg-light-subtle border rounded p-0 m-1" style="aspect-ratio: 1/1.5;"></span>
<span>adalah tanda bahwa jawaban BELUM diisi.</span>
</div>
<div class="card-text d-flex align-items-center">
<span class="col-md-1 bg-secondary-subtle border rounded p-0 m-1" style="aspect-ratio: 1/1.5;"></span>
<span>adalah tanda bahwa jawaban SUDAH diisi.</span>
</div>
<div class="card-text d-flex align-items-center">
<span class="col-md-1 bg-info border rounded p-0 m-1 text-white" style="aspect-ratio: 1/1.5;"></span>
<span>adalah tanda bahwa anda sedang membaca/menjawab soal ini.</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -75,10 +94,41 @@
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) {
current_num = '{{ $question_number }}';
}
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,7 +137,96 @@ 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');
if(!current_num) {
current_num = '{{ $question_number }}';
}
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() {
Swal.fire({
title: 'Alert',
icon: 'question',
text: 'Apakah Anda yakin ingin mengakhiri ujian? (aksi tidak dapat dikembalikan)',
confirmButtonText: 'Ya',
cancelButtonText: 'Tidak',
showCancelButton: true,
showCloseButton: true,
}).then((result) => {
if (result.isConfirmed) {
event.preventDefault();
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();
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', current_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);
console.log("sending answer (finish)");
}
ws.send(JSON.stringify({
type: "command",
action: "stop",
@ -105,6 +244,10 @@ function finishExam() {
status_sessions: false,
session_id: SESSION_ID
}));
window.location.href = "{{ route('hasil-tes.index') }}?id_test_sessions=" + SESSION_ID;
}
});
}
function startTobii() {
@ -123,6 +266,63 @@ function stopTobii() {
}));
}
function openFullscreen() {
const status = $('#fullscreen_btn').attr('data-status');
const elem = document.documentElement;
if (status === 'fullscreen') {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen(); // Safari
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen(); // IE
}
$('#fullscreen_btn').attr('data-status', 'exit-fullscreen');
$('#fullscreen_btn').text('Exit Fullscreen');
s.send(JSON.stringify({
type: "command",
action: "start",
session_id: SESSION_ID
}));
} else if (status === 'exit-fullscreen') {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen(); // Safari
} else if (document.msExitFullscreen) {
document.msExitFullscreen(); // IE
}
$('#fullscreen_btn').attr('data-status', 'fullscreen');
$('#fullscreen_btn').text('Enter Fullscreen');
}
}
function checkFullscreen() {
if (!document.fullscreenElement) {
ws.send(JSON.stringify({
type: "command",
action: "stop",
session_id: SESSION_ID
}));
Swal.fire({
title: 'Alert',
icon: 'warning',
text: 'Anda keluar dari mode fullscreen, harap masuk ke mode fullscreen untuk melanjutkan ujian.',
confirmButtonText: 'Ya',
showCancelButton: false,
showCloseButton: false,
allowOutsideClick: false,
allowEscapeKey: false,
backdrop: `rgba(0, 0, 0, 0.4)`
}).then((result) => {
if (result.isConfirmed) {
openFullscreen();
}
});
}
}
$(document).ready(function() {
ws.onopen = () => {
console.log("Connected to WebSocket");
@ -133,13 +333,43 @@ function stopTobii() {
role: "web"
}));
document.addEventListener('fullscreenchange', checkFullscreen);
checkFullscreen();
ws.send(JSON.stringify({
type: "update_status",
status_sessions: true,
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);
};
});
</script>

View File

@ -4,6 +4,7 @@
use App\Http\Controllers\CekStatusController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\DataSoalController;
use App\Http\Controllers\FinishUjianController;
use App\Http\Controllers\HalamanUjianController;
use App\Http\Controllers\HasilTesController;
use App\Http\Controllers\LandingController;
@ -38,6 +39,10 @@
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::resource('hasil-tes', FinishUjianController::class)->only(['index', 'show']);
Route::get('hasil-tes/get/all-answer', [FinishUjianController::class, 'getAllAnswer'])->name('hasil-tes.getAllAnswer');
Route::middleware(['auth', 'role.auth'])->group(function() {
Route::resource('dashboard', DashboardController::class);
Route::resource('users', UsersController::class);