1186 lines
51 KiB
PHP
1186 lines
51 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AbsensiKegiatan;
|
|
use App\Models\Kegiatan;
|
|
use App\Models\KategoriKegiatan;
|
|
use App\Models\Santri;
|
|
use App\Models\Kelas;
|
|
use App\Models\KelompokKelas;
|
|
use App\Models\SantriKelas;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Carbon\Carbon;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
|
|
class LaporanKegiatanController extends Controller
|
|
{
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* A. INDEX - Main Dashboard Laporan Multi-Tab
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
// 1. Period Selector
|
|
$periode = $request->get('periode', 'minggu_ini');
|
|
[$startDate, $endDate] = $this->getPeriodeRange($periode, $request);
|
|
[$prevStart, $prevEnd] = $this->getPreviousPeriodeRange($periode, $startDate, $endDate);
|
|
|
|
$periodeLabel = $this->getPeriodeLabel($periode, $startDate, $endDate);
|
|
|
|
// 2. KPI Cards
|
|
$kpi = $this->calculateKpi($startDate, $endDate);
|
|
$kpiPrev = $this->calculateKpi($prevStart, $prevEnd);
|
|
|
|
$kpiComparison = [
|
|
'total_kegiatan' => $kpi['total_kegiatan'] - $kpiPrev['total_kegiatan'],
|
|
'avg_kehadiran' => round($kpi['avg_kehadiran'] - $kpiPrev['avg_kehadiran'], 1),
|
|
'santri_perlu_perhatian' => $kpi['santri_perlu_perhatian'] - $kpiPrev['santri_perlu_perhatian'],
|
|
];
|
|
|
|
// 3. Trend Data (line chart)
|
|
$trendData = $this->getTrendData($startDate, $endDate);
|
|
|
|
// 4. Distribusi Santri (funnel chart)
|
|
$distribusiSantri = $this->getDistribusiSantri($startDate, $endDate);
|
|
|
|
// 5. Top & Bottom Kegiatan
|
|
$topKegiatan = $this->getTopBottomKegiatan($startDate, $endDate, 'top', 5);
|
|
$bottomKegiatan = $this->getTopBottomKegiatan($startDate, $endDate, 'bottom', 5);
|
|
|
|
// 6. Kehadiran Per Kelas
|
|
$kehadiranPerKelas = $this->getKehadiranPerKelas($startDate, $endDate);
|
|
|
|
// 7. Heatmap Kelas vs Kategori
|
|
$heatmapData = Cache::remember(
|
|
'laporan_heatmap_' . $startDate->format('Ymd') . '_' . $endDate->format('Ymd'),
|
|
1800,
|
|
fn() => $this->getHeatmapData($startDate, $endDate)
|
|
);
|
|
|
|
// 8. Pattern & Anomaly Detection
|
|
$patterns = Cache::remember(
|
|
'laporan_patterns_' . now()->format('Ymd'),
|
|
3600,
|
|
fn() => $this->patternDetection(new Request)
|
|
);
|
|
|
|
// 9. Santri Perlu Perhatian list (for Tab 3)
|
|
$santriPerluPerhatianList = $this->getSantriPerluPerhatianList($startDate, $endDate, 10);
|
|
|
|
// 10. Leaderboard (for Tab 3)
|
|
$leaderboard = $this->getLeaderboard($startDate, $endDate, 10);
|
|
|
|
// 11. Kegiatan Performance (for Tab 4)
|
|
$kegiatanPerformance = $this->getKegiatanPerformance($startDate, $endDate);
|
|
|
|
// Data filter
|
|
$kategoris = KategoriKegiatan::select('kategori_id', 'nama_kategori')->get();
|
|
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
|
|
|
|
return view('admin.kegiatan.laporan.index', compact(
|
|
'periode', 'startDate', 'endDate', 'periodeLabel',
|
|
'kpi', 'kpiPrev', 'kpiComparison',
|
|
'trendData', 'distribusiSantri',
|
|
'topKegiatan', 'bottomKegiatan',
|
|
'kehadiranPerKelas', 'heatmapData',
|
|
'patterns',
|
|
'santriPerluPerhatianList', 'leaderboard',
|
|
'kegiatanPerformance',
|
|
'kategoris', 'kelasList'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* B. ANALISIS PER KELAS (AJAX)
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function analisPerKelas(Request $request)
|
|
{
|
|
$idKelas = $request->get('id_kelas');
|
|
$kelas = Kelas::with('kelompok')->findOrFail($idKelas);
|
|
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
|
|
$santriIds = SantriKelas::where('id_kelas', $idKelas)->pluck('id_santri');
|
|
|
|
// Kehadiran per santri
|
|
$kehadiranPerSantri = [];
|
|
if ($santriIds->isNotEmpty()) {
|
|
$kehadiranPerSantri = AbsensiKegiatan::whereIn('id_santri', $santriIds)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->select(
|
|
'santris.id_santri', 'santris.nama_lengkap',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Izin" THEN 1 ELSE 0 END) as izin'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa')
|
|
)
|
|
->groupBy('santris.id_santri', 'santris.nama_lengkap')
|
|
->orderBy('hadir', 'desc')
|
|
->get()
|
|
->map(function ($s) {
|
|
$s->persen = $s->total > 0 ? round(($s->hadir / $s->total) * 100, 1) : 0;
|
|
return $s;
|
|
});
|
|
}
|
|
|
|
// Distribusi status
|
|
$distribusi = AbsensiKegiatan::whereIn('id_santri', $santriIds)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->select('status', DB::raw('COUNT(*) as total'))
|
|
->groupBy('status')
|
|
->pluck('total', 'status')
|
|
->toArray();
|
|
|
|
// Trend 4 minggu
|
|
$trend = [];
|
|
for ($i = 3; $i >= 0; $i--) {
|
|
$weekStart = Carbon::parse($endDate)->subWeeks($i)->startOfWeek();
|
|
$weekEnd = Carbon::parse($endDate)->subWeeks($i)->endOfWeek();
|
|
|
|
$weekData = AbsensiKegiatan::whereIn('id_santri', $santriIds)
|
|
->whereBetween('tanggal', [$weekStart, $weekEnd])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$trend[] = [
|
|
'label' => 'Minggu ' . (4 - $i),
|
|
'persen' => ($weekData->total ?? 0) > 0
|
|
? round(($weekData->hadir / $weekData->total) * 100, 1) : 0,
|
|
];
|
|
}
|
|
|
|
return response()->json([
|
|
'kelas' => $kelas,
|
|
'kehadiran_per_santri' => $kehadiranPerSantri,
|
|
'distribusi' => $distribusi,
|
|
'trend' => $trend,
|
|
'jumlah_santri' => $santriIds->count(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* C. DETAIL SANTRI - Individual Report
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function detailSantri($id_santri, Request $request)
|
|
{
|
|
$santri = Santri::where('id_santri', $id_santri)
|
|
->with('kelasSantri.kelas.kelompok')
|
|
->firstOrFail();
|
|
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
|
|
$periodeLabel = $this->getPeriodeLabel($request->get('periode', 'bulan_ini'), $startDate, $endDate);
|
|
|
|
// Stats summary
|
|
$stats = AbsensiKegiatan::where('id_santri', $id_santri)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->select(
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN status = "Izin" THEN 1 ELSE 0 END) as izin'),
|
|
DB::raw('SUM(CASE WHEN status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
|
|
DB::raw('SUM(CASE WHEN status = "Alpa" THEN 1 ELSE 0 END) as alpa')
|
|
)
|
|
->first();
|
|
|
|
$persenKehadiran = $stats->total > 0 ? round(($stats->hadir / $stats->total) * 100, 1) : 0;
|
|
|
|
// Kehadiran per kegiatan
|
|
$perKegiatan = AbsensiKegiatan::where('id_santri', $id_santri)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
|
->select(
|
|
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Izin" THEN 1 ELSE 0 END) as izin'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa')
|
|
)
|
|
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan')
|
|
->get()
|
|
->map(function ($k) {
|
|
$k->persen = $k->total > 0 ? round(($k->hadir / $k->total) * 100, 1) : 0;
|
|
return $k;
|
|
});
|
|
|
|
// Trend 4 minggu
|
|
$trend = [];
|
|
for ($i = 3; $i >= 0; $i--) {
|
|
$ws = Carbon::parse($endDate)->subWeeks($i)->startOfWeek();
|
|
$we = Carbon::parse($endDate)->subWeeks($i)->endOfWeek();
|
|
|
|
$wd = AbsensiKegiatan::where('id_santri', $id_santri)
|
|
->whereBetween('tanggal', [$ws, $we])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$trend[] = [
|
|
'label' => 'Mg ' . (4 - $i),
|
|
'persen' => ($wd->total ?? 0) > 0 ? round(($wd->hadir / $wd->total) * 100, 1) : 0,
|
|
];
|
|
}
|
|
|
|
// Kegiatan paling sering bolos
|
|
$kegiatanBolos = AbsensiKegiatan::where('id_santri', $id_santri)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->where('status', 'Alpa')
|
|
->select('kegiatan_id', DB::raw('COUNT(*) as total_alpa'))
|
|
->groupBy('kegiatan_id')
|
|
->orderByDesc('total_alpa')
|
|
->with('kegiatan:kegiatan_id,nama_kegiatan')
|
|
->first();
|
|
|
|
// Streak: consecutive days hadir
|
|
$streak = $this->calculateStreak($id_santri);
|
|
|
|
// Riwayat absensi terbaru
|
|
$riwayatTerbaru = AbsensiKegiatan::where('id_santri', $id_santri)
|
|
->with('kegiatan.kategori')
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->orderByDesc('tanggal')
|
|
->orderByDesc('waktu_absen')
|
|
->limit(30)
|
|
->get();
|
|
|
|
return view('admin.kegiatan.laporan.detail-santri', compact(
|
|
'santri', 'stats', 'persenKehadiran', 'perKegiatan',
|
|
'trend', 'kegiatanBolos', 'streak',
|
|
'riwayatTerbaru', 'startDate', 'endDate', 'periodeLabel'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* D. SANTRI PERLU PERHATIAN
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function santriPerluPerhatian(Request $request)
|
|
{
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
|
|
$periodeLabel = $this->getPeriodeLabel($request->get('periode', 'bulan_ini'), $startDate, $endDate);
|
|
|
|
$query = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->where('santris.status', 'Aktif')
|
|
->select(
|
|
'santris.id_santri', 'santris.nama_lengkap',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Alpa" THEN 1 ELSE 0 END) as alpa'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Izin" THEN 1 ELSE 0 END) as izin'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status = "Sakit" THEN 1 ELSE 0 END) as sakit'),
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status = "Hadir" THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) as persen')
|
|
)
|
|
->groupBy('santris.id_santri', 'santris.nama_lengkap')
|
|
->having('persen', '<', 70)
|
|
->orderBy('persen', 'asc');
|
|
|
|
// Filter kelas
|
|
if ($request->filled('id_kelas')) {
|
|
$santriIdsInKelas = SantriKelas::where('id_kelas', $request->id_kelas)->pluck('id_santri');
|
|
$query->whereIn('santris.id_santri', $santriIdsInKelas);
|
|
}
|
|
|
|
$santris = $query->paginate(20)->appends(request()->query());
|
|
|
|
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
|
|
|
|
return view('admin.kegiatan.laporan.santri-perlu-perhatian', compact(
|
|
'santris', 'kelasList', 'startDate', 'endDate', 'periodeLabel'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* E. LEADERBOARD
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function leaderboard(Request $request)
|
|
{
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
|
|
$limit = $request->get('limit', 10);
|
|
$data = $this->getLeaderboard($startDate, $endDate, $limit, $request->get('id_kelas'));
|
|
|
|
if ($request->ajax()) {
|
|
return response()->json($data);
|
|
}
|
|
|
|
return view('admin.kegiatan.laporan.leaderboard', [
|
|
'leaderboard' => $data,
|
|
'startDate' => $startDate,
|
|
'endDate' => $endDate,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* F. ANALISIS KEGIATAN (Deep Dive)
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function analisKegiatan($kegiatan_id, Request $request)
|
|
{
|
|
$kegiatan = Kegiatan::with(['kategori', 'kelasKegiatan.kelompok'])
|
|
->where('kegiatan_id', $kegiatan_id)
|
|
->firstOrFail();
|
|
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
$periodeLabel = $this->getPeriodeLabel($request->get('periode', 'bulan_ini'), $startDate, $endDate);
|
|
|
|
// Stats overview
|
|
$stats = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->select(
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN status="Izin" THEN 1 ELSE 0 END) as izin'),
|
|
DB::raw('SUM(CASE WHEN status="Sakit" THEN 1 ELSE 0 END) as sakit'),
|
|
DB::raw('SUM(CASE WHEN status="Alpa" THEN 1 ELSE 0 END) as alpa')
|
|
)
|
|
->first();
|
|
$stats->persen = $stats->total > 0 ? round(($stats->hadir / $stats->total) * 100, 1) : 0;
|
|
|
|
// Trend 4 minggu
|
|
$trend = [];
|
|
for ($i = 3; $i >= 0; $i--) {
|
|
$ws = Carbon::parse($endDate)->subWeeks($i)->startOfWeek();
|
|
$we = Carbon::parse($endDate)->subWeeks($i)->endOfWeek();
|
|
|
|
$wd = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
|
->whereBetween('tanggal', [$ws, $we])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$trend[] = [
|
|
'label' => 'Mg ' . (4 - $i),
|
|
'persen' => ($wd->total ?? 0) > 0 ? round(($wd->hadir / $wd->total) * 100, 1) : 0,
|
|
];
|
|
}
|
|
|
|
// Breakdown per kelas (if multiple)
|
|
$breakdownPerKelas = [];
|
|
if (!$kegiatan->isForAllClasses()) {
|
|
foreach ($kegiatan->kelasKegiatan as $kelas) {
|
|
$sIds = SantriKelas::where('id_kelas', $kelas->id)->pluck('id_santri');
|
|
if ($sIds->isEmpty()) continue;
|
|
|
|
$kd = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
|
->whereIn('id_santri', $sIds)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$breakdownPerKelas[] = [
|
|
'kelas' => $kelas->nama_kelas,
|
|
'total' => $kd->total ?? 0,
|
|
'hadir' => $kd->hadir ?? 0,
|
|
'persen' => ($kd->total ?? 0) > 0 ? round(($kd->hadir / $kd->total) * 100, 1) : 0,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Punctuality (RFID data)
|
|
$punctuality = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->where('status', 'Hadir')
|
|
->whereNotNull('waktu_absen')
|
|
->select(
|
|
DB::raw('SUM(CASE WHEN TIME(waktu_absen) <= TIME(
|
|
(SELECT waktu_mulai FROM kegiatans WHERE kegiatan_id = absensi_kegiatans.kegiatan_id)
|
|
) THEN 1 ELSE 0 END) as tepat_waktu'),
|
|
DB::raw('SUM(CASE WHEN TIME(waktu_absen) > TIME(
|
|
(SELECT waktu_mulai FROM kegiatans WHERE kegiatan_id = absensi_kegiatans.kegiatan_id)
|
|
) THEN 1 ELSE 0 END) as terlambat'),
|
|
DB::raw('COUNT(*) as total')
|
|
)
|
|
->first();
|
|
|
|
// Santri tidak pernah hadir
|
|
$santriTidakPernahHadir = AbsensiKegiatan::where('kegiatan_id', $kegiatan_id)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->select('id_santri', DB::raw('SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir'))
|
|
->groupBy('id_santri')
|
|
->having('hadir', '=', 0)
|
|
->with('santri:id_santri,nama_lengkap')
|
|
->get();
|
|
|
|
// Insights
|
|
$insights = $this->generateKegiatanInsights($kegiatan, $stats, $trend, $breakdownPerKelas);
|
|
|
|
return view('admin.kegiatan.laporan.analisis-kegiatan', compact(
|
|
'kegiatan', 'stats', 'trend', 'breakdownPerKelas',
|
|
'punctuality', 'santriTidakPernahHadir', 'insights',
|
|
'startDate', 'endDate', 'periodeLabel'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* G. PATTERN DETECTION
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function patternDetection(Request $request)
|
|
{
|
|
$patterns = [];
|
|
|
|
// Pattern 1: Consistent Low Attendance (kegiatan <75% for >3 weeks)
|
|
$threeWeeksAgo = Carbon::now()->subWeeks(3);
|
|
$lowAttendance = AbsensiKegiatan::where('tanggal', '>=', $threeWeeksAgo)
|
|
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
|
->select(
|
|
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan',
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan')
|
|
->having('persen', '<', 75)
|
|
->get();
|
|
|
|
foreach ($lowAttendance as $la) {
|
|
$patterns[] = [
|
|
'type' => 'warning',
|
|
'category' => 'Kehadiran Rendah Konsisten',
|
|
'title' => $la->nama_kegiatan . ' (' . $la->persen . '%)',
|
|
'description' => "Kegiatan {$la->nama_kegiatan} memiliki kehadiran konsisten di bawah 75% selama 3 minggu terakhir.",
|
|
'action_url' => route('admin.laporan-kegiatan.analisis-kegiatan', $la->kegiatan_id),
|
|
'action_text' => 'Analisis Detail',
|
|
];
|
|
}
|
|
|
|
// Pattern 2: Day-Specific Low Attendance
|
|
$dayStats = AbsensiKegiatan::where('tanggal', '>=', Carbon::now()->subMonth())
|
|
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
|
->select(
|
|
'kegiatans.hari',
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('kegiatans.hari')
|
|
->having('persen', '<', 70)
|
|
->get();
|
|
|
|
foreach ($dayStats as $ds) {
|
|
$patterns[] = [
|
|
'type' => 'info',
|
|
'category' => 'Pola Hari Tertentu',
|
|
'title' => "Kehadiran rendah di hari {$ds->hari} ({$ds->persen}%)",
|
|
'description' => "Rata-rata kehadiran di hari {$ds->hari} hanya {$ds->persen}% dalam sebulan terakhir.",
|
|
'action_url' => null,
|
|
'action_text' => null,
|
|
];
|
|
}
|
|
|
|
// Pattern 3: Class Attendance Drop (>10% drop vs last week)
|
|
$thisWeekStart = Carbon::now()->startOfWeek();
|
|
$thisWeekEnd = Carbon::now()->endOfWeek();
|
|
$lastWeekStart = Carbon::now()->subWeek()->startOfWeek();
|
|
$lastWeekEnd = Carbon::now()->subWeek()->endOfWeek();
|
|
|
|
$kelasAll = Kelas::active()->get();
|
|
foreach ($kelasAll as $kelas) {
|
|
$sIds = SantriKelas::where('id_kelas', $kelas->id)->pluck('id_santri');
|
|
if ($sIds->isEmpty()) continue;
|
|
|
|
$thisWeek = AbsensiKegiatan::whereIn('id_santri', $sIds)
|
|
->whereBetween('tanggal', [$thisWeekStart, $thisWeekEnd])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
$lastWeek = AbsensiKegiatan::whereIn('id_santri', $sIds)
|
|
->whereBetween('tanggal', [$lastWeekStart, $lastWeekEnd])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$persenThis = ($thisWeek->total ?? 0) > 0 ? round(($thisWeek->hadir / $thisWeek->total) * 100, 1) : null;
|
|
$persenLast = ($lastWeek->total ?? 0) > 0 ? round(($lastWeek->hadir / $lastWeek->total) * 100, 1) : null;
|
|
|
|
if ($persenThis !== null && $persenLast !== null && ($persenLast - $persenThis) > 10) {
|
|
$drop = round($persenLast - $persenThis, 1);
|
|
$patterns[] = [
|
|
'type' => 'danger',
|
|
'category' => 'Penurunan Kelas',
|
|
'title' => "Kelas {$kelas->nama_kelas} turun {$drop}%",
|
|
'description' => "Kehadiran kelas {$kelas->nama_kelas} turun dari {$persenLast}% ke {$persenThis}% dalam seminggu.",
|
|
'action_url' => null,
|
|
'action_text' => 'Lihat Detail Kelas',
|
|
];
|
|
}
|
|
}
|
|
|
|
// Pattern 4: Santri Absent Streak (3+ consecutive absences)
|
|
$santriAbsentStreak = DB::select("
|
|
SELECT s.id_santri, s.nama_lengkap, COUNT(*) as consecutive_absent
|
|
FROM absensi_kegiatans a
|
|
JOIN santris s ON a.id_santri = s.id_santri
|
|
WHERE a.status = 'Alpa'
|
|
AND a.tanggal >= ?
|
|
AND s.status = 'Aktif'
|
|
GROUP BY s.id_santri, s.nama_lengkap
|
|
HAVING consecutive_absent >= 3
|
|
ORDER BY consecutive_absent DESC
|
|
LIMIT 10
|
|
", [Carbon::now()->subWeeks(2)->format('Y-m-d')]);
|
|
|
|
foreach ($santriAbsentStreak as $sas) {
|
|
$patterns[] = [
|
|
'type' => 'danger',
|
|
'category' => 'Absen Beruntun',
|
|
'title' => "{$sas->nama_lengkap} ({$sas->consecutive_absent}x Alpa)",
|
|
'description' => "Santri {$sas->nama_lengkap} tercatat {$sas->consecutive_absent} kali Alpa dalam 2 minggu terakhir.",
|
|
'action_url' => route('admin.laporan-kegiatan.detail-santri', $sas->id_santri),
|
|
'action_text' => 'Lihat Detail',
|
|
];
|
|
}
|
|
|
|
if ($request->ajax()) {
|
|
return response()->json($patterns);
|
|
}
|
|
|
|
return $patterns;
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* H. EXPORT EXCEL (CSV Fallback)
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function exportExcel(Request $request)
|
|
{
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
|
|
$contents = $request->get('content', ['summary']);
|
|
|
|
$filename = 'laporan_kegiatan_' . $startDate->format('Ymd') . '_' . $endDate->format('Ymd') . '.csv';
|
|
|
|
return response()->streamDownload(function () use ($startDate, $endDate, $contents) {
|
|
$handle = fopen('php://output', 'w');
|
|
// BOM for Excel UTF-8
|
|
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
|
|
|
// Sheet 1: Summary
|
|
if (in_array('summary', $contents)) {
|
|
fputcsv($handle, ['=== RINGKASAN LAPORAN KEGIATAN ===']);
|
|
fputcsv($handle, ['Periode', $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y')]);
|
|
|
|
$kpi = $this->calculateKpi($startDate, $endDate);
|
|
fputcsv($handle, ['Total Kegiatan', $kpi['total_kegiatan']]);
|
|
fputcsv($handle, ['Rata-rata Kehadiran', $kpi['avg_kehadiran'] . '%']);
|
|
fputcsv($handle, ['Kegiatan Terbaik', $kpi['kegiatan_terbaik']['nama'] ?? '-', ($kpi['kegiatan_terbaik']['persen'] ?? 0) . '%']);
|
|
fputcsv($handle, ['Santri Perlu Perhatian', $kpi['santri_perlu_perhatian']]);
|
|
fputcsv($handle, []);
|
|
}
|
|
|
|
// Per Kelas
|
|
if (in_array('per_kelas', $contents)) {
|
|
fputcsv($handle, ['=== KEHADIRAN PER KELAS ===']);
|
|
fputcsv($handle, ['Kelompok', 'Kelas', 'Jumlah Santri', 'Total Absensi', 'Hadir', 'Izin', 'Sakit', 'Alpa', '% Kehadiran']);
|
|
|
|
$perKelas = $this->getKehadiranPerKelas($startDate, $endDate);
|
|
foreach ($perKelas as $kelompok) {
|
|
foreach ($kelompok['kelas'] as $k) {
|
|
fputcsv($handle, [
|
|
$kelompok['nama_kelompok'], $k['nama_kelas'], $k['jumlah_santri'],
|
|
$k['total'], $k['hadir'], $k['izin'], $k['sakit'], $k['alpa'], $k['persen'] . '%'
|
|
]);
|
|
}
|
|
}
|
|
fputcsv($handle, []);
|
|
}
|
|
|
|
// Per Santri (warning: large)
|
|
if (in_array('per_santri', $contents)) {
|
|
fputcsv($handle, ['=== DETAIL PER SANTRI ===']);
|
|
fputcsv($handle, ['ID Santri', 'Nama', 'Total', 'Hadir', 'Izin', 'Sakit', 'Alpa', '% Kehadiran']);
|
|
|
|
$perSantri = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->where('santris.status', 'Aktif')
|
|
->select(
|
|
'santris.id_santri', 'santris.nama_lengkap',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Izin" THEN 1 ELSE 0 END) as izin'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Sakit" THEN 1 ELSE 0 END) as sakit'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Alpa" THEN 1 ELSE 0 END) as alpa')
|
|
)
|
|
->groupBy('santris.id_santri', 'santris.nama_lengkap')
|
|
->orderBy('santris.nama_lengkap')
|
|
->get();
|
|
|
|
foreach ($perSantri as $s) {
|
|
$persen = $s->total > 0 ? round(($s->hadir / $s->total) * 100, 1) : 0;
|
|
fputcsv($handle, [$s->id_santri, $s->nama_lengkap, $s->total, $s->hadir, $s->izin, $s->sakit, $s->alpa, $persen . '%']);
|
|
}
|
|
fputcsv($handle, []);
|
|
}
|
|
|
|
fclose($handle);
|
|
}, $filename, [
|
|
'Content-Type' => 'text/csv',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* I. EXPORT PDF
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function exportPdf(Request $request)
|
|
{
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'bulan_ini'), $request
|
|
);
|
|
|
|
$periodeLabel = $this->getPeriodeLabel($request->get('periode', 'bulan_ini'), $startDate, $endDate);
|
|
|
|
$kpi = $this->calculateKpi($startDate, $endDate);
|
|
$kehadiranPerKelas = $this->getKehadiranPerKelas($startDate, $endDate);
|
|
$topKegiatan = $this->getTopBottomKegiatan($startDate, $endDate, 'top', 5);
|
|
$bottomKegiatan = $this->getTopBottomKegiatan($startDate, $endDate, 'bottom', 5);
|
|
$distribusiSantri = $this->getDistribusiSantri($startDate, $endDate);
|
|
$santriPerluPerhatianList = $this->getSantriPerluPerhatianList($startDate, $endDate, 15);
|
|
|
|
$pdf = Pdf::loadView('admin.kegiatan.laporan.pdf-template', compact(
|
|
'kpi', 'periodeLabel', 'startDate', 'endDate',
|
|
'kehadiranPerKelas', 'topKegiatan', 'bottomKegiatan',
|
|
'distribusiSantri', 'santriPerluPerhatianList'
|
|
));
|
|
|
|
$pdf->setPaper('A4', 'portrait');
|
|
|
|
return $pdf->download('laporan_kegiatan_' . $startDate->format('Ymd') . '_' . $endDate->format('Ymd') . '.pdf');
|
|
}
|
|
|
|
/**
|
|
* ═══════════════════════════════════════════
|
|
* J. REFRESH KPI (AJAX)
|
|
* ═══════════════════════════════════════════
|
|
*/
|
|
public function refreshKpi(Request $request)
|
|
{
|
|
[$startDate, $endDate] = $this->getPeriodeRange(
|
|
$request->get('periode', 'minggu_ini'), $request
|
|
);
|
|
|
|
$kpi = $this->calculateKpi($startDate, $endDate);
|
|
|
|
return response()->json($kpi);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════
|
|
// PRIVATE HELPER METHODS
|
|
// ═══════════════════════════════════════════
|
|
|
|
/**
|
|
* Get date range for a given period
|
|
*/
|
|
private function getPeriodeRange($periode, Request $request = null)
|
|
{
|
|
switch ($periode) {
|
|
case 'hari_ini':
|
|
return [Carbon::today(), Carbon::today()];
|
|
case 'minggu_ini':
|
|
return [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()];
|
|
case 'bulan_ini':
|
|
return [Carbon::now()->startOfMonth(), Carbon::now()->endOfMonth()];
|
|
case 'semester_ini':
|
|
$now = Carbon::now();
|
|
if ($now->month >= 7) {
|
|
return [Carbon::create($now->year, 7, 1), Carbon::create($now->year, 12, 31)];
|
|
}
|
|
return [Carbon::create($now->year, 1, 1), Carbon::create($now->year, 6, 30)];
|
|
case 'custom':
|
|
$dari = $request ? $request->get('tanggal_dari', Carbon::now()->startOfMonth()->format('Y-m-d')) : Carbon::now()->startOfMonth()->format('Y-m-d');
|
|
$sampai = $request ? $request->get('tanggal_sampai', Carbon::now()->format('Y-m-d')) : Carbon::now()->format('Y-m-d');
|
|
return [Carbon::parse($dari), Carbon::parse($sampai)];
|
|
default:
|
|
return [Carbon::now()->startOfWeek(), Carbon::now()->endOfWeek()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the previous period range (same length, shifted back)
|
|
*/
|
|
private function getPreviousPeriodeRange($periode, $startDate, $endDate)
|
|
{
|
|
$diff = $startDate->diffInDays($endDate) + 1;
|
|
return [
|
|
Carbon::parse($startDate)->subDays($diff),
|
|
Carbon::parse($endDate)->subDays($diff),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Human-readable period label
|
|
*/
|
|
private function getPeriodeLabel($periode, $startDate, $endDate)
|
|
{
|
|
$labels = [
|
|
'hari_ini' => 'Hari Ini (' . Carbon::today()->locale('id')->isoFormat('D MMMM YYYY') . ')',
|
|
'minggu_ini' => 'Minggu Ini',
|
|
'bulan_ini' => Carbon::now()->locale('id')->isoFormat('MMMM YYYY'),
|
|
'semester_ini' => 'Semester ' . (Carbon::now()->month >= 7 ? 'Ganjil' : 'Genap') . ' ' . Carbon::now()->year,
|
|
'custom' => $startDate->format('d/m/Y') . ' - ' . $endDate->format('d/m/Y'),
|
|
];
|
|
return $labels[$periode] ?? $labels['minggu_ini'];
|
|
}
|
|
|
|
/**
|
|
* Calculate KPI metrics
|
|
*/
|
|
private function calculateKpi($startDate, $endDate)
|
|
{
|
|
// Total kegiatan unik
|
|
$totalKegiatan = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->distinct('kegiatan_id')
|
|
->count('kegiatan_id');
|
|
|
|
// Rata-rata kehadiran
|
|
$avgData = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
$avgKehadiran = ($avgData->total ?? 0) > 0 ? round(($avgData->hadir / $avgData->total) * 100, 1) : 0;
|
|
|
|
// Kegiatan terbaik
|
|
$kegiatanTerbaik = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
|
->select(
|
|
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan',
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan')
|
|
->orderByDesc('persen')
|
|
->first();
|
|
|
|
// Santri perlu perhatian (<70%)
|
|
$santriPerluPerhatian = DB::table('absensi_kegiatans')
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->where('santris.status', 'Aktif')
|
|
->select(
|
|
'santris.id_santri',
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('santris.id_santri')
|
|
->having('persen', '<', 70)
|
|
->get()
|
|
->count();
|
|
|
|
return [
|
|
'total_kegiatan' => $totalKegiatan,
|
|
'avg_kehadiran' => $avgKehadiran,
|
|
'kegiatan_terbaik' => $kegiatanTerbaik ? [
|
|
'nama' => $kegiatanTerbaik->nama_kegiatan,
|
|
'persen' => $kegiatanTerbaik->persen,
|
|
] : ['nama' => '-', 'persen' => 0],
|
|
'santri_perlu_perhatian' => $santriPerluPerhatian,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get trend data for line chart
|
|
*/
|
|
private function getTrendData($startDate, $endDate)
|
|
{
|
|
$diffDays = $startDate->diffInDays($endDate);
|
|
$groupBy = $diffDays > 14 ? 'week' : 'day';
|
|
|
|
$kategoris = KategoriKegiatan::all();
|
|
$labels = [];
|
|
$datasets = [];
|
|
|
|
if ($groupBy === 'week') {
|
|
$current = Carbon::parse($startDate)->startOfWeek();
|
|
$weekNum = 1;
|
|
while ($current->lte($endDate)) {
|
|
$labels[] = 'Mg ' . $weekNum;
|
|
$weekNum++;
|
|
$current->addWeek();
|
|
}
|
|
|
|
foreach ($kategoris as $kategori) {
|
|
$data = [];
|
|
$current = Carbon::parse($startDate)->startOfWeek();
|
|
while ($current->lte($endDate)) {
|
|
$ws = Carbon::parse($current);
|
|
$we = Carbon::parse($current)->endOfWeek();
|
|
|
|
$wd = AbsensiKegiatan::whereBetween('tanggal', [$ws, $we])
|
|
->whereHas('kegiatan', fn($q) => $q->where('kategori_id', $kategori->kategori_id))
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$data[] = ($wd->total ?? 0) > 0 ? round(($wd->hadir / $wd->total) * 100, 1) : null;
|
|
$current->addWeek();
|
|
}
|
|
$datasets[] = ['kategori' => $kategori->nama_kategori, 'data' => $data];
|
|
}
|
|
} else {
|
|
$current = Carbon::parse($startDate);
|
|
while ($current->lte($endDate)) {
|
|
$labels[] = $current->format('d/m');
|
|
|
|
$current->addDay();
|
|
}
|
|
|
|
foreach ($kategoris as $kategori) {
|
|
$data = [];
|
|
$current = Carbon::parse($startDate);
|
|
while ($current->lte($endDate)) {
|
|
$wd = AbsensiKegiatan::whereDate('tanggal', $current)
|
|
->whereHas('kegiatan', fn($q) => $q->where('kategori_id', $kategori->kategori_id))
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$data[] = ($wd->total ?? 0) > 0 ? round(($wd->hadir / $wd->total) * 100, 1) : null;
|
|
$current->addDay();
|
|
}
|
|
$datasets[] = ['kategori' => $kategori->nama_kategori, 'data' => $data];
|
|
}
|
|
}
|
|
|
|
return ['labels' => $labels, 'datasets' => $datasets];
|
|
}
|
|
|
|
/**
|
|
* Get santri distribution by attendance percentage
|
|
*/
|
|
private function getDistribusiSantri($startDate, $endDate)
|
|
{
|
|
$santriStats = DB::table('absensi_kegiatans')
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->where('santris.status', 'Aktif')
|
|
->select(
|
|
'santris.id_santri',
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('santris.id_santri')
|
|
->get();
|
|
|
|
$distribusi = [
|
|
'Perfect (100%)' => 0,
|
|
'Sangat Baik (95-99%)' => 0,
|
|
'Baik (85-94%)' => 0,
|
|
'Cukup (75-84%)' => 0,
|
|
'Perlu Perhatian (<75%)' => 0,
|
|
];
|
|
|
|
foreach ($santriStats as $s) {
|
|
if ($s->persen >= 100) $distribusi['Perfect (100%)']++;
|
|
elseif ($s->persen >= 95) $distribusi['Sangat Baik (95-99%)']++;
|
|
elseif ($s->persen >= 85) $distribusi['Baik (85-94%)']++;
|
|
elseif ($s->persen >= 75) $distribusi['Cukup (75-84%)']++;
|
|
else $distribusi['Perlu Perhatian (<75%)']++;
|
|
}
|
|
|
|
$total = $santriStats->count();
|
|
|
|
return collect($distribusi)->map(function ($count, $label) use ($total) {
|
|
return [
|
|
'label' => $label,
|
|
'count' => $count,
|
|
'percentage' => $total > 0 ? round(($count / $total) * 100, 1) : 0,
|
|
];
|
|
})->values()->toArray();
|
|
}
|
|
|
|
/**
|
|
* Get top or bottom kegiatan by attendance
|
|
*/
|
|
private function getTopBottomKegiatan($startDate, $endDate, $type = 'top', $limit = 5)
|
|
{
|
|
$order = $type === 'top' ? 'desc' : 'asc';
|
|
|
|
return AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
|
->leftJoin('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
|
|
->select(
|
|
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan',
|
|
'kategori_kegiatans.nama_kategori',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kategori_kegiatans.nama_kategori')
|
|
->orderBy('persen', $order)
|
|
->limit($limit)
|
|
->get()
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Get kehadiran per kelas grouped by kelompok
|
|
*/
|
|
private function getKehadiranPerKelas($startDate, $endDate)
|
|
{
|
|
$kelompoks = KelompokKelas::active()->ordered()
|
|
->with(['kelas' => fn($q) => $q->active()->ordered()])
|
|
->get();
|
|
|
|
$result = [];
|
|
foreach ($kelompoks as $kelompok) {
|
|
$kelasData = [];
|
|
foreach ($kelompok->kelas as $kelas) {
|
|
$santriIds = SantriKelas::where('id_kelas', $kelas->id)->pluck('id_santri');
|
|
if ($santriIds->isEmpty()) continue;
|
|
|
|
$absensi = AbsensiKegiatan::whereIn('id_santri', $santriIds)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->selectRaw('
|
|
COUNT(*) as total,
|
|
SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir,
|
|
SUM(CASE WHEN status="Izin" THEN 1 ELSE 0 END) as izin,
|
|
SUM(CASE WHEN status="Sakit" THEN 1 ELSE 0 END) as sakit,
|
|
SUM(CASE WHEN status="Alpa" THEN 1 ELSE 0 END) as alpa
|
|
')
|
|
->first();
|
|
|
|
$kelasData[] = [
|
|
'id_kelas' => $kelas->id,
|
|
'nama_kelas' => $kelas->nama_kelas,
|
|
'jumlah_santri' => $santriIds->count(),
|
|
'total' => $absensi->total ?? 0,
|
|
'hadir' => $absensi->hadir ?? 0,
|
|
'izin' => $absensi->izin ?? 0,
|
|
'sakit' => $absensi->sakit ?? 0,
|
|
'alpa' => $absensi->alpa ?? 0,
|
|
'persen' => ($absensi->total ?? 0) > 0
|
|
? round(($absensi->hadir / $absensi->total) * 100, 1) : 0,
|
|
];
|
|
}
|
|
if (!empty($kelasData)) {
|
|
$result[] = [
|
|
'nama_kelompok' => $kelompok->nama_kelompok,
|
|
'kelas' => $kelasData,
|
|
];
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get heatmap data: Kelas vs Kategori
|
|
*/
|
|
private function getHeatmapData($startDate, $endDate)
|
|
{
|
|
$kategoris = KategoriKegiatan::all();
|
|
$kelasList = Kelas::active()->ordered()->with('kelompok')->get();
|
|
|
|
$heatmap = [];
|
|
foreach ($kelasList as $kelas) {
|
|
$santriIds = SantriKelas::where('id_kelas', $kelas->id)->pluck('id_santri');
|
|
$row = ['kelas' => $kelas->nama_kelas, 'kelompok' => $kelas->kelompok->nama_kelompok ?? '-', 'data' => []];
|
|
|
|
foreach ($kategoris as $kategori) {
|
|
if ($santriIds->isEmpty()) {
|
|
$row['data'][$kategori->nama_kategori] = null;
|
|
continue;
|
|
}
|
|
|
|
// Check if this kelas has kegiatan in this kategori
|
|
$kegiatanIds = Kegiatan::where('kategori_id', $kategori->kategori_id)
|
|
->pluck('kegiatan_id');
|
|
|
|
$absensi = AbsensiKegiatan::whereIn('id_santri', $santriIds)
|
|
->whereIn('kegiatan_id', $kegiatanIds)
|
|
->whereBetween('tanggal', [$startDate, $endDate])
|
|
->selectRaw('COUNT(*) as total, SUM(CASE WHEN status="Hadir" THEN 1 ELSE 0 END) as hadir')
|
|
->first();
|
|
|
|
$row['data'][$kategori->nama_kategori] = ($absensi->total ?? 0) > 0
|
|
? round(($absensi->hadir / $absensi->total) * 100, 1) : null;
|
|
}
|
|
$heatmap[] = $row;
|
|
}
|
|
|
|
return ['rows' => $heatmap, 'columns' => $kategoris->pluck('nama_kategori')->toArray()];
|
|
}
|
|
|
|
/**
|
|
* Get santri perlu perhatian list
|
|
*/
|
|
private function getSantriPerluPerhatianList($startDate, $endDate, $limit = 10)
|
|
{
|
|
return AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->where('santris.status', 'Aktif')
|
|
->select(
|
|
'santris.id_santri', 'santris.nama_lengkap',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Alpa" THEN 1 ELSE 0 END) as alpa'),
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('santris.id_santri', 'santris.nama_lengkap')
|
|
->having('persen', '<', 70)
|
|
->orderBy('persen', 'asc')
|
|
->limit($limit)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Get leaderboard (top santri)
|
|
*/
|
|
private function getLeaderboard($startDate, $endDate, $limit = 10, $idKelas = null)
|
|
{
|
|
$query = AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('santris', 'absensi_kegiatans.id_santri', '=', 'santris.id_santri')
|
|
->where('santris.status', 'Aktif')
|
|
->select(
|
|
'santris.id_santri', 'santris.nama_lengkap',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('santris.id_santri', 'santris.nama_lengkap')
|
|
->orderByDesc('persen')
|
|
->orderByDesc('hadir')
|
|
->limit($limit);
|
|
|
|
if ($idKelas) {
|
|
$sIds = SantriKelas::where('id_kelas', $idKelas)->pluck('id_santri');
|
|
$query->whereIn('santris.id_santri', $sIds);
|
|
}
|
|
|
|
return $query->get()->map(function ($s) {
|
|
$s->streak = $this->calculateStreak($s->id_santri);
|
|
return $s;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get kegiatan performance table
|
|
*/
|
|
private function getKegiatanPerformance($startDate, $endDate)
|
|
{
|
|
return AbsensiKegiatan::whereBetween('tanggal', [$startDate, $endDate])
|
|
->join('kegiatans', 'absensi_kegiatans.kegiatan_id', '=', 'kegiatans.kegiatan_id')
|
|
->leftJoin('kategori_kegiatans', 'kegiatans.kategori_id', '=', 'kategori_kegiatans.kategori_id')
|
|
->select(
|
|
'kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kegiatans.hari',
|
|
'kategori_kegiatans.nama_kategori',
|
|
DB::raw('COUNT(*) as total'),
|
|
DB::raw('SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END) as hadir'),
|
|
DB::raw('ROUND(SUM(CASE WHEN absensi_kegiatans.status="Hadir" THEN 1 ELSE 0 END)/COUNT(*)*100,1) as persen')
|
|
)
|
|
->groupBy('kegiatans.kegiatan_id', 'kegiatans.nama_kegiatan', 'kegiatans.hari', 'kategori_kegiatans.nama_kategori')
|
|
->orderByDesc('persen')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Calculate consecutive hadir streak for a santri
|
|
*/
|
|
private function calculateStreak($id_santri)
|
|
{
|
|
$absensis = AbsensiKegiatan::where('id_santri', $id_santri)
|
|
->orderByDesc('tanggal')
|
|
->orderByDesc('waktu_absen')
|
|
->select('status')
|
|
->limit(50)
|
|
->get();
|
|
|
|
$streak = 0;
|
|
foreach ($absensis as $a) {
|
|
if ($a->status === 'Hadir') {
|
|
$streak++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return $streak;
|
|
}
|
|
|
|
/**
|
|
* Generate insights for a specific kegiatan
|
|
*/
|
|
private function generateKegiatanInsights($kegiatan, $stats, $trend, $breakdownPerKelas)
|
|
{
|
|
$insights = [];
|
|
|
|
// Insight: Overall performance
|
|
if ($stats->persen >= 90) {
|
|
$insights[] = [
|
|
'type' => 'success',
|
|
'icon' => 'fas fa-star',
|
|
'text' => "Kegiatan {$kegiatan->nama_kegiatan} memiliki kehadiran sangat baik ({$stats->persen}%).",
|
|
];
|
|
} elseif ($stats->persen < 70) {
|
|
$insights[] = [
|
|
'type' => 'danger',
|
|
'icon' => 'fas fa-exclamation-triangle',
|
|
'text' => "Kehadiran {$kegiatan->nama_kegiatan} di bawah standar ({$stats->persen}%). Perlu evaluasi.",
|
|
];
|
|
}
|
|
|
|
// Insight: Trend direction
|
|
if (count($trend) >= 2) {
|
|
$last = end($trend)['persen'];
|
|
$prev = $trend[count($trend) - 2]['persen'];
|
|
$diff = $last - $prev;
|
|
if ($diff > 5) {
|
|
$insights[] = [
|
|
'type' => 'success',
|
|
'icon' => 'fas fa-arrow-up',
|
|
'text' => "Tren kehadiran meningkat +{$diff}% dalam minggu terakhir.",
|
|
];
|
|
} elseif ($diff < -5) {
|
|
$insights[] = [
|
|
'type' => 'warning',
|
|
'icon' => 'fas fa-arrow-down',
|
|
'text' => "Tren kehadiran menurun {$diff}% dalam minggu terakhir.",
|
|
];
|
|
}
|
|
}
|
|
|
|
// Insight: Kelas with lowest attendance
|
|
if (!empty($breakdownPerKelas)) {
|
|
$lowest = collect($breakdownPerKelas)->sortBy('persen')->first();
|
|
if ($lowest && $lowest['persen'] < 70) {
|
|
$insights[] = [
|
|
'type' => 'warning',
|
|
'icon' => 'fas fa-users',
|
|
'text' => "Kelas {$lowest['kelas']} memiliki kehadiran terendah ({$lowest['persen']}%).",
|
|
];
|
|
}
|
|
}
|
|
|
|
// Insight: Alpa count
|
|
if ($stats->alpa > 0) {
|
|
$alpaPercent = $stats->total > 0 ? round(($stats->alpa / $stats->total) * 100, 1) : 0;
|
|
if ($alpaPercent > 10) {
|
|
$insights[] = [
|
|
'type' => 'danger',
|
|
'icon' => 'fas fa-user-times',
|
|
'text' => "Tingkat Alpa mencapai {$alpaPercent}% ({$stats->alpa} kali). Perlu tindakan segera.",
|
|
];
|
|
}
|
|
}
|
|
|
|
return $insights;
|
|
}
|
|
}
|