first commit

This commit is contained in:
2026-05-22 20:46:29 +08:00
commit b04f87f2b0
121 changed files with 14851 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Survey;
use App\Models\Response;
use App\Models\User;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function index()
{
// count total borang
$totalSurveys = Survey::count();
// Count total respons
$totalResponses = Response::count();
// Count total users
$totalUsers = User::count();
// 5 recent borang
$recentSurveys = Survey::with(['user', 'responses'])
->latest()
->take(5)
->get();
// Recent Reviews (Ulasan) - "sikit je" (e.g., 5)
$recentReviews = Survey::whereNotNull('ulasan')
->where('ulasan', '!=', '')
->with('user') // Assuming we might want to know who created the survey
->latest('updated_at')
->take(5)
->get();
// Recent Users - "sikit je" (e.g., 5)
$recentUsers = User::latest()
->take(5)
->get();
return view('admin.dashboard', compact(
'totalSurveys',
'totalResponses',
'totalUsers',
'recentSurveys',
'recentReviews',
'recentUsers'
));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Survey;
use App\Models\Response;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ResponseController extends Controller
{
public function list(Request $request)
{
$query = Survey::withCount('responses');
if ($request->filled('search')) {
$search = $request->search;
$query->where('title', 'like', "%{$search}%");
}
$surveys = $query->latest()->get();
return view('admin.responses.list', compact('surveys'));
}
public function showRespondents(Survey $survey)
{
$responses = $survey->responses()
->with('user')
->orderByDesc('created_at')
->get();
return view('admin.responses.respondents', compact('survey', 'responses'));
}
public function detail(Response $response)
{
$response->load([
'survey.sections.questions',
'answers.question'
]);
$answersByQuestionId = $response->answers->keyBy('question_id');
return view('admin.responses.detail', compact('response', 'answersByQuestionId'));
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Survey;
use App\Http\Controllers\Controller;
use App\Models\Question;
use App\Models\Response;
use Illuminate\Http\Request;
class StatsController extends Controller
{
public function showStats($id)
{
$survey = Survey::with(['sections.questions.options', 'sections.questions.answers'])->findOrFail($id);
$totalSurveyRespondents = \App\Models\Response::where('survey_id', $id)->count();
$totalSurveyQuestions = \App\Models\Question::whereHas('section', function ($q) use ($id) {
$q->where('survey_id', $id);
})->count();
$results = [];
foreach ($survey->sections as $section) {
foreach ($section->questions as $question) {
$totalRespondents = $question->answers->count();
$stats = [];
$allAnswers = $question->answers->pluck('answer_text')->map(fn($item) => trim($item))->toArray();
// HANDLE TEXT QUESTIONS
if ($question->type === 'text') {
$results[$question->id] = [
'question' => $question->question_text,
'type' => 'text',
'total' => $totalRespondents,
'data' => array_filter($allAnswers) // Filter empty answers if any
];
continue; // Skip the rest of the loop (chart logic)
}
// HANDLE RADIO/CHECKBOX (EXISTING LOGIC)
foreach ($question->options as $option) {
$optionLabel = trim($option->option_text);
$count = 0;
foreach ($allAnswers as $ans) {
if ($ans === $optionLabel) {
$count++;
}
}
$stats[] = [
'label' => $optionLabel,
'count' => $count,
'percentage' => $totalRespondents > 0 ? round(($count / $totalRespondents) * 100, 1) : 0
];
}
$optionTexts = $question->options->pluck('option_text')->map(fn($item) => trim($item))->toArray();
$othersCount = 0;
$otherAnswersList = [];
foreach ($allAnswers as $ans) {
if (!in_array($ans, $optionTexts)) {
$othersCount++;
// Clean up "Lain-lain: " prefix if exists (based on public view logic)
$cleanAns = str_replace("Lain-lain: ", "", $ans);
$otherAnswersList[] = $cleanAns;
}
}
if ($question->allow_other_option && $othersCount > 0) {
$stats[] = [
'label' => 'Lain-lain',
'count' => $othersCount,
'percentage' => $totalRespondents > 0 ? round(($othersCount / $totalRespondents) * 100, 1) : 0
];
}
$results[$question->id] = [
'question' => $question->question_text,
'type' => 'chart', // Mark as chart
'total' => $totalRespondents,
'data' => $stats,
'other_answers' => $otherAnswersList
];
}
}
return view('admin.surveys.statistics', compact('survey', 'results', 'totalSurveyRespondents', 'totalSurveyQuestions'));
}
public function printStats($id)
{
$survey = Survey::with(['sections.questions.options', 'sections.questions.answers'])->findOrFail($id);
$totalSurveyRespondents = \App\Models\Response::where('survey_id', $id)->count();
$totalSurveyQuestions = \App\Models\Question::whereHas('section', function ($q) use ($id) {
$q->where('survey_id', $id);
})->count();
$results = [];
foreach ($survey->sections as $section) {
foreach ($section->questions as $question) {
$totalRespondents = $question->answers->count();
$stats = [];
$allAnswers = $question->answers->pluck('answer_text')->map(fn($item) => trim($item))->toArray();
// HANDLE TEXT QUESTIONS
if ($question->type === 'text') {
$results[$question->id] = [
'question' => $question->question_text,
'type' => 'text',
'total' => $totalRespondents,
'data' => array_filter($allAnswers) // Filter empty answers if any
];
continue;
}
// HANDLE RADIO/CHECKBOX (EXISTING LOGIC)
foreach ($question->options as $option) {
$optionLabel = trim($option->option_text);
$count = 0;
foreach ($allAnswers as $ans) {
if ($ans === $optionLabel) {
$count++;
}
}
$stats[] = [
'label' => $optionLabel,
'count' => $count,
'percentage' => $totalRespondents > 0 ? round(($count / $totalRespondents) * 100, 1) : 0
];
}
$optionTexts = $question->options->pluck('option_text')->map(fn($item) => trim($item))->toArray();
$othersCount = 0;
$otherAnswersList = [];
foreach ($allAnswers as $ans) {
if (!in_array($ans, $optionTexts)) {
$othersCount++;
// Clean up "Lain-lain: " prefix if exists
$cleanAns = str_replace("Lain-lain: ", "", $ans);
$otherAnswersList[] = $cleanAns;
}
}
if ($question->allow_other_option && $othersCount > 0) {
$stats[] = [
'label' => 'Lain-lain',
'count' => $othersCount,
'percentage' => $totalRespondents > 0 ? round(($othersCount / $totalRespondents) * 100, 1) : 0
];
}
$results[$question->id] = [
'question' => $question->question_text,
'type' => 'chart',
'total' => $totalRespondents,
'data' => $stats,
'other_answers' => $otherAnswersList
];
}
}
return view('admin.surveys.print_statistics', compact('survey', 'results', 'totalSurveyRespondents', 'totalSurveyQuestions'));
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Survey;
use App\Models\Section;
use App\Models\Question;
use App\Models\Option;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
use App\Http\Controllers\Controller;
class SurveyController extends Controller
{
public function index(Request $request)
{
$query = Survey::with('user', 'questions', 'responses');
if ($request->filled('search')) {
$s = $request->search;
$query->where('title', 'like', "%$s%");
}
$surveys = $query->latest()->paginate(20);
return view('admin.surveys.index', compact('surveys'));
}
public function create()
{
return view('admin.surveys.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'perincian' => 'nullable|string',
'date' => 'required|date',
'sections' => 'required|array|min:1',
'sections.*.title' => 'required|string',
'sections.*.questions' => 'required|array|min:1',
'sections.*.questions.*.text' => 'required|string',
'sections.*.questions.*.type' => 'required|in:radio,text,checkbox',
'sections.*.questions.*.allow_other_option' => 'nullable|boolean',
'sections.*.questions.*.options' => 'required_if:sections.*.questions.*.type,radio|required_if:sections.*.questions.*.type,checkbox|array',
'sections.*.questions.*.options.*' => 'required|string',
]);
try {
DB::beginTransaction();
$survey = Survey::create([
'title' => $request->title,
'perincian' => $request->perincian,
'date' => $request->date,
'user_id' => Auth::id(),
]);
foreach ($request->sections as $sectionIndex => $sectionData) {
$section = $survey->sections()->create([
'title' => $sectionData['title'],
'description' => $sectionData['description'] ?? null,
'order' => $sectionIndex,
]);
foreach ($sectionData['questions'] as $questionIndex => $questionData) {
$question = $section->questions()->create([
'question_text' => $questionData['text'],
'type' => $questionData['type'],
'allow_other_option' => $questionData['allow_other_option'] ?? false,
'order' => $questionIndex,
]);
if (isset($questionData['options'])) {
foreach ($questionData['options'] as $optionText) {
$question->options()->create([
'option_text' => $optionText,
]);
}
}
}
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e->getMessage());
return redirect()->back()->with('error', 'Gagal menyimpan borang. Sila cuba lagi.')->withInput();
}
return redirect()->route('admin.surveys.index')->with('success', 'Borang berjaya dicipta.');
}
public function edit(Survey $survey)
{
$survey->load('sections.questions.options');
$sections_with_questions = $survey->sections->map(function ($section) {
return [
'title' => $section->title,
'description' => $section->description,
'questions' => $section->questions->map(function ($q) {
return [
'text' => $q->question_text,
'type' => $q->type,
'allow_other_option' => (bool) $q->allow_other_option,
'options' => $q->options->map(function ($opt) {
return ['text' => $opt->option_text];
})->toArray(),
];
})->toArray(),
];
})->toArray();
return view('admin.surveys.edit', compact('survey', 'sections_with_questions'));
}
public function update(Request $request, Survey $survey)
{
$request->validate([
'title' => 'required|string|max:255',
'perincian' => 'nullable|string',
'date' => 'required|date',
'sections' => 'required|array|min:1',
'sections.*.title' => 'required|string',
'sections.*.questions' => 'required|array|min:1',
'sections.*.questions.*.text' => 'required|string',
'sections.*.questions.*.type' => 'required|in:radio,text,checkbox',
'sections.*.questions.*.allow_other_option' => 'nullable|boolean',
'sections.*.questions.*.options' => 'required_if:sections.*.questions.*.type,radio|required_if:sections.*.questions.*.type,checkbox|array',
'sections.*.questions.*.options.*' => 'required|string',
]);
try {
DB::beginTransaction();
$survey->update([
'title' => $request->title,
'perincian' => $request->perincian,
'date' => $request->date,
]);
foreach ($survey->sections as $oldSection) {
foreach ($oldSection->questions as $oldQuestion) {
$oldQuestion->options()->delete();
}
$oldSection->questions()->delete();
}
$survey->sections()->delete();
foreach ($request->sections as $sectionIndex => $sectionData) {
$section = $survey->sections()->create([
'title' => $sectionData['title'],
'description' => $sectionData['description'] ?? null,
'order' => $sectionIndex,
]);
foreach ($sectionData['questions'] as $questionIndex => $questionData) {
$question = $section->questions()->create([
'question_text' => $questionData['text'],
'type' => $questionData['type'],
'allow_other_option' => $questionData['allow_other_option'] ?? false,
'order' => $questionIndex,
]);
if (isset($questionData['options']) && is_array($questionData['options'])) {
foreach ($questionData['options'] as $optionText) {
if (is_array($optionText)) {
$optionText = $optionText['text'] ?? '';
}
$question->options()->create([
'option_text' => $optionText,
]);
}
}
}
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
\Log::error('Survey update failed: ' . $e->getMessage());
return redirect()->back()->with('error', 'Gagal kemaskini borang. Sila cuba lagi.')->withInput();
}
return redirect()->route('admin.surveys.index')->with('success', 'Borang berjaya dikemaskini.');
}
public function destroy(Survey $survey)
{
$survey->delete();
return redirect()->route('admin.surveys.index')->with('success', 'Borang berjaya dihapuskan.');
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class UlasanController extends Controller
{
public function ulasanPage(Request $request)
{
$query = \App\Models\Survey::whereNotNull('ulasan')
->where('ulasan', '!=', '');
if ($request->filled('search')) {
$search = $request->search;
$query->where('title', 'like', "%{$search}%");
}
$surveys = $query->orderBy('updated_at', 'desc')->get();
return view('admin.surveys.ulasan', compact('surveys'));
}
public function updateUlasan(Request $request, $id)
{
$survey = \App\Models\Survey::findOrFail($id);
$survey->ulasan = $request->ulasan;
$survey->save();
return back()->with('success', 'Ulasan berjaya dikemaskini!');
}
public function downloadCSV()
{
$surveys = \App\Models\Survey::whereNotNull('ulasan')
->where('ulasan', '!=', '')
->orderBy('updated_at', 'desc')
->get();
$filename = "keputusan_postmortem" . date('Ymd_His') . ".csv";
$handle = fopen('php://output', 'w');
// Add UTF-8 BOM for Excel compatibility
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
// Header
fputcsv($handle, ['ID', 'Tarikh Kemaskini', 'Tajuk Borang', 'Ulasan']);
// Data
foreach ($surveys as $survey) {
fputcsv($handle, [
$survey->id,
$survey->updated_at->format('d/m/Y'),
$survey->title,
$survey->ulasan
]);
}
fclose($handle);
return response()->streamDownload(function () use ($handle) {
// Already handled by fputcsv to php://output
}, $filename, [
'Content-Type' => 'text/csv',
]);
}
}