first commit
This commit is contained in:
342
resources/views/admin/surveys/create.blade.php
Normal file
342
resources/views/admin/surveys/create.blade.php
Normal file
@@ -0,0 +1,342 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<strong>Ralat! Sila semak input anda:</strong>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container">
|
||||
<div class="admin-header-box mb-4">
|
||||
<div>
|
||||
<h4>Daftar Soal Selidik</h4>
|
||||
<p>Sila isi maklumat borang di bawah.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Ralat Sistem:</strong> {{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.surveys.store') }}" id="survey-builder-form">
|
||||
@csrf
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="title" class="form-label fw-bold">Tajuk Soal Selidik</label>
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="eg: Sambutan Maulidur Rasul" value="{{ old('title') }}" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="date" class="form-label fw-bold">Tarikh</label>
|
||||
<input type="date" name="date" id="date" class="form-control" value="{{ old('date') }}" required>
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="perincian" class="form-label fw-bold">Perincian</label>
|
||||
<textarea name="perincian" id="perincian" class="form-control" rows="3" placeholder="Terangkan tujuan borang ini (Pilihan)">{{ old('perincian') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sections-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary" id="add-section-btn">+ Tambah Bahagian</button>
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-secondary btn-lg">Batal</a>
|
||||
<button type="submit" class="btn btn-success btn-lg">Daftar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template id="section-template">
|
||||
<div class="card shadow-sm mb-4 section-block">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 section-title">Bahagian</h5>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-section-btn">Hapus Bahagian</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][title]" class="form-control section-title-input" placeholder="Tajuk Bahagian (cth: Maklumat Peribadi)" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][description]" class="form-control" placeholder="Keterangan (Pilihan)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="questions-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-info add-question-btn">+ Tambah soalan</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="question-template">
|
||||
<div class="border rounded p-3 mb-3 question-block bg-white">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold question-title">Soalan 1</label>
|
||||
<div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="radio">Radio</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="text">Text</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="checkbox">Checkbox</button>
|
||||
</div>
|
||||
<input type="hidden" name="sections[0][questions][0][type]" class="question-type-input" value="radio">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2 remove-question-btn">Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="sections[0][questions][0][text]" class="form-control mb-3 question-text-input" placeholder="Isi soalan di sini" required>
|
||||
|
||||
<div class="form-check allow-other-container mb-3" data-allowed-types="radio,checkbox">
|
||||
<input class="form-check-input allow-other-option-checkbox" type="checkbox"
|
||||
value="1"
|
||||
name="sections[0][questions][0][allow_other_option]">
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
Lain-lain
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<label class="form-label">Jawapan Pilihan</label>
|
||||
<button type="button" class="btn btn-sm btn-link add-option-btn p-0">+ Tambah pilihan</button>
|
||||
</div>
|
||||
|
||||
<div class="text-placeholder-container" style="display:none;">
|
||||
<input type="text" class="form-control bg-light" placeholder="Ruang responden menjawab" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="option-template">
|
||||
<div class="input-group mb-2 option-group">
|
||||
<input type="text" name="sections[0][questions][0][options][]" class="form-control option-input" placeholder="Pilihan jawapan" required>
|
||||
<button type="button" class="btn btn-outline-danger remove-option-btn">Hapus</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sectionsContainer = document.getElementById('sections-container');
|
||||
const addSectionBtn = document.getElementById('add-section-btn');
|
||||
|
||||
const sectionTemplate = document.getElementById('section-template');
|
||||
const questionTemplate = document.getElementById('question-template');
|
||||
const optionTemplate = document.getElementById('option-template');
|
||||
|
||||
let sectionCounter = 0;
|
||||
|
||||
function addSection() {
|
||||
const sectionIndex = sectionCounter++;
|
||||
const newSection = sectionTemplate.content.cloneNode(true).firstElementChild;
|
||||
|
||||
newSection.dataset.sectionIndex = sectionIndex;
|
||||
newSection.querySelector('.section-title').textContent = `Bahagian`;
|
||||
|
||||
|
||||
newSection.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name.replace('sections[0]', `sections[${sectionIndex}]`);
|
||||
});
|
||||
|
||||
addQuestion(newSection.querySelector('.questions-container'), sectionIndex);
|
||||
|
||||
sectionsContainer.appendChild(newSection);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addQuestion(questionsContainer, sectionIndex) {
|
||||
const questionIndex = questionsContainer.querySelectorAll('.question-block').length;
|
||||
const newQuestion = questionTemplate.content.cloneNode(true).firstElementChild;
|
||||
|
||||
newQuestion.dataset.questionIndex = questionIndex;
|
||||
newQuestion.querySelector('.question-title').textContent = `Soalan ${questionIndex + 1}`;
|
||||
|
||||
newQuestion.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace('sections[0]', `sections[${sectionIndex}]`)
|
||||
.replace('[questions][0]', `[questions][${questionIndex}]`);
|
||||
});
|
||||
|
||||
newQuestion.querySelector('.type-selector[data-type="radio"]').classList.add('active', 'btn-primary');
|
||||
newQuestion.querySelector('.question-type-input').value = 'radio';
|
||||
|
||||
const otherCheckbox = newQuestion.querySelector('.allow-other-option-checkbox');
|
||||
otherCheckbox.name = `sections[${sectionIndex}][questions][${questionIndex}][allow_other_option]`;
|
||||
otherCheckbox.checked = false; // Pastikan bermula sebagai tidak dicentang
|
||||
otherCheckbox.removeAttribute('checked')
|
||||
|
||||
const optionsContainer = newQuestion.querySelector('.options-container');
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
|
||||
questionsContainer.appendChild(newQuestion);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addOption(optionsContainer, sectionIndex, questionIndex) {
|
||||
const newOption = optionTemplate.content.cloneNode(true).firstElementChild;
|
||||
newOption.querySelector('.option-input').name = `sections[${sectionIndex}][questions][${questionIndex}][options][]`;
|
||||
|
||||
const addBtn = optionsContainer.querySelector('.add-option-btn');
|
||||
optionsContainer.insertBefore(newOption, addBtn);
|
||||
}
|
||||
|
||||
function reindexQuestions() {
|
||||
sectionsContainer.querySelectorAll('.section-block').forEach((section, sIdx) => {
|
||||
section.dataset.sectionIndex = sIdx;
|
||||
section.querySelectorAll('.section-title-input').forEach(input => {
|
||||
input.name = input.name.replace(/sections\[\d+\]/, `sections[${sIdx}]`);
|
||||
});
|
||||
|
||||
section.querySelectorAll('.question-block').forEach((q, qIdx) => {
|
||||
q.dataset.questionIndex = qIdx;
|
||||
q.querySelector('.question-title').textContent = `Soalan ${qIdx + 1}`;
|
||||
q.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace(/sections\[\d+\]/, `sections[${sIdx}]`)
|
||||
.replace(/questions\[\d+\]/, `questions[${qIdx}]`);
|
||||
});
|
||||
|
||||
const type = q.querySelector('.question-type-input').value;
|
||||
const optionsContainer = q.querySelector('.options-container');
|
||||
const textPlaceholder = q.querySelector('.text-placeholder-container');
|
||||
const otherContainer = q.querySelector('.allow-other-container');
|
||||
|
||||
if (type === 'text') {
|
||||
optionsContainer.style.display = 'none';
|
||||
textPlaceholder.style.display = 'block';
|
||||
otherContainer.style.display = 'none'; // Sembunyikan untuk jenis 'text'
|
||||
q.querySelector('.allow-other-option-checkbox').checked = false;
|
||||
q.querySelector('.allow-other-option-checkbox').removeAttribute('checked');
|
||||
|
||||
// Buang input options supaya tidak dihantar jika jenis 'text'
|
||||
// Ini penting, jika tidak, validation akan gagal
|
||||
q.querySelectorAll('.option-group').forEach(group => group.remove());
|
||||
|
||||
} else {
|
||||
optionsContainer.style.display = 'block';
|
||||
textPlaceholder.style.display = 'none';
|
||||
otherContainer.style.display = 'block'; // Tunjukkan untuk jenis 'radio'/'checkbox'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addSectionBtn.addEventListener('click', addSection);
|
||||
|
||||
sectionsContainer.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('add-question-btn')) {
|
||||
const questionsContainer = e.target.closest('.card-body').querySelector('.questions-container');
|
||||
const sectionIndex = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
addQuestion(questionsContainer, sectionIndex);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('add-option-btn')) {
|
||||
const optionsContainer = e.target.closest('.options-container');
|
||||
const questionBlock = e.target.closest('.question-block');
|
||||
const sectionIndex = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
const questionIndex = parseInt(questionBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('type-selector')) {
|
||||
const type = e.target.dataset.type;
|
||||
const questionBlock = e.target.closest('.question-block');
|
||||
|
||||
questionBlock.querySelectorAll('.type-selector').forEach(btn => btn.classList.remove('active', 'btn-primary'));
|
||||
e.target.classList.add('active', 'btn-primary');
|
||||
|
||||
questionBlock.querySelector('.question-type-input').value = type;
|
||||
|
||||
const optionsContainer = questionBlock.querySelector('.options-container');
|
||||
const textPlaceholder = questionBlock.querySelector('.text-placeholder-container');
|
||||
// BARU: Dapatkan container untuk checkbox 'Lain-lain'
|
||||
const allowOtherContainer = questionBlock.querySelector('.allow-other-container');
|
||||
|
||||
if (type === 'text') {
|
||||
optionsContainer.style.display = 'none';
|
||||
textPlaceholder.style.display = 'block';
|
||||
// BARU: Sembunyikan checkbox 'Lain-lain' untuk jenis 'text'
|
||||
allowOtherContainer.style.display = 'none';
|
||||
questionBlock.querySelector('.allow-other-option-checkbox').checked = false;
|
||||
|
||||
// Remove options
|
||||
optionsContainer.querySelectorAll('.option-group').forEach(group => group.remove());
|
||||
|
||||
} else {
|
||||
optionsContainer.style.display = 'block';
|
||||
textPlaceholder.style.display = 'none';
|
||||
// BARU: Tunjukkan checkbox 'Lain-lain' untuk jenis 'radio'/'checkbox'
|
||||
allowOtherContainer.style.display = 'block';
|
||||
|
||||
// Tambah 2 pilihan jika tiada pilihan (bila tukar dari 'text')
|
||||
if (optionsContainer.querySelectorAll('.option-group').length === 0) {
|
||||
const sectionIndex = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
const questionIndex = parseInt(questionBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
}
|
||||
}
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
if (e.target.closest('.remove-section-btn')) {
|
||||
e.target.closest('.section-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-question-btn')) {
|
||||
e.target.closest('.question-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-option-btn')) {
|
||||
e.target.closest('.option-group').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('survey-builder-form').addEventListener('submit', function (e) {
|
||||
sectionsContainer.querySelectorAll('.question-block').forEach(q => {
|
||||
const isText = q.querySelector('.question-type-input').value === 'text';
|
||||
|
||||
// 1. Logik untuk soalan 'text' (buang options)
|
||||
if (isText) {
|
||||
q.querySelectorAll('.option-group').forEach(group => group.remove());
|
||||
}
|
||||
|
||||
// 2. Logik untuk checkbox 'allow_other_option'
|
||||
const otherCheckbox = q.querySelector('.allow-other-option-checkbox');
|
||||
if (otherCheckbox) {
|
||||
if (otherCheckbox.checked) {
|
||||
otherCheckbox.value = '1';
|
||||
otherCheckbox.setAttribute('checked', 'checked'); // Pastikan dihantar
|
||||
} else {
|
||||
otherCheckbox.removeAttribute('name');
|
||||
otherCheckbox.removeAttribute('checked');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
addSection();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
380
resources/views/admin/surveys/edit.blade.php
Normal file
380
resources/views/admin/surveys/edit.blade.php
Normal file
@@ -0,0 +1,380 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="admin-header-box mb-4">
|
||||
<div>
|
||||
<h4>Edit Soal Selidik</h4>
|
||||
<p>Kemaskini maklumat borang di bawah.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<strong>Ralat! Sila semak input anda:</strong>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Ralat Sistem:</strong> {{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.surveys.update', $survey->id) }}" id="survey-builder-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="title" class="form-label fw-bold">Tajuk Soal Selidik</label>
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="eg: Sambutan Maulidur Rasul" value="{{ old('title', $survey->title) }}" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="date" class="form-label fw-bold">Tarikh</label>
|
||||
<input type="date" name="date" id="date" class="form-control" value="{{ old('date', $survey->date) }}" required>
|
||||
</div>
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="perincian" class="form-label fw-bold">Perincian</label>
|
||||
<textarea name="perincian" id="perincian" class="form-control" rows="3" placeholder="Terangkan tujuan borang ini (Pilihan)">{{ old('perincian', $survey->perincian) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sections-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-outline-primary" id="add-section-btn">+ Tambah Seksyen</button>
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-secondary">Batal</a>
|
||||
<button type="submit" class="btn btn-success">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- TEMPLATES --}}
|
||||
<template id="section-template">
|
||||
<div class="card shadow-sm mb-4 section-block">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 section-title">Bahagian</h5>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-section-btn">Buang Seksyen</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][title]" class="form-control section-title-input" placeholder="Tajuk Bahagian (cth: Maklumat Peribadi)" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="text" name="sections[0][description]" class="form-control" placeholder="Optional description">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="questions-container"></div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-info add-question-btn">+ Tambah soalan</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="question-template">
|
||||
<div class="border rounded p-3 mb-3 question-block bg-white">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold question-title">Soalan 1</label>
|
||||
<div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="radio">Radio</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="text">Text</button>
|
||||
<button type="button" class="btn btn-outline-secondary type-selector" data-type="checkbox">Checkbox</button>
|
||||
</div>
|
||||
<input type="hidden" name="sections[0][questions][0][type]" class="question-type-input" value="radio">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger ms-2 remove-question-btn">Buang</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" name="sections[0][questions][0][text]" class="form-control mb-3 question-text-input" placeholder="Isi soalan di sini" required>
|
||||
|
||||
<div class="form-check allow-other-container mb-3">
|
||||
<input class="form-check-input allow-other-option-checkbox" type="checkbox" value="1" name="sections[0][questions][0][allow_other_option]">
|
||||
<label class="form-check-label">
|
||||
Lain-lain
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
<label class="form-label">Jawapan Pilihan</label>
|
||||
<button type="button" class="btn btn-sm btn-link add-option-btn p-0">+ Tambah pilihan</button>
|
||||
</div>
|
||||
|
||||
<div class="text-placeholder-container" style="display:none;">
|
||||
<input type="text" class="form-control bg-light" placeholder="Ruang responden menjawab" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="option-template">
|
||||
<div class="input-group mb-2 option-group">
|
||||
<input type="text" name="sections[0][questions][0][options][]" class="form-control option-input" placeholder="Pilihan jawapan" required>
|
||||
<button type="button" class="btn btn-outline-danger remove-option-btn">Buang</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sectionsContainer = document.getElementById('sections-container');
|
||||
const addSectionBtn = document.getElementById('add-section-btn');
|
||||
|
||||
const sectionTemplate = document.getElementById('section-template');
|
||||
const questionTemplate = document.getElementById('question-template');
|
||||
const optionTemplate = document.getElementById('option-template');
|
||||
|
||||
let sectionCounter = 0;
|
||||
|
||||
function addSection(data = null) {
|
||||
const sectionIndex = sectionCounter++;
|
||||
const newSection = sectionTemplate.content.cloneNode(true).firstElementChild;
|
||||
newSection.dataset.sectionIndex = sectionIndex;
|
||||
|
||||
const titleInput = newSection.querySelector('.section-title-input');
|
||||
const descInput = newSection.querySelector('input[name$="[description]"]');
|
||||
titleInput.value = data?.title ?? '';
|
||||
descInput.value = data?.description ?? '';
|
||||
newSection.querySelector('.section-title').textContent = data?.title ?? 'Bahagian';
|
||||
|
||||
// Adjust names
|
||||
newSection.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name.replace('sections[0]', `sections[${sectionIndex}]`);
|
||||
});
|
||||
|
||||
const questionsContainer = newSection.querySelector('.questions-container');
|
||||
|
||||
if (data?.questions && data.questions.length) {
|
||||
data.questions.forEach(q => addQuestion(questionsContainer, sectionIndex, q));
|
||||
} else {
|
||||
addQuestion(questionsContainer, sectionIndex);
|
||||
}
|
||||
|
||||
sectionsContainer.appendChild(newSection);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addQuestion(questionsContainer, sectionIndex, qdata = null) {
|
||||
const questionIndex = questionsContainer.querySelectorAll('.question-block').length;
|
||||
const newQuestion = questionTemplate.content.cloneNode(true).firstElementChild;
|
||||
newQuestion.dataset.questionIndex = questionIndex;
|
||||
|
||||
newQuestion.querySelector('.question-title').textContent = `Soalan ${questionIndex + 1}`;
|
||||
const qtext = newQuestion.querySelector('.question-text-input');
|
||||
const qtype = newQuestion.querySelector('.question-type-input');
|
||||
const otherCheckbox = newQuestion.querySelector('.allow-other-option-checkbox');
|
||||
|
||||
qtext.value = qdata?.text ?? '';
|
||||
qtype.value = qdata?.type ?? 'radio';
|
||||
|
||||
// BARU: Set status checkbox Lain-lain based on DB data
|
||||
// Check if qdata.allow_other_option is true or '1'
|
||||
if (qdata && (qdata.allow_other_option == 1 || qdata.allow_other_option === true)) {
|
||||
otherCheckbox.checked = true;
|
||||
} else {
|
||||
otherCheckbox.checked = false;
|
||||
}
|
||||
|
||||
newQuestion.querySelectorAll('.type-selector').forEach(btn => {
|
||||
btn.classList.remove('active','btn-primary');
|
||||
if (btn.dataset.type === qtype.value) btn.classList.add('active','btn-primary');
|
||||
});
|
||||
|
||||
newQuestion.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace('sections[0]', `sections[${sectionIndex}]`)
|
||||
.replace('[questions][0]', `[questions][${questionIndex}]`);
|
||||
});
|
||||
|
||||
const optionsContainer = newQuestion.querySelector('.options-container');
|
||||
|
||||
if (qdata?.options && qdata.options.length && qtype.value !== 'text') {
|
||||
qdata.options.forEach(opt => addOption(optionsContainer, sectionIndex, questionIndex, opt.text));
|
||||
} else if (qtype.value !== 'text') {
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
addOption(optionsContainer, sectionIndex, questionIndex);
|
||||
}
|
||||
|
||||
// Initialize display state (options vs text vs other checkbox)
|
||||
toggleQuestionType(newQuestion, qtype.value);
|
||||
|
||||
questionsContainer.appendChild(newQuestion);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
function addOption(optionsContainer, sectionIndex, questionIndex, value = '') {
|
||||
const newOption = optionTemplate.content.cloneNode(true).firstElementChild;
|
||||
const optInput = newOption.querySelector('.option-input');
|
||||
optInput.name = `sections[${sectionIndex}][questions][${questionIndex}][options][]`;
|
||||
optInput.value = value ?? '';
|
||||
const addBtn = optionsContainer.querySelector('.add-option-btn');
|
||||
optionsContainer.insertBefore(newOption, addBtn);
|
||||
}
|
||||
|
||||
function reindexQuestions() {
|
||||
sectionsContainer.querySelectorAll('.section-block').forEach((section, sIdx) => {
|
||||
section.dataset.sectionIndex = sIdx;
|
||||
section.querySelectorAll('.section-title-input').forEach(input => {
|
||||
input.name = input.name.replace(/sections\[\d+\]/, `sections[${sIdx}]`);
|
||||
});
|
||||
|
||||
section.querySelectorAll('.question-block').forEach((q, qIdx) => {
|
||||
q.dataset.questionIndex = qIdx;
|
||||
q.querySelector('.question-title').textContent = `Soalan ${qIdx + 1}`;
|
||||
|
||||
// Reindex all inputs including the new checkbox
|
||||
q.querySelectorAll('[name]').forEach(input => {
|
||||
input.name = input.name
|
||||
.replace(/sections\[\d+\]/, `sections[${sIdx}]`)
|
||||
.replace(/questions\[\d+\]/, `questions[${qIdx}]`);
|
||||
});
|
||||
|
||||
// Reindex dynamic options
|
||||
q.querySelectorAll('.option-input').forEach((opt, optIdx) => {
|
||||
opt.name = `sections[${sIdx}][questions][${qIdx}][options][${optIdx}]`;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleQuestionType(questionBlock, type) {
|
||||
const optionsContainer = questionBlock.querySelector('.options-container');
|
||||
const textPlaceholder = questionBlock.querySelector('.text-placeholder-container');
|
||||
const otherContainer = questionBlock.querySelector('.allow-other-container');
|
||||
const otherCheckbox = questionBlock.querySelector('.allow-other-option-checkbox');
|
||||
|
||||
if (type === 'text') {
|
||||
// Text Mode
|
||||
optionsContainer.style.display = 'none';
|
||||
textPlaceholder.style.display = 'block';
|
||||
|
||||
// Hide 'Other' checkbox and uncheck it
|
||||
otherContainer.style.display = 'none';
|
||||
otherCheckbox.checked = false;
|
||||
|
||||
// Remove options inputs
|
||||
optionsContainer.querySelectorAll('.option-input').forEach(i=>i.remove());
|
||||
} else {
|
||||
// Radio/Checkbox Mode
|
||||
optionsContainer.style.display = 'block';
|
||||
textPlaceholder.style.display = 'none';
|
||||
|
||||
// Show 'Other' checkbox
|
||||
otherContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
addSectionBtn.addEventListener('click', () => addSection());
|
||||
|
||||
sectionsContainer.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('add-question-btn')) {
|
||||
const questionsContainer = e.target.closest('.card-body').querySelector('.questions-container');
|
||||
const sIdx = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
addQuestion(questionsContainer, sIdx);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('add-option-btn')) {
|
||||
const optionsContainer = e.target.closest('.options-container');
|
||||
const qBlock = e.target.closest('.question-block');
|
||||
const sIdx = parseInt(e.target.closest('.section-block').dataset.sectionIndex);
|
||||
const qIdx = parseInt(qBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sIdx, qIdx);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('type-selector')) {
|
||||
const type = e.target.dataset.type;
|
||||
const qBlock = e.target.closest('.question-block');
|
||||
|
||||
qBlock.querySelectorAll('.type-selector').forEach(btn => btn.classList.remove('active','btn-primary'));
|
||||
e.target.classList.add('active','btn-primary');
|
||||
|
||||
qBlock.querySelector('.question-type-input').value = type;
|
||||
|
||||
// Check if we need to add default options when switching from Text to Radio/Checkbox
|
||||
const optionsContainer = qBlock.querySelector('.options-container');
|
||||
if (type !== 'text' && optionsContainer.querySelectorAll('.option-group').length === 0) {
|
||||
const sIdx = parseInt(qBlock.closest('.section-block').dataset.sectionIndex);
|
||||
const qIdx = parseInt(qBlock.dataset.questionIndex);
|
||||
addOption(optionsContainer, sIdx, qIdx);
|
||||
addOption(optionsContainer, sIdx, qIdx);
|
||||
}
|
||||
|
||||
toggleQuestionType(qBlock, type);
|
||||
reindexQuestions();
|
||||
}
|
||||
|
||||
if (e.target.closest('.remove-section-btn')) {
|
||||
e.target.closest('.section-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-question-btn')) {
|
||||
e.target.closest('.question-block').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
if (e.target.closest('.remove-option-btn')) {
|
||||
e.target.closest('.option-group').remove();
|
||||
reindexQuestions();
|
||||
}
|
||||
});
|
||||
|
||||
// Form Submission Logic
|
||||
document.getElementById('survey-builder-form').addEventListener('submit', function () {
|
||||
sectionsContainer.querySelectorAll('.question-block').forEach(q=>{
|
||||
const type = q.querySelector('.question-type-input').value;
|
||||
|
||||
// Handle Options Clean up
|
||||
if (type === 'text') {
|
||||
q.querySelectorAll('.option-input').forEach(i=>i.remove());
|
||||
} else {
|
||||
q.querySelectorAll('.option-input').forEach(i => {
|
||||
if (typeof i.value === 'undefined') i.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Other Option Checkbox
|
||||
const otherCheckbox = q.querySelector('.allow-other-option-checkbox');
|
||||
if (otherCheckbox) {
|
||||
if (otherCheckbox.checked) {
|
||||
otherCheckbox.value = '1';
|
||||
} else {
|
||||
// Remove name attribute so it doesn't send "on" or any value if unchecked
|
||||
// This relies on the backend treating missing boolean field as false/0
|
||||
otherCheckbox.removeAttribute('name');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initial Data Loading
|
||||
const oldSections = @json(old('sections') ?? null);
|
||||
const serverSections = @json($sections_with_questions ?? []);
|
||||
|
||||
const initialSections = oldSections ?? serverSections;
|
||||
|
||||
if (Array.isArray(initialSections) && initialSections.length) {
|
||||
initialSections.forEach(sec => addSection(sec));
|
||||
} else {
|
||||
addSection();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
235
resources/views/admin/surveys/index.blade.php
Normal file
235
resources/views/admin/surveys/index.blade.php
Normal file
@@ -0,0 +1,235 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/surveysAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
{{-- Header --}}
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Senarai Borang soal selidik</h4>
|
||||
<p>Beserta statistik dan analisis Post-Mortem.</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.surveys.create') }}" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-plus-lg"></i> Daftar soal selidik
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Carian borang --}}
|
||||
<form method="GET" action="{{ route('admin.surveys.index') }}" class="mb-4">
|
||||
<div class="input-group shadow-sm">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="form-control border-end-0" placeholder="Cari tajuk atau kata kunci...">
|
||||
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i> </button>
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-outline-secondary"><i class="bi bi-arrow-counterclockwise"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card survey-table-card shadow-sm border-0">
|
||||
<div class="card-header bg-secondary text-white py-3">
|
||||
<h5 class="mb-0 text-center fw-bold">Senarai Soal Selidik</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive-lg">
|
||||
<table class="table table-hover table-striped table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-center">#</th>
|
||||
<th style="width: 40%;">Tajuk</th>
|
||||
<th style="width: 20%;">Dibuat oleh</th>
|
||||
<th class="text-center">Respons</th>
|
||||
<th>Tarikh</th>
|
||||
<th class="text-center">Stats & Analisis</th>
|
||||
<th class="text-center">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($surveys as $index => $survey)
|
||||
<tr>
|
||||
<td class="text-center">{{ $index + 1 }}</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-uppercase text-primary position-relative survey-title-container">
|
||||
<a href="{{ route('surveys.show', $survey) }}"
|
||||
target="_blank"
|
||||
class="text-decoration-none fw-bold text-primary">
|
||||
{{ $survey->title }}
|
||||
</a>
|
||||
<button class="btn btn-link btn-sm p-0 ms-2 text-secondary"
|
||||
onclick="copySurveyLink('{{ $survey->uuid }}')"
|
||||
title="Copy Link">
|
||||
<i class="bi bi-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $survey->user->name }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-info text-white px-3">
|
||||
{{ $survey->responses->count() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $survey->date }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ route('admin.surveys.statistics', $survey->id) }}" class="btn btn-sm btn-info text-white fw-semibold">
|
||||
<i class="bi bi-bar-chart"></i> Graf
|
||||
</a>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-warning fw-semibold" data-bs-toggle="modal" data-bs-target="#modalUlasan{{ $survey->id }}">
|
||||
<i class="bi bi-clipboard-check"></i> Ulasan
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu shadow border-0">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('admin.surveys.edit', $survey->id) }}">
|
||||
<i class="bi bi-pencil me-2 text-primary"></i> Edit Borang
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{{ route('admin.surveys.destroy', $survey->id) }}" method="POST" onsubmit="return confirm('Padam borang ini?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
<i class="bi bi-trash me-2"></i> Hapus
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div class="modal fade" id="modalUlasan{{ $survey->id }}" tabindex="-1" aria-labelledby="modalLabel{{ $survey->id }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title fw-bold" id="modalLabel{{ $survey->id }}">
|
||||
<i class="bi bi-pencil-square me-2"></i>Ulasan Post-Mortem
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.surveys.update_ulasan', $survey->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="modal-body py-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Tajuk Borang:</label>
|
||||
<p class="text-muted border-bottom pb-2">{{ $survey->title }}</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ulasan" class="form-label fw-bold text-dark">Nota Penambahbaikan:</label>
|
||||
<textarea name="ulasan" class="form-control border-warning shadow-sm" rows="6" placeholder="Tulis ulasan penambahbaikan di sini">{{ $survey->ulasan }}</textarea>
|
||||
<div class="form-text mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i> Perkara yang perlu ditambah baik pada masa akan datang.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light text-end">
|
||||
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary px-4 shadow-sm">
|
||||
<i class="bi bi-save me-1"></i> Simpan Ulasan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="text-center p-5 text-muted">
|
||||
<i class="bi bi-clipboard-x display-4 d-block mb-3 opacity-25"></i>
|
||||
Tiada borang soal selidik ditemui.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
{{ $surveys->withQueryString()->links() }}
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification for Copy Success -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
|
||||
<div id="copyToast" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>Link berjaya disalin!
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function copySurveyLink(surveyUuid) {
|
||||
const baseUrl = "{{ url('/') }}";
|
||||
const surveyUrl = `${baseUrl}/survey/${surveyUuid}`;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(surveyUrl).then(function() {
|
||||
showCopyToast();
|
||||
}).catch(function(err) {
|
||||
console.error('Clipboard API failed: ', err);
|
||||
fallbackCopy(surveyUrl);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(surveyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopyToast();
|
||||
} else {
|
||||
alert('Gagal menyalin link. Sila cuba lagi.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed: ', err);
|
||||
alert('Gagal menyalin link. Sila cuba lagi.');
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
function showCopyToast() {
|
||||
const toastEl = document.getElementById('copyToast');
|
||||
const toast = new bootstrap.Toast(toastEl);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
198
resources/views/admin/surveys/print_statistics.blade.php
Normal file
198
resources/views/admin/surveys/print_statistics.blade.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Statistik - {{ $survey->title }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: white !important;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.break-inside-avoid {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
.chart-container {
|
||||
height: 220px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-body {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
.card-header {
|
||||
padding: 0.5rem 1rem !important;
|
||||
}
|
||||
h1.h3 { font-size: 1.5rem !important; }
|
||||
h5 { font-size: 1rem !important; }
|
||||
.table-sm { font-size: 0.8rem !important; }
|
||||
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
|
||||
.badge { border: 1px solid #ccc !important; color: black !important; background: white !important; }
|
||||
body { padding: 0; }
|
||||
.container-fluid { padding: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="text-center mb-4 border-bottom pb-2">
|
||||
<h1 class="fw-bold h3 mb-1">{{ $survey->title }}</h1>
|
||||
<p class="text-muted mb-0 small">Laporan Statistik - {{ date('d/m/Y H:i') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<div class="card text-center py-1 bg-light border-0">
|
||||
<div class="card-body py-2">
|
||||
<h4 class="fw-bold mb-0">{{ $totalSurveyRespondents }}</h4>
|
||||
<small class="text-muted">Responden</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card text-center py-1 bg-light border-0">
|
||||
<div class="card-body py-2">
|
||||
<h4 class="fw-bold mb-0">{{ $totalSurveyQuestions }}</h4>
|
||||
<small class="text-muted">Soalan</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach($results as $questionId => $result)
|
||||
<div class="card mb-3 break-inside-avoid border shadow-none">
|
||||
<div class="card-header bg-light border-bottom p-2">
|
||||
<h6 class="fw-bold mb-0">Soalan {{ $loop->iteration }}: {{ $result['question'] }}</h6>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
@if(isset($result['type']) && $result['type'] == 'text')
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
<th>Jawapan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($result['data'] as $answer)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $answer }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">Tiada jawapan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-7">
|
||||
<div class="chart-container">
|
||||
<canvas id="chart-{{ $questionId }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<table class="table table-bordered table-sm mb-0" style="font-size: 0.75rem;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Pilihan Jawapan</th>
|
||||
<th class="text-center">Kekerapan</th>
|
||||
<th class="text-center">Peratus(%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($result['data'] as $stat)
|
||||
<tr>
|
||||
<td>{{ $stat['label'] }}</td>
|
||||
<td class="text-center">{{ $stat['count'] }}</td>
|
||||
<td class="text-center fw-bold">{{ $stat['percentage'] }}%</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td>JUMLAH</td>
|
||||
<td class="text-center">{{ $result['total'] }}</td>
|
||||
<td class="text-center">100%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($result['other_answers']))
|
||||
<div class="mt-2 border-top pt-1">
|
||||
<small class="fw-bold text-secondary" style="font-size: 0.7rem;">Lain-lain:</small>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||
@foreach($result['other_answers'] as $otherAns)
|
||||
<span class="badge bg-white text-dark border p-1 fw-normal" style="font-size: 0.7rem;">{{ $otherAns }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="text-center mt-3 no-print">
|
||||
<button onclick="window.print()" class="btn btn-primary px-4 py-1 fw-bold">KLIK UNTUK CETAK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const results = @json($results);
|
||||
|
||||
Object.keys(results).forEach(id => {
|
||||
const data = results[id];
|
||||
if(data.type === 'text') return;
|
||||
|
||||
const ctx = document.getElementById(`chart-${id}`).getContext('2d');
|
||||
const labels = data.data.map(item => item.label);
|
||||
const counts = data.data.map(item => item.count);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Bilangan',
|
||||
data: counts,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 0 },
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { precision: 0, font: { size: 10 } } },
|
||||
x: { ticks: { font: { size: 10 } } }
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.print();
|
||||
}, 800);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
189
resources/views/admin/surveys/statistics.blade.php
Normal file
189
resources/views/admin/surveys/statistics.blade.php
Normal file
@@ -0,0 +1,189 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/statisticsAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Statistik: {{ $survey->title }}</h4>
|
||||
<p>Analisis data respons bagi borang ini.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('admin.surveys.statistics_print', $survey->id) }}" target="_blank" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-printer me-1"></i> Cetak
|
||||
</a>
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-sm btn-light px-3 fw-bold text-primary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border text-center py-2">
|
||||
<div class="card-body p-2">
|
||||
<h3 class="stat-card-number">{{ $totalSurveyRespondents }}</h3>
|
||||
<small class="text-muted">Jumlah Responden</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border text-center py-2">
|
||||
<div class="card-body p-2">
|
||||
<h3 class="stat-card-number">{{ $totalSurveyQuestions }}</h3>
|
||||
<small class="text-muted">Jumlah Soalan</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach($results as $questionId => $result)
|
||||
<div class="card shadow-sm mb-5 break-inside-avoid">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="fw-bold mb-0">Soalan: {{ $result['question'] }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(isset($result['type']) && $result['type'] == 'text')
|
||||
{{-- DISPLAY TEXT ANSWERS --}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>Jawapan Responden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($result['data'] as $index => $answer)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $answer }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">Tiada jawapan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
{{-- DISPLAY CHART FOR RADIO/CHECKBOX --}}
|
||||
<div class="row">
|
||||
<div class="col-md-7 mb-4 mb-md-0">
|
||||
<div class="chart-container-box">
|
||||
<canvas id="chart-{{ $questionId }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<table class="table table-bordered table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th class="text-center">Pilihan Jawapan</th>
|
||||
<th class="text-center">Kekerapan</th>
|
||||
<th class="text-center">Peratus (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($result['data'] as $stat)
|
||||
<tr>
|
||||
<td>{{ $stat['label'] }}</td>
|
||||
<td class="text-center">{{ $stat['count'] }}</td>
|
||||
<td class="text-center fw-bold">{{ $stat['percentage'] }}%</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td>JUMLAH</td>
|
||||
<td class="text-center">{{ $result['total'] }}</td>
|
||||
<td class="text-center">100%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($result['other_answers']))
|
||||
<div class="mt-4">
|
||||
<h6 class="fw-bold text-secondary">Jawapan Lain-lain:</h6>
|
||||
<ul class="list-group list-group-flush border rounded">
|
||||
@foreach($result['other_answers'] as $otherAns)
|
||||
<li class="list-group-item other-answer-item">{{ $otherAns }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Load Chart.js CDN (Kalau belum ada dalam layout) --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const results = @json($results);
|
||||
|
||||
Object.keys(results).forEach(id => {
|
||||
const data = results[id];
|
||||
|
||||
if(data.type === 'text') return;
|
||||
|
||||
const ctx = document.getElementById(`chart-${id}`).getContext('2d');
|
||||
|
||||
// Extract labels dan values untuk chart
|
||||
const labels = data.data.map(item => item.label);
|
||||
const counts = data.data.map(item => item.count);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Bilangan Responden',
|
||||
data: counts,
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)',
|
||||
'rgba(255, 159, 64, 0.2)',
|
||||
'rgba(255, 205, 86, 0.2)',
|
||||
'rgba(75, 192, 192, 0.2)',
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
'rgba(153, 102, 255, 0.2)',
|
||||
'rgba(201, 203, 207, 0.2)'],
|
||||
borderColor: [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(255, 159, 64)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(75, 192, 192)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(153, 102, 255)',
|
||||
'rgb(201, 203, 207)'],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
// indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
106
resources/views/admin/surveys/ulasan.blade.php
Normal file
106
resources/views/admin/surveys/ulasan.blade.php
Normal file
@@ -0,0 +1,106 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@push('styles')
|
||||
<link rel="stylesheet" href="{{ asset('css/adminHeader.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/ulasanAdmin.css') }}">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
{{-- Header --}}
|
||||
<div class="admin-header-box">
|
||||
<div>
|
||||
<h4>Nota Post-Mortem</h4>
|
||||
<p>Klik kad untuk baca ulasan penuh.</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<a href="{{ route('admin.surveys.ulasan.export') }}" class="btn btn-sm btn-success rounded-pill px-3 fw-bold me-2">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i> Export
|
||||
</a>
|
||||
|
||||
{{-- Search Form --}}
|
||||
<form method="GET" action="{{ route('admin.surveys.ulasan') }}" class="d-flex gap-2 mb-0">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="form-control rounded-pill px-3" placeholder="Cari tajuk..." style="width: 200px;">
|
||||
<button class="btn btn-light rounded-circle ms-1 p-1 d-flex align-items-center justify-content-center" type="submit" style="width: 30px; height: 30px;">
|
||||
<i class="bi bi-search text-primary"></i>
|
||||
</button>
|
||||
@if(request('search'))
|
||||
<a href="{{ route('admin.surveys.ulasan') }}" class="btn btn-light rounded-circle ms-1 p-1 d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;">
|
||||
<i class="bi bi-x-lg text-danger"></i>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a href="{{ route('admin.surveys.index') }}" class="btn btn-sm btn-light rounded-pill px-3 fw-bold text-primary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
@forelse($surveys as $survey)
|
||||
<div class="col-6 col-md-4 col-lg-3 mb-2">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-4 position-relative overflow-hidden cursor-pointer ulasan-hover-effect"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#ulasanModal{{ $survey->id }}"
|
||||
style="cursor: pointer; background: #ffffff;">
|
||||
|
||||
<div class="position-absolute top-0 start-0 h-100 bg-primary-subtle" style="width: 3px;"></div>
|
||||
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<small class="text-primary fw-bold" style="font-size: 0.65rem;">#{{ $survey->id }}</small>
|
||||
<small class="text-muted" style="font-size: 0.65rem;">{{ $survey->updated_at->format('d/m/y') }}</small>
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold text-dark mb-2 text-truncate" style="text-transform: uppercase; font-size: 0.9rem;">
|
||||
{{ $survey->title }}
|
||||
</h6>
|
||||
|
||||
<div class="p-2 rounded-3" style="background-color: #f8f9fa;">
|
||||
<p class="text-secondary mb-0" style="font-size: 0.75rem; line-height: 1.4;">
|
||||
{{ Str::limit($survey->ulasan, 60) }}
|
||||
@if(strlen($survey->ulasan) > 60)
|
||||
<span class="text-primary fw-bold" style="font-size: 0.7rem;">...more</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- MODAL UNTUK AYAT PENUH --}}
|
||||
<div class="modal fade" id="ulasanModal{{ $survey->id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 rounded-4 shadow">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h6 class="modal-title fw-bold text-primary">Ulasan Penuh</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<p class="text-muted small mb-1">Tajuk Borang:</p>
|
||||
<h6 class="fw-bold mb-3">{{ $survey->title }}</h6>
|
||||
<hr class="opacity-10">
|
||||
<p class="text-dark" style="white-space: pre-line; line-height: 1.6; font-size: 0.9rem;">
|
||||
{{ $survey->ulasan }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<small class="text-muted me-auto" style="font-size: 0.7rem;">Oleh: {{ $survey->user->name ?? 'Admin' }}</small>
|
||||
<button type="button" class="btn btn-sm btn-secondary rounded-pill px-3" data-bs-dismiss="modal">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@empty
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted small">Tiada ulasan ditemui.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user