get(); // Determine filter parameters $tanggal = $request->get('tanggal'); $bulan = $request->get('bulan'); $tahun = $request->get('tahun'); // Default ke hari ini hanya jika tidak ada filter apapun if (!$tanggal && !$bulan && !$tahun) { $tanggal = now()->format('Y-m-d'); } // Build query untuk fetch attendance records $query = AttendanceHistoryDaily::with(['student', 'class']) ->where('id_class', $classId); // Jika tanggal diset, filter by exact date if ($tanggal && !$bulan && !$tahun) { $query->whereDate('created_at', $tanggal); } // Jika bulan dan tahun keduanya diset, filter by month AND year else if ($bulan && $tahun) { $query->whereMonth('created_at', $bulan) ->whereYear('created_at', $tahun); } // Jika hanya bulan diset, filter by month else if ($bulan) { $query->whereMonth('created_at', $bulan); } // Jika hanya tahun diset, filter by year else if ($tahun) { $query->whereYear('created_at', $tahun); } $absensiFromDB = $query->orderBy('created_at', 'desc')->get(); // Dapatkan semua hari UNIK dari records $uniqueDates = $absensiFromDB ->map(function($item) { return $item->created_at->format('Y-m-d'); }) ->unique() ->values() ->sort() ->reverse() // Terbaru dulu ->values(); // Jika filter tanggal spesifik dan tidak ada data, tambahkan tanggal tersebut if ($tanggal && !$bulan && !$tahun && !$uniqueDates->contains($tanggal)) { $uniqueDates = collect([$tanggal])->merge($uniqueDates); } // Build result: untuk SETIAP HARI, tampilkan SEMUA SISWA $result = []; foreach ($uniqueDates as $date) { foreach ($students as $student) { // Cari record di database untuk student + date ini $record = $absensiFromDB ->where('id_student', $student->id) ->filter(function($item) use ($date) { return $item->created_at->format('Y-m-d') === $date; }) ->first(); if ($record) { // Ada record di database $result[] = (object)[ 'id' => $record->id, 'student' => $record->student, 'class' => $record->class, 'status' => $record->status, 'created_at' => $record->created_at, 'picture' => $record->picture, ]; } else { // Dummy record: siswa belum absen di hari ini // Set created_at ke hari tersebut 00:00:00 untuk filter bekerja dengan baik $dummyDateTime = \Carbon\Carbon::parse($date . ' 00:00:00'); $result[] = (object)[ 'id' => null, 'student' => $student, 'class' => $kelas, 'status' => 'in progress', 'created_at' => $dummyDateTime, 'picture' => null, ]; } } } // Group data by tanggal (created_at) $groupedByDate = collect(); foreach ($result as $item) { $dateKey = $item->created_at->format('Y-m-d'); if (!$groupedByDate->has($dateKey)) { $groupedByDate->put($dateKey, []); } $items = $groupedByDate->get($dateKey); $items[] = $item; $groupedByDate->put($dateKey, $items); } // Urutkan by tanggal (terbaru dulu) $groupedArray = $groupedByDate->toArray(); krsort($groupedArray); // Get today's date untuk default filter $today = now()->format('Y-m-d'); return view('pages.absensi.absensi_harian', [ 'absensi' => collect($result), 'groupedByDate' => $groupedArray, 'today' => $today, 'kelas' => $kelas, 'selectedTanggal' => $tanggal, 'selectedBulan' => $bulan, 'selectedTahun' => $tahun ]); } public function editStatus(Request $request, $id) { $request->validate([ 'status' => 'required|string' ]); // Jika $id adalah 'null', artinya create baru (belum ada di database) if ($id === 'null') { $request->validate([ 'id_student' => 'required|exists:students,id', 'id_class' => 'required|exists:clases,id', ]); try { AttendanceHistoryDaily::create([ 'id_student' => $request->id_student, 'id_class' => $request->id_class, 'status' => $request->status, 'picture' => $request->picture ?? '', ]); return response()->json(['success' => true, 'message' => 'Data absensi berhasil ditambahkan.']); } catch (\Exception $e) { return response()->json(['success' => false, 'message' => 'Error: ' . $e->getMessage()], 500); } } else { // Update existing $absensi = AttendanceHistoryDaily::findOrFail($id); $absensi->status = $request->status; $absensi->save(); return response()->json(['success' => true, 'message' => 'Status absensi berhasil diupdate.']); } } public function exportExcel($classId, Request $request) { // Get kelas info $kelas = Classes::findOrFail($classId); $kelasName = $kelas->name ?? 'Kelas'; $tanggal = $request->get('tanggal'); $bulan = $request->get('bulan'); $tahun = $request->get('tahun'); // Jika bulan kosong tapi tahun ada, lakukan export tahunan if (empty($tanggal) && empty($bulan) && $tahun) { return $this->exportExcelYearly($classId, $tahun); } // Get data absensi harian $rows = $this->getAbsensiHarianRowsForExport($classId, $request); // Build filename suffix if ($tanggal) { $suffixTanggal = '_' . preg_replace('/[^0-9\-]/', '', $tanggal); } else if ($bulan && $tahun) { $suffixTanggal = '_' . $tahun . '-' . $bulan; } else if ($bulan) { $suffixTanggal = '_Bulan' . $bulan; } else if ($tahun) { $suffixTanggal = '_' . $tahun; } else { $suffixTanggal = '_' . now()->format('Y-m-d'); } $filename = 'Rekap_Absensi_Harian_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . $suffixTanggal; // Create spreadsheet $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Absensi Harian'); // Insert 2 rows untuk title dan subtitle $sheet->insertNewRowBefore(1, 2); // Title and subtitle $title = 'REKAP ABSENSI HARIAN - ' . strtoupper($kelasName); $subtitle = 'Diekspor pada: ' . now()->timezone('Asia/Jakarta')->format('d M Y H:i:s') . ' WIB'; $sheet->setCellValue('A1', $title); $sheet->setCellValue('A2', $subtitle); // Merge cells untuk title dan subtitle $sheet->mergeCells('A1:F1'); $sheet->mergeCells('A2:F2'); // Set row heights $sheet->getRowDimension(1)->setRowHeight(24); $sheet->getRowDimension(2)->setRowHeight(18); // Style title (Row 1) $sheet->getStyle('A1:G1')->applyFromArray([ 'font' => [ 'bold' => true, 'size' => 14, 'color' => ['rgb' => '1F2937'], ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], ]); // Style subtitle (Row 2) $sheet->getStyle('A2:G2')->applyFromArray([ 'font' => [ 'size' => 10, 'color' => ['rgb' => '6B7280'], ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], ]); // Add headers at row 3 $headers = ['No', 'Nama Siswa', 'NISN', 'Kelas', 'Status', 'Waktu']; foreach ($headers as $col => $header) { $sheet->setCellValue($this->getColumnLetter($col + 1) . '3', $header); } // Style headers (Row 3) $headerRange = 'A3:F3'; $sheet->getStyle($headerRange)->applyFromArray([ 'font' => [ 'bold' => true, 'color' => ['rgb' => 'FFFFFF'], ], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '365CF5'], ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true, ], ]); $sheet->getRowDimension(3)->setRowHeight(20); // Add data rows starting from row 4 $dataStartRow = 4; foreach ($rows as $index => $row) { $currentRow = $dataStartRow + $index; $sheet->setCellValue('A' . $currentRow, $row['no']); $sheet->setCellValue('B' . $currentRow, $row['student_name']); $sheet->setCellValue('C' . $currentRow, $row['nisn']); $sheet->setCellValue('D' . $currentRow, $row['class_name']); $sheet->setCellValue('E' . $currentRow, $row['status']); $sheet->setCellValue('F' . $currentRow, $row['waktu']); // Zebra rows (alternate background color) if (($index % 2) === 1) { $sheet->getStyle('A' . $currentRow . ':F' . $currentRow)->applyFromArray([ 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F4F7FF'], ], ]); } } // Get highest row $highestRow = $sheet->getHighestRow(); // Apply borders to all data $tableRange = 'A3:F' . $highestRow; $sheet->getStyle($tableRange)->applyFromArray([ 'borders' => [ 'allBorders' => [ 'borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'D1D5DB'], ], ], ]); // Center align specific columns $sheet->getStyle('A4:A' . $highestRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle('D4:D' . $highestRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle('E4:E' . $highestRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); // Set column widths $sheet->getColumnDimension('A')->setWidth(6); $sheet->getColumnDimension('B')->setWidth(28); $sheet->getColumnDimension('C')->setWidth(16); $sheet->getColumnDimension('D')->setWidth(14); $sheet->getColumnDimension('E')->setWidth(16); $sheet->getColumnDimension('F')->setWidth(20); // Freeze top 3 rows (title + subtitle + header) $sheet->freezePane('A4'); // Save and download $writer = new Xlsx($spreadsheet); $tempPath = storage_path('app/temp/' . $filename . '.xlsx'); @mkdir(dirname($tempPath), 0755, true); $writer->save($tempPath); return response()->download($tempPath)->deleteFileAfterSend(true); } /** * Export absensi harian ke Excel - Format Tahunan (12 Bulan) */ private function exportExcelYearly($classId, $tahun) { // Get kelas info $kelas = Classes::findOrFail($classId); $kelasName = $kelas->name ?? 'Kelas'; // Get all students $students = Student::where('id_class', $classId)->orderBy('name', 'ASC')->get(); if ($students->isEmpty()) { return response()->json(['error' => 'Tidak ada siswa di kelas'], 400); } // Generate filename $filename = 'Laporan_Tahunan_Absensi_Harian_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . '_' . $tahun . '.xlsx'; // Create spreadsheet $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Laporan Tahunan'); // Row 1: Title (Yellow background) $title = 'LAPORAN TAHUNAN ABSENSI HARIAN ' . strtoupper($kelasName); $sheet->setCellValue('A1', $title); $sheet->mergeCells('A1:Q1'); $sheet->getRowDimension(1)->setRowHeight(25); $sheet->getStyle('A1')->applyFromArray([ 'font' => ['bold' => true, 'size' => 14, 'color' => ['rgb' => '000000']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); // Row 2: Period info $periodText = 'TAHUN ' . $tahun; $sheet->setCellValue('A2', $periodText); $sheet->mergeCells('A2:Q2'); $sheet->getRowDimension(2)->setRowHeight(20); $sheet->getStyle('A2')->applyFromArray([ 'font' => ['bold' => true, 'size' => 12, 'color' => ['rgb' => '000000']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); // Row 3: Empty $sheet->getRowDimension(3)->setRowHeight(10); // Row 4: Headers $sheet->setCellValue('A4', 'Nama'); $sheet->setCellValue('B4', 'Jabatan/Kelas'); // Month Names $months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agt', 'Sep', 'Okt', 'Nov', 'Des']; foreach ($months as $idx => $monthName) { $colLetter = $this->getColumnLetter($idx + 3); // C = 3 $sheet->setCellValue($colLetter . '4', $monthName); } // Totals Headers $sheet->setCellValue('O4', "Total\nTepat Waktu"); $sheet->setCellValue('P4', "Total\nTerlambat"); $sheet->setCellValue('Q4', "Total\nIzin"); // Style headers (Row 4) - Yellow background $headerRange = 'A4:Q4'; $sheet->getStyle($headerRange)->applyFromArray([ 'font' => ['bold' => true, 'size' => 11, 'color' => ['rgb' => '000000']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); $sheet->getRowDimension(4)->setRowHeight(30); // Fetch attendance data for the whole year $attendanceData = []; $attendance = AttendanceHistoryDaily::where('id_class', $classId) ->whereYear('created_at', $tahun) ->get(); foreach ($attendance as $att) { $date = \Carbon\Carbon::parse($att->created_at); $month = $date->month; $studentId = $att->id_student; $status = $att->status; if (!isset($attendanceData[$studentId])) { $attendanceData[$studentId] = []; } if (!isset($attendanceData[$studentId][$month])) { $attendanceData[$studentId][$month] = [ 'tepat_waktu' => 0, 'terlambat' => 0, 'izin' => 0 ]; } if (in_array($status, ['tepat_waktu', 'terlambat', 'izin'])) { $attendanceData[$studentId][$month][$status]++; } } // Add student rows $dataRow = 5; foreach ($students as $student) { $sheet->setCellValue('A' . $dataRow, $student->name); $sheet->setCellValue('B' . $dataRow, $kelasName); $totalTepatWaktu = 0; $totalTerlambat = 0; $totalIzin = 0; // Populate months (C to N) for ($month = 1; $month <= 12; $month++) { $colLetter = $this->getColumnLetter($month + 2); // C = 3 $tw = 0; $tl = 0; $i = 0; if (isset($attendanceData[$student->id][$month])) { $tw = $attendanceData[$student->id][$month]['tepat_waktu'] ?? 0; $tl = $attendanceData[$student->id][$month]['terlambat'] ?? 0; $i = $attendanceData[$student->id][$month]['izin'] ?? 0; } $totalTepatWaktu += $tw; $totalTerlambat += $tl; $totalIzin += $i; if ($tw > 0 || $tl > 0 || $i > 0) { $displayText = "TW:{$tw}\nTL:{$tl}\nI:{$i}"; $sheet->setCellValue($colLetter . $dataRow, $displayText); $sheet->getStyle($colLetter . $dataRow)->getAlignment()->setWrapText(true); } else { $sheet->setCellValue($colLetter . $dataRow, '-'); } $sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); } // Populate totals $sheet->setCellValue('O' . $dataRow, $totalTepatWaktu); $sheet->setCellValue('P' . $dataRow, $totalTerlambat); $sheet->setCellValue('Q' . $dataRow, $totalIzin); for ($c = 15; $c <= 17; $c++) { $colLetter = $this->getColumnLetter($c); $sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); } // Border styling $dataRange = 'A' . $dataRow . ':Q' . $dataRow; $sheet->getStyle($dataRange)->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); // Set height for dynamic wrapped text $sheet->getRowDimension($dataRow)->setRowHeight(45); $dataRow++; } // Add legend $legendRow = $dataRow + 2; $sheet->setCellValue('A' . $legendRow, 'Keterangan format bulanan:'); $sheet->getStyle('A' . $legendRow)->applyFromArray(['font' => ['bold' => true, 'size' => 11]]); $legendRow++; $legends = [ 'TW' => 'Tepat Waktu', 'TL' => 'Terlambat', 'I' => 'Izin' ]; foreach ($legends as $code => $meaning) { $sheet->setCellValue('A' . $legendRow, $code); $sheet->setCellValue('B' . $legendRow, $meaning); $sheet->getStyle('A' . $legendRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $legendRow++; } // Set column widths $sheet->getColumnDimension('A')->setWidth(25); $sheet->getColumnDimension('B')->setWidth(15); // Month columns width for ($month = 1; $month <= 12; $month++) { $colLetter = $this->getColumnLetter($month + 2); $sheet->getColumnDimension($colLetter)->setWidth(10); } $sheet->getColumnDimension('O')->setWidth(15); $sheet->getColumnDimension('P')->setWidth(12); $sheet->getColumnDimension('Q')->setWidth(12); // Save and download $writer = new Xlsx($spreadsheet); $tempPath = storage_path('app/temp/' . $filename); @mkdir(dirname($tempPath), 0755, true); $writer->save($tempPath); return response()->download($tempPath)->deleteFileAfterSend(true); } /** * Convert column number to letter (1->A, 2->B, etc) */ private function getColumnLetter($col) { $letter = ''; while ($col > 0) { $col--; $letter = chr(65 + ($col % 26)) . $letter; $col = intdiv($col, 26); } return $letter; } /** * Helper method untuk prepare data export absensi harian */ private function getAbsensiHarianRowsForExport($classId, Request $request) { // Get kelas $kelas = Classes::findOrFail($classId); // Get semua siswa di kelas $students = Student::where('id_class', $classId)->get(); // Determine filter parameters $tanggal = $request->get('tanggal'); $bulan = $request->get('bulan'); $tahun = $request->get('tahun'); // Default ke hari ini hanya jika tidak ada filter apapun if (!$tanggal && !$bulan && !$tahun) { $tanggal = now()->format('Y-m-d'); } // Build query untuk fetch attendance records $query = AttendanceHistoryDaily::with(['student', 'class']) ->where('id_class', $classId); // Jika tanggal diset, filter by exact date if ($tanggal && !$bulan && !$tahun) { $query->whereDate('created_at', $tanggal); } // Jika bulan dan tahun keduanya diset, filter by month AND year else if ($bulan && $tahun) { $query->whereMonth('created_at', $bulan) ->whereYear('created_at', $tahun); } // Jika hanya bulan diset, filter by month else if ($bulan) { $query->whereMonth('created_at', $bulan); } // Jika hanya tahun diset, filter by year else if ($tahun) { $query->whereYear('created_at', $tahun); } $absensi = $query->get(); // Get unique dates dari filtered data $uniqueDates = $absensi ->map(function($item) { return $item->created_at->format('Y-m-d'); }) ->unique() ->values() ->sort() ->reverse() ->values(); // Build rows untuk export: untuk setiap hari, untuk setiap siswa $rows = []; $no = 1; foreach ($uniqueDates as $date) { foreach ($students as $student) { // Cari record untuk student + date ini $record = $absensi ->where('id_student', $student->id) ->filter(function($item) use ($date) { return $item->created_at->format('Y-m-d') === $date; }) ->first(); if ($record) { // Ada record di database $rows[] = [ 'no' => $no, 'student_name' => $record->student->name ?? '-', 'nisn' => $record->student->nisn ?? '-', 'class_name' => $record->class->name ?? '-', 'status' => ucfirst($record->status), 'waktu' => $record->created_at->format('d M Y H:i:s'), ]; } else { // Dummy record: siswa belum absen di hari ini $rows[] = [ 'no' => $no, 'student_name' => $student->name, 'nisn' => $student->nisn ?? '-', 'class_name' => $kelas->name, 'status' => 'In Progress', 'waktu' => '-', ]; } $no++; } } return $rows; } }