Compare commits

...

2 Commits

4 changed files with 944 additions and 97 deletions

View File

@ -23,10 +23,18 @@ class SoalController extends Controller
}
$selectedSoalId = (int) $request->query('soal_id', $daftarSoal->first()->id);
$pegawai = session('pegawai') ?? [];
$pegawaiId = is_array($pegawai) ? ($pegawai['id'] ?? null) : null;
if (!$request->boolean('start')) {
$completedSoalIds = [];
if ($pegawaiId) {
$completedSoalIds = Jawaban::where('pegawai_id', $pegawaiId)
->pluck('lms_mutu_soal_id')
->toArray();
}
return view('soal.list', [
'daftarSoal' => $daftarSoal,
'completedSoalIds' => $completedSoalIds,
]);
}
@ -70,7 +78,21 @@ class SoalController extends Controller
$soal->setRelation('soalDetail', $detailSoal);
$halPertama = $daftarHal->first() ?? $hal;
$existingJawaban = [];
$formLocked = false;
if ($pegawaiId) {
$jawabanTersimpan = Jawaban::where('pegawai_id', $pegawaiId)
->where('lms_mutu_soal_id', $soal->id)
->latest('tanggal_isi')
->first();
if ($jawabanTersimpan) {
$existingJawaban = $jawabanTersimpan->jawabanDetail
->pluck('jawaban', 'lms_mutu_soal_detail_id')
->toArray();
$formLocked = true;
}
}
$prefillJawaban = $this->generatePrefillJawaban($detailSoal, $pegawai);
return view('soal.index', [
'soal' => $soal,
'hal' => $hal,
@ -78,6 +100,10 @@ class SoalController extends Controller
'daftarHal' => $daftarHal,
'totalHal' => $daftarHal->count(),
'soalId' => $soal->id,
'pegawai' => $pegawai,
'prefillJawaban' => $prefillJawaban,
'existingJawaban' => $existingJawaban,
'formLocked' => $formLocked,
]);
}
@ -117,11 +143,13 @@ class SoalController extends Controller
$jawabanLainnya = $validated['jawaban_lainnya'] ?? [];
$namaResponden = null;
$unitKerja = null;
$pegawai = session('pegawai') ?? [];
$pegawaiId = is_array($pegawai) ? ($pegawai['id'] ?? null) : null;
DB::connection('dbLmsMutu')->transaction(function () use (&$jawabanBaru, $validated, $jawabanLainnya, $detailMeta, &$namaResponden, &$unitKerja) {
DB::connection('dbLmsMutu')->transaction(function () use (&$jawabanBaru, $validated, $jawabanLainnya, $detailMeta, &$namaResponden, &$unitKerja, $pegawaiId) {
$jawabanBaru = Jawaban::create([
'lms_mutu_soal_id' => $validated['lms_mutu_soal_id'],
'pegawai_id' => 1,
'pegawai_id' => $pegawaiId,
'tanggal_isi' => Carbon::now()->addHour(7),
]);
@ -190,4 +218,69 @@ class SoalController extends Controller
session(['pegawai' => $array]);
return redirect('/');
}
protected function generatePrefillJawaban($detailSoal, $pegawai): array
{
if (!is_iterable($detailSoal) || !is_array($pegawai) || empty($pegawai)) {
return [];
}
$prefill = [];
foreach ($detailSoal as $detail) {
$decoded = json_decode($detail->soal, true) ?? [];
$question = $decoded['soal'] ?? '';
$value = $this->mapPegawaiValueToQuestion($question, $pegawai);
if ($value !== null && $value !== '') {
$prefill[$detail->id] = $value;
}
}
return $prefill;
}
protected function mapPegawaiValueToQuestion(?string $question, array $pegawai): ?string
{
if (!$question) {
return null;
}
$normalized = Str::lower($question);
if (Str::contains($normalized, 'nama/inisial responden')) {
return $pegawai['namaLengkap'] ?? $pegawai['nama'] ?? null;
}
if (Str::contains($normalized, 'nomor telepon') || Str::contains($normalized, 'hp')) {
return $pegawai['noHandphone'] ?? null;
}
if (Str::contains($normalized, 'jenis kelamin')) {
return $pegawai['jenisKelamin']['jenisKelamin'] ?? $pegawai['jenisKelamin']['namaExternal'] ?? null;
}
if (Str::contains($normalized, 'unit/area kerja')) {
return $pegawai['ruangan']['namaRuangan'] ?? $pegawai['ruangan']['namaExternal'] ?? null;
}
if (Str::contains($normalized, 'posisi kerja')) {
return $pegawai['jabatanInternal']['namaJabatan']
?? $pegawai['jabatanFungsional']['namaJabatan']
?? $pegawai['jenisPegawai']['jenisPegawai']
?? null;
}
if (Str::contains($normalized, 'email')) {
return $pegawai['email'] ?? null;
}
if (Str::contains($normalized, 'nip') || Str::contains($normalized, 'nomor identitas')) {
return $pegawai['nipPns'] ?? $pegawai['noIdentitas'] ?? null;
}
if (Str::contains($normalized, 'nama faskes')) {
return $pegawai['ruangan']['namaExternal'] ?? 'RSAB HARAPAN KITA';
}
return null;
}
}

View File

@ -3,6 +3,7 @@
@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;
@ -12,6 +13,27 @@
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;
@ -21,6 +43,27 @@
background-color: #f8f9fa;
}
.select2-container {
width: 100% !important;
}
.select2-container .select2-selection--single {
min-height: calc(2.625rem + 2px);
padding: 0.5rem 1rem;
border: 1px solid #ced4da;
border-radius: 0.5rem;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 1.6;
color: #4f4f4f;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
top: 0.6rem;
right: 1rem;
}
.option-scroll::-webkit-scrollbar {
width: 6px;
}
@ -38,6 +81,10 @@
display: block;
margin-top: 0.85rem;
}
.dual-form-wrapper .form-label {
font-weight: 600;
}
</style>
@endsection
@ -53,6 +100,9 @@
$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">
@ -88,9 +138,7 @@
@if ($hal === $halPertama)
<div class="card border-0 shadow-sm mb-4" id="head_soal">
<div class="card-body">
<p class="text-muted mb-1">Judul Soal</p>
<h5 class="mb-3 text-primary">{{ $soal->judul_soal ?? '-' }}</h5>
<p class="text-muted mb-2">Keterangan</p>
<div class="border rounded p-3 bg-light text-body">
{!! $soal->keterangan_soal !!}
</div>
@ -106,13 +154,18 @@
<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())'>
<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 }}">
@ -123,82 +176,241 @@
$pertanyaan = $detailConfig['soal'] ?? 'Pertanyaan tidak tersedia';
$type = $detailConfig['type'] ?? 'option';
$options = $detailConfig['options'] ?? [];
$oldAnswer = old('jawaban.' . $detail->id);
$oldOtherAnswer = old('jawaban_lainnya.' . $detail->id);
$showLainnya = $oldAnswer === 'Lainnya' || (!empty($oldOtherAnswer));
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;
$currentAnswer = old('jawaban.' . $detail->id, $existingAnswer ?? $prefillAnswer);
$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();
$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') {
$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;
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];
}
}
@endphp
<div class="question-card mb-4"
data-hal-card="{{ $detailHal }}"
data-detail-id="{{ $detail->id }}"
data-consent-question="{{ $isConsentQuestion ? '1' : '0' }}"
data-question-type="{{ $type }}"
style="{{ $isVisible ? '' : 'display: none;' }}">
<div class="d-flex align-items-center justify-content-between mb-2 flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<span class="badge rounded-pill bg-label-primary fs-6">{{ $loop->iteration }}</span>
<div 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>
<h5 class="fw-semibold mb-0">{{ $pertanyaan }}</h5>
</div>
@if ($isConsentQuestion)
<p class="text-muted small mb-0">Pertanyaan persetujuan</p>
@endif
</div>
</div>
<h5 class="fw-semibold mb-3">{{ $pertanyaan }}</h5>
<div>
@if ($type === 'textarea')
<textarea class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" rows="4" required
data-field-hal="{{ $detailHal }}"
placeholder="Tulis jawaban Anda di sini">{{ old('jawaban.' . $detail->id) }}</textarea>
@elseif ($type === 'text')
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ old('jawaban.' . $detail->id) }}" required
data-field-hal="{{ $detailHal }}"
placeholder="Masukkan jawaban Anda">
@else
@if (!empty($options))
<div class="option-scroll">
@foreach ($options as $optionIndex => $option)
@php
$optionId = 'jawaban-' . $detail->id . '-' . $optionIndex;
$isLainnya = strtolower(trim($option)) === 'lainnya';
@endphp
<div class="form-check mb-2">
<input class="form-check-input @error('jawaban.' . $detail->id) is-invalid @enderror"
type="radio"
name="jawaban[{{ $detail->id }}]"
id="{{ $optionId }}"
value="{{ $option }}"
data-lainnya-radio="{{ $isLainnya ? $detail->id : '' }}" required
<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 }}"
{{ $oldAnswer === $option ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $optionId }}">
{{ $option }}
</label>
@if($formLocked) disabled @endif
placeholder="Masukkan tahun">
</div>
@if ($isLainnya)
<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" @endif
@if($formLocked) disabled @endif
required>
</div>
@elseif ($type === 'textarea')
<textarea class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" rows="4" required
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
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Masukkan jawaban Anda">
@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 }}"
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-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' : '' }}>
</div>
@endif
@else
@php
$lainnyaWrapperRendered = false;
@endphp
<div class="option-scroll">
@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">
<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
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>
</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
</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
@endforeach
</div>
@else
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ old('jawaban.' . $detail->id) }}" required
data-field-hal="{{ $detailHal }}"
placeholder="Masukkan jawaban Anda">
@endif
@else
<input type="text" class="form-control @error('jawaban.' . $detail->id) is-invalid @enderror"
name="jawaban[{{ $detail->id }}]" value="{{ $currentAnswer }}" required
data-field-hal="{{ $detailHal }}"
@if ($isConsentQuestion) data-consent-input="1" @endif
@if($formLocked) disabled @endif
placeholder="Masukkan jawaban Anda">
@endif
@endif
@endif
@error('jawaban.' . $detail->id)
<div class="invalid-feedback">{{ $message }}</div>
@enderror
@error('jawaban_lainnya.' . $detail->id)
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
@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>
</div>
@empty
@ -231,8 +443,8 @@
</div>
<button type="submit" class="btn btn-primary ms-md-auto"
data-final-submit="true"
style="{{ $isHalTerakhir ? '' : 'display: none;' }}"
{{ $soal->soalDetail->isEmpty() ? 'disabled' : '' }}>
style="{{ (!$isHalTerakhir || $formLocked) ? 'display: none;' : '' }}"
{{ $soal->soalDetail->isEmpty() || $formLocked ? 'disabled' : '' }}>
Simpan Jawaban
</button>
</div>
@ -243,6 +455,7 @@
@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');
@ -265,6 +478,7 @@
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]');
@ -276,8 +490,53 @@
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"]');
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';
}
});
initSelectSearch();
setupLainnyaInputs();
setupDualFormInputs();
setupConsentWatcher();
updateQuestionVisibility();
updateNavigationUI();
@ -300,21 +559,44 @@
});
}
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(targetHal === 1){
document.getElementById('head_soal').classList.remove('d-none')
}else{
document.getElementById('head_soal').classList.add('d-none')
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;
}
@ -371,20 +653,22 @@
const isLast = currentIndex === halList.length - 1;
if (prevButton) {
prevButton.disabled = isFirst;
prevButton.disabled = immediateSubmitActive ? true : isFirst;
}
if (nextButton) {
nextButton.disabled = isLast;
if (immediateSubmitActive) {
nextButton.style.display = 'none';
} else {
nextButton.style.display = '';
nextButton.disabled = isLast;
}
}
navHalButtons.forEach(function (button) {
if (parseInt(button.dataset.navHal, 10) === currentHal) {
button.classList.remove('btn-outline-primary');
button.classList.add('btn-primary');
} else {
button.classList.add('btn-outline-primary');
button.classList.remove('btn-primary');
}
const isActive = parseInt(button.dataset.navHal, 10) === currentHal;
button.classList.toggle('btn-primary', isActive);
button.classList.toggle('btn-outline-primary', !isActive);
button.disabled = immediateSubmitActive;
});
const progress = totalHal > 0 ? Math.round(((currentIndex + 1) / totalHal) * 100) : 100;
@ -402,34 +686,48 @@
summaryHal.textContent = 'Halaman ' + (currentIndex + 1) + ' dari ' + totalHal;
}
if (finalSubmitButton) {
finalSubmitButton.style.display = isLast ? '' : 'none';
const canShowFinalButton = !formLocked && (isLast || immediateSubmitActive);
finalSubmitButton.style.display = canShowFinalButton ? '' : 'none';
finalSubmitButton.disabled = !canShowFinalButton;
}
}
function setupLainnyaInputs() {
document.querySelectorAll('[data-lainnya-radio]').forEach(function (radio) {
document.querySelectorAll('input[type="radio"]').forEach(function (radio) {
radio.addEventListener('change', function (event) {
const detailId = event.target.getAttribute('data-lainnya-radio');
if (!detailId) {
return;
}
const targetWrapper = document.querySelector('[data-lainnya-wrapper="' + detailId + '"]');
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 labelText = getLabelTextByInput(event.target);
const shouldShow = !!customDetailId
|| isOtherChoice(event.target.value)
|| isOtherChoice(event.target.dataset.originalValue)
|| isOtherChoice(labelText);
if (targetWrapper) {
handleLainnyaInput(targetWrapper, true);
if (shouldShow) {
handleLainnyaInput(targetWrapper, true);
} else {
handleLainnyaInput(targetWrapper, false, true);
}
}
});
});
document.querySelectorAll('input[type="radio"]').forEach(function (radio) {
radio.addEventListener('change', function (event) {
const detailId = event.target.name.replace('jawaban[', '').replace(']', '');
const targetWrapper = document.querySelector('[data-lainnya-wrapper="' + detailId + '"]');
if (targetWrapper && event.target.value !== 'Lainnya') {
handleLainnyaInput(targetWrapper, false, true);
}
document.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);
});
}
function handleLainnyaInput(wrapper, show, clearValue = false) {
@ -438,15 +736,220 @@
}
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();
} else if (clearValue) {
input.value = '';
syncCustomOptionValue(detailId, input.value);
} else {
if (clearValue) {
input.value = '';
}
resetCustomOptionValue(detailId);
}
} else if (!show) {
resetCustomOptionValue(detailId);
}
}
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 + '"]');
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 + '"]');
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 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 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');
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) {
field.addEventListener('change', function (event) {
if (field.type === 'radio' && !field.checked) {
return;
}
evaluateConsentValue(event.target.value);
});
if (
(field.type === 'radio' && field.checked) ||
(field.tagName === 'SELECT' && field.value) ||
(field.type !== 'radio' && field.value)
) {
evaluateConsentValue(field.value);
}
});
}
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();
}
});
</script>
@endsection

View File

@ -35,20 +35,32 @@
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@php
$completedSoalIds = $completedSoalIds ?? [];
@endphp
<div class="row g-4">
@foreach ($daftarSoal as $soal)
@php
$sudahDikerjakan = in_array($soal->id, $completedSoalIds);
@endphp
<div class="col-lg-4 col-md-6">
<div class="card soal-card h-100 shadow-sm">
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-3">
<span class="badge bg-label-primary">Kuesioner</span>
<span class="badge {{ $sudahDikerjakan ? 'bg-success' : 'bg-label-primary' }}">
{{ $sudahDikerjakan ? 'Sudah Dikerjakan' : 'Kuesioner' }}
</span>
</div>
<h5 class="fw-semibold mb-2">{{ $soal->judul_soal ?? 'Tanpa Judul' }}</h5>
<p class="text-muted flex-grow-1">{{ \Illuminate\Support\Str::limit(strip_tags($soal->keterangan_soal ?? 'Belum ada keterangan'), 120) }}</p>
<div class="mt-3 d-flex justify-content-between align-items-center">
<a href="{{ route('soal.index', ['start' => 1, 'soal_id' => $soal->id]) }}" class="btn btn-primary btn-sm btn-start">
Mulai Isi
</a>
@if ($sudahDikerjakan)
<span class="text-success fw-semibold">Terisi</span>
@else
<a href="{{ route('soal.index', ['start' => 1, 'soal_id' => $soal->id]) }}" class="btn btn-primary btn-sm btn-start">
Mulai Isi
</a>
@endif
</div>
</div>
</div>

239
session_pegawai.json Normal file
View File

@ -0,0 +1,239 @@
array:65 [ // app\Http\Controllers\SoalController.php:74
"kdProfile" => 0
"statusEnabled" => true
"id" => 96
"noRec" => "E3C54413-605D-41C4-9738-F9909C6A"
"kodeExternal" => "1"
"idFinger" => "927"
"nama" => "Cathline Freya Adhiwidjaja"
"kedudukan" => array:5 [
"statusEnabled" => true
"id" => 1
"reportDisplay" => "Aktif"
"namaExternal" => "Aktif"
"name" => "Aktif"
]
"kedudukanId" => "1"
"tglPensiun" => 2001171600000
"pensiun" => 58
"statusKawin" => array:10 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 2
"noRec" => "b842fb2a-6621-489a-ba34-b058027f"
"reportDisplay" => "Kawin"
"kodeExternal" => "02"
"namaExternal" => "Kawin"
"kdStatusPerkawinan" => 2
"qStatusPerkawinan" => 2
"statusPerkawinan" => "Kawin"
]
"statusKawinId" => "2"
"alamat" => "Jl. Sutera Amaryllis VI / 5 Pakulonan Serpong Utara TANGERANG Banten"
"kodePos" => ""
"bankRekeningAtasNama" => "dr. Cathline Freya Adhiwidjaja, Sp.BA"
"bankRekeningNama" => "PT. BANK RAKYAT INDONESIA (Persero) Tbk."
"bankRekeningNomor" => "096201002367509"
"agama" => array:10 []
"agamaId" => 2
"detailKategoryPegawai" => array:9 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 18
"noRec" => "42D0F2DC-C541-4206-BAF8-4C76DE5F"
"reportDisplay" => "PNS"
"namaExternal" => "PNS"
"detailKategoryPegawai" => "PNS"
"kdDetailKategoryPegawai" => "A1"
"qDetailKategoryPegawai" => 31
]
"detailKategoryPegawaiId" => 18
"eselon" => array:10 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 5
"noRec" => "5C3183E3-08EC-44CC-B568-F47AFD2D"
"reportDisplay" => "III A"
"kodeExternal" => "0005"
"namaExternal" => "III A"
"eselon" => "III A"
"kdEselon" => 5
"qEselon" => 5
]
"eselonId" => 5
"jabatanFungsional" => array:13 [
"statusEnabled" => true
"id" => 35
"reportDisplay" => "Dokter Ahli Muda"
"kodeExternal" => "35"
"namaExternal" => "Dokter Ahli Muda"
"namaJabatan" => "Dokter Ahli Muda"
"levelJabatan" => 5
"eselonId" => 0
"usiaPensiun" => 58
"jenisJabatanId" => 1
"kelompokJabatanId" => 3
"kdJabatan" => "35"
"unitKerjaId" => 61
]
"jabatanFungsionalId" => 35
"shiftKerja" => array:15 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 1
"noRec" => "1 "
"reportDisplay" => "Non Shift"
"kodeExternal" => ""
"namaExternal" => ""
"detail" => []
"kode" => 1
"name" => "Non Shift"
"kdKelompokShiftKerja" => 1
"kelompokShiftKerja" => "Non Shift"
"factorRate" => 160
"operatorFactorRate" => "/"
"qKelompokShift" => 1
]
"shiftKerjaId" => 1
"jenisKelamin" => array:10 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 2
"noRec" => "6014ef02-a0b5-4d39-a17f-19654e16"
"reportDisplay" => "P"
"kodeExternal" => "F"
"namaExternal" => "Perempuan"
"jenisKelamin" => "Perempuan"
"kdJenisKelamin" => 2
"qJenisKelamin" => 2
]
"jenisKelaminId" => 2
"jenisPegawai" => array:8 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 1
"noRec" => "385cd9d8-e6dc-435e-be1a-0da6dd89"
"reportDisplay" => "DOKTER"
"jenisPegawai" => "DOKTER"
"kdJenisPegawai" => "A0"
"qJenisPegawai" => 1
]
"jenisPegawaiId" => 1
"negara" => array:10 []
"negaraId" => 0
"pangkat" => array:10 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 14
"noRec" => "14 "
"reportDisplay" => "Pembina Tingkat I"
"kdPangkat" => 14
"namaPangkat" => "Pembina Tingkat I"
"noUrut" => 14
"qPangkat" => 14
"ruang" => "b"
]
"pangkatId" => 14
"ruangan" => array:14 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 102
"noRec" => "102 "
"reportDisplay" => "Komite Medik"
"kodeExternal" => "0117"
"namaExternal" => "Komite Medik"
"departemenId" => 51
"kdRuangan" => "102"
"namaRuangan" => "Komite Medik"
"statusViewData" => true
"kamarSet" => []
"kelompokRuangan" => "REGULER"
"qruangan" => 102
]
"ruanganId" => 102
"kategoryPegawai" => array:10 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 1
"noRec" => "09a02190-0c08-4c10-b178-51abf99d"
"reportDisplay" => "PNS"
"kodeExternal" => "02"
"namaExternal" => "PNS"
"kategoryPegawai" => "PNS"
"kdKategoryPegawai" => "A"
"qkategoryPegawai" => 1
]
"statusPegawai" => array:11 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 2
"noRec" => "6877c359-aa73-474b-89ce-c33439d1"
"reportDisplay" => "Aktif"
"kodeExternal" => ""
"namaExternal" => "A"
"statusPegawaiHeadSet" => []
"kdStatusPegawai" => 2
"qStatusPegawai" => 2
"statusPegawai" => "Aktif"
]
"statusPegawaiId" => 2
"typePegawai" => array:10 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 1
"noRec" => "221fee76-a4db-4c72-8ed3-959c7549"
"reportDisplay" => "PURNA WAKTU (FULL TIME)"
"kodeExternal" => "1"
"namaExternal" => "PURNA WAKTU (FULL TIME)"
"kdTypePegawai" => 1
"qTypePegawai" => 1
"typePegawai" => "PURNA WAKTU (FULL TIME)"
]
"typePegawaiId" => 1
"namaKeluarga" => ""
"namaLengkap" => "dr. Cathline Freya Adhiwidjaja, Sp.BA"
"namaPanggilan" => ""
"nipPns" => "197505132014122001"
"noIdentitas" => "3674025305750005"
"npwp" => "573859154411000"
"photoDiri" => ""
"qtyAnak" => 0
"statusRhesus" => ""
"tempatLahir" => "Bandung"
"tglLahir" => 169146000000
"tglMasuk" => 1317315600000
"email" => "cfreya@yahoo.com"
"noHandphone" => "08118004286"
"satuanKerjaId" => 15
"jadwalPemeriksaanSet" => []
"jabatanInternal" => array:14 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 561
"noRec" => "561 "
"reportDisplay" => "Dokter Spesialis Bedah Anak"
"namaJabatan" => "Dokter Spesialis Bedah Anak 1 A"
"noUrut" => 561
"levelJabatan" => 5
"subLevelJabatan" => 1
"jenisJabatanId" => 8
"kelompokJabatanId" => 3
"kdJabatan" => "561"
"qJabatan" => 561
"unitKerjaId" => 63
]
"jabatanInternalId" => 561
"riwayatPendidikanSet" => []
"golongan" => array:8 [
"kdProfile" => 0
"statusEnabled" => true
"id" => 4
"noRec" => "c14cd8d6-e128-4c5f-a50d-c6b95c14"
"reportDisplay" => "IV/b"
"kodeExternal" => "04"
"namaExternal" => "PEMBINA TINGKAT I"
"name" => "IV/b"
]
"qpegawai" => 96
"golonganId" => 4
]