done soal

This commit is contained in:
JokoPrasetio 2025-12-05 15:35:05 +07:00
parent b6677d9103
commit a672d8f606
3 changed files with 144 additions and 23 deletions

View File

@ -75,6 +75,25 @@ class SoalController extends Controller
return $detail;
});
$groupInfoByHal = [];
foreach ($detailSoal as $detail) {
$halValue = (int) ($detail->hal ?? $daftarHal->first());
if (!array_key_exists($halValue, $groupInfoByHal)) {
$groupInfoByHal[$halValue] = [
'nama' => $detail->group_nama,
'keterangan' => $detail->group_keterangan,
];
continue;
}
if (empty($groupInfoByHal[$halValue]['nama']) && !empty($detail->group_nama)) {
$groupInfoByHal[$halValue]['nama'] = $detail->group_nama;
}
if (empty($groupInfoByHal[$halValue]['keterangan']) && !empty($detail->group_keterangan)) {
$groupInfoByHal[$halValue]['keterangan'] = $detail->group_keterangan;
}
}
$soal->setRelation('soalDetail', $detailSoal);
$halPertama = $daftarHal->first() ?? $hal;
@ -104,6 +123,7 @@ class SoalController extends Controller
'prefillJawaban' => $prefillJawaban,
'existingJawaban' => $existingJawaban,
'formLocked' => $formLocked,
'groupInfoByHal' => $groupInfoByHal,
]);
}

View File

@ -193,6 +193,8 @@
$formLocked = $formLocked ?? false;
$prefillJawaban = $prefillJawaban ?? [];
$existingJawaban = $existingJawaban ?? [];
$groupInfoByHal = $groupInfoByHal ?? [];
$currentGroupInfo = $groupInfoByHal[$hal] ?? ['nama' => null, 'keterangan' => null];
@endphp
<div class="py-4">
@ -224,6 +226,19 @@
</div>
</div>
</div>
@php
$groupNamaAktif = $currentGroupInfo['nama'] ?? null;
$groupKeteranganAktif = $currentGroupInfo['keterangan'] ?? null;
$groupNamaLabel = $groupNamaAktif ?: '-';
$groupKeteranganLabel = $groupKeteranganAktif ?: '-';
$shouldHideGroupCard = ($groupNamaLabel === '-' && $groupKeteranganLabel === '-');
@endphp
<div class="card border-0 shadow-sm mb-4 {{ $shouldHideGroupCard ? 'd-none' : '' }}" id="group_info_card">
<div class="card-body">
<h6 class="mb-2 text-primary" id="group-info-nama">{{ $groupNamaLabel }}</h6>
<p class="mb-0 text-body" id="group-info-keterangan">{{ $groupKeteranganLabel }}</p>
</div>
</div>
@if ($hal === $halPertama)
<div class="card border-0 shadow-sm mb-4" id="head_soal">
<div class="card-body">
@ -254,7 +269,7 @@
<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' }}">
<form id="form-soal" method="POST" action="{{ route('soal.store') }}" data-hal-list='@json($listHal->values())' data-form-locked="{{ $formLocked ? '1' : '0' }}" data-group-info='@json($groupInfoByHal)'>
@csrf
<input type="hidden" name="lms_mutu_soal_id" value="{{ $soal->id }}">
<input type="hidden" name="hal" id="input-hal" value="{{ $hal }}">
@ -263,20 +278,62 @@
$questionCounter = 0;
$groupedQuestionDetails = [];
$groupIndexMap = [];
$isLainnyaLabel = function ($value) {
if (!is_string($value)) {
return false;
}
$normalized = trim(strtolower($value));
if ($normalized === '') {
return false;
}
return $normalized === 'lainnya'
|| str_contains($normalized, 'lainnya')
|| str_contains($normalized, 'other');
};
$isTidakBerlakuTidakTahuLabel = function ($value) {
if (!is_string($value)) {
return false;
}
$normalized = trim(strtolower(preg_replace('/\s+/', ' ', $value)));
if ($normalized === '') {
return false;
}
$hasTidakBerlakuTidakTahu = str_contains($normalized, 'tidak berlaku') && str_contains($normalized, 'tidak tahu');
return $hasTidakBerlakuTidakTahu || str_contains($normalized, 'sebutkan alasan');
};
$isOtherOptionLabel = function ($value) use ($isLainnyaLabel, $isTidakBerlakuTidakTahuLabel) {
if (!is_string($value)) {
return false;
}
return $isLainnyaLabel($value) || $isTidakBerlakuTidakTahuLabel($value);
};
foreach ($soal->soalDetail as $detailItem) {
$detailConfigItem = json_decode($detailItem->soal, true) ?? [];
$detailHalValue = $detailItem->hal ?? $listHal->first();
$questionNumberValue = $detailConfigItem['no'] ?? null;
if (is_string($questionNumberValue)) {
$questionNumberValue = trim($questionNumberValue);
}
$hasNumberValue = $questionNumberValue !== null && $questionNumberValue !== '';
$groupNumberNormalized = null;
if ($hasNumberValue) {
$compactedNumber = strtolower(preg_replace('/[^a-z0-9]/i', '', (string) $questionNumberValue));
if (preg_match('/^(\d+)[a-z]+$/', $compactedNumber, $numberMatch)) {
$groupNumberNormalized = $numberMatch[1];
} elseif (preg_match('/^(\d+)$/', $compactedNumber, $numberMatch)) {
$groupNumberNormalized = $numberMatch[1];
}
}
$groupNumberDisplay = $hasNumberValue ? ($groupNumberNormalized ?? $questionNumberValue) : null;
$groupKey = $hasNumberValue
? $detailHalValue . '|' . $questionNumberValue
? $detailHalValue . '|' . ($groupNumberNormalized ?? (string) $questionNumberValue)
: 'detail_' . $detailItem->id;
if (!array_key_exists($groupKey, $groupIndexMap)) {
$groupIndexMap[$groupKey] = count($groupedQuestionDetails);
$groupedQuestionDetails[] = [
'hal' => $detailHalValue,
'number' => $hasNumberValue ? $questionNumberValue : null,
'number' => $groupNumberDisplay,
'items' => [],
];
}
@ -348,28 +405,35 @@
$oldOtherAnswer = $currentAnswer;
}
$showLainnya = $isCustomCurrentAnswer
|| (is_string($currentAnswer) && stripos($currentAnswer, 'lainnya') !== false)
|| (is_string($currentAnswer) && $isOtherOptionLabel($currentAnswer))
|| (!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;
$hasCustomOtherOption = collect($options)->contains(function ($optionItem) use ($isOtherOptionLabel) {
return $isOtherOptionLabel($optionItem);
});
if (!$hasLainnyaOption && $type === 'option_with_other') {
if (!$hasCustomOtherOption && $type === 'option_with_other') {
$options[] = 'Lainnya';
$optionsCount = count($options);
$hasLainnyaOption = true;
$hasCustomOtherOption = true;
}
$lainnyaOptionLabel = collect($options)->first(function ($optionItem) use ($isLainnyaLabel) {
return $isLainnyaLabel($optionItem);
});
$tidakBerlakuOptionLabel = collect($options)->first(function ($optionItem) use ($isTidakBerlakuTidakTahuLabel) {
return $isTidakBerlakuTidakTahuLabel($optionItem);
});
$otherOptionHintLabel = $lainnyaOptionLabel ?: $tidakBerlakuOptionLabel;
$shouldForceOtherSelection = $hasCustomOtherOption && $showLainnya;
$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);
$displayNumber = $questionNumber !== '' ? $questionNumber : ($hasGroupNumber ? $groupNumber : '');
$shouldShowNumber = $displayNumber !== null && $displayNumber !== '';
$rangeMin = null;
$rangeMax = null;
$rangeStep = 1;
@ -490,9 +554,9 @@
<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>
<span class="badge rounded-pill bg-label-primary fs-6">{{ $displayNumber }}</span>
@elseif($hasGroupNumber)
<span class="badge rounded-pill bg-label-primary fs-6 badge-placeholder" aria-hidden="true">{{ $badgeNumber }}</span>
<span class="badge rounded-pill bg-label-primary fs-6 badge-placeholder" aria-hidden="true">{{ $displayNumber }}</span>
@endif
<h5 class="fw-semibold mb-0">{{ $pertanyaan }}</h5>
</div>
@ -637,9 +701,9 @@
@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);
$isOtherOption = $isOtherOptionLabel($optionLabel);
$optionValue = $isOtherOption && $oldOtherAnswer ? $oldOtherAnswer : $optionLabel;
$shouldCheck = $currentAnswer === $optionValue || ($isOtherOption && $shouldForceOtherSelection);
@endphp
<div class="form-check mb-2" data-option-item>
<input class="form-check-input @error('jawaban.' . $detail->id) is-invalid @enderror"
@ -648,7 +712,7 @@
id="{{ $optionId }}"
value="{{ $optionValue }}"
data-original-value="{{ $optionLabel }}"
data-lainnya-radio="{{ $isLainnya ? $detail->id : '' }}"
data-lainnya-radio="{{ $isOtherOption ? $detail->id : '' }}"
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" required @endif
@if($formLocked) disabled @endif
@ -658,7 +722,7 @@
</label>
</div>
@if ($isLainnya)
@if ($isOtherOption)
@php
$lainnyaWrapperRendered = true;
@endphp
@ -675,9 +739,9 @@
@endif
@endforeach
</div>
@if ($hasLainnyaOption || $type === 'option_with_other')
@if ($otherOptionHintLabel)
<small class="form-text text-muted">
Jika jawaban yang Anda cari tidak ada di daftar, pilih opsi <strong>"Lainnya"</strong> lalu isi sesuai kebutuhan.
Jika jawaban yang Anda cari tidak ada di daftar, pilih opsi <strong>"{{ $otherOptionHintLabel }}"</strong> lalu isi sesuai kebutuhan.
</small>
@endif
<div class="text-muted small mt-2" data-option-empty="{{ $detail->id }}" style="display: none;">
@ -794,8 +858,25 @@
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'];
const consentNegativeKeywords = ['tidak', 'tidak setuju', 'saya tidak bersedia'];
let immediateSubmitActive = false;
let groupInfoMap = {};
try {
groupInfoMap = JSON.parse(form.dataset.groupInfo || '{}');
} catch (error) {
groupInfoMap = {};
}
if (Array.isArray(groupInfoMap)) {
groupInfoMap = groupInfoMap.reduce(function (acc, value, index) {
if (value && typeof value === 'object') {
acc[index] = value;
}
return acc;
}, {});
}
const groupInfoCard = document.getElementById('group_info_card');
const groupInfoNama = document.getElementById('group-info-nama');
const groupInfoKeterangan = document.getElementById('group-info-keterangan');
function normalizeOtherValue(value) {
return (value || '').toString().trim().toLowerCase();
@ -806,6 +887,9 @@
if (!normalized) {
return false;
}
const includesTidakBerlakuTidakTahu =
normalized.includes('tidak berlaku') && normalized.includes('tidak tahu');
const includesSebutkanAlasan = normalized.includes('sebutkan alasan');
return normalized === 'lainnya'
|| normalized === 'lainnya (sebutkan)'
|| normalized === 'lainnya/other'
@ -813,7 +897,9 @@
|| normalized === 'other'
|| normalized === 'others'
|| normalized.includes('lainnya')
|| normalized.includes('other');
|| normalized.includes('other')
|| includesTidakBerlakuTidakTahu
|| includesSebutkanAlasan;
}
function getLabelTextByInput(input) {
@ -841,6 +927,7 @@
setupConsentWatcher();
updateQuestionVisibility();
updateNavigationUI();
updateGroupInfoUI();
navHalButtons.forEach(function (button) {
button.addEventListener('click', function () {
@ -916,6 +1003,7 @@
}
updateQuestionVisibility();
updateNavigationUI();
updateGroupInfoUI();
scrollPageToTop();
}
@ -999,6 +1087,19 @@
}
}
function updateGroupInfoUI() {
if (!groupInfoCard || !groupInfoNama || !groupInfoKeterangan) {
return;
}
const info = (groupInfoMap && groupInfoMap[currentHal]) || {};
const namaText = (info.nama || '').toString().trim() || '-';
const ketText = (info.keterangan || '').toString().trim() || '-';
groupInfoNama.textContent = namaText;
groupInfoKeterangan.textContent = ketText;
const shouldHide = namaText === '-' && ketText === '-';
groupInfoCard.classList.toggle('d-none', shouldHide);
}
function setupLainnyaInputs() {
const radios = form.querySelectorAll('input[type="radio"][name^="jawaban["]');
radios.forEach(function (radio) {

View File

@ -26,7 +26,7 @@
@section('content')
<div class="py-5">
<div class="text-center mb-5">
<h2 class="fw-bold mb-2">Selamat Datang di Survei Mutu</h2>
<h2 class="fw-bold mb-2">Selamat Datang di Survei Komite Mutu Rumah Sakit</h2>
<p class="text-muted mb-0">Silakan pilih kuesioner yang ingin Anda isi. Pastikan data yang diberikan sesuai kondisi unit kerja Anda.</p>
</div>
@if (session('success'))