Detailing fungsi semua modul dari bugs error
This commit is contained in:
@@ -9,32 +9,159 @@ use App\Models\Sop;
|
||||
use App\Models\ExamResult;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Department;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 1. Fungsi Dashboard Utama Admin
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(Request $request)
|
||||
{
|
||||
$statistics = [
|
||||
// PENTING: Menggunakan whereHas() untuk mengecek relasi Role di tabel pivot
|
||||
'total_karyawan' => User::whereHas('roles', function ($query) {
|
||||
$query->where('name', 'trainee'); // Atau 'karyawan', sesuaikan dengan nama role di database Anda
|
||||
})->count(),
|
||||
// 1. FILTER TAHUN & BULAN
|
||||
$selectedYear = $request->input('year', Carbon::now()->year);
|
||||
$selectedMonth = $request->input('month', Carbon::now()->month);
|
||||
|
||||
'total_trainer' => User::whereHas('roles', function ($query) {
|
||||
// Menghitung admin dan trainer
|
||||
$query->whereIn('name', ['trainer', 'admin']);
|
||||
})->count(),
|
||||
// 2. STATISTIK OVERDUE (Training Terlewat)
|
||||
// Mengambil penugasan yang deadlinenya sudah lewat dan belum lulus
|
||||
$overdueTrainings = DB::table('exam_assignments')
|
||||
->join('users', 'exam_assignments.user_id', '=', 'users.id')
|
||||
->join('exams', 'exam_assignments.exam_id', '=', 'exams.id')
|
||||
->join('departments', 'users.department_id', '=', 'departments.id')
|
||||
->whereMonth('exam_assignments.deadline_date', $selectedMonth)
|
||||
->whereYear('exam_assignments.deadline_date', $selectedYear)
|
||||
->where('exam_assignments.deadline_date', '<', Carbon::now())
|
||||
->where('exam_assignments.status', '!=', 'passed')
|
||||
->select('users.first_name', 'users.last_name', 'exams.title', 'departments.name as dept_name', 'exam_assignments.deadline_date')
|
||||
->orderBy('exam_assignments.deadline_date', 'asc')
|
||||
->take(5) // Ambil 5 teratas untuk list ringkas
|
||||
->get();
|
||||
|
||||
'total_sop' => Sop::where('status', 'Active')->count(),
|
||||
$totalOverdue = DB::table('exam_assignments')
|
||||
->whereMonth('deadline_date', $selectedMonth)
|
||||
->whereYear('deadline_date', $selectedYear)
|
||||
->where('deadline_date', '<', Carbon::now())
|
||||
->where('status', '!=', 'passed')
|
||||
->count();
|
||||
|
||||
// 3. PERSENTASE PEMENUHAN TRAINING PER DEPARTEMEN
|
||||
// Menghitung (Total Lulus / Total Ditugaskan) * 100 per departemen
|
||||
$departments = Department::all();
|
||||
$deptFulfillment = [];
|
||||
|
||||
foreach ($departments as $dept) {
|
||||
$totalAssigned = DB::table('exam_assignments')
|
||||
->join('users', 'exam_assignments.user_id', '=', 'users.id')
|
||||
->where('users.department_id', $dept->id)
|
||||
->whereMonth('exam_assignments.created_at', $selectedMonth)
|
||||
->whereYear('exam_assignments.created_at', $selectedYear)
|
||||
->count();
|
||||
|
||||
$totalPassed = DB::table('exam_assignments')
|
||||
->join('users', 'exam_assignments.user_id', '=', 'users.id')
|
||||
->where('users.department_id', $dept->id)
|
||||
->where('exam_assignments.status', 'passed')
|
||||
->whereMonth('exam_assignments.created_at', $selectedMonth)
|
||||
->whereYear('exam_assignments.created_at', $selectedYear)
|
||||
->count();
|
||||
|
||||
$percentage = $totalAssigned > 0 ? round(($totalPassed / $totalAssigned) * 100) : 0;
|
||||
|
||||
'lulus_ujian' => ExamResult::where('is_passed', true)->count(),
|
||||
];
|
||||
$deptFulfillment[] = [
|
||||
'id' => $dept->id,
|
||||
'name' => $dept->name,
|
||||
'assigned' => $totalAssigned,
|
||||
'passed' => $totalPassed,
|
||||
'percentage' => $percentage
|
||||
];
|
||||
}
|
||||
|
||||
// Pastikan path view ini benar-benar ada di resources/views/pages/admin/dashboard/dashboard.blade.php
|
||||
return view('pages.admin.dashboard.dashboard', compact('statistics'));
|
||||
// Urutkan dari persentase terkecil (yang butuh perhatian) ke terbesar
|
||||
usort($deptFulfillment, function($a, $b) {
|
||||
return $a['percentage'] <=> $b['percentage'];
|
||||
});
|
||||
|
||||
// 4. DATA ACTIVE / ONGOING EXAM (Untuk injeksi awal Realtime)
|
||||
// Kita tarik data awal, selanjutnya Alpine.js akan melakukan polling AJAX
|
||||
$activeExamsCount = DB::table('exam_attempts')->where('status', 'ongoing')->count();
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 5. DATA UNTUK KALENDER TRAINING (Berdasarkan Role)
|
||||
// -------------------------------------------------------------------
|
||||
$user = \Illuminate\Support\Facades\Auth::user();
|
||||
|
||||
$calendarQuery = DB::table('exam_assignments')
|
||||
->join('users', 'exam_assignments.user_id', '=', 'users.id')
|
||||
->join('exams', 'exam_assignments.exam_id', '=', 'exams.id')
|
||||
->select(
|
||||
'exam_assignments.id',
|
||||
'exams.title as exam_title',
|
||||
'users.first_name',
|
||||
'exam_assignments.created_at',
|
||||
'exam_assignments.deadline_date',
|
||||
'exam_assignments.status'
|
||||
);
|
||||
|
||||
// LOGIKA ROLE: Jika BUKAN superadmin, batasi hanya melihat departemennya sendiri
|
||||
if (!$user->hasRole('superadmin')) {
|
||||
$calendarQuery->where('users.department_id', $user->department_id);
|
||||
}
|
||||
|
||||
$calendarData = $calendarQuery->get()->map(function($assign) {
|
||||
// Menentukan warna event di kalender berdasarkan status
|
||||
$color = '#f59e0b'; // Amber (Pending)
|
||||
if ($assign->status === 'passed') $color = '#10b981'; // Emerald (Lulus)
|
||||
if ($assign->status === 'ongoing') $color = '#3b82f6'; // Blue (Sedang dikerjakan)
|
||||
if ($assign->status != 'passed' && Carbon::parse($assign->deadline_date)->isPast()) {
|
||||
$color = '#ef4444'; // Red (Overdue)
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $assign->id,
|
||||
'title' => $assign->first_name . ' - ' . $assign->exam_title,
|
||||
'start' => Carbon::parse($assign->created_at)->format('Y-m-d'), // Tanggal ditugaskan
|
||||
'end' => Carbon::parse($assign->deadline_date)->addDay()->format('Y-m-d'), // Tanggal deadline (+1 agar FullCalendar merender sampai hari H penuh)
|
||||
'color' => $color,
|
||||
'allDay'=> true
|
||||
];
|
||||
});
|
||||
|
||||
return view('pages.admin.dashboard.dashboard', compact(
|
||||
'selectedYear', 'selectedMonth', 'overdueTrainings', 'totalOverdue', 'deptFulfillment', 'activeExamsCount', 'calendarData' // <--- Tambahkan ini
|
||||
));
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// FUNGSI API UNTUK REAL-TIME MONITORING (AJAX Polling)
|
||||
// ==========================================================
|
||||
public function getLiveExams()
|
||||
{
|
||||
$liveExams = DB::table('exam_attempts')
|
||||
->join('exam_assignments', 'exam_attempts.exam_assignment_id', '=', 'exam_assignments.id')
|
||||
->join('users', 'exam_assignments.user_id', '=', 'users.id')
|
||||
->join('exams', 'exam_assignments.exam_id', '=', 'exams.id')
|
||||
->join('departments', 'users.department_id', '=', 'departments.id')
|
||||
->where('exam_attempts.status', 'ongoing')
|
||||
->select(
|
||||
'exam_attempts.id as attempt_id',
|
||||
'users.first_name',
|
||||
'users.last_name',
|
||||
'departments.name as dept_name',
|
||||
'exams.title as exam_title',
|
||||
'exam_attempts.started_at',
|
||||
'exams.duration_minutes'
|
||||
)
|
||||
->get()
|
||||
->map(function ($exam) {
|
||||
// Kalkulasi sisa waktu secara server-side
|
||||
$started = Carbon::parse($exam->started_at);
|
||||
$endsAt = $started->copy()->addMinutes($exam->duration_minutes);
|
||||
$exam->time_left_seconds = Carbon::now()->diffInSeconds($endsAt, false); // Minus jika kelebihan
|
||||
return $exam;
|
||||
});
|
||||
|
||||
return response()->json($liveExams);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,4 +361,31 @@ class DashboardController extends Controller
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// FUNGSI UNTUK HALAMAN DETAIL PEMENUHAN DEPARTEMEN
|
||||
// ==========================================================
|
||||
public function departmentFulfillment($id, Request $request)
|
||||
{
|
||||
$department = Department::findOrFail($id);
|
||||
$selectedMonth = $request->input('month', Carbon::now()->month);
|
||||
$selectedYear = $request->input('year', Carbon::now()->year);
|
||||
|
||||
// Ambil detail penugasan ujian khusus departemen ini di bulan/tahun yang dipilih
|
||||
$assignments = DB::table('exam_assignments')
|
||||
->join('users', 'exam_assignments.user_id', '=', 'users.id')
|
||||
->join('exams', 'exam_assignments.exam_id', '=', 'exams.id')
|
||||
->where('users.department_id', $id)
|
||||
->whereMonth('exam_assignments.created_at', $selectedMonth)
|
||||
->whereYear('exam_assignments.created_at', $selectedYear)
|
||||
->select(
|
||||
'users.first_name', 'users.last_name', 'users.nik',
|
||||
'exams.title', 'exam_assignments.status',
|
||||
'exam_assignments.deadline_date', 'exam_assignments.created_at'
|
||||
)
|
||||
->orderBy('exam_assignments.created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('pages.admin.dashboard.department-detail', compact('department', 'assignments', 'selectedMonth', 'selectedYear'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,9 +49,11 @@ class DepartmentController extends Controller
|
||||
->with('success', 'Departemen baru berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function show(Department $department)
|
||||
public function show($id)
|
||||
{
|
||||
// Hanya melempar object $department ke view tanpa load relasi positions
|
||||
// Pastikan kita me-load relasi 'users' (Karyawan) beserta 'position' (Jabatan) mereka
|
||||
$department = \App\Models\Department::with(['users.position'])->findOrFail($id);
|
||||
|
||||
return view('pages.admin.departments.show', compact('department'));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,110 +9,331 @@ use App\Models\Position;
|
||||
use App\Models\Subject;
|
||||
use App\Models\User;
|
||||
use App\Models\QuestionBank;
|
||||
use App\Models\Exam;
|
||||
|
||||
class ExamManagementController extends Controller
|
||||
{
|
||||
public function questionBank(Request $request)
|
||||
// ==============================================================================
|
||||
// 1. AREA MANAJEMEN UJIAN (CBT)
|
||||
// ==============================================================================
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
// 1. Ambil ID unik siapa saja yang PERNAH membuat soal di tabel question_banks
|
||||
$creatorIds = \App\Models\QuestionBank::whereNotNull('created_by')
|
||||
->distinct()
|
||||
->pluck('created_by');
|
||||
$query = Exam::with('subject');
|
||||
|
||||
// 2. Tarik data User HANYA berdasarkan ID pembuat soal tersebut
|
||||
$creators = \App\Models\User::whereIn('id', $creatorIds)
|
||||
->orderBy('first_name', 'asc')
|
||||
->get();
|
||||
|
||||
// 3. Tarik data pendukung lainnya untuk filter & dropdown
|
||||
$subjects = \App\Models\Subject::orderBy('name', 'asc')->get();
|
||||
$departments = \App\Models\Department::orderBy('name', 'asc')->get();
|
||||
$positions = \App\Models\Position::orderBy('name', 'asc')->get();
|
||||
|
||||
// 4. Hitung Total Soal Keseluruhan (TANPA FILTER) untuk Badge di View
|
||||
$totalQuestions = \App\Models\QuestionBank::count(); // <--- TAMBAHKAN BARIS INI
|
||||
|
||||
// 5. Query utama Bank Soal (dengan fitur pencarian/filter)
|
||||
$query = \App\Models\QuestionBank::with(['subject', 'department', 'creator', 'position']);
|
||||
|
||||
// Filter berdasarkan pembuat soal
|
||||
if ($request->filled('creator_id')) {
|
||||
$query->where('created_by', $request->creator_id);
|
||||
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();
|
||||
|
||||
// Filter berdasarkan subject/materi
|
||||
if ($request->filled('subject_id')) {
|
||||
$query->where('subject_id', $request->subject_id);
|
||||
// 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.');
|
||||
}
|
||||
|
||||
// Filter berdasarkan departemen
|
||||
if ($request->filled('department_id')) {
|
||||
$query->where('department_id', $request->department_id);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Filter berdasarkan posisi/jabatan
|
||||
if ($request->filled('position_id')) {
|
||||
$query->where('position_id', $request->position_id);
|
||||
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}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Pagination
|
||||
// 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);
|
||||
|
||||
// Pastikan $totalQuestions ikut dikirim ke compact
|
||||
return view('pages.admin.question-banks.question-bank', compact('questions', 'creators', 'subjects', 'departments', 'positions', 'totalQuestions'));
|
||||
}
|
||||
|
||||
// Fungsi untuk menampilkan halaman Tambah Soal
|
||||
public function create()
|
||||
// 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(); // Kirim Subject ke form
|
||||
$subjects = Subject::orderBy('name', 'asc')->get();
|
||||
|
||||
return view('pages.admin.question-banks.create', compact('departments', 'positions', 'subjects'));
|
||||
}
|
||||
|
||||
// Fungsi untuk menampilkan halaman Edit Soal
|
||||
public function edit($id)
|
||||
// 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(); // Kirim Subject ke form
|
||||
$subjects = Subject::orderBy('name', 'asc')->get();
|
||||
|
||||
return view('pages.admin.question-banks.edit', compact('question', 'departments', 'positions', 'subjects'));
|
||||
}
|
||||
|
||||
// Fungsi Show untuk Detail Soal
|
||||
public function show($id)
|
||||
// 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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghapus banyak soal sekaligus (Bulk Delete)
|
||||
*/
|
||||
public function bulkDelete(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'question_ids' => 'required|array',
|
||||
'question_ids.*' => 'exists:questions,id'
|
||||
'question_ids.*' => 'exists:question_banks,id' // Diperbaiki dari tabel questions ke question_banks
|
||||
]);
|
||||
|
||||
try {
|
||||
Question::whereIn('id', $request->question_ids)->delete();
|
||||
// Diperbaiki dari model Question ke QuestionBank
|
||||
QuestionBank::whereIn('id', $request->question_ids)->delete();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan Halaman Import Soal
|
||||
*/
|
||||
public function importView()
|
||||
{
|
||||
return view('pages.admin.exams.questions.import');
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-6
@@ -2,20 +2,28 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Exam extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'old_id',
|
||||
'title',
|
||||
'duration',
|
||||
'passing_percentage'
|
||||
'description',
|
||||
'training_type',
|
||||
'subject_id',
|
||||
'duration_minutes',
|
||||
'passing_grade',
|
||||
'max_retakes',
|
||||
'is_random_order',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
public function results(): HasMany
|
||||
// Relasi: Setiap Ujian pasti milik satu Materi (Subject)
|
||||
public function subject()
|
||||
{
|
||||
return $this->hasMany(ExamResult::class);
|
||||
return $this->belongsTo(Subject::class);
|
||||
}
|
||||
}
|
||||
@@ -10,25 +10,10 @@ class QuestionBank extends Model
|
||||
protected $table = 'question_banks';
|
||||
|
||||
protected $fillable = [
|
||||
// Atribut Soal dari desain Anda
|
||||
'old_id',
|
||||
'subject_id',
|
||||
'question_type',
|
||||
'question_level',
|
||||
'passing_grade',
|
||||
'duration',
|
||||
'question_text',
|
||||
'option_a',
|
||||
'option_b',
|
||||
'option_c',
|
||||
'option_d',
|
||||
'option_e',
|
||||
'correct_answer',
|
||||
|
||||
// PENTING: Atribut Relasi yang dibutuhkan form Create/Edit & Filter Index
|
||||
'department_id',
|
||||
'position_id',
|
||||
'training_matrix_id',
|
||||
'subject_id', 'department_id', 'position_id', 'question_type',
|
||||
'question_level', 'question_text', 'option_a', 'option_b',
|
||||
'option_c', 'option_d', 'option_e', 'correct_answer',
|
||||
'essay_keywords', 'score_weight', 'passing_grade', 'duration',
|
||||
'created_by'
|
||||
];
|
||||
|
||||
|
||||
@@ -19,4 +19,12 @@ class Subject extends Model
|
||||
{
|
||||
return $this->hasMany(QuestionBank::class, 'subject_id');
|
||||
}
|
||||
|
||||
public function users() // atau employees()
|
||||
{
|
||||
// Paksa pencarian menggunakan 'user_id'
|
||||
return $this->belongsToMany(User::class, 'employee_subject', 'subject_id', 'user_id')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -73,10 +73,18 @@ class User extends Authenticatable
|
||||
return $this->belongsTo(Position::class, 'position_id');
|
||||
}
|
||||
|
||||
public function subjects()
|
||||
{
|
||||
// Parameter ke-3 'user_id' akan memaksa Laravel mengabaikan pencarian 'employee_id'
|
||||
return $this->belongsToMany(\App\Models\Subject::class, 'employee_subject', 'user_id', 'subject_id')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function trainings()
|
||||
{
|
||||
// Ubah 'user_id' menjadi 'employee_id' agar sesuai dengan database Anda saat ini
|
||||
return $this->belongsToMany(Subject::class, 'employee_subject', 'employee_id', 'subject_id')
|
||||
// Karena ini Model User, foreign key-nya harus 'user_id'
|
||||
return $this->belongsToMany(Subject::class, 'employee_subject', 'user_id', 'subject_id')
|
||||
->withPivot('status')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user