progress penggaris

This commit is contained in:
JokoPrasetio 2025-12-03 15:47:01 +07:00
parent 915d3d8f8a
commit 7a44867011
4 changed files with 535 additions and 253 deletions

View File

@ -92,7 +92,7 @@ class SoalController extends Controller
$formLocked = true;
}
}
$prefillJawaban = $this->generatePrefillJawaban($detailSoal, $pegawai);
$prefillJawaban = [];
return view('soal.index', [
'soal' => $soal,
'hal' => $hal,
@ -100,7 +100,7 @@ class SoalController extends Controller
'daftarHal' => $daftarHal,
'totalHal' => $daftarHal->count(),
'soalId' => $soal->id,
'pegawai' => $pegawai,
'pegawai' => [],
'prefillJawaban' => $prefillJawaban,
'existingJawaban' => $existingJawaban,
'formLocked' => $formLocked,
@ -112,8 +112,8 @@ class SoalController extends Controller
$validator = Validator::make($request->all(), [
'lms_mutu_soal_id' => 'required|integer',
'hal' => 'nullable|integer|min:1',
'jawaban' => 'required|array',
'jawaban.*' => 'required',
'jawaban' => 'nullable|array',
'jawaban.*' => 'nullable',
'jawaban_lainnya' => 'nullable|array',
'jawaban_lainnya.*' => 'nullable|string',
]);
@ -133,8 +133,9 @@ class SoalController extends Controller
});
$validated = $validator->validate();
$jawabanUtama = $validated['jawaban'] ?? [];
$detailIds = array_map('intval', array_keys($validated['jawaban']));
$detailIds = array_map('intval', array_keys($jawabanUtama));
$detailMeta = SoalDetail::whereIn('id', $detailIds)
->get(['id', 'soal'])
->keyBy('id');
@ -144,16 +145,16 @@ class SoalController extends Controller
$namaResponden = null;
$unitKerja = null;
$pegawai = session('pegawai') ?? [];
$pegawaiId = is_array($pegawai) ? ($pegawai['id'] ?? null) : null;
$pegawaiId = is_array($pegawai) ? ($pegawai['id'] ?? null) : 1;
DB::connection('dbLmsMutu')->transaction(function () use (&$jawabanBaru, $validated, $jawabanLainnya, $detailMeta, &$namaResponden, &$unitKerja, $pegawaiId) {
DB::connection('dbLmsMutu')->transaction(function () use (&$jawabanBaru, $validated, $jawabanUtama, $jawabanLainnya, $detailMeta, &$namaResponden, &$unitKerja, $pegawaiId) {
$jawabanBaru = Jawaban::create([
'lms_mutu_soal_id' => $validated['lms_mutu_soal_id'],
'pegawai_id' => $pegawaiId,
'tanggal_isi' => Carbon::now()->addHour(7),
]);
foreach ($validated['jawaban'] as $detailId => $answer) {
foreach ($jawabanUtama as $detailId => $answer) {
if (is_array($answer)) {
$answer = json_encode($answer);
}
@ -205,9 +206,15 @@ class SoalController extends Controller
}
});
return redirect('/')
return redirect()->route('soal.thankyou')
->with('success', 'Jawaban berhasil disimpan.');
}
public function thankYou()
{
return view('soal.thank-you');
}
public function redirectSmart()
{
$data = request()->input('data');

View File

@ -3,88 +3,165 @@
@section('title', 'Kuesioner Soal')
@section('custom_css')
<link rel="stylesheet" href="{{ asset('vuexy/assets/vendor/libs/select2/select2.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);
}
.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);
}
.question-layout .question-text-col {
border-right: 1px solid #f1f3f5;
}
.question-layout .answer-section {
padding-left: 1.5rem;
}
@media (max-width: 767.98px) {
.question-layout .question-text-col {
border-right: 0;
border-bottom: 1px dashed #e9ecef;
padding-bottom: 1rem;
margin-bottom: 1rem;
border-right: 1px solid #f1f3f5;
}
.question-layout .answer-section {
padding-left: 0;
padding-left: 1.5rem;
}
}
.option-scroll {
max-height: 220px;
overflow-y: auto;
border: 1px dashed #ced4da;
border-radius: 0.65rem;
padding: 0.75rem 1rem;
background-color: #f8f9fa;
}
@media (max-width: 767.98px) {
.question-layout .question-text-col {
border-right: 0;
border-bottom: 1px dashed #e9ecef;
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.select2-container {
width: 100% !important;
}
.question-layout .answer-section {
padding-left: 0;
}
}
.select2-container .select2-selection--single {
min-height: calc(2.625rem + 2px);
padding: 0.5rem 1rem;
border: 1px solid #ced4da;
border-radius: 0.5rem;
}
.option-scroll {
max-height: 220px;
overflow-y: auto;
border: 1px dashed #ced4da;
border-radius: 0.65rem;
padding: 0.75rem 1rem;
background-color: #f8f9fa;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 1.6;
color: #4f4f4f;
}
.option-scroll::-webkit-scrollbar {
width: 6px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
top: 0.6rem;
right: 1rem;
}
.option-scroll::-webkit-scrollbar-thumb {
background-color: rgba(13, 110, 253, 0.4);
border-radius: 3px;
}
.option-scroll::-webkit-scrollbar {
width: 6px;
}
.option-search {
position: relative;
}
.option-scroll::-webkit-scrollbar-thumb {
background-color: rgba(13, 110, 253, 0.4);
border-radius: 3px;
}
.option-search .search-icon {
position: absolute;
top: 50%;
left: 0.85rem;
transform: translateY(-50%);
color: #adb5bd;
pointer-events: none;
}
.lainnya-input {
display: none;
}
.option-search .search-icon svg {
width: 14px;
height: 14px;
display: block;
}
.lainnya-input.show {
display: block;
margin-top: 0.85rem;
}
.option-search input {
padding-left: 2.4rem;
}
.lainnya-input {
display: none;
}
.lainnya-input.show {
display: block;
margin-top: 0.85rem;
}
.dual-form-wrapper .form-label {
font-weight: 600;
}
/* Skala di atas slider */
.range-scale {
position: relative;
height: 40px;
margin-bottom: 8px;
}
.range-track-colored .range-colored {
width: 100%;
-webkit-appearance: none;
background: #ffffff; /* default putih, nanti diwarnai JS */
height: 8px;
border-radius: 4px;
}
/* hilangkan track default browser */
.range-colored::-webkit-slider-runnable-track {
background: transparent;
height: 8px;
}
.range-colored::-moz-range-track {
background: transparent;
height: 8px;
}
/* thumb */
.range-colored::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #000;
cursor: pointer;
position: relative;
z-index: 2;
}
.range-colored::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #000;
cursor: pointer;
position: relative;
z-index: 2;
}
.range-scale-item {
position: absolute;
bottom: 0;
transform: translateX(-50%);
text-align: center;
font-size: 10px;
color: #555;
}
.range-scale-mark {
display: block;
width: 1px;
height: 8px;
margin-top:-10px;
background-color: #555;
}
/* Tick besar tiap 10 */
.range-scale-item-major .range-scale-mark {
height: 12px;
background-color: #000;
font-weight: 600;
}
/* Tick khusus angka 1 biar lebih pendek (opsional) */
.range-scale-item-one .range-scale-mark {
height: 10px;
}
.dual-form-wrapper .form-label {
font-weight: 600;
}
</style>
@endsection
@ -170,12 +247,23 @@
<input type="hidden" name="lms_mutu_soal_id" value="{{ $soal->id }}">
<input type="hidden" name="hal" id="input-hal" value="{{ $hal }}">
@php
$questionCounter = 0;
@endphp
@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'] ?? [];
$optionsRaw = $detailConfig['options'] ?? [];
if (is_array($optionsRaw)) {
$options = $optionsRaw;
} elseif ($type === 'option_with_range' && is_string($optionsRaw)) {
$options = [$optionsRaw];
} else {
$options = [];
}
$autoSelect = ($type === 'option' && count($options) == 1) ? true : false;
if (!is_array($options)) {
$options = [];
}
@ -184,7 +272,12 @@
})->all();
$existingAnswer = $existingJawaban[$detail->id] ?? null;
$prefillAnswer = $prefillJawaban[$detail->id] ?? null;
$hasDefaultAnswer = array_key_exists('default', $detailConfig);
$defaultAnswer = $hasDefaultAnswer ? $detailConfig['default'] : null;
$currentAnswer = old('jawaban.' . $detail->id, $existingAnswer ?? $prefillAnswer);
if (($currentAnswer === null || $currentAnswer === '') && $hasDefaultAnswer) {
$currentAnswer = $defaultAnswer;
}
$existingOtherAnswer = $existingAnswer && !in_array($existingAnswer, $optionValues, true) ? $existingAnswer : null;
$prefillOtherAnswer = $prefillAnswer && !in_array($prefillAnswer, $optionValues, true) ? $prefillAnswer : null;
$oldOtherAnswer = old('jawaban_lainnya.' . $detail->id, $existingOtherAnswer ?? $prefillOtherAnswer);
@ -199,11 +292,12 @@
$detailHal = $detail->hal ?? $listHal->first();
$isVisible = $detailHal == $hal;
$optionsCount = is_array($options) ? count($options) : 0;
$useSelectSearch = $optionsCount > 4;
$hasLainnyaOption = collect($options)->contains(function ($optionItem) {
return is_string($optionItem) && stripos($optionItem, 'lainnya') !== false;
});
if (!$hasLainnyaOption && $type === 'option_with_other') {
$options[] = 'Lainnya';
$optionsCount = count($options);
$hasLainnyaOption = true;
}
$isConsentQuestion = !empty($detailConfig['persetujuan_form']);
@ -212,6 +306,12 @@
$useDualForm = $type === 'dual_form' || (!empty($dualFormConfig) && $dualFormConfig !== false);
$dualYearOld = null;
$dualMonthOld = null;
$questionNumber = $detailConfig['no'] ?? '';
$rangeMin = null;
$rangeMax = null;
$rangeStep = 1;
$rangeDefault = null;
$rangeTicks = [];
if ($useDualForm && $currentAnswer) {
if (preg_match('/([0-9]+)\\s*\\(Tahun\\)/i', $currentAnswer, $matchYear)) {
$dualYearOld = $matchYear[1];
@ -220,8 +320,48 @@
$dualMonthOld = $matchMonth[1];
}
}
@endphp
if ($type === 'option_with_range' && !empty($options)) {
$rangeSource = $options[0];
if (is_array($rangeSource)) {
$rangeSource = $rangeSource['range'] ?? (isset($rangeSource['min'], $rangeSource['max']) ? $rangeSource['min'] . '-' . $rangeSource['max'] : null);
}
if (is_string($rangeSource) && preg_match('/(-?[0-9]+(?:[\\.,][0-9]+)?)\\s*-\\s*(-?[0-9]+(?:[\\.,][0-9]+)?)/', $rangeSource, $rangeMatch)) {
$rangeMin = (float) str_replace(',', '.', $rangeMatch[1]);
$rangeMax = (float) str_replace(',', '.', $rangeMatch[2]);
if ($rangeMin > $rangeMax) {
[$rangeMin, $rangeMax] = [$rangeMax, $rangeMin];
}
$rangeUsesDecimal = (fmod($rangeMin, 1) !== 0.0) || (fmod($rangeMax, 1) !== 0.0);
$rangeStep = $rangeUsesDecimal ? 0.1 : 1;
if (is_numeric($currentAnswer)) {
$numericAnswer = (float) $currentAnswer;
$rangeDefault = min(max($numericAnswer, $rangeMin), $rangeMax);
} else {
$rangeDefault = $rangeMin;
}
if ($rangeDefault === null) {
$rangeDefault = $rangeMin;
}
$rangeSpan = max($rangeMax - $rangeMin, 1);
$startTick = (int) ceil($rangeMin);
$endTick = (int) floor($rangeMax);
for ($v = $startTick; $v <= $endTick; $v++) {
$position = (($v - $rangeMin) / $rangeSpan) * 100;
$rangeTicks[] = [
'value' => $v, // nilai asli
'label' => ($v === 1 || $v % 10 === 0) ? $v : null, // label hanya 1 & kelipatan 10
'position' => max(0, min(100, $position)),
];
}
}
}
$rangeMinDisplay = ($rangeMin !== null && floor($rangeMin) == $rangeMin) ? (int) $rangeMin : $rangeMin;
$rangeMaxDisplay = ($rangeMax !== null && floor($rangeMax) == $rangeMax) ? (int) $rangeMax : $rangeMax;
$rangeDefaultDisplay = ($rangeDefault !== null && floor($rangeDefault) == $rangeDefault) ? (int) $rangeDefault : $rangeDefault;
@endphp
<div class="question-card mb-4"
data-hal-card="{{ $detailHal }}"
data-detail-id="{{ $detail->id }}"
@ -231,7 +371,9 @@
<div class="row g-3 align-items-start question-layout">
<div class="col-md-6 question-text-col">
<div class="d-flex align-items-center gap-3 mb-1">
<span class="badge rounded-pill bg-label-primary fs-6">{{ $loop->iteration }}</span>
@if ($questionNumber !== null)
<span class="badge rounded-pill bg-label-primary fs-6">{{ $questionNumber }}</span>
@endif
<h5 class="fw-semibold mb-0">{{ $pertanyaan }}</h5>
</div>
@if ($isConsentQuestion)
@ -269,134 +411,169 @@
data-dual-hidden="{{ $detail->id }}"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
required>
@if($formLocked) disabled @endif>
</div>
@elseif ($type === 'textarea')
<textarea class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" rows="4" required
name="jawaban[{{ $detail->id }}]" rows="4"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Tulis jawaban Anda di sini">{{ $currentAnswer }}</textarea>
@elseif ($type === 'text')
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ $currentAnswer }}" required
name="jawaban[{{ $detail->id }}]" value="{{ $currentAnswer }}"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Masukkan jawaban Anda">
@elseif ($type === 'option_with_range')
@if ($rangeMin !== null && $rangeMax !== null)
<div class="range-slider-wrapper" data-range-wrapper="{{ $detail->id }}">
<div class="range-scale" aria-hidden="true">
@foreach ($rangeTicks as $tick)
@php
$value = (int) $tick['value'];
$label = $tick['label']; // bisa null
$isMajor = ($value === 1) || ($value % 10 === 0);
$isOneTick = ($value === 1);
@endphp
<div class="range-scale-item
{{ $isMajor ? 'range-scale-item-major' : '' }}
{{ $isOneTick ? 'range-scale-item-one' : '' }}"
style="left: {{ $tick['position'] }}%;">
<span class="range-scale-mark"></span>
@if(!is_null($label))
<span class="range-scale-label">{{ $label }}</span>
@endif
</div>
@endforeach
</div>
{{-- Track warna + slider --}}
<div class="range-track-colored">
<input type="range"
class="form-range range-colored border"
name="jawaban[{{ $detail->id }}]"
min="{{ $rangeMin }}"
max="{{ $rangeMax }}"
step="{{ $rangeStep }}"
value="{{ $rangeDefault ?? $rangeMin }}"
data-range-input="{{ $detail->id }}"
data-range-min-value="{{ $rangeMin }}"
data-range-max-value="{{ $rangeMax }}"
data-field-hal="{{ $detailHal }}"
@if($formLocked) disabled @endif>
</div>
<div class="text-center mt-2 fw-semibold">
Nilai saat ini:
<span data-range-output="{{ $detail->id }}">{{ $rangeDefaultDisplay ?? $rangeMinDisplay }}</span>
</div>
<div class="input-group input-group-sm mt-3 range-manual-input">
<span class="input-group-text">Atau isi manual</span>
<input type="number"
class="form-control"
min="{{ $rangeMin }}"
max="{{ $rangeMax }}"
step="{{ $rangeStep }}"
value="{{ $rangeDefault ?? $rangeMin }}"
data-range-manual="{{ $detail->id }}"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif>
</div>
</div>
@else
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ $currentAnswer }}"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Masukkan jawaban Anda">
@endif
@else
@if (!empty($options))
@if ($useSelectSearch)
<select class="form-select select2 @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if ($hasLainnyaOption || $type === 'option_with_other') data-lainnya-select="{{ $detail->id }}" @endif
data-select-search="true"
@if($formLocked) disabled @endif
required>
<option value="" disabled {{ $currentAnswer ? '' : 'selected' }}>Pilih jawaban</option>
@foreach ($options as $option)
@php
$optionLabel = is_scalar($option) ? (string) $option : '';
$isLainnya = stripos($optionLabel, 'lainnya') !== false;
$optionValue = $isLainnya && $oldOtherAnswer ? $oldOtherAnswer : $optionLabel;
$shouldSelect = $currentAnswer === $optionValue || ($isLainnya && $shouldForceLainnyaSelection);
@endphp
<option value="{{ $optionValue }}"
@php
$lainnyaWrapperRendered = false;
@endphp
<div class="option-search mb-3">
@if(count($options) > 6)
<input type="text"
class="form-control form-control-sm option-search-input"
placeholder="Cari jawaban"
data-option-search="{{ $detail->id }}"
autocomplete="off">
@endif
</div>
<div class="option-scroll" data-option-list="{{ $detail->id }}">
@foreach ($options as $optionIndex => $option)
@php
$optionId = 'jawaban-' . $detail->id . '-' . $optionIndex;
$optionLabel = is_scalar($option) ? (string) $option : '';
$isLainnya = stripos($optionLabel, 'lainnya') !== false;
$optionValue = $isLainnya && $oldOtherAnswer ? $oldOtherAnswer : $optionLabel;
$shouldCheck = $currentAnswer === $optionValue || ($isLainnya && $shouldForceLainnyaSelection);
@endphp
<div class="form-check mb-2" data-option-item>
<input class="form-check-input @error('jawaban.' . $detail->id) is-invalid @enderror"
type="radio"
name="jawaban[{{ $detail->id }}]"
id="{{ $optionId }}"
value="{{ $optionValue }}"
data-original-value="{{ $optionLabel }}"
@if ($isLainnya) data-lainnya-option="{{ $detail->id }}" @endif
{{ $shouldSelect ? 'selected' : '' }}>
{{ $optionLabel }}
</option>
@endforeach
@if ($type === 'option_with_other' && !$hasLainnyaOption)
<option value="Lainnya"
data-original-value="Lainnya"
data-lainnya-option="{{ $detail->id }}">
Lainnya
</option>
@endif
</select>
@if ($hasLainnyaOption || $type === 'option_with_other')
<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-radio="{{ $isLainnya ? $detail->id : '' }}"
data-field-hal="{{ $detailHal }}"
data-lainnya-input="{{ $detail->id }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Tuliskan jawaban lainnya"
{{ $showLainnya ? 'required' : '' }}>
{{ $shouldCheck || $autoSelect ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $optionId }}">
{{ $optionLabel }}
</label>
</div>
@endif
@else
@php
$lainnyaWrapperRendered = false;
@endphp
<div class="option-scroll">
@foreach ($options as $optionIndex => $option)
@if ($isLainnya)
@php
$optionId = 'jawaban-' . $detail->id . '-' . $optionIndex;
$optionLabel = is_scalar($option) ? (string) $option : '';
$isLainnya = stripos($optionLabel, 'lainnya') !== false;
$optionValue = $isLainnya && $oldOtherAnswer ? $oldOtherAnswer : $optionLabel;
$shouldCheck = $currentAnswer === $optionValue || ($isLainnya && $shouldForceLainnyaSelection);
$lainnyaWrapperRendered = true;
@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="{{ $optionValue }}"
data-original-value="{{ $optionLabel }}"
data-lainnya-radio="{{ $isLainnya ? $detail->id : '' }}" required
<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 }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
{{ $shouldCheck ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $optionId }}">
{{ $optionLabel }}
</label>
placeholder="Tuliskan jawaban lainnya">
</div>
@if ($isLainnya)
@php
$lainnyaWrapperRendered = true;
@endphp
<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 }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Tuliskan jawaban lainnya"
{{ $showLainnya ? 'required' : '' }}>
</div>
@endif
@endforeach
@endif
@endforeach
</div>
@if ($hasLainnyaOption || $type === 'option_with_other')
<small class="form-text text-muted">
Jika jawaban yang Anda cari tidak ada di daftar, pilih opsi <strong>"Lainnya"</strong> lalu isi sesuai kebutuhan.
</small>
@endif
<div class="text-muted small mt-2" data-option-empty="{{ $detail->id }}" style="display: none;">
Tidak ada jawaban yang cocok.
</div>
@if (!$lainnyaWrapperRendered && $type === 'option_with_other')
<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 }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Tuliskan jawaban lainnya">
</div>
@if (!$lainnyaWrapperRendered && $type === 'option_with_other')
<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 }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Tuliskan jawaban lainnya"
{{ $showLainnya ? 'required' : '' }}>
</div>
@endif
@endif
@else
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ $currentAnswer }}" required
name="jawaban[{{ $detail->id }}]" value="{{ $currentAnswer }}"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
@ -455,7 +632,6 @@
@endsection
@section('custom_js')
<script src="{{ asset('vuexy/assets/vendor/libs/select2/select2.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('form-soal');
@ -533,8 +709,9 @@
}
});
initSelectSearch();
setupLainnyaInputs();
setupOptionSearch();
setupRangeInputs();
setupDualFormInputs();
setupConsentWatcher();
updateQuestionVisibility();
@ -693,11 +870,12 @@
}
function setupLainnyaInputs() {
document.querySelectorAll('input[type="radio"]').forEach(function (radio) {
const radios = form.querySelectorAll('input[type="radio"][name^="jawaban["]');
radios.forEach(function (radio) {
radio.addEventListener('change', function (event) {
const customDetailId = event.target.getAttribute('data-lainnya-radio');
const detailId = customDetailId || event.target.name.replace('jawaban[', '').replace(']', '');
const targetWrapper = detailId ? document.querySelector('[data-lainnya-wrapper="' + detailId + '"]') : null;
const targetWrapper = detailId ? form.querySelector('[data-lainnya-wrapper="' + detailId + '"]') : null;
const labelText = getLabelTextByInput(event.target);
const shouldShow = !!customDetailId
|| isOtherChoice(event.target.value)
@ -714,19 +892,15 @@
});
});
document.querySelectorAll('[data-lainnya-input]').forEach(function (input) {
form.querySelectorAll('[data-lainnya-input]').forEach(function (input) {
input.addEventListener('input', function (event) {
const detailId = input.getAttribute('data-lainnya-input');
syncCustomOptionValue(detailId, event.target.value);
});
});
document.querySelectorAll('[data-lainnya-select]').forEach(function (select) {
const detailId = select.name.replace('jawaban[', '').replace(']', '');
select.addEventListener('change', function (event) {
handleLainnyaSelect(event.target, detailId, true);
});
handleLainnyaSelect(select, detailId, false);
form.querySelectorAll('[data-lainnya-wrapper].show').forEach(function (wrapper) {
handleLainnyaInput(wrapper, true);
});
}
@ -754,67 +928,160 @@
}
}
function handleLainnyaSelect(selectElement, detailId, canClear) {
if (!selectElement || !detailId) {
return;
}
const targetWrapper = document.querySelector('[data-lainnya-wrapper="' + detailId + '"]');
if (!targetWrapper) {
return;
}
const selectedOption = selectElement.options[selectElement.selectedIndex];
const optionText = selectedOption ? (selectedOption.textContent || selectedOption.innerText || '') : '';
const originalValue = selectedOption ? (selectedOption.dataset.originalValue || '') : '';
const isLainnyaSelected = selectedOption && (
selectedOption.dataset.lainnyaOption
|| isOtherChoice(selectElement.value)
|| isOtherChoice(optionText)
|| isOtherChoice(originalValue)
);
if (isLainnyaSelected) {
handleLainnyaInput(targetWrapper, true);
} else {
handleLainnyaInput(targetWrapper, false, canClear);
}
}
function syncCustomOptionValue(detailId, customValue) {
if (!detailId) {
return;
}
const trimmedValue = (customValue || '').toString().trim();
const radio = document.querySelector('input[data-lainnya-radio="' + detailId + '"]');
const radio = form.querySelector('input[data-lainnya-radio="' + detailId + '"]');
if (radio) {
const originalValue = radio.dataset.originalValue || 'lainnya';
radio.value = trimmedValue || originalValue;
}
const select = document.querySelector('[data-lainnya-select="' + detailId + '"]');
if (select) {
const option = select.querySelector('[data-lainnya-option="' + detailId + '"]');
if (option) {
const originalOptionValue = option.dataset.originalValue || 'lainnya';
option.value = trimmedValue || originalOptionValue;
}
}
}
function resetCustomOptionValue(detailId) {
if (!detailId) {
return;
}
const radio = document.querySelector('input[data-lainnya-radio="' + detailId + '"]');
const radio = form.querySelector('input[data-lainnya-radio="' + detailId + '"]');
if (radio && radio.dataset.originalValue) {
radio.value = radio.dataset.originalValue;
}
const select = document.querySelector('[data-lainnya-select="' + detailId + '"]');
if (select) {
const option = select.querySelector('[data-lainnya-option="' + detailId + '"]');
if (option && option.dataset.originalValue) {
option.value = option.dataset.originalValue;
}
function setupOptionSearch() {
const searchInputs = form.querySelectorAll('[data-option-search]');
searchInputs.forEach(function (input) {
const detailId = input.getAttribute('data-option-search');
const listContainer = detailId ? form.querySelector('[data-option-list="' + detailId + '"]') : null;
if (!listContainer) {
return;
}
const emptyState = detailId ? form.querySelector('[data-option-empty="' + detailId + '"]') : null;
const handler = function () {
filterOptionList(listContainer, input.value, emptyState);
};
input.addEventListener('input', handler);
handler();
});
}
function filterOptionList(container, query, emptyState) {
const normalized = (query || '').toString().trim().toLowerCase();
let visibleCount = 0;
container.querySelectorAll('[data-option-item]').forEach(function (item) {
const label = item.querySelector('.form-check-label');
const text = (label ? label.textContent : item.textContent || '').toString().trim().toLowerCase();
const shouldShow = !normalized || text.indexOf(normalized) !== -1;
item.style.display = shouldShow ? '' : 'none';
if (shouldShow) {
visibleCount++;
}
});
if (emptyState) {
emptyState.style.display = visibleCount ? 'none' : '';
}
}
function setupRangeInputs() {
const rangeInputs = form.querySelectorAll('[data-range-input]');
rangeInputs.forEach(function (input) {
const detailId = input.getAttribute('data-range-input');
const output = detailId ? form.querySelector('[data-range-output="' + detailId + '"]') : null;
const manualInput = detailId ? form.querySelector('[data-range-manual="' + detailId + '"]') : null;
const minValue = parseFloat(input.dataset.rangeMinValue || input.min || '0');
const maxValue = parseFloat(input.dataset.rangeMaxValue || input.max || '100');
const stepValue = parseFloat(input.step || '1') || 1;
function clampAndSnap(rawVal) {
let v = parseFloat(rawVal);
if (isNaN(v)) v = minValue;
if (v < minValue) v = minValue;
if (v > maxValue) v = maxValue;
return snapToStep(v, minValue, stepValue);
}
function updateBoth(fromManual) {
const current = fromManual && manualInput ? manualInput.value : input.value;
const value = clampAndSnap(current);
input.value = value;
if (output) {
output.textContent = formatRangeDisplay(value);
}
if (manualInput && !fromManual) {
manualInput.value = value;
}
updateRangeFill(input, minValue, maxValue);
}
input.addEventListener('input', function () {
updateBoth(false);
});
if (manualInput) {
manualInput.addEventListener('input', function () {
updateBoth(true);
});
}
// initial
updateBoth(false);
});
}
function formatRangeDisplay(value) {
if (value === null || value === undefined) {
return '';
}
const numberValue = Number(value);
if (!isNaN(numberValue) && Number.isFinite(numberValue)) {
return Number.isInteger(numberValue) ? String(numberValue) : numberValue.toFixed(1);
}
return value;
}
function updateRangeFill(input, min, max) {
if (!input || !isFinite(min) || !isFinite(max) || max === min) {
return;
}
const value = parseFloat(input.value || min);
const ratio = Math.max(0, Math.min(1, (value - min) / (max - min))); // 01
const percent = ratio * 100;
// hijau dari 0 sampai nilai sekarang, sisanya putih
input.style.background =
'linear-gradient(90deg, #00c070 0% ' + percent + '%, #ffffff ' + percent + '% 100%)';
}
function snapToStep(value, min, step) {
if (!isFinite(step) || step <= 0) {
return value;
}
const delta = value - min;
const steps = Math.round(delta / step);
const snapped = min + steps * step;
const decimals = getDecimalPlaces(step);
return parseFloat(snapped.toFixed(decimals));
}
function getDecimalPlaces(number) {
const value = number.toString();
if (value.indexOf('.') === -1) {
return 0;
}
return value.length - value.indexOf('.') - 1;
}
function shouldDisableFieldForImmediate(field) {
if (!field || field.dataset.consentInput === '1') {
return false;
@ -832,32 +1099,6 @@
return value === '';
}
function initSelectSearch() {
if (!window.jQuery || !window.jQuery.fn || typeof window.jQuery.fn.select2 === 'undefined') {
return;
}
const $ = window.jQuery;
$('[data-select-search="true"]').each(function () {
const $select = $(this);
if ($select.data('select2')) {
return;
}
$select.wrap('<div class="position-relative w-100"></div>');
$select.select2({
dropdownParent: $select.parent(),
width: '100%',
placeholder: $select.find('option[disabled]').first().text() || 'Pilih jawaban',
minimumResultsForSearch: 0
});
if ($select.is('[data-lainnya-select]')) {
const detailId = ($select.attr('name') || '').replace('jawaban[', '').replace(']', '');
$select.on('select2:select', function () {
handleLainnyaSelect(this, detailId, true);
});
}
});
}
function setupDualFormInputs() {
document.querySelectorAll('[data-dual-wrapper]').forEach(function (wrapper) {
const detailId = wrapper.getAttribute('data-dual-wrapper');
@ -952,4 +1193,5 @@
}
});
</script>
@endsection

View File

@ -0,0 +1,32 @@
@extends('layouts.template')
@section('title', 'Terima Kasih')
@section('content')
<div class="py-5">
<div class="row justify-content-center">
<div class="col-lg-6 col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-body text-center p-5">
<div class="rounded-circle bg-label-success d-inline-flex align-items-center justify-content-center mb-3" style="width: 72px; height: 72px;">
<svg viewBox="0 0 24 24" width="32" height="32" role="img" aria-hidden="true">
<path fill="currentColor" d="M12 2a10 10 0 1010 10A10.011 10.011 0 0012 2zm4.3 8.38l-5.07 5.08a1 1 0 01-1.42 0l-2.09-2.09a1 1 0 111.42-1.41l1.38 1.38 4.36-4.37a1 1 0 111.42 1.41z"/>
</svg>
</div>
<h3 class="fw-semibold mb-2">Terima kasih sudah mengisi halaman ini</h3>
<p class="text-muted mb-4">
@if (session('success'))
{{ session('success') }}
@else
Jawaban Anda sudah kami terima dan akan dipergunakan untuk pengembangan layanan terbaik.
@endif
</p>
<a href="{{ route('soal.index') }}" class="btn btn-primary">
Kembali ke daftar kuesioner
</a>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -8,8 +8,9 @@ use Illuminate\Support\Facades\Route;
Route::get('/login', [AuthController::class, 'login']);
Route::get('/', [SoalController::class, 'index'])->name('soal.index');
Route::post('/jawaban', [SoalController::class, 'store'])->name('soal.store');
Route::get('/jawaban/terima-kasih', [SoalController::class, 'thankYou'])->name('soal.thankyou');
Route::get('/admin', [AdminController::class, 'index']);
Route::post('/admin/get_data_pegawai_sudah_survey', [AdminController::class, 'get_data_pegawai_sudah_survey']);
Route::get('/admin/dashboard_jawaban', [AdminController::class, 'dashboard_analisis']);
Route::post('/admin/get_data_dashboard_jawaban', [AdminController::class, 'get_data_dashboard_analisis']);
Route::get('/redirect-smart', [SoalController::class, 'redirectSmart']);
Route::get('/redirect-smart', [SoalController::class, 'redirectSmart']);