Initial commit - lms-v2 + CLAUDE.md
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Models\User;
|
||||
|
||||
class MigrateLmsData extends Command
|
||||
{
|
||||
protected $signature = 'lms:migrate-data';
|
||||
protected $description = 'Migrasi komprehensif: Master Data -> Users -> Sinkronisasi Jabatan -> Bank Soal -> History Test';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Memulai pemindahan data dari CI3 Lama ke Laravel 13 V2...');
|
||||
|
||||
// 1. Eksekusi Master Data Terlebih Dahulu
|
||||
$this->migrateDepartments();
|
||||
$this->migratePositions();
|
||||
$this->migrateDepartmentPositions();
|
||||
|
||||
// 2. Eksekusi Data User
|
||||
$this->migrateStaffToUsers();
|
||||
$this->migrateStudentsToUsers();
|
||||
|
||||
// 3. Sinkronisasi Departemen & Posisi ke User
|
||||
$this->syncUserDepartmentAndPosition();
|
||||
|
||||
// 4. Eksekusi Ujian & Hasil
|
||||
$this->migrateQuestions();
|
||||
$this->migrateExamHistory();
|
||||
|
||||
$this->info('Sukses! Semua data Master, Karyawan, Sinkronisasi Jabatan, Bank Soal, dan Histori Test berhasil dipindahkan.');
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 1A: Memindahkan data `sections` (Lama) ke `departments` (Baru)
|
||||
*/
|
||||
/**
|
||||
* TAHAP 1A: Memindahkan data `sections` (Lama) ke `departments` (Baru)
|
||||
*/
|
||||
private function migrateDepartments()
|
||||
{
|
||||
$this->warn('1A. Menarik data Departemen (Sections)...');
|
||||
|
||||
$oldSections = DB::table('lmsv2-old.sections')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldSections));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldSections as $section) {
|
||||
// Generate kode otomatis untuk Departments karena kolom 'code' WAJIB ada
|
||||
$code = 'DPT-' . str_pad($section->id, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
DB::table('lmsv2.departments')->updateOrInsert(
|
||||
['id' => $section->id],
|
||||
[
|
||||
'code' => $code, // Tabel ini punya kolom 'code'
|
||||
'name' => trim($section->section),
|
||||
'created_at' => $section->created_at ?? now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 1B: Memindahkan data `classes` (Lama) ke `positions` (Baru)
|
||||
*/
|
||||
private function migratePositions()
|
||||
{
|
||||
$this->warn('1B. Menarik data Posisi/Jabatan (Classes)...');
|
||||
|
||||
$oldClasses = DB::table('lmsv2-old.classes')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldClasses));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldClasses as $class) {
|
||||
DB::table('lmsv2.positions')->updateOrInsert(
|
||||
['id' => $class->id],
|
||||
[
|
||||
// Tabel ini HANYA punya id, name, created_at, updated_at
|
||||
// (Pastikan Anda sudah menjalankan Langkah 1 untuk menambah kolom 'name')
|
||||
'name' => trim($class->class),
|
||||
'created_at' => $class->created_at ?? now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 2A: Memindahkan data `staff` (Trainer/Admin) ke tabel `users`
|
||||
*/
|
||||
private function migrateStaffToUsers()
|
||||
{
|
||||
$this->warn('2A. Menarik data Staff (Trainer/Admin)...');
|
||||
$oldStaffs = DB::connection('mysql_old')->table('staff')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldStaffs));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldStaffs as $staff) {
|
||||
$email = !empty($staff->email) ? $staff->email : $staff->employee_id . '@tunggal-pharma.com';
|
||||
|
||||
$gender = null;
|
||||
if (strtolower($staff->gender) == 'male') $gender = 'L';
|
||||
if (strtolower($staff->gender) == 'female') $gender = 'P';
|
||||
|
||||
User::updateOrCreate(
|
||||
['email' => $email],
|
||||
[
|
||||
'nik' => $staff->employee_id,
|
||||
'first_name' => trim($staff->name),
|
||||
'last_name' => trim($staff->surname),
|
||||
'gender' => $gender,
|
||||
'phone' => $staff->contact_no,
|
||||
'password' => Hash::make('Tunggal123!'),
|
||||
'role' => 'trainer',
|
||||
'old_id' => $staff->id,
|
||||
'old_password_hash' => $staff->password,
|
||||
]
|
||||
);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 2B: Memindahkan data `students` (Karyawan) ke tabel `users`
|
||||
*/
|
||||
private function migrateStudentsToUsers()
|
||||
{
|
||||
$this->warn('2B. Menarik data Karyawan (Students)...');
|
||||
$oldStudents = DB::connection('mysql_old')->table('students')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldStudents));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldStudents as $student) {
|
||||
$email = !empty($student->email) ? $student->email : $student->admission_no . '@tunggal-pharma.com';
|
||||
|
||||
$gender = null;
|
||||
if (strtolower($student->gender) == 'male') $gender = 'L';
|
||||
if (strtolower($student->gender) == 'female') $gender = 'P';
|
||||
|
||||
User::updateOrCreate(
|
||||
['email' => $email],
|
||||
[
|
||||
'nik' => $student->admission_no,
|
||||
'first_name' => trim($student->firstname),
|
||||
'last_name' => trim($student->lastname),
|
||||
'gender' => $gender,
|
||||
'phone' => $student->mobileno,
|
||||
'date_of_birth' => $student->dob,
|
||||
'password' => Hash::make('Karyawan123!'),
|
||||
'role' => 'karyawan',
|
||||
'old_id' => $student->id,
|
||||
]
|
||||
);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 3: MENGHUBUNGKAN USER DENGAN DEPARTEMEN & POSISI
|
||||
*/
|
||||
private function syncUserDepartmentAndPosition()
|
||||
{
|
||||
$this->warn('3. Menyinkronkan Karyawan dengan Departemen & Posisi...');
|
||||
|
||||
// Membaca relasi dari tabel student_session di DB lama
|
||||
$oldSessions = DB::connection('mysql_old')->table('student_session')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldSessions));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldSessions as $session) {
|
||||
// Cari user di database baru berdasarkan old_id
|
||||
$user = User::where('old_id', $session->student_id)
|
||||
->where('role', 'karyawan')
|
||||
->first();
|
||||
|
||||
if ($user) {
|
||||
// Update jabatan dan departemen
|
||||
// Asumsi: ID Departemen dan Posisi sama dengan ID Sections dan Classes dari DB Lama
|
||||
$user->update([
|
||||
'position_id' => $session->class_id,
|
||||
'department_id' => $session->section_id,
|
||||
]);
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 4A: Memindahkan data Bank Soal
|
||||
*/
|
||||
private function migrateQuestions()
|
||||
{
|
||||
$this->warn('4A. Menarik data Bank Soal...');
|
||||
$oldQuestions = DB::connection('mysql_old')->table('questions')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldQuestions));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldQuestions as $q) {
|
||||
DB::table('question_banks')->updateOrInsert(
|
||||
['old_id' => $q->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,
|
||||
]
|
||||
);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 4B: Memindahkan data Ujian & Hasil Historis Karyawan
|
||||
*/
|
||||
private function migrateExamHistory()
|
||||
{
|
||||
$this->warn('4B. Menarik data Historis Nilai Ujian...');
|
||||
|
||||
// Tarik Master Ujian
|
||||
$oldExams = DB::connection('mysql_old')->table('onlineexam')->get();
|
||||
foreach ($oldExams as $exam) {
|
||||
DB::table('exams')->updateOrInsert(
|
||||
['old_id' => $exam->id],
|
||||
[
|
||||
'title' => $exam->exam,
|
||||
'duration' => $exam->duration,
|
||||
'passing_percentage' => $exam->passing_percentage,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Tarik Hasil Test Karyawan
|
||||
$oldResults = DB::connection('mysql_old')
|
||||
->table('onlineexam_students')
|
||||
->join('student_session', 'onlineexam_students.student_session_id', '=', 'student_session.id')
|
||||
->join('students', 'student_session.student_id', '=', 'students.id')
|
||||
->select('onlineexam_students.*', 'students.id as old_student_id')
|
||||
->get();
|
||||
|
||||
$bar = $this->output->createProgressBar(count($oldResults));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldResults as $result) {
|
||||
$user = DB::table('users')->where('old_id', $result->old_student_id)->where('role', 'karyawan')->first();
|
||||
$exam = DB::table('exams')->where('old_id', $result->onlineexam_id)->first();
|
||||
|
||||
if ($user && $exam) {
|
||||
DB::table('exam_results')->updateOrInsert(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'exam_id' => $exam->id
|
||||
],
|
||||
[
|
||||
'is_attempted' => $result->is_attempted,
|
||||
'created_at' => $result->created_at,
|
||||
]
|
||||
);
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* TAHAP 1C: Memindahkan pemetaan class_sections (Lama) ke department_position (Baru)
|
||||
*/
|
||||
private function migrateDepartmentPositions()
|
||||
{
|
||||
$this->warn('1C. Menarik data Relasi Departemen & Posisi (Class Sections)...');
|
||||
|
||||
$oldRelations = \Illuminate\Support\Facades\DB::table('lmsv2-old.class_sections')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldRelations));
|
||||
$bar->start();
|
||||
|
||||
foreach ($oldRelations as $relation) {
|
||||
// Asumsi kolom lama bernama section_id dan class_id
|
||||
\Illuminate\Support\Facades\DB::table('lmsv2.department_position')->updateOrInsert(
|
||||
[
|
||||
'department_id' => $relation->section_id,
|
||||
'position_id' => $relation->class_id,
|
||||
]
|
||||
);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\QuestionBank;
|
||||
use App\Models\User;
|
||||
|
||||
class MigrateOldQuestions extends Command
|
||||
{
|
||||
protected $signature = 'lms:migrate-questions';
|
||||
protected $description = 'Menyalin data soal dari lmsv2-old.questions ke lmsv2.question_banks secara presisi';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Memulai Sinkronisasi Bank Soal dari Database Lama...');
|
||||
|
||||
$oldQuestions = DB::table('lmsv2-old.questions')->get();
|
||||
|
||||
if ($oldQuestions->isEmpty()) {
|
||||
$this->warn('Tidak ada data di tabel lmsv2-old.questions!');
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar(count($oldQuestions));
|
||||
$bar->start();
|
||||
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($oldQuestions as $old) {
|
||||
|
||||
// 1. Pemetaan Tipe Soal
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
[
|
||||
'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
|
||||
|
||||
'created_by' => $creatorId,
|
||||
'created_at' => $old->created_at ?? now(),
|
||||
'updated_at' => $old->updated_at ?? now(),
|
||||
]
|
||||
);
|
||||
|
||||
$successCount++;
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Sinkronisasi Selesai! Berhasil memindahkan {$successCount} Soal beserta kunci jawabannya.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\User;
|
||||
use Spatie\Permission\Models\Role; // Pastikan library Role dari Spatie dipanggil
|
||||
|
||||
class MigrateStaffToTrainer extends Command
|
||||
{
|
||||
protected $signature = 'lms:migrate-trainers';
|
||||
protected $description = 'Mencocokkan data staff lama dan menetapkannya sebagai Trainer & Trainee';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Memastikan Role (Hak Akses) tersedia di database...');
|
||||
|
||||
// Membuat role otomatis jika belum ada di tabel 'roles'
|
||||
Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||
Role::firstOrCreate(['name' => 'trainer', 'guard_name' => 'web']);
|
||||
Role::firstOrCreate(['name' => 'trainee', 'guard_name' => 'web']);
|
||||
|
||||
$this->info('Memulai pencocokan data Staff (Trainer)...');
|
||||
|
||||
$oldStaffs = DB::table('lmsv2-old.staff')->get();
|
||||
$bar = $this->output->createProgressBar(count($oldStaffs));
|
||||
$bar->start();
|
||||
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($oldStaffs as $staff) {
|
||||
$matchedUser = null;
|
||||
|
||||
// Prioritas 1: Pencocokan Email
|
||||
if (!empty($staff->email)) {
|
||||
$matchedUser = User::where('email', $staff->email)->first();
|
||||
}
|
||||
|
||||
// Prioritas 2: Pencocokan Nama
|
||||
if (!$matchedUser && !empty($staff->name)) {
|
||||
$matchedUser = User::where(DB::raw("CONCAT(first_name, ' ', COALESCE(last_name, ''))"), 'LIKE', "%{$staff->name}%")->first();
|
||||
}
|
||||
|
||||
// Jika ketemu, tetapkan role-nya
|
||||
if ($matchedUser) {
|
||||
// assignRole() kini dijamin aman karena role sudah dibuat di awal script
|
||||
$matchedUser->assignRole(['trainer', 'trainee']);
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Selesai! {$successCount} karyawan berhasil diperbarui sebagai Trainer.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MigrateUserInitials extends Command
|
||||
{
|
||||
protected $signature = 'lms:migrate-initials';
|
||||
protected $description = 'Migrasi kolom roll_no dari tabel student lama dengan pencocokan Email / Nama karyawan';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Memulai tarik data inisial (roll_no) dengan pencocokan pintar...');
|
||||
|
||||
// Ambil data student dari database lama yang memiliki roll_no
|
||||
$oldStudents = DB::table('lmsv2-old.students')->whereNotNull('roll_no')->get();
|
||||
|
||||
$bar = $this->output->createProgressBar(count($oldStudents));
|
||||
$bar->start();
|
||||
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
foreach ($oldStudents as $oldStudent) {
|
||||
// Ubah object ke array agar lebih aman saat mengecek keberadaan kolom (mencegah error jika kolom tidak ada)
|
||||
$old = (array) $oldStudent;
|
||||
|
||||
$matchedUser = null;
|
||||
|
||||
// PRIORITAS 1: Cocokkan berdasarkan Email (Paling Akurat)
|
||||
if (!empty($old['email'])) {
|
||||
$matchedUser = DB::table('lmsv2.users')->where('email', $old['email'])->first();
|
||||
}
|
||||
|
||||
// PRIORITAS 2: Jika email tidak cocok/kosong, cocokkan berdasarkan Nama
|
||||
if (!$matchedUser) {
|
||||
$query = DB::table('lmsv2.users');
|
||||
|
||||
// Cek apakah database lama menggunakan 'first_name' atau 'name' tunggal
|
||||
if (!empty($old['first_name'])) {
|
||||
$query->where('first_name', $old['first_name']);
|
||||
if (!empty($old['last_name'])) {
|
||||
$query->where('last_name', $old['last_name']);
|
||||
}
|
||||
} elseif (!empty($old['name'])) {
|
||||
// Jika DB lama hanya punya 1 kolom 'name'
|
||||
$query->where(DB::raw("CONCAT(first_name, ' ', COALESCE(last_name, ''))"), 'LIKE', "%{$old['name']}%");
|
||||
} else {
|
||||
// Tidak ada indikator nama yang bisa dicari
|
||||
$query->where('id', '<', 0); // Force empty result
|
||||
}
|
||||
|
||||
$matchedUser = $query->first();
|
||||
}
|
||||
|
||||
// JIKA KETEMU: Update tabel users yang baru
|
||||
if ($matchedUser) {
|
||||
DB::table('lmsv2.users')
|
||||
->where('id', $matchedUser->id) // Update berdasarkan ID baru yang ditemukan
|
||||
->update(['initial' => $old['roll_no']]);
|
||||
|
||||
$successCount++;
|
||||
} else {
|
||||
$failCount++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Selesai! Berhasil memetakan: {$successCount} data. Tidak ditemukan: {$failCount} data.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MigrateUserKtp extends Command
|
||||
{
|
||||
protected $signature = 'lms:migrate-ktp';
|
||||
protected $description = 'Migrasi data nomor KTP dari adhar_no atau note berdasarkan Email/Nama';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Memulai pemetaan Nomor KTP (Identity Number)...');
|
||||
|
||||
// Ambil dari database lama yang adhar_no ATAU note-nya ada isinya
|
||||
$oldStudents = DB::table('lmsv2-old.students')
|
||||
->where(function($query) {
|
||||
$query->whereNotNull('adhar_no')->where('adhar_no', '!=', '')
|
||||
->orWhereNotNull('note')->where('note', '!=', '');
|
||||
})->get();
|
||||
|
||||
$bar = $this->output->createProgressBar(count($oldStudents));
|
||||
$bar->start();
|
||||
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
foreach ($oldStudents as $oldStudent) {
|
||||
$old = (array) $oldStudent;
|
||||
|
||||
// Prioritaskan adhar_no, jika kosong gunakan note
|
||||
$ktpNumber = !empty($old['adhar_no']) ? $old['adhar_no'] : $old['note'];
|
||||
|
||||
$matchedUser = null;
|
||||
|
||||
// PRIORITAS 1: Pencocokan Email
|
||||
if (!empty($old['email'])) {
|
||||
$matchedUser = DB::table('lmsv2.users')->where('email', $old['email'])->first();
|
||||
}
|
||||
|
||||
// PRIORITAS 2: Pencocokan Nama
|
||||
if (!$matchedUser) {
|
||||
$query = DB::table('lmsv2.users');
|
||||
|
||||
if (!empty($old['first_name'])) {
|
||||
$query->where('first_name', $old['first_name']);
|
||||
if (!empty($old['last_name'])) {
|
||||
$query->where('last_name', $old['last_name']);
|
||||
}
|
||||
} elseif (!empty($old['name'])) {
|
||||
$query->where(DB::raw("CONCAT(first_name, ' ', COALESCE(last_name, ''))"), 'LIKE', "%{$old['name']}%");
|
||||
} else {
|
||||
$query->where('id', '<', 0); // Skip jika data nama rusak
|
||||
}
|
||||
|
||||
$matchedUser = $query->first();
|
||||
}
|
||||
|
||||
// JIKA KETEMU: Update kolom identity_number
|
||||
if ($matchedUser) {
|
||||
DB::table('lmsv2.users')
|
||||
->where('id', $matchedUser->id)
|
||||
->update(['identity_number' => $ktpNumber]);
|
||||
|
||||
$successCount++;
|
||||
} else {
|
||||
$failCount++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Pembaruan KTP Selesai! Berhasil terpetakan: {$successCount} data. Tidak ditemukan (Nama/Email beda): {$failCount} data.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromArray;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
|
||||
class EmployeeTemplateExport implements FromArray, WithHeadings, ShouldAutoSize
|
||||
{
|
||||
public function headings(): array
|
||||
{
|
||||
// PENTING: Header ini dibaca oleh Controller, jangan diubah formatnya
|
||||
return [
|
||||
'nik', 'no_ktp', 'inisial', 'nama_depan', 'nama_belakang',
|
||||
'email', 'telepon', 'jenis_kelamin', 'tgl_lahir', 'tgl_masuk',
|
||||
'department_id', 'position_id'
|
||||
];
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
return [
|
||||
// Baris contoh (Gunakan format YYYY-MM-DD untuk tanggal)
|
||||
['10102023', '3201012345678901', 'BDS', 'Budi', 'Santoso', 'budi@perusahaan.com', '0812345678', 'L', '1990-01-30', '2023-01-01', '1', '2'],
|
||||
['10102024', '', 'STA', 'Siti', 'Aminah', 'siti@perusahaan.com', '0887654321', 'P', '1995-12-15', '2023-05-10', '1', '3'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
|
||||
use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class EmployeesExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithEvents, WithCustomStartCell
|
||||
{
|
||||
protected $employees;
|
||||
|
||||
// Menangkap data hasil filter dari Controller
|
||||
public function __construct($employees)
|
||||
{
|
||||
$this->employees = $employees;
|
||||
}
|
||||
|
||||
public function collection()
|
||||
{
|
||||
return $this->employees;
|
||||
}
|
||||
|
||||
// Mulai tabel data di baris ke-6 (Baris 1-4 untuk informasi header)
|
||||
public function startCell(): string
|
||||
{
|
||||
return 'A6';
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return ['NIK', 'No. KTP', 'Inisial', 'Nama Depan', 'Nama Belakang', 'Email', 'No. HP', 'Jenis Kelamin', 'Tgl Lahir', 'Tgl Masuk', 'Departemen', 'Jabatan', 'Status', 'Hak Akses'];
|
||||
}
|
||||
|
||||
public function map($user): array
|
||||
{
|
||||
return [
|
||||
$user->nik,
|
||||
$user->identity_number ?? '-',
|
||||
strtoupper($user->initial ?? '-'),
|
||||
$user->first_name,
|
||||
$user->last_name,
|
||||
$user->email,
|
||||
$user->phone ?? '-',
|
||||
$user->gender == 'L' ? 'Laki-laki' : 'Perempuan',
|
||||
$user->date_of_birth ? Carbon::parse($user->date_of_birth)->format('Y-m-d') : '-',
|
||||
$user->join_date ? Carbon::parse($user->join_date)->format('Y-m-d') : '-',
|
||||
$user->department->name ?? '-',
|
||||
$user->position->name ?? '-',
|
||||
$user->is_active ? 'Aktif' : 'Non-Aktif',
|
||||
$user->roles->pluck('name')->implode(', '),
|
||||
];
|
||||
}
|
||||
|
||||
// Event untuk menyuntikkan Teks Header di atas tabel dan memberi warna
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
AfterSheet::class => function(AfterSheet $event) {
|
||||
$sheet = $event->sheet->getDelegate();
|
||||
$count = $this->employees->count();
|
||||
$user = auth()->user()->first_name . ' ' . auth()->user()->last_name;
|
||||
$date = Carbon::now()->translatedFormat('d F Y - H:i:s');
|
||||
|
||||
// Menyuntikkan Informasi di baris atas
|
||||
$sheet->setCellValue('A1', 'LAPORAN DATA KARYAWAN');
|
||||
$sheet->setCellValue('A2', 'Total Data (Sesuai Filter) : ' . $count . ' Karyawan');
|
||||
$sheet->setCellValue('A3', 'Dicetak Oleh : ' . $user);
|
||||
$sheet->setCellValue('A4', 'Waktu Cetak : ' . $date);
|
||||
|
||||
// Styling (Merge judul & warna header tabel)
|
||||
$sheet->mergeCells('A1:N1');
|
||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||
$sheet->getStyle('A2:A4')->getFont()->setBold(true);
|
||||
$sheet->getStyle('A6:N6')->getFont()->setBold(true);
|
||||
|
||||
// Memberi warna latar abu-abu pada header tabel (Baris 6)
|
||||
$sheet->getStyle('A6:N6')->getFill()
|
||||
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
|
||||
->getStartColor()->setARGB('FFE2E8F0');
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Position;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithCustomStartCell;
|
||||
use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PositionsExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithEvents, WithCustomStartCell
|
||||
{
|
||||
public function collection()
|
||||
{
|
||||
return Position::orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
public function startCell(): string
|
||||
{
|
||||
return 'A6';
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return ['No', 'Nama Jabatan / Posisi', 'Dibuat Pada'];
|
||||
}
|
||||
|
||||
public function map($position): array
|
||||
{
|
||||
static $number = 0;
|
||||
$number++;
|
||||
|
||||
return [
|
||||
$number,
|
||||
$position->name,
|
||||
$position->created_at ? Carbon::parse($position->created_at)->format('d-m-Y H:i') : '-',
|
||||
];
|
||||
}
|
||||
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
AfterSheet::class => function(AfterSheet $event) {
|
||||
$sheet = $event->sheet->getDelegate();
|
||||
$count = Position::count();
|
||||
$user = auth()->user()->first_name . ' ' . auth()->user()->last_name;
|
||||
$date = Carbon::now()->translatedFormat('d F Y - H:i:s');
|
||||
|
||||
$sheet->setCellValue('A1', 'LAPORAN MASTER DATA JABATAN');
|
||||
$sheet->setCellValue('A2', 'Total Data : ' . $count . ' Jabatan');
|
||||
$sheet->setCellValue('A3', 'Dicetak Oleh: ' . $user);
|
||||
$sheet->setCellValue('A4', 'Waktu Cetak : ' . $date);
|
||||
|
||||
$sheet->mergeCells('A1:C1');
|
||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||
$sheet->getStyle('A2:A4')->getFont()->setBold(true);
|
||||
$sheet->getStyle('A6:C6')->getFont()->setBold(true);
|
||||
|
||||
$sheet->getStyle('A6:C6')->getFill()
|
||||
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
|
||||
->getStartColor()->setARGB('FFE2E8F0');
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuditTrailController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Menarik data log terbaru, beserta relasi user yang melakukannya
|
||||
$logs = Activity::with('causer')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(50);
|
||||
|
||||
return view('pages.admin.audit.index', compact('logs'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use App\Models\Sop;
|
||||
use App\Models\ExamResult;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 1. Fungsi Dashboard Utama Admin
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$statistics = [
|
||||
// PENTING: Menggunakan whereHas() untuk mengecek relasi Role di tabel pivot
|
||||
'total_karyawan' => User::whereHas('roles', function ($query) {
|
||||
$query->where('name', 'trainee'); // Atau 'karyawan', sesuaikan dengan nama role di database Anda
|
||||
})->count(),
|
||||
|
||||
'total_trainer' => User::whereHas('roles', function ($query) {
|
||||
// Menghitung admin dan trainer
|
||||
$query->whereIn('name', ['trainer', 'admin']);
|
||||
})->count(),
|
||||
|
||||
'total_sop' => Sop::where('status', 'Active')->count(),
|
||||
|
||||
'lulus_ujian' => ExamResult::where('is_passed', true)->count(),
|
||||
];
|
||||
|
||||
// Pastikan path view ini benar-benar ada di resources/views/pages/admin/dashboard/dashboard.blade.php
|
||||
return view('pages.admin.dashboard.dashboard', compact('statistics'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. Fungsi untuk menu Departemen & Posisi (Master Data)
|
||||
*/
|
||||
public function masterIndex(): View
|
||||
{
|
||||
return view('pages.admin.master-data.index');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 4. Fungsi untuk menu Dokumen SOP
|
||||
*/
|
||||
public function sopIndex(): View
|
||||
{
|
||||
return view('pages.admin.sops.index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Http\Request;
|
||||
use Barryvdh\DomPDF\Facade\Pdf; // Jika pakai DomPDF
|
||||
use Maatwebsite\Excel\Facades\Excel; // Jika pakai Laravel-Excel
|
||||
|
||||
class DepartmentController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// 1. Inisialisasi query builder dari model Department
|
||||
$query = Department::query();
|
||||
|
||||
// 2. Fitur Pencarian (Filter)
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('name', 'like', "%{$search}%")
|
||||
->orWhere('code', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
// 3. Eksekusi query dengan pagination dan pertahankan parameter URL
|
||||
$departments = $query->latest()->paginate(10)->withQueryString();
|
||||
|
||||
// 4. Total rekor untuk ditampilkan di UI
|
||||
$totalRecords = $departments->total();
|
||||
|
||||
return view('pages.admin.departments.index', compact('departments', 'totalRecords'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('pages.admin.departments.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:departments,name',
|
||||
'code' => 'nullable|string|max:50|unique:departments,code',
|
||||
]);
|
||||
|
||||
Department::create($validated);
|
||||
|
||||
return redirect()->route('admin.departments.index')
|
||||
->with('success', 'Departemen baru berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function show(Department $department)
|
||||
{
|
||||
// Hanya melempar object $department ke view tanpa load relasi positions
|
||||
return view('pages.admin.departments.show', compact('department'));
|
||||
}
|
||||
|
||||
public function edit(Department $department)
|
||||
{
|
||||
return view('pages.admin.departments.edit', compact('department'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Department $department)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:departments,name,' . $department->id,
|
||||
'code' => 'nullable|string|max:50|unique:departments,code,' . $department->id,
|
||||
]);
|
||||
|
||||
$department->update($validated);
|
||||
|
||||
return redirect()->route('admin.departments.index')
|
||||
->with('success', 'Data Departemen berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function exportPdf(Request $request)
|
||||
{
|
||||
// 1. Ambil data sesuai filter/search saat ini
|
||||
$search = $request->search;
|
||||
$departments = Department::when($search, function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%");
|
||||
})->get();
|
||||
|
||||
// 2. Tangkap Metadata Siapa & Kapan
|
||||
$metadata = [
|
||||
'downloaded_by' => auth()->user()->first_name . ' ' . auth()->user()->last_name, // Mengambil nama admin yang login
|
||||
'download_time' => now()->translatedFormat('l, d F Y - H:i:s'), // Format waktu Indonesia
|
||||
'total_data' => $departments->count()
|
||||
];
|
||||
|
||||
// 3. Render View ke PDF
|
||||
$pdf = Pdf::loadView('pages.admin.departments.export-pdf', compact('departments', 'metadata'));
|
||||
|
||||
return $pdf->download('Data_Departemen_'.date('YmdHis').'.pdf');
|
||||
}
|
||||
|
||||
public function destroy(Department $department)
|
||||
{
|
||||
// Opsional: Cek apakah ada posisi terkait sebelum menghapus
|
||||
if ($department->positions()->count() > 0) {
|
||||
return back()->with('error', 'Gagal menghapus: Departemen ini masih memiliki Posisi/Jabatan aktif.');
|
||||
}
|
||||
|
||||
$department->delete();
|
||||
return redirect()->route('admin.departments.index')
|
||||
->with('success', 'Departemen berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\User;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use App\Models\TrainingMatrix;
|
||||
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$chartDept = User::select('department_id', DB::raw('count(*) as total'))
|
||||
->whereNotNull('department_id')
|
||||
->with('department')
|
||||
->groupBy('department_id')
|
||||
->get();
|
||||
|
||||
$chartPos = User::select('position_id', DB::raw('count(*) as total'))
|
||||
->whereNotNull('position_id')
|
||||
->with('position')
|
||||
->groupBy('position_id')
|
||||
->get();
|
||||
|
||||
$departments = Department::with('positions')->get();
|
||||
$positions = Position::all();
|
||||
|
||||
$deptPosMapping = $departments->mapWithKeys(function ($dept) {
|
||||
return [$dept->id => $dept->positions->map(function($pos) {
|
||||
return ['id' => $pos->id, 'name' => $pos->name];
|
||||
})];
|
||||
});
|
||||
|
||||
$query = User::with(['department', 'position', 'roles']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$columns = \Illuminate\Support\Facades\Schema::getColumnListing('users');
|
||||
|
||||
$query->where(function($q) use ($search, $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$q->orWhere($column, 'like', "%{$search}%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('department_id')) {
|
||||
$query->where('department_id', $request->department_id);
|
||||
}
|
||||
if ($request->filled('position_id')) {
|
||||
$query->where('position_id', $request->position_id);
|
||||
}
|
||||
|
||||
$employees = $query->latest()->paginate(10)->withQueryString();
|
||||
$totalRecords = $employees->total();
|
||||
|
||||
return view('pages.admin.employee.index', compact(
|
||||
'employees', 'totalRecords', 'departments', 'positions', 'chartDept', 'chartPos', 'deptPosMapping'
|
||||
));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$departments = Department::with('positions')->orderBy('name', 'asc')->get();
|
||||
$positions = Position::all();
|
||||
|
||||
$deptPosMapping = $departments->mapWithKeys(function ($dept) {
|
||||
return [$dept->id => $dept->positions->map(function($pos) {
|
||||
return ['id' => $pos->id, 'name' => $pos->name];
|
||||
})];
|
||||
});
|
||||
|
||||
$marketingDeptIds = Department::where('name', 'LIKE', '%MKT%')
|
||||
->orWhere('name', 'LIKE', '%MARKETING%')
|
||||
->pluck('id');
|
||||
|
||||
$trainingMatrices = TrainingMatrix::whereIn('department_id', $marketingDeptIds)->get();
|
||||
|
||||
$existingInitials = User::whereNotNull('initial')
|
||||
->pluck('initial')
|
||||
->map(fn($i) => strtolower($i))
|
||||
->toArray();
|
||||
|
||||
return view('pages.admin.employee.create', compact(
|
||||
'departments', 'positions', 'trainingMatrices', 'deptPosMapping', 'existingInitials'
|
||||
));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nik' => 'required|unique:users,nik',
|
||||
'identity_number' => 'nullable|string|max:50',
|
||||
'initial' => 'nullable|string|max:10',
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'nullable|string|max:255',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'role' => 'nullable|string',
|
||||
'is_trainer' => 'nullable|boolean',
|
||||
'gender' => 'required|in:L,P',
|
||||
'date_of_birth' => 'nullable|date',
|
||||
'join_date' => 'nullable|date',
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
'position_id' => 'required|exists:positions,id',
|
||||
'training_matrix_id' => 'nullable|integer',
|
||||
'documents.*' => 'nullable|file|max:5120',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$validated['password'] = Hash::make('Karyawan123!');
|
||||
$validated['role'] = $request->role ?? 'karyawan';
|
||||
$validated['must_change_password'] = true;
|
||||
|
||||
$userData = Arr::except($validated, ['documents', 'is_trainer']);
|
||||
|
||||
$user = User::create($userData);
|
||||
|
||||
$rolesToAssign = [];
|
||||
if ($validated['role'] == 'admin') {
|
||||
$rolesToAssign[] = 'admin';
|
||||
} elseif ($validated['role'] == 'trainer') {
|
||||
$rolesToAssign[] = 'trainer';
|
||||
$rolesToAssign[] = 'trainee';
|
||||
} else {
|
||||
$rolesToAssign[] = 'trainee';
|
||||
}
|
||||
|
||||
if ($request->has('is_trainer') && $request->is_trainer) {
|
||||
$rolesToAssign[] = 'trainer';
|
||||
$rolesToAssign[] = 'trainee';
|
||||
}
|
||||
|
||||
$user->assignRole(array_unique($rolesToAssign));
|
||||
|
||||
if ($request->hasFile('documents')) {
|
||||
foreach ($request->file('documents') as $file) {
|
||||
$fileName = time() . '_' . str_replace(' ', '_', $file->getClientOriginalName());
|
||||
$path = $file->storeAs('employee_docs/' . $user->nik, $fileName, 'nextcloud');
|
||||
|
||||
DB::table('employee_documents')->insert([
|
||||
'user_id' => $user->id,
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
'file_path' => $path,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('audit_trails')->insert([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => 'Create Employee',
|
||||
'description' => "Menambahkan karyawan baru: {$user->first_name} {$user->last_name} (NIK: {$user->nik})",
|
||||
'ip_address' => $request->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// FIX: Menggunakan rute jamak (employees)
|
||||
return redirect()->route('admin.employees.index')
|
||||
->with('success', "Data Karyawan {$user->first_name} berhasil ditambahkan! Password default: Karyawan123!");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return back()->with('error', 'Gagal menyimpan data! ' . $e->getMessage())->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function show(User $employee)
|
||||
{
|
||||
$employee->load(['department', 'position', 'roles']);
|
||||
return view('pages.admin.employee.show', compact('employee'));
|
||||
}
|
||||
|
||||
public function edit(User $employee)
|
||||
{
|
||||
$departments = Department::with('positions')->orderBy('name', 'asc')->get();
|
||||
$positions = Position::all();
|
||||
|
||||
$deptPosMapping = $departments->mapWithKeys(function ($dept) {
|
||||
return [$dept->id => $dept->positions->map(function($pos) {
|
||||
return ['id' => $pos->id, 'name' => $pos->name];
|
||||
})];
|
||||
});
|
||||
|
||||
$marketingDeptIds = Department::where('name', 'LIKE', '%MKT%')->orWhere('name', 'LIKE', '%MARKETING%')->pluck('id');
|
||||
$trainingMatrices = TrainingMatrix::whereIn('department_id', $marketingDeptIds)->get();
|
||||
|
||||
return view('pages.admin.employee.edit', compact('employee', 'departments', 'positions', 'trainingMatrices', 'deptPosMapping'));
|
||||
}
|
||||
|
||||
public function update(Request $request, User $employee)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'nik' => ['required', Rule::unique('users', 'nik')->ignore($employee->id)],
|
||||
'identity_number' => 'nullable|string|max:50',
|
||||
'initial' => 'nullable|string|max:10',
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'nullable|string|max:255',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($employee->id)],
|
||||
'role' => 'nullable|string',
|
||||
'is_trainer' => 'nullable|boolean',
|
||||
'gender' => 'required|in:L,P',
|
||||
'date_of_birth' => 'nullable|date',
|
||||
'join_date' => 'nullable|date',
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
'position_id' => 'required|exists:positions,id',
|
||||
'training_matrix_id' => 'nullable|integer',
|
||||
'is_active' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
if ($request->filled('password')) {
|
||||
$validated['password'] = Hash::make($request->password);
|
||||
}
|
||||
|
||||
$userData = Arr::except($validated, ['is_trainer', 'is_active']);
|
||||
$userData['is_active'] = $request->has('is_active');
|
||||
$employee->update($userData);
|
||||
|
||||
$rolesToAssign = [];
|
||||
$inputRole = $validated['role'] ?? 'karyawan';
|
||||
|
||||
if ($inputRole == 'admin') {
|
||||
$rolesToAssign[] = 'admin';
|
||||
} elseif ($inputRole == 'trainer' || $request->is_trainer) {
|
||||
$rolesToAssign[] = 'trainer';
|
||||
$rolesToAssign[] = 'trainee';
|
||||
} else {
|
||||
$rolesToAssign[] = 'trainee';
|
||||
}
|
||||
|
||||
$employee->syncRoles(array_unique($rolesToAssign));
|
||||
|
||||
DB::table('audit_trails')->insert([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => 'Update Employee',
|
||||
'description' => "Memperbarui data karyawan: {$employee->first_name} (NIK: {$employee->nik})",
|
||||
'ip_address' => $request->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
// FIX: Menggunakan rute jamak (employees)
|
||||
return redirect()->route('admin.employees.index')->with('success', "Data Karyawan {$employee->first_name} berhasil diperbarui.");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return back()->with('error', 'Gagal memperbarui data! ' . $e->getMessage())->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(User $employee)
|
||||
{
|
||||
if ($employee->hasRole('admin')) {
|
||||
return back()->with('error', 'Aksi ditolak! Anda tidak dapat menonaktifkan akun Administrator.');
|
||||
}
|
||||
|
||||
$nama = $employee->first_name;
|
||||
$employee->update(['is_active' => false]);
|
||||
|
||||
DB::table('audit_trails')->insert([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => 'Deactivate Employee',
|
||||
'description' => "Menonaktifkan karyawan (Resign/Keluar): {$nama}",
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
// FIX: Menggunakan rute jamak (employees)
|
||||
return redirect()->route('admin.employees.index')->with('success', "Karyawan {$nama} berhasil dinonaktifkan. Data historis tetap tersimpan.");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FITUR EXPORT & IMPORT
|
||||
// =========================================================================
|
||||
|
||||
private function getFilteredEmployees(Request $request)
|
||||
{
|
||||
$query = User::with(['department', 'position', 'roles'])->whereNotNull('department_id');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$columns = \Illuminate\Support\Facades\Schema::getColumnListing('users');
|
||||
|
||||
$query->where(function($q) use ($search, $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$q->orWhere($column, 'like', "%{$search}%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('department_id')) {
|
||||
$query->where('department_id', $request->department_id);
|
||||
}
|
||||
if ($request->filled('position_id')) {
|
||||
$query->where('position_id', $request->position_id);
|
||||
}
|
||||
|
||||
return $query->latest()->get(); // Ambil semua data TANPA pagination
|
||||
}
|
||||
|
||||
|
||||
public function exportExcel(Request $request)
|
||||
{
|
||||
// Ambil data yang sudah difilter
|
||||
$employees = $this->getFilteredEmployees($request);
|
||||
|
||||
$fileName = 'Data_Karyawan_' . date('Y-m-d_H-i') . '.xlsx';
|
||||
|
||||
// Lempar data ke Class Export
|
||||
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\EmployeesExport($employees), $fileName);
|
||||
}
|
||||
|
||||
|
||||
public function exportPdf(Request $request)
|
||||
{
|
||||
// Ambil data yang sudah difilter
|
||||
$employees = $this->getFilteredEmployees($request);
|
||||
|
||||
// PENTING: Perhatikan penamaan folder (employee tanpa 's')
|
||||
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pages.admin.employee.pdf', compact('employees'))
|
||||
->setPaper('a4', 'landscape');
|
||||
|
||||
return $pdf->download('Data_Karyawan_' . date('Y-m-d_H-i') . '.pdf');
|
||||
}
|
||||
|
||||
public function importView()
|
||||
{
|
||||
return view('pages.admin.employee.import');
|
||||
}
|
||||
|
||||
public function downloadTemplate()
|
||||
{
|
||||
// Menggunakan library Excel untuk menghasilkan format murni .xlsx
|
||||
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\EmployeeTemplateExport, 'Template_Import_Karyawan.xlsx');
|
||||
}
|
||||
|
||||
public function importPreview(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|mimes:xlsx,xls,csv|max:5120'
|
||||
]);
|
||||
|
||||
try {
|
||||
$rows = \Maatwebsite\Excel\Facades\Excel::toArray(new class implements \Maatwebsite\Excel\Concerns\ToModel, \Maatwebsite\Excel\Concerns\WithHeadingRow {
|
||||
public function model(array $row) { return $row; }
|
||||
}, $request->file('file'))[0];
|
||||
|
||||
session()->put('import_rows_data', $rows);
|
||||
|
||||
return view('pages.admin.employee.import-preview', compact('rows'));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('error', 'Gagal membaca format file Excel. Pastikan menggunakan template yang benar. Error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function importProcess(Request $request)
|
||||
{
|
||||
$rows = session('import_rows_data');
|
||||
|
||||
if (!$rows) {
|
||||
return redirect()->route('admin.employees.import')->with('error', 'Sesi import telah kedaluwarsa atau kosong. Silakan unggah ulang file Anda.');
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
if (empty($row['nik']) || empty($row['nama_depan']) || empty($row['email'])) {
|
||||
$failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$user = User::updateOrCreate(
|
||||
['nik' => $row['nik']], // Jika NIK sama, perbarui. Jika tidak ada, buat baru.
|
||||
[
|
||||
'identity_number' => $row['no_ktp'] ?? null,
|
||||
'initial' => !empty($row['inisial']) ? strtoupper($row['inisial']) : null,
|
||||
'first_name' => $row['nama_depan'],
|
||||
'last_name' => $row['nama_belakang'] ?? null,
|
||||
'email' => $row['email'],
|
||||
'phone' => $row['telepon'] ?? null,
|
||||
'gender' => in_array(strtoupper($row['jenis_kelamin'] ?? ''), ['L', 'P']) ? strtoupper($row['jenis_kelamin']) : 'L',
|
||||
'date_of_birth' => !empty($row['tgl_lahir']) ? \Carbon\Carbon::parse($row['tgl_lahir'])->format('Y-m-d') : null,
|
||||
'join_date' => !empty($row['tgl_masuk']) ? \Carbon\Carbon::parse($row['tgl_masuk'])->format('Y-m-d') : null,
|
||||
'department_id' => $row['department_id'] ?? 1,
|
||||
'position_id' => $row['position_id'] ?? 1,
|
||||
'password' => Hash::make('12345678'),
|
||||
'is_active' => true,
|
||||
'must_change_password' => true,
|
||||
]
|
||||
);
|
||||
if (!$user->hasAnyRole(['admin', 'trainer', 'trainee'])) {
|
||||
$user->assignRole('trainee');
|
||||
}
|
||||
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
session()->forget('import_rows_data');
|
||||
|
||||
return redirect()->route('admin.employees.index')
|
||||
->with('success', "Proses Import Selesai! Berhasil: {$successCount} data. Gagal/Dilewati: {$failCount} data.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use App\Models\TrainingMatrix;
|
||||
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();
|
||||
|
||||
// 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']);
|
||||
|
||||
// 3. Logika Filter Dinamis
|
||||
if ($request->filled('search')) {
|
||||
$query->where('question_text', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
if ($request->filled('department_id')) {
|
||||
$query->where('department_id', $request->department_id);
|
||||
}
|
||||
if ($request->filled('position_id')) {
|
||||
$query->where('position_id', $request->position_id);
|
||||
}
|
||||
// if ($request->filled('matrix_id')) {
|
||||
// $query->where('training_matrix_id', $request->matrix_id);
|
||||
// }
|
||||
if ($request->filled('question_type')) {
|
||||
$query->where('type', $request->question_type);
|
||||
}
|
||||
if ($request->filled('question_level')) {
|
||||
$query->where('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'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menghapus banyak soal sekaligus (Bulk Delete)
|
||||
*/
|
||||
public function bulkDelete(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'question_ids' => 'required|array',
|
||||
'question_ids.*' => 'exists:questions,id'
|
||||
]);
|
||||
|
||||
try {
|
||||
Question::whereIn('id', $request->question_ids)->delete();
|
||||
return back()->with('success', count($request->question_ids) . ' Soal berhasil dihapus secara massal.');
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('error', 'Gagal menghapus soal: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan Halaman Import Soal
|
||||
*/
|
||||
public function importView()
|
||||
{
|
||||
return view('pages.admin.exams.import-questions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MasterDataController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$departments = Department::withCount('users')->get();
|
||||
$positions = Position::withCount('users')->get();
|
||||
|
||||
return view('pages.admin.master.index', compact('departments', 'positions'));
|
||||
}
|
||||
|
||||
public function storeDepartment(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'code' => 'required|string|unique:departments,code'
|
||||
]);
|
||||
|
||||
Department::create($validated);
|
||||
return back()->with('success', 'Departemen baru berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function storePosition(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:positions,name'
|
||||
]);
|
||||
|
||||
Position::create($validated);
|
||||
return back()->with('success', 'Jabatan baru berhasil ditambahkan.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Position;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PositionController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = \App\Models\Position::query();
|
||||
|
||||
// Logika pencarian (jika ada input search)
|
||||
if ($request->has('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
// Ambil data tanpa memuat relasi 'department'
|
||||
$positions = $query->latest()->paginate(10);
|
||||
|
||||
return view('pages.admin.positions.index', compact('positions'));
|
||||
}
|
||||
|
||||
|
||||
public function create()
|
||||
{
|
||||
$departments = Department::orderBy('name', 'asc')->get();
|
||||
return view('pages.admin.positions.create', compact('departments'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
Position::create($validated);
|
||||
|
||||
return redirect()->route('admin.positions.index')
|
||||
->with('success', 'Posisi/Jabatan baru berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function show(Position $position)
|
||||
{
|
||||
// Hanya melempar object $department ke view tanpa load relasi positions
|
||||
return view('pages.admin.positions.show', compact('position'));
|
||||
}
|
||||
|
||||
public function edit(Position $position)
|
||||
{
|
||||
$departments = Department::orderBy('name', 'asc')->get();
|
||||
return view('pages.admin.positions.edit', compact('position', 'departments'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Position $position)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$position->update($validated);
|
||||
|
||||
return redirect()->route('admin.positions.index')
|
||||
->with('success', 'Data Posisi berhasil diperbarui.');
|
||||
}
|
||||
// Tambahkan ini di bagian fungsi-fungsi paling bawah
|
||||
public function exportExcel()
|
||||
{
|
||||
$fileName = 'Master_Data_Jabatan_' . date('Ymd_Hi') . '.xlsx';
|
||||
return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\PositionsExport, $fileName);
|
||||
}
|
||||
|
||||
public function exportPdf()
|
||||
{
|
||||
$positions = \App\Models\Position::all();
|
||||
|
||||
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pages.admin.positions.export-pdf', compact('positions'))
|
||||
->setPaper('a4', 'portrait');
|
||||
|
||||
return $pdf->download('Master_Data_Jabatan_' . date('Ymd_Hi') . '.pdf');
|
||||
}
|
||||
|
||||
public function destroy(Position $position)
|
||||
{
|
||||
$position->delete();
|
||||
return redirect()->route('admin.positions.index')
|
||||
->with('success', 'Posisi berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\ExamResult;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menampilkan daftar Karyawan (Trainee) beserta ringkasan progres mereka.
|
||||
*/
|
||||
public function training()
|
||||
{
|
||||
// Ambil semua user dengan role 'trainee', beserta relasi hasil ujiannya
|
||||
$trainees = User::whereHas('roles', function ($query) {
|
||||
$query->where('name', 'trainee');
|
||||
})
|
||||
->with('examResults') // Pastikan relasi examResults sudah dibuat di model User
|
||||
->latest()
|
||||
->paginate(15);
|
||||
|
||||
return view('pages.admin.reports.training', compact('trainees'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menampilkan detail riwayat training & ujian per individu.
|
||||
*/
|
||||
public function showTraining(User $user)
|
||||
{
|
||||
// Pastikan user tersebut benar-benar trainee
|
||||
if (!$user->hasRole('trainee')) {
|
||||
abort(404, 'Data riwayat training tidak ditemukan atau user bukan karyawan.');
|
||||
}
|
||||
|
||||
// Ambil riwayat ujian, relasikan ke data Soal/Sesi (Exam) dan Dokumen (SOP)
|
||||
$examHistory = ExamResult::where('user_id', $user->id)
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
// Kalkulasi Statistik Cepat
|
||||
$stats = [
|
||||
'total_exams' => $examHistory->count(),
|
||||
'passed' => $examHistory->where('is_passed', true)->count(),
|
||||
'failed' => $examHistory->where('is_passed', false)->count(),
|
||||
'avg_score' => $examHistory->avg('score') ?? 0,
|
||||
];
|
||||
|
||||
return view('pages.admin.reports.show', compact('user', 'examHistory', 'stats'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sop;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SopController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$sops = Sop::orderBy('sop_code', 'asc')->paginate(15);
|
||||
return view('pages.admin.sop.index', compact('sops'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('pages.admin.sop.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'sop_code' => 'required|unique:sops,sop_code',
|
||||
'category_name' => 'nullable|string',
|
||||
'version' => 'required|string',
|
||||
'status' => 'required|in:Draft,Active,Obsolete',
|
||||
]);
|
||||
|
||||
Sop::create($validated);
|
||||
|
||||
return redirect()->route('admin.sops.index')->with('success', 'Dokumen SOP berhasil didaftarkan.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Cbt;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Exam;
|
||||
use App\Models\ExamResult;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ExamSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menampilkan halaman persiapan/ruang ujian
|
||||
*/
|
||||
public function show(Exam $exam)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// 1. Validasi: Cek apakah user sudah pernah lulus ujian ini
|
||||
$existingResult = ExamResult::where('user_id', $user->id)
|
||||
->where('exam_id', $exam->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
if ($existingResult && $existingResult->is_passed) {
|
||||
return redirect()->route('cbt.dashboard')
|
||||
->with('error', 'Anda sudah pernah lulus ujian ini. Tidak perlu mengulang.');
|
||||
}
|
||||
|
||||
// 2. Jika aman, arahkan ke View Ruang Ujian yang memuat komponen Livewire
|
||||
return view('pages.cbt.exam-room', compact('exam'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Cbt;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Exam;
|
||||
use App\Models\ExamResult;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class UserDashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Menampilkan Dashboard Portal Ujian Karyawan
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Ambil data user yang sedang login
|
||||
$user = Auth::user();
|
||||
|
||||
// Tarik riwayat ujian yang pernah diikuti karyawan ini beserta detail ujiannya
|
||||
$examHistory = ExamResult::where('user_id', $user->id)
|
||||
->with('exam')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Tarik daftar ujian yang tersedia (Bisa ditambahkan filter berdasarkan departemen/jabatan nanti)
|
||||
$availableExams = Exam::orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Kirim data ke view dashboard CBT
|
||||
return view('pages.cbt.dashboard.dashboard', compact('user', 'examHistory', 'availableExams'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ForcePasswordChange
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Cek jika user login DAN wajib ganti password
|
||||
if (Auth::check() && Auth::user()->must_change_password) {
|
||||
|
||||
// Pengecualian agar tidak terjebak dalam loop:
|
||||
// Izinkan akses ke rute ganti password dan rute logout
|
||||
$allowedRoutes = ['password.force-change', 'password.force-update', 'logout'];
|
||||
|
||||
if (!in_array($request->route()->getName(), $allowedRoutes)) {
|
||||
return redirect()->route('password.force-change');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EmployeesImport implements ToCollection, WithHeadingRow
|
||||
{
|
||||
public $successCount = 0;
|
||||
public $failCount = 0;
|
||||
|
||||
/**
|
||||
* Asumsi Header Excel (Baris ke-1):
|
||||
* nik | nama_depan | nama_belakang | email | telepon | jenis_kelamin | department_id | position_id
|
||||
*/
|
||||
public function collection(Collection $rows)
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
// Validasi: Abaikan baris jika NIK, Nama Depan, atau Email kosong
|
||||
if (empty($row['nik']) || empty($row['nama_depan']) || empty($row['email'])) {
|
||||
$this->failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update jika NIK sudah ada, Create jika belum ada
|
||||
$user = User::updateOrCreate(
|
||||
['nik' => $row['nik']], // Pencarian berdasarkan NIK
|
||||
[
|
||||
'first_name' => $row['nama_depan'],
|
||||
'last_name' => $row['nama_belakang'] ?? null,
|
||||
'email' => $row['email'],
|
||||
'phone' => $row['telepon'] ?? null,
|
||||
'gender' => in_array(strtoupper($row['jenis_kelamin'] ?? ''), ['L', 'P']) ? strtoupper($row['jenis_kelamin']) : 'L',
|
||||
'department_id' => $row['department_id'] ?? 1, // Fallback ID 1 jika kosong
|
||||
'position_id' => $row['position_id'] ?? 1,
|
||||
'password' => Hash::make('Karyawan123!'),
|
||||
'is_active' => true,
|
||||
'must_change_password' => true,
|
||||
]
|
||||
);
|
||||
|
||||
// Otomatis jadikan Trainee
|
||||
if (!$user->hasAnyRole(['admin', 'trainer', 'trainee'])) {
|
||||
$user->assignRole('trainee');
|
||||
}
|
||||
|
||||
$this->successCount++;
|
||||
} catch (\Exception $e) {
|
||||
// Jika error (misal format email salah atau duplikat), masukkan ke hitungan gagal
|
||||
$this->failCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Actions;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class Logout
|
||||
{
|
||||
/**
|
||||
* Log the current user out of the application.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
Session::invalidate();
|
||||
Session::regenerateToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class LoginForm extends Form
|
||||
{
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string')]
|
||||
public string $password = '';
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
// 1. Jalankan proses pencocokan email & password
|
||||
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. Ambil data user yang berhasil login
|
||||
$user = Auth::user();
|
||||
|
||||
// 3. VALIDASI RBAC: Pastikan user memiliki minimal satu role aktif di database
|
||||
// Ini mencegah user tanpa role (data menggantung) bisa masuk ke sistem
|
||||
if ($user->roles()->count() === 0) {
|
||||
// Jika tidak punya role, paksa logout demi keamanan
|
||||
Auth::logout();
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerateToken();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => 'Akun Anda aktif, namun belum dikonfigurasi memiliki Role (Akses) oleh Admin. Silakan hubungi IT/HRD.',
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the authentication request is not rate limited.
|
||||
*/
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiting throttle key.
|
||||
*/
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Matrix;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\User;
|
||||
use App\Models\ExamResult;
|
||||
|
||||
class ComplianceMonitor extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
// Contoh logika sederhana: Menghitung persentase karyawan yang sudah lulus ujian
|
||||
$totalKaryawan = User::where('role', 'karyawan')->count();
|
||||
|
||||
// Hitung karyawan unik yang sudah memiliki minimal 1 hasil lulus
|
||||
$karyawanLulus = ExamResult::where('is_passed', true)
|
||||
->distinct('user_id')
|
||||
->count('user_id');
|
||||
|
||||
$complianceRate = $totalKaryawan > 0 ? round(($karyawanLulus / $totalKaryawan) * 100) : 0;
|
||||
|
||||
return view('livewire.matrix.compliance-monitor', [
|
||||
'complianceRate' => $complianceRate,
|
||||
'totalKaryawan' => $totalKaryawan,
|
||||
'karyawanLulus' => $karyawanLulus
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Department extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'code'];
|
||||
|
||||
// public function users(): HasMany
|
||||
// {
|
||||
// return $this->hasMany(User::class);
|
||||
// }
|
||||
|
||||
// Relasi ke Posisi (dari tabel class_sections lama)
|
||||
public function positions()
|
||||
{
|
||||
return $this->belongsToMany(Position::class, 'department_position');
|
||||
}
|
||||
|
||||
// Relasi ke Karyawan (Users) untuk menghitung total karyawan di departemen ini
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class, 'department_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Exam extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'old_id',
|
||||
'title',
|
||||
'duration',
|
||||
'passing_percentage'
|
||||
];
|
||||
|
||||
public function results(): HasMany
|
||||
{
|
||||
return $this->hasMany(ExamResult::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ExamResult extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'exam_id',
|
||||
'is_attempted',
|
||||
'score',
|
||||
'is_passed'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_attempted' => 'boolean',
|
||||
'is_passed' => 'boolean',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function exam(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Exam::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Position extends Model
|
||||
{
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class, 'position_id');
|
||||
}
|
||||
|
||||
public function departments()
|
||||
{
|
||||
return $this->belongsToMany(Department::class, 'department_position');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Question extends Model
|
||||
{
|
||||
// Pastikan ini sesuai dengan nama tabel Anda di database (misalnya 'questions')
|
||||
protected $table = 'questions';
|
||||
|
||||
// Sesuaikan array ini dengan nama kolom yang ada di database Anda
|
||||
protected $fillable = [
|
||||
'question_text',
|
||||
'category',
|
||||
'type',
|
||||
'answer_a',
|
||||
'answer_b',
|
||||
'answer_c',
|
||||
'answer_d',
|
||||
'correct_answer'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class QuestionBank extends Model
|
||||
{
|
||||
// Opsional, tapi sangat disarankan agar Laravel tidak salah menebak nama tabel
|
||||
protected $table = 'question_banks';
|
||||
|
||||
protected $fillable = [
|
||||
// Atribut Soal dari desain Anda
|
||||
'old_id',
|
||||
'subject',
|
||||
'question_type',
|
||||
'question_level',
|
||||
'passing_grade',
|
||||
'duration',
|
||||
'question_text',
|
||||
'option_a',
|
||||
'option_b',
|
||||
'option_c',
|
||||
'option_d',
|
||||
'option_e',
|
||||
'correct_answer',
|
||||
|
||||
// PENTING: Atribut Relasi yang dibutuhkan form Create/Edit & Filter Index
|
||||
'department_id',
|
||||
'position_id',
|
||||
'training_matrix_id',
|
||||
'created_by'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'correct_answer' => 'array', // Otomatis ubah JSON di database jadi Array di PHP
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| RELASI DATABASE (Diperlukan agar Filter Dropdown & Tabel berfungsi)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function department()
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
|
||||
public function position()
|
||||
{
|
||||
return $this->belongsTo(Position::class, 'position_id');
|
||||
}
|
||||
|
||||
public function matrix()
|
||||
{
|
||||
return $this->belongsTo(TrainingMatrix::class, 'training_matrix_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
// Merujuk ke tabel users untuk mengetahui siapa staff/admin yang buat soal
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
// 1. Baris Spatie di-comment/dihapus
|
||||
// use Spatie\Activitylog\Traits\LogsActivity;
|
||||
// use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Sop extends Model
|
||||
{
|
||||
// 2. Trait LogsActivity di-comment/dihapus
|
||||
// use LogsActivity;
|
||||
|
||||
protected $table = 'sops';
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'sop_code',
|
||||
'category_name',
|
||||
'version',
|
||||
'revision_history',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'revision_history' => 'array',
|
||||
];
|
||||
|
||||
// 3. Fungsi getActivitylogOptions di-comment/dihapus
|
||||
/*
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logFillable()
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs()
|
||||
->setDescriptionForEvent(fn(string $eventName) => "SOP {$eventName}");
|
||||
}
|
||||
*/
|
||||
|
||||
public function trainingMatrices(): HasMany
|
||||
{
|
||||
return $this->hasMany(TrainingMatrix::class, 'sop_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class TrainingMatrix extends Model
|
||||
{
|
||||
protected $table = 'training_matrices';
|
||||
|
||||
protected $fillable = [
|
||||
'department_id',
|
||||
'position_id',
|
||||
'sop_id'
|
||||
];
|
||||
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function position(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class);
|
||||
}
|
||||
|
||||
public function sop(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Sop::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable, HasRoles;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
* Kolom-kolom ini wajib didaftarkan agar form Create & Edit tidak diblokir Laravel.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'nik',
|
||||
'identity_number',
|
||||
'initial',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'phone',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
'gender',
|
||||
'date_of_birth',
|
||||
'join_date',
|
||||
'photo',
|
||||
'department_id',
|
||||
'position_id',
|
||||
'training_matrix_id',
|
||||
'old_id',
|
||||
'old_password_hash',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'old_password_hash',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mengubah format string dari database menjadi format Date/Time Carbon otomatis.
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'date_of_birth' => 'date',
|
||||
'join_date' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// RELASI ANTAR TABEL (MASTER DATA)
|
||||
// =========================================================================
|
||||
|
||||
// Relasi ke tabel departments
|
||||
public function department()
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
|
||||
// Relasi ke tabel positions
|
||||
public function position()
|
||||
{
|
||||
return $this->belongsTo(Position::class, 'position_id');
|
||||
}
|
||||
|
||||
// Jika model TrainingMatrix sudah Anda buat, buka komentar di bawah ini:
|
||||
/*
|
||||
public function trainingMatrix()
|
||||
{
|
||||
return $this->belongsTo(TrainingMatrix::class);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserDocument extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'document_name',
|
||||
'file_path',
|
||||
'file_type'
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\WebDAV\WebDAVAdapter;
|
||||
use Sabre\DAV\Client;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Mencatat setiap kali user Login
|
||||
Event::listen(function (Login $event) {
|
||||
activity('autentikasi')
|
||||
->causedBy($event->user)
|
||||
->log('User berhasil login ke dalam sistem');
|
||||
});
|
||||
|
||||
// Mencatat setiap kali user Logout
|
||||
Event::listen(function (Logout $event) {
|
||||
activity('autentikasi')
|
||||
->causedBy($event->user)
|
||||
->log('User keluar (logout) dari sistem');
|
||||
});
|
||||
|
||||
// Mendaftarkan Custom Driver Nextcloud (WebDAV)
|
||||
Storage::extend('webdav', function ($app, $config) {
|
||||
$client = new Client([
|
||||
'baseUri' => $config['baseUri'],
|
||||
'userName' => $config['userName'],
|
||||
'password' => $config['password'],
|
||||
]);
|
||||
|
||||
$adapter = new WebDAVAdapter($client, $config['pathPrefix'] ?? '');
|
||||
$driver = new Filesystem($adapter);
|
||||
|
||||
return new FilesystemAdapter($driver, $adapter, $config);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
class VoltServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Volt::mount([
|
||||
config('livewire.view_path', resource_path('views/livewire')),
|
||||
resource_path('views/pages'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.guest');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user