1406 lines
54 KiB
PHP
1406 lines
54 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);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.question-layout .answer-section {
|
||
padding-left: 0;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.option-search {
|
||
position: relative;
|
||
}
|
||
|
||
.option-search .search-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0.85rem;
|
||
transform: translateY(-50%);
|
||
color: #adb5bd;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.option-search .search-icon svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
display: block;
|
||
}
|
||
|
||
.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: 45px;
|
||
margin-top: 0px;
|
||
border-top: 2px solid #495057;
|
||
padding-top: 2px;
|
||
}
|
||
|
||
.range-track-colored {
|
||
position: relative;
|
||
margin-top: 0px;
|
||
}
|
||
|
||
.range-track-colored .range-colored {
|
||
width: 100%;
|
||
-webkit-appearance: none;
|
||
background: #ffffff;
|
||
height: 10px;
|
||
border-radius: 5px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
/* hilangkan track default browser */
|
||
.range-colored::-webkit-slider-runnable-track {
|
||
background: transparent;
|
||
height: 10px;
|
||
}
|
||
.range-colored::-moz-range-track {
|
||
background: transparent;
|
||
height: 10px;
|
||
}
|
||
|
||
|
||
|
||
.range-scale-item {
|
||
position: absolute;
|
||
top: 0;
|
||
transform: translateX(-50%);
|
||
text-align: center;
|
||
font-size: 11px;
|
||
color: #495057;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.range-scale-label {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.range-scale-mark {
|
||
display: block;
|
||
width: 1px;
|
||
height: 8px;
|
||
background-color: #495057;
|
||
}
|
||
|
||
/* Tick besar untuk angka 1 dan kelipatan 10 */
|
||
.range-scale-item-major .range-scale-mark {
|
||
height: 12px;
|
||
width: 2px;
|
||
background-color: #212529;
|
||
}
|
||
|
||
.range-scale-item-major .range-scale-label {
|
||
font-weight: 600;
|
||
color: #212529;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Tick khusus angka 1 */
|
||
.range-scale-item-one .range-scale-mark {
|
||
height: 12px;
|
||
background-color: #212529;
|
||
width: 2px;
|
||
}
|
||
|
||
.range-scale-item-one .range-scale-label {
|
||
color: #212529;
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.badge-placeholder {
|
||
visibility: hidden;
|
||
}
|
||
</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;
|
||
$formLocked = $formLocked ?? false;
|
||
$prefillJawaban = $prefillJawaban ?? [];
|
||
$existingJawaban = $existingJawaban ?? [];
|
||
@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">
|
||
← 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">
|
||
<h5 class="mb-3 text-primary">{{ $soal->judul_soal ?? '-' }}</h5>
|
||
<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
|
||
@if ($formLocked)
|
||
<div class="alert alert-info">
|
||
Anda sudah mengisi kuesioner ini. Jawaban di bawah ditampilkan dalam mode baca.
|
||
</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())' data-form-locked="{{ $formLocked ? '1' : '0' }}">
|
||
@csrf
|
||
<input type="hidden" name="lms_mutu_soal_id" value="{{ $soal->id }}">
|
||
<input type="hidden" name="hal" id="input-hal" value="{{ $hal }}">
|
||
|
||
@php
|
||
$questionCounter = 0;
|
||
$groupedQuestionDetails = [];
|
||
$groupIndexMap = [];
|
||
foreach ($soal->soalDetail as $detailItem) {
|
||
$detailConfigItem = json_decode($detailItem->soal, true) ?? [];
|
||
$detailHalValue = $detailItem->hal ?? $listHal->first();
|
||
$questionNumberValue = $detailConfigItem['no'] ?? null;
|
||
$hasNumberValue = $questionNumberValue !== null && $questionNumberValue !== '';
|
||
$groupKey = $hasNumberValue
|
||
? $detailHalValue . '|' . $questionNumberValue
|
||
: 'detail_' . $detailItem->id;
|
||
|
||
if (!array_key_exists($groupKey, $groupIndexMap)) {
|
||
$groupIndexMap[$groupKey] = count($groupedQuestionDetails);
|
||
$groupedQuestionDetails[] = [
|
||
'hal' => $detailHalValue,
|
||
'number' => $hasNumberValue ? $questionNumberValue : null,
|
||
'items' => [],
|
||
];
|
||
}
|
||
|
||
$groupPosition = $groupIndexMap[$groupKey];
|
||
$groupedQuestionDetails[$groupPosition]['items'][] = [
|
||
'detail' => $detailItem,
|
||
'config' => $detailConfigItem,
|
||
];
|
||
}
|
||
@endphp
|
||
@forelse ($groupedQuestionDetails as $groupData)
|
||
@php
|
||
$groupHal = $groupData['hal'];
|
||
$groupNumber = $groupData['number'];
|
||
$groupItems = $groupData['items'];
|
||
$groupFirstDetail = $groupItems[0]['detail'] ?? null;
|
||
$groupFirstConfig = $groupItems[0]['config'] ?? [];
|
||
$groupType = $groupFirstConfig['type'] ?? 'option';
|
||
$groupHasConsent = false;
|
||
foreach ($groupItems as $groupItemCheck) {
|
||
if (!empty($groupItemCheck['config']['persetujuan_form'])) {
|
||
$groupHasConsent = true;
|
||
break;
|
||
}
|
||
}
|
||
@endphp
|
||
<div class="question-card mb-4"
|
||
data-hal-card="{{ $groupHal }}"
|
||
data-detail-id="{{ $groupFirstDetail ? $groupFirstDetail->id : '' }}"
|
||
data-consent-question="{{ $groupHasConsent ? '1' : '0' }}"
|
||
data-question-type="{{ $groupType }}"
|
||
style="{{ $groupHal == $hal ? '' : 'display: none;' }}">
|
||
@foreach ($groupItems as $groupItem)
|
||
@php
|
||
$detail = $groupItem['detail'];
|
||
$detailConfig = $groupItem['config'] ?? [];
|
||
$pertanyaan = $detailConfig['soal'] ?? 'Pertanyaan tidak tersedia';
|
||
$type = $detailConfig['type'] ?? 'option';
|
||
$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 = [];
|
||
}
|
||
$optionValues = collect($options)->map(function ($value) {
|
||
return is_scalar($value) ? (string) $value : '';
|
||
})->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);
|
||
$hasCurrentAnswer = $currentAnswer !== null && $currentAnswer !== '';
|
||
$isCustomCurrentAnswer = $hasCurrentAnswer && !in_array($currentAnswer, $optionValues, true);
|
||
if (!$oldOtherAnswer && $isCustomCurrentAnswer) {
|
||
$oldOtherAnswer = $currentAnswer;
|
||
}
|
||
$showLainnya = $isCustomCurrentAnswer
|
||
|| (is_string($currentAnswer) && stripos($currentAnswer, 'lainnya') !== false)
|
||
|| (!empty($oldOtherAnswer));
|
||
$detailHal = $detail->hal ?? $listHal->first();
|
||
$optionsCount = is_array($options) ? count($options) : 0;
|
||
$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']);
|
||
$shouldForceLainnyaSelection = $hasLainnyaOption && $showLainnya;
|
||
$dualFormConfig = $detailConfig['dual_form'] ?? null;
|
||
$useDualForm = $type === 'dual_form' || (!empty($dualFormConfig) && $dualFormConfig !== false);
|
||
$dualYearOld = null;
|
||
$dualMonthOld = null;
|
||
$questionNumber = $detailConfig['no'] ?? '';
|
||
$hasGroupNumber = $groupNumber !== null && $groupNumber !== '';
|
||
$badgeNumber = $hasGroupNumber ? $groupNumber : $questionNumber;
|
||
$shouldShowNumber = $badgeNumber !== null && $badgeNumber !== '' && (!$hasGroupNumber || $loop->first);
|
||
$rangeMin = null;
|
||
$rangeMax = null;
|
||
$rangeStep = 1;
|
||
$rangeDefault = null;
|
||
$rangeTicks = [];
|
||
$rangeSubTickCount = 4;
|
||
$rangeMajorStep = 10;
|
||
if ($useDualForm && $currentAnswer) {
|
||
if (preg_match('/([0-9]+)\\s*\\(Tahun\\)/i', $currentAnswer, $matchYear)) {
|
||
$dualYearOld = $matchYear[1];
|
||
}
|
||
if (preg_match('/([0-9]+)\\s*\\(Bulan\\)/i', $currentAnswer, $matchMonth)) {
|
||
$dualMonthOld = $matchMonth[1];
|
||
}
|
||
}
|
||
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);
|
||
|
||
$majorAnchors = [$rangeMin, $rangeMax];
|
||
if ($rangeMin <= 1 && $rangeMax >= 1) {
|
||
$majorAnchors[] = 1;
|
||
}
|
||
$firstMajor = max($rangeMin, ($rangeMin <= 1 ? 10 : ceil($rangeMin / $rangeMajorStep) * $rangeMajorStep));
|
||
for ($v = $firstMajor; $v <= $rangeMax; $v += $rangeMajorStep) {
|
||
$majorAnchors[] = $v;
|
||
}
|
||
|
||
sort($majorAnchors);
|
||
$uniqueAnchors = [];
|
||
foreach ($majorAnchors as $anchor) {
|
||
if (empty($uniqueAnchors) || abs(end($uniqueAnchors) - $anchor) > 0.0001) {
|
||
$uniqueAnchors[] = $anchor;
|
||
}
|
||
}
|
||
$majorAnchors = array_values(array_filter($uniqueAnchors, function ($value) use ($rangeMin, $rangeMax) {
|
||
return $value >= $rangeMin && $value <= $rangeMax;
|
||
}));
|
||
|
||
$ticks = [];
|
||
$addTick = function ($value, $isMajor = false) use (&$ticks, $rangeMin, $rangeSpan, $rangeMajorStep) {
|
||
$position = $rangeSpan > 0 ? (($value - $rangeMin) / $rangeSpan) * 100 : 0;
|
||
$isOne = abs($value - 1) < 0.0001;
|
||
$isMultiple = $rangeMajorStep > 0 ? abs(fmod($value, $rangeMajorStep)) < 0.0001 : false;
|
||
$ticks[] = [
|
||
'value' => $value,
|
||
'label' => ($isOne || $isMultiple) ? (int) round($value) : null,
|
||
'position' => max(0, min(100, $position)),
|
||
'is_major' => $isMajor,
|
||
'is_one' => $isOne,
|
||
];
|
||
};
|
||
|
||
$countAnchors = count($majorAnchors);
|
||
if ($countAnchors === 0) {
|
||
$addTick($rangeMin, true);
|
||
} else {
|
||
for ($i = 0; $i < $countAnchors; $i++) {
|
||
$currentAnchor = $majorAnchors[$i];
|
||
$addTick($currentAnchor, true);
|
||
|
||
if ($i === $countAnchors - 1) {
|
||
continue;
|
||
}
|
||
|
||
$nextAnchor = $majorAnchors[$i + 1];
|
||
$segmentLength = $nextAnchor - $currentAnchor;
|
||
if ($segmentLength <= 0 || $rangeSubTickCount <= 0) {
|
||
continue;
|
||
}
|
||
|
||
for ($sub = 1; $sub <= $rangeSubTickCount; $sub++) {
|
||
$fraction = $sub / ($rangeSubTickCount + 1);
|
||
$value = $currentAnchor + ($segmentLength * $fraction);
|
||
$addTick($value, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
usort($ticks, function ($a, $b) {
|
||
if ($a['value'] == $b['value']) {
|
||
return 0;
|
||
}
|
||
return ($a['value'] < $b['value']) ? -1 : 1;
|
||
});
|
||
|
||
$rangeTicks = $ticks;
|
||
}
|
||
}
|
||
$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
|
||
@if (!$loop->first)
|
||
<hr class="my-4">
|
||
@endif
|
||
<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">
|
||
@if ($shouldShowNumber)
|
||
<span class="badge rounded-pill bg-label-primary fs-6">{{ $badgeNumber }}</span>
|
||
@elseif($hasGroupNumber)
|
||
<span class="badge rounded-pill bg-label-primary fs-6 badge-placeholder" aria-hidden="true">{{ $badgeNumber }}</span>
|
||
@endif
|
||
<h5 class="fw-semibold mb-0">{{ $pertanyaan }}</h5>
|
||
</div>
|
||
@if ($isConsentQuestion)
|
||
<p class="text-muted small mb-0">Pertanyaan persetujuan</p>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="col-md-6 answer-section">
|
||
@if ($useDualForm)
|
||
<div class="dual-form-wrapper" data-dual-wrapper="{{ $detail->id }}">
|
||
<div class="row g-3">
|
||
<div class="col-sm-6">
|
||
<label class="form-label small text-muted mb-1">Tahun</label>
|
||
<input type="number" min="0" class="form-control"
|
||
data-dual-input="tahun"
|
||
value="{{ $dualYearOld }}"
|
||
data-field-hal="{{ $detailHal }}"
|
||
@if($formLocked) disabled @endif
|
||
placeholder="Masukkan tahun">
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<label class="form-label small text-muted mb-1">Bulan</label>
|
||
<input type="number" min="0" class="form-control"
|
||
data-dual-input="bulan"
|
||
value="{{ $dualMonthOld }}"
|
||
data-field-hal="{{ $detailHal }}"
|
||
@if($formLocked) disabled @endif
|
||
placeholder="Masukkan bulan">
|
||
</div>
|
||
</div>
|
||
<input type="hidden"
|
||
class="@error('jawaban.' . $detail->id) is-invalid @enderror"
|
||
name="jawaban[{{ $detail->id }}]"
|
||
value="{{ $currentAnswer }}"
|
||
data-dual-hidden="{{ $detail->id }}"
|
||
data-field-hal="{{ $detailHal }}"
|
||
@if ($isConsentQuestion) data-consent-input="1" required @endif
|
||
@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"
|
||
data-field-hal="{{ $detailHal }}"
|
||
@if ($isConsentQuestion) data-consent-input="1" required @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 }}"
|
||
data-field-hal="{{ $detailHal }}"
|
||
@if ($isConsentQuestion) data-consent-input="1" required @endif
|
||
@if($formLocked) disabled @endif
|
||
placeholder="Masukkan jawaban Anda">
|
||
@elseif ($type === 'number')
|
||
<input type="number" 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" required @endif
|
||
@if($formLocked) disabled @endif>
|
||
@elseif ($type === 'option_with_range')
|
||
@if ($rangeMin !== null && $rangeMax !== null)
|
||
<div class="range-slider-wrapper" data-range-wrapper="{{ $detail->id }}">
|
||
{{-- 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 ($isConsentQuestion) data-consent-input="1" required @endif
|
||
@if($formLocked) disabled @endif>
|
||
</div>
|
||
<div class="range-scale" aria-hidden="true">
|
||
@foreach ($rangeTicks as $tick)
|
||
@php
|
||
$label = $tick['label'] ?? null;
|
||
$isMajor = !empty($tick['is_major']);
|
||
$isOneTick = !empty($tick['is_one']);
|
||
@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>
|
||
<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" required @endif
|
||
@if($formLocked) disabled @endif
|
||
placeholder="Masukkan jawaban Anda">
|
||
@endif
|
||
@else
|
||
@if (!empty($options))
|
||
@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 }}"
|
||
data-lainnya-radio="{{ $isLainnya ? $detail->id : '' }}"
|
||
data-field-hal="{{ $detailHal }}"
|
||
@if ($isConsentQuestion) data-consent-input="1" required @endif
|
||
@if($formLocked) disabled @endif
|
||
{{ $shouldCheck || $autoSelect ? 'checked' : '' }}>
|
||
<label class="form-check-label" for="{{ $optionId }}">
|
||
{{ $optionLabel }}
|
||
</label>
|
||
</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">
|
||
</div>
|
||
@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>
|
||
@endif
|
||
@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
|
||
@endif
|
||
|
||
@error('jawaban.' . $detail->id)
|
||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||
@enderror
|
||
@error('jawaban_lainnya.' . $detail->id)
|
||
<div class="text-danger small mt-1">{{ $message }}</div>
|
||
@enderror
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</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' }}>
|
||
← Halaman Sebelumnya
|
||
</button>
|
||
<button type="button"
|
||
class="btn btn-outline-secondary btn-next-hal"
|
||
data-nav-control="next"
|
||
{{ $halBerikut ? '' : 'disabled' }}>
|
||
Halaman Berikutnya →
|
||
</button>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary ms-md-auto"
|
||
data-final-submit="true"
|
||
style="{{ (!$isHalTerakhir || $formLocked) ? 'display: none;' : '' }}"
|
||
{{ $soal->soalDetail->isEmpty() || $formLocked ? '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 formLocked = form.dataset.formLocked === '1';
|
||
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;
|
||
const nonConsentFields = document.querySelectorAll('[data-field-hal]:not([data-consent-input="1"])');
|
||
const consentFields = document.querySelectorAll('[data-consent-input="1"][name]');
|
||
const headSoalCard = document.getElementById('head_soal');
|
||
const consentNegativeKeywords = ['tidak', 'tidak setuju'];
|
||
let immediateSubmitActive = false;
|
||
|
||
function normalizeOtherValue(value) {
|
||
return (value || '').toString().trim().toLowerCase();
|
||
}
|
||
|
||
function isOtherChoice(value) {
|
||
const normalized = normalizeOtherValue(value);
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return normalized === 'lainnya'
|
||
|| normalized === 'lainnya (sebutkan)'
|
||
|| normalized === 'lainnya/other'
|
||
|| normalized === 'lainnya / other'
|
||
|| normalized === 'other'
|
||
|| normalized === 'others'
|
||
|| normalized.includes('lainnya')
|
||
|| normalized.includes('other');
|
||
}
|
||
|
||
function getLabelTextByInput(input) {
|
||
if (!input || !input.id) {
|
||
return '';
|
||
}
|
||
const label = document.querySelector('label[for="' + input.id + '"]');
|
||
return label ? (label.textContent || '') : '';
|
||
}
|
||
|
||
halInputs.forEach(function (field) {
|
||
if (field.dataset && field.dataset.lainnyaInput) {
|
||
field.dataset.originalRequired = '0';
|
||
field.dataset.dynamicRequired = field.required ? '1' : '0';
|
||
} else {
|
||
field.dataset.originalRequired = field.required ? '1' : '0';
|
||
field.dataset.dynamicRequired = field.dataset.dynamicRequired || '0';
|
||
}
|
||
});
|
||
|
||
setupLainnyaInputs();
|
||
setupOptionSearch();
|
||
setupRangeInputs();
|
||
setupDualFormInputs();
|
||
setupConsentWatcher();
|
||
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);
|
||
});
|
||
}
|
||
|
||
form.addEventListener('submit', function (event) {
|
||
if (formLocked) {
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
if (!immediateSubmitActive) {
|
||
return;
|
||
}
|
||
nonConsentFields.forEach(function (field) {
|
||
if (shouldDisableFieldForImmediate(field)) {
|
||
field.disabled = true;
|
||
}
|
||
});
|
||
});
|
||
|
||
function navigateRelative(step) {
|
||
if (immediateSubmitActive) {
|
||
return;
|
||
}
|
||
const currentIndex = halList.indexOf(currentHal);
|
||
const targetHal = halList[currentIndex + step];
|
||
if (typeof targetHal === 'undefined') {
|
||
return;
|
||
}
|
||
if (headSoalCard) {
|
||
if (targetHal === 1) {
|
||
headSoalCard.classList.remove('d-none');
|
||
} else {
|
||
headSoalCard.classList.add('d-none');
|
||
}
|
||
}
|
||
changeHal(targetHal);
|
||
}
|
||
|
||
function changeHal(targetHal) {
|
||
if (immediateSubmitActive) {
|
||
return;
|
||
}
|
||
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();
|
||
scrollPageToTop();
|
||
}
|
||
|
||
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;
|
||
|
||
const consentBlocked = hasIncompleteConsentForHal(currentHal);
|
||
|
||
if (prevButton) {
|
||
prevButton.disabled = immediateSubmitActive ? true : isFirst;
|
||
}
|
||
if (nextButton) {
|
||
if (immediateSubmitActive || consentBlocked) {
|
||
nextButton.style.display = 'none';
|
||
} else {
|
||
nextButton.style.display = '';
|
||
nextButton.disabled = isLast;
|
||
}
|
||
}
|
||
|
||
navHalButtons.forEach(function (button) {
|
||
const targetHal = parseInt(button.dataset.navHal, 10);
|
||
const isActive = targetHal === currentHal;
|
||
const movingForward = targetHal > currentHal;
|
||
button.classList.toggle('btn-primary', isActive);
|
||
button.classList.toggle('btn-outline-primary', !isActive);
|
||
button.disabled = immediateSubmitActive || (consentBlocked && movingForward);
|
||
});
|
||
|
||
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) {
|
||
const canShowFinalButton = !formLocked && (isLast || immediateSubmitActive);
|
||
finalSubmitButton.style.display = canShowFinalButton ? '' : 'none';
|
||
finalSubmitButton.disabled = !canShowFinalButton;
|
||
}
|
||
}
|
||
|
||
function setupLainnyaInputs() {
|
||
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 ? form.querySelector('[data-lainnya-wrapper="' + detailId + '"]') : null;
|
||
const labelText = getLabelTextByInput(event.target);
|
||
const shouldShow = !!customDetailId
|
||
|| isOtherChoice(event.target.value)
|
||
|| isOtherChoice(event.target.dataset.originalValue)
|
||
|| isOtherChoice(labelText);
|
||
|
||
if (targetWrapper) {
|
||
if (shouldShow) {
|
||
handleLainnyaInput(targetWrapper, true);
|
||
} else {
|
||
handleLainnyaInput(targetWrapper, false, true);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
|
||
form.querySelectorAll('[data-lainnya-wrapper].show').forEach(function (wrapper) {
|
||
handleLainnyaInput(wrapper, true);
|
||
});
|
||
}
|
||
|
||
function handleLainnyaInput(wrapper, show, clearValue = false) {
|
||
if (!wrapper) {
|
||
return;
|
||
}
|
||
wrapper.classList.toggle('show', show);
|
||
const input = wrapper.querySelector('input');
|
||
const detailId = wrapper.getAttribute('data-lainnya-wrapper');
|
||
if (input) {
|
||
input.required = show;
|
||
input.dataset.dynamicRequired = show ? '1' : '0';
|
||
if (show) {
|
||
input.focus();
|
||
syncCustomOptionValue(detailId, input.value);
|
||
} else {
|
||
if (clearValue) {
|
||
input.value = '';
|
||
}
|
||
resetCustomOptionValue(detailId);
|
||
}
|
||
} else if (!show) {
|
||
resetCustomOptionValue(detailId);
|
||
}
|
||
}
|
||
|
||
function syncCustomOptionValue(detailId, customValue) {
|
||
if (!detailId) {
|
||
return;
|
||
}
|
||
const trimmedValue = (customValue || '').toString().trim();
|
||
const radio = form.querySelector('input[data-lainnya-radio="' + detailId + '"]');
|
||
if (radio) {
|
||
const originalValue = radio.dataset.originalValue || 'lainnya';
|
||
radio.value = trimmedValue || originalValue;
|
||
}
|
||
}
|
||
|
||
function resetCustomOptionValue(detailId) {
|
||
if (!detailId) {
|
||
return;
|
||
}
|
||
const radio = form.querySelector('input[data-lainnya-radio="' + detailId + '"]');
|
||
if (radio && radio.dataset.originalValue) {
|
||
radio.value = radio.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))); // 0–1
|
||
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;
|
||
}
|
||
const tagName = (field.tagName || '').toLowerCase();
|
||
const type = (field.type || '').toLowerCase();
|
||
if (type === 'radio' || type === 'checkbox') {
|
||
return false;
|
||
}
|
||
if (tagName === 'select') {
|
||
const value = field.value;
|
||
return value === null || value === '';
|
||
}
|
||
const value = (field.value || '').toString().trim();
|
||
return value === '';
|
||
}
|
||
|
||
function hasIncompleteConsentForHal(halValue) {
|
||
if (!halValue || formLocked) {
|
||
return false;
|
||
}
|
||
const fields = form.querySelectorAll('[data-consent-input="1"][name][data-field-hal="' + halValue + '"]');
|
||
if (!fields.length) {
|
||
return false;
|
||
}
|
||
const groupMap = {};
|
||
let needsValue = false;
|
||
|
||
fields.forEach(function (field) {
|
||
if (!field || field.disabled) {
|
||
return;
|
||
}
|
||
const type = (field.type || '').toLowerCase();
|
||
const tagName = (field.tagName || '').toLowerCase();
|
||
|
||
if (type === 'radio' || type === 'checkbox') {
|
||
const groupName = field.name || field.id || ('consent-' + halValue);
|
||
if (!groupMap[groupName]) {
|
||
groupMap[groupName] = [];
|
||
}
|
||
groupMap[groupName].push(field);
|
||
return;
|
||
}
|
||
|
||
if (tagName === 'select') {
|
||
if (field.value === null || field.value === '') {
|
||
needsValue = true;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const value = (field.value || '').toString().trim();
|
||
if (!value) {
|
||
needsValue = true;
|
||
}
|
||
});
|
||
|
||
if (needsValue) {
|
||
return true;
|
||
}
|
||
|
||
return Object.keys(groupMap).some(function (groupName) {
|
||
const fieldsInGroup = groupMap[groupName] || [];
|
||
return !fieldsInGroup.some(function (input) {
|
||
return input.checked;
|
||
});
|
||
});
|
||
}
|
||
|
||
function setupDualFormInputs() {
|
||
document.querySelectorAll('[data-dual-wrapper]').forEach(function (wrapper) {
|
||
const detailId = wrapper.getAttribute('data-dual-wrapper');
|
||
const hiddenInput = wrapper.querySelector('[data-dual-hidden]');
|
||
const yearInput = wrapper.querySelector('[data-dual-input="tahun"]');
|
||
const monthInput = wrapper.querySelector('[data-dual-input="bulan"]');
|
||
const inputs = [yearInput, monthInput].filter(Boolean);
|
||
inputs.forEach(function (input) {
|
||
input.addEventListener('input', function () {
|
||
updateDualFormAnswer(detailId);
|
||
});
|
||
});
|
||
updateDualFormAnswer(detailId);
|
||
});
|
||
}
|
||
|
||
function updateDualFormAnswer(detailId) {
|
||
if (!detailId) {
|
||
return;
|
||
}
|
||
const wrapper = document.querySelector('[data-dual-wrapper="' + detailId + '"]');
|
||
if (!wrapper) {
|
||
return;
|
||
}
|
||
const hiddenInput = wrapper.querySelector('[data-dual-hidden]');
|
||
if (!hiddenInput) {
|
||
return;
|
||
}
|
||
const yearInput = wrapper.querySelector('[data-dual-input="tahun"]');
|
||
const monthInput = wrapper.querySelector('[data-dual-input="bulan"]');
|
||
const yearValue = (yearInput && yearInput.value ? yearInput.value : '').toString().trim();
|
||
const monthValue = (monthInput && monthInput.value ? monthInput.value : '').toString().trim();
|
||
const parts = [];
|
||
if (yearValue !== '') {
|
||
parts.push(yearValue + ' (Tahun)');
|
||
}
|
||
if (monthValue !== '') {
|
||
parts.push(monthValue + ' (Bulan)');
|
||
}
|
||
const combined = parts.join(' ');
|
||
hiddenInput.value = combined;
|
||
if (hiddenInput.required) {
|
||
hiddenInput.setCustomValidity(combined ? '' : 'Harap isi minimal salah satu kolom (Tahun/Bulan).');
|
||
}
|
||
}
|
||
|
||
function setupConsentWatcher() {
|
||
if (!consentFields.length) {
|
||
return;
|
||
}
|
||
consentFields.forEach(function (field) {
|
||
const handleChange = function () {
|
||
const type = (field.type || '').toLowerCase();
|
||
if ((type === 'radio' || type === 'checkbox') && !field.checked) {
|
||
return;
|
||
}
|
||
evaluateConsentValue(field.value);
|
||
};
|
||
|
||
field.addEventListener('change', handleChange);
|
||
if (shouldUseInputEvent(field)) {
|
||
field.addEventListener('input', handleChange);
|
||
}
|
||
|
||
if (
|
||
(field.type === 'radio' && field.checked) ||
|
||
(field.tagName === 'SELECT' && field.value) ||
|
||
(field.type !== 'radio' && field.value)
|
||
) {
|
||
evaluateConsentValue(field.value);
|
||
}
|
||
});
|
||
}
|
||
|
||
function shouldUseInputEvent(field) {
|
||
const tagName = (field.tagName || '').toLowerCase();
|
||
const type = (field.type || '').toLowerCase();
|
||
if (tagName === 'textarea') {
|
||
return true;
|
||
}
|
||
return type === 'text' || type === 'number' || type === 'range';
|
||
}
|
||
|
||
function evaluateConsentValue(value) {
|
||
const normalized = (value || '').toString().trim().toLowerCase();
|
||
const shouldStop = consentNegativeKeywords.some(function (keyword) {
|
||
return normalized.startsWith(keyword);
|
||
});
|
||
toggleImmediateSubmit(shouldStop);
|
||
}
|
||
|
||
function toggleImmediateSubmit(activate) {
|
||
if (immediateSubmitActive === activate) {
|
||
updateNavigationUI();
|
||
return;
|
||
}
|
||
immediateSubmitActive = activate;
|
||
halInputs.forEach(function (field) {
|
||
const originalRequired = field.dataset.originalRequired === '1';
|
||
const dynamicRequired = field.dataset.dynamicRequired === '1';
|
||
field.required = activate ? false : (originalRequired || dynamicRequired);
|
||
if (!field.required && typeof field.setCustomValidity === 'function') {
|
||
field.setCustomValidity('');
|
||
}
|
||
});
|
||
updateNavigationUI();
|
||
}
|
||
|
||
function scrollPageToTop() {
|
||
if (typeof window === 'undefined' || typeof window.scrollTo !== 'function') {
|
||
return;
|
||
}
|
||
try {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
} catch (error) {
|
||
window.scrollTo(0, 0);
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
@endsection
|