426 lines
17 KiB
PHP
426 lines
17 KiB
PHP
|
|
<?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.");
|
||
|
|
}
|
||
|
|
}
|