924 lines
36 KiB
PHP
924 lines
36 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\AttendanceHistory;
|
|
use App\Models\Classes;
|
|
use App\Models\Schedule;
|
|
use App\Models\Student;
|
|
use App\Models\Subject;
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
|
|
|
class AttendanceHistoryController extends Controller
|
|
{
|
|
public function absensiMapel($classId, Request $request)
|
|
{
|
|
$kelas = Classes::findOrFail($classId);
|
|
|
|
// Ambil semua siswa di kelas
|
|
$allStudents = Student::where('id_class', $classId)->get();
|
|
|
|
// Ambil daftar mapel dari jadwal kelas
|
|
$mapelList = Schedule::with('subject')
|
|
->where('id_class', $classId)
|
|
->distinct('id_subject')
|
|
->get()
|
|
->map(function($schedule) {
|
|
return $schedule->subject;
|
|
})
|
|
->unique('id')
|
|
->sortBy('name')
|
|
->values();
|
|
|
|
// Default ke hari ini jika tidak ada filter
|
|
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
|
|
$subjectId = $request->get('subject_id');
|
|
|
|
$absensi = collect();
|
|
$belumAbsen = $allStudents;
|
|
|
|
// Jika ada subject filter, ambil data berdasarkan tanggal + day_of_week
|
|
if ($subjectId) {
|
|
// Parse tanggal untuk ambil day of week
|
|
$date = \Carbon\Carbon::createFromFormat('Y-m-d', $tanggal);
|
|
|
|
// Map hari ke format Indonesia
|
|
$dayMap = [
|
|
0 => 'Minggu',
|
|
1 => 'Senin',
|
|
2 => 'Selasa',
|
|
3 => 'Rabu',
|
|
4 => 'Kamis',
|
|
5 => 'Jumat',
|
|
6 => 'Sabtu'
|
|
];
|
|
|
|
$dayName = $dayMap[$date->dayOfWeek] ?? '';
|
|
|
|
// Get schedule IDs untuk subject dan day ini
|
|
$scheduleIds = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $subjectId)
|
|
->where('day_of_week', $dayName)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Get attendance history untuk tanggal dan schedules spesifik
|
|
if (!empty($scheduleIds)) {
|
|
$absensi = AttendanceHistory::with(['student', 'class'])
|
|
->whereIn('id_schedule', $scheduleIds)
|
|
->where(function($q) use ($tanggal) {
|
|
$q->whereDate('created_at', $tanggal)
|
|
->orWhereDate('attendance_date', $tanggal);
|
|
})
|
|
->get();
|
|
}
|
|
|
|
// Ambil siswa yang sudah absen untuk tanggal + day ini
|
|
$sudahAbsenIds = $absensi->pluck('id_student')->unique();
|
|
|
|
// Siswa yang belum absen untuk tanggal + day ini
|
|
$belumAbsen = $allStudents->whereNotIn('id', $sudahAbsenIds)->values();
|
|
}
|
|
|
|
return view('pages.absensi.absensi_mapel', [
|
|
'absensi' => $absensi,
|
|
'kelas' => $kelas,
|
|
'belumAbsen' => $belumAbsen,
|
|
'mapelList' => $mapelList,
|
|
'allStudents' => $allStudents,
|
|
'selectedTanggal' => $tanggal,
|
|
]);
|
|
}
|
|
|
|
public function getAbsensiByMapel($classId, $subjectId, Request $request)
|
|
{
|
|
try {
|
|
// Get schedules untuk subject ini di kelas ini
|
|
$scheduleIds = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $subjectId)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Build query untuk attendance history
|
|
$query = AttendanceHistory::with(['student', 'class']);
|
|
|
|
if (!empty($scheduleIds)) {
|
|
$query->whereIn('id_schedule', $scheduleIds);
|
|
}
|
|
|
|
// Apply date filters jika ada
|
|
$bulan = $request->get('bulan');
|
|
$tahun = $request->get('tahun');
|
|
|
|
if ($bulan && $tahun) {
|
|
$query->whereMonth('created_at', $bulan)
|
|
->whereYear('created_at', $tahun);
|
|
} elseif ($bulan) {
|
|
$query->whereMonth('created_at', $bulan);
|
|
} elseif ($tahun) {
|
|
$query->whereYear('created_at', $tahun);
|
|
}
|
|
|
|
// Get attendance history
|
|
$absensi = $query->get();
|
|
|
|
// Get all students
|
|
$allStudents = Student::where('id_class', $classId)->get();
|
|
|
|
// Ambil id siswa yang sudah absen untuk mapel ini
|
|
$sudahAbsenIds = $absensi->pluck('id_student')->unique();
|
|
|
|
// Siswa yang belum absen untuk mapel ini
|
|
$belumAbsen = $allStudents->whereNotIn('id', $sudahAbsenIds)->values();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'absensi' => $absensi,
|
|
'belumAbsen' => $belumAbsen,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Error: ' . $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function getSchedules($classId, $subjectId)
|
|
{
|
|
try {
|
|
// Get all schedules untuk subject ini di kelas ini
|
|
$schedules = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $subjectId)
|
|
->get()
|
|
->map(function($schedule) {
|
|
return [
|
|
'id' => $schedule->id,
|
|
'day' => $schedule->day_of_week ?? 'N/A',
|
|
'time' => ($schedule->start_time ? \Carbon\Carbon::parse($schedule->start_time)->format('H:i') : '') . ' - ' .
|
|
($schedule->end_time ? \Carbon\Carbon::parse($schedule->end_time)->format('H:i') : '')
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'schedules' => $schedules,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Error: ' . $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function getAbsensiByMapelAndDate($classId, $subjectId, $tanggal)
|
|
{
|
|
try {
|
|
// Parse tanggal
|
|
$date = \Carbon\Carbon::createFromFormat('Y-m-d', $tanggal);
|
|
|
|
// Map hari ke format Indonesia
|
|
$dayMap = [
|
|
0 => 'Minggu',
|
|
1 => 'Senin',
|
|
2 => 'Selasa',
|
|
3 => 'Rabu',
|
|
4 => 'Kamis',
|
|
5 => 'Jumat',
|
|
6 => 'Sabtu'
|
|
];
|
|
|
|
$dayName = $dayMap[$date->dayOfWeek] ?? '';
|
|
|
|
// Get schedules untuk subject ini yang sesuai dengan hari ini
|
|
$scheduleIds = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $subjectId)
|
|
->where('day_of_week', $dayName)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Get attendance history untuk tanggal spesifik dan schedules tersebut
|
|
$absensi = collect();
|
|
if (!empty($scheduleIds)) {
|
|
$absensi = AttendanceHistory::with(['student', 'class'])
|
|
->whereIn('id_schedule', $scheduleIds)
|
|
->where(function($q) use ($tanggal) {
|
|
$q->whereDate('created_at', $tanggal)
|
|
->orWhereDate('attendance_date', $tanggal);
|
|
})
|
|
->get();
|
|
}
|
|
|
|
// Get all students
|
|
$allStudents = Student::where('id_class', $classId)->get();
|
|
|
|
// Siswa yang sudah absen untuk tanggal dan schedule ini
|
|
$sudahAbsenIds = $absensi->pluck('id_student')->unique();
|
|
|
|
// Siswa yang belum absen
|
|
$belumAbsen = $allStudents->whereNotIn('id', $sudahAbsenIds)->values();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'absensi' => $absensi,
|
|
'belumAbsen' => $belumAbsen,
|
|
'scheduleIds' => $scheduleIds,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Error: ' . $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function editStatus(Request $request, $id)
|
|
{
|
|
try {
|
|
$request->validate([
|
|
'status' => 'required|string'
|
|
]);
|
|
|
|
// Jika $id adalah 'null', create baru
|
|
if ($id === 'null') {
|
|
$request->validate([
|
|
'id_student' => 'required|integer',
|
|
'id_class' => 'required|integer',
|
|
'id_schedule' => 'required|integer',
|
|
]);
|
|
|
|
// Create attendance record dengan id_schedule dari request
|
|
$currentDate = now()->format('Y-m-d');
|
|
AttendanceHistory::create([
|
|
'id_student' => $request->id_student,
|
|
'id_class' => $request->id_class,
|
|
'status' => $request->status,
|
|
'period_number' => 1,
|
|
'id_schedule' => $request->id_schedule,
|
|
'coordinate' => config('coordinate.coordinate', '-7.604032330848524, 112.10176449791652'),
|
|
'attendance_date' => $currentDate,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return response()->json(['success' => true, 'message' => 'Data absensi berhasil ditambahkan.']);
|
|
} else {
|
|
// Update existing
|
|
$absensi = AttendanceHistory::findOrFail($id);
|
|
$currentDate = now()->format('Y-m-d');
|
|
$absensi->update([
|
|
'status' => $request->status,
|
|
'attendance_date' => $currentDate,
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return response()->json(['success' => true, 'message' => 'Status absensi berhasil diupdate.']);
|
|
}
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export absensi mapel ke Excel - Format Grid per Periode (seperti standar absensi)
|
|
*/
|
|
public function exportExcel($classId, Request $request)
|
|
{
|
|
try {
|
|
// Get kelas info
|
|
$kelas = Classes::findOrFail($classId);
|
|
$kelasName = $kelas->name ?? 'Kelas';
|
|
|
|
// Get filter info
|
|
$mapelId = $request->get('mapel');
|
|
$bulan = $request->get('bulan');
|
|
$tahun = $request->get('tahun');
|
|
|
|
// Default ke bulan & tahun sekarang jika kedua-duanya kosong
|
|
if (empty($bulan) && empty($tahun)) {
|
|
$bulan = date('m');
|
|
$tahun = date('Y');
|
|
} elseif ($bulan && empty($tahun)) {
|
|
$tahun = date('Y');
|
|
}
|
|
|
|
// Jika bulan kosong tapi tahun ada, lakukan export tahunan
|
|
if (empty($bulan) && $tahun) {
|
|
return $this->exportExcelYearly($classId, $mapelId, $tahun);
|
|
}
|
|
|
|
if (empty($bulan)) {
|
|
$bulan = date('m');
|
|
}
|
|
|
|
// Get mapel/subject name
|
|
$mapelName = 'Mapel';
|
|
if ($mapelId) {
|
|
$subject = Subject::find($mapelId);
|
|
$mapelName = $subject ? $subject->name : 'Mapel';
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Get schedules untuk mapel ini
|
|
$scheduleIds = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $mapelId)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Generate filename
|
|
$month_name = $this->getMonthName($bulan);
|
|
$filename = 'Absensi_Mapel_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $mapelName) . '_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . '.xlsx';
|
|
|
|
// Create spreadsheet
|
|
$spreadsheet = new Spreadsheet();
|
|
$sheet = $spreadsheet->getActiveSheet();
|
|
$sheet->setTitle('Absensi Mapel');
|
|
|
|
// Row 1: Title (Yellow background)
|
|
$title = 'ABSENSI ' . strtoupper($mapelName) . ' ' . strtoupper($kelasName);
|
|
$sheet->setCellValue('A1', $title);
|
|
$sheet->mergeCells('A1:AE1');
|
|
$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 = 'PERIODE ' . strtoupper($month_name) . ' ' . $tahun;
|
|
$sheet->setCellValue('A2', $periodText);
|
|
$sheet->mergeCells('A2:AE2');
|
|
$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');
|
|
|
|
// Days columns (1-31)
|
|
$daysInMonth = \Carbon\Carbon::createFromDate($tahun, $bulan, 1)->daysInMonth;
|
|
for ($day = 1; $day <= 31; $day++) {
|
|
$colLetter = $this->getColumnLetter($day + 2); // +2 untuk offset dari kolom A (Nama) dan B (Jabatan)
|
|
if ($day <= $daysInMonth) {
|
|
$sheet->setCellValue($colLetter . '4', $day);
|
|
} else {
|
|
$sheet->setCellValue($colLetter . '4', '');
|
|
}
|
|
}
|
|
|
|
// Summary columns
|
|
$jumlahCol = $this->getColumnLetter(35); // Column after day 31
|
|
$keteranganCol = $this->getColumnLetter(36);
|
|
$sheet->setCellValue($jumlahCol . '4', 'Jumlah\nHari Kerja');
|
|
$sheet->setCellValue($keteranganCol . '4', 'Keterangan');
|
|
|
|
// Style headers (Row 4) - Yellow background
|
|
$headerRange = 'A4:' . $keteranganCol . '4';
|
|
$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);
|
|
|
|
// Get attendance data untuk bulan+tahun ini
|
|
$attendanceData = [];
|
|
if (!empty($scheduleIds)) {
|
|
$attendance = AttendanceHistory::where(function($q) use ($bulan, $tahun) {
|
|
$q->where(function($qq) use ($bulan, $tahun) {
|
|
$qq->whereMonth('created_at', $bulan)->whereYear('created_at', $tahun);
|
|
})->orWhere(function($qq) use ($bulan, $tahun) {
|
|
$qq->whereMonth('attendance_date', $bulan)->whereYear('attendance_date', $tahun);
|
|
});
|
|
})->whereIn('id_schedule', $scheduleIds)->get();
|
|
|
|
foreach ($attendance as $att) {
|
|
$date = $att->attendance_date ? \Carbon\Carbon::parse($att->attendance_date) : \Carbon\Carbon::parse($att->created_at);
|
|
$key = $att->id_student . '_' . $date->day;
|
|
$attendanceData[$key] = $att->status;
|
|
}
|
|
}
|
|
|
|
// Add student rows
|
|
$dataRow = 5;
|
|
foreach ($students as $student) {
|
|
$sheet->setCellValue('A' . $dataRow, $student->name);
|
|
$sheet->setCellValue('B' . $dataRow, $student->classroom ? $student->classroom->name : '-');
|
|
|
|
// Add attendance status for each day
|
|
for ($day = 1; $day <= 31; $day++) {
|
|
$colLetter = $this->getColumnLetter($day + 2);
|
|
$key = $student->id . '_' . $day;
|
|
$status = isset($attendanceData[$key]) ? $attendanceData[$key] : '';
|
|
|
|
// Convert status to letter code
|
|
$statusCode = '';
|
|
if ($status) {
|
|
switch($status) {
|
|
case 'hadir':
|
|
$statusCode = 'H';
|
|
break;
|
|
case 'izin':
|
|
$statusCode = 'I';
|
|
break;
|
|
case 'sakit':
|
|
$statusCode = 'S';
|
|
break;
|
|
case 'alpha':
|
|
$statusCode = 'A';
|
|
break;
|
|
default:
|
|
$statusCode = '';
|
|
}
|
|
}
|
|
|
|
$sheet->setCellValue($colLetter . $dataRow, $statusCode);
|
|
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
}
|
|
|
|
// Borders untuk semua kolom
|
|
$dataRange = 'A' . $dataRow . ':' . $keteranganCol . $dataRow;
|
|
$sheet->getStyle($dataRange)->applyFromArray([
|
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
|
]);
|
|
|
|
$dataRow++;
|
|
}
|
|
|
|
// Add legend
|
|
$legendRow = $dataRow + 2;
|
|
$sheet->setCellValue('A' . $legendRow, 'Keterangan:');
|
|
$sheet->getStyle('A' . $legendRow)->applyFromArray(['font' => ['bold' => true, 'size' => 11]]);
|
|
|
|
$legendRow++;
|
|
$legends = [
|
|
'H' => 'Hadir',
|
|
'I' => 'Izin',
|
|
'S' => 'Sakit',
|
|
'A' => 'Alpha'
|
|
];
|
|
|
|
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(20);
|
|
$sheet->getColumnDimension('B')->setWidth(15);
|
|
|
|
// Day columns
|
|
for ($day = 1; $day <= 31; $day++) {
|
|
$colLetter = $this->getColumnLetter($day + 2);
|
|
$sheet->getColumnDimension($colLetter)->setWidth(5);
|
|
}
|
|
|
|
$sheet->getColumnDimension($jumlahCol)->setWidth(12);
|
|
$sheet->getColumnDimension($keteranganCol)->setWidth(15);
|
|
|
|
// 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);
|
|
} catch (\Exception $e) {
|
|
return response()->json(['error' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: Get absensi mapel rows for export sesuai dengan filter
|
|
* Export semua siswa di kelas dengan status absensi mereka (jika ada)
|
|
*/
|
|
private function getAbsensiMapelRowsForExport($classId, $mapelId, $tanggal = null, $bulan = null, $tahun = null)
|
|
{
|
|
// Get kelas
|
|
$kelas = Classes::findOrFail($classId);
|
|
|
|
// Get semua siswa di kelas (PENTING: ambil SEMUA siswa)
|
|
$students = Student::where('id_class', $classId)
|
|
->orderBy('name', 'ASC')
|
|
->get();
|
|
|
|
if ($students->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
// Get schedules untuk mapel ini
|
|
$scheduleIds = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $mapelId)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Build query untuk attendance history - ambil SEMUA attendance untuk mapel ini
|
|
$query = AttendanceHistory::with(['student', 'class']);
|
|
|
|
// Filter by schedule if available
|
|
if (!empty($scheduleIds)) {
|
|
$query->whereIn('id_schedule', $scheduleIds);
|
|
} else {
|
|
// Jika tidak ada schedule, kembalikan semua siswa dengan keterangan "-"
|
|
return $this->buildEmptyAttendanceRows($students);
|
|
}
|
|
|
|
// Apply date filters - PENTING: include semua attendance data sesuai filter
|
|
if ($tanggal) {
|
|
// Untuk tanggal spesifik
|
|
$query->where(function($q) use ($tanggal) {
|
|
$q->whereDate('created_at', $tanggal)
|
|
->orWhereDate('attendance_date', $tanggal);
|
|
});
|
|
} elseif ($bulan && $tahun) {
|
|
// Untuk bulan+tahun spesifik
|
|
$query->where(function($q) use ($bulan, $tahun) {
|
|
$q->where(function($qq) use ($bulan, $tahun) {
|
|
$qq->whereMonth('created_at', $bulan)
|
|
->whereYear('created_at', $tahun);
|
|
})->orWhere(function($qq) use ($bulan, $tahun) {
|
|
$qq->whereMonth('attendance_date', $bulan)
|
|
->whereYear('attendance_date', $tahun);
|
|
});
|
|
});
|
|
} elseif ($bulan) {
|
|
// Untuk bulan spesifik
|
|
$query->where(function($q) use ($bulan) {
|
|
$q->whereMonth('created_at', $bulan)
|
|
->orWhereMonth('attendance_date', $bulan);
|
|
});
|
|
} elseif ($tahun) {
|
|
// Untuk tahun spesifik
|
|
$query->where(function($q) use ($tahun) {
|
|
$q->whereYear('created_at', $tahun)
|
|
->orWhereYear('attendance_date', $tahun);
|
|
});
|
|
}
|
|
|
|
// Get attendance records
|
|
$absensi = $query->get();
|
|
|
|
// Build rows untuk export - PENTING: include SEMUA students
|
|
$rows = [];
|
|
$no = 1;
|
|
|
|
foreach ($students as $student) {
|
|
// Cari attendance record untuk student ini
|
|
// Jika ada multiple records, ambil yang paling recent
|
|
$absensiItem = $absensi->where('id_student', $student->id)->sortByDesc('created_at')->first();
|
|
|
|
$keterangan = '-';
|
|
$attendanceDate = '-';
|
|
$jamPertemuan = '-';
|
|
|
|
if ($absensiItem) {
|
|
// Tentukan keterangan berdasarkan status
|
|
switch($absensiItem->status) {
|
|
case 'hadir':
|
|
$keterangan = 'Hadir';
|
|
break;
|
|
case 'izin':
|
|
$keterangan = 'Izin';
|
|
break;
|
|
case 'sakit':
|
|
$keterangan = 'Sakit';
|
|
break;
|
|
case 'alpha':
|
|
$keterangan = 'Alpha';
|
|
break;
|
|
case 'dispen':
|
|
$keterangan = 'Dispen';
|
|
break;
|
|
default:
|
|
$keterangan = ucfirst($absensiItem->status);
|
|
}
|
|
|
|
// Format tanggal dan jam dari attendance record
|
|
$attendanceDate = $absensiItem->attendance_date
|
|
? \Carbon\Carbon::parse($absensiItem->attendance_date)->format('d/m/Y')
|
|
: ($absensiItem->created_at ? $absensiItem->created_at->format('d/m/Y') : '-');
|
|
|
|
$jamPertemuan = $absensiItem->created_at ? $absensiItem->created_at->format('H:i') : '-';
|
|
}
|
|
|
|
// Add row untuk student ini (terlepas ada attendance atau tidak)
|
|
$rows[] = [
|
|
'no' => $no,
|
|
'student_name' => $student->name,
|
|
'nisn' => $student->nisn ?? '-',
|
|
'attendance_date' => $attendanceDate,
|
|
'jam_pertemuan' => $jamPertemuan,
|
|
'keterangan' => $keterangan,
|
|
];
|
|
|
|
$no++;
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Helper: Build empty attendance rows jika tidak ada schedule
|
|
*/
|
|
private function buildEmptyAttendanceRows($students)
|
|
{
|
|
$rows = [];
|
|
$no = 1;
|
|
|
|
foreach ($students as $student) {
|
|
$rows[] = [
|
|
'no' => $no,
|
|
'student_name' => $student->name,
|
|
'nisn' => $student->nisn ?? '-',
|
|
'attendance_date' => '-',
|
|
'jam_pertemuan' => '-',
|
|
'keterangan' => '-',
|
|
];
|
|
$no++;
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Helper: Convert column number to letter
|
|
*/
|
|
private function getColumnLetter($col)
|
|
{
|
|
$letter = '';
|
|
while ($col > 0) {
|
|
$col--;
|
|
$letter = chr(65 + ($col % 26)) . $letter;
|
|
$col = intdiv($col, 26);
|
|
}
|
|
return $letter;
|
|
}
|
|
|
|
/**
|
|
* Helper: Get month name in Indonesian
|
|
*/
|
|
private function getMonthName($bulan)
|
|
{
|
|
$months = [
|
|
'01' => 'Januari', '02' => 'Februari', '03' => 'Maret',
|
|
'04' => 'April', '05' => 'Mei', '06' => 'Juni',
|
|
'07' => 'Juli', '08' => 'Agustus', '09' => 'September',
|
|
'10' => 'Oktober', '11' => 'November', '12' => 'Desember'
|
|
];
|
|
return $months[$bulan] ?? 'Bulan';
|
|
}
|
|
|
|
/**
|
|
* Export absensi mapel ke Excel - Format Tahunan (12 Bulan)
|
|
*/
|
|
private function exportExcelYearly($classId, $mapelId, $tahun)
|
|
{
|
|
// Get kelas info
|
|
$kelas = Classes::findOrFail($classId);
|
|
$kelasName = $kelas->name ?? 'Kelas';
|
|
|
|
// Get mapel/subject name
|
|
$mapelName = 'Mapel';
|
|
if ($mapelId) {
|
|
$subject = Subject::find($mapelId);
|
|
$mapelName = $subject ? $subject->name : 'Mapel';
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Get schedules untuk mapel ini
|
|
$scheduleIds = Schedule::where('id_class', $classId)
|
|
->where('id_subject', $mapelId)
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
// Generate filename
|
|
$filename = 'Laporan_Tahunan_Absensi_Mapel_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $mapelName) . '_' . 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 ' . strtoupper($mapelName) . ' ' . strtoupper($kelasName);
|
|
$sheet->setCellValue('A1', $title);
|
|
$sheet->mergeCells('A1:R1');
|
|
$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:R2');
|
|
$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\nHadir");
|
|
$sheet->setCellValue('P4', "Total\nIzin");
|
|
$sheet->setCellValue('Q4', "Total\nSakit");
|
|
$sheet->setCellValue('R4', "Total\nAlpha");
|
|
|
|
// Style headers (Row 4) - Yellow background
|
|
$headerRange = 'A4:R4';
|
|
$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 = [];
|
|
if (!empty($scheduleIds)) {
|
|
$attendance = AttendanceHistory::whereIn('id_schedule', $scheduleIds)
|
|
->where(function($q) use ($tahun) {
|
|
$q->whereYear('created_at', $tahun)
|
|
->orWhereYear('attendance_date', $tahun);
|
|
})->get();
|
|
|
|
foreach ($attendance as $att) {
|
|
$date = $att->attendance_date ? \Carbon\Carbon::parse($att->attendance_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] = [
|
|
'hadir' => 0,
|
|
'izin' => 0,
|
|
'sakit' => 0,
|
|
'alpha' => 0,
|
|
'dispen' => 0
|
|
];
|
|
}
|
|
if (in_array($status, ['hadir', 'izin', 'sakit', 'alpha', 'dispen'])) {
|
|
$attendanceData[$studentId][$month][$status]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add student rows
|
|
$dataRow = 5;
|
|
foreach ($students as $student) {
|
|
$sheet->setCellValue('A' . $dataRow, $student->name);
|
|
$sheet->setCellValue('B' . $dataRow, $kelasName);
|
|
|
|
$totalHadir = 0;
|
|
$totalIzin = 0;
|
|
$totalSakit = 0;
|
|
$totalAlpha = 0;
|
|
|
|
// Populate months (C to N)
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
$colLetter = $this->getColumnLetter($month + 2); // C = 3
|
|
|
|
$h = 0; $i = 0; $s = 0; $a = 0;
|
|
if (isset($attendanceData[$student->id][$month])) {
|
|
$h = $attendanceData[$student->id][$month]['hadir'] ?? 0;
|
|
$i = $attendanceData[$student->id][$month]['izin'] ?? 0;
|
|
$s = $attendanceData[$student->id][$month]['sakit'] ?? 0;
|
|
$a = (($attendanceData[$student->id][$month]['alpha'] ?? 0) + ($attendanceData[$student->id][$month]['dispen'] ?? 0));
|
|
}
|
|
|
|
$totalHadir += $h;
|
|
$totalIzin += $i;
|
|
$totalSakit += $s;
|
|
$totalAlpha += $a;
|
|
|
|
if ($h > 0 || $i > 0 || $s > 0 || $a > 0) {
|
|
$displayText = "H:{$h}\nI:{$i}\nS:{$s}\nA:{$a}";
|
|
$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, $totalHadir);
|
|
$sheet->setCellValue('P' . $dataRow, $totalIzin);
|
|
$sheet->setCellValue('Q' . $dataRow, $totalSakit);
|
|
$sheet->setCellValue('R' . $dataRow, $totalAlpha);
|
|
|
|
for ($c = 15; $c <= 18; $c++) {
|
|
$colLetter = $this->getColumnLetter($c);
|
|
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
}
|
|
|
|
// Border styling
|
|
$dataRange = 'A' . $dataRow . ':R' . $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(55);
|
|
|
|
$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 = [
|
|
'H' => 'Hadir',
|
|
'I' => 'Izin',
|
|
'S' => 'Sakit',
|
|
'A' => 'Alpha / Dispen'
|
|
];
|
|
|
|
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(12);
|
|
$sheet->getColumnDimension('P')->setWidth(12);
|
|
$sheet->getColumnDimension('Q')->setWidth(12);
|
|
$sheet->getColumnDimension('R')->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);
|
|
}
|
|
}
|