mutu-rsab/resources/views/soal/index.blade.php

453 lines
16 KiB
PHP

@extends('layouts.template')
@section('title', 'Kuesioner Soal')
@section('custom_css')
<style>
.question-card {
border: 1px solid #dee2e6;
border-radius: 12px;
padding: 1.25rem;
background: #fff;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.04);
}
.option-scroll {
max-height: 220px;
overflow-y: auto;
border: 1px dashed #ced4da;
border-radius: 0.65rem;
padding: 0.75rem 1rem;
background-color: #f8f9fa;
}
.option-scroll::-webkit-scrollbar {
width: 6px;
}
.option-scroll::-webkit-scrollbar-thumb {
background-color: rgba(13, 110, 253, 0.4);
border-radius: 3px;
}
.lainnya-input {
display: none;
}
.lainnya-input.show {
display: block;
margin-top: 0.85rem;
}
</style>
@endsection
@section('content')
@php
$listHal = $daftarHal ?? collect([$hal]);
$posisiAktif = $listHal->search($hal);
if ($posisiAktif === false) {
$posisiAktif = 0;
}
$totalHalaman = $totalHal ?? $listHal->count();
$progressPercentage = $totalHalaman > 0 ? round((($posisiAktif + 1) / max($totalHalaman, 1)) * 100) : 100;
$halSebelumnya = $posisiAktif > 0 ? $listHal[$posisiAktif - 1] : null;
$halBerikut = ($posisiAktif < $listHal->count() - 1) ? $listHal[$posisiAktif + 1] : null;
$isHalTerakhir = $halBerikut === null;
@endphp
<div class="py-4">
<div class="row justify-content-center">
<div class="col-xl-8 col-lg-10">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mb-3">
<div>
<p class="text-muted mb-1">Kuesioner</p>
<h3 class="fw-semibold mb-0">{{ $soal->judul_soal ?? 'Daftar Soal' }}</h3>
</div>
<div class="d-flex align-items-center gap-2">
<a href="{{ route('soal.index') }}" class="btn btn-outline-secondary btn-sm">
&larr; Daftar Judul
</a>
<span class="badge bg-label-primary fs-6 px-3 py-2" id="badge-hal">Halaman {{ $hal }}</span>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between small text-muted mb-1">
<span>Progres Halaman</span>
<span id="progress-text">{{ $progressPercentage }}%</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" id="progress-bar" role="progressbar" style="width: {{ $progressPercentage }}%;" aria-valuenow="{{ $progressPercentage }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
@if ($hal === $halPertama)
<div class="card border-0 shadow-sm mb-4" id="head_soal">
<div class="card-body">
<p class="text-muted mb-1">Judul Soal</p>
<h5 class="mb-3 text-primary">{{ $soal->judul_soal ?? '-' }}</h5>
<p class="text-muted mb-2">Keterangan</p>
<div class="border rounded p-3 bg-light text-body">
{!! $soal->keterangan_soal !!}
</div>
</div>
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>Terjadi kesalahan.</strong> Silakan periksa kembali jawaban Anda.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h5 class="mb-0">Daftar Pertanyaan</h5>
<small class="text-muted" id="summary-hal">Halaman {{ $hal }} dari {{ $listHal->count() }}</small>
</div>
<form id="form-soal" method="POST" action="{{ route('soal.store') }}" data-hal-list='@json($listHal->values())'>
@csrf
<input type="hidden" name="lms_mutu_soal_id" value="{{ $soal->id }}">
<input type="hidden" name="hal" id="input-hal" value="{{ $hal }}">
@forelse ($soal->soalDetail as $detail)
@php
$detailConfig = json_decode($detail->soal, true) ?? [];
$pertanyaan = $detailConfig['soal'] ?? 'Pertanyaan tidak tersedia';
$type = $detailConfig['type'] ?? 'option';
$options = $detailConfig['options'] ?? [];
$oldAnswer = old('jawaban.' . $detail->id);
$oldOtherAnswer = old('jawaban_lainnya.' . $detail->id);
$showLainnya = $oldAnswer === 'Lainnya' || (!empty($oldOtherAnswer));
$detailHal = $detail->hal ?? $listHal->first();
$isVisible = $detailHal == $hal;
@endphp
<div class="question-card mb-4"
data-hal-card="{{ $detailHal }}"
style="{{ $isVisible ? '' : 'display: none;' }}">
<div class="d-flex align-items-center justify-content-between mb-2 flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<span class="badge rounded-pill bg-label-primary fs-6">{{ $loop->iteration }}</span>
</div>
</div>
<h5 class="fw-semibold mb-3">{{ $pertanyaan }}</h5>
<div>
@if ($type === 'textarea')
<textarea class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" rows="4" required
data-field-hal="{{ $detailHal }}"
placeholder="Tulis jawaban Anda di sini">{{ old('jawaban.' . $detail->id) }}</textarea>
@elseif ($type === 'text')
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ old('jawaban.' . $detail->id) }}" required
data-field-hal="{{ $detailHal }}"
placeholder="Masukkan jawaban Anda">
@else
@if (!empty($options))
<div class="option-scroll">
@foreach ($options as $optionIndex => $option)
@php
$optionId = 'jawaban-' . $detail->id . '-' . $optionIndex;
$isLainnya = strtolower(trim($option)) === 'lainnya';
@endphp
<div class="form-check mb-2">
<input class="form-check-input @error('jawaban.' . $detail->id) is-invalid @enderror"
type="radio"
name="jawaban[{{ $detail->id }}]"
id="{{ $optionId }}"
value="{{ $option }}"
data-lainnya-radio="{{ $isLainnya ? $detail->id : '' }}" required
data-field-hal="{{ $detailHal }}"
{{ $oldAnswer === $option ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $optionId }}">
{{ $option }}
</label>
</div>
@if ($isLainnya)
<div class="lainnya-input {{ $showLainnya ? 'show' : '' }}" data-lainnya-wrapper="{{ $detail->id }}">
<input type="text" class="form-control form-control-sm mt-2"
name="jawaban_lainnya[{{ $detail->id }}]"
value="{{ $oldOtherAnswer }}"
data-lainnya-input="{{ $detail->id }}"
data-field-hal="{{ $detailHal }}"
placeholder="Tuliskan jawaban lainnya"
{{ $showLainnya ? 'required' : '' }}>
</div>
@endif
@endforeach
</div>
@else
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ old('jawaban.' . $detail->id) }}" required
data-field-hal="{{ $detailHal }}"
placeholder="Masukkan jawaban Anda">
@endif
@endif
@error('jawaban.' . $detail->id)
<div class="invalid-feedback">{{ $message }}</div>
@enderror
@error('jawaban_lainnya.' . $detail->id)
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
</div>
</div>
@empty
<div class="alert alert-info">
Belum ada pertanyaan pada halaman ini.
</div>
@endforelse
@error('lms_mutu_soal_id')
<p class="text-danger mb-0">{{ $message }}</p>
@enderror
@error('jawaban')
<p class="text-danger mb-0">{{ $message }}</p>
@enderror
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mt-4 gap-3">
<div class="d-flex gap-2">
<button type="button"
class="btn btn-outline-secondary btn-prev-hal"
data-nav-control="prev"
{{ $halSebelumnya ? '' : 'disabled' }}>
&larr; Halaman Sebelumnya
</button>
<button type="button"
class="btn btn-outline-secondary btn-next-hal"
data-nav-control="next"
{{ $halBerikut ? '' : 'disabled' }}>
Halaman Berikutnya &rarr;
</button>
</div>
<button type="submit" class="btn btn-primary ms-md-auto"
data-final-submit="true"
style="{{ $isHalTerakhir ? '' : 'display: none;' }}"
{{ $soal->soalDetail->isEmpty() ? 'disabled' : '' }}>
Simpan Jawaban
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('custom_js')
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('form-soal');
if (!form) {
return;
}
let halList;
try {
halList = JSON.parse(form.dataset.halList || '[]');
} catch (error) {
halList = [];
}
if (!Array.isArray(halList) || !halList.length) {
halList = [parseInt(document.getElementById('input-hal')?.value || '1', 10) || 1];
}
halList = halList.map(function (value) {
return parseInt(value, 10);
});
const hiddenHalInput = document.getElementById('input-hal');
let currentHal = parseInt(hiddenHalInput?.value || halList[0], 10);
const navHalButtons = document.querySelectorAll('.nav-hal-btn');
const questionCards = document.querySelectorAll('[data-hal-card]');
const halInputs = document.querySelectorAll('[data-field-hal]');
const prevButton = document.querySelector('.btn-prev-hal');
const nextButton = document.querySelector('.btn-next-hal');
const finalSubmitButton = document.querySelector('[data-final-submit="true"]');
const badgeHal = document.getElementById('badge-hal');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const summaryHal = document.getElementById('summary-hal');
const totalHal = halList.length || 1;
setupLainnyaInputs();
updateQuestionVisibility();
updateNavigationUI();
navHalButtons.forEach(function (button) {
button.addEventListener('click', function () {
const targetHal = parseInt(button.dataset.navHal, 10);
changeHal(targetHal);
});
});
if (prevButton) {
prevButton.addEventListener('click', function () {
navigateRelative(-1);
});
}
if (nextButton) {
nextButton.addEventListener('click', function () {
navigateRelative(1);
});
}
function navigateRelative(step) {
const currentIndex = halList.indexOf(currentHal);
const targetHal = halList[currentIndex + step];
if (typeof targetHal === 'undefined') {
return;
}
if(targetHal === 1){
document.getElementById('head_soal').classList.remove('d-none')
}else{
document.getElementById('head_soal').classList.add('d-none')
}
changeHal(targetHal);
}
function changeHal(targetHal) {
if (targetHal === currentHal || halList.indexOf(targetHal) === -1) {
return;
}
const currentIndex = halList.indexOf(currentHal);
const targetIndex = halList.indexOf(targetHal);
const movingForward = targetIndex > currentIndex;
if (movingForward && !validateHal(currentHal)) {
return;
}
currentHal = targetHal;
if (hiddenHalInput) {
hiddenHalInput.value = currentHal;
}
updateQuestionVisibility();
updateNavigationUI();
}
function validateHal(halValue) {
const fieldsToDisable = [];
halInputs.forEach(function (field) {
const fieldHal = parseInt(field.dataset.fieldHal, 10);
if (fieldHal !== halValue && field.required) {
field.dataset.tmpRequired = '1';
field.required = false;
fieldsToDisable.push(field);
}
});
const isValid = form.checkValidity();
if (!isValid) {
form.reportValidity();
}
fieldsToDisable.forEach(function (field) {
field.required = true;
delete field.dataset.tmpRequired;
});
return isValid;
}
function updateQuestionVisibility() {
questionCards.forEach(function (card) {
const halValue = parseInt(card.dataset.halCard, 10);
card.style.display = halValue === currentHal ? '' : 'none';
});
}
function updateNavigationUI() {
const currentIndex = halList.indexOf(currentHal);
const isFirst = currentIndex <= 0;
const isLast = currentIndex === halList.length - 1;
if (prevButton) {
prevButton.disabled = isFirst;
}
if (nextButton) {
nextButton.disabled = isLast;
}
navHalButtons.forEach(function (button) {
if (parseInt(button.dataset.navHal, 10) === currentHal) {
button.classList.remove('btn-outline-primary');
button.classList.add('btn-primary');
} else {
button.classList.add('btn-outline-primary');
button.classList.remove('btn-primary');
}
});
const progress = totalHal > 0 ? Math.round(((currentIndex + 1) / totalHal) * 100) : 100;
if (badgeHal) {
badgeHal.textContent = 'Halaman ' + currentHal;
}
if (progressBar) {
progressBar.style.width = progress + '%';
progressBar.setAttribute('aria-valuenow', progress);
}
if (progressText) {
progressText.textContent = progress + '%';
}
if (summaryHal) {
summaryHal.textContent = 'Halaman ' + (currentIndex + 1) + ' dari ' + totalHal;
}
if (finalSubmitButton) {
finalSubmitButton.style.display = isLast ? '' : 'none';
}
}
function setupLainnyaInputs() {
document.querySelectorAll('[data-lainnya-radio]').forEach(function (radio) {
radio.addEventListener('change', function (event) {
const detailId = event.target.getAttribute('data-lainnya-radio');
if (!detailId) {
return;
}
const targetWrapper = document.querySelector('[data-lainnya-wrapper="' + detailId + '"]');
if (targetWrapper) {
handleLainnyaInput(targetWrapper, true);
}
});
});
document.querySelectorAll('input[type="radio"]').forEach(function (radio) {
radio.addEventListener('change', function (event) {
const detailId = event.target.name.replace('jawaban[', '').replace(']', '');
const targetWrapper = document.querySelector('[data-lainnya-wrapper="' + detailId + '"]');
if (targetWrapper && event.target.value !== 'Lainnya') {
handleLainnyaInput(targetWrapper, false, true);
}
});
});
}
function handleLainnyaInput(wrapper, show, clearValue = false) {
if (!wrapper) {
return;
}
wrapper.classList.toggle('show', show);
const input = wrapper.querySelector('input');
if (input) {
input.required = show;
if (show) {
input.focus();
} else if (clearValue) {
input.value = '';
}
}
}
});
</script>
@endsection