Files

312 lines
17 KiB
PHP

@extends('layouts.app')
@section('content')
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js'></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* Styling kalender agar sesuai dengan Tailwind */
.fc-theme-standard .fc-scrollgrid { border-color: #e2e8f0; border-radius: 0.75rem; overflow: hidden; }
.fc-theme-standard td, .fc-theme-standard th { border-color: #e2e8f0; }
.fc .fc-toolbar-title { font-size: 1.125rem; font-weight: 700; color: #1e293b; }
.fc .fc-button-primary { background-color: #2563eb; border-color: #2563eb; font-weight: 600; text-transform: capitalize; border-radius: 0.5rem;}
.fc .fc-button-primary:hover { background-color: #1d4ed8; border-color: #1d4ed8; }
.fc .fc-button-primary:disabled { background-color: #94a3b8; border-color: #94a3b8; }
.fc-day-today { background-color: #f8fafc !important; }
.fc-event { cursor: pointer; border-radius: 4px; padding: 2px 4px; border: none; font-size: 0.75rem; font-weight: bold;}
</style>
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<div>
<h1 class="text-2xl font-bold text-slate-800">Command Center Training</h1>
<p class="text-sm text-slate-500">Pantau pemenuhan kompetensi dan aktivitas CBT Karyawan secara real-time.</p>
</div>
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex items-center space-x-3 bg-white p-2 rounded-xl shadow-sm border border-slate-200">
<select name="month" class="bg-slate-50 border-none text-sm font-bold text-slate-700 rounded-lg focus:ring-0 cursor-pointer">
@foreach(range(1, 12) as $m)
<option value="{{ $m }}" {{ $selectedMonth == $m ? 'selected' : '' }}>
{{ date('F', mktime(0, 0, 0, $m, 1)) }}
</option>
@endforeach
</select>
<select name="year" class="bg-slate-50 border-none text-sm font-bold text-slate-700 rounded-lg focus:ring-0 cursor-pointer">
@foreach(range(date('Y')-2, date('Y')+1) as $y)
<option value="{{ $y }}" {{ $selectedYear == $y ? 'selected' : '' }}>{{ $y }}</option>
@endforeach
</select>
<button type="submit" class="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</button>
</form>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="lg:col-span-2 space-y-6">
<div class="bg-slate-900 rounded-2xl shadow-lg border border-slate-800 overflow-hidden"
x-data="liveMonitoring()"
x-init="fetchData(); setInterval(() => fetchData(), 5000);">
<div class="p-5 border-b border-slate-700 flex justify-between items-center bg-slate-800/50">
<div class="flex items-center space-x-3">
<div class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</div>
<h2 class="text-sm font-bold text-white uppercase tracking-wider">Live Exam Monitoring</h2>
</div>
<div class="text-xs font-bold text-slate-400 bg-slate-800 px-3 py-1 rounded-full border border-slate-700">
<span x-text="activeTrainees.length"></span> Karyawan Online
</div>
</div>
<div class="p-0 max-h-[300px] overflow-y-auto custom-scrollbar">
<table class="w-full text-left text-sm text-slate-300">
<tbody class="divide-y divide-slate-800/50">
<tr x-show="activeTrainees.length === 0">
<td class="px-6 py-8 text-center text-slate-500">
Tidak ada karyawan yang sedang mengerjakan ujian saat ini.
</td>
</tr>
<template x-for="trainee in activeTrainees" :key="trainee.attempt_id">
<tr class="hover:bg-slate-800/50 transition-colors group">
<td class="px-5 py-4">
<div class="font-bold text-white" x-text="trainee.first_name + ' ' + (trainee.last_name || '')"></div>
<div class="text-xs text-indigo-400 mt-0.5" x-text="trainee.dept_name"></div>
</td>
<td class="px-5 py-4">
<div class="text-xs font-bold text-slate-300 truncate w-48" x-text="trainee.exam_title"></div>
</td>
<td class="px-5 py-4 text-center">
<span class="font-mono text-xs font-bold px-2.5 py-1 rounded-md border"
:class="trainee.time_left_seconds > 300 ? 'bg-emerald-900/50 text-emerald-400 border-emerald-800' : 'bg-red-900/50 text-red-400 border-red-800 animate-pulse'"
x-text="formatTime(trainee.time_left_seconds)">
</span>
</td>
<td class="px-5 py-4 text-right">
<button @click="forceSubmit(trainee.attempt_id)" class="opacity-0 group-hover:opacity-100 px-3 py-1 bg-red-600 hover:bg-red-500 text-white text-[10px] font-bold rounded transition-all">
FORCE SUBMIT
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<div class="p-5 border-b border-slate-100 flex justify-between items-center bg-red-50/30">
<div class="flex items-center space-x-2">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<h2 class="text-base font-bold text-slate-800">Training Terlewat (Overdue)</h2>
</div>
<span class="px-3 py-1 bg-red-100 text-red-700 text-xs font-bold rounded-full">{{ $totalOverdue }} Kasus Bulan Ini</span>
</div>
<div class="p-0">
<table class="w-full text-left text-sm text-slate-600">
<tbody class="divide-y divide-slate-50">
@forelse($overdueTrainings as $overdue)
<tr class="hover:bg-slate-50 transition-colors">
<td class="px-5 py-3">
<div class="font-bold text-slate-700">{{ $overdue->first_name }} {{ $overdue->last_name }}</div>
<div class="text-[10px] uppercase font-bold text-slate-400 mt-0.5">{{ $overdue->dept_name }}</div>
</td>
<td class="px-5 py-3 font-medium text-slate-600">{{ $overdue->title }}</td>
<td class="px-5 py-3 text-right">
<span class="text-xs font-bold text-red-500">
Tenggat: {{ \Carbon\Carbon::parse($overdue->deadline_date)->format('d M Y') }}
</span>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-6 py-8 text-center text-emerald-600 font-bold bg-emerald-50/50">
🎉 Luar biasa! Tidak ada training yang terlewat bulan ini.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="lg:col-span-1">
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden h-full flex flex-col">
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
<div>
<h2 class="text-base font-bold text-slate-800">Pemenuhan Training Dept.</h2>
<p class="text-[11px] text-slate-500 mt-0.5">Klik pada batang grafik untuk melihat detail</p>
</div>
</div>
<div class="p-4 flex-grow relative w-full h-[320px] flex items-center justify-center">
@if(count($deptFulfillment) > 0)
<canvas id="deptFulfillmentChart"></canvas>
@else
<div class="text-center text-slate-400 text-sm flex flex-col items-center">
<svg class="w-10 h-10 mb-2 opacity-50" 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>
Belum ada data bulan ini
</div>
@endif
</div>
</div>
</div>
</div> <div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<div class="p-6 border-b border-slate-100 flex flex-col md:flex-row justify-between items-start md:items-center bg-slate-50 gap-4">
<div>
<h2 class="text-lg font-bold text-slate-800">Kalender Penugasan Training</h2>
<p class="text-sm text-slate-500">
@if(auth()->user()->hasRole('superadmin'))
Menampilkan jadwal training untuk <span class="font-bold text-blue-600">Seluruh Karyawan (Global)</span>.
@else
Menampilkan jadwal training khusus untuk departemen <span class="font-bold text-blue-600">{{ auth()->user()->department->name ?? 'Anda' }}</span>.
@endif
</p>
</div>
<div class="flex flex-wrap gap-3 text-xs font-bold text-slate-500">
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-emerald-500 mr-1.5"></span> Lulus</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-blue-500 mr-1.5"></span> Dikerjakan</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-amber-500 mr-1.5"></span> Pending</div>
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-red-500 mr-1.5"></span> Terlewat</div>
</div>
</div>
<div class="p-6">
<div id="trainingCalendar"></div>
</div>
</div>
</div>
<script>
// 1. Inisialisasi Alpine JS untuk Live Monitoring
document.addEventListener('alpine:init', () => {
Alpine.data('liveMonitoring', () => ({
activeTrainees: [],
fetchData() {
// Perbaikan penulisan string URL agar aman
fetch("{{ route('admin.api.live-exams') }}")
.then(response => response.json())
.then(data => {
this.activeTrainees = data;
})
.catch(error => console.error('Error fetching live data:', error));
},
formatTime(seconds) {
if (seconds <= 0) return "WAKTU HABIS";
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
const s = (seconds % 60).toString().padStart(2, '0');
return m + ":" + s;
},
forceSubmit(attemptId) {
if(confirm('Peringatan: Anda akan memaksa menghentikan ujian karyawan ini secara sepihak. Lanjutkan?')) {
alert('Sinyal penghentian ujian #' + attemptId + ' terkirim!');
this.fetchData();
}
}
}))
});
// 2. Inisialisasi FullCalendar JS
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('trainingCalendar');
var eventsData = @json($calendarData ?? []);
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,listWeek'
},
events: eventsData,
height: 650,
eventClick: function(info) {
alert('Detail Penugasan:\n' + info.event.title + '\nTenggat Waktu: ' + info.event.endStr.split('T')[0]);
}
});
calendar.render();
});
// 3. Inisialisasi Chart.js untuk Pemenuhan Departemen
document.addEventListener('DOMContentLoaded', function() {
if(document.getElementById('deptFulfillmentChart')) {
const ctx = document.getElementById('deptFulfillmentChart').getContext('2d');
const deptData = @json($deptFulfillment ?? []);
const labels = deptData.map(d => d.name);
const percentages = deptData.map(d => d.percentage);
const ids = deptData.map(d => d.id);
const bgColors = percentages.map(p => {
if (p < 50) return 'rgba(239, 68, 68, 0.8)';
if (p < 80) return 'rgba(245, 158, 11, 0.8)';
return 'rgba(16, 185, 129, 0.8)';
});
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Persentase Lulus (%)',
data: percentages,
backgroundColor: bgColors,
borderRadius: 6,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
const index = context.dataIndex;
return `Lulus: ${deptData[index].passed}/${deptData[index].assigned} (${context.raw}%)`;
}
}
}
},
scales: {
x: { min: 0, max: 100, ticks: { stepSize: 20 } },
y: { grid: { display: false } }
},
onClick: (e, activeElements) => {
if (activeElements.length > 0) {
const index = activeElements[0].index;
const deptId = ids[index];
const month = '{{ $selectedMonth }}';
const year = '{{ $selectedYear }}';
window.location.href = `/admin/dashboard/department/${deptId}/fulfillment?month=${month}&year=${year}`;
}
},
onHover: (event, chartElement) => {
event.native.target.style.cursor = chartElement[0] ? 'pointer' : 'default';
}
}
});
}
});
</script>
@endsection