Sync SOP ke e-doc Plant & Add Subject dan penyempurnaan bagian matrix training karyawan

This commit is contained in:
Iwit
2026-06-04 19:01:17 +07:00
parent 91c0a8147f
commit 1089f60a3d
26 changed files with 1241 additions and 155 deletions
@@ -8,6 +8,7 @@ use App\Models\User;
use App\Models\Sop;
use App\Models\ExamResult;
use Illuminate\View\View;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
@@ -44,13 +45,193 @@ class DashboardController extends Controller
return view('pages.admin.master-data.index');
}
public function openSopFile($id)
{
// 1. Cari tahu versi terbaru dari dokumen ini di database
$sop = \Illuminate\Support\Facades\DB::connection('plantdoc')
->table('tblDocumentContent')
->where('document', $id)
->orderBy('version', 'desc')
->first();
// Jika file tidak ada di database, kembalikan ke halaman sebelumnya dengan pesan error
if (!$sop) {
return back()->with('error', 'File PDF untuk dokumen ini belum diunggah atau tidak ditemukan.');
}
$versi = $sop->version;
// 2. Susun URL persis seperti fitur "View Online" bawaan SeedDMS
$seeddmsUrl = "http://192.168.10.11/plantlib/plantlibrary/op/op.ViewOnline.php?documentid=" . $id . "&version=" . $versi;
// 3. Lontarkan pengguna ke URL tersebut
return redirect()->away($seeddmsUrl);
}
/**
* 4. Fungsi untuk menu Dokumen SOP
*/
public function sopIndex(): View
public function sopIndex(\Illuminate\Http\Request $request)
{
return view('pages.admin.sop.index');
$search = $request->input('search');
$department = $request->input('department');
$category = $request->input('category');
$version = $request->input('version');
// 1. Departemen
$departments = \Illuminate\Support\Facades\DB::connection('plantdoc')
->table('tblFolders')
->where('parent', 59)
->orderBy('name', 'asc')
->get();
// 2. Kategori
$categories = \Illuminate\Support\Facades\DB::connection('plantdoc')
->table('tblCategory')
->orderBy('name', 'asc')
->get();
// 3. Versi: Diambil dengan Logika Fallback (Cek Nomor -> Cek Judul)
$allDocs = \Illuminate\Support\Facades\DB::connection('plantdoc')
->table('tblDocuments')
->select('name', 'comment')
->get();
$versions = $allDocs->map(function ($doc) {
// Prioritas 1: 2 Karakter Terakhir dari Nomor SOP
$vName = substr(trim($doc->name), -2);
if (is_numeric($vName)) return $vName;
// Prioritas 2: 2 Karakter Terakhir dari Judul SOP
$vComment = substr(trim($doc->comment), -2);
if (is_numeric($vComment)) return $vComment;
return null; // Abaikan jika keduanya tidak memiliki akhiran angka
})->filter()->unique()->sort()->values();
// 4. Query Utama
$query = \Illuminate\Support\Facades\DB::connection('plantdoc')
->table('tblDocuments as d')
->leftJoin('tblFolders as f_doc', 'd.folder', '=', 'f_doc.id')
->leftJoin('tblFolders as f_parent', 'f_doc.parent', '=', 'f_parent.id')
->leftJoin('tblDocumentCategory as dc', 'd.id', '=', 'dc.documentID')
->leftJoin('tblCategory as cat', 'dc.categoryID', '=', 'cat.id')
->leftJoin('tblDocumentContent as c', 'd.id', '=', 'c.document')
->select(
'd.id',
'd.name as nomor_sop',
'd.comment as judul_sop',
'c.orgFileName as file_pdf',
'c.dir as direktori',
'd.date as tanggal_dibuat',
'cat.name as nama_kategori',
\Illuminate\Support\Facades\DB::raw("
CASE
WHEN f_doc.parent = 59 THEN f_doc.name
WHEN f_parent.parent = 59 THEN f_parent.name
ELSE f_doc.name
END as nama_departemen
")
);
// 5. Logika Filter
if ($search) {
$query->where(function($q) use ($search) {
$q->where('d.name', 'LIKE', "%{$search}%")
->orWhere('d.comment', 'LIKE', "%{$search}%");
});
}
if ($department) {
$query->where(function($q) use ($department) {
$q->where('f_doc.id', $department)
->orWhere('f_doc.parent', $department)
->orWhere('f_doc.folderList', 'LIKE', "%:{$department}:%");
});
}
if ($category) {
$query->where('dc.categoryID', $category);
}
// Filter Versi Cerdas SQL (Jika 2 digit nama bukan angka, cari di judul)
if ($version !== null && $version !== '') {
$query->where(function($q) use ($version) {
$q->whereRaw("RIGHT(TRIM(d.name), 2) = ?", [$version])
->orWhere(function($sub) use ($version) {
$sub->whereRaw("RIGHT(TRIM(d.name), 2) NOT REGEXP '^[0-9]+$'")
->whereRaw("RIGHT(TRIM(d.comment), 2) = ?", [$version]);
});
});
}
// 6. Logika Ekspor dengan Metadata
if ($request->has('export')) {
$dataExport = $query->orderBy('d.date', 'desc')->get();
$totalRecords = $dataExport->count();
$downloader = auth()->user()->first_name . ' ' . auth()->user()->last_name;
$downloadTime = now()->format('d M Y H:i:s');
if ($request->export == 'excel') {
return $this->exportCsv($dataExport, $totalRecords, $downloader, $downloadTime);
} elseif ($request->export == 'pdf') {
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pages.admin.sops.pdf', compact('dataExport', 'totalRecords', 'downloader', 'downloadTime'))
->setPaper('a4', 'landscape');
return $pdf->download('Data_Dokumen_Tunggal_Pharma.pdf');
}
}
$sops = $query->orderBy('d.date', 'desc')->paginate(20);
return view('pages.admin.sops.index', compact('sops', 'departments', 'categories', 'versions'));
}
private function exportCsv($data, $totalRecords, $downloader, $downloadTime)
{
$fileName = 'Daftar_Dokumen_' . date('Ymd_His') . '.csv';
$headers = [
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=$fileName",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
];
$callback = function() use($data, $totalRecords, $downloader, $downloadTime) {
$file = fopen('php://output', 'w');
fputcsv($file, ['DAFTAR MASTER DOKUMEN - TUNGGAL PHARMA']);
fputcsv($file, ['Diunduh Oleh:', $downloader]);
fputcsv($file, ['Waktu Unduh:', $downloadTime . ' WIB']);
fputcsv($file, ['Total Record:', $totalRecords . ' Dokumen']);
fputcsv($file, []);
fputcsv($file, ['No', 'Departemen/Folder', 'Kategori', 'Nomor Dokumen', 'Judul Dokumen', 'Versi', 'Tanggal Upload']);
$i = 1;
foreach ($data as $row) {
// Logika Ekstraksi Versi untuk Excel
$versi = substr(trim($row->nomor_sop), -2);
if (!is_numeric($versi)) {
$versi = substr(trim($row->judul_sop), -2);
if (!is_numeric($versi)) $versi = '-';
}
$tanggal = \Carbon\Carbon::createFromTimestamp($row->tanggal_dibuat)->format('d-m-Y');
fputcsv($file, [
$i++,
$row->nama_departemen ?? '-',
$row->nama_kategori ?? '-',
$row->nomor_sop,
$row->judul_sop,
"v." . $versi,
$tanggal
]);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
}
@@ -87,9 +87,12 @@ class EmployeeController extends Controller
->pluck('initial')
->map(fn($i) => strtolower($i))
->toArray();
// [TAMBAHAN] Menarik daftar Materi/SOP untuk ditampilkan di form
$subjects = \App\Models\Subject::orderBy('name', 'asc')->get();
return view('pages.admin.employee.create', compact(
'departments', 'positions', 'trainingMatrices', 'deptPosMapping', 'existingInitials'
'departments', 'positions', 'trainingMatrices', 'deptPosMapping', 'existingInitials', 'subjects'
));
}
@@ -112,6 +115,7 @@ class EmployeeController extends Controller
'position_id' => 'required|exists:positions,id',
'training_matrix_id' => 'nullable|integer',
'documents.*' => 'nullable|file|max:5120',
'trainings' => 'nullable|array', // Validasi input matrix
]);
DB::beginTransaction();
@@ -121,10 +125,11 @@ class EmployeeController extends Controller
$validated['role'] = $request->role ?? 'karyawan';
$validated['must_change_password'] = true;
$userData = Arr::except($validated, ['documents', 'is_trainer']);
$userData = Arr::except($validated, ['documents', 'is_trainer', 'trainings']);
$user = User::create($userData);
// Pengaturan Hak Akses (Role)
$rolesToAssign = [];
if ($validated['role'] == 'admin') {
$rolesToAssign[] = 'admin';
@@ -142,6 +147,18 @@ class EmployeeController extends Controller
$user->assignRole(array_unique($rolesToAssign));
// [TAMBAHAN] Menyimpan Training Matrix ke tabel Pivot (employee_subject)
if ($request->has('trainings')) {
$matrixData = [];
foreach ($request->trainings as $subjectId => $training) {
if (isset($training['selected']) && $training['selected'] == 1) {
$matrixData[$subjectId] = ['status' => $training['status']];
}
}
$user->trainings()->sync($matrixData);
}
// Menyimpan Dokumen
if ($request->hasFile('documents')) {
foreach ($request->file('documents') as $file) {
$fileName = time() . '_' . str_replace(' ', '_', $file->getClientOriginalName());
@@ -167,7 +184,6 @@ class EmployeeController extends Controller
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!");
@@ -179,12 +195,16 @@ class EmployeeController extends Controller
public function show(User $employee)
{
$employee->load(['department', 'position', 'roles']);
// [TAMBAHAN] Memuat relasi 'trainings' agar matrix terlihat di halaman Detail
$employee->load(['department', 'position', 'roles', 'trainings']);
return view('pages.admin.employee.show', compact('employee'));
}
public function edit(User $employee)
{
// [TAMBAHAN] Memuat relasi trainings yang sudah dimiliki karyawan
$employee->load('trainings');
$departments = Department::with('positions')->orderBy('name', 'asc')->get();
$positions = Position::all();
@@ -197,7 +217,10 @@ class EmployeeController extends Controller
$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'));
// [TAMBAHAN] Menarik daftar semua Materi/SOP
$subjects = \App\Models\Subject::orderBy('name', 'asc')->get();
return view('pages.admin.employee.edit', compact('employee', 'departments', 'positions', 'trainingMatrices', 'deptPosMapping', 'subjects'));
}
public function update(Request $request, User $employee)
@@ -219,6 +242,7 @@ class EmployeeController extends Controller
'position_id' => 'required|exists:positions,id',
'training_matrix_id' => 'nullable|integer',
'is_active' => 'nullable|boolean',
'trainings' => 'nullable|array', // Validasi input matrix
]);
DB::beginTransaction();
@@ -228,10 +252,11 @@ class EmployeeController extends Controller
$validated['password'] = Hash::make($request->password);
}
$userData = Arr::except($validated, ['is_trainer', 'is_active']);
$userData = Arr::except($validated, ['is_trainer', 'is_active', 'trainings']);
$userData['is_active'] = $request->has('is_active');
$employee->update($userData);
// Perbarui Hak Akses (Role)
$rolesToAssign = [];
$inputRole = $validated['role'] ?? 'karyawan';
@@ -246,6 +271,22 @@ class EmployeeController extends Controller
$employee->syncRoles(array_unique($rolesToAssign));
// [TAMBAHAN] Memperbarui Training Matrix
if ($request->has('trainings')) {
$matrixData = [];
foreach ($request->trainings as $subjectId => $training) {
// Cek jika dicentang
if (isset($training['selected']) && $training['selected'] == 1) {
$matrixData[$subjectId] = ['status' => $training['status']];
}
}
// sync() akan secara otomatis menghapus yang tidak dicentang dan menambah/memperbarui yang dicentang
$employee->trainings()->sync($matrixData);
} else {
// Jika tidak ada materi yang dicentang sama sekali, kosongkan riwayat trainingnya
$employee->trainings()->detach();
}
DB::table('audit_trails')->insert([
'user_id' => auth()->id(),
'action' => 'Update Employee',
@@ -256,7 +297,6 @@ class EmployeeController extends Controller
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) {
@@ -264,7 +304,7 @@ class EmployeeController extends Controller
return back()->with('error', 'Gagal memperbarui data! ' . $e->getMessage())->withInput();
}
}
public function destroy(User $employee)
{
if ($employee->hasRole('admin')) {
@@ -6,54 +6,60 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Department;
use App\Models\Position;
use App\Models\Subject; // PASTIKAN SUBJECT DI-IMPORT
use App\Models\Subject;
use App\Models\User;
use App\Models\QuestionBank;
class ExamManagementController extends Controller
{
public function questionBank(Request $request)
public function questionBank(Request $request)
{
$departments = Department::orderBy('name', 'asc')->get();
$positions = Position::orderBy('name', 'asc')->get();
$subjects = Subject::orderBy('name', 'asc')->get(); // Mengambil data Subject
$creators = User::whereHas('roles', function($q){
$q->whereIn('name', ['admin', 'trainer']);
})->orderBy('first_name', 'asc')->get();
// 1. Ambil ID unik siapa saja yang PERNAH membuat soal di tabel question_banks
$creatorIds = \App\Models\QuestionBank::whereNotNull('created_by')
->distinct()
->pluck('created_by');
// Load relasi subject, department, position, dan creator
$query = QuestionBank::with(['subject', 'department', 'position', 'creator']);
// 2. Tarik data User HANYA berdasarkan ID pembuat soal tersebut
$creators = \App\Models\User::whereIn('id', $creatorIds)
->orderBy('first_name', 'asc')
->get();
// Logika Filter
if ($request->filled('search')) {
$query->where('question_text', 'like', '%' . $request->search . '%');
// 3. Tarik data pendukung lainnya untuk filter & dropdown
$subjects = \App\Models\Subject::orderBy('name', 'asc')->get();
$departments = \App\Models\Department::orderBy('name', 'asc')->get();
$positions = \App\Models\Position::orderBy('name', 'asc')->get();
// 4. Hitung Total Soal Keseluruhan (TANPA FILTER) untuk Badge di View
$totalQuestions = \App\Models\QuestionBank::count(); // <--- TAMBAHKAN BARIS INI
// 5. Query utama Bank Soal (dengan fitur pencarian/filter)
$query = \App\Models\QuestionBank::with(['subject', 'department', 'creator', 'position']);
// Filter berdasarkan pembuat soal
if ($request->filled('creator_id')) {
$query->where('created_by', $request->creator_id);
}
// Filter berdasarkan subject/materi
if ($request->filled('subject_id')) {
$query->where('subject_id', $request->subject_id);
}
// Filter berdasarkan departemen
if ($request->filled('department_id')) {
$query->where('department_id', $request->department_id);
}
// Filter berdasarkan posisi/jabatan
if ($request->filled('position_id')) {
$query->where('position_id', $request->position_id);
}
if ($request->filled('subject_id')) { // Filter berdasarkan Subject
$query->where('subject_id', $request->subject_id);
}
if ($request->filled('question_type')) {
$query->where('question_type', $request->question_type);
}
if ($request->filled('question_level')) {
$query->where('question_level', $request->question_level);
}
if ($request->filled('created_by')) {
$query->where('created_by', $request->created_by);
}
$questions = $query->latest()->paginate(15)->withQueryString();
$totalQuestions = $questions->total();
// Pagination
$questions = $query->latest()->paginate(20);
return view('pages.admin.exams.question-bank', compact(
'questions', 'totalQuestions', 'departments', 'positions', 'subjects', 'creators'
));
// Pastikan $totalQuestions ikut dikirim ke compact
return view('pages.admin.question-banks.question-bank', compact('questions', 'creators', 'subjects', 'departments', 'positions', 'totalQuestions'));
}
// Fungsi untuk menampilkan halaman Tambah Soal
@@ -63,7 +69,7 @@ class ExamManagementController extends Controller
$positions = Position::orderBy('name', 'asc')->get();
$subjects = Subject::orderBy('name', 'asc')->get(); // Kirim Subject ke form
return view('pages.admin.exams.create', compact('departments', 'positions', 'subjects'));
return view('pages.admin.question-banks.create', compact('departments', 'positions', 'subjects'));
}
// Fungsi untuk menampilkan halaman Edit Soal
@@ -74,14 +80,14 @@ class ExamManagementController extends Controller
$positions = Position::orderBy('name', 'asc')->get();
$subjects = Subject::orderBy('name', 'asc')->get(); // Kirim Subject ke form
return view('pages.admin.exams.edit', compact('question', 'departments', 'positions', 'subjects'));
return view('pages.admin.question-banks.edit', compact('question', 'departments', 'positions', 'subjects'));
}
// Fungsi Show untuk Detail Soal
public function show($id)
{
$question = QuestionBank::with(['subject', 'department', 'position', 'creator'])->findOrFail($id);
return view('pages.admin.exams.show', compact('question'));
return view('pages.admin.question-banks.show', compact('question'));
}
/**
@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Subject;
use Illuminate\Http\Request;
class SubjectController extends Controller
{
public function index()
{
// Tarik data materi LMS
$subjects = Subject::withCount('questions')->orderBy('name', 'asc')->paginate(20);
// Tarik data SOP dari database server 11 untuk opsi Dropdown
$sops = \Illuminate\Support\Facades\DB::connection('plantdoc')
->table('tblDocuments')
->select('name as nomor', 'comment as judul')
->orderBy('name', 'asc')
->get();
// Pastikan $sops dikirim ke view
return view('pages.admin.subjects.index', compact('subjects', 'sops'));
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255|unique:subjects,name',
]);
Subject::create(['name' => $request->name]);
return redirect()->route('admin.subjects.index')->with('success', 'Materi baru berhasil ditambahkan.');
}
public function update(Request $request, $id)
{
$request->validate([
'name' => 'required|string|max:255|unique:subjects,name,' . $id,
]);
$subject = Subject::findOrFail($id);
$subject->update(['name' => $request->name]);
return redirect()->route('admin.subjects.index')->with('success', 'Nama materi berhasil diperbarui.');
}
public function show($id)
{
// Tarik data materi beserta soal-soalnya secara menurun (terbaru di atas)
$subject = \App\Models\Subject::with(['questions' => function($query) {
$query->latest();
}])->withCount('questions')->findOrFail($id);
return view('pages.admin.subjects.show', compact('subject'));
}
public function edit($id)
{
$subject = Subject::findOrFail($id);
return view('pages.admin.subjects.edit', compact('subject'));
}
public function destroy($id)
{
$subject = Subject::findOrFail($id);
// Cek apakah materi ini sudah dipakai di bank soal
if ($subject->questions()->count() > 0) {
return redirect()->route('admin.subjects.index')->with('error', 'Gagal menghapus! Materi ini sedang digunakan pada Bank Soal.');
}
$subject->delete();
return redirect()->route('admin.subjects.index')->with('success', 'Materi berhasil dihapus.');
}
}
+7 -5
View File
@@ -73,11 +73,13 @@ class User extends Authenticatable
return $this->belongsTo(Position::class, 'position_id');
}
// Jika model TrainingMatrix sudah Anda buat, buka komentar di bawah ini:
/*
public function trainingMatrix()
public function trainings()
{
return $this->belongsTo(TrainingMatrix::class);
// Ubah 'user_id' menjadi 'employee_id' agar sesuai dengan database Anda saat ini
return $this->belongsToMany(Subject::class, 'employee_subject', 'employee_id', 'subject_id')
->withPivot('status')
->withTimestamps();
}
*/
}