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();
|
||||
|
||||
'lulus_ujian' => ExamResult::where('is_passed', true)->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;
|
||||
|
||||
$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
|
||||
{
|
||||
// ==============================================================================
|
||||
// 1. AREA MANAJEMEN UJIAN (CBT)
|
||||
// ==============================================================================
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$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.');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// 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');
|
||||
$creatorIds = QuestionBank::whereNotNull('created_by')->distinct()->pluck('created_by');
|
||||
$creators = User::whereIn('id', $creatorIds)->orderBy('first_name', 'asc')->get();
|
||||
|
||||
// 2. Tarik data User HANYA berdasarkan ID pembuat soal tersebut
|
||||
$creators = \App\Models\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();
|
||||
|
||||
// 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();
|
||||
$totalQuestions = QuestionBank::count();
|
||||
|
||||
// 4. Hitung Total Soal Keseluruhan (TANPA FILTER) untuk Badge di View
|
||||
$totalQuestions = \App\Models\QuestionBank::count(); // <--- TAMBAHKAN BARIS INI
|
||||
// Query utama dengan relasi
|
||||
$query = QuestionBank::with(['subject', 'department', 'creator', 'position']);
|
||||
|
||||
// 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);
|
||||
// 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}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Filter berdasarkan subject/materi
|
||||
if ($request->filled('subject_id')) {
|
||||
$query->where('subject_id', $request->subject_id);
|
||||
// 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.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
// Filter berdasarkan departemen
|
||||
if ($request->filled('department_id')) {
|
||||
$query->where('department_id', $request->department_id);
|
||||
}
|
||||
// LOGIKA EXPORT PDF (Menggunakan Facade DomPDF)
|
||||
if ($request->action === 'export_pdf') {
|
||||
$fileName = 'Data_Bank_Soal_' . now()->format('Ymd_His') . '.pdf';
|
||||
|
||||
// Filter berdasarkan posisi/jabatan
|
||||
if ($request->filled('position_id')) {
|
||||
$query->where('position_id', $request->position_id);
|
||||
}
|
||||
// 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
|
||||
|
||||
// Pagination
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('exams', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
|
||||
// Mengakomodasi 3 jenis training sesuai permintaan Anda
|
||||
$table->enum('training_type', ['matrix_plant', 'product', 'external']);
|
||||
|
||||
// Relasi ke Materi/Subject (Satu ujian bisa mewakili satu materi SOP)
|
||||
$table->foreignId('subject_id')->constrained()->onDelete('cascade');
|
||||
|
||||
// Aturan CBT
|
||||
$table->integer('duration_minutes')->default(60); // Durasi ujian
|
||||
$table->integer('passing_grade')->default(80); // Nilai standar kelulusan
|
||||
$table->integer('max_retakes')->default(1); // Kesempatan remedial (1 = bisa remedial 1x)
|
||||
$table->boolean('is_random_order')->default(true); // Opsi acak soal antar peserta
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('exams');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('exam_assignments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id'); // Karyawan yang ditugaskan
|
||||
$table->foreignId('exam_id')->constrained()->onDelete('cascade');
|
||||
|
||||
$table->dateTime('deadline_date'); // Batas akhir pengerjaan
|
||||
|
||||
// Status pengerjaan
|
||||
$table->enum('status', ['pending', 'in_progress', 'passed', 'failed'])->default('pending');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('exam_assignments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('exam_attempts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('exam_assignment_id')->constrained()->onDelete('cascade'); // Link ke penugasan
|
||||
|
||||
$table->integer('attempt_number')->default(1); // Percobaan ke-1, ke-2 (remedial), dst
|
||||
$table->decimal('final_score', 5, 2)->nullable(); // Nilai akhir (0-100)
|
||||
|
||||
$table->enum('status', ['ongoing', 'completed'])->default('ongoing');
|
||||
|
||||
$table->dateTime('started_at');
|
||||
$table->dateTime('completed_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('exam_attempts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('question_banks', function (Blueprint $table) {
|
||||
// Menandai jenis soal (default 'multiple_choice' agar data lama Anda tetap aman)
|
||||
if (!Schema::hasColumn('question_banks', 'question_type')) {
|
||||
$table->enum('question_type', ['multiple_choice', 'essay'])->default('multiple_choice')->after('id');
|
||||
}
|
||||
|
||||
// Menyimpan kata kunci wajib untuk soal esai (pisahkan dengan koma)
|
||||
if (!Schema::hasColumn('question_banks', 'essay_keywords')) {
|
||||
$table->text('essay_keywords')->nullable()->after('question_type');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('question_banks', function (Blueprint $table) {
|
||||
$table->dropColumn(['question_type', 'essay_keywords']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('exam_answers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('exam_attempt_id')->constrained('exam_attempts')->cascadeOnDelete();
|
||||
$table->foreignId('question_id')->constrained('question_banks')->cascadeOnDelete();
|
||||
|
||||
// Snapshot tipe soal (berjaga-jaga jika master soal diubah setelah ujian selesai)
|
||||
$table->string('question_type', 50)->nullable();
|
||||
|
||||
// Jawaban peserta (bisa teks esai, atau huruf opsi A/B/C/D)
|
||||
$table->longText('student_answer')->nullable();
|
||||
|
||||
// Pengecekan Sistem Otomatis (True jika PG benar, False jika salah)
|
||||
$table->boolean('is_correct')->nullable();
|
||||
|
||||
// --- SISTEM PENILAIAN HYBRID ---
|
||||
// Nilai hitungan mesin/sistem (bisa dari cocoknya kunci jawaban atau deteksi keyword)
|
||||
$table->decimal('auto_score', 8, 2)->default(0);
|
||||
|
||||
// Poin tambahan/pengurangan dari koreksi manual oleh Trainer
|
||||
$table->decimal('manual_score', 8, 2)->default(0);
|
||||
|
||||
// Total nilai mutlak untuk 1 soal ini (auto_score + manual_score)
|
||||
$table->decimal('final_score', 8, 2)->default(0);
|
||||
|
||||
// Feedback/Catatan dari trainer khusus untuk jawaban ini
|
||||
$table->text('trainer_notes')->nullable();
|
||||
|
||||
// Siapa trainer yang menilai soal ini (jika manual)
|
||||
$table->unsignedBigInteger('graded_by')->nullable();
|
||||
$table->timestamp('graded_at')->nullable();
|
||||
|
||||
// --- FUTURE PROOFING ---
|
||||
// Kolom JSON super fleksibel untuk menyimpan data apapun di masa depan
|
||||
// (contoh: log pindah tab browser, durasi waktu spesifik di soal ini, dll)
|
||||
$table->json('metadata')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('exam_answers');
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-PaZlYqLy.css",
|
||||
"file": "assets/app-ChgCWa45.css",
|
||||
"name": "app",
|
||||
"names": [
|
||||
"app.css"
|
||||
|
||||
@@ -33,5 +33,70 @@
|
||||
</div>
|
||||
|
||||
@livewireScripts
|
||||
|
||||
@if(session()->has('success') || session()->has('error') || session()->has('info') || session()->has('warning') || $errors->any())
|
||||
<div x-data="{ show: true }"
|
||||
x-show="show"
|
||||
x-init="setTimeout(() => show = false, 5000)"
|
||||
x-transition:enter="transform ease-out duration-300 transition"
|
||||
x-transition:enter-start="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-10"
|
||||
x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 sm:translate-x-0"
|
||||
x-transition:leave-end="opacity-0 sm:translate-x-10"
|
||||
class="fixed top-5 right-5 z-[9999] flex w-full max-w-sm overflow-hidden bg-white rounded-xl shadow-2xl border border-slate-100"
|
||||
style="display: none;">
|
||||
|
||||
@if(session('success'))
|
||||
<div class="flex items-center justify-center w-12 bg-emerald-500">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
||||
</div>
|
||||
@elseif(session('error') || $errors->any())
|
||||
<div class="flex items-center justify-center w-12 bg-red-500">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</div>
|
||||
@elseif(session('info'))
|
||||
<div class="flex items-center justify-center w-12 bg-blue-500">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
@elseif(session('warning'))
|
||||
<div class="flex items-center justify-center w-12 bg-amber-500">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="px-4 py-3 -mx-3 w-full">
|
||||
<div class="mx-3">
|
||||
@if(session('success')) <span class="font-bold text-emerald-500">Berhasil!</span>
|
||||
@elseif(session('error') || $errors->any()) <span class="font-bold text-red-500">Gagal Menyimpan!</span>
|
||||
@elseif(session('info')) <span class="font-bold text-blue-500">Informasi</span>
|
||||
@elseif(session('warning')) <span class="font-bold text-amber-500">Peringatan</span>
|
||||
@endif
|
||||
|
||||
<p class="text-sm text-slate-600 mt-1 font-medium line-clamp-2">
|
||||
@if($errors->any())
|
||||
{{ $errors->first() }}
|
||||
@else
|
||||
{{ session('success') ?? session('error') ?? session('info') ?? session('warning') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-start px-2 py-2 shrink-0">
|
||||
<button @click="show = false" class="text-slate-400 hover:text-slate-600 focus:outline-none transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 h-1
|
||||
{{ session('success') ? 'bg-emerald-200' : (session('error') || $errors->any() ? 'bg-red-200' : (session('info') ? 'bg-blue-200' : 'bg-amber-200')) }}
|
||||
w-full"
|
||||
x-data="{ width: '100%' }"
|
||||
x-init="setTimeout(() => width = '0%', 50)"
|
||||
:style="`width: ${width}; transition: width 5s linear;`">
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +1,312 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Dashboard HR - LMS V2.0')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="mb-8 flex justify-between items-end">
|
||||
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js'></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<style>
|
||||
/* Styling kalender agar sesuai dengan Tailwind */
|
||||
.fc-theme-standard .fc-scrollgrid { border-color: #e2e8f0; border-radius: 0.75rem; overflow: hidden; }
|
||||
.fc-theme-standard td, .fc-theme-standard th { border-color: #e2e8f0; }
|
||||
.fc .fc-toolbar-title { font-size: 1.125rem; font-weight: 700; color: #1e293b; }
|
||||
.fc .fc-button-primary { background-color: #2563eb; border-color: #2563eb; font-weight: 600; text-transform: capitalize; border-radius: 0.5rem;}
|
||||
.fc .fc-button-primary:hover { background-color: #1d4ed8; border-color: #1d4ed8; }
|
||||
.fc .fc-button-primary:disabled { background-color: #94a3b8; border-color: #94a3b8; }
|
||||
.fc-day-today { background-color: #f8fafc !important; }
|
||||
.fc-event { cursor: pointer; border-radius: 4px; padding: 2px 4px; border: none; font-size: 0.75rem; font-weight: bold;}
|
||||
</style>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-extrabold text-slate-900 tracking-tight">Statistik Pelatihan CPOB</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Ringkasan performa dan kepatuhan training karyawan bulan ini.</p>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Command Center Training</h1>
|
||||
<p class="text-sm text-slate-500">Pantau pemenuhan kompetensi dan aktivitas CBT Karyawan secara real-time.</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="bg-indigo-600 text-white px-4 py-2 rounded-md shadow hover:bg-indigo-700 text-sm font-semibold">
|
||||
+ Buat Ujian Baru
|
||||
|
||||
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex items-center space-x-3 bg-white p-2 rounded-xl shadow-sm border border-slate-200">
|
||||
<select name="month" class="bg-slate-50 border-none text-sm font-bold text-slate-700 rounded-lg focus:ring-0 cursor-pointer">
|
||||
@foreach(range(1, 12) as $m)
|
||||
<option value="{{ $m }}" {{ $selectedMonth == $m ? 'selected' : '' }}>
|
||||
{{ date('F', mktime(0, 0, 0, $m, 1)) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="year" class="bg-slate-50 border-none text-sm font-bold text-slate-700 rounded-lg focus:ring-0 cursor-pointer">
|
||||
@foreach(range(date('Y')-2, date('Y')+1) as $y)
|
||||
<option value="{{ $y }}" {{ $selectedYear == $y ? 'selected' : '' }}>{{ $y }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
|
||||
<div class="bg-slate-900 rounded-2xl shadow-lg border border-slate-800 overflow-hidden"
|
||||
x-data="liveMonitoring()"
|
||||
x-init="fetchData(); setInterval(() => fetchData(), 5000);">
|
||||
|
||||
<div class="p-5 border-b border-slate-700 flex justify-between items-center bg-slate-800/50">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="relative flex h-3 w-3">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
||||
</div>
|
||||
<h2 class="text-sm font-bold text-white uppercase tracking-wider">Live Exam Monitoring</h2>
|
||||
</div>
|
||||
<div class="text-xs font-bold text-slate-400 bg-slate-800 px-3 py-1 rounded-full border border-slate-700">
|
||||
<span x-text="activeTrainees.length"></span> Karyawan Online
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
||||
<p class="text-xs font-bold text-slate-400 uppercase">Total Karyawan</p>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mt-1">{{ $statistics['total_karyawan'] }}</h3>
|
||||
</div>
|
||||
<div class="p-0 max-h-[300px] overflow-y-auto custom-scrollbar">
|
||||
<table class="w-full text-left text-sm text-slate-300">
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
<tr x-show="activeTrainees.length === 0">
|
||||
<td class="px-6 py-8 text-center text-slate-500">
|
||||
Tidak ada karyawan yang sedang mengerjakan ujian saat ini.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
||||
<p class="text-xs font-bold text-slate-400 uppercase">Trainer Internal</p>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mt-1">{{ $statistics['total_trainer'] }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
||||
<p class="text-xs font-bold text-slate-400 uppercase">SOP Aktif</p>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mt-1">{{ $statistics['total_sop'] }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
||||
<p class="text-xs font-bold text-slate-400 uppercase">Total Kelulusan</p>
|
||||
<h3 class="text-3xl font-bold text-emerald-600 mt-1">{{ $statistics['lulus_ujian'] }}</h3>
|
||||
<template x-for="trainee in activeTrainees" :key="trainee.attempt_id">
|
||||
<tr class="hover:bg-slate-800/50 transition-colors group">
|
||||
<td class="px-5 py-4">
|
||||
<div class="font-bold text-white" x-text="trainee.first_name + ' ' + (trainee.last_name || '')"></div>
|
||||
<div class="text-xs text-indigo-400 mt-0.5" x-text="trainee.dept_name"></div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="text-xs font-bold text-slate-300 truncate w-48" x-text="trainee.exam_title"></div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="font-mono text-xs font-bold px-2.5 py-1 rounded-md border"
|
||||
:class="trainee.time_left_seconds > 300 ? 'bg-emerald-900/50 text-emerald-400 border-emerald-800' : 'bg-red-900/50 text-red-400 border-red-800 animate-pulse'"
|
||||
x-text="formatTime(trainee.time_left_seconds)">
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-right">
|
||||
<button @click="forceSubmit(trainee.attempt_id)" class="opacity-0 group-hover:opacity-100 px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-[10px] font-bold rounded transition-all">
|
||||
FORCE SUBMIT
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 mb-4">Matriks Kepatuhan SOP Departemen</h3>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="p-5 border-b border-slate-100 flex justify-between items-center bg-red-50/30">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<h2 class="text-base font-bold text-slate-800">Training Terlewat (Overdue)</h2>
|
||||
</div>
|
||||
<span class="px-3 py-1 bg-red-100 text-red-700 text-xs font-bold rounded-full">{{ $totalOverdue }} Kasus Bulan Ini</span>
|
||||
</div>
|
||||
|
||||
@livewire('matrix.compliance-monitor')
|
||||
<div class="p-0">
|
||||
<table class="w-full text-left text-sm text-slate-600">
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse($overdueTrainings as $overdue)
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-5 py-3">
|
||||
<div class="font-bold text-slate-700">{{ $overdue->first_name }} {{ $overdue->last_name }}</div>
|
||||
<div class="text-[10px] uppercase font-bold text-slate-400 mt-0.5">{{ $overdue->dept_name }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-3 font-medium text-slate-600">{{ $overdue->title }}</td>
|
||||
<td class="px-5 py-3 text-right">
|
||||
<span class="text-xs font-bold text-red-500">
|
||||
Tenggat: {{ \Carbon\Carbon::parse($overdue->deadline_date)->format('d M Y') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-8 text-center text-emerald-600 font-bold bg-emerald-50/50">
|
||||
🎉 Luar biasa! Tidak ada training yang terlewat bulan ini.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden h-full flex flex-col">
|
||||
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-base font-bold text-slate-800">Pemenuhan Training Dept.</h2>
|
||||
<p class="text-[11px] text-slate-500 mt-0.5">Klik pada batang grafik untuk melihat detail</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex-grow relative w-full h-[320px] flex items-center justify-center">
|
||||
@if(count($deptFulfillment) > 0)
|
||||
<canvas id="deptFulfillmentChart"></canvas>
|
||||
@else
|
||||
<div class="text-center text-slate-400 text-sm flex flex-col items-center">
|
||||
<svg class="w-10 h-10 mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
|
||||
Belum ada data bulan ini
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="p-6 border-b border-slate-100 flex flex-col md:flex-row justify-between items-start md:items-center bg-slate-50 gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-slate-800">Kalender Penugasan Training</h2>
|
||||
<p class="text-sm text-slate-500">
|
||||
@if(auth()->user()->hasRole('superadmin'))
|
||||
Menampilkan jadwal training untuk <span class="font-bold text-blue-600">Seluruh Karyawan (Global)</span>.
|
||||
@else
|
||||
Menampilkan jadwal training khusus untuk departemen <span class="font-bold text-blue-600">{{ auth()->user()->department->name ?? 'Anda' }}</span>.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-xs font-bold text-slate-500">
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-emerald-500 mr-1.5"></span> Lulus</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-blue-500 mr-1.5"></span> Dikerjakan</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-amber-500 mr-1.5"></span> Pending</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-red-500 mr-1.5"></span> Terlewat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div id="trainingCalendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 1. Inisialisasi Alpine JS untuk Live Monitoring
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('liveMonitoring', () => ({
|
||||
activeTrainees: [],
|
||||
|
||||
fetchData() {
|
||||
// Perbaikan penulisan string URL agar aman
|
||||
fetch("{{ route('admin.api.live-exams') }}")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.activeTrainees = data;
|
||||
})
|
||||
.catch(error => console.error('Error fetching live data:', error));
|
||||
},
|
||||
|
||||
formatTime(seconds) {
|
||||
if (seconds <= 0) return "WAKTU HABIS";
|
||||
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
|
||||
const s = (seconds % 60).toString().padStart(2, '0');
|
||||
return m + ":" + s;
|
||||
},
|
||||
|
||||
forceSubmit(attemptId) {
|
||||
if(confirm('Peringatan: Anda akan memaksa menghentikan ujian karyawan ini secara sepihak. Lanjutkan?')) {
|
||||
alert('Sinyal penghentian ujian #' + attemptId + ' terkirim!');
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
// 2. Inisialisasi FullCalendar JS
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('trainingCalendar');
|
||||
|
||||
var eventsData = @json($calendarData ?? []);
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,listWeek'
|
||||
},
|
||||
events: eventsData,
|
||||
height: 650,
|
||||
eventClick: function(info) {
|
||||
alert('Detail Penugasan:\n' + info.event.title + '\nTenggat Waktu: ' + info.event.endStr.split('T')[0]);
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
|
||||
// 3. Inisialisasi Chart.js untuk Pemenuhan Departemen
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if(document.getElementById('deptFulfillmentChart')) {
|
||||
const ctx = document.getElementById('deptFulfillmentChart').getContext('2d');
|
||||
const deptData = @json($deptFulfillment ?? []);
|
||||
|
||||
const labels = deptData.map(d => d.name);
|
||||
const percentages = deptData.map(d => d.percentage);
|
||||
const ids = deptData.map(d => d.id);
|
||||
|
||||
const bgColors = percentages.map(p => {
|
||||
if (p < 50) return 'rgba(239, 68, 68, 0.8)';
|
||||
if (p < 80) return 'rgba(245, 158, 11, 0.8)';
|
||||
return 'rgba(16, 185, 129, 0.8)';
|
||||
});
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Persentase Lulus (%)',
|
||||
data: percentages,
|
||||
backgroundColor: bgColors,
|
||||
borderRadius: 6,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const index = context.dataIndex;
|
||||
return `Lulus: ${deptData[index].passed}/${deptData[index].assigned} (${context.raw}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { min: 0, max: 100, ticks: { stepSize: 20 } },
|
||||
y: { grid: { display: false } }
|
||||
},
|
||||
onClick: (e, activeElements) => {
|
||||
if (activeElements.length > 0) {
|
||||
const index = activeElements[0].index;
|
||||
const deptId = ids[index];
|
||||
const month = '{{ $selectedMonth }}';
|
||||
const year = '{{ $selectedYear }}';
|
||||
|
||||
window.location.href = `/admin/dashboard/department/${deptId}/fulfillment?month=${month}&year=${year}`;
|
||||
}
|
||||
},
|
||||
onHover: (event, chartElement) => {
|
||||
event.native.target.style.cursor = chartElement[0] ? 'pointer' : 'default';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,82 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||
|
||||
<div class="flex items-center space-x-4 mb-6">
|
||||
<a href="{{ route('admin.dashboard', ['month' => $selectedMonth, 'year' => $selectedYear]) }}" class="p-2 bg-slate-100 text-slate-500 rounded-xl hover:bg-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-800">Detail Pemenuhan: Dept. {{ $department->name }}</h1>
|
||||
<p class="text-sm text-slate-500">
|
||||
Data penugasan training periode <span class="font-bold text-slate-700">{{ date('F', mktime(0, 0, 0, $selectedMonth, 1)) }} {{ $selectedYear }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-sm text-slate-600">
|
||||
<thead class="bg-slate-50 border-b border-slate-200 text-slate-500 uppercase text-[11px] tracking-wider">
|
||||
<tr>
|
||||
<th class="px-6 py-4 font-bold">Karyawan</th>
|
||||
<th class="px-6 py-4 font-bold">Modul / SOP</th>
|
||||
<th class="px-6 py-4 font-bold">Tanggal Ditugaskan</th>
|
||||
<th class="px-6 py-4 font-bold">Batas Waktu (Deadline)</th>
|
||||
<th class="px-6 py-4 font-bold text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($assignments as $assign)
|
||||
@php
|
||||
$isOverdue = $assign->status !== 'passed' && \Carbon\Carbon::parse($assign->deadline_date)->isPast();
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-bold text-slate-800">{{ $assign->first_name }} {{ $assign->last_name }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1">NIK: {{ $assign->nik ?? '-' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-slate-700">
|
||||
{{ $assign->title }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ \Carbon\Carbon::parse($assign->created_at)->format('d M Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="{{ $isOverdue ? 'text-red-500 font-bold' : 'text-slate-500' }}">
|
||||
{{ \Carbon\Carbon::parse($assign->deadline_date)->format('d M Y, H:i') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($assign->status === 'passed')
|
||||
<span class="px-2.5 py-1 bg-emerald-100 text-emerald-700 rounded-lg text-[11px] font-bold border border-emerald-200">LULUS</span>
|
||||
@elseif($isOverdue)
|
||||
<span class="px-2.5 py-1 bg-red-100 text-red-700 rounded-lg text-[11px] font-bold border border-red-200 animate-pulse">OVERDUE</span>
|
||||
@elseif($assign->status === 'ongoing')
|
||||
<span class="px-2.5 py-1 bg-blue-100 text-blue-700 rounded-lg text-[11px] font-bold border border-blue-200">DIKERJAKAN</span>
|
||||
@else
|
||||
<span class="px-2.5 py-1 bg-amber-100 text-amber-700 rounded-lg text-[11px] font-bold border border-amber-200">PENDING</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-10 text-center text-slate-500">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<svg class="w-12 h-12 text-slate-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
|
||||
Tidak ada riwayat penugasan untuk departemen ini di bulan yang dipilih.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-slate-50 border-t border-slate-100">
|
||||
{{ $assignments->appends(request()->query())->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-500">Total Karyawan Aktif</p>
|
||||
<p class="text-2xl font-bold text-slate-900">{{ $department->users()->count() }} Orang</p>
|
||||
<p class="text-2xl font-bold text-slate-900">{{ $department->users->count() }} Orang</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,25 +104,39 @@
|
||||
<th class="px-6 py-3 font-semibold text-right">Opsi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 text-slate-700">
|
||||
@forelse($department->users as $user)
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-6 py-3 font-mono text-xs font-bold">{{ $user->nik ?? '-' }}</td>
|
||||
<td class="px-6 py-3">
|
||||
<div class="font-bold text-slate-900">{{ $user->first_name }} {{ $user->last_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ $user->email }}</div>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($department->users as $employee)
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-4 text-sm font-medium text-slate-600">
|
||||
{{ $employee->nik ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-3 text-sm">{{ $user->position->name ?? '-' }}</td>
|
||||
<td class="px-6 py-3 text-right">
|
||||
<a href="{{ route('admin.employees.show', $user->id) }}" class="text-indigo-600 hover:text-indigo-800 font-medium text-xs">Detail</a>
|
||||
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-bold text-slate-800">{{ $employee->first_name }} {{ $employee->last_name }}</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">{{ $employee->email }}</div>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 text-sm font-semibold text-indigo-600">
|
||||
{{ $employee->position->name ?? 'Belum ada jabatan' }}
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 text-sm text-right">
|
||||
<a href="{{ route('admin.employees.show', $employee->id) }}" class="inline-flex items-center px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 font-bold rounded-lg transition-colors">
|
||||
Detail Profil
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-8 text-center text-slate-500 italic">Belum ada karyawan di departemen ini.</td>
|
||||
<td colspan="4" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center justify-center text-slate-400">
|
||||
<svg class="w-12 h-12 mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
<p class="font-medium text-slate-500">Belum ada karyawan yang terdaftar di departemen ini.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* Styling Select2 Single (Materi/SOP) untuk Tailwind */
|
||||
.select2-container .select2-selection--single { height: 42px; border-radius: 0.75rem; border-color: #e2e8f0; background-color: #f8fafc; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 42px; font-size: 0.875rem; color: #334155; padding-left: 0.75rem; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
||||
|
||||
/* Styling Select2 Multiple (Pilih Karyawan Bulk) untuk Tailwind */
|
||||
.select2-container--default .select2-selection--multiple { min-height: 44px; border-radius: 0.75rem; border-color: #e2e8f0; background-color: #f8fafc; padding: 4px 8px; }
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple { border-color: #2563eb; ring: 2px; --tw-ring-color: rgba(37, 99, 235, 0.2); }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice { bg-color: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; font-weight: 700; font-size: 0.75rem; border-radius: 0.5rem; padding: 2px 8px; margin-top: 4px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { color: #3b82f6; margin-right: 6px; font-weight: bold; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { color: #1d4ed8; }
|
||||
|
||||
/* Global Dropdown Search Field */
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field { border-radius: 0.5rem; border-color: #cbd5e1; outline: none; padding: 0.5rem; }
|
||||
</style>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 py-8"
|
||||
x-data="{
|
||||
trainingType: 'matrix_plant',
|
||||
sops: [{ id: Date.now() }], // State untuk Dynamic Array
|
||||
|
||||
addSop() {
|
||||
this.sops.push({ id: Date.now() });
|
||||
// Panggil ulang Select2 untuk elemen yang baru dirender
|
||||
setTimeout(() => { initDynamicSelect2(); }, 50);
|
||||
},
|
||||
removeSop(id) {
|
||||
if(this.sops.length > 1) {
|
||||
this.sops = this.sops.filter(sop => sop.id !== id);
|
||||
}
|
||||
}
|
||||
}">
|
||||
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<a href="{{ route('admin.exams.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Assign Training & Ujian Baru</h2>
|
||||
<p class="text-sm text-slate-500">Tugaskan modul SOP ke dalam sesi Ujian (CBT).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('info'))
|
||||
<div class="mb-6 p-4 bg-blue-50 text-blue-700 rounded-xl border border-blue-100 text-sm font-bold flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
{{ session('info') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.exams.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-6">
|
||||
<label class="block text-base font-bold text-slate-800 mb-4">1. Pilih Jenis Training <span class="text-red-500">*</span></label>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<label class="flex items-center p-4 bg-slate-50 border rounded-xl cursor-pointer transition-all hover:bg-blue-50"
|
||||
:class="trainingType === 'matrix_plant' ? 'border-blue-500 ring-2 ring-blue-500/20' : 'border-slate-200'">
|
||||
<input type="radio" x-model="trainingType" name="training_type" value="matrix_plant" class="w-5 h-5 text-blue-600 focus:ring-blue-500">
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-bold text-slate-800">Training Matrix (Plant)</span>
|
||||
<span class="block text-xs text-slate-500 mt-0.5">SOP & Prosedur Teknis Pabrik</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center p-4 bg-slate-50 border rounded-xl cursor-pointer transition-all hover:bg-purple-50"
|
||||
:class="trainingType === 'general' ? 'border-purple-500 ring-2 ring-purple-500/20' : 'border-slate-200'">
|
||||
<input type="radio" x-model="trainingType" name="training_type" value="general" class="w-5 h-5 text-purple-600 focus:ring-purple-500">
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-bold text-slate-800">General (Non-Plant / Produk)</span>
|
||||
<span class="block text-xs text-slate-500 mt-0.5">Marketing, Sales, & Modul Umum</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center p-4 bg-slate-50 border rounded-xl cursor-pointer transition-all hover:bg-emerald-50"
|
||||
:class="trainingType === 'external' ? 'border-emerald-500 ring-2 ring-emerald-500/20' : 'border-slate-200'">
|
||||
<input type="radio" x-model="trainingType" name="training_type" value="external" class="w-5 h-5 text-emerald-600 focus:ring-emerald-500">
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-bold text-slate-800">External Training</span>
|
||||
<span class="block text-xs text-slate-500 mt-0.5">Pelatihan via Vendor / HR</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-6" x-show="trainingType !== 'external'">
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6">
|
||||
<div>
|
||||
<label class="block text-base font-bold text-slate-800">2. Konfigurasi Modul & Aturan CBT</label>
|
||||
<p class="text-sm text-slate-500">Anda dapat menugaskan banyak SOP sekaligus dalam satu kali simpan.</p>
|
||||
</div>
|
||||
<button type="button" @click="addSop()" class="mt-3 sm:mt-0 px-4 py-2 bg-indigo-50 text-indigo-700 font-bold rounded-xl border border-indigo-100 hover:bg-indigo-100 transition-colors flex items-center shadow-sm">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
Tambah Modul SOP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<template x-for="(sop, index) in sops" :key="sop.id">
|
||||
<div class="p-5 bg-slate-50 border border-slate-200 rounded-xl relative hover:border-blue-300 transition-colors">
|
||||
|
||||
<button type="button" x-show="sops.length > 1" @click="removeSop(sop.id)" class="absolute top-4 right-4 p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors" title="Hapus Modul">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-slate-200 text-slate-600 text-xs font-bold" x-text="index + 1"></span>
|
||||
<h4 class="text-sm font-bold text-slate-700" x-text="trainingType === 'matrix_plant' ? 'Dokumen SOP' : 'Subject / Modul'"></h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-5">
|
||||
|
||||
<div class="md:col-span-5">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Pilih Materi <span class="text-red-500">*</span></label>
|
||||
<select :name="'exams['+index+'][subject_id]'" required class="select2-dynamic w-full">
|
||||
<option value="">-- Ketik untuk mencari materi/SOP --</option>
|
||||
@foreach($subjects as $subject)
|
||||
<option value="{{ $subject->id }}">{{ $subject->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-7 md:pr-8">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Nama Quiz Khusus (Opsional)</label>
|
||||
<input type="text" :name="'exams['+index+'][title]'" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" placeholder="Kosongkan jika ingin mengikuti nama materi...">
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Durasi (Menit) <span class="text-red-500">*</span></label>
|
||||
<input type="number" :name="'exams['+index+'][duration_minutes]'" value="60" min="1" required class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Nilai KKM <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<input type="number" :name="'exams['+index+'][passing_grade]'" value="80" min="1" max="100" required class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
||||
<span class="absolute right-3 top-2.5 text-slate-400 text-xs font-bold">/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-6 md:pr-8">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Jatah Remedial <span class="text-red-500">*</span></label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="number" :name="'exams['+index+'][max_retakes]'" value="1" min="0" required class="w-24 px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all">
|
||||
<p class="text-[11px] leading-tight text-slate-500 font-medium">Kali. Jika melebihi jatah, <span class="text-red-500 font-bold">Ujian terkunci (Offline)</span>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-5 border-t border-slate-200">
|
||||
<label class="flex items-center cursor-pointer p-3 bg-indigo-50/50 border border-indigo-100 rounded-xl hover:bg-indigo-50 transition-colors w-max">
|
||||
<input type="checkbox" name="is_random_order" value="1" checked class="w-5 h-5 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500">
|
||||
<span class="ml-3 text-sm font-bold text-indigo-900">Acak Urutan Soal (Terapkan ke seluruh modul di atas)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-6" x-show="trainingType !== 'external'">
|
||||
<div class="mb-4 border-b border-slate-100 pb-4">
|
||||
<label class="block text-base font-bold text-slate-800">3. Pilih Peserta & Deadline Pelatihan</label>
|
||||
<p class="text-sm text-slate-500">Karyawan yang dipilih akan langsung menerima seluruh modul di atas pada dashboard mereka.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-2">Pilih Karyawan Pelatihan (Bisa Bulk / Banyak) <span class="text-red-500">*</span></label>
|
||||
<select name="employee_ids[]" id="employeeSelect" multiple="multiple" class="w-full" required>
|
||||
@foreach($employees as $emp)
|
||||
<option value="{{ $emp->id }}">
|
||||
{{ $emp->first_name }} {{ $emp->last_name }} - {{ $emp->nik ?? 'No NIK' }} ({{ $emp->position->name ?? '-' }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-2">Batas Akhir Penyelesaian (Deadline) <span class="text-red-500">*</span></label>
|
||||
<input type="datetime-local" name="deadline_date" required class="w-full px-4 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" style="height: 44px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="trainingType === 'external'" x-cloak class="bg-white p-10 rounded-2xl shadow-sm border border-slate-200 mb-6 text-center">
|
||||
<div class="w-20 h-20 bg-emerald-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-emerald-100">
|
||||
<svg class="w-10 h-10 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-800 mb-2">Modul External Training Segera Hadir</h3>
|
||||
<p class="text-slate-500 max-w-lg mx-auto">Sistem sedang menyiapkan integrasi dengan Database Vendor Pelatihan dari HRD (Mencakup Nama Vendor, Kategori Kompetensi, Sertifikat, dan JPL).</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{{ route('admin.exams.index') }}" class="px-6 py-3 text-sm font-bold text-slate-600 hover:bg-slate-200 bg-slate-100 rounded-xl transition-colors">Batal</a>
|
||||
<button type="submit" class="px-8 py-3 text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 rounded-xl shadow-md transition-all transform hover:-translate-y-0.5">
|
||||
Simpan & Distribusikan Training
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function initDynamicSelect2() {
|
||||
$('.select2-dynamic').each(function() {
|
||||
if ($(this).hasClass("select2-hidden-accessible")) {
|
||||
$(this).select2('destroy');
|
||||
}
|
||||
});
|
||||
|
||||
$('.select2-dynamic').select2({
|
||||
placeholder: "-- Ketik untuk mencari materi/SOP --",
|
||||
width: '100%',
|
||||
allowClear: true
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Inisialisasi awal dropdown dinamis SOP
|
||||
initDynamicSelect2();
|
||||
|
||||
// Inisialisasi dropdown multiple karyawan bulk
|
||||
$('#employeeSelect').select2({
|
||||
placeholder: "Ketik nama atau NIK karyawan...",
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
language: {
|
||||
noResults: function() { return "Karyawan tidak ditemukan"; }
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,86 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Manajemen Sesi Ujian (CBT)</h2>
|
||||
<a href="{{ route('admin.exams.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded-xl text-sm font-bold hover:bg-blue-700 shadow-sm transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
Buat Ujian / Assign Training
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="mb-6 p-4 bg-emerald-50 text-emerald-700 rounded-xl border border-emerald-100 text-sm font-bold flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-sm text-slate-600">
|
||||
<thead class="bg-slate-50 border-b border-slate-200 text-slate-500 uppercase text-xs tracking-wider">
|
||||
<tr>
|
||||
<th class="px-6 py-4 font-bold">Detail Ujian</th>
|
||||
<th class="px-6 py-4 font-bold">Materi & Jenis</th>
|
||||
<th class="px-6 py-4 font-bold text-center">Aturan</th>
|
||||
<th class="px-6 py-4 font-bold text-center">Status</th>
|
||||
<th class="px-6 py-4 font-bold text-right">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($exams as $exam)
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-bold text-slate-800 text-base">{{ $exam->title }}</div>
|
||||
<div class="text-xs text-slate-400 mt-1 flex items-center">
|
||||
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
{{ $exam->duration_minutes }} Menit
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-bold text-indigo-700">{{ $exam->subject->name ?? 'Materi Terhapus' }}</div>
|
||||
<div class="text-[11px] font-bold uppercase mt-1.5 px-2 py-0.5 rounded inline-block
|
||||
{{ $exam->training_type == 'matrix_plant' ? 'bg-amber-100 text-amber-700' : ($exam->training_type == 'product' ? 'bg-purple-100 text-purple-700' : 'bg-slate-100 text-slate-700') }}">
|
||||
{{ str_replace('_', ' ', $exam->training_type) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<div class="text-xs font-bold text-slate-700">KKM: <span class="text-emerald-600">{{ $exam->passing_grade }}</span></div>
|
||||
<div class="text-[10px] text-slate-500 mt-1">Remedial: {{ $exam->max_retakes }}x</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($exam->is_active)
|
||||
<span class="px-2 py-1 bg-emerald-50 text-emerald-600 rounded text-xs font-bold border border-emerald-200">Aktif</span>
|
||||
@else
|
||||
<span class="px-2 py-1 bg-red-50 text-red-600 rounded text-xs font-bold border border-red-200">Draft</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 flex justify-end space-x-2">
|
||||
<button class="p-2 text-indigo-500 hover:bg-indigo-50 rounded-lg transition-colors" title="Kelola Soal Ujian">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-8 text-center text-slate-500">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<svg class="w-12 h-12 text-slate-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg>
|
||||
Belum ada jadwal / sesi ujian.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-slate-100 bg-slate-50">
|
||||
{{ $exams->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -124,8 +124,9 @@
|
||||
</div>
|
||||
|
||||
<div x-show="questionType === 'descriptive'">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Panduan Jawaban Kunci (Opsional)</label>
|
||||
<textarea name="option_a" rows="3" class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm focus:ring-blue-500" placeholder="Tuliskan kata kunci poin yang harus ada dalam esai..."></textarea>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Keyword Jawaban (Pisahkan dengan koma)</label>
|
||||
<textarea name="essay_keywords" rows="3" class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm focus:ring-blue-500" placeholder="Contoh: mesin washing, suhu 80 derajat, steril">{{ old('essay_keywords') }}</textarea>
|
||||
<p class="text-[11px] text-slate-500 mt-2">* Sistem akan mendeteksi kata kunci ini pada teks jawaban peserta untuk memberikan nilai hitungan awal secara otomatis.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<style>
|
||||
.select2-container .select2-selection--single { height: 42px; border-radius: 0.75rem; border-color: #e2e8f0; background-color: #f8fafc; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 42px; font-size: 0.875rem; color: #334155; padding-left: 1rem; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
||||
/* Menambahkan warna border merah jika error */
|
||||
.has-error .select2-selection { border-color: #ef4444 !important; background-color: #fef2f2 !important; }
|
||||
</style>
|
||||
|
||||
<div id="exam-edit-container" class="max-w-5xl mx-auto px-4 py-8" x-data="{ questionType: '{{ old('question_type', $question->question_type ?? '') }}' }">
|
||||
<div class="mb-6 flex items-center space-x-3">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Edit Soal: #{{ $question->id }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<form action="{{ url('admin/exams/questions/'.$question->id) }}" method="POST" class="p-6 sm:p-8 space-y-6">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Subject / Materi <span class="text-red-500">*</span></label>
|
||||
<select name="subject_id" class="select2 w-full text-sm @error('subject_id') has-error @enderror" required>
|
||||
<option value="">-- Pilih Judul Materi --</option>
|
||||
@foreach($subjects as $subj)
|
||||
<option value="{{ $subj->id }}" {{ old('subject_id', $question->subject_id) == $subj->id ? 'selected' : '' }}>{{ $subj->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('subject_id') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Departemen</label>
|
||||
<select name="department_id" class="select2 w-full text-sm @error('department_id') has-error @enderror">
|
||||
<option value="">-- Global (Semua Dept) --</option>
|
||||
@foreach($departments as $dept)
|
||||
<option value="{{ $dept->id }}" {{ old('department_id', $question->department_id) == $dept->id ? 'selected' : '' }}>{{ $dept->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('department_id') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Jabatan / Posisi</label>
|
||||
<select name="position_id" class="select2 w-full text-sm @error('position_id') has-error @enderror">
|
||||
<option value="">-- Global (Semua Jabatan) --</option>
|
||||
@foreach($positions as $pos)
|
||||
<option value="{{ $pos->id }}" {{ old('position_id', $question->position_id) == $pos->id ? 'selected' : '' }}>{{ $pos->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('position_id') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tipe Soal <span class="text-red-500">*</span></label>
|
||||
<select name="question_type" class="select2 w-full text-sm @error('question_type') has-error @enderror" required>
|
||||
<option value="">-- Pilih Tipe --</option>
|
||||
<option value="single" {{ old('question_type', $question->question_type) == 'single' ? 'selected' : '' }}>Single Choice (1 Kunci)</option>
|
||||
<option value="multiple" {{ old('question_type', $question->question_type) == 'multiple' ? 'selected' : '' }}>Multiple Choice (Multi Kunci)</option>
|
||||
<option value="true_false" {{ old('question_type', $question->question_type) == 'true_false' ? 'selected' : '' }}>True / False</option>
|
||||
<option value="descriptive" {{ old('question_type', $question->question_type) == 'descriptive' ? 'selected' : '' }}>Descriptive (Esai)</option>
|
||||
</select>
|
||||
@error('question_type') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Level <span class="text-red-500">*</span></label>
|
||||
<select name="question_level" class="select2 w-full text-sm @error('question_level') has-error @enderror" required>
|
||||
<option value="mudah" {{ old('question_level', $question->question_level) == 'mudah' ? 'selected' : '' }}>Mudah</option>
|
||||
<option value="sedang" {{ old('question_level', $question->question_level) == 'sedang' ? 'selected' : '' }}>Sedang</option>
|
||||
<option value="sulit" {{ old('question_level', $question->question_level) == 'sulit' ? 'selected' : '' }}>Sulit</option>
|
||||
</select>
|
||||
@error('question_level') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Bobot Nilai (Score) <span class="text-red-500">*</span></label>
|
||||
<input type="number" name="score_weight" value="{{ old('score_weight', $question->score_weight ?? 1) }}" class="w-full px-3 py-2 bg-slate-50 border @error('score_weight') border-red-500 bg-red-50 @else border-slate-200 @enderror rounded-xl text-sm focus:ring-blue-500" min="1" required>
|
||||
@error('score_weight') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 lg:col-span-3">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Passing Grade</label>
|
||||
<input type="number" name="passing_grade" value="{{ old('passing_grade', $question->passing_grade ?? 0) }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-blue-500" min="0" max="100">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Durasi (Detik)</label>
|
||||
<input type="number" name="duration" value="{{ old('duration', $question->duration ?? 0) }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 pt-6">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Teks Pertanyaan <span class="text-red-500">*</span></label>
|
||||
<textarea name="question_text" rows="4" required class="w-full px-4 py-3 bg-slate-50 border @error('question_text') border-red-500 bg-red-50 @else border-slate-200 @enderror rounded-xl text-sm focus:bg-white focus:ring-2 focus:ring-blue-500/20" placeholder="Tuliskan pertanyaan di sini...">{{ old('question_text', $question->question_text) }}</textarea>
|
||||
@error('question_text') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 pt-6 bg-slate-50/50 p-6 rounded-xl mt-6 border" x-show="questionType !== ''" x-cloak>
|
||||
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4">Pengaturan Opsi & Kunci Jawaban</h3>
|
||||
|
||||
<div x-show="questionType === 'single'" class="space-y-3">
|
||||
@foreach(['A' => 'option_a', 'B' => 'option_b', 'C' => 'option_c', 'D' => 'option_d', 'E' => 'option_e'] as $key => $col)
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="radio" name="correct_answer" value="{{ $key }}" {{ old('correct_answer', $question->correct_answer) == $key ? 'checked' : '' }} class="w-5 h-5 text-blue-600 border-slate-300">
|
||||
<span class="font-bold text-slate-500">{{ $key }}.</span>
|
||||
<input type="text" name="{{ $col }}" value="{{ old($col, $question->$col) }}" placeholder="Opsi {{ $key }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-blue-500">
|
||||
</div>
|
||||
@endforeach
|
||||
<p class="text-xs text-slate-500 mt-2">* Klik radio button (bulatan) untuk menandai kunci jawaban yang benar.</p>
|
||||
</div>
|
||||
|
||||
<div x-show="questionType === 'multiple'" class="space-y-3">
|
||||
@php
|
||||
$correctAnswers = is_array($question->correct_answer) ? $question->correct_answer : json_decode($question->correct_answer, true) ?? explode(',', $question->correct_answer ?? '');
|
||||
@endphp
|
||||
|
||||
@foreach(['A' => 'option_a', 'B' => 'option_b', 'C' => 'option_c', 'D' => 'option_d', 'E' => 'option_e'] as $key => $col)
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" name="correct_answer[]" value="{{ $key }}" {{ in_array($key, (array) old('correct_answer', $correctAnswers)) ? 'checked' : '' }} class="w-5 h-5 text-blue-600 border-slate-300 rounded">
|
||||
<span class="font-bold text-slate-500">{{ $key }}.</span>
|
||||
<input type="text" name="{{ $col }}" value="{{ old($col, $question->$col) }}" placeholder="Opsi {{ $key }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-blue-500">
|
||||
</div>
|
||||
@endforeach
|
||||
<p class="text-xs text-slate-500 mt-2">* Centang kotak (checkbox) untuk menandai lebih dari satu kunci jawaban.</p>
|
||||
</div>
|
||||
|
||||
<div x-show="questionType === 'true_false'" class="flex gap-4">
|
||||
<label class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl cursor-pointer hover:border-emerald-500 hover:bg-emerald-50 transition-colors w-48">
|
||||
<input type="radio" name="correct_answer" value="True" {{ old('correct_answer', $question->correct_answer) == 'True' ? 'checked' : '' }} class="w-5 h-5 text-emerald-600 border-slate-300">
|
||||
<span class="font-bold text-slate-700">True (Benar)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-4 bg-white border border-slate-200 rounded-xl cursor-pointer hover:border-red-500 hover:bg-red-50 transition-colors w-48">
|
||||
<input type="radio" name="correct_answer" value="False" {{ old('correct_answer', $question->correct_answer) == 'False' ? 'checked' : '' }} class="w-5 h-5 text-red-600 border-slate-300">
|
||||
<span class="font-bold text-slate-700">False (Salah)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div x-show="questionType === 'descriptive'">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Keyword Jawaban (Pisahkan dengan koma)</label>
|
||||
<textarea name="essay_keywords" rows="3" class="w-full px-4 py-3 bg-white border @error('essay_keywords') border-red-500 bg-red-50 @else border-slate-200 @enderror rounded-xl text-sm focus:ring-blue-500" placeholder="Contoh: mesin washing, suhu 80 derajat, steril">{{ old('essay_keywords', $question->essay_keywords) }}</textarea>
|
||||
@error('essay_keywords') <p class="text-xs text-red-500 mt-1.5 font-medium">{{ $message }}</p> @enderror
|
||||
<p class="text-[11px] text-slate-500 mt-2">* Sistem akan mendeteksi kata kunci ini pada teks jawaban peserta untuk memberikan nilai hitungan awal secara otomatis.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="px-5 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl transition-colors">Batal</a>
|
||||
<button type="submit" class="px-6 py-2 text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 rounded-xl shadow-sm transition-colors">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Inisialisasi Select2
|
||||
$('.select2').select2({ width: '100%' });
|
||||
|
||||
// Memastikan AlpineJS selalu selaras ketika Select2 diubah
|
||||
$('select[name="question_type"]').on('change', function() {
|
||||
let container = document.getElementById('exam-edit-container');
|
||||
if(container) {
|
||||
Alpine.$data(container).questionType = $(this).val();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -3,7 +3,7 @@
|
||||
@section('content')
|
||||
<div class="max-w-3xl mx-auto px-4 py-8">
|
||||
<div class="mb-6 flex items-center space-x-3">
|
||||
<a href="{{ route('admin.exams.questions') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Import Bank Soal</h2>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 py-8" x-data="{ questionType: '{{ old('question_type', '') }}' }" @update-type.window="questionType = $event.detail">
|
||||
<div class="mb-6 flex items-center space-x-3">
|
||||
<a href="{{ route('admin.exams.questions') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Tambah Soal Baru</h2>
|
||||
@@ -126,7 +126,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100 mt-8">
|
||||
<a href="{{ route('admin.exams.questions') }}" class="px-5 py-2.5 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl">Batal</a>
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="px-5 py-2.5 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl">Batal</a>
|
||||
<button type="submit" class="px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-xl shadow-md">Simpan Soal</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 py-8" x-data="{ questionType: '{{ old('question_type', $question->question_type) }}' }" @update-type.window="questionType = $event.detail">
|
||||
<div class="mb-6 flex items-center space-x-3">
|
||||
<a href="{{ route('admin.exams.questions') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Edit Pertanyaan #{{ $question->id }}</h2>
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100 mt-8">
|
||||
<a href="{{ route('admin.exams.questions') }}" class="px-5 py-2.5 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl">Batal</a>
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="px-5 py-2.5 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl">Batal</a>
|
||||
<button type="submit" class="px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-xl shadow-md">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
@if(request('action') == 'export_excel')
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif;">
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="7" align="center" style="font-size: 16px; font-weight: bold; height: 30px; vertical-align: middle;">
|
||||
LAPORAN DATABASE BANK SOAL (CBT)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="7" align="center" style="font-size: 11px; height: 20px; vertical-align: top;">
|
||||
LMS TUNGGAL PHARMA
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="7"></td></tr> <tr>
|
||||
<td style="font-size: 11px; font-weight: bold;">Diunduh Oleh</td>
|
||||
<td style="font-size: 11px;">:</td>
|
||||
<td colspan="2" style="font-size: 11px;">{{ $exporterName }}</td>
|
||||
<td style="font-size: 11px; font-weight: bold;">Total Record</td>
|
||||
<td style="font-size: 11px;">:</td>
|
||||
<td style="font-size: 11px; font-weight: bold;">{{ count($exportData) }} Soal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size: 11px; font-weight: bold;">Waktu Unduh</td>
|
||||
<td style="font-size: 11px;">:</td>
|
||||
<td colspan="2" style="font-size: 11px;">{{ $exportTime }}</td>
|
||||
<td style="font-size: 11px; font-weight: bold;">Kriteria</td>
|
||||
<td style="font-size: 11px;">:</td>
|
||||
<td style="font-size: 11px;">Sesuai Filter Pencarian</td>
|
||||
</tr>
|
||||
<tr><td colspan="7"></td></tr> </table>
|
||||
|
||||
<table border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; height: 30px; vertical-align: middle;">NO</th>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; vertical-align: middle;">Q.ID</th>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; vertical-align: middle; width: 250px;">DEPARTEMEN & JABATAN</th>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; vertical-align: middle; width: 250px;">MATERI (SUBJECT)</th>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; vertical-align: middle; width: 150px;">TIPE & LEVEL</th>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; vertical-align: middle; width: 400px;">TEKS PERTANYAAN</th>
|
||||
<th style="background-color: #d1d5db; font-size: 11px; font-weight: bold; text-align: center; vertical-align: middle; width: 150px;">PEMBUAT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($exportData as $index => $q)
|
||||
<tr>
|
||||
<td align="center" style="font-size: 11px; vertical-align: top;">{{ $index + 1 }}</td>
|
||||
<td align="center" style="font-size: 11px; vertical-align: top;">{{ $q->id }}</td>
|
||||
<td style="font-size: 11px; vertical-align: top;">
|
||||
<strong>{{ $q->department->name ?? 'Semua Dept' }}</strong><br>
|
||||
{{ $q->position->name ?? 'Semua Jabatan' }}
|
||||
</td>
|
||||
<td style="font-size: 11px; vertical-align: top;">{{ $q->subject->name ?? 'Umum' }}</td>
|
||||
<td style="font-size: 11px; vertical-align: top;">
|
||||
<strong>{{ ucwords(str_replace('_', ' ', $q->question_type)) }}</strong><br>
|
||||
Level: {{ ucfirst($q->question_level) }}
|
||||
</td>
|
||||
<td style="font-size: 11px; vertical-align: top;">{{ strip_tags($q->question_text) }}</td>
|
||||
<td style="font-size: 11px; vertical-align: top;">{{ $q->creator->first_name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" align="center" style="font-size: 11px;">Tidak ada data soal pada kriteria ini.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@else
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>Export Data Bank Soal</title>
|
||||
<style>
|
||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #000; margin: 0; padding: 10px; }
|
||||
.header-title { text-align: center; font-size: 16px; font-weight: bold; margin-bottom: 5px; }
|
||||
.header-subtitle { text-align: center; font-size: 12px; margin-bottom: 20px; }
|
||||
.info-table { width: 100%; margin-bottom: 15px; font-size: 11px; }
|
||||
.info-table td { padding: 3px; }
|
||||
.data-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
.data-table th, .data-table td { border: 1px solid #000000; padding: 6px; vertical-align: top; }
|
||||
.data-table th { background-color: #d1d5db; font-weight: bold; text-align: center; text-transform: uppercase; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-title">LAPORAN DATABASE BANK SOAL (CBT)</div>
|
||||
<div class="header-subtitle">LMS TUNGGAL PHARMA</div>
|
||||
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td width="15%"><strong>Diunduh Oleh</strong></td>
|
||||
<td width="2%">:</td>
|
||||
<td width="33%">{{ $exporterName }}</td>
|
||||
<td width="15%"><strong>Total Record</strong></td>
|
||||
<td width="2%">:</td>
|
||||
<td width="33%"><strong>{{ count($exportData) }} Soal</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Waktu Unduh</strong></td>
|
||||
<td>:</td>
|
||||
<td>{{ $exportTime }}</td>
|
||||
<td><strong>Kriteria</strong></td>
|
||||
<td>:</td>
|
||||
<td>Sesuai Filter Pencarian</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="data-table" border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">NO</th>
|
||||
<th width="8%">Q.ID</th>
|
||||
<th width="18%">DEPARTEMEN & JABATAN</th>
|
||||
<th width="15%">MATERI (SUBJECT)</th>
|
||||
<th width="15%">TIPE & LEVEL</th>
|
||||
<th width="27%">TEKS PERTANYAAN</th>
|
||||
<th width="12%">PEMBUAT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($exportData as $index => $q)
|
||||
<tr>
|
||||
<td align="center">{{ $index + 1 }}</td>
|
||||
<td align="center">{{ $q->id }}</td>
|
||||
<td>
|
||||
<strong>{{ $q->department->name ?? 'Semua Dept' }}</strong><br>
|
||||
{{ $q->position->name ?? 'Semua Jabatan' }}
|
||||
</td>
|
||||
<td>{{ $q->subject->name ?? 'Umum' }}</td>
|
||||
<td>
|
||||
<strong>{{ ucwords(str_replace('_', ' ', $q->question_type)) }}</strong><br>
|
||||
Level: {{ ucfirst($q->question_level) }}
|
||||
</td>
|
||||
<td>{{ strip_tags($q->question_text) }}</td>
|
||||
<td>{{ $q->creator->first_name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" align="center" style="padding: 15px;">Tidak ada data soal pada kriteria ini.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@endif
|
||||
@@ -6,9 +6,9 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<style>
|
||||
.select2-container .select2-selection--single { height: 38px; border-radius: 0.5rem; border-color: #e2e8f0; background-color: #f8fafc; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 38px; font-size: 0.875rem; color: #334155; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
|
||||
.select2-container .select2-selection--single { height: 42px; border-radius: 0.5rem; border-color: #e2e8f0; background-color: #f8fafc; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 42px; font-size: 0.875rem; color: #334155; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; }
|
||||
</style>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 py-8"
|
||||
@@ -38,33 +38,37 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<form action="{{ url('admin/exams/questions/bulk-delete') }}" method="POST" x-show="selectedQuestions.length > 0" x-transition @submit="return confirmBulkDelete()">
|
||||
<form action="{{ route('admin.exams.questions.bulkDelete') }}" method="POST" x-show="selectedQuestions.length > 0" x-transition @submit="return confirmBulkDelete()">
|
||||
@csrf @method('DELETE')
|
||||
<template x-for="id in selectedQuestions" :key="id">
|
||||
<input type="hidden" name="question_ids[]" :value="id">
|
||||
</template>
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-red-50 text-red-600 border border-red-200 rounded-lg text-sm font-bold hover:bg-red-100 shadow-sm transition-all">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-red-50 text-red-600 border border-red-200 rounded-xl text-sm font-bold hover:bg-red-100 shadow-sm transition-all">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
Bulk Delete (<span x-text="selectedQuestions.length"></span>)
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ url('admin/exams/questions/import') }}" class="inline-flex items-center px-4 py-2 bg-slate-800 text-white rounded-lg text-sm font-semibold hover:bg-slate-900 shadow-sm transition-all">
|
||||
<a href="{{ route('admin.exams.questions.import') }}" class="inline-flex items-center px-4 py-2 bg-slate-800 text-white rounded-xl text-sm font-semibold hover:bg-slate-900 shadow-sm transition-all">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
|
||||
Import Soal
|
||||
</a>
|
||||
|
||||
<a href="{{ url('admin/exams/questions/create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-semibold hover:bg-blue-700 shadow-sm transition-all">
|
||||
<a href="{{ route('admin.exams.questions.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-xl text-sm font-semibold hover:bg-blue-700 shadow-sm transition-all">
|
||||
+ Tambah Soal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm mb-6">
|
||||
<form action="{{ route('admin.exams.questions') }}" method="GET">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-4">
|
||||
<div class="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm mb-6">
|
||||
|
||||
<form action="{{ route('admin.question-bank.index') }}" method="GET">
|
||||
|
||||
<button type="submit" class="hidden" aria-hidden="true"></button>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-5 mb-5">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Departemen</label>
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Departemen</label>
|
||||
<select name="department_id" class="select2 w-full text-sm">
|
||||
<option value="">Semua</option>
|
||||
@foreach($departments as $dept)
|
||||
@@ -74,7 +78,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Posisi / Jabatan</label>
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Posisi / Jabatan</label>
|
||||
<select name="position_id" class="select2 w-full text-sm">
|
||||
<option value="">Semua</option>
|
||||
@foreach($positions as $pos)
|
||||
@@ -84,7 +88,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Subject / Materi</label>
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Subject / Materi</label>
|
||||
<select name="subject_id" class="select2 w-full text-sm">
|
||||
<option value="">Semua Materi</option>
|
||||
@foreach($subjects as $subj)
|
||||
@@ -94,18 +98,18 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Tipe Soal</label>
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Tipe Soal</label>
|
||||
<select name="question_type" class="select2 w-full text-sm">
|
||||
<option value="">Semua</option>
|
||||
<option value="single" {{ request('question_type') == 'single' ? 'selected' : '' }}>Single Choice</option>
|
||||
<option value="multiple" {{ request('question_type') == 'multiple' ? 'selected' : '' }}>Multiple Choice</option>
|
||||
<option value="true_false" {{ request('question_type') == 'true_false' ? 'selected' : '' }}>True / False</option>
|
||||
<option value="descriptive" {{ request('question_type') == 'descriptive' ? 'selected' : '' }}>Deskriptif</option>
|
||||
<option value="essay" {{ request('question_type') == 'essay' ? 'selected' : '' }}>Esai / Deskriptif</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Level</label>
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Level</label>
|
||||
<select name="question_level" class="select2 w-full text-sm">
|
||||
<option value="">Semua</option>
|
||||
<option value="mudah" {{ request('question_level') == 'mudah' ? 'selected' : '' }}>Mudah (Easy)</option>
|
||||
@@ -115,8 +119,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Pembuat Soal</label>
|
||||
<select name="creator_id" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-blue-500" onchange="this.form.submit()">
|
||||
<label class="block text-[11px] font-bold text-slate-500 uppercase tracking-wide mb-1.5">Pembuat Soal</label>
|
||||
<select name="creator_id" class="w-full px-3 py-2.5 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-blue-500">
|
||||
<option value="">-- Semua Trainer --</option>
|
||||
@foreach($creators as $creator)
|
||||
<option value="{{ $creator->id }}" {{ request('creator_id') == $creator->id ? 'selected' : '' }}>
|
||||
@@ -125,17 +129,36 @@
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 items-center justify-between border-t border-slate-100 pt-4">
|
||||
<div class="w-full md:w-1/3">
|
||||
<input type="text" name="search" value="{{ request('search') }}" placeholder="Cari teks pertanyaan..." class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="flex gap-2 w-full md:w-auto">
|
||||
@if(request()->hasAny(['search', 'department_id', 'position_id', 'subject_id', 'question_type', 'question_level', 'created_by']))
|
||||
<a href="{{ route('admin.exams.questions') }}" class="px-5 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm font-semibold hover:bg-slate-200 transition-all text-center w-full md:w-auto">Reset</a>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 items-center justify-between border-t border-slate-100 pt-5 mt-2">
|
||||
|
||||
<div class="w-full flex-1 md:pr-10">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</div>
|
||||
<input type="text" name="search" value="{{ request('search') }}" placeholder="Ketik kata kunci untuk mencari di semua kolom (Teks Soal, Dept, Materi, ID, dll)..." class="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 w-full md:w-auto shrink-0">
|
||||
@if(request()->hasAny(['search', 'department_id', 'position_id', 'subject_id', 'question_type', 'question_level', 'creator_id']))
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="px-4 py-2.5 bg-slate-100 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-200 transition-all text-center" title="Reset Semua Filter">Reset</a>
|
||||
@endif
|
||||
<button type="submit" class="px-5 py-2 bg-blue-600 text-white rounded-lg text-sm font-semibold hover:bg-blue-700 transition-all text-center w-full md:w-auto">
|
||||
Terapkan Filter
|
||||
|
||||
<button type="submit" name="action" value="export_excel" class="px-4 py-2.5 bg-emerald-50 text-emerald-700 border border-emerald-200 rounded-xl text-sm font-bold hover:bg-emerald-100 transition-all text-center flex items-center" title="Download Excel">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> Excel
|
||||
</button>
|
||||
|
||||
<button type="submit" name="action" value="export_pdf" formtarget="_blank" class="px-4 py-2.5 bg-rose-50 text-rose-700 border border-rose-200 rounded-xl text-sm font-bold hover:bg-rose-100 transition-all text-center flex items-center" title="Cetak PDF">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path></svg> PDF
|
||||
</button>
|
||||
|
||||
<div class="border-l border-slate-200 mx-1"></div>
|
||||
|
||||
<button type="submit" class="px-6 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-bold hover:bg-blue-700 shadow-md transition-all transform hover:-translate-y-0.5 text-center flex items-center">
|
||||
Cari / Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,18 +168,18 @@
|
||||
<div class="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider border-b border-slate-200">
|
||||
<thead class="bg-slate-50 text-slate-600 text-[11px] uppercase tracking-wider border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="px-4 py-4 w-10 text-center">
|
||||
<input type="checkbox" x-model="selectAll" @change="toggleAll" class="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500 cursor-pointer">
|
||||
</th>
|
||||
<th class="px-4 py-4 font-semibold w-12">Q.ID</th>
|
||||
<th class="px-4 py-4 font-semibold w-1/5">Subject / Materi</th>
|
||||
<th class="px-4 py-4 font-semibold w-1/5">Penempatan</th>
|
||||
<th class="px-4 py-4 font-semibold">Tipe & Level</th>
|
||||
<th class="px-4 py-4 font-semibold w-1/3">Pertanyaan</th>
|
||||
<th class="px-4 py-4 font-semibold">Pembuat</th>
|
||||
<th class="px-4 py-4 font-semibold text-right">Aksi</th>
|
||||
<th class="px-4 py-4 font-bold w-12">Q.ID</th>
|
||||
<th class="px-4 py-4 font-bold w-1/5">Subject / Materi</th>
|
||||
<th class="px-4 py-4 font-bold w-1/5">Penempatan</th>
|
||||
<th class="px-4 py-4 font-bold">Tipe & Level</th>
|
||||
<th class="px-4 py-4 font-bold w-1/3">Pertanyaan</th>
|
||||
<th class="px-4 py-4 font-bold">Pembuat</th>
|
||||
<th class="px-4 py-4 font-bold text-right">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 text-slate-700">
|
||||
@@ -195,9 +218,9 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right space-x-2 whitespace-nowrap">
|
||||
<a href="{{ url('admin/exams/questions/'.$q->id) }}" class="text-slate-400 hover:text-blue-600"><svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg></a>
|
||||
<a href="{{ url('admin/exams/questions/'.$q->id.'/edit') }}" class="text-slate-400 hover:text-amber-500"><svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg></a>
|
||||
<form action="{{ url('admin/exams/questions/'.$q->id) }}" method="POST" class="inline-block" onsubmit="return confirm('Hapus soal ini secara permanen?');">
|
||||
<a href="{{ route('admin.exams.questions.show', $q->id) }}" class="text-slate-400 hover:text-blue-600"><svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg></a>
|
||||
<a href="{{ route('admin.exams.questions.edit', $q->id) }}" class="text-slate-400 hover:text-amber-500"><svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg></a>
|
||||
<form action="{{ route('admin.exams.questions.destroy', $q->id) }}" method="POST" class="inline-block" onsubmit="return confirm('Hapus soal ini secara permanen?');">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-slate-400 hover:text-red-500"><svg class="w-5 h-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></button>
|
||||
</form>
|
||||
@@ -206,7 +229,10 @@
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-12 text-center text-slate-500 font-medium">
|
||||
Belum ada data soal yang sesuai kriteria filter.
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<svg class="w-12 h-12 text-slate-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
Tidak ada data soal yang sesuai dengan kriteria pencarian/filter.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<a href="{{ route('admin.exams.questions') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Pratinjau Soal</h2>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<span class="text-sm font-medium">Dokumen SOP</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.exams.questions') }}" class="flex items-center px-3 py-2.5 rounded-lg transition-colors {{ request()->routeIs('admin.exams.questions') ? 'bg-indigo-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white' }}">
|
||||
<a href="{{ route('admin.question-bank.index') }}" class="flex items-center px-3 py-2.5 rounded-lg transition-colors {{ request()->routeIs('admin.exams.questions') ? 'bg-indigo-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
<span class="text-sm font-medium">Bank Soal</span>
|
||||
</a>
|
||||
|
||||
+28
-22
@@ -12,6 +12,7 @@ use App\Http\Controllers\Admin\ReportController;
|
||||
use App\Http\Controllers\Admin\DepartmentController;
|
||||
use App\Http\Controllers\Admin\PositionController;
|
||||
use App\Http\Controllers\Admin\EmployeeController;
|
||||
use App\Http\Controllers\Admin\SubjectController;
|
||||
use App\Http\Controllers\Cbt\UserDashboardController;
|
||||
|
||||
/*
|
||||
@@ -82,56 +83,61 @@ Route::middleware(['auth'])->group(function () {
|
||||
|
||||
// Dashboard Utama Admin
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
Route::get('/api/live-exams', [DashboardController::class, 'getLiveExams'])->name('api.live-exams');
|
||||
|
||||
// Modul Ujian & Bank Soal
|
||||
Route::get('/question-bank', [ExamManagementController::class, 'questionBank'])->name('exams.questions');
|
||||
// DETAIL GRAFIK DEPARTEMEN
|
||||
Route::get('/dashboard/department/{id}/fulfillment', [DashboardController::class, 'departmentFulfillment'])->name('dashboard.dept-detail');
|
||||
|
||||
// ================================================================
|
||||
// PENAMBAHAN ROUTE UNTUK CRUD BANK SOAL
|
||||
// Note: Route statis harus di atas route dinamis ({id})
|
||||
// MODUL 1: BANK SOAL (QUESTIONS)
|
||||
// ================================================================
|
||||
Route::get('/question-bank', [ExamManagementController::class, 'questionBank'])->name('question-bank.index');
|
||||
Route::delete('exams/questions/bulk-delete', [ExamManagementController::class, 'bulkDelete'])->name('exams.questions.bulkDelete');
|
||||
Route::get('exams/questions/import', [ExamManagementController::class, 'importView'])->name('exams.questions.import');
|
||||
Route::post('exams/questions/import', [ExamManagementController::class, 'importProcess']);
|
||||
|
||||
Route::get('exams/questions/create', [ExamManagementController::class, 'create'])->name('exams.questions.create');
|
||||
Route::post('exams/questions', [ExamManagementController::class, 'store'])->name('exams.questions.store');
|
||||
Route::get('exams/questions/{id}', [ExamManagementController::class, 'show'])->name('exams.questions.show');
|
||||
Route::get('exams/questions/{id}/edit', [ExamManagementController::class, 'edit'])->name('exams.questions.edit');
|
||||
Route::put('exams/questions/{id}', [ExamManagementController::class, 'update'])->name('exams.questions.update');
|
||||
Route::delete('exams/questions/{id}', [ExamManagementController::class, 'destroy'])->name('exams.questions.destroy');
|
||||
// PERBAIKAN: Rute 'create' WAJIB ditaruh di ATAS rute '{id}'
|
||||
Route::get('exams/questions/create', [ExamManagementController::class, 'createQuestion'])->name('exams.questions.create');
|
||||
Route::post('exams/questions', [ExamManagementController::class, 'storeQuestion'])->name('exams.questions.store');
|
||||
|
||||
// Rute yang mengandung variabel dinamis {id} ditaruh paling bawah
|
||||
Route::get('exams/questions/{id}/edit', [ExamManagementController::class, 'editQuestion'])->name('exams.questions.edit');
|
||||
Route::put('exams/questions/{id}', [ExamManagementController::class, 'updateQuestion'])->name('exams.questions.update');
|
||||
Route::delete('exams/questions/{id}', [ExamManagementController::class, 'destroyQuestion'])->name('exams.questions.destroy');
|
||||
Route::get('exams/questions/{id}', [ExamManagementController::class, 'showQuestion'])->name('exams.questions.show');
|
||||
|
||||
// ================================================================
|
||||
// MODUL 2: MANAJEMEN SESI UJIAN (CBT EXAMS)
|
||||
// ================================================================
|
||||
|
||||
Route::get('/exams', [ExamManagementController::class, 'index'])->name('exams.index');
|
||||
Route::get('/exams/create', [ExamManagementController::class, 'create'])->name('exams.create');
|
||||
Route::post('/exams', [ExamManagementController::class, 'store'])->name('exams.store');
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PERBAIKAN: Rute Export & Import Karyawan (Disesuaikan dengan Prefix Grup)
|
||||
// ----------------------------------------------------------------
|
||||
// ================================================================
|
||||
// MODUL 3: EXPORT & IMPORT DATA (KARYAWAN & MASTER DATA)
|
||||
// Resource diletakkan di bawah custom route agar tidak ditimpa
|
||||
// ================================================================
|
||||
Route::get('employees/export/excel', [EmployeeController::class, 'exportExcel'])->name('employees.export.excel');
|
||||
Route::get('employees/export/pdf', [EmployeeController::class, 'exportPdf'])->name('employees.export.pdf');
|
||||
|
||||
Route::get('employees/import', [EmployeeController::class, 'importView'])->name('employees.import');
|
||||
Route::get('employees/import/template', [EmployeeController::class, 'downloadTemplate'])->name('employees.import.template');
|
||||
Route::post('employees/import/preview', [EmployeeController::class, 'importPreview'])->name('employees.import.preview');
|
||||
Route::post('employees/import/process', [EmployeeController::class, 'importProcess'])->name('employees.import.process');
|
||||
|
||||
// Rute Export Master Data
|
||||
Route::get('departments/export/excel', [DepartmentController::class, 'exportExcel'])->name('departments.export.excel');
|
||||
Route::get('departments/export/pdf', [DepartmentController::class, 'exportPdf'])->name('departments.export.pdf');
|
||||
|
||||
Route::get('positions/export/excel', [PositionController::class, 'exportExcel'])->name('positions.export.excel');
|
||||
Route::get('positions/export/pdf', [PositionController::class, 'exportPdf'])->name('positions.export.pdf');
|
||||
|
||||
// Master Data & Karyawan (Resources harus diletakkan di bawah rute custom export/import)
|
||||
Route::resource('departments', DepartmentController::class);
|
||||
Route::resource('positions', PositionController::class);
|
||||
Route::resource('employees', EmployeeController::class);
|
||||
Route::resource('subjects', SubjectController::class);
|
||||
|
||||
//Subject
|
||||
Route::resource('subjects', App\Http\Controllers\Admin\SubjectController::class);
|
||||
|
||||
// ================================================================
|
||||
// MODUL 4: MANAJEMEN DOKUMEN SOP
|
||||
// ================================================================
|
||||
Route::get('/sops/open/{id}', [DashboardController::class, 'openSopFile'])->name('sops.open');
|
||||
|
||||
Route::get('/sops', [DashboardController::class, 'sopIndex'])->name('sops.index');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user