Files
eCert-MBIP/resources/views/admin/questionnaires/show.blade.php
2026-05-19 09:53:36 +08:00

524 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.admin')
@section('title', $set->title)
@section('header', $set->title)
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.questionnaires.index') }}">Soalselidik</a></li>
<li class="breadcrumb-item active">{{ Str::limit($set->title, 35) }}</li>
@endsection
@section('header-actions')
<div class="d-flex gap-2">
<a href="{{ route('admin.questionnaires.edit', $set) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil me-1"></i> Edit
</a>
@if($set->status === 'draft')
<form method="POST" action="{{ route('admin.questionnaires.publish', $set) }}">
@csrf
<button class="btn btn-sm btn-success" onclick="return confirm('Terbitkan set soalselidik ini?')">
<i class="bi bi-send me-1"></i> Terbitkan
</button>
</form>
@elseif($set->status === 'published')
<form method="POST" action="{{ route('admin.questionnaires.archive', $set) }}">
@csrf
<button class="btn btn-sm btn-outline-secondary" onclick="return confirm('Arkibkan set soalselidik ini?')">
<i class="bi bi-archive me-1"></i> Arkib
</button>
</form>
@endif
</div>
@endsection
@push('styles')
<style>
.tajuk-block { border-left: 3px solid #6c757d; }
.tajuk-header { background: #f8f9fa; }
.children-list { border-top: 1px solid #dee2e6; }
.children-list .list-group-item { background: #fff; }
.children-list .list-group-item:last-child { border-bottom: 0; }
.drag-handle { cursor: grab; color: #adb5bd; }
.drag-handle:active { cursor: grabbing; }
.sortable-ghost { opacity: .4; background: #e9f0ff !important; }
</style>
@endpush
@section('content')
<div class="row g-4">
{{-- Left: Questions --}}
<div class="col-md-8">
@if($set->status === 'draft')
<div class="alert alert-warning mb-3 small">
<i class="bi bi-exclamation-triangle me-2"></i>
Set ini masih dalam status <strong>Draf</strong>. Terbitkan setelah siap untuk dilampirkan ke program.
</div>
@elseif($set->status === 'archived')
<div class="alert alert-secondary mb-3 small">
<i class="bi bi-archive me-2"></i>
Set ini telah <strong>diarkibkan</strong> dan tidak boleh dilampirkan ke program baru.
</div>
@endif
{{-- Question List --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-list-ul me-2 text-primary"></i>Senarai Soalan
<span class="badge bg-secondary ms-2">{{ $totalCount }}</span>
</h6>
<small class="text-muted"><i class="bi bi-grip-vertical me-1"></i>Seret untuk susun semula</small>
</div>
@if($totalCount === 0)
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-question-circle d-block fs-1 mb-3 opacity-25"></i>
Belum ada soalan. Tambah soalan menggunakan borang di sebelah kanan.
</div>
@else
<ul class="list-group list-group-flush" id="questionList">
@foreach($topLevel as $q)
@if($q->question_type === 'tajuk')
{{-- ── TAJUK BLOCK ── --}}
<li class="list-group-item p-0 tajuk-block" data-id="{{ $q->id }}">
{{-- Tajuk header row --}}
<div class="tajuk-header d-flex align-items-center gap-2 px-3 py-2">
<i class="bi bi-grip-vertical drag-handle drag-handle-top fs-5"></i>
<span class="badge bg-dark small">Tajuk</span>
<div class="flex-grow-1 fw-semibold">{{ $q->question_text }}</div>
@if($q->rating_labels)
<div class="small text-muted text-nowrap d-none d-md-block">
@php $labels = array_filter($q->rating_labels); @endphp
@if(!empty($labels))
<i class="bi bi-tag me-1"></i>{{ implode(' · ', $labels) }}
@endif
</div>
@endif
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary"
onclick="editQuestion({{ $q->id }}, @json($q->question_text), 'tajuk', false, [], null, @json($q->rating_labels ?? []))">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ route('admin.questions.destroy', $q) }}"
onsubmit="return confirm('Padam bahagian '{{ addslashes($q->question_text) }}' dan semua soalan di dalamnya?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</div>
{{-- Rating labels pills --}}
@if($q->rating_labels && array_filter($q->rating_labels))
<div class="px-3 py-1 bg-light border-bottom d-flex gap-2 flex-wrap d-md-none">
@foreach(array_filter($q->rating_labels) as $val => $lbl)
<span class="badge bg-light text-dark border small">{{ $val }}: {{ $lbl }}</span>
@endforeach
</div>
@endif
{{-- Children list --}}
<ul class="children-list list-group list-group-flush" data-parent-id="{{ $q->id }}">
@foreach($q->children as $child)
<li class="list-group-item d-flex align-items-start gap-2 py-2 ps-4" data-id="{{ $child->id }}">
<i class="bi bi-grip-vertical drag-handle drag-handle-child fs-5 mt-1"></i>
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-light text-dark border small">{{ $loop->iteration }}</span>
<span class="badge bg-primary bg-opacity-10 text-primary small">Rating 15</span>
@if($child->is_required)
<span class="badge bg-danger bg-opacity-10 text-danger small">Wajib</span>
@endif
</div>
<div class="small">{{ $child->question_text }}</div>
</div>
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary"
onclick="editQuestion({{ $child->id }}, @json($child->question_text), 'rating', {{ $child->is_required ? 'true' : 'false' }}, [], {{ $child->parent_id ?? 'null' }}, [])">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ route('admin.questions.destroy', $child) }}"
onsubmit="return confirm('Padam soalan ini?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</li>
@endforeach
@if($q->children->isEmpty())
<li class="list-group-item text-muted small text-center py-2 fst-italic ps-4">
<i class="bi bi-arrow-down-short me-1"></i>Tiada soalan dalam bahagian ini
</li>
@endif
</ul>
</li>
@else
{{-- ── STANDALONE QUESTION ── --}}
<li class="list-group-item py-3" data-id="{{ $q->id }}">
<div class="d-flex justify-content-between align-items-start gap-2">
<div class="d-flex align-items-start gap-2 flex-grow-1">
<i class="bi bi-grip-vertical drag-handle drag-handle-top fs-5 mt-1"></i>
<div>
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-light text-dark border small">{{ $loop->iteration }}</span>
<span class="badge bg-primary bg-opacity-10 text-primary small">
{{ match($q->question_type) {
'rating' => 'Rating 15',
'single_choice' => 'Pilihan Tunggal',
'multiple_choice' => 'Pilihan Berganda',
'short_text' => 'Teks Pendek',
'long_text' => 'Teks Panjang',
default => $q->question_type,
} }}
</span>
@if($q->is_required)
<span class="badge bg-danger bg-opacity-10 text-danger small">Wajib</span>
@endif
</div>
<div class="fw-medium">{{ $q->question_text }}</div>
@if($q->options_json)
<div class="mt-1">
@foreach($q->options_json as $opt)
<span class="badge bg-light text-dark border me-1 small">{{ $opt }}</span>
@endforeach
</div>
@endif
</div>
</div>
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary"
onclick="editQuestion({{ $q->id }}, @json($q->question_text), '{{ $q->question_type }}', {{ $q->is_required ? 'true' : 'false' }}, @json($q->options_json ?? []), null, [])">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ route('admin.questions.destroy', $q) }}"
onsubmit="return confirm('Padam soalan ini?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</div>
</li>
@endif
@endforeach
</ul>
@endif
</div>
{{-- Used In Programs --}}
@if($usedInPrograms->count())
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-diagram-3 me-2 text-secondary"></i>Digunakan Dalam Program</h6>
</div>
<ul class="list-group list-group-flush">
@foreach($usedInPrograms as $program)
<li class="list-group-item d-flex justify-content-between align-items-center py-2">
<a href="{{ route('admin.programs.show', $program) }}" class="text-decoration-none small">
{{ $program->title }}
</a>
@if($program->pivot->is_confirmed ?? false)
<span class="badge bg-success">Disahkan</span>
@else
<span class="badge bg-warning text-dark">Belum Disahkan</span>
@endif
</li>
@endforeach
</ul>
</div>
@endif
</div>
{{-- Right: Add / Edit Question Form --}}
<div class="col-md-4">
<div class="card border-0 shadow-sm sticky-top" style="top:80px;">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold" id="formTitle">
<i class="bi bi-plus-circle me-2 text-primary"></i>Tambah Soalan
</h6>
</div>
<div class="card-body">
<form method="POST" id="questionForm" action="{{ route('admin.questions.store', $set) }}">
@csrf
<input type="hidden" name="_method" id="formMethod" value="POST">
<input type="hidden" name="_question_id" id="questionId" value="">
{{-- Question text --}}
<div class="mb-3">
<label class="form-label small fw-medium">Teks Soalan / Tajuk <span class="text-danger">*</span></label>
<textarea name="question_text" id="questionText" rows="3"
class="form-control form-control-sm @error('question_text') is-invalid @enderror"
placeholder="Taip soalan atau nama bahagian..."></textarea>
@error('question_text')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
{{-- Question type --}}
<div class="mb-3">
<label class="form-label small fw-medium">Jenis <span class="text-danger">*</span></label>
<select name="question_type" id="questionType" class="form-select form-select-sm" onchange="onTypeChange()">
<option value="tajuk">Tajuk Bahagian</option>
<option value="rating">Rating (15)</option>
<option value="single_choice">Pilihan Tunggal</option>
<option value="multiple_choice">Pilihan Berganda</option>
<option value="short_text">Teks Pendek</option>
<option value="long_text">Teks Panjang</option>
</select>
</div>
{{-- Required (hidden for tajuk) --}}
<div class="mb-3 form-check d-none" id="requiredSection">
<input type="checkbox" name="is_required" id="isRequired"
class="form-check-input" value="1" checked>
<label class="form-check-label small" for="isRequired">Wajib dijawab</label>
</div>
{{-- Parent selector (rating only) --}}
<div id="parentSection" class="mb-3 d-none">
<label class="form-label small fw-medium">Bahagian (Tajuk) <span class="text-danger">*</span></label>
<select name="parent_id" id="parentId" class="form-select form-select-sm">
<option value=""> Pilih Tajuk </option>
</select>
@error('parent_id')
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
</div>
{{-- Rating labels (tajuk only) --}}
<div id="ratingLabelsSection" class="mb-3">
<label class="form-label small fw-medium">Label Skala Rating</label>
<div class="d-flex flex-column gap-1">
@for ($i = 1; $i <= 5; $i++)
<div class="input-group input-group-sm">
<span class="input-group-text fw-bold" style="width:32px;justify-content:center;">{{ $i }}</span>
<input type="text" name="rating_labels[{{ $i }}]" id="ratingLabel{{ $i }}"
class="form-control"
placeholder="{{ $i === 1 ? 'cth: Sangat Tidak Setuju' : ($i === 5 ? 'cth: Sangat Setuju' : '') }}">
</div>
@endfor
</div>
<div class="form-text">Kosongkan jika tiada label untuk nilai tersebut.</div>
</div>
{{-- Options (choice types) --}}
<div id="optionsSection" class="mb-3 d-none">
<label class="form-label small fw-medium">Pilihan Jawapan</label>
<div id="optionsList">
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 1">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 2">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary w-100" onclick="addOption()">
<i class="bi bi-plus me-1"></i> Tambah Pilihan
</button>
@error('options')
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-check-lg me-1"></i> <span id="submitLabel">Tambah</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="cancelEdit"
style="display:none;" onclick="resetForm()">
Batal
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.3/Sortable.min.js"></script>
<script>
const setId = {{ $set->id }};
const storeUrl = "{{ route('admin.questions.store', $set) }}";
const updateBase = "{{ url('admin/questions') }}/";
const reorderUrl = "{{ route('admin.questions.reorder') }}";
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// Tajuk questions available as parents for rating questions
const tajukList = @json($topLevel->where('question_type', 'tajuk')->map(fn($q) => ['id' => $q->id, 'text' => $q->question_text])->values());
// ── UI helpers ──────────────────────────────────────────────────────────────
function populateParentDropdown(selectedId) {
const sel = document.getElementById('parentId');
sel.innerHTML = '<option value="">— Pilih Tajuk —</option>';
tajukList.forEach(function(t) {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.text;
if (selectedId && t.id == selectedId) opt.selected = true;
sel.appendChild(opt);
});
}
function onTypeChange() {
const type = document.getElementById('questionType').value;
const isTajuk = type === 'tajuk';
const isRating = type === 'rating';
const isChoice = ['single_choice', 'multiple_choice'].includes(type);
document.getElementById('requiredSection').classList.toggle('d-none', isTajuk);
document.getElementById('ratingLabelsSection').classList.toggle('d-none', !isTajuk);
document.getElementById('parentSection').classList.toggle('d-none', !isRating);
document.getElementById('optionsSection').classList.toggle('d-none', !isChoice);
if (isRating) populateParentDropdown(null);
}
function addOption() {
const list = document.getElementById('optionsList');
const count = list.querySelectorAll('input').length + 1;
const div = document.createElement('div');
div.className = 'input-group input-group-sm mb-2';
div.innerHTML = `<input type="text" name="options[]" class="form-control" placeholder="Pilihan ${count}">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>`;
list.appendChild(div);
}
function removeOption(btn) {
const list = document.getElementById('optionsList');
if (list.querySelectorAll('.input-group').length > 1) {
btn.closest('.input-group').remove();
}
}
function editQuestion(id, text, type, required, options, parentId, ratingLabels) {
document.getElementById('formTitle').innerHTML = '<i class="bi bi-pencil me-2 text-warning"></i>Edit Soalan';
document.getElementById('submitLabel').textContent = 'Kemaskini';
document.getElementById('cancelEdit').style.display = '';
document.getElementById('questionId').value = id;
document.getElementById('questionText').value = text;
document.getElementById('questionType').value = type;
document.getElementById('isRequired').checked = required;
const form = document.getElementById('questionForm');
form.action = updateBase + id;
document.getElementById('formMethod').value = 'PUT';
onTypeChange(); // show/hide sections
// Set parent if rating
if (type === 'rating' && parentId) {
populateParentDropdown(parentId);
}
// Set rating labels if tajuk
if (type === 'tajuk') {
for (let i = 1; i <= 5; i++) {
const el = document.getElementById('ratingLabel' + i);
if (el) el.value = (ratingLabels && (ratingLabels[i] || ratingLabels[String(i)])) || '';
}
}
// Options list
const list = document.getElementById('optionsList');
list.innerHTML = '';
if (options && options.length) {
options.forEach(function(opt, i) {
const div = document.createElement('div');
div.className = 'input-group input-group-sm mb-2';
div.innerHTML = `<input type="text" name="options[]" class="form-control" value="${opt}" placeholder="Pilihan ${i+1}">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>`;
list.appendChild(div);
});
} else {
// Restore default empty options for choice types
list.innerHTML = `<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 1">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 2">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>`;
}
form.scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formTitle').innerHTML = '<i class="bi bi-plus-circle me-2 text-primary"></i>Tambah Soalan';
document.getElementById('submitLabel').textContent = 'Tambah';
document.getElementById('cancelEdit').style.display = 'none';
document.getElementById('questionForm').reset();
document.getElementById('questionForm').action = storeUrl;
document.getElementById('formMethod').value = 'POST';
document.getElementById('questionId').value = '';
// Clear rating labels
for (let i = 1; i <= 5; i++) {
const el = document.getElementById('ratingLabel' + i);
if (el) el.value = '';
}
document.getElementById('parentId').value = '';
onTypeChange();
document.getElementById('optionsList').innerHTML = `
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 1">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 2">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>`;
}
// ── Drag & Drop ──────────────────────────────────────────────────────────────
function sendReorder(order, parentId) {
fetch(reorderUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ order: order, parent_id: parentId }),
});
}
document.addEventListener('DOMContentLoaded', function () {
// Init: show sections for default type (tajuk)
onTypeChange();
// Top-level sortable — sorts tajuk blocks + standalone questions
const topList = document.getElementById('questionList');
if (topList) {
Sortable.create(topList, {
animation: 150,
handle: '.drag-handle-top',
ghostClass: 'sortable-ghost',
onEnd: function() {
const order = [...topList.querySelectorAll(':scope > [data-id]')].map(el => +el.dataset.id);
sendReorder(order, null);
},
});
}
// Per-tajuk sortable — sorts rating children within a group
document.querySelectorAll('.children-list').forEach(function(list) {
Sortable.create(list, {
animation: 150,
handle: '.drag-handle-child',
ghostClass: 'sortable-ghost',
onEnd: function() {
const parentId = +list.dataset.parentId;
const order = [...list.querySelectorAll(':scope > [data-id]')].map(el => +el.dataset.id);
sendReorder(order, parentId);
},
});
});
});
</script>
@endpush