Initial commit - lms-v2 + CLAUDE.md

This commit is contained in:
Iwit
2026-05-30 22:15:16 +07:00
commit 5811409e2d
183 changed files with 23225 additions and 0 deletions
@@ -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'));
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}