Files
aplikasi-ketertiban/app/Http/Controllers/AttendanceController.php
T
Bro 91b7f1e591 fix: rekap alpha proporsional berdasarkan max 10 jam/hari
Sebelum: alpha = alphaCount / totalRecord (jika jadwal 1 jam,
  1 alpha = 1 hari — tidak akurat)
Sesudah: alpha = alphaCount / 10 (max jam sekolah per hari)
  1 alpha = 0.1 hari, 10 alpha = 1 hari
- Berlaku untuk rekap mingguan dan bulanan
2026-07-24 13:12:55 +07:00

718 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers;
use App\Models\Attendance;
use App\Models\Student;
use App\Models\ViolationType;
use App\Services\ViolationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AttendanceController extends Controller
{
protected ViolationService $violationService;
public function __construct(ViolationService $violationService)
{
$this->violationService = $violationService;
}
public function index(Request $request): View
{
$today = now()->toDateString();
$thisMonth = now()->month;
$thisYear = now()->year;
// Hitung berdasarkan jumlah siswa unik, bukan total record per jam
$todayStudents = \App\Models\Attendance::where('date', $today)
->distinct('student_id')
->count('student_id');
$todayAlphaStudents = \App\Models\Attendance::where('date', $today)
->where('status', 'alpha')
->distinct('student_id')
->count('student_id');
$monthStudents = \App\Models\Attendance::whereMonth('date', $thisMonth)
->whereYear('date', $thisYear)
->distinct('student_id')
->count('student_id');
$monthAlphaStudents = \App\Models\Attendance::whereMonth('date', $thisMonth)
->whereYear('date', $thisYear)
->where('status', 'alpha')
->distinct('student_id')
->count('student_id');
// Calendar data for initial month
$calendarStart = now()->startOfMonth()->toDateString();
$calendarEnd = now()->endOfMonth()->toDateString();
$totalRows = \App\Models\Attendance::selectRaw('date, COUNT(DISTINCT student_id) as total')
->whereBetween('date', [$calendarStart, $calendarEnd])
->groupBy('date')
->pluck('total', 'date');
$alphaRows = \App\Models\Attendance::selectRaw('date, COUNT(DISTINCT student_id) as alpha')
->whereBetween('date', [$calendarStart, $calendarEnd])
->where('status', 'alpha')
->groupBy('date')
->pluck('alpha', 'date');
$calendarData = [];
foreach ($totalRows as $date => $total) {
$calendarData[$date] = [
'total' => $total,
'alpha' => $alphaRows[$date] ?? 0,
];
}
return view('attendances.index', compact(
'todayStudents', 'todayAlphaStudents',
'monthStudents', 'monthAlphaStudents',
'calendarData'
));
}
public function create(Request $request): View
{
$date = $request->input('date', now()->toDateString());
$className = $request->input('class_name');
$students = collect();
if ($className) {
$students = Student::where('is_active', true)
->where('class_name', $className)
->orderBy('full_name')
->get();
}
// Load existing attendances for this class + date
$existing = collect();
if ($students->isNotEmpty()) {
$existing = Attendance::where('date', $date)
->whereIn('student_id', $students->pluck('id'))
->get()
->keyBy(fn($a) => $a->student_id . '-' . $a->lesson_hour);
}
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->distinct()
->orderBy('class_name')
->pluck('class_name');
$lessonHours = range(1, 10);
return view('attendances.create', compact(
'students', 'date', 'className',
'classNames', 'lessonHours', 'existing'
));
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'date' => ['required', 'date'],
'class_name' => ['nullable', 'string'],
'attendances' => ['required', 'array'],
'auto_violation' => ['nullable', 'boolean'],
]);
$date = $request->input('date');
$class_name = $request->input('class_name');
$saved = 0;
$autoViolations = 0;
$alphaIds = [];
$terlambatIds = [];
foreach ($request->input('attendances') as $studentId => $studentData) {
foreach ($studentData as $hour => $data) {
if (!isset($data['student_id'], $data['lesson_hour'], $data['status'])) {
continue;
}
$data['lesson_hour'] = (int) $data['lesson_hour'];
Attendance::updateOrCreate(
[
'student_id' => $data['student_id'],
'date' => $date,
'lesson_hour' => $data['lesson_hour'],
],
[
'status' => $data['status'],
'recorded_by' => $request->user()->id,
]
);
$saved++;
if ($data['status'] === 'alpha') {
$alphaIds[$data['student_id']] = ($alphaIds[$data['student_id']] ?? 0) + 1;
} elseif ($data['status'] === 'terlambat') {
$terlambatIds[$data['student_id']] = ($terlambatIds[$data['student_id']] ?? 0) + 1;
}
}
}
// Auto-generate violation untuk alpha & terlambat
if ($request->boolean('auto_violation') && (!empty($alphaIds) || !empty($terlambatIds))) {
$alphaType = ViolationType::where('slug', 'alpha')->first();
$terlambatType = ViolationType::where('slug', 'terlambat')->first();
foreach ($alphaIds as $studentId => $count) {
if ($alphaType) {
$this->violationService->recordViolation([
'student_id' => $studentId,
'violation_type_id' => $alphaType->id,
'violation_date' => $date,
'points' => max(1, (int) round(($alphaType->points / 10) * $count)),
'description' => "Alpha - Tidak hadir tanpa keterangan ({$count} jam pelajaran)",
'notes' => 'Dibuat otomatis dari presensi.',
'evidences' => [],
], $request->user()->id);
$autoViolations++;
}
}
foreach ($terlambatIds as $studentId => $count) {
if ($terlambatType) {
$this->violationService->recordViolation([
'student_id' => $studentId,
'violation_type_id' => $terlambatType->id,
'violation_date' => $date,
'points' => $terlambatType->points,
'description' => "Terlambat ({$count} jam pelajaran)",
'notes' => 'Dibuat otomatis dari presensi.',
'evidences' => [],
], $request->user()->id);
$autoViolations++;
}
}
if (!$alphaType) {
$msgExtra = 'Jenis pelanggaran "Alpha" tidak ditemukan. Buat jenis pelanggaran dengan slug "alpha".';
}
if (!$terlambatType) {
$msgExtra = (empty($msgExtra) ? '' : $msgExtra . ' ')
. 'Jenis pelanggaran "Terlambat" tidak ditemukan. Buat dengan slug "terlambat".';
}
}
$studentCount = count($request->input('attendances'));
$lessonHourCount = $saved / max($studentCount, 1);
$msg = "Presensi berhasil disimpan — {$studentCount} siswa × " . round($lessonHourCount) . " jam pelajaran.";
if ($autoViolations > 0) {
$msg .= " {$autoViolations} pelanggaran dibuat otomatis (alpha/terlambat).";
} elseif (!empty($msgExtra)) {
$msg .= " Peringatan: {$msgExtra}";
}
return redirect()->route('attendances.create', [
'date' => $date,
'class_name' => $class_name,
])->with('success', $msg);
}
public function recap(Request $request): View
{
$type = $request->input('type', 'daily');
$className = $request->input('class_name');
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->distinct()
->orderBy('class_name')
->pluck('class_name');
if ($type === 'daily') {
return $this->recapDaily($request, $classNames, $type);
} elseif ($type === 'weekly') {
return $this->recapWeekly($request, $classNames, $type);
} else {
return $this->recapMonthly($request, $classNames, $type);
}
}
protected function recapDaily(Request $request, $classNames, $type = 'daily'): View
{
$date = $request->input('date', now()->toDateString());
$className = $request->input('class_name');
$students = collect();
$attendances = collect();
if ($className) {
$students = Student::where('is_active', true)
->where('class_name', $className)
->orderBy('full_name')
->get();
$attendances = Attendance::where('date', $date)
->whereIn('student_id', $students->pluck('id'))
->get()
->keyBy(fn($a) => $a->student_id . '-' . $a->lesson_hour);
}
$lessonHours = range(1, 10);
return view('attendances.recap', compact(
'type', 'date', 'className', 'classNames',
'students', 'attendances', 'lessonHours'
));
}
protected function recapWeekly(Request $request, $classNames, $type = 'weekly'): View
{
$weekStart = $request->input('week_start', now()->startOfWeek()->toDateString());
$start = \Carbon\Carbon::parse($weekStart)->startOfWeek();
$end = (clone $start)->addDays(5); // Senin - Sabtu
$className = $request->input('class_name');
$students = collect();
$recap = collect();
$totals = [];
if ($className) {
$students = Student::where('is_active', true)
->where('class_name', $className)
->orderBy('full_name')
->get();
$allAttendances = Attendance::whereBetween('date', [$start->toDateString(), $end->toDateString()])
->whereIn('student_id', $students->pluck('id'))
->get()
->groupBy(fn($a) => \Illuminate\Support\Carbon::parse($a->date)->toDateString() . '-' . $a->student_id);
$dayLabels = ['Senin', 'Selasa', 'Rabu', 'Kamis', "Jum'at", 'Sabtu'];
for ($i = 0; $i <= 5; $i++) {
$day = (clone $start)->addDays($i);
$dateStr = $day->toDateString();
$row = ['label' => $dayLabels[$i], 'date' => $dateStr, 'students' => []];
foreach ($students as $student) {
$key = $dateStr . '-' . $student->id;
$studentAtt = $allAttendances->get($key, collect());
// Per hari: binary untuk hadir/sakit/izin/terlambat
$hadir = $studentAtt->where('status', 'hadir')->count() > 0 ? 1 : 0;
// Alpha proporsional: jumlah jam alpha / 10 (max jam sekolah per hari)
$alphaCount = $studentAtt->where('status', 'alpha')->count();
$alpha = round($alphaCount / 10, 1);
$sakit = $studentAtt->where('status', 'sakit')->count() > 0 ? 1 : 0;
$izin = $studentAtt->where('status', 'izin')->count() > 0 ? 1 : 0;
$terlambat = $studentAtt->where('status', 'terlambat')->count() > 0 ? 1 : 0;
$total = $studentAtt->count();
$row['students'][$student->id] = [
'name' => $student->full_name,
'hadir' => $hadir,
'alpha' => $alpha,
'sakit' => $sakit,
'izin' => $izin,
'terlambat' => $terlambat,
'total' => $studentAtt->count(),
];
}
$recap->push($row);
}
// Hitung total per siswa selama seminggu
$totals = [];
foreach ($students as $student) {
$totals[$student->id] = ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'terlambat' => 0];
}
foreach ($recap as $row) {
foreach ($row['students'] as $sid => $s) {
$totals[$sid]['hadir'] += $s['hadir'];
$totals[$sid]['sakit'] += $s['sakit'];
$totals[$sid]['izin'] += $s['izin'];
$totals[$sid]['alpha'] += $s['alpha'];
$totals[$sid]['terlambat'] += $s['terlambat'];
}
}
}
return view('attendances.recap', compact(
'type', 'start', 'end', 'className', 'classNames',
'students', 'recap', 'totals'
));
}
protected function recapMonthly(Request $request, $classNames, $type = 'monthly'): View
{
$month = (int) $request->input('month', now()->month);
$year = (int) $request->input('year', now()->year);
$className = $request->input('class_name');
$firstDay = \Carbon\Carbon::create($year, $month, 1);
$lastDay = \Carbon\Carbon::create($year, $month, $firstDay->daysInMonth);
// Generate minggu dalam bulan
$weeks = [];
$weekStart = (clone $firstDay)->startOfWeek();
$weekNum = 1;
while ($weekStart <= $lastDay) {
$weekEnd = (clone $weekStart)->addDays(6);
$weeks[$weekNum] = [
'label' => 'Minggu ' . $weekNum,
'start' => $weekStart->copy()->max($firstDay)->toDateString(),
'end' => $weekEnd->copy()->min($lastDay)->toDateString(),
];
$weekStart->addWeek();
$weekNum++;
}
$students = collect();
$recap = collect();
$totals = [];
if ($className) {
$students = Student::where('is_active', true)
->where('class_name', $className)
->orderBy('full_name')
->get();
$allAttendances = Attendance::whereMonth('date', $month)
->whereYear('date', $year)
->whereIn('student_id', $students->pluck('id'))
->get();
foreach ($weeks as $weekNum => $week) {
$weekAtt = $allAttendances->filter(fn($a) =>
$a->date >= $week['start'] && $a->date <= $week['end']
);
// Ambil tanggal unik dalam minggu ini
$datesInWeek = $weekAtt->map(fn($a) => \Illuminate\Support\Carbon::parse($a->date)->toDateString())->unique();
$totalDays = $datesInWeek->count();
$weekByDay = $weekAtt->groupBy(fn($a) => \Illuminate\Support\Carbon::parse($a->date)->toDateString());
$row = ['label' => $week['label'], 'start' => $week['start'], 'end' => $week['end'], 'students' => []];
foreach ($students as $student) {
$hCount = 0; $sCount = 0; $iCount = 0; $aCount = 0; $tCount = 0; $totalDaysForStudent = 0;
foreach ($datesInWeek as $dateStr) {
$dayAtt = $weekByDay->get($dateStr, collect())->where('student_id', $student->id);
if ($dayAtt->isEmpty()) continue;
$totalDaysForStudent++;
$hasHadir = $dayAtt->where('status', 'hadir')->isNotEmpty();
$hasSakit = $dayAtt->where('status', 'sakit')->isNotEmpty();
$hasIzin = $dayAtt->where('status', 'izin')->isNotEmpty();
$hasAlpha = $dayAtt->where('status', 'alpha')->isNotEmpty();
$hasTerlambat = $dayAtt->where('status', 'terlambat')->isNotEmpty();
if ($hasHadir) $hCount++;
if ($hasSakit) $sCount++;
if ($hasIzin) $iCount++;
if ($hasTerlambat) $tCount++;
// Alpha proporsional: jumlah jam alpha / 10 (max jam sekolah per hari)
$alphaJam = $dayAtt->where('status', 'alpha')->count();
$aCount += round($alphaJam / 10, 1);
}
$row['students'][$student->id] = [
'name' => $student->full_name,
'hadir' => $hCount,
'alpha' => $aCount,
'sakit' => $sCount,
'izin' => $iCount,
'terlambat' => $tCount,
'total' => $totalDaysForStudent,
];
}
$recap->push($row);
}
// Hitung total per siswa selama sebulan
$totals = [];
foreach ($students as $student) {
$totals[$student->id] = ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'terlambat' => 0];
}
foreach ($recap as $row) {
foreach ($row['students'] as $sid => $s) {
$totals[$sid]['hadir'] += $s['hadir'];
$totals[$sid]['sakit'] += $s['sakit'];
$totals[$sid]['izin'] += $s['izin'];
$totals[$sid]['alpha'] += $s['alpha'];
$totals[$sid]['terlambat'] += $s['terlambat'];
}
}
}
return view('attendances.recap', compact(
'type', 'month', 'year', 'className', 'classNames',
'students', 'recap', 'weeks', 'totals'
));
}
public function calendarData(Request $request): JsonResponse
{
$year = (int) $request->input('year', now()->year);
$month = (int) $request->input('month', now()->month);
$daysInMonth = now()->setDate($year, $month, 1)->daysInMonth;
$dateFrom = sprintf('%04d-%02d-01', $year, $month);
$dateTo = sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth);
// Total unique students per date
$totalRows = Attendance::selectRaw('date, COUNT(DISTINCT student_id) as total')
->whereBetween('date', [$dateFrom, $dateTo])
->groupBy('date')
->pluck('total', 'date');
// Alpha unique students per date
$alphaRows = Attendance::selectRaw('date, COUNT(DISTINCT student_id) as alpha')
->whereBetween('date', [$dateFrom, $dateTo])
->where('status', 'alpha')
->groupBy('date')
->pluck('alpha', 'date');
$result = [];
foreach ($totalRows as $date => $total) {
$result[$date] = [
'total' => $total,
'alpha' => $alphaRows[$date] ?? 0,
];
}
return response()->json($result);
}
public function exportWeekly(Request $request)
{
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->distinct()
->orderBy('class_name')
->pluck('class_name');
$view = $this->recapWeekly($request, $classNames);
$data = $view->getData();
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Title
$title = 'Rekap Presensi Mingguan';
if ($data['className']) {
$title .= ' - ' . $data['className'];
}
$title .= ' (' . \Carbon\Carbon::parse($data['start'])->format('d/m/Y') . ' - ' . \Carbon\Carbon::parse($data['end'])->format('d/m/Y') . ')';
$sheet->setCellValue('A1', $title);
$totalCols = 1 + $data['recap']->count() * 4 + 4; // +4 untuk kolom TOTAL
$sheet->mergeCells('A1:' . $this->getColLetter($totalCols - 1) . '1');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
// Header row 2: day names + TOTAL
$sheet->setCellValue('A2', 'Siswa');
$sheet->getStyle('A2')->getFont()->setBold(true);
$col = 2;
foreach ($data['recap'] as $row) {
$cell = $this->getColLetter($col) . '2';
$sheet->setCellValue($cell, $row['label'] . ' ' . \Carbon\Carbon::parse($row['date'])->format('d/m'));
$endCell = $this->getColLetter($col + 3) . '2';
$sheet->mergeCells($cell . ':' . $endCell);
$sheet->getStyle($cell)->getFont()->setBold(true);
$col += 4;
}
// Kolom TOTAL
$totalCell = $this->getColLetter($col) . '2';
$sheet->setCellValue($totalCell, 'TOTAL');
$sheet->mergeCells($totalCell . ':' . $this->getColLetter($col + 3) . '2');
$sheet->getStyle($totalCell)->getFont()->setBold(true);
// Header row 3: H, S, I, A per day + TOTAL
$sheet->setCellValue('A3', '');
$col = 2;
foreach ($data['recap'] as $row) {
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
foreach (range(0, 3) as $i) {
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
}
$col += 4;
}
// Sub-header TOTAL
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
foreach (range(0, 3) as $i) {
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
}
// Data rows
$rowIdx = 4;
foreach ($data['students'] as $student) {
$sheet->setCellValue('A' . $rowIdx, $student->full_name);
$col = 2;
foreach ($data['recap'] as $row) {
$s = $row['students'][$student->id] ?? null;
if ($s) {
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $s['hadir']);
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $s['sakit']);
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $s['izin']);
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $s['alpha']);
} else {
foreach (range(0, 3) as $i) {
$sheet->setCellValue($this->getColLetter($col + $i) . $rowIdx, '-');
}
}
$col += 4;
}
// TOTAL per siswa
$t = $data['totals'][$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $t['hadir']);
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $t['sakit']);
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $t['izin']);
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $t['alpha']);
$rowIdx++;
}
// Auto width
for ($c = 1; $c < $totalCols; $c++) {
$sheet->getColumnDimension($this->getColLetter($c))->setAutoSize(true);
}
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$filename = 'rekap-mingguan' . ($data['className'] ? '-' . $data['className'] : '') . '.xlsx';
$filename = str_replace(['/', '\\', ' '], '-', $filename);
return response()->streamDownload(function () use ($writer) {
$writer->save('php://output');
}, $filename, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
public function exportMonthly(Request $request)
{
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->distinct()
->orderBy('class_name')
->pluck('class_name');
$view = $this->recapMonthly($request, $classNames);
$data = $view->getData();
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Title
$title = 'Rekap Presensi Bulanan';
if ($data['className']) {
$title .= ' - ' . $data['className'];
}
$title .= ' (' . \Carbon\Carbon::create()->month($data['month'])->format('F') . ' ' . $data['year'] . ')';
$sheet->setCellValue('A1', $title);
$totalCols = 1 + $data['recap']->count() * 4 + 4; // +4 untuk kolom TOTAL
$sheet->mergeCells('A1:' . $this->getColLetter($totalCols - 1) . '1');
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
// Header row 2: week names + TOTAL
$sheet->setCellValue('A2', 'Siswa');
$sheet->getStyle('A2')->getFont()->setBold(true);
$col = 2;
foreach ($data['recap'] as $row) {
$cell = $this->getColLetter($col) . '2';
$sheet->setCellValue($cell, $row['label'] . ' ' . \Carbon\Carbon::parse($row['start'])->format('d/m') . '-' . \Carbon\Carbon::parse($row['end'])->format('d/m'));
$endCell = $this->getColLetter($col + 3) . '2';
$sheet->mergeCells($cell . ':' . $endCell);
$sheet->getStyle($cell)->getFont()->setBold(true);
$col += 4;
}
// Kolom TOTAL
$totalCell = $this->getColLetter($col) . '2';
$sheet->setCellValue($totalCell, 'TOTAL');
$sheet->mergeCells($totalCell . ':' . $this->getColLetter($col + 3) . '2');
$sheet->getStyle($totalCell)->getFont()->setBold(true);
// Header row 3: H, S, I, A per week + TOTAL
$sheet->setCellValue('A3', '');
$col = 2;
foreach ($data['recap'] as $row) {
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
foreach (range(0, 3) as $i) {
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
}
$col += 4;
}
// Sub-header TOTAL
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
foreach (range(0, 3) as $i) {
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
}
// Data rows
$rowIdx = 4;
foreach ($data['students'] as $student) {
$sheet->setCellValue('A' . $rowIdx, $student->full_name);
$col = 2;
foreach ($data['recap'] as $row) {
$s = $row['students'][$student->id] ?? null;
if ($s) {
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $s['hadir']);
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $s['sakit']);
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $s['izin']);
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $s['alpha']);
} else {
foreach (range(0, 3) as $i) {
$sheet->setCellValue($this->getColLetter($col + $i) . $rowIdx, '-');
}
}
$col += 4;
}
// TOTAL per siswa
$t = $data['totals'][$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $t['hadir']);
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $t['sakit']);
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $t['izin']);
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $t['alpha']);
$rowIdx++;
}
// Auto width
for ($c = 1; $c < $totalCols; $c++) {
$sheet->getColumnDimension($this->getColLetter($c))->setAutoSize(true);
}
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$filename = 'rekap-bulanan' . ($data['className'] ? '-' . $data['className'] : '') . '.xlsx';
$filename = str_replace(['/', '\\', ' '], '-', $filename);
return response()->streamDownload(function () use ($writer) {
$writer->save('php://output');
}, $filename, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
private function getColLetter(int $col): string
{
$letter = '';
while ($col > 0) {
$col--;
$letter = chr(65 + $col % 26) . $letter;
$col = intval($col / 26);
}
return $letter;
}
}