288 lines
17 KiB
PHP
288 lines
17 KiB
PHP
|
|
@extends('layouts.app')
|
||
|
|
|
||
|
|
@section('content')
|
||
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
/* Kustomisasi Scrollbar */
|
||
|
|
.custom-scrollbar::-webkit-scrollbar { height: 6px; }
|
||
|
|
.custom-scrollbar::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
|
||
|
|
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
||
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"
|
||
|
|
x-data="{ showCols: { nik: true, name: true, role: true, department: true, action: true }, showCharts: false }">
|
||
|
|
|
||
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||
|
|
<div>
|
||
|
|
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Manajemen Karyawan</h2>
|
||
|
|
<p class="text-sm text-slate-500 mt-1">Total <span class="font-bold text-blue-600">{{ $totalRecords }}</span> data karyawan terdaftar.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="flex flex-wrap items-center gap-3">
|
||
|
|
|
||
|
|
<button @click="showCharts = !showCharts" class="inline-flex items-center px-4 py-2 bg-slate-100 text-slate-700 border border-slate-200 rounded-lg text-sm font-semibold hover:bg-slate-200 shadow-sm transition-all">
|
||
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
|
||
|
|
<span x-text="showCharts ? 'Sembunyikan Grafik' : 'Lihat Grafik'"></span>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<div x-data="{ exportMenuOpen: false }" class="relative">
|
||
|
|
<button @click="exportMenuOpen = !exportMenuOpen" @click.away="exportMenuOpen = false" class="inline-flex items-center px-4 py-2 bg-emerald-50 text-emerald-700 border border-emerald-200 rounded-lg text-sm font-semibold hover:bg-emerald-100 shadow-sm transition-all">
|
||
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
|
||
|
|
Export / Import
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<div x-show="exportMenuOpen" x-transition style="display: none;" class="absolute right-0 mt-2 w-48 bg-white border border-slate-100 rounded-xl shadow-xl py-2 z-50">
|
||
|
|
|
||
|
|
<a href="{{ route('admin.employees.export.excel', request()->query()) }}" class="flex items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">
|
||
|
|
<span class="w-2 h-2 rounded-full bg-emerald-500 mr-2"></span> Export to Excel
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<a href="{{ route('admin.employees.export.pdf', request()->query()) }}" target="_blank" class="flex items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">
|
||
|
|
<span class="w-2 h-2 rounded-full bg-red-500 mr-2"></span> Export to PDF
|
||
|
|
</a>
|
||
|
|
|
||
|
|
<div class="border-t border-slate-100 my-1"></div>
|
||
|
|
|
||
|
|
<a href="{{ route('admin.employees.import') }}" class="w-full flex items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left">
|
||
|
|
<span class="w-2 h-2 rounded-full bg-blue-500 mr-2"></span> Import dari Excel
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<a href="{{ route('admin.employees.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-semibold hover:bg-blue-700 shadow-sm transition-all">
|
||
|
|
+ Tambah Karyawan
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div x-show="showCharts" x-transition style="display: none;" class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||
|
|
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm">
|
||
|
|
<h3 class="text-sm font-bold text-slate-700 mb-4">Distribusi per Departemen (Geser 👉)</h3>
|
||
|
|
<div class="overflow-x-auto w-full pb-2 custom-scrollbar">
|
||
|
|
<div id="deptChartContainer" style="height: 220px;">
|
||
|
|
<canvas id="deptChart"></canvas>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col h-full">
|
||
|
|
<h3 class="text-sm font-bold text-slate-700 mb-4">Distribusi per Jabatan</h3>
|
||
|
|
<div class="relative h-40 w-full mb-4">
|
||
|
|
<canvas id="posChart"></canvas>
|
||
|
|
</div>
|
||
|
|
<div class="overflow-x-auto pb-2 custom-scrollbar mt-auto">
|
||
|
|
<div id="posLegend" class="flex gap-2 whitespace-nowrap min-w-max px-1"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-white p-5 rounded-t-2xl border border-slate-200 border-b-0 relative z-10">
|
||
|
|
<form action="{{ route('admin.employees.index') }}" method="GET" class="flex flex-col md:flex-row gap-4 items-end">
|
||
|
|
<div class="w-full md:w-1/3">
|
||
|
|
<label class="block text-xs font-semibold text-slate-500 mb-1">Pencarian Semua Kolom</label>
|
||
|
|
<input type="text" name="search" value="{{ request('search') }}" placeholder="Ketik keyword apapun..."
|
||
|
|
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-indigo-500">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="w-full md:w-1/4">
|
||
|
|
<label class="block text-xs font-semibold text-slate-500 mb-1">Pilih Departemen</label>
|
||
|
|
<select name="department_id" id="deptSelect" class="select2 w-full">
|
||
|
|
<option value="">Semua Departemen</option>
|
||
|
|
@foreach($departments as $dept)
|
||
|
|
<option value="{{ $dept->id }}" {{ request('department_id') == $dept->id ? 'selected' : '' }}>
|
||
|
|
{{ $dept->name }}
|
||
|
|
</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="w-full md:w-1/4">
|
||
|
|
<label class="block text-xs font-semibold text-slate-500 mb-1">Pilih Jabatan</label>
|
||
|
|
<select name="position_id" id="posSelect" class="select2 w-full">
|
||
|
|
<option value="">Semua Jabatan</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="w-full md:w-auto flex gap-2">
|
||
|
|
<button type="submit" class="px-5 py-2 bg-slate-800 text-white rounded-xl text-sm font-semibold hover:bg-slate-700">Filter</button>
|
||
|
|
@if(request()->hasAny(['search', 'department_id', 'position_id']))
|
||
|
|
<a href="{{ route('admin.employees.index') }}" class="px-5 py-2 bg-red-50 text-red-600 border border-red-100 rounded-xl text-sm font-semibold hover:bg-red-100">Reset</a>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="bg-white border border-slate-200 rounded-b-2xl overflow-hidden shadow-sm relative z-0 mt-4 md:mt-0">
|
||
|
|
<div class="overflow-x-auto custom-scrollbar">
|
||
|
|
<table class="w-full text-left text-sm whitespace-nowrap">
|
||
|
|
<thead class="bg-slate-50/80 text-slate-500 text-xs uppercase tracking-wider border-b border-slate-200">
|
||
|
|
<tr>
|
||
|
|
<th x-show="showCols.nik" class="px-6 py-4 font-semibold">NIK & Inisial</th>
|
||
|
|
<th x-show="showCols.name" class="px-6 py-4 font-semibold">Profil & Kontak</th>
|
||
|
|
<th x-show="showCols.role" class="px-6 py-4 font-semibold text-center">Hak Akses Sistem</th>
|
||
|
|
<th x-show="showCols.department" class="px-6 py-4 font-semibold">Penempatan</th>
|
||
|
|
<th x-show="showCols.action" class="px-6 py-4 font-semibold text-right">Opsi</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody class="divide-y divide-slate-100 text-slate-700">
|
||
|
|
@forelse($employees as $emp)
|
||
|
|
<tr class="hover:bg-slate-50">
|
||
|
|
<td x-show="showCols.nik" class="px-6 py-4">
|
||
|
|
<div class="font-mono font-bold text-slate-900">{{ $emp->nik ?? '-' }}</div>
|
||
|
|
<div class="mt-1">
|
||
|
|
<span class="bg-indigo-50 text-indigo-700 px-2 py-0.5 rounded text-[10px] font-bold border border-indigo-100">
|
||
|
|
{{ $emp->initial ?? 'N/A' }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td x-show="showCols.name" class="px-6 py-4">
|
||
|
|
<div class="font-bold text-slate-900">
|
||
|
|
{{ $emp->first_name }} {{ $emp->last_name }}
|
||
|
|
@if(!$emp->is_active)
|
||
|
|
<span class="ml-2 px-2 py-0.5 bg-red-100 text-red-600 text-[10px] font-bold rounded uppercase">Non-Aktif</span>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
<div class="text-xs text-slate-500">{{ $emp->email }}</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td x-show="showCols.role" class="px-6 py-4 text-center">
|
||
|
|
<div class="flex flex-wrap gap-1 justify-center">
|
||
|
|
@php
|
||
|
|
$isTrainer = $emp->hasRole('trainer') || $emp->is_trainer == 1 || (isset($emp->role) && strtolower($emp->role) == 'trainer');
|
||
|
|
$isAdmin = $emp->hasRole('admin') || $emp->hasRole('superadmin') || (isset($emp->role) && strtolower($emp->role) == 'admin');
|
||
|
|
@endphp
|
||
|
|
|
||
|
|
@if($isAdmin)
|
||
|
|
<span class="px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide bg-red-100 text-red-700">Admin</span>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
@if($isTrainer)
|
||
|
|
<span class="px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide bg-amber-100 text-amber-700">Trainer</span>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
<span class="px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide bg-emerald-100 text-emerald-700">Trainee</span>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td x-show="showCols.department" class="px-6 py-4">
|
||
|
|
<div class="text-sm font-semibold text-slate-800">{{ $emp->position->name ?? '-' }}</div>
|
||
|
|
<div class="text-xs text-slate-500 mt-0.5">{{ $emp->department->name ?? '-' }}</div>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
<td x-show="showCols.action" class="px-6 py-4 text-right space-x-2">
|
||
|
|
<a href="{{ route('admin.employees.show', $emp->id) }}" class="text-slate-400 hover:text-indigo-600 font-medium text-sm">Detail</a>
|
||
|
|
<a href="{{ route('admin.employees.edit', $emp->id) }}" class="text-indigo-600 hover:text-indigo-800 font-medium text-sm">Edit</a>
|
||
|
|
|
||
|
|
<form action="{{ route('admin.employees.destroy', $emp->id) }}" method="POST" class="inline-block" onsubmit="return confirm('Nonaktifkan karyawan ini? Data historis tidak akan dihapus.');">
|
||
|
|
@csrf @method('DELETE')
|
||
|
|
<button type="submit" class="text-red-500 hover:text-red-700 font-medium text-sm ml-2">Hapus</button>
|
||
|
|
</form>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
@empty
|
||
|
|
<tr>
|
||
|
|
<td colspan="5" class="px-6 py-10 text-center text-slate-500 italic">Data tidak ditemukan.</td>
|
||
|
|
</tr>
|
||
|
|
@endforelse
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if(method_exists($employees, 'hasPages') && $employees->hasPages())
|
||
|
|
<div class="p-4 border-t border-slate-100 bg-slate-50/50">
|
||
|
|
{{ $employees->links() }}
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
$(document).ready(function() {
|
||
|
|
$('.select2').select2({ width: '100%' });
|
||
|
|
|
||
|
|
const mapping = @json($deptPosMapping ?? []);
|
||
|
|
const posSelect = $('#posSelect');
|
||
|
|
const deptSelect = $('#deptSelect');
|
||
|
|
const oldPos = "{{ request('position_id') }}";
|
||
|
|
|
||
|
|
function updatePositionDropdown() {
|
||
|
|
let deptId = deptSelect.val();
|
||
|
|
posSelect.empty();
|
||
|
|
posSelect.append(new Option('Semua Jabatan', ''));
|
||
|
|
|
||
|
|
if (deptId && mapping[deptId]) {
|
||
|
|
mapping[deptId].forEach(function(pos) {
|
||
|
|
let selected = (pos.id == oldPos);
|
||
|
|
posSelect.append(new Option(pos.name, pos.id, false, selected));
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
@foreach($positions as $p)
|
||
|
|
posSelect.append(new Option("{{ $p->name }}", "{{ $p->id }}", false, ("{{ $p->id }}" == oldPos)));
|
||
|
|
@endforeach
|
||
|
|
}
|
||
|
|
posSelect.trigger('change.select2');
|
||
|
|
}
|
||
|
|
|
||
|
|
deptSelect.on('change', updatePositionDropdown);
|
||
|
|
updatePositionDropdown();
|
||
|
|
});
|
||
|
|
|
||
|
|
// KONFIGURASI GRAFIK
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
Chart.register(ChartDataLabels);
|
||
|
|
const chartColors = ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4', '#14B8A6', '#F43F5E', '#84CC16', '#3B82F6', '#F97316', '#6366F1'];
|
||
|
|
|
||
|
|
const rawDeptData = @json($chartDept ?? []);
|
||
|
|
const deptLabels = rawDeptData.map(d => d.department ? d.department.name : 'N/A');
|
||
|
|
const deptCounts = rawDeptData.map(d => d.total);
|
||
|
|
const deptBgColors = rawDeptData.map((d, i) => chartColors[i % chartColors.length]);
|
||
|
|
|
||
|
|
const minDeptWidth = Math.max(600, rawDeptData.length * 60);
|
||
|
|
document.getElementById('deptChartContainer').style.width = minDeptWidth + 'px';
|
||
|
|
|
||
|
|
new Chart(document.getElementById('deptChart'), {
|
||
|
|
type: 'bar',
|
||
|
|
data: {
|
||
|
|
labels: deptLabels,
|
||
|
|
datasets: [{ data: deptCounts, backgroundColor: deptBgColors, borderRadius: 4 }]
|
||
|
|
},
|
||
|
|
options: {
|
||
|
|
responsive: true, maintainAspectRatio: false,
|
||
|
|
plugins: { legend: { display: false }, datalabels: { color: '#1e293b', anchor: 'end', align: 'top', font: { weight: 'bold' } } },
|
||
|
|
scales: { y: { beginAtZero: true, grace: '10%' }, x: { ticks: { autoSkip: false, maxRotation: 45, minRotation: 45 } } }
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const rawPosData = @json($chartPos ?? []);
|
||
|
|
const posLabels = rawPosData.map(d => d.position ? d.position.name : 'N/A');
|
||
|
|
const posCounts = rawPosData.map(d => d.total);
|
||
|
|
const posBgColors = rawPosData.map((d, i) => chartColors[(i + 3) % chartColors.length]);
|
||
|
|
|
||
|
|
new Chart(document.getElementById('posChart'), {
|
||
|
|
type: 'doughnut',
|
||
|
|
data: { labels: posLabels, datasets: [{ data: posCounts, backgroundColor: posBgColors, borderWidth: 2 }] },
|
||
|
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } }, cutout: '65%' }
|
||
|
|
});
|
||
|
|
|
||
|
|
const legendContainer = document.getElementById('posLegend');
|
||
|
|
posLabels.forEach((label, index) => {
|
||
|
|
const color = posBgColors[index];
|
||
|
|
const count = posCounts[index];
|
||
|
|
legendContainer.innerHTML += `<div class="flex items-center text-[11px] text-slate-600 bg-slate-50 px-2.5 py-1.5 rounded-lg border border-slate-200 shadow-sm shrink-0"><span class="w-3 h-3 rounded-full mr-2" style="background-color: ${color}"></span><span class="font-bold text-slate-800 mr-1">${count}</span> ${label}</div>`;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.select2-container .select2-selection--single { height: 38px; border-radius: 0.75rem; border-color: #e2e8f0; background-color: #f8fafc; }
|
||
|
|
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 38px; font-size: 0.875rem; color: #334155; }
|
||
|
|
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
|
||
|
|
</style>
|
||
|
|
@endsection
|