Commit 6e11c3af by Alfiro Pratama

Chart PMW per Fakultas

parent 2b7759b3
...@@ -90,6 +90,7 @@ class InseoHelper ...@@ -90,6 +90,7 @@ class InseoHelper
'psikologi' => 'FP', 'psikologi' => 'FP',
'hukum' => 'FH', 'hukum' => 'FH',
'ilmu hukum' => 'FH',
'kampus unesa magetan' => 'PSDKU Magetan', 'kampus unesa magetan' => 'PSDKU Magetan',
]; ];
......
...@@ -2,8 +2,17 @@ ...@@ -2,8 +2,17 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\DaftarProposal;
use App\Models\DaftarProposalMonev;
use App\Models\Jenis;
use App\Models\JenisMonev;
use App\Models\MonevInternal;
use App\Models\Pengumuman; use App\Models\Pengumuman;
use App\Models\Periode;
use App\Models\Proposal;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class DashboardController extends Controller class DashboardController extends Controller
{ {
...@@ -16,14 +25,136 @@ class DashboardController extends Controller ...@@ -16,14 +25,136 @@ class DashboardController extends Controller
{ {
// //
$title = 'Dashboard PMW'; $title = 'Dashboard PMW';
// $pengumuman = $pengumuman = Pengumuman::query()->first(); $pengumuman = $pengumuman = Pengumuman::query()->where('status', 1)->first();
$tahun = Periode::orderBy('nama', 'ASC')->get();
$jenis = Jenis::where('status_hapus', 0)->whereNotNull('nama')->orderBy('nama', 'ASC')->get();
$proposal = DaftarProposal::all();
$jenis_monev = JenisMonev::where('status_hapus', 0)->get();
$monev = DaftarProposalMonev::where('status_hapus', 0)->get();
$fakultas = DB::connection('siakadu')->table('sms')->whereNull('id_induk_sms')->whereNull('kode_prodi')->get();
$data = [ $data = [
'title' => $title, 'title' => $title,
'pengumuman' => null, 'pengumuman' => $pengumuman,
'tahun' => $tahun,
'jenis' => $jenis,
'proposal' => $proposal,
'tahap' => $jenis_monev,
'monev' => $monev,
'fakultas' => $fakultas,
]; ];
return view('backend.index', $data); return view('backend.index', $data);
} }
public function getChartData(Request $request)
{
try {
$jenis_id = $request->get('reqJenisPmw');
$tahap = $request->get('reqStatus');
$tahun = $request->get('tahun');
// Debug: log received parameters
Log::info('Chart data request parameters:', [
'jenis_id' => $jenis_id,
'tahap' => $tahap,
'tahun' => $tahun
]);
// 1️⃣ Ambil semua fakultas (sekali saja)
$fakultas = DB::connection('siakadu')->table('sms')
->select('id_sms', 'nm_lemb')
->whereNull('id_induk_sms')
->whereNull('kode_prodi')
->where('nm_lemb', '!=', 'FPS')
->orderBy('nm_lemb', 'ASC')
->get()
->map(function ($item) {
if (strtolower($item->nm_lemb) === 'vokasi') {
$item->nm_lemb = 'VOKASI';
}
if (strtolower($item->nm_lemb) === 'kedokteran') {
$item->nm_lemb = 'FK';
}
if (strtolower($item->nm_lemb) === 'fpsi') {
$item->nm_lemb = 'FP';
}
if (strtolower($item->nm_lemb) === 'psdku') {
$item->nm_lemb = "PSDKU\nMagetan";
}
return $item;
})
->sortBy(function ($item) {
$urutanKhusus = [
'VOKASI' => 1,
"PSDKU\nMagetan" => 2,
'FK' => 3,
'FKP' => 4,
];
$rank = $urutanKhusus[$item->nm_lemb] ?? 0;
return sprintf('%02d_%s', $rank, $item->nm_lemb);
})
->values();
// 2️⃣ Query proposal per fakultas (1 query saja)
$proposalQuery = DaftarProposal::select('fakultas_ketua', DB::raw('COUNT(*) as total'))
->where('status_hapus', 0)
->when($jenis_id, fn($q) => $q->where('jenis_id', $jenis_id))
->when($tahun, fn($q) => $q->where('periode', $tahun))
->when($tahap === 'proposal', fn($q) => $q) // Only show proposal data when tahap is 'proposal'
->when($tahap && $tahap !== 'proposal', fn($q) => $q->whereRaw('1 = 0')) // Hide proposal data for other tahap
->groupBy('fakultas_ketua')
->get()
->pluck('total', 'fakultas_ketua');
// 3️⃣ Query Monev 1 (1 query)
$monev1Query = DaftarProposalMonev::select('fakultas_ketua', DB::raw('COUNT(*) as total'))
->where('jenis_monev_desc', 'Monev Internal I')
->where('status_hapus', 0)
->when($jenis_id, fn($q) => $q->where('jenis_id', $jenis_id))
->when($tahun, fn($q) => $q->where('periode', $tahun))
->when($tahap === 'monev1', fn($q) => $q) // Only show monev1 data when tahap is 'monev1'
->when($tahap && $tahap !== 'monev1', fn($q) => $q->whereRaw('1 = 0')) // Hide monev1 data for other tahap
->groupBy('fakultas_ketua')
->get()
->pluck('total', 'fakultas_ketua');
// 4️⃣ Query Monev 2 (1 query)
$monev2Query = DaftarProposalMonev::select('fakultas_ketua', DB::raw('COUNT(*) as total'))
->where('jenis_monev_desc', 'Monev Internal II')
->where('status_hapus', 0)
->when($jenis_id, fn($q) => $q->where('jenis_id', $jenis_id))
->when($tahun, fn($q) => $q->where('periode', $tahun))
->when($tahap === 'monev2', fn($q) => $q) // Only show monev2 data when tahap is 'monev2'
->when($tahap && $tahap !== 'monev2', fn($q) => $q->whereRaw('1 = 0')) // Hide monev2 data for other tahap
->groupBy('fakultas_ketua')
->get()
->pluck('total', 'fakultas_ketua');
// 5️⃣ Gabungkan data
$chartData = $fakultas->map(function ($f) use ($proposalQuery, $monev1Query, $monev2Query) {
$namaFak = $f->nm_lemb;
return [
'fakultas' => $namaFak,
'proposal' => $proposalQuery[$namaFak] ?? 0,
'monev1' => $monev1Query[$namaFak] ?? 0,
'monev2' => $monev2Query[$namaFak] ?? 0,
];
});
return response()->json($chartData);
} catch (\Throwable $e) {
Log::error([
'error' => true,
'message' => $e->getMessage(),
'line' => $e->getLine(),
'file' => $e->getFile(),
]);
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data chart.'], 500);
}
}
} }
...@@ -12,7 +12,7 @@ class DaftarProposal extends Model ...@@ -12,7 +12,7 @@ class DaftarProposal extends Model
protected $keyType = 'string'; protected $keyType = 'string';
protected $fillable = [ protected $fillable = [
'proposal_id', 'jenis_id', 'kode', 'jenis_pkm', 'judul', 'status', 'status_hapus', 'status_administrasi_1', 'status_administrasi_2', 'reviewer_id_1', 'reviewer_id_2', 'status_final', 'nidn_reviewer_id_1', 'nidn_reviewer_id_2', 'upload_dokumen', 'date_upload', 'date_approval', 'identitas_ketua', 'identitas_dospem', 'periode', 'url' 'proposal_id', 'jenis_id', 'kode', 'jenis_pkm', 'judul', 'status', 'status_hapus', 'status_administrasi_1', 'status_administrasi_2', 'reviewer_id_1', 'reviewer_id_2', 'status_final', 'nidn_reviewer_id_1', 'nidn_reviewer_id_2', 'upload_dokumen', 'date_upload', 'date_approval', 'identitas_ketua', 'identitas_dospem', 'periode', 'url', 'fakultas',
]; ];
public function rKelompokDetil() public function rKelompokDetil()
......
...@@ -12,9 +12,9 @@ class DaftarProposalMonev extends Model ...@@ -12,9 +12,9 @@ class DaftarProposalMonev extends Model
protected $fillable = [ protected $fillable = [
'monev_internal_id', 'proposal_id', 'jenis_monev_id', 'kode', 'judul', 'monev_internal_id', 'proposal_id', 'jenis_monev_id', 'kode', 'judul',
'jenis_pkm', 'jenis_monev', 'status', 'status_administrasi_1', 'status_administrasi_2', 'jenis_pkm', 'jenis_monev', 'jenis_monev_desc', 'status', 'status_administrasi_1', 'status_administrasi_2',
'nilai_1', 'nilai_2', 'reviewer_komentar_1', 'reviewer_komentar_2', 'reviewer_monev_id_1', 'nilai_1', 'nilai_2', 'reviewer_komentar_1', 'reviewer_komentar_2', 'reviewer_monev_id_1',
'reviewer_monev_id_2', 'reviewer_id_1', 'reviewer_id_2', 'kelompok_id' 'reviewer_monev_id_2', 'reviewer_id_1', 'reviewer_id_2', 'kelompok_id', 'fakultas_ketua', 'status_hapus', 'status', 'periode',
// 'proposal_id', 'reviewer_monev_id', 'jenis_id', 'kode', 'jenis_pkm', 'judul', 'status', 'status_administrasi_1', 'status_administrasi_2', 'nilai_1', 'nilai_2', 'reviewer_monev_id_1', 'reviewer_monev_id_1', 'status_final' // 'proposal_id', 'reviewer_monev_id', 'jenis_id', 'kode', 'jenis_pkm', 'judul', 'status', 'status_administrasi_1', 'status_administrasi_2', 'nilai_1', 'nilai_2', 'reviewer_monev_id_1', 'reviewer_monev_id_1', 'status_final'
]; ];
......
...@@ -11,10 +11,47 @@ ...@@ -11,10 +11,47 @@
</div> </div>
@endsection @endsection
@section('css')
<style>
.btn-fakultas {
background: none;
border: none;
color: #007bff;
font-weight: 600;
cursor: pointer;
padding: 4px 8px;
transition: all 0.2s ease-in-out;
border-radius: 8px;
}
.btn-fakultas:hover {
background: #007bff;
color: white;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.tooltip-fakultas {
position: absolute;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
display: none;
z-index: 9999;
pointer-events: none;
max-width: 220px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transition: opacity 0.15s ease-in-out;
}
</style>
@endsection
@section('contents') @section('contents')
@php @php
$menu = 'dashboard'; $menu = 'dashboard';
@endphp @endphp
<!-- start page title --> <!-- start page title -->
<div class="page-title-box"> <div class="page-title-box">
...@@ -41,10 +78,349 @@ ...@@ -41,10 +78,349 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-12 col-lg-4 col-md-4 col-sm-12">
<div class="row">
<label class="col-sm-3 col-form-label">Kategori : </label>
<div class="col-sm-6">
<select class="form-select select2" name="reqJenisPmw" id="reqJenisPmw">
<option value="">Semua</option>
@foreach ($jenis as $item)
<option value="{{ $item->jenis_id }}">{{ $item->nama }}</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="col-12 col-lg-4 col-md-4 col-sm-12">
<div class="row">
<label class="col-sm-3 col-form-label">Tahap : </label>
<div class="col-sm-6">
<select class="form-select select2" name="reqStatus" id="reqStatus">
<option value="">Semua</option>
<option value="proposal">Proposal</option>
{{-- <option value="internal">Seleksi Internal</option> --}}
<option value="monev1">Monev Internal I</option>
<option value="monev2">Monev Internal II</option>
</select>
</div>
</div>
</div>
<div class="col-12 col-lg-4 col-md-4 col-sm-12">
<div class="row">
<label class="col-sm-3 col-form-label">Tahun : </label>
<div class="col-sm-6">
<select class="form-select select2" name="tahun" id="tahun">
<option value="">Semua</option>
@foreach ($tahun as $item)
<option value="{{ $item->nama }}" {{ $item->nama == date('Y') ? 'selected' : '' }}>
{{ $item->nama }}</option>
@endforeach
</select>
{{-- <select class="form-select select2" name="reqTahun" id="reqTahun">
<option value="0">Semua</option>
@foreach ($periode as $res)
<option value="{{ $res->nama }}">{{ $res->nama }}</option>
@endforeach
</select> --}}
</div>
</div>
</div>
</div>
</div>
@if (Auth::user()->hasrole(['operator']))
<div class="card-body">
<div class="card-title card-title-grafik">
<h4>Grafik Data PMW Per Fakultas</h4>
</div>
<div class="row">
<div class="col-12">
<div id="chartLoading" class="shadow rounded-pill col-3 mx-auto p-3 text-center my-4" style="display: none;">
{{-- <div class="shadow rounded border col-3 mx-auto p-3 text-center py-4"> --}}
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2 mb-0 d-none d-lg-block">Memuat data grafik...</p>
{{-- </div> --}}
</div>
<canvas id="chartFakultas" style="min-height: 400px; display:none;"></canvas>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="alert alert-info">
<i class="mdi mdi-information-outline me-2"></i>
<strong>Petunjuk:</strong> Klik pada bar chart untuk memfilter data berdasarkan fakultas tertentu.
</div>
</div>
</div>
</div>
@endif
</div>
</div>
{{-- <div class="col-lg-12">
<div class="card">
</div>
</div> --}}
</div> </div>
@endsection @endsection
@section('js') @section('js')
@if (Auth::user()->hasrole(['operator']))
{{-- <script src="{{ asset('theme/libs/chart.js/Chart.min.js') }}"></script> --}}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let chartInstance = null;
let fullData = [];
@endsection async function loadChart() {
const spinner = document.getElementById('chartLoading');
const canvas = document.getElementById('chartFakultas');
try {
// Tampilkan spinner, sembunyikan canvas
spinner.style.display = 'block';
canvas.style.display = 'none';
// Ambil nilai filter
const kategori = document.getElementById('reqJenisPmw').value;
const tahap = document.getElementById('reqStatus').value;
const tahun = document.getElementById('tahun').value;
// Buat URL dengan parameter filter
const url = new URL("{{ route('dashboard.chart-data') }}", window.location.origin);
if (kategori) url.searchParams.append('reqJenisPmw', kategori);
if (tahap) url.searchParams.append('reqStatus', tahap);
if (tahun) url.searchParams.append('tahun', tahun);
// Debug: log filter parameters
// console.log('Filter parameters:', { kategori, tahap, tahun });
// console.log('Request URL:', url.toString());
// Timeout 3 detik agar loading tidak terlalu lama
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
if (!response.ok) throw new Error('Gagal mengambil data');
const data = await response.json();
// Validasi format data
if (!Array.isArray(data) || !data.length || !data[0].fakultas) {
throw new Error('Format data tidak sesuai');
}
const labels = data.map(item => {
// Handle multi-line labels for Chart.js
let label = item.fakultas;
if (label.includes('\n')) {
label = label.split('\n');
}
// Change FP to FPsi for display only
if (Array.isArray(label)) {
return label.map(line => line === 'FP' ? 'FPsi' : line);
} else {
return label === 'FP' ? 'FPsi' : label;
}
});
const proposalData = data.map(item => item.proposal);
const monev1Data = data.map(item => item.monev1);
const monev2Data = data.map(item => item.monev2);
const ctx = document.getElementById('chartFakultas').getContext('2d');
// const { labels, proposalData, monev1Data, monev2Data } = data;
// Hapus chart lama jika ada
if (window.chartFakultasInstance) {
window.chartFakultasInstance.destroy();
}
// Buat chart baru
window.chartFakultasInstance = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'Proposal', data: proposalData, backgroundColor: 'rgba(54, 162, 235, 0.8)' },
{ label: 'Monev Internal I', data: monev1Data, backgroundColor: 'rgba(255, 206, 86, 0.8)' },
{ label: 'Monev Internal II', data: monev2Data, backgroundColor: 'rgba(255, 99, 132, 0.8)' },
]
},
options: {
responsive: true,
animation: { duration: 1000, easing: 'easeOutQuart' },
plugins: {
legend: {
position: 'top',
labels: { padding: 32, cursor: 'pointer' },
onHover: (e) => e.native.target.style.cursor = 'pointer',
onLeave: (e) => e.native.target.style.cursor = 'default'
},
title: {
display: true,
text: 'Jumlah Data Proposal & Monev Internal per Fakultas',
font: { size: 16 }
}
},
scales: {
x: {
ticks: {
maxRotation: 0,
minRotation: 0
}
},
y: {
beginAtZero: true,
ticks: { stepSize: 50 }
}
}
}
});
// ✅ Simpan data penuh agar bisa difilter nanti
fullData = data;
// ✅ Tambahkan event klik label fakultas di bawah chart (area sumbu X)
canvas.addEventListener('click', function (event) {
const chart = window.chartFakultasInstance;
const xAxis = chart.scales.x;
const yAxis = chart.scales.y;
const x = event.offsetX;
const y = event.offsetY;
// Pastikan klik di area label bawah
if (y > yAxis.bottom && y < yAxis.bottom + 30) {
const labelIndex = Math.floor(
((x - xAxis.left) / (xAxis.right - xAxis.left)) * chart.data.labels.length
);
const fakultas = Array.isArray(chart.data.labels[labelIndex])
? chart.data.labels[labelIndex].join(' ')
: chart.data.labels[labelIndex];
if (!fakultas) return;
// Toggle filter: jika sudah difilter, klik lagi untuk tampilkan semua
const isFiltered = chart.data.labels.length === 1 && chart.data.labels[0] === fakultas;
const filtered = isFiltered
? fullData
: fullData.filter(d =>
(Array.isArray(d.fakultas) ? d.fakultas.join(' ') : d.fakultas) === fakultas
);
chart.data.labels = filtered.map(d => d.fakultas.includes('\n') ? d.fakultas.split('\n') : d.fakultas);
chart.data.datasets[0].data = filtered.map(d => d.proposal);
chart.data.datasets[1].data = filtered.map(d => d.monev1);
chart.data.datasets[2].data = filtered.map(d => d.monev2);
chart.update();
}
});
// Sembunyikan spinner, tampilkan chart
spinner.style.display = 'none';
canvas.style.display = 'block';
let labelTooltip = document.createElement('div');
labelTooltip.className = 'tooltip-fakultas shadow';
labelTooltip.style.display = 'none';
document.body.appendChild(labelTooltip);
// event listener untuk mendeteksi hover di atas tick label
canvas.addEventListener('mousemove', function (event) {
const chart = window.chartFakultasInstance;
const xAxis = chart.scales.x;
const yAxis = chart.scales.y;
const x = event.offsetX;
const y = event.offsetY;
// Cek apakah posisi mouse berada di area bawah chart (dekat label fakultas)
if (y > yAxis.bottom && y < yAxis.bottom + 30) {
const labelIndex = Math.floor(
((x - xAxis.left) / (xAxis.right - xAxis.left)) * chart.data.labels.length
);
const item = data[labelIndex];
const fakultas = Array.isArray(chart.data.labels[labelIndex])
? chart.data.labels[labelIndex].join(' ')
: chart.data.labels[labelIndex];
if (item) {
labelTooltip.innerHTML = `
<strong>${fakultas}</strong><br>
<div class="d-flex align-items-center mt-1">
<div style="width:12px; height:12px; background-color:rgba(54, 162, 235, 0.8); margin-right:6px; border-radius:2px;"></div>
Proposal: ${item.proposal}
</div>
<div class="d-flex align-items-center mt-1">
<div style="width:12px; height:12px; background-color:rgba(255, 206, 86, 0.8); margin-right:6px; border-radius:2px;"></div>
Monev I: ${item.monev1}
</div>
<div class="d-flex align-items-center mt-1">
<div style="width:12px; height:12px; background-color:rgba(255, 99, 132, 0.8); margin-right:6px; border-radius:2px;"></div>
Monev II: ${item.monev2}
</div>
`;
labelTooltip.style.display = 'block';
labelTooltip.style.left = `${event.pageX + 10}px`;
labelTooltip.style.top = `${event.pageY - labelTooltip.offsetHeight - 10}px`;
canvas.style.cursor = 'pointer';
}
} else {
labelTooltip.style.display = 'none';
canvas.style.cursor = 'default';
}
});
canvas.addEventListener('mouseleave', function () {
labelTooltip.style.display = 'none';
canvas.style.cursor = 'default';
});
} catch (error) {
console.error('Gagal memuat chart:', error);
spinner.style.display = 'none';
canvas.style.display = 'none';
await Swal.fire({
icon: 'error',
title: 'Gagal Memuat Grafik',
text: 'Terjadi kesalahan saat mengambil data grafik.\nSilakan coba lagi.',
confirmButtonText: 'OK',
timer: 4000
});
}
}
// Muat chart pertama kali
document.addEventListener('DOMContentLoaded', () => {
loadChart();
// Tambahkan event listener untuk filter dropdowns
const filterSelects = ['reqJenisPmw', 'reqStatus', 'tahun'];
filterSelects.forEach(selectId => {
const select = document.getElementById(selectId);
if (select) {
select.addEventListener('change', () => {
loadChart();
});
}
});
});
</script>
@endif
@endsection
...@@ -67,7 +67,8 @@ Route::post('login', [Laravel\Fortify\Http\Controllers\AuthenticatedSessionContr ...@@ -67,7 +67,8 @@ Route::post('login', [Laravel\Fortify\Http\Controllers\AuthenticatedSessionContr
Route::group(['middleware' => ['auth:sanctum', 'verified']], function () { Route::group(['middleware' => ['auth:sanctum', 'verified']], function () {
Route::resource('dashboard', DashboardController::class); Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard.index');
Route::get('getChartData', [DashboardController::class, 'getChartData'])->name('dashboard.chart-data');
Route::post('selectmahasiswa', [SelectController::class, 'mahasiswa'])->name('mahasiswa-select'); Route::post('selectmahasiswa', [SelectController::class, 'mahasiswa'])->name('mahasiswa-select');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment