Perbaikan modul data karyawan dan staff + modul SOP import
This commit is contained in:
@@ -22,11 +22,11 @@ class MigrateLmsData extends Command
|
|||||||
$this->migrateDepartmentPositions();
|
$this->migrateDepartmentPositions();
|
||||||
|
|
||||||
// 2. Eksekusi Data User
|
// 2. Eksekusi Data User
|
||||||
$this->migrateStaffToUsers();
|
// $this->migrateStaffToUsers();
|
||||||
$this->migrateStudentsToUsers();
|
// $this->migrateStudentsToUsers();
|
||||||
|
|
||||||
// 3. Sinkronisasi Departemen & Posisi ke User
|
// 3. Sinkronisasi Departemen & Posisi ke User
|
||||||
$this->syncUserDepartmentAndPosition();
|
// $this->syncUserDepartmentAndPosition();
|
||||||
|
|
||||||
// 4. Eksekusi Ujian & Hasil
|
// 4. Eksekusi Ujian & Hasil
|
||||||
$this->migrateQuestions();
|
$this->migrateQuestions();
|
||||||
@@ -205,30 +205,79 @@ class MigrateLmsData extends Command
|
|||||||
/**
|
/**
|
||||||
* TAHAP 4A: Memindahkan data Bank Soal
|
* TAHAP 4A: Memindahkan data Bank Soal
|
||||||
*/
|
*/
|
||||||
private function migrateQuestions()
|
protected function migrateQuestions()
|
||||||
{
|
{
|
||||||
$this->warn('4A. Menarik data Bank Soal...');
|
$this->info('4A. Menarik data Bank Soal...');
|
||||||
$oldQuestions = DB::connection('mysql_old')->table('questions')->get();
|
|
||||||
|
// Pastikan Master Subject ditarik terlebih dahulu agar tidak ada yang terlewat
|
||||||
|
$oldSubjects = DB::table('lmsv2-old.subjects')->get();
|
||||||
|
foreach ($oldSubjects as $os) {
|
||||||
|
\App\Models\Subject::updateOrCreate(
|
||||||
|
['id' => $os->id],
|
||||||
|
['name' => $os->name ?? $os->title ?? 'Materi ' . $os->id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldQuestions = DB::table('lmsv2-old.questions')->get();
|
||||||
|
|
||||||
|
if ($oldQuestions->isEmpty()) {
|
||||||
|
$this->warn('Tidak ada data soal ditemukan.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$bar = $this->output->createProgressBar(count($oldQuestions));
|
$bar = $this->output->createProgressBar(count($oldQuestions));
|
||||||
$bar->start();
|
$bar->start();
|
||||||
|
|
||||||
foreach ($oldQuestions as $q) {
|
foreach ($oldQuestions as $old) {
|
||||||
DB::table('question_banks')->updateOrInsert(
|
// Mapping Tipe
|
||||||
['old_id' => $q->id],
|
$type = 'single';
|
||||||
|
$oldType = strtolower($old->question_type ?? '');
|
||||||
|
if (str_contains($oldType, 'multiple')) { $type = 'multiple'; }
|
||||||
|
elseif (str_contains($oldType, 'true') || str_contains($oldType, 'boolean')) { $type = 'true_false'; }
|
||||||
|
elseif (str_contains($oldType, 'desc') || str_contains($oldType, 'essay')) { $type = 'descriptive'; }
|
||||||
|
|
||||||
|
// Mapping Level
|
||||||
|
$level = 'sedang';
|
||||||
|
$oldLevel = strtolower($old->level ?? '');
|
||||||
|
if (str_contains($oldLevel, 'mudah') || str_contains($oldLevel, 'easy')) { $level = 'mudah'; }
|
||||||
|
elseif (str_contains($oldLevel, 'sulit') || str_contains($oldLevel, 'hard')) { $level = 'sulit'; }
|
||||||
|
|
||||||
|
// Mapping ID (Departemen, Posisi, Subject, Creator)
|
||||||
|
$deptId = (!empty($old->section_id) && $old->section_id != 0) ? $old->section_id : null;
|
||||||
|
$posId = (!empty($old->class_id) && $old->class_id != 0) ? $old->class_id : null;
|
||||||
|
$subjId = (!empty($old->subject_id) && $old->subject_id != 0) ? $old->subject_id : null;
|
||||||
|
$creatorId = (!empty($old->staff_id) && $old->staff_id != 0) ? $old->staff_id : null;
|
||||||
|
|
||||||
|
$correctAnswer = !empty($old->correct) ? [$old->correct] : null;
|
||||||
|
|
||||||
|
// Menggunakan Model agar array di-cast otomatis menjadi format yang benar
|
||||||
|
\App\Models\QuestionBank::updateOrCreate(
|
||||||
|
['old_id' => $old->id],
|
||||||
[
|
[
|
||||||
'question_text' => $q->question,
|
'department_id' => $deptId,
|
||||||
'option_a' => $q->opt_a,
|
'position_id' => $posId,
|
||||||
'option_b' => $q->opt_b,
|
'subject_id' => $subjId,
|
||||||
'option_c' => $q->opt_c,
|
'question_type' => $type,
|
||||||
'option_d' => $q->opt_d,
|
'question_level' => $level,
|
||||||
'option_e' => $q->opt_e,
|
'question_text' => $old->question,
|
||||||
'correct_answer' => $q->correct,
|
'option_a' => $old->opt_a,
|
||||||
|
'option_b' => $old->opt_b,
|
||||||
|
'option_c' => $old->opt_c,
|
||||||
|
'option_d' => $old->opt_d,
|
||||||
|
'option_e' => $old->opt_e,
|
||||||
|
'correct_answer' => $correctAnswer,
|
||||||
|
'created_by' => $creatorId,
|
||||||
|
'created_at' => $old->created_at ?? now(),
|
||||||
|
'updated_at' => $old->updated_at ?? now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$bar->advance();
|
$bar->advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
$bar->finish();
|
$bar->finish();
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
|
$this->info('Bank Soal berhasil ditarik!');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,17 +5,30 @@ namespace App\Console\Commands;
|
|||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use App\Models\QuestionBank;
|
use App\Models\QuestionBank;
|
||||||
use App\Models\User;
|
use App\Models\Subject;
|
||||||
|
|
||||||
class MigrateOldQuestions extends Command
|
class MigrateOldQuestions extends Command
|
||||||
{
|
{
|
||||||
|
// Ini adalah 'kunci' agar command muncul di php artisan list
|
||||||
protected $signature = 'lms:migrate-questions';
|
protected $signature = 'lms:migrate-questions';
|
||||||
protected $description = 'Menyalin data soal dari lmsv2-old.questions ke lmsv2.question_banks secara presisi';
|
protected $description = 'Menyalin data soal & subject dari lmsv2-old ke lmsv2 secara presisi';
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$this->info('Memulai Sinkronisasi Bank Soal dari Database Lama...');
|
$this->info('Memulai Sinkronisasi Master Subject & Bank Soal...');
|
||||||
|
|
||||||
|
// --- 1. SINKRONISASI MASTER SUBJECT ---
|
||||||
|
$this->info('Menarik data Subject...');
|
||||||
|
$oldSubjects = DB::table('lmsv2-old.subjects')->get();
|
||||||
|
foreach ($oldSubjects as $os) {
|
||||||
|
Subject::updateOrCreate(
|
||||||
|
['id' => $os->id],
|
||||||
|
['name' => $os->name ?? $os->title ?? 'Materi ' . $os->id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. SINKRONISASI BANK SOAL ---
|
||||||
|
$this->info('Menarik data Bank Soal...');
|
||||||
$oldQuestions = DB::table('lmsv2-old.questions')->get();
|
$oldQuestions = DB::table('lmsv2-old.questions')->get();
|
||||||
|
|
||||||
if ($oldQuestions->isEmpty()) {
|
if ($oldQuestions->isEmpty()) {
|
||||||
@@ -30,53 +43,41 @@ class MigrateOldQuestions extends Command
|
|||||||
|
|
||||||
foreach ($oldQuestions as $old) {
|
foreach ($oldQuestions as $old) {
|
||||||
|
|
||||||
// 1. Pemetaan Tipe Soal
|
// Pemetaan Tipe & Level
|
||||||
$type = 'single';
|
$type = 'single';
|
||||||
$oldType = strtolower($old->question_type ?? '');
|
$oldType = strtolower($old->question_type ?? '');
|
||||||
if (str_contains($oldType, 'multiple')) { $type = 'multiple'; }
|
if (str_contains($oldType, 'multiple')) { $type = 'multiple'; }
|
||||||
elseif (str_contains($oldType, 'true') || str_contains($oldType, 'boolean')) { $type = 'true_false'; }
|
elseif (str_contains($oldType, 'true') || str_contains($oldType, 'boolean')) { $type = 'true_false'; }
|
||||||
elseif (str_contains($oldType, 'desc') || str_contains($oldType, 'essay')) { $type = 'descriptive'; }
|
elseif (str_contains($oldType, 'desc') || str_contains($oldType, 'essay')) { $type = 'descriptive'; }
|
||||||
|
|
||||||
// 2. Pemetaan Level
|
|
||||||
$level = 'sedang';
|
$level = 'sedang';
|
||||||
$oldLevel = strtolower($old->level ?? '');
|
$oldLevel = strtolower($old->level ?? '');
|
||||||
if (str_contains($oldLevel, 'mudah') || str_contains($oldLevel, 'easy')) { $level = 'mudah'; }
|
if (str_contains($oldLevel, 'mudah') || str_contains($oldLevel, 'easy')) { $level = 'mudah'; }
|
||||||
elseif (str_contains($oldLevel, 'sulit') || str_contains($oldLevel, 'hard')) { $level = 'sulit'; }
|
elseif (str_contains($oldLevel, 'sulit') || str_contains($oldLevel, 'hard')) { $level = 'sulit'; }
|
||||||
|
|
||||||
// 3. Pemetaan Pembuat (Creator)
|
// Pemetaan ID (Lama ke Baru)
|
||||||
$creatorId = null;
|
$deptId = (!empty($old->section_id) && $old->section_id != 0) ? $old->section_id : null;
|
||||||
if (isset($old->staff_id)) {
|
$posId = (!empty($old->class_id) && $old->class_id != 0) ? $old->class_id : null;
|
||||||
$oldStaff = DB::table('lmsv2-old.staff')->where('id', $old->staff_id)->first();
|
$subjId = (!empty($old->subject_id) && $old->subject_id != 0) ? $old->subject_id : null;
|
||||||
if ($oldStaff && !empty($oldStaff->email)) {
|
$creatorId = (!empty($old->staff_id) && $old->staff_id != 0) ? $old->staff_id : null;
|
||||||
$user = User::where('email', $oldStaff->email)->first();
|
|
||||||
if ($user) $creatorId = $user->id;
|
$correctAnswer = !empty($old->correct) ? [$old->correct] : null;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Penanganan Kunci Jawaban (Karena di Model di-cast sebagai Array)
|
|
||||||
$correctAnswer = null;
|
|
||||||
if (!empty($old->correct)) {
|
|
||||||
// Dibungkus ke dalam array agar tidak error saat Laravel mencoba json_encode
|
|
||||||
$correctAnswer = [$old->correct];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Simpan menggunakan format nama kolom yang BARU
|
|
||||||
QuestionBank::updateOrCreate(
|
QuestionBank::updateOrCreate(
|
||||||
['old_id' => $old->id], // Pencocokan menggunakan old_id
|
['old_id' => $old->id],
|
||||||
[
|
[
|
||||||
|
'department_id' => $deptId,
|
||||||
|
'position_id' => $posId,
|
||||||
|
'subject_id' => $subjId,
|
||||||
'question_type' => $type,
|
'question_type' => $type,
|
||||||
'question_level' => $level,
|
'question_level' => $level,
|
||||||
'question_text' => $old->question,
|
'question_text' => $old->question,
|
||||||
|
|
||||||
// Opsi A sampai E
|
|
||||||
'option_a' => $old->opt_a,
|
'option_a' => $old->opt_a,
|
||||||
'option_b' => $old->opt_b,
|
'option_b' => $old->opt_b,
|
||||||
'option_c' => $old->opt_c,
|
'option_c' => $old->opt_c,
|
||||||
'option_d' => $old->opt_d,
|
'option_d' => $old->opt_d,
|
||||||
'option_e' => $old->opt_e,
|
'option_e' => $old->opt_e,
|
||||||
|
'correct_answer' => $correctAnswer,
|
||||||
'correct_answer' => $correctAnswer, // Sudah dalam bentuk array
|
|
||||||
|
|
||||||
'created_by' => $creatorId,
|
'created_by' => $creatorId,
|
||||||
'created_at' => $old->created_at ?? now(),
|
'created_at' => $old->created_at ?? now(),
|
||||||
'updated_at' => $old->updated_at ?? now(),
|
'updated_at' => $old->updated_at ?? now(),
|
||||||
@@ -89,6 +90,6 @@ class MigrateOldQuestions extends Command
|
|||||||
|
|
||||||
$bar->finish();
|
$bar->finish();
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
$this->info("Sinkronisasi Selesai! Berhasil memindahkan {$successCount} Soal beserta kunci jawabannya.");
|
$this->info("Sinkronisasi Selesai! Berhasil memperbarui {$successCount} Soal.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use App\Models\User;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class MigrateUsers extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'lms:migrate-users';
|
||||||
|
protected $description = 'Menggabungkan 65 Staff & 810 Students menjadi tabel Users terpadu dengan Role dinamis.';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// 1. Pastikan Role Tersedia
|
||||||
|
Role::firstOrCreate(['name' => 'trainer', 'guard_name' => 'web']);
|
||||||
|
Role::firstOrCreate(['name' => 'trainee', 'guard_name' => 'web']);
|
||||||
|
|
||||||
|
$this->info('1. Memproses 65 Data Staff (sebagai Trainer)...');
|
||||||
|
$staffs = DB::table('lmsv2-old.staff')->get();
|
||||||
|
|
||||||
|
foreach ($staffs as $staff) {
|
||||||
|
$email = !empty($staff->email) ? trim($staff->email) : 'staff_' . $staff->id . '@noemail.com';
|
||||||
|
|
||||||
|
// Gunakan email sebagai kunci penyatuan
|
||||||
|
$user = User::updateOrCreate(
|
||||||
|
['email' => $email],
|
||||||
|
[
|
||||||
|
// Gunakan 'name' dan 'surname' sesuai tabel staff lama
|
||||||
|
'first_name' => !empty($staff->name) ? trim($staff->name) : 'Staff ' . $staff->id,
|
||||||
|
'last_name' => !empty($staff->surname) ? trim($staff->surname) : '',
|
||||||
|
'password' => User::where('email', $email)->exists() ? User::where('email', $email)->value('password') : (!empty($staff->password) ? $staff->password : Hash::make('rahasia1234')),
|
||||||
|
'is_active' => $staff->status ?? 1,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$user->assignRole('trainer');
|
||||||
|
}
|
||||||
|
$this->info('Data Staff berhasil diproses!');
|
||||||
|
|
||||||
|
$this->info('2. Memproses 810 Data Students (sebagai Trainee)...');
|
||||||
|
$students = DB::table('lmsv2-old.students')->get();
|
||||||
|
$bar = $this->output->createProgressBar(count($students));
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
foreach ($students as $student) {
|
||||||
|
$email = !empty($student->email) ? trim($student->email) : 'student_' . $student->id . '@noemail.com';
|
||||||
|
|
||||||
|
$user = User::updateOrCreate(
|
||||||
|
['email' => $email],
|
||||||
|
[
|
||||||
|
// Gunakan 'firstname' dan 'lastname' TANPA garis bawah
|
||||||
|
'first_name' => !empty($student->firstname) ? trim($student->firstname) : 'Student ' . $student->id,
|
||||||
|
'last_name' => !empty($student->lastname) ? trim($student->lastname) : '',
|
||||||
|
'old_id' => $student->id,
|
||||||
|
'password' => User::where('email', $email)->exists() ? User::where('email', $email)->value('password') : (!empty($student->password) ? $student->password : Hash::make('rahasia1234')),
|
||||||
|
'is_active' => $student->status ?? 1,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Jika akun ini sebelumnya sudah jadi Staff (Trainer), ia akan mendapatkan Role Trainee JUGA! (Rangkap 2)
|
||||||
|
$user->assignRole('trainee');
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$total = User::count();
|
||||||
|
$this->info("Migrasi Selesai! Total unik akun di sistem saat ini: $total User.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,6 @@ class DashboardController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function sopIndex(): View
|
public function sopIndex(): View
|
||||||
{
|
{
|
||||||
return view('pages.admin.sops.index');
|
return view('pages.admin.sop.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,32 +6,26 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\Department;
|
use App\Models\Department;
|
||||||
use App\Models\Position;
|
use App\Models\Position;
|
||||||
use App\Models\TrainingMatrix;
|
use App\Models\Subject; // PASTIKAN SUBJECT DI-IMPORT
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\QuestionBank;
|
use App\Models\QuestionBank;
|
||||||
|
|
||||||
class ExamManagementController extends Controller
|
class ExamManagementController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Menampilkan halaman Bank Soal dengan Filter Komprehensif
|
|
||||||
*/
|
|
||||||
public function questionBank(Request $request)
|
public function questionBank(Request $request)
|
||||||
{
|
{
|
||||||
// 1. Siapkan Data untuk Dropdown Filter
|
|
||||||
$departments = Department::orderBy('name', 'asc')->get();
|
$departments = Department::orderBy('name', 'asc')->get();
|
||||||
$positions = Position::orderBy('name', 'asc')->get();
|
$positions = Position::orderBy('name', 'asc')->get();
|
||||||
// $matrices = TrainingMatrix::orderBy('title', 'asc')->get();
|
$subjects = Subject::orderBy('name', 'asc')->get(); // Mengambil data Subject
|
||||||
|
|
||||||
// Ambil daftar user yang pernah membuat soal (untuk filter 'Pembuat')
|
|
||||||
// Sesuaikan nama relasi/kolom jika berbeda
|
|
||||||
$creators = User::whereHas('roles', function($q){
|
$creators = User::whereHas('roles', function($q){
|
||||||
$q->whereIn('name', ['admin', 'trainer']);
|
$q->whereIn('name', ['admin', 'trainer']);
|
||||||
})->orderBy('first_name', 'asc')->get();
|
})->orderBy('first_name', 'asc')->get();
|
||||||
|
|
||||||
// 2. Query Utama dengan Relasi
|
// Load relasi subject, department, position, dan creator
|
||||||
$query = QuestionBank::with(['department', 'position', 'creator']);
|
$query = QuestionBank::with(['subject', 'department', 'position', 'creator']);
|
||||||
|
|
||||||
// 3. Logika Filter Dinamis
|
// Logika Filter
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$query->where('question_text', 'like', '%' . $request->search . '%');
|
$query->where('question_text', 'like', '%' . $request->search . '%');
|
||||||
}
|
}
|
||||||
@@ -41,28 +35,55 @@ class ExamManagementController extends Controller
|
|||||||
if ($request->filled('position_id')) {
|
if ($request->filled('position_id')) {
|
||||||
$query->where('position_id', $request->position_id);
|
$query->where('position_id', $request->position_id);
|
||||||
}
|
}
|
||||||
// if ($request->filled('matrix_id')) {
|
if ($request->filled('subject_id')) { // Filter berdasarkan Subject
|
||||||
// $query->where('training_matrix_id', $request->matrix_id);
|
$query->where('subject_id', $request->subject_id);
|
||||||
// }
|
}
|
||||||
if ($request->filled('question_type')) {
|
if ($request->filled('question_type')) {
|
||||||
$query->where('type', $request->question_type);
|
$query->where('question_type', $request->question_type);
|
||||||
}
|
}
|
||||||
if ($request->filled('question_level')) {
|
if ($request->filled('question_level')) {
|
||||||
$query->where('level', $request->question_level);
|
$query->where('question_level', $request->question_level);
|
||||||
}
|
}
|
||||||
if ($request->filled('created_by')) {
|
if ($request->filled('created_by')) {
|
||||||
$query->where('created_by', $request->created_by);
|
$query->where('created_by', $request->created_by);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Eksekusi Query dengan Pagination
|
|
||||||
$questions = $query->latest()->paginate(15)->withQueryString();
|
$questions = $query->latest()->paginate(15)->withQueryString();
|
||||||
$totalQuestions = $questions->total();
|
$totalQuestions = $questions->total();
|
||||||
|
|
||||||
return view('pages.admin.exams.question-bank', compact(
|
return view('pages.admin.exams.question-bank', compact(
|
||||||
'questions', 'totalQuestions', 'departments', 'positions', 'creators'
|
'questions', 'totalQuestions', 'departments', 'positions', 'subjects', 'creators'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk menampilkan halaman Tambah Soal
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$departments = Department::orderBy('name', 'asc')->get();
|
||||||
|
$positions = Position::orderBy('name', 'asc')->get();
|
||||||
|
$subjects = Subject::orderBy('name', 'asc')->get(); // Kirim Subject ke form
|
||||||
|
|
||||||
|
return view('pages.admin.exams.create', compact('departments', 'positions', 'subjects'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk menampilkan halaman Edit Soal
|
||||||
|
public function edit($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
|
||||||
|
|
||||||
|
return view('pages.admin.exams.edit', compact('question', 'departments', 'positions', 'subjects'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi Show untuk Detail Soal
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$question = QuestionBank::with(['subject', 'department', 'position', 'creator'])->findOrFail($id);
|
||||||
|
return view('pages.admin.exams.show', compact('question'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menghapus banyak soal sekaligus (Bulk Delete)
|
* Menghapus banyak soal sekaligus (Bulk Delete)
|
||||||
*/
|
*/
|
||||||
@@ -86,6 +107,6 @@ class ExamManagementController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function importView()
|
public function importView()
|
||||||
{
|
{
|
||||||
return view('pages.admin.exams.import-questions');
|
return view('pages.admin.exams.questions.import');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ class QuestionBank extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
// Atribut Soal dari desain Anda
|
// Atribut Soal dari desain Anda
|
||||||
'old_id',
|
'old_id',
|
||||||
'subject',
|
'subject_id',
|
||||||
'question_type',
|
'question_type',
|
||||||
'question_level',
|
'question_level',
|
||||||
'passing_grade',
|
'passing_grade',
|
||||||
@@ -36,6 +36,11 @@ class QuestionBank extends Model
|
|||||||
'correct_answer' => 'array', // Otomatis ubah JSON di database jadi Array di PHP
|
'correct_answer' => 'array', // Otomatis ubah JSON di database jadi Array di PHP
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function subject()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Subject::class, 'subject_id');
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| RELASI DATABASE (Diperlukan agar Filter Dropdown & Tabel berfungsi)
|
| RELASI DATABASE (Diperlukan agar Filter Dropdown & Tabel berfungsi)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Subject extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'subjects';
|
||||||
|
|
||||||
|
// Mengizinkan pengisian massal untuk kolom id dan name
|
||||||
|
protected $fillable = [
|
||||||
|
'id',
|
||||||
|
'name'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relasi balik ke QuestionBank (Opsional, tapi baik untuk ke depannya)
|
||||||
|
public function questions()
|
||||||
|
{
|
||||||
|
return $this->hasMany(QuestionBank::class, 'subject_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\Permission\DefaultTeamResolver;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||||
|
* is often just the "Permission" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Permission model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Permission` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permission' => Permission::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||||
|
* is often just the "Role" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Role model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => Role::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "Teams" feature from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your teams. Of course, it
|
||||||
|
* is often just the "Team" model but you may use whatever you like.
|
||||||
|
*/
|
||||||
|
'team' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasModels" trait and passing raw IDs to syncModels,
|
||||||
|
* attachModels, or detachModels, this model class will be used to
|
||||||
|
* resolve those IDs. If null, defaults to the guard's model.
|
||||||
|
*/
|
||||||
|
'default_model' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'roles' => 'roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your permissions. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models roles. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related pivots other than defaults
|
||||||
|
*/
|
||||||
|
'role_pivot_key' => null, // default 'role_id',
|
||||||
|
'permission_pivot_key' => null, // default 'permission_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related model primary key other than
|
||||||
|
* `model_id`.
|
||||||
|
*
|
||||||
|
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||||
|
* that case, name this `model_uuid`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to use the teams feature and your related model's
|
||||||
|
* foreign key is other than `team_id`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'team_foreign_key' => 'team_id',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the method for checking permissions will be registered on the gate.
|
||||||
|
* Set this to false if you want to implement custom logic for checking permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||||
|
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||||
|
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||||
|
*/
|
||||||
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Events will fire when a role or permission is assigned/unassigned:
|
||||||
|
* \Spatie\Permission\Events\RoleAttachedEvent
|
||||||
|
* \Spatie\Permission\Events\RoleDetachedEvent
|
||||||
|
* \Spatie\Permission\Events\PermissionAttachedEvent
|
||||||
|
* \Spatie\Permission\Events\PermissionDetachedEvent
|
||||||
|
*
|
||||||
|
* To enable, set to true, and then create listeners to watch these events.
|
||||||
|
*/
|
||||||
|
'events_enabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Teams Feature.
|
||||||
|
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||||
|
* If you want the migrations to register the 'team_foreign_key', you must
|
||||||
|
* set this to true before doing the migration.
|
||||||
|
* If you already did the migration then you must make a new migration to also
|
||||||
|
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||||
|
* (view the latest version of this package's migration file)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'teams' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use to resolve the permissions team id
|
||||||
|
*/
|
||||||
|
'team_resolver' => DefaultTeamResolver::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Passport Client Credentials Grant
|
||||||
|
* When set to true the package will use Passports Client to check permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required permission names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required role names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default wildcard permission lookups are disabled.
|
||||||
|
* See documentation to understand supported syntax.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use for interpreting wildcard permissions.
|
||||||
|
* If you need to modify delimiters, override the class and specify its name here.
|
||||||
|
*/
|
||||||
|
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||||
|
|
||||||
|
/* Cache-specific settings */
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all permissions are cached for 24 hours to speed up performance.
|
||||||
|
* When permissions or roles are updated the cache is flushed automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration_time' => DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The cache key used to store all permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may optionally indicate a specific cache driver to use for permission and
|
||||||
|
* role caching using any of the `store` drivers listed in the cache.php config
|
||||||
|
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => 'default',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -25,11 +25,12 @@ class UserFactory extends Factory
|
|||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => fake()->name(),
|
'first_name' => fake()->firstName(),
|
||||||
|
'last_name' => fake()->lastName(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= \Illuminate\Support\Facades\Hash::make('password'),
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => \Illuminate\Support\Str::random(10),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('question_banks', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->unsignedBigInteger('old_id')->nullable()->index(); // Kolom penaut data lama
|
|
||||||
$table->longText('question_text');
|
|
||||||
$table->longText('option_a')->nullable();
|
|
||||||
$table->longText('option_b')->nullable();
|
|
||||||
$table->longText('option_c')->nullable();
|
|
||||||
$table->longText('option_d')->nullable();
|
|
||||||
$table->longText('option_e')->nullable();
|
|
||||||
$table->text('correct_answer')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('question_banks');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -13,6 +13,7 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('positions', function (Blueprint $table) {
|
Schema::create('positions', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
$table->string('name'); // PASTIKAN BARIS INI ADA
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?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(): void
|
|
||||||
{
|
|
||||||
Schema::create('role_user', function (Blueprint $table) {
|
|
||||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->foreignId('role_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->primary(['user_id', 'role_id']); // Mencegah duplikasi
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('role_user');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,7 @@ return new class extends Migration
|
|||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$teams = config('permission.teams');
|
$teams = config('permission.teams');
|
||||||
$tableNames = config('permission.table_names');
|
$tableNames = config('permission.table_names');
|
||||||
@@ -20,88 +20,103 @@ return new class extends Migration
|
|||||||
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
|
||||||
// 1. Permissions Table
|
/**
|
||||||
if (!Schema::hasTable($tableNames['permissions'])) {
|
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
*/
|
||||||
$table->id();
|
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||||
$table->string('name');
|
$table->id(); // permission id
|
||||||
$table->string('guard_name');
|
$table->string('name');
|
||||||
$table->timestamps();
|
$table->string('guard_name');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||||
|
*/
|
||||||
|
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
|
$table->id(); // role id
|
||||||
|
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||||
|
}
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('guard_name');
|
||||||
|
$table->timestamps();
|
||||||
|
if ($teams || config('permission.testing')) {
|
||||||
|
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||||
|
} else {
|
||||||
$table->unique(['name', 'guard_name']);
|
$table->unique(['name', 'guard_name']);
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// 2. Roles Table
|
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||||
if (!Schema::hasTable($tableNames['roles'])) {
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
|
||||||
$table->id();
|
|
||||||
if ($teams || config('permission.testing')) {
|
|
||||||
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
|
||||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
|
||||||
}
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('guard_name');
|
|
||||||
$table->timestamps();
|
|
||||||
if ($teams || config('permission.testing')) {
|
|
||||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
|
||||||
} else {
|
|
||||||
$table->unique(['name', 'guard_name']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Model Has Permissions
|
$table->string('model_type');
|
||||||
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||||
$table->unsignedBigInteger($pivotPermission);
|
|
||||||
$table->string('model_type');
|
|
||||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
|
||||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
|
||||||
|
|
||||||
$table->foreign($pivotPermission)->references('id')->on($tableNames['permissions'])->cascadeOnDelete();
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
if ($teams) {
|
->on($tableNames['permissions'])
|
||||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
->cascadeOnDelete();
|
||||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
if ($teams) {
|
||||||
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
} else {
|
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||||
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Model Has Roles
|
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
if (!Schema::hasTable($tableNames['model_has_roles'])) {
|
'model_has_permissions_permission_model_type_primary');
|
||||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
} else {
|
||||||
$table->unsignedBigInteger($pivotRole);
|
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
$table->string('model_type');
|
'model_has_permissions_permission_model_type_primary');
|
||||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
}
|
||||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
});
|
||||||
|
|
||||||
$table->foreign($pivotRole)->references('id')->on($tableNames['roles'])->cascadeOnDelete();
|
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
if ($teams) {
|
|
||||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
|
||||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
|
||||||
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
|
|
||||||
} else {
|
|
||||||
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Role Has Permissions
|
$table->string('model_type');
|
||||||
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||||
$table->unsignedBigInteger($pivotPermission);
|
|
||||||
$table->unsignedBigInteger($pivotRole);
|
|
||||||
$table->foreign($pivotPermission)->references('id')->on($tableNames['permissions'])->cascadeOnDelete();
|
|
||||||
$table->foreign($pivotRole)->references('id')->on($tableNames['roles'])->cascadeOnDelete();
|
|
||||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app('cache')->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)->forget(config('permission.cache.key'));
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
app('cache')
|
||||||
|
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||||
|
->forget(config('permission.cache.key'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ return new class extends Migration {
|
|||||||
Schema::create('question_banks', function (Blueprint $table) {
|
Schema::create('question_banks', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->integer('old_id')->nullable(); // Untuk menyimpan ID dari database lama
|
$table->integer('old_id')->nullable(); // Untuk menyimpan ID dari database lama
|
||||||
$table->string('subject')->nullable();
|
$table->unsignedBigInteger('subject_id')->nullable();
|
||||||
|
|
||||||
$table->unsignedBigInteger('department_id')->nullable();
|
$table->unsignedBigInteger('department_id')->nullable();
|
||||||
$table->unsignedBigInteger('position_id')->nullable();
|
$table->unsignedBigInteger('position_id')->nullable();
|
||||||
|
|||||||
+6
-6
@@ -11,11 +11,11 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('roles', function (Blueprint $table) {
|
Schema::create('subjects', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('name'); // Contoh: 'trainer', 'trainee'
|
$table->string('name'); // PASTIKAN ADA
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +23,6 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('roles');
|
Schema::dropIfExists('subjects');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3,23 +3,35 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
use WithoutModelEvents;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seed the application's database.
|
|
||||||
*/
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
// 1. Bersihkan Cache Spatie
|
||||||
|
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||||
|
|
||||||
User::factory()->create([
|
// 2. Buat Role Baku
|
||||||
'name' => 'Test User',
|
$roleAdmin = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||||
'email' => 'test@example.com',
|
Role::firstOrCreate(['name' => 'trainer', 'guard_name' => 'web']);
|
||||||
]);
|
|
||||||
|
// 3. Patenkan Akun Utama Anda
|
||||||
|
$user = User::updateOrCreate(
|
||||||
|
['email' => 'iwit@tia-pharma.com'],
|
||||||
|
[
|
||||||
|
'first_name' => 'Iwit',
|
||||||
|
'last_name' => 'Admin',
|
||||||
|
'password' => Hash::make('rahasia1234'), // Password baku permanen
|
||||||
|
'is_active' => 1,
|
||||||
|
'must_change_password' => 0,
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Otomatis Jadikan Admin
|
||||||
|
$user->assignRole($roleAdmin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="max-w-5xl mx-auto px-4 py-8" x-data="{ questionType: '{{ old('type', '') }}' }">
|
<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">
|
<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.exams.questions') }}" 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>
|
<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>
|
||||||
@@ -20,106 +20,114 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
<form action="{{ url('admin/exams/questions') }}" method="POST" enctype="multipart/form-data" class="p-6 sm:p-8 space-y-6">
|
<form action="{{ url('admin/exams/questions') }}" method="POST" class="p-6 sm:p-8 space-y-6">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Departemen</label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Subject / Materi Pembelajaran <span class="text-red-500">*</span></label>
|
||||||
|
<select name="subject_id" class="select2 w-full text-sm" required>
|
||||||
|
<option value="">-- Pilih Judul Materi --</option>
|
||||||
|
@foreach($subjects as $subj)
|
||||||
|
<option value="{{ $subj->id }}" {{ old('subject_id') == $subj->id ? 'selected' : '' }}>{{ $subj->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-bold text-slate-700 mb-2">Departemen Khusus</label>
|
||||||
<select name="department_id" class="select2 w-full text-sm">
|
<select name="department_id" class="select2 w-full text-sm">
|
||||||
<option value="">-- Berlaku untuk Semua Departemen --</option>
|
<option value="">-- Berlaku Semua Departemen --</option>
|
||||||
@foreach($departments as $dept)
|
@foreach($departments as $dept)
|
||||||
<option value="{{ $dept->id }}" {{ old('department_id') == $dept->id ? 'selected' : '' }}>{{ $dept->name }}</option>
|
<option value="{{ $dept->id }}" {{ old('department_id') == $dept->id ? 'selected' : '' }}>{{ $dept->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Posisi / Jabatan</label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Posisi / Jabatan Khusus</label>
|
||||||
<select name="position_id" class="select2 w-full text-sm">
|
<select name="position_id" class="select2 w-full text-sm">
|
||||||
<option value="">-- Berlaku untuk Semua Jabatan --</option>
|
<option value="">-- Berlaku Semua Jabatan --</option>
|
||||||
@foreach($positions as $pos)
|
@foreach($positions as $pos)
|
||||||
<option value="{{ $pos->id }}" {{ old('position_id') == $pos->id ? 'selected' : '' }}>{{ $pos->name }}</option>
|
<option value="{{ $pos->id }}" {{ old('position_id') == $pos->id ? 'selected' : '' }}>{{ $pos->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Materi SOP / Modul <span class="text-red-500">*</span></label>
|
|
||||||
<select name="training_matrix_id" class="select2 w-full text-sm" required>
|
|
||||||
<option value="">-- Pilih Materi Pembelajaran --</option>
|
|
||||||
@foreach($matrices as $matrix)
|
|
||||||
<option value="{{ $matrix->id }}" {{ old('training_matrix_id') == $matrix->id ? 'selected' : '' }}>{{ $matrix->title }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tipe Soal <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Tipe Bentuk Soal <span class="text-red-500">*</span></label>
|
||||||
<select name="type" x-model="questionType" class="select2 w-full text-sm" required>
|
<select name="question_type" class="select2 w-full text-sm" required id="question_type_select">
|
||||||
<option value="">-- Pilih Tipe Pertanyaan --</option>
|
<option value="">-- Pilih Tipe --</option>
|
||||||
<option value="single">Single Choice (Satu Jawaban Benar)</option>
|
<option value="single" {{ old('question_type') == 'single' ? 'selected' : '' }}>Single Choice (1 Kunci Jawaban)</option>
|
||||||
<option value="multiple">Multiple Choice (Banyak Jawaban Benar)</option>
|
<option value="multiple" {{ old('question_type') == 'multiple' ? 'selected' : '' }}>Multiple Choice (Multi Jawaban)</option>
|
||||||
<option value="true_false">True / False (Benar / Salah)</option>
|
<option value="true_false" {{ old('question_type') == 'true_false' ? 'selected' : '' }}>True / False (Benar / Salah)</option>
|
||||||
<option value="descriptive">Descriptive (Esai / Uraian)</option>
|
<option value="descriptive" {{ old('question_type') == 'descriptive' ? 'selected' : '' }}>Descriptive (Esai / Uraian)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tingkat Kesulitan <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Tingkat Kesulitan Soal <span class="text-red-500">*</span></label>
|
||||||
<select name="level" class="select2 w-full text-sm" required>
|
<select name="question_level" class="select2 w-full text-sm" required>
|
||||||
<option value="mudah" {{ old('level') == 'mudah' ? 'selected' : '' }}>Mudah (Easy)</option>
|
<option value="mudah" {{ old('question_level') == 'mudah' ? 'selected' : '' }}>Mudah</option>
|
||||||
<option value="sedang" {{ old('level') == 'sedang' ? 'selected' : '' }}>Sedang (Medium)</option>
|
<option value="sedang" {{ old('question_level') == 'sedang' ? 'selected' : '' }}>Sedang</option>
|
||||||
<option value="sulit" {{ old('level') == 'sulit' ? 'selected' : '' }}>Sulit (Hard)</option>
|
<option value="sulit" {{ old('question_level') == 'sulit' ? 'selected' : '' }}>Sulit</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-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', 0) }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500">
|
||||||
|
</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', 0) }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-slate-100 pt-6">
|
<div class="border-t border-slate-100 pt-6">
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Pertanyaan <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Isi 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 border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm" placeholder="Tuliskan pertanyaan di sini...">{{ old('question_text') }}</textarea>
|
<textarea name="question_text" rows="4" required class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 text-sm" placeholder="Masukkan konten pertanyaan...">{{ old('question_text') }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-slate-100 pt-6" x-show="questionType !== ''" x-transition>
|
<div class="border-t border-slate-100 pt-6" x-show="questionType !== ''" x-transition>
|
||||||
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4 border-l-4 border-blue-500 pl-3">Pengaturan Jawaban</h3>
|
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4 border-l-4 border-blue-500 pl-3">Kerangka Pilihan & Kunci Jawaban</h3>
|
||||||
|
|
||||||
<div x-show="questionType === 'single'" style="display: none;" class="space-y-3">
|
<div x-show="questionType === 'single'" class="space-y-3">
|
||||||
<p class="text-xs text-slate-500 mb-3">Pilih satu radio button sebagai jawaban yang benar.</p>
|
@foreach(['A' => 'option_a', 'B' => 'option_b', 'C' => 'option_b', 'D' => 'option_d', 'E' => 'option_e'] as $key => $column)
|
||||||
@foreach(['A', 'B', 'C', 'D'] as $opt)
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<input type="radio" name="correct_answer" value="{{ $opt }}" class="w-5 h-5 text-blue-600 border-slate-300 focus:ring-blue-500 cursor-pointer">
|
<input type="radio" name="correct_answer" value="{{ $key }}" class="w-5 h-5 text-blue-600 border-slate-300">
|
||||||
<input type="text" name="options[{{ $opt }}]" placeholder="Opsi Jawaban {{ $opt }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:border-blue-500">
|
<input type="text" name="{{ $column }}" placeholder="Isi Pilihan {{ $key }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm">
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="questionType === 'multiple'" style="display: none;" class="space-y-3">
|
<div x-show="questionType === 'multiple'" class="space-y-3">
|
||||||
<p class="text-xs text-slate-500 mb-3">Centang lebih dari satu checkbox untuk jawaban yang benar.</p>
|
@foreach(['A' => 'option_a', 'B' => 'option_b', 'C' => 'option_c', 'D' => 'option_d', 'E' => 'option_e'] as $key => $column)
|
||||||
@foreach(['A', 'B', 'C', 'D', 'E'] as $opt)
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<input type="checkbox" name="correct_answers[]" value="{{ $opt }}" class="w-5 h-5 text-blue-600 border-slate-300 rounded focus:ring-blue-500 cursor-pointer">
|
<input type="checkbox" name="correct_answer[]" value="{{ $key }}" class="w-5 h-5 text-blue-600 border-slate-300 rounded">
|
||||||
<input type="text" name="options[{{ $opt }}]" placeholder="Opsi Jawaban {{ $opt }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:border-blue-500">
|
<input type="text" name="{{ $column }}" placeholder="Isi Pilihan {{ $key }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm">
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="questionType === 'true_false'" style="display: none;" class="flex gap-6">
|
<div x-show="questionType === 'true_false'" class="flex gap-6">
|
||||||
<label class="flex items-center gap-2 p-4 border border-slate-200 rounded-xl cursor-pointer hover:bg-slate-50 w-48">
|
<label class="flex items-center gap-2 p-4 border border-slate-200 rounded-xl cursor-pointer hover:bg-slate-50 w-48">
|
||||||
<input type="radio" name="correct_answer" value="true" class="w-5 h-5 text-emerald-600 border-slate-300 focus:ring-emerald-500">
|
<input type="radio" name="correct_answer" value="True" class="w-5 h-5 text-emerald-600 border-slate-300">
|
||||||
<span class="font-bold text-slate-700">True (Benar)</span>
|
<span class="font-bold text-slate-700">True (Benar)</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 p-4 border border-slate-200 rounded-xl cursor-pointer hover:bg-slate-50 w-48">
|
<label class="flex items-center gap-2 p-4 border border-slate-200 rounded-xl cursor-pointer hover:bg-slate-50 w-48">
|
||||||
<input type="radio" name="correct_answer" value="false" class="w-5 h-5 text-red-600 border-slate-300 focus:ring-red-500">
|
<input type="radio" name="correct_answer" value="False" class="w-5 h-5 text-red-600 border-slate-300">
|
||||||
<span class="font-bold text-slate-700">False (Salah)</span>
|
<span class="font-bold text-slate-700">False (Salah)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="questionType === 'descriptive'" style="display: none;">
|
<div x-show="questionType === 'descriptive'">
|
||||||
<p class="text-xs text-slate-500 mb-3">Tuliskan kata kunci / panduan jawaban benar untuk penilai (Opsional).</p>
|
<p class="text-xs text-slate-400 mb-2">Tuliskan panduan atau esensi jawaban yang dinilai benar:</p>
|
||||||
<textarea name="expected_answer" rows="3" class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500" placeholder="Kata kunci jawaban..."></textarea>
|
<textarea name="option_a" rows="3" class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500" placeholder="Catatan/Kunci Esai untuk Korektor..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100 mt-8">
|
<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 transition-colors">Batal</a>
|
<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>
|
||||||
<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 transition-all">Simpan Soal</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,17 +136,8 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('.select2').select2({ width: '100%' });
|
$('.select2').select2({ width: '100%' });
|
||||||
|
$('#question_type_select').on('change', function() {
|
||||||
// Menjembatani Select2 dengan Alpine.js agar x-model ter-update saat Select2 berubah
|
window.dispatchEvent(new CustomEvent('update-type', { detail: $(this).val() }));
|
||||||
$('select[name="type"]').on('change', function() {
|
|
||||||
// Ambil elemen div pembungkus Alpine (x-data) dan ubah nilainya
|
|
||||||
let el = document.querySelector('[x-data]');
|
|
||||||
if(el && el.__x) {
|
|
||||||
el.__x.$data.questionType = $(this).val();
|
|
||||||
} else {
|
|
||||||
// Alternatif dispatch event custom jika pakai versi Alpine V3 terbaru
|
|
||||||
el.dispatchEvent(new CustomEvent('update-type', { detail: $(this).val() }));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="max-w-5xl mx-auto px-4 py-8" x-data="{ questionType: '{{ old('type', $question->type) }}' }" @update-type.window="questionType = $event.detail">
|
<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">
|
<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.exams.questions') }}" 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>
|
<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>
|
</a>
|
||||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Edit Soal: #{{ $question->id }}</h2>
|
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Edit Pertanyaan #{{ $question->id }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
@@ -24,6 +24,14 @@
|
|||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<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 Pembelajaran <span class="text-red-500">*</span></label>
|
||||||
|
<select name="subject_id" class="select2 w-full text-sm" required>
|
||||||
|
@foreach($subjects as $subj)
|
||||||
|
<option value="{{ $subj->id }}" {{ old('subject_id', $question->subject_id) == $subj->id ? 'selected' : '' }}>{{ $subj->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Departemen</label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Departemen</label>
|
||||||
<select name="department_id" class="select2 w-full text-sm">
|
<select name="department_id" class="select2 w-full text-sm">
|
||||||
@@ -42,48 +50,86 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Materi SOP / Modul <span class="text-red-500">*</span></label>
|
|
||||||
<select name="training_matrix_id" class="select2 w-full text-sm" required>
|
|
||||||
<option value="">-- Pilih Materi Pembelajaran --</option>
|
|
||||||
@foreach($matrices as $matrix)
|
|
||||||
<option value="{{ $matrix->id }}" {{ old('training_matrix_id', $question->training_matrix_id) == $matrix->id ? 'selected' : '' }}>{{ $matrix->title }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tipe Soal <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Tipe Bentuk Soal <span class="text-red-500">*</span></label>
|
||||||
<select name="type" class="select2 w-full text-sm" required>
|
<select name="question_type" class="select2 w-full text-sm" required id="question_type_select">
|
||||||
<option value="single" {{ $question->type == 'single' ? 'selected' : '' }}>Single Choice (Satu Jawaban Benar)</option>
|
<option value="single" {{ $question->question_type == 'single' ? 'selected' : '' }}>Single Choice</option>
|
||||||
<option value="multiple" {{ $question->type == 'multiple' ? 'selected' : '' }}>Multiple Choice (Banyak Jawaban Benar)</option>
|
<option value="multiple" {{ $question->question_type == 'multiple' ? 'selected' : '' }}>Multiple Choice</option>
|
||||||
<option value="true_false" {{ $question->type == 'true_false' ? 'selected' : '' }}>True / False (Benar / Salah)</option>
|
<option value="true_false" {{ $question->question_type == 'true_false' ? 'selected' : '' }}>True / False</option>
|
||||||
<option value="descriptive" {{ $question->type == 'descriptive' ? 'selected' : '' }}>Descriptive (Esai / Uraian)</option>
|
<option value="descriptive" {{ $question->question_type == 'descriptive' ? 'selected' : '' }}>Deskriptif</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tingkat Kesulitan <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Tingkat Kesulitan <span class="text-red-500">*</span></label>
|
||||||
<select name="level" class="select2 w-full text-sm" required>
|
<select name="question_level" class="select2 w-full text-sm" required>
|
||||||
<option value="mudah" {{ $question->level == 'mudah' ? 'selected' : '' }}>Mudah (Easy)</option>
|
<option value="mudah" {{ $question->question_level == 'mudah' ? 'selected' : '' }}>Mudah</option>
|
||||||
<option value="sedang" {{ $question->level == 'sedang' ? 'selected' : '' }}>Sedang (Medium)</option>
|
<option value="sedang" {{ $question->question_level == 'sedang' ? 'selected' : '' }}>Sedang</option>
|
||||||
<option value="sulit" {{ $question->level == 'sulit' ? 'selected' : '' }}>Sulit (Hard)</option>
|
<option value="sulit" {{ $question->question_level == 'sulit' ? 'selected' : '' }}>Sulit</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-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) }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500">
|
||||||
|
</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) }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-slate-100 pt-6">
|
<div class="border-t border-slate-100 pt-6">
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-2">Pertanyaan <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-slate-700 mb-2">Isi 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 border-slate-200 rounded-xl text-sm focus:border-blue-500">{{ old('question_text', $question->question_text) }}</textarea>
|
<textarea name="question_text" rows="4" required class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:border-blue-500">{{ old('question_text', $question->question_text) }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-amber-50 border-l-4 border-amber-500 p-4 mt-4">
|
<div class="border-t border-slate-100 pt-6" x-show="questionType !== ''" x-transition>
|
||||||
<p class="text-sm text-amber-800 font-semibold">Tipe Soal diubah?</p>
|
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4 border-l-4 border-blue-500 pl-3">Pengaturan Pilihan & Kunci Jawaban</h3>
|
||||||
<p class="text-xs text-amber-700 mt-1">Jika Anda mengubah tipe soal, pastikan Anda mengisi kembali opsi jawabannya di bawah ini karena format jawabannya berubah.</p>
|
|
||||||
|
@php
|
||||||
|
// Pastikan array terstruktur aman untuk dibaca blade
|
||||||
|
$checkedAnswers = is_array($question->correct_answer) ? $question->correct_answer : [$question->correct_answer];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<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 }}" {{ in_array($key, $checkedAnswers) ? 'checked' : '' }} class="w-5 h-5 text-blue-600 border-slate-300">
|
||||||
|
<input type="text" name="{{ $col }}" value="{{ $question->$col }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm">
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="questionType === 'multiple'" 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="checkbox" name="correct_answer[]" value="{{ $key }}" {{ in_array($key, $checkedAnswers) ? 'checked' : '' }} class="w-5 h-5 text-blue-600 border-slate-300 rounded">
|
||||||
|
<input type="text" name="{{ $col }}" value="{{ $question->$col }}" class="flex-1 px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm">
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="questionType === 'true_false'" class="flex gap-6">
|
||||||
|
<label class="flex items-center gap-2 p-4 border border-slate-200 rounded-xl cursor-pointer hover:bg-slate-50 w-48">
|
||||||
|
<input type="radio" name="correct_answer" value="True" {{ in_array('True', $checkedAnswers) || in_array('opt_a', $checkedAnswers) ? '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-2 p-4 border border-slate-200 rounded-xl cursor-pointer hover:bg-slate-50 w-48">
|
||||||
|
<input type="radio" name="correct_answer" value="False" {{ in_array('False', $checkedAnswers) || in_array('opt_b', $checkedAnswers) ? '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'">
|
||||||
|
<textarea name="option_a" rows="3" class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm" placeholder="Catatan esai korektor...">{{ $question->option_a }}</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100 mt-8">
|
<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 transition-colors">Batal</a>
|
<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>
|
||||||
<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 transition-all">Perbarui Soal</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,8 +138,7 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('.select2').select2({ width: '100%' });
|
$('.select2').select2({ width: '100%' });
|
||||||
|
$('#question_type_select').on('change', function() {
|
||||||
$('select[name="type"]').on('change', function() {
|
|
||||||
window.dispatchEvent(new CustomEvent('update-type', { detail: $(this).val() }));
|
window.dispatchEvent(new CustomEvent('update-type', { detail: $(this).val() }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
|
|
||||||
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm mb-6">
|
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm mb-6">
|
||||||
<form action="{{ route('admin.exams.questions') }}" method="GET">
|
<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="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Departemen</label>
|
<label class="block text-xs font-semibold text-slate-500 mb-1">Departemen</label>
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Posisi</label>
|
<label class="block text-xs font-semibold text-slate-500 mb-1">Posisi / Jabatan</label>
|
||||||
<select name="position_id" class="select2 w-full text-sm">
|
<select name="position_id" class="select2 w-full text-sm">
|
||||||
<option value="">Semua</option>
|
<option value="">Semua</option>
|
||||||
@foreach($positions as $pos)
|
@foreach($positions as $pos)
|
||||||
@@ -85,8 +84,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-slate-500 mb-1">Materi / Modul</label>
|
<label class="block text-xs font-semibold text-slate-500 mb-1">Subject / Materi</label>
|
||||||
|
<select name="subject_id" class="select2 w-full text-sm">
|
||||||
|
<option value="">Semua Materi</option>
|
||||||
|
@foreach($subjects as $subj)
|
||||||
|
<option value="{{ $subj->id }}" {{ request('subject_id') == $subj->id ? 'selected' : '' }}>{{ $subj->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -115,7 +119,7 @@
|
|||||||
<select name="created_by" class="select2 w-full text-sm">
|
<select name="created_by" class="select2 w-full text-sm">
|
||||||
<option value="">Semua</option>
|
<option value="">Semua</option>
|
||||||
@foreach($creators as $creator)
|
@foreach($creators as $creator)
|
||||||
<option value="{{ $creator->id }}" {{ request('created_by') == $creator->id ? 'selected' : '' }}>{{ $creator->first_name }}</option>
|
<option value="{{ $creator->id }}" {{ request('created_by') == $creator->id ? 'selected' : '' }}>{{ $creator->first_name }} {{ $creator->last_name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +130,7 @@
|
|||||||
<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">
|
<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>
|
||||||
<div class="flex gap-2 w-full md:w-auto">
|
<div class="flex gap-2 w-full md:w-auto">
|
||||||
@if(request()->hasAny(['search', 'department_id', 'position_id', 'question_type', 'question_level', 'created_by']))
|
@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>
|
<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>
|
||||||
@endif
|
@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">
|
<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">
|
||||||
@@ -145,8 +149,9 @@
|
|||||||
<th class="px-4 py-4 w-10 text-center">
|
<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">
|
<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>
|
||||||
<th class="px-4 py-4 font-semibold">Q.ID</th>
|
<th class="px-4 py-4 font-semibold w-12">Q.ID</th>
|
||||||
<th class="px-4 py-4 font-semibold">Materi / Modul</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">Tipe & Level</th>
|
||||||
<th class="px-4 py-4 font-semibold w-1/3">Pertanyaan</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">Pembuat</th>
|
||||||
@@ -161,28 +166,37 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 font-mono font-bold text-slate-500">{{ $q->id }}</td>
|
<td class="px-4 py-3 font-mono font-bold text-slate-500">{{ $q->id }}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<span class="font-semibold text-slate-800">{{ $q->matrix->title ?? 'Umum' }}</span>
|
<div class="font-bold text-slate-800 max-w-[200px] truncate" title="{{ $q->subject->name ?? 'Materi Umum' }}">
|
||||||
<div class="text-[11px] text-slate-500 mt-0.5">{{ $q->department->name ?? 'All Dept' }}</div>
|
{{ $q->subject->name ?? 'Materi Umum' }}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<span class="px-2 py-0.5 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold uppercase tracking-wider border border-indigo-100">{{ str_replace('_', ' ', $q->type) }}</span>
|
<span class="font-semibold text-slate-700 block">{{ $q->department->name ?? 'Semua Dept' }}</span>
|
||||||
|
<span class="text-[11px] text-slate-400 block mt-0.5">{{ $q->position->name ?? 'Semua Jabatan' }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span class="px-2 py-0.5 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold uppercase tracking-wider border border-indigo-100">
|
||||||
|
{{ str_replace('_', ' ', $q->question_type) }}
|
||||||
|
</span>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
@if($q->level == 'mudah') <span class="text-[11px] font-bold text-emerald-500">Mudah</span>
|
@if($q->question_level == 'mudah') <span class="text-[11px] font-bold text-emerald-500">Mudah</span>
|
||||||
@elseif($q->level == 'sedang') <span class="text-[11px] font-bold text-amber-500">Sedang</span>
|
@elseif($q->question_level == 'sedang') <span class="text-[11px] font-bold text-amber-500">Sedang</span>
|
||||||
@else <span class="text-[11px] font-bold text-red-500">Sulit</span> @endif
|
@else <span class="text-[11px] font-bold text-red-500">Sulit</span> @endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<p class="text-sm text-slate-800 line-clamp-2">{{ strip_tags($q->question_text) }}</p>
|
<p class="text-sm text-slate-800 line-clamp-2">{{ strip_tags($q->question_text) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
<div class="text-sm font-semibold">{{ $q->creator->first_name ?? 'Sistem' }}</div>
|
<div class="text-sm font-semibold text-slate-800">{{ $q->creator->first_name ?? 'ID: '.$q->created_by }}</div>
|
||||||
<div class="text-[11px] text-slate-400">@if($q->creator && $q->creator->hasRole('trainer')) Trainer @else Admin @endif</div>
|
<div class="text-[11px] text-slate-400">
|
||||||
|
@if($q->creator && $q->creator->hasRole('trainer')) Trainer @else Staf / Admin @endif
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right space-x-2 whitespace-nowrap">
|
<td class="px-4 py-3 text-right space-x-2 whitespace-nowrap">
|
||||||
<a href="#" 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) }}" 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="#" 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>
|
<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="#" method="POST" class="inline-block" onsubmit="return confirm('Hapus soal ini?');">
|
<form action="{{ url('admin/exams/questions/'.$q->id) }}" method="POST" class="inline-block" onsubmit="return confirm('Hapus soal ini secara permanen?');">
|
||||||
@csrf @method('DELETE')
|
@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>
|
<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>
|
</form>
|
||||||
@@ -190,9 +204,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="px-4 py-12 text-center">
|
<td colspan="8" class="px-4 py-12 text-center text-slate-500 font-medium">
|
||||||
<svg class="w-12 h-12 text-slate-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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-6 9l2 2 4-4"></path></svg>
|
Belum ada data soal yang sesuai kriteria filter.
|
||||||
<p class="text-slate-500 font-medium">Belum ada data soal yang sesuai filter.</p>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
@@ -210,7 +223,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Inisialisasi Select2 untuk semua filter
|
|
||||||
$('.select2').select2({ width: '100%' });
|
$('.select2').select2({ width: '100%' });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
@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; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="max-w-5xl mx-auto px-4 py-8" x-data="{ questionType: '{{ old('question_type', '') }}' }">
|
||||||
|
<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">
|
||||||
|
<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">Buat Soal Baru</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<form action="{{ url('admin/exams/questions') }}" method="POST" class="p-6 sm:p-8 space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<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" required>
|
||||||
|
<option value="">-- Pilih Judul Materi --</option>
|
||||||
|
@foreach($subjects as $subj)
|
||||||
|
<option value="{{ $subj->id }}" {{ old('subject_id') == $subj->id ? 'selected' : '' }}>{{ $subj->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</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">
|
||||||
|
<option value="">-- Global (Semua Dept) --</option>
|
||||||
|
@foreach($departments as $dept)
|
||||||
|
<option value="{{ $dept->id }}" {{ old('department_id') == $dept->id ? 'selected' : '' }}>{{ $dept->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</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">
|
||||||
|
<option value="">-- Global (Semua Jabatan) --</option>
|
||||||
|
@foreach($positions as $pos)
|
||||||
|
<option value="{{ $pos->id }}" {{ old('position_id') == $pos->id ? 'selected' : '' }}>{{ $pos->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</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" required x-model="questionType">
|
||||||
|
<option value="">-- Pilih Tipe --</option>
|
||||||
|
<option value="single">Single Choice (1 Kunci)</option>
|
||||||
|
<option value="multiple">Multiple Choice (Multi Kunci)</option>
|
||||||
|
<option value="true_false">True / False</option>
|
||||||
|
<option value="descriptive">Descriptive (Esai)</option>
|
||||||
|
</select>
|
||||||
|
</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" required>
|
||||||
|
<option value="mudah" {{ old('question_level') == 'mudah' ? 'selected' : '' }}>Mudah</option>
|
||||||
|
<option value="sedang" {{ old('question_level', 'sedang') == 'sedang' ? 'selected' : '' }}>Sedang</option>
|
||||||
|
<option value="sulit" {{ old('question_level') == 'sulit' ? 'selected' : '' }}>Sulit</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-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', 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', 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 border-slate-200 rounded-xl text-sm focus:bg-white focus:ring-2 focus:ring-blue-500/20" placeholder="Tuliskan pertanyaan di sini...">{{ old('question_text') }}</textarea>
|
||||||
|
</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 }}" 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 }}" 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">
|
||||||
|
@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 }}" 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 }}" 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" 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" 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">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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100">
|
||||||
|
<a href="{{ route('admin.exams.questions') }}" 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 Soal</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.select2').select2({ width: '100%' });
|
||||||
|
|
||||||
|
// Integrasi Select2 dengan Alpine.js
|
||||||
|
$('select[name="question_type"]').on('change', function() {
|
||||||
|
let el = document.querySelector('[x-data]');
|
||||||
|
let data = Alpine.$data(el);
|
||||||
|
data.questionType = $(this).val();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden p-6 sm:p-8">
|
||||||
|
<div class="mb-8 p-4 bg-blue-50 border border-blue-100 rounded-xl text-sm text-blue-800 flex items-start gap-4">
|
||||||
|
<svg class="w-6 h-6 text-blue-500 mt-0.5" 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>
|
||||||
|
<p class="font-bold mb-1">Panduan Import</p>
|
||||||
|
<p>Gunakan format Excel standar untuk mengimpor soal. Pastikan kolom Departemen ID, Posisi ID, dan Subject ID diisi menggunakan ID numerik (bukan nama/teks). Tipe soal ditulis sebagai <code>single</code>, <code>multiple</code>, <code>true_false</code>, atau <code>descriptive</code>.</p>
|
||||||
|
<a href="#" class="inline-block mt-3 px-4 py-2 bg-white text-blue-600 font-bold border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors shadow-sm text-xs">Unduh Template Excel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ url('admin/exams/questions/import') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="border-2 border-dashed border-slate-300 rounded-2xl p-10 text-center hover:bg-slate-50 transition-colors relative">
|
||||||
|
<input type="file" name="file" required accept=".xlsx,.xls,.csv" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" id="file_upload">
|
||||||
|
<svg class="w-12 h-12 text-slate-400 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 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>
|
||||||
|
<p class="text-sm font-bold text-slate-700">Klik untuk memilih file excel</p>
|
||||||
|
<p class="text-xs text-slate-500 mt-1" id="file_name">atau seret dan lepas (Drag & Drop) di sini (.xlsx, .csv)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-4">
|
||||||
|
<button type="submit" class="px-6 py-2.5 text-sm font-bold text-white bg-slate-800 hover:bg-slate-900 rounded-xl shadow-md transition-colors w-full md:w-auto">
|
||||||
|
Mulai Proses Import
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Script sederhana untuk menampilkan nama file yang dipilih
|
||||||
|
document.getElementById('file_upload').addEventListener('change', function(e) {
|
||||||
|
if(e.target.files.length > 0) {
|
||||||
|
document.getElementById('file_name').innerHTML = '<span class="text-blue-600 font-bold">' + e.target.files[0].name + '</span> siap diunggah.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<a href="{{ url('admin/exams/questions/'.$question->id.'/edit') }}" class="px-4 py-2 bg-amber-500 text-white rounded-lg text-sm font-bold hover:bg-amber-600 shadow-sm transition-colors">Edit</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
|
<div>
|
||||||
|
<span class="block text-xs font-semibold text-slate-400 uppercase">Q.ID</span>
|
||||||
|
<span class="block text-sm font-mono font-bold text-slate-900 mt-1">#{{ $question->id }} <span class="font-normal text-slate-400">(Old: {{ $question->old_id ?? '-' }})</span></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="block text-xs font-semibold text-slate-400 uppercase">Materi</span>
|
||||||
|
<span class="block text-sm font-bold text-slate-800 mt-1">{{ $question->subject->name ?? 'Umum' }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="block text-xs font-semibold text-slate-400 uppercase">Tipe Soal</span>
|
||||||
|
<span class="inline-block mt-1 px-2 py-0.5 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold uppercase border border-indigo-100">
|
||||||
|
{{ str_replace('_', ' ', $question->question_type) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="block text-xs font-semibold text-slate-400 uppercase">Level</span>
|
||||||
|
<span class="block text-sm font-bold mt-1 {{ $question->question_level == 'mudah' ? 'text-emerald-500' : ($question->question_level == 'sedang' ? 'text-amber-500' : 'text-red-500') }}">
|
||||||
|
{{ strtoupper($question->question_level) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-slate-100 mt-4 pt-4 grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-slate-500">
|
||||||
|
<div><b>Departemen:</b> {{ $question->department->name ?? 'Global' }}</div>
|
||||||
|
<div><b>Posisi:</b> {{ $question->position->name ?? 'Global' }}</div>
|
||||||
|
<div><b>Kriteria:</b> PG: {{ $question->passing_grade }} | Durasi: {{ $question->duration }} dtk</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 sm:p-8">
|
||||||
|
<div class="text-slate-800 text-base font-medium mb-8 leading-relaxed">
|
||||||
|
{!! nl2br(e($question->question_text)) !!}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4 border-b border-slate-100 pb-2">Struktur Kunci & Pilihan</h4>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
@php
|
||||||
|
$ans = $question->correct_answer;
|
||||||
|
$arrKeys = is_string($ans) ? json_decode($ans, true) : (is_array($ans) ? $ans : [$ans]);
|
||||||
|
if(!is_array($arrKeys)) $arrKeys = [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($question->question_type === 'descriptive')
|
||||||
|
<div class="p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm text-slate-700">
|
||||||
|
{!! nl2br(e($question->option_a)) !!}
|
||||||
|
</div>
|
||||||
|
@elseif($question->question_type === 'true_false')
|
||||||
|
<div class="p-3 rounded-xl border {{ in_array('True', $arrKeys) || in_array('opt_a', $arrKeys) ? 'bg-emerald-50 border-emerald-200 font-bold text-emerald-900' : 'border-slate-100 text-slate-600' }}">True (Benar)</div>
|
||||||
|
<div class="p-3 rounded-xl border {{ in_array('False', $arrKeys) || in_array('opt_b', $arrKeys) ? 'bg-emerald-50 border-emerald-200 font-bold text-emerald-900' : 'border-slate-100 text-slate-600' }}">False (Salah)</div>
|
||||||
|
@else
|
||||||
|
@foreach(['A' => 'option_a', 'B' => 'option_b', 'C' => 'option_c', 'D' => 'option_d', 'E' => 'option_e'] as $k => $col)
|
||||||
|
@if(!empty($question->$col))
|
||||||
|
<div class="flex items-center justify-between p-3 rounded-xl border {{ in_array($k, $arrKeys) || in_array($col, $arrKeys) ? 'bg-emerald-50 border-emerald-200 text-emerald-900 font-bold shadow-sm' : 'border-slate-100 text-slate-600 bg-slate-50' }}">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-7 h-7 rounded-md text-xs font-mono font-bold flex items-center justify-center mr-3 {{ in_array($k, $arrKeys) || in_array($col, $arrKeys) ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-500' }}">{{ $k }}</span>
|
||||||
|
<span>{{ $question->$col }}</span>
|
||||||
|
</div>
|
||||||
|
@if(in_array($k, $arrKeys) || in_array($col, $arrKeys))
|
||||||
|
<span class="text-[10px] bg-emerald-600 text-white px-2 py-1 rounded font-bold uppercase tracking-widest">Kunci</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-4 border-t border-slate-100 text-xs text-slate-400 text-right">
|
||||||
|
Dibuat oleh: <span class="font-semibold">{{ $question->creator->first_name ?? 'ID '.$question->created_by }}</span> pada {{ $question->created_at->format('d M Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -7,66 +7,85 @@
|
|||||||
<a href="{{ route('admin.exams.questions') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
<a href="{{ route('admin.exams.questions') }}" 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>
|
<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>
|
</a>
|
||||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Detail Pertanyaan</h2>
|
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Pratinjau Soal</h2>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url('admin/exams/questions/'.$question->id.'/edit') }}" class="px-4 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm font-bold hover:bg-blue-100 transition-colors">
|
<a href="{{ url('admin/exams/questions/'.$question->id.'/edit') }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-bold hover:bg-blue-700 transition-colors shadow-sm">
|
||||||
Edit Soal
|
Edit Konten Soal
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 mb-6">
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-xs font-semibold text-slate-400 uppercase">Q.ID</span>
|
<span class="block text-xs font-semibold text-slate-400 uppercase tracking-wider">ID Sistem</span>
|
||||||
<span class="block text-sm font-mono font-bold text-slate-900">#{{ $question->id }}</span>
|
<span class="block text-sm font-mono font-bold text-slate-900 mt-0.5">#{{ $question->id }} <span class="text-xs font-normal text-slate-400">(Old ID: {{ $question->old_id ?? '-' }})</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-xs font-semibold text-slate-400 uppercase">Materi SOP</span>
|
<span class="block text-xs font-semibold text-slate-400 uppercase tracking-wider">Subject / Materi</span>
|
||||||
<span class="block text-sm font-medium text-slate-900">{{ $question->matrix->title ?? 'Umum' }}</span>
|
<span class="block text-sm font-bold text-slate-800 mt-0.5">{{ $question->subject->name ?? 'Materi Umum' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-xs font-semibold text-slate-400 uppercase">Tipe Soal</span>
|
<span class="block text-xs font-semibold text-slate-400 uppercase tracking-wider">Bentuk Tipe</span>
|
||||||
<span class="inline-block mt-1 px-2 py-0.5 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold uppercase border border-indigo-100">
|
<span class="inline-block mt-1 px-2 py-0.5 bg-indigo-50 text-indigo-700 rounded text-[10px] font-bold uppercase border border-indigo-100">
|
||||||
{{ str_replace('_', ' ', $question->type) }}
|
{{ str_replace('_', ' ', $question->question_type) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-xs font-semibold text-slate-400 uppercase">Level</span>
|
<span class="block text-xs font-semibold text-slate-400 uppercase tracking-wider">Tingkat Kesulitan</span>
|
||||||
<span class="block text-sm font-bold mt-1 {{ $question->level == 'mudah' ? 'text-emerald-500' : ($question->level == 'sedang' ? 'text-amber-500' : 'text-red-500') }}">
|
<span class="block text-sm font-bold mt-0.5 {{ $question->question_level == 'mudah' ? 'text-emerald-500' : ($question->question_level == 'sedang' ? 'text-amber-500' : 'text-red-500') }}">
|
||||||
{{ strtoupper($question->level) }}
|
{{ strtoupper($question->question_level) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-slate-100 mt-4 pt-4 flex gap-6">
|
<div class="border-t border-slate-100 mt-4 pt-4 grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-slate-500">
|
||||||
<div>
|
<div><b>Departemen Target:</b> {{ $question->department->name ?? 'Semua Departemen (Global)' }}</div>
|
||||||
<span class="block text-xs font-semibold text-slate-400 uppercase">Dikhususkan untuk Departemen:</span>
|
<div><b>Jabatan Target:</b> {{ $question->position->name ?? 'Semua Struktur Jabatan' }}</div>
|
||||||
<span class="block text-sm text-slate-700">{{ $question->department->name ?? 'Semua Departemen' }}</span>
|
<div><b>Kelayakan Kelulusan:</b> Min score <span class="font-bold text-slate-800">{{ $question->passing_grade }}</span> (Durasi: {{ $question->duration }} detik)</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="block text-xs font-semibold text-slate-400 uppercase">Dikhususkan untuk Jabatan:</span>
|
|
||||||
<span class="block text-sm text-slate-700">{{ $question->position->name ?? 'Semua Jabatan' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 sm:p-8">
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 sm:p-8">
|
||||||
<h4 class="text-sm font-bold text-slate-400 uppercase tracking-wider mb-4 border-b border-slate-100 pb-2">Isi Pertanyaan</h4>
|
<h4 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 border-b border-slate-100 pb-2">Konten Soal</h4>
|
||||||
|
<div class="text-slate-800 text-base font-medium mb-8 leading-relaxed">
|
||||||
<div class="prose prose-slate max-w-none text-slate-800 text-lg mb-8">
|
|
||||||
{!! nl2br(e($question->question_text)) !!}
|
{!! nl2br(e($question->question_text)) !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="text-sm font-bold text-slate-400 uppercase tracking-wider mb-4 border-b border-slate-100 pb-2">Opsi Jawaban & Kunci</h4>
|
<h4 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4 border-b border-slate-100 pb-2">Struktur Kunci & Pilihan</h4>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-20">
|
||||||
@if($question->type === 'descriptive')
|
@if($question->question_type === 'descriptive')
|
||||||
<div class="p-4 bg-slate-50 border border-slate-200 rounded-xl">
|
<div class="p-4 bg-slate-50 border border-slate-200 rounded-xl">
|
||||||
<span class="text-xs font-bold text-slate-500 uppercase">Kunci / Panduan Penilaian:</span>
|
<span class="text-xs font-bold text-slate-400 uppercase block mb-1">Panduan Jawaban Kunci Esai:</span>
|
||||||
<p class="text-slate-800 mt-2">{{ $question->expected_answer ?? 'Tidak ada kata kunci khusus yang diatur.' }}</p>
|
<p class="text-slate-700 text-sm">{!! nl2br(e($question->option_a)) !!}</p>
|
||||||
|
</div>
|
||||||
|
@elseif($question->question_type === 'true_false')
|
||||||
|
@php $correct = is_array($question->correct_answer) ? $question->correct_answer[0] : $question->correct_answer; @endphp
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center p-3 rounded-xl border {{ in_array($correct, ['True','opt_a']) ? 'bg-emerald-50/70 border-emerald-200 text-emerald-900 font-bold' : 'border-slate-100 text-slate-600' }}">
|
||||||
|
<span class="w-6 text-center text-xs font-bold mr-2">✔</span> True (Benar)
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center p-3 rounded-xl border {{ in_array($correct, ['False','opt_b']) ? 'bg-emerald-50/70 border-emerald-200 text-emerald-900 font-bold' : 'border-slate-100 text-slate-600' }}">
|
||||||
|
<span class="w-6 text-center text-xs font-bold mr-2">✔</span> False (Salah)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<p class="italic text-sm text-slate-500">Tampilan opsi jawaban disesuaikan dengan struktur relasi database opsi Anda.</p>
|
<div class="space-y-2">
|
||||||
|
@php $arrKeys = is_array($question->correct_answer) ? $question->correct_answer : [$question->correct_answer]; @endphp
|
||||||
|
@foreach(['A' => 'option_a', 'B' => 'option_b', 'C' => 'option_c', 'D' => 'option_d', 'E' => 'option_e'] as $k => $col)
|
||||||
|
@if(!empty($question->$col))
|
||||||
|
<div class="flex items-center justify-between p-3 rounded-xl border {{ in_array($k, $arrKeys) ? 'bg-emerald-50/70 border-emerald-200 text-emerald-900 font-bold' : 'border-slate-100 text-slate-600' }}">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="w-6 h-6 rounded-md text-xs font-mono font-bold bg-slate-100 flex items-center justify-center mr-3 {{ in_array($k, $arrKeys) ? 'bg-emerald-500 text-white' : '' }}">{{ $k }}</span>
|
||||||
|
<span>{{ $question->$col }}</span>
|
||||||
|
</div>
|
||||||
|
@if(in_array($k, $arrKeys))
|
||||||
|
<span class="text-xs bg-emerald-600 text-white px-2 py-0.5 rounded font-bold uppercase tracking-wider">Kunci</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,57 +1,79 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
|
|
||||||
@section('title', 'Master SOP - HRIS')
|
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-7xl mx-auto">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="flex justify-between items-center mb-6">
|
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Dokumen SOP & CPOB</h2>
|
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Manajemen SOP & Training Matrix</h2>
|
||||||
<p class="text-sm text-slate-500">Pusat data prosedur operasi standar perusahaan.</p>
|
<p class="text-sm text-slate-500 mt-1">Kelola dokumen Standar Operasional Prosedur dan modul pelatihan.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<a href="#" 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">
|
||||||
|
+ Tambah SOP Baru
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ route('admin.sops.create') }}" class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-semibold hover:bg-indigo-700 shadow-sm transition-all">
|
|
||||||
+ Registrasi SOP
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
<div class="bg-white p-5 rounded-t-2xl border border-slate-200 border-b-0">
|
||||||
|
<form action="{{ route('admin.sops.index') }}" method="GET" class="flex flex-col md:flex-row gap-4">
|
||||||
|
<div class="w-full md:w-1/3">
|
||||||
|
<input type="text" name="search" value="{{ request('search') }}" placeholder="Cari judul SOP atau Modul..."
|
||||||
|
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="px-5 py-2 bg-slate-800 text-white rounded-xl text-sm font-semibold hover:bg-slate-700">Cari</button>
|
||||||
|
@if(request('search'))
|
||||||
|
<a href="{{ route('admin.sops.index') }}" class="px-5 py-2 bg-red-50 text-red-600 border border-red-100 rounded-xl text-sm font-semibold hover:bg-red-100">Reset</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-slate-200 rounded-b-2xl overflow-hidden shadow-sm">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left text-sm whitespace-nowrap">
|
||||||
<thead>
|
<thead class="bg-slate-50/80 text-slate-500 text-xs uppercase tracking-wider border-b border-slate-200">
|
||||||
<tr class="bg-slate-50 border-b border-slate-200 text-xs uppercase tracking-wider text-slate-500 font-bold">
|
<tr>
|
||||||
<th class="p-4">Kode SOP</th>
|
<th class="px-6 py-4 font-semibold">No</th>
|
||||||
<th class="p-4">Judul Dokumen</th>
|
<th class="px-6 py-4 font-semibold">Judul SOP / Modul</th>
|
||||||
<th class="p-4">Versi</th>
|
<th class="px-6 py-4 font-semibold">Departemen Terkait</th>
|
||||||
<th class="p-4">Status</th>
|
<th class="px-6 py-4 font-semibold">Status</th>
|
||||||
<th class="p-4 text-right">Aksi</th>
|
<th class="px-6 py-4 font-semibold text-right">Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-100 text-sm text-slate-700">
|
<tbody class="divide-y divide-slate-100 text-slate-700">
|
||||||
@foreach($sops as $sop)
|
{{-- Ganti $sops dengan variabel yang Anda kirim dari DashboardController@sopIndex --}}
|
||||||
|
@forelse($sops ?? [] as $index => $sop)
|
||||||
<tr class="hover:bg-slate-50">
|
<tr class="hover:bg-slate-50">
|
||||||
<td class="p-4 font-mono font-medium">{{ $sop->sop_code }}</td>
|
<td class="px-6 py-4 text-slate-500">{{ $index + 1 }}</td>
|
||||||
<td class="p-4 font-bold text-slate-800">{{ $sop->title }}</td>
|
<td class="px-6 py-4 font-bold text-slate-900">{{ $sop->title ?? $sop->name ?? 'Judul SOP' }}</td>
|
||||||
<td class="p-4">v{{ $sop->version }}</td>
|
<td class="px-6 py-4">{{ $sop->department->name ?? 'Semua Departemen' }}</td>
|
||||||
<td class="p-4">
|
<td class="px-6 py-4">
|
||||||
@if($sop->status == 'Active')
|
<span class="px-2 py-1 bg-emerald-50 text-emerald-600 rounded text-[10px] font-bold uppercase tracking-wider border border-emerald-100">Aktif</span>
|
||||||
<span class="px-2.5 py-1 bg-emerald-100 text-emerald-700 text-xs font-bold rounded-full">Active</span>
|
|
||||||
@elseif($sop->status == 'Draft')
|
|
||||||
<span class="px-2.5 py-1 bg-amber-100 text-amber-700 text-xs font-bold rounded-full">Draft</span>
|
|
||||||
@else
|
|
||||||
<span class="px-2.5 py-1 bg-slate-200 text-slate-700 text-xs font-bold rounded-full">Obsolete</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
</td>
|
||||||
<td class="p-4 text-right">
|
<td class="px-6 py-4 text-right space-x-3">
|
||||||
<a href="#" class="text-slate-500 hover:text-indigo-600 font-medium text-xs">Lihat Detail</a>
|
<a href="#" class="text-indigo-600 hover:text-indigo-800 font-medium text-sm">Lihat Dokumen</a>
|
||||||
|
<a href="#" class="text-slate-400 hover:text-blue-600 font-medium text-sm">Edit</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-6 py-12 text-center">
|
||||||
|
<svg class="w-12 h-12 text-slate-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||||
|
<p class="text-slate-500 font-medium">Belum ada data SOP atau Training Matrix.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 border-t border-slate-100">
|
|
||||||
|
{{-- Jika ada pagination --}}
|
||||||
|
@if(isset($sops) && method_exists($sops, 'hasPages') && $sops->hasPages())
|
||||||
|
<div class="p-4 border-t border-slate-100 bg-slate-50/50">
|
||||||
{{ $sops->links() }}
|
{{ $sops->links() }}
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
@@ -85,6 +85,23 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
|
|
||||||
// Modul Ujian & Bank Soal
|
// Modul Ujian & Bank Soal
|
||||||
Route::get('/question-bank', [ExamManagementController::class, 'questionBank'])->name('exams.questions');
|
Route::get('/question-bank', [ExamManagementController::class, 'questionBank'])->name('exams.questions');
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// PENAMBAHAN ROUTE UNTUK CRUD BANK SOAL
|
||||||
|
// Note: Route statis harus di atas route dinamis ({id})
|
||||||
|
// ================================================================
|
||||||
|
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');
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
Route::get('/exams', [ExamManagementController::class, 'index'])->name('exams.index');
|
Route::get('/exams', [ExamManagementController::class, 'index'])->name('exams.index');
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user