From 91c0a8147f7f858e46a704f0b0d7f6f28831c553 Mon Sep 17 00:00:00 2001 From: Iwit Date: Thu, 4 Jun 2026 08:36:18 +0700 Subject: [PATCH] Perbaikan modul data karyawan dan staff + modul SOP import --- app/Console/Commands/MigrateLmsData.php | 81 +++++-- app/Console/Commands/MigrateOldQuestions.php | 59 ++--- app/Console/Commands/MigrateUsers.php | 74 ++++++ .../Controllers/Admin/DashboardController.php | 2 +- .../Admin/ExamManagementController.php | 59 +++-- app/Models/QuestionBank.php | 7 +- app/Models/Subject.php | 22 ++ config/permission.php | 219 ++++++++++++++++++ database/factories/UserFactory.php | 7 +- ..._26_043341_create_question_banks_table.php | 29 --- ...26_05_26_050716_create_positions_table.php | 1 + ...26_05_28_005635_create_role_user_table.php | 28 --- ..._05_28_061520_create_permission_tables.php | 165 +++++++------ ..._30_204343_create_question_banks_table.php | 2 +- ...26_06_03_193800_create_subjects_table.php} | 12 +- database/seeders/DatabaseSeeder.php | 36 ++- .../views/pages/admin/exams/create.blade.php | 119 +++++----- .../views/pages/admin/exams/edit.blade.php | 105 ++++++--- .../pages/admin/exams/question-bank.blade.php | 58 +++-- .../admin/exams/questions/create.blade.php | 152 ++++++++++++ .../admin/exams/questions/edit.blade.php | 0 .../admin/exams/questions/import.blade.php | 49 ++++ .../admin/exams/questions/show.blade.php | 90 +++++++ .../views/pages/admin/exams/show.blade.php | 81 ++++--- .../views/pages/admin/sop/index.blade.php | 92 +++++--- routes/web.php | 17 ++ 26 files changed, 1167 insertions(+), 399 deletions(-) create mode 100644 app/Console/Commands/MigrateUsers.php create mode 100644 app/Models/Subject.php create mode 100644 config/permission.php delete mode 100644 database/migrations/2026_05_26_043341_create_question_banks_table.php delete mode 100644 database/migrations/2026_05_28_005635_create_role_user_table.php rename database/migrations/{2026_05_28_005631_create_roles_table.php => 2026_06_03_193800_create_subjects_table.php} (60%) create mode 100644 resources/views/pages/admin/exams/questions/create.blade.php create mode 100644 resources/views/pages/admin/exams/questions/edit.blade.php create mode 100644 resources/views/pages/admin/exams/questions/import.blade.php create mode 100644 resources/views/pages/admin/exams/questions/show.blade.php diff --git a/app/Console/Commands/MigrateLmsData.php b/app/Console/Commands/MigrateLmsData.php index 3ecb944..7bc1609 100644 --- a/app/Console/Commands/MigrateLmsData.php +++ b/app/Console/Commands/MigrateLmsData.php @@ -22,11 +22,11 @@ class MigrateLmsData extends Command $this->migrateDepartmentPositions(); // 2. Eksekusi Data User - $this->migrateStaffToUsers(); - $this->migrateStudentsToUsers(); + // $this->migrateStaffToUsers(); + // $this->migrateStudentsToUsers(); // 3. Sinkronisasi Departemen & Posisi ke User - $this->syncUserDepartmentAndPosition(); + // $this->syncUserDepartmentAndPosition(); // 4. Eksekusi Ujian & Hasil $this->migrateQuestions(); @@ -205,30 +205,79 @@ class MigrateLmsData extends Command /** * TAHAP 4A: Memindahkan data Bank Soal */ - private function migrateQuestions() + protected function migrateQuestions() { - $this->warn('4A. Menarik data Bank Soal...'); - $oldQuestions = DB::connection('mysql_old')->table('questions')->get(); + $this->info('4A. Menarik data Bank Soal...'); + + // 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->start(); - foreach ($oldQuestions as $q) { - DB::table('question_banks')->updateOrInsert( - ['old_id' => $q->id], + foreach ($oldQuestions as $old) { + // Mapping Tipe + $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, - 'option_a' => $q->opt_a, - 'option_b' => $q->opt_b, - 'option_c' => $q->opt_c, - 'option_d' => $q->opt_d, - 'option_e' => $q->opt_e, - 'correct_answer' => $q->correct, + 'department_id' => $deptId, + 'position_id' => $posId, + 'subject_id' => $subjId, + 'question_type' => $type, + 'question_level' => $level, + 'question_text' => $old->question, + '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->finish(); $this->newLine(); + $this->info('Bank Soal berhasil ditarik!'); } /** diff --git a/app/Console/Commands/MigrateOldQuestions.php b/app/Console/Commands/MigrateOldQuestions.php index 2ec9044..55a88d2 100644 --- a/app/Console/Commands/MigrateOldQuestions.php +++ b/app/Console/Commands/MigrateOldQuestions.php @@ -5,17 +5,30 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use App\Models\QuestionBank; -use App\Models\User; +use App\Models\Subject; class MigrateOldQuestions extends Command { + // Ini adalah 'kunci' agar command muncul di php artisan list 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() { - $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(); if ($oldQuestions->isEmpty()) { @@ -30,53 +43,41 @@ class MigrateOldQuestions extends Command foreach ($oldQuestions as $old) { - // 1. Pemetaan Tipe Soal + // Pemetaan Tipe & Level $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'; } - // 2. Pemetaan 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'; } - // 3. Pemetaan Pembuat (Creator) - $creatorId = null; - if (isset($old->staff_id)) { - $oldStaff = DB::table('lmsv2-old.staff')->where('id', $old->staff_id)->first(); - if ($oldStaff && !empty($oldStaff->email)) { - $user = User::where('email', $oldStaff->email)->first(); - if ($user) $creatorId = $user->id; - } - } + // Pemetaan ID (Lama ke Baru) + $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; - // 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( - ['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_level' => $level, 'question_text' => $old->question, - - // Opsi A sampai E '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, // Sudah dalam bentuk array - + 'correct_answer' => $correctAnswer, 'created_by' => $creatorId, 'created_at' => $old->created_at ?? now(), 'updated_at' => $old->updated_at ?? now(), @@ -89,6 +90,6 @@ class MigrateOldQuestions extends Command $bar->finish(); $this->newLine(); - $this->info("Sinkronisasi Selesai! Berhasil memindahkan {$successCount} Soal beserta kunci jawabannya."); + $this->info("Sinkronisasi Selesai! Berhasil memperbarui {$successCount} Soal."); } } \ No newline at end of file diff --git a/app/Console/Commands/MigrateUsers.php b/app/Console/Commands/MigrateUsers.php new file mode 100644 index 0000000..dba51a0 --- /dev/null +++ b/app/Console/Commands/MigrateUsers.php @@ -0,0 +1,74 @@ + '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."); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 41967b4..47d74ff 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -51,6 +51,6 @@ class DashboardController extends Controller */ public function sopIndex(): View { - return view('pages.admin.sops.index'); + return view('pages.admin.sop.index'); } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/ExamManagementController.php b/app/Http/Controllers/Admin/ExamManagementController.php index bf26eac..8fe5230 100644 --- a/app/Http/Controllers/Admin/ExamManagementController.php +++ b/app/Http/Controllers/Admin/ExamManagementController.php @@ -6,32 +6,26 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Department; use App\Models\Position; -use App\Models\TrainingMatrix; +use App\Models\Subject; // PASTIKAN SUBJECT DI-IMPORT use App\Models\User; use App\Models\QuestionBank; class ExamManagementController extends Controller { - /** - * Menampilkan halaman Bank Soal dengan Filter Komprehensif - */ public function questionBank(Request $request) { - // 1. Siapkan Data untuk Dropdown Filter $departments = Department::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){ $q->whereIn('name', ['admin', 'trainer']); })->orderBy('first_name', 'asc')->get(); - // 2. Query Utama dengan Relasi - $query = QuestionBank::with(['department', 'position', 'creator']); + // Load relasi subject, department, position, dan creator + $query = QuestionBank::with(['subject', 'department', 'position', 'creator']); - // 3. Logika Filter Dinamis + // Logika Filter if ($request->filled('search')) { $query->where('question_text', 'like', '%' . $request->search . '%'); } @@ -41,28 +35,55 @@ class ExamManagementController extends Controller if ($request->filled('position_id')) { $query->where('position_id', $request->position_id); } - // if ($request->filled('matrix_id')) { - // $query->where('training_matrix_id', $request->matrix_id); - // } + if ($request->filled('subject_id')) { // Filter berdasarkan Subject + $query->where('subject_id', $request->subject_id); + } if ($request->filled('question_type')) { - $query->where('type', $request->question_type); + $query->where('question_type', $request->question_type); } if ($request->filled('question_level')) { - $query->where('level', $request->question_level); + $query->where('question_level', $request->question_level); } if ($request->filled('created_by')) { $query->where('created_by', $request->created_by); } - // 4. Eksekusi Query dengan Pagination $questions = $query->latest()->paginate(15)->withQueryString(); $totalQuestions = $questions->total(); 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) */ @@ -86,6 +107,6 @@ class ExamManagementController extends Controller */ public function importView() { - return view('pages.admin.exams.import-questions'); + return view('pages.admin.exams.questions.import'); } } \ No newline at end of file diff --git a/app/Models/QuestionBank.php b/app/Models/QuestionBank.php index 48d902a..1792da6 100644 --- a/app/Models/QuestionBank.php +++ b/app/Models/QuestionBank.php @@ -12,7 +12,7 @@ class QuestionBank extends Model protected $fillable = [ // Atribut Soal dari desain Anda 'old_id', - 'subject', + 'subject_id', 'question_type', 'question_level', 'passing_grade', @@ -36,6 +36,11 @@ class QuestionBank extends Model '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) diff --git a/app/Models/Subject.php b/app/Models/Subject.php new file mode 100644 index 0000000..9199161 --- /dev/null +++ b/app/Models/Subject.php @@ -0,0 +1,22 @@ +hasMany(QuestionBank::class, 'subject_id'); + } +} \ No newline at end of file diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..8f1f452 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,219 @@ + [ + + /* + * 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', + ], +]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index c4ceb07..f74784e 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -25,11 +25,12 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), + 'first_name' => fake()->firstName(), + 'last_name' => fake()->lastName(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), + 'password' => static::$password ??= \Illuminate\Support\Facades\Hash::make('password'), + 'remember_token' => \Illuminate\Support\Str::random(10), ]; } diff --git a/database/migrations/2026_05_26_043341_create_question_banks_table.php b/database/migrations/2026_05_26_043341_create_question_banks_table.php deleted file mode 100644 index f270e0e..0000000 --- a/database/migrations/2026_05_26_043341_create_question_banks_table.php +++ /dev/null @@ -1,29 +0,0 @@ -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'); - } -}; diff --git a/database/migrations/2026_05_26_050716_create_positions_table.php b/database/migrations/2026_05_26_050716_create_positions_table.php index 1c806ca..b1290f0 100644 --- a/database/migrations/2026_05_26_050716_create_positions_table.php +++ b/database/migrations/2026_05_26_050716_create_positions_table.php @@ -13,6 +13,7 @@ return new class extends Migration { Schema::create('positions', function (Blueprint $table) { $table->id(); + $table->string('name'); // PASTIKAN BARIS INI ADA $table->timestamps(); }); } diff --git a/database/migrations/2026_05_28_005635_create_role_user_table.php b/database/migrations/2026_05_28_005635_create_role_user_table.php deleted file mode 100644 index 71d059e..0000000 --- a/database/migrations/2026_05_28_005635_create_role_user_table.php +++ /dev/null @@ -1,28 +0,0 @@ -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'); - } -}; diff --git a/database/migrations/2026_05_28_061520_create_permission_tables.php b/database/migrations/2026_05_28_061520_create_permission_tables.php index d68df05..8986275 100644 --- a/database/migrations/2026_05_28_061520_create_permission_tables.php +++ b/database/migrations/2026_05_28_061520_create_permission_tables.php @@ -9,7 +9,7 @@ return new class extends Migration /** * Run the migrations. */ - public function up(): void + public function up(): void { $teams = config('permission.teams'); $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($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'])) { - Schema::create($tableNames['permissions'], static function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('guard_name'); - $table->timestamps(); + /** + * 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(); // permission id + $table->string('name'); + $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']); - }); - } + } + }); - // 2. Roles Table - if (!Schema::hasTable($tableNames['roles'])) { - 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']); - } - }); - } + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); - // 3. Model Has Permissions - if (!Schema::hasTable($tableNames['model_has_permissions'])) { - Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { - $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->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(); - - if ($teams) { - $table->unsignedBigInteger($columnNames['team_foreign_key']); - $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); - $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); - } else { - $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary'); - } - }); - } + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); - // 4. Model Has Roles - if (!Schema::hasTable($tableNames['model_has_roles'])) { - Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { - $table->unsignedBigInteger($pivotRole); - $table->string('model_type'); - $table->unsignedBigInteger($columnNames['model_morph_key']); - $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + }); - $table->foreign($pivotRole)->references('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['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); - // 5. Role Has Permissions - if (!Schema::hasTable($tableNames['role_has_permissions'])) { - 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')->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'); - }); - } + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); - 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')); } /** diff --git a/database/migrations/2026_05_30_204343_create_question_banks_table.php b/database/migrations/2026_05_30_204343_create_question_banks_table.php index eba4125..9d08ad8 100644 --- a/database/migrations/2026_05_30_204343_create_question_banks_table.php +++ b/database/migrations/2026_05_30_204343_create_question_banks_table.php @@ -10,7 +10,7 @@ return new class extends Migration { Schema::create('question_banks', function (Blueprint $table) { $table->id(); $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('position_id')->nullable(); diff --git a/database/migrations/2026_05_28_005631_create_roles_table.php b/database/migrations/2026_06_03_193800_create_subjects_table.php similarity index 60% rename from database/migrations/2026_05_28_005631_create_roles_table.php rename to database/migrations/2026_06_03_193800_create_subjects_table.php index 74a3a01..25d1b4c 100644 --- a/database/migrations/2026_05_28_005631_create_roles_table.php +++ b/database/migrations/2026_06_03_193800_create_subjects_table.php @@ -11,11 +11,11 @@ return new class extends Migration */ public function up(): void { - Schema::create('roles', function (Blueprint $table) { - $table->id(); - $table->string('name'); // Contoh: 'trainer', 'trainee' - $table->timestamps(); -}); + Schema::create('subjects', function (Blueprint $table) { + $table->id(); + $table->string('name'); // PASTIKAN ADA + $table->timestamps(); + }); } /** @@ -23,6 +23,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('roles'); + Schema::dropIfExists('subjects'); } }; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..3c53a30 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,23 +3,35 @@ namespace Database\Seeders; use App\Models\User; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; +use Spatie\Permission\Models\Role; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - - /** - * Seed the application's database. - */ public function run(): void { - // User::factory(10)->create(); + // 1. Bersihkan Cache Spatie + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + // 2. Buat Role Baku + $roleAdmin = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); + 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); } -} +} \ No newline at end of file diff --git a/resources/views/pages/admin/exams/create.blade.php b/resources/views/pages/admin/exams/create.blade.php index 1bbe8ef..f9eb7d8 100644 --- a/resources/views/pages/admin/exams/create.blade.php +++ b/resources/views/pages/admin/exams/create.blade.php @@ -11,7 +11,7 @@ .select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; } -
+
-
+ @csrf
- + + +
+
+
- +
-
- - -
-
- - + + + + +
- - + + +
+
+
+ + +
+
+ + +
+
- - + +
-

Pengaturan Jawaban

+

Kerangka Pilihan & Kunci Jawaban

-