Files

339 lines
15 KiB
PHP
Raw Permalink Normal View History

2026-05-30 22:15:16 +07:00
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Department;
use App\Models\Position;
use App\Models\Subject;
2026-05-30 22:15:16 +07:00
use App\Models\User;
use App\Models\QuestionBank;
use App\Models\Exam;
2026-05-30 22:15:16 +07:00
class ExamManagementController extends Controller
{
// ==============================================================================
// 1. AREA MANAJEMEN UJIAN (CBT)
// ==============================================================================
public function index(Request $request)
2026-05-30 22:15:16 +07:00
{
$query = Exam::with('subject');
if ($request->filled('search')) {
$query->where('title', 'LIKE', '%' . $request->search . '%');
}
$exams = $query->latest()->paginate(20);
return view('pages.admin.exams.index', compact('exams'));
}
public function create()
{
// Tarik data materi untuk Select2 Modul
$subjects = Subject::orderBy('name', 'asc')->get();
// Tarik data karyawan untuk Select2 Peserta (Bisa pilih banyak)
$employees = User::orderBy('first_name', 'asc')->get();
return view('pages.admin.exams.create', compact('subjects', 'employees'));
}
public function store(Request $request)
{
$request->validate([
'training_type' => 'required|in:matrix_plant,general,external',
'is_random_order' => 'nullable|boolean',
]);
if ($request->training_type === 'external') {
return back()->with('info', 'Fitur External Training dengan database HR sedang dalam tahap pengembangan.');
2026-05-30 22:15:16 +07:00
}
// Validasi Tambahan: Peserta, Deadline, dan Array Ujian
$request->validate([
'employee_ids' => 'required|array|min:1', // Wajib pilih minimal 1 peserta
'employee_ids.*' => 'exists:users,id',
'deadline_date' => 'required|date|after_or_equal:today', // Deadline tidak boleh masa lalu
'exams' => 'required|array|min:1',
'exams.*.subject_id' => 'required|exists:subjects,id',
'exams.*.title' => 'nullable|string|max:255',
'exams.*.duration_minutes' => 'required|integer|min:1',
'exams.*.passing_grade' => 'required|integer|min:1|max:100',
'exams.*.max_retakes' => 'required|integer|min:0',
]);
$isRandom = $request->has('is_random_order');
$trainingType = $request->training_type === 'general' ? 'product' : 'matrix_plant';
// 1. Simpan setiap modul sebagai sesi ujian tersendiri
foreach ($request->exams as $examData) {
$exam = Exam::create([
'title' => $examData['title'] ?: Subject::find($examData['subject_id'])->name,
'subject_id' => $examData['subject_id'],
'training_type' => $trainingType,
'duration_minutes' => $examData['duration_minutes'],
'passing_grade' => $examData['passing_grade'],
'max_retakes' => $examData['max_retakes'],
'is_random_order' => $isRandom,
'is_active' => true,
]);
// 2. Tugaskan (Assign) ujian yang baru dibuat ini ke SEMUA karyawan yang dipilih
$assignments = [];
foreach ($request->employee_ids as $userId) {
$assignments[] = [
'user_id' => $userId,
'exam_id' => $exam->id,
'deadline_date' => $request->deadline_date,
'status' => 'pending',
'created_at' => now(),
'updated_at' => now(),
];
}
// Insert massal ke database agar prosesnya secepat kilat
\Illuminate\Support\Facades\DB::table('exam_assignments')->insert($assignments);
2026-05-30 22:15:16 +07:00
}
return redirect()->route('admin.exams.index')
->with('success', count($request->exams) . ' Modul Ujian berhasil dibuat dan ditugaskan ke ' . count($request->employee_ids) . ' Karyawan!');
}
// ==============================================================================
// 2. AREA MANAJEMEN BANK SOAL
// ==============================================================================
public function questionBank(Request $request)
{
$creatorIds = QuestionBank::whereNotNull('created_by')->distinct()->pluck('created_by');
$creators = User::whereIn('id', $creatorIds)->orderBy('first_name', 'asc')->get();
$subjects = Subject::orderBy('name', 'asc')->get();
$departments = Department::orderBy('name', 'asc')->get();
$positions = Position::orderBy('name', 'asc')->get();
$totalQuestions = QuestionBank::count();
// Query utama dengan relasi
$query = QuestionBank::with(['subject', 'department', 'creator', 'position']);
// 1. FILTER GLOBAL SEARCH (Mencari di semua kolom & relasi)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
// Cari di ID atau Teks Soal
$q->where('id', 'LIKE', "%{$search}%")
->orWhere('question_text', 'LIKE', "%{$search}%")
->orWhere('question_type', 'LIKE', "%{$search}%")
->orWhere('question_level', 'LIKE', "%{$search}%")
// Cari di Nama Materi
->orWhereHas('subject', function($sub) use ($search) {
$sub->where('name', 'LIKE', "%{$search}%");
})
// Cari di Nama Departemen
->orWhereHas('department', function($dep) use ($search) {
$dep->where('name', 'LIKE', "%{$search}%");
})
// Cari di Nama Jabatan
->orWhereHas('position', function($pos) use ($search) {
$pos->where('name', 'LIKE', "%{$search}%");
})
// Cari di Nama Pembuat Soal
->orWhereHas('creator', function($usr) use ($search) {
$usr->where('first_name', 'LIKE', "%{$search}%")
->orWhere('last_name', 'LIKE', "%{$search}%");
});
});
2026-05-30 22:15:16 +07:00
}
// 2. FILTER DROPDOWN SPESIFIK
if ($request->filled('creator_id')) { $query->where('created_by', $request->creator_id); }
if ($request->filled('subject_id')) { $query->where('subject_id', $request->subject_id); }
if ($request->filled('department_id')) { $query->where('department_id', $request->department_id); }
if ($request->filled('position_id')) { $query->where('position_id', $request->position_id); }
if ($request->filled('question_type')) { $query->where('question_type', $request->question_type); }
if ($request->filled('question_level')) { $query->where('question_level', $request->question_level); }
// ==============================================================================
// 3. INTERCEPTOR UNTUK EXPORT (Excel & PDF)
// ==============================================================================
if ($request->action === 'export_excel' || $request->action === 'export_pdf') {
$exportData = $query->latest()->get();
$exporterName = auth()->user()->first_name . ' ' . auth()->user()->last_name;
$exportTime = now()->format('d F Y, H:i:s');
// LOGIKA EXPORT EXCEL (Download Paksa)
if ($request->action === 'export_excel') {
$fileName = 'Data_Bank_Soal_' . now()->format('Ymd_His') . '.xls';
return response()->streamDownload(function () use ($exportData, $exporterName, $exportTime, $request) {
echo view('pages.admin.question-banks.export-report', compact('exportData', 'exporterName', 'exportTime', 'request'))->render();
}, $fileName, [
'Content-Type' => 'application/vnd.ms-excel',
'Content-Disposition' => 'attachment; filename="'.$fileName.'"',
]);
}
// LOGIKA EXPORT PDF (Menggunakan Facade DomPDF)
if ($request->action === 'export_pdf') {
$fileName = 'Data_Bank_Soal_' . now()->format('Ymd_His') . '.pdf';
// Gunakan library PDF yang sudah ada di project Anda
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pages.admin.question-banks.export-report', compact('exportData', 'exporterName', 'exportTime', 'request'))
->setPaper('a4', 'landscape'); // Format landscape agar tabel muat
return $pdf->download($fileName);
}
}
// 4. JIKA BUKAN EXPORT, LANJUTKAN KE TAMPILAN TABEL NORMAL DENGAN PAGINATION
$questions = $query->latest()->paginate(20);
2026-05-30 22:15:16 +07:00
return view('pages.admin.question-banks.question-bank', compact('questions', 'creators', 'subjects', 'departments', 'positions', 'totalQuestions'));
2026-05-30 22:15:16 +07:00
}
// Nama diubah agar tidak bentrok dengan create() ujian
public function createQuestion()
{
$departments = Department::orderBy('name', 'asc')->get();
$positions = Position::orderBy('name', 'asc')->get();
$subjects = Subject::orderBy('name', 'asc')->get();
return view('pages.admin.question-banks.create', compact('departments', 'positions', 'subjects'));
}
// Nama diubah agar spesifik untuk soal
public function editQuestion($id)
{
$question = QuestionBank::findOrFail($id);
$departments = Department::orderBy('name', 'asc')->get();
$positions = Position::orderBy('name', 'asc')->get();
$subjects = Subject::orderBy('name', 'asc')->get();
return view('pages.admin.question-banks.edit', compact('question', 'departments', 'positions', 'subjects'));
}
// Nama diubah agar spesifik untuk soal
public function showQuestion($id)
{
$question = QuestionBank::with(['subject', 'department', 'position', 'creator'])->findOrFail($id);
return view('pages.admin.question-banks.show', compact('question'));
}
2026-05-30 22:15:16 +07:00
public function bulkDelete(Request $request)
{
$request->validate([
'question_ids' => 'required|array',
'question_ids.*' => 'exists:question_banks,id' // Diperbaiki dari tabel questions ke question_banks
2026-05-30 22:15:16 +07:00
]);
try {
// Diperbaiki dari model Question ke QuestionBank
QuestionBank::whereIn('id', $request->question_ids)->delete();
2026-05-30 22:15:16 +07:00
return back()->with('success', count($request->question_ids) . ' Soal berhasil dihapus secara massal.');
} catch (\Exception $e) {
return back()->with('error', 'Gagal menghapus soal: ' . $e->getMessage());
}
}
public function importView()
{
return view('pages.admin.exams.questions.import');
2026-05-30 22:15:16 +07:00
}
// ==============================================================================
// FUNGSI PROSES SIMPAN, EDIT, DAN HAPUS BANK SOAL
// ==============================================================================
public function storeQuestion(Request $request)
{
$request->validate([
'subject_id' => 'required|exists:subjects,id',
'department_id' => 'nullable|exists:departments,id',
'position_id' => 'nullable|exists:positions,id',
// VALIDASI INI SUDAH DISAMAKAN DENGAN FORM UI
'question_type' => 'required|in:single,multiple,true_false,descriptive',
'question_text' => 'required|string',
'score_weight' => 'required|numeric|min:1',
'duration' => 'nullable|numeric',
'passing_grade' => 'nullable|numeric',
]);
$data = $request->all();
$data['created_by'] = auth()->id(); // Rekam siapa yang membuat soal
// Bersihkan data yang tidak relevan berdasarkan jenis soal
if ($request->question_type === 'descriptive') {
$data['option_a'] = null;
$data['option_b'] = null;
$data['option_c'] = null;
$data['option_d'] = null;
$data['option_e'] = null;
$data['correct_answer'] = null;
} else {
$data['essay_keywords'] = null;
}
// Jika tipe soal adalah Multiple (bisa centang lebih dari 1), ubah array menjadi JSON
// if (isset($request->correct_answer) && is_array($request->correct_answer)) {
// $data['correct_answer'] = json_encode($request->correct_answer);
// }
QuestionBank::create($data);
return redirect()->route('admin.question-bank.index')->with('success', 'Soal baru berhasil ditambahkan ke Bank Soal!');
}
public function updateQuestion(Request $request, $id)
{
$request->validate([
'subject_id' => 'required|exists:subjects,id',
'department_id' => 'nullable|exists:departments,id',
'position_id' => 'nullable|exists:positions,id',
'question_type' => 'required|in:single,multiple,true_false,descriptive',
'question_text' => 'required|string',
'score_weight' => 'required|numeric|min:1',
'duration' => 'nullable|numeric',
'passing_grade' => 'nullable|numeric',
]);
$question = QuestionBank::findOrFail($id);
$data = $request->all();
// 1. Bersihkan data yang tidak relevan berdasarkan jenis soal
if ($request->question_type === 'descriptive') {
$data['option_a'] = null;
$data['option_b'] = null;
$data['option_c'] = null;
$data['option_d'] = null;
$data['option_e'] = null;
$data['correct_answer'] = null;
} else {
$data['essay_keywords'] = null;
}
// 2. Jika tipe soal adalah Multiple (bisa centang lebih dari 1), ubah array menjadi JSON
// if (is_array($request->correct_answer)) {
// $data['correct_answer'] = json_encode($request->correct_answer);
// }
// 3. Simpan pembaruan
$question->update($data);
return redirect()->route('admin.question-bank.index')->with('success', 'Perubahan soal berhasil disimpan!');
}
public function destroyQuestion($id)
{
try {
$question = QuestionBank::findOrFail($id);
$question->delete();
return back()->with('success', 'Soal berhasil dihapus.');
} catch (\Exception $e) {
return back()->with('error', 'Gagal menghapus soal: ' . $e->getMessage());
}
}
2026-05-30 22:15:16 +07:00
}