feat: rekap presensi harian, mingguan, bulanan + export excel
- Add 3 jenis rekap presensi (harian/mingguan/bulanan) per kelas - Weekly recap: tabel H,S,I,A per hari + kolom TOTAL - Alpha proporsional: A = jam_alpha / total_jam - Pemisah border antar hari dan kolom TOTAL - Export Excel rekap mingguan (.xlsx) - Fix max_input_vars PHP limit (1000→5000) - Fix Carbon date object issue di groupBy
This commit is contained in:
@@ -217,43 +217,197 @@ class AttendanceController extends Controller
|
|||||||
|
|
||||||
public function recap(Request $request): View
|
public function recap(Request $request): View
|
||||||
{
|
{
|
||||||
$month = $request->input('month', now()->month);
|
$type = $request->input('type', 'daily');
|
||||||
$year = $request->input('year', now()->year);
|
|
||||||
$className = $request->input('class_name');
|
$className = $request->input('class_name');
|
||||||
|
|
||||||
$query = Student::where('is_active', true);
|
|
||||||
if ($className) {
|
|
||||||
$query->where('class_name', $className);
|
|
||||||
}
|
|
||||||
$students = $query->orderBy('class_name')->orderBy('full_name')->get();
|
|
||||||
|
|
||||||
$recap = [];
|
|
||||||
foreach ($students as $student) {
|
|
||||||
$attendances = Attendance::where('student_id', $student->id)
|
|
||||||
->whereMonth('date', $month)
|
|
||||||
->whereYear('date', $year)
|
|
||||||
->get()
|
|
||||||
->groupBy('status')
|
|
||||||
->map(fn($g) => $g->count());
|
|
||||||
|
|
||||||
$recap[] = (object) [
|
|
||||||
'student' => $student,
|
|
||||||
'hadir' => $attendances->get('hadir', 0),
|
|
||||||
'sakit' => $attendances->get('sakit', 0),
|
|
||||||
'izin' => $attendances->get('izin', 0),
|
|
||||||
'alpha' => $attendances->get('alpha', 0),
|
|
||||||
'terlambat' => $attendances->get('terlambat', 0),
|
|
||||||
'total' => $attendances->sum(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$classNames = Student::where('is_active', true)
|
$classNames = Student::where('is_active', true)
|
||||||
->whereNotNull('class_name')
|
->whereNotNull('class_name')
|
||||||
->distinct()
|
->distinct()
|
||||||
->orderBy('class_name')
|
->orderBy('class_name')
|
||||||
->pluck('class_name');
|
->pluck('class_name');
|
||||||
|
|
||||||
return view('attendances.recap', compact('recap', 'month', 'year', 'className', 'classNames'));
|
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: 1 jika ada minimal 1 record status tsb
|
||||||
|
$hadir = $studentAtt->where('status', 'hadir')->count() > 0 ? 1 : 0;
|
||||||
|
// Alpha proporsional: jumlah jam alpha / total jam
|
||||||
|
$total = $studentAtt->count();
|
||||||
|
$alphaCount = $studentAtt->where('status', 'alpha')->count();
|
||||||
|
$alpha = $total > 0 ? round($alphaCount / $total, 1) : 0;
|
||||||
|
$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;
|
||||||
|
|
||||||
|
$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();
|
||||||
|
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']
|
||||||
|
)->groupBy('student_id');
|
||||||
|
|
||||||
|
$row = ['label' => $week['label'], 'start' => $week['start'], 'end' => $week['end'], 'students' => []];
|
||||||
|
foreach ($students as $student) {
|
||||||
|
$studentAtt = $weekAtt->get($student->id, collect());
|
||||||
|
|
||||||
|
$hadir = $studentAtt->where('status', 'hadir')->count();
|
||||||
|
$alpha = $studentAtt->where('status', 'alpha')->count();
|
||||||
|
$sakit = $studentAtt->where('status', 'sakit')->count();
|
||||||
|
$izin = $studentAtt->where('status', 'izin')->count();
|
||||||
|
$terlambat = $studentAtt->where('status', 'terlambat')->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('attendances.recap', compact(
|
||||||
|
'type', 'month', 'year', 'className', 'classNames',
|
||||||
|
'students', 'recap', 'weeks'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function calendarData(Request $request): JsonResponse
|
public function calendarData(Request $request): JsonResponse
|
||||||
@@ -289,4 +443,127 @@ class AttendanceController extends Controller
|
|||||||
return response()->json($result);
|
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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getColLetter(int $col): string
|
||||||
|
{
|
||||||
|
$letter = '';
|
||||||
|
while ($col > 0) {
|
||||||
|
$col--;
|
||||||
|
$letter = chr(65 + $col % 26) . $letter;
|
||||||
|
$col = intval($col / 26);
|
||||||
|
}
|
||||||
|
return $letter;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,14 @@
|
|||||||
<nav class="flex items-center gap-1.5 text-sm text-gray-400 mb-1">
|
<nav class="flex items-center gap-1.5 text-sm text-gray-400 mb-1">
|
||||||
<a href="{{ route('attendances.index') }}" class="hover:text-gray-600 transition">Presensi</a>
|
<a href="{{ route('attendances.index') }}" class="hover:text-gray-600 transition">Presensi</a>
|
||||||
<span class="text-gray-300">/</span>
|
<span class="text-gray-300">/</span>
|
||||||
<span class="text-gray-700 font-medium">Rekap Bulanan</span>
|
<span class="text-gray-700 font-medium">Rekap</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Rekap Presensi Bulanan</h1>
|
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Rekap Presensi</h1>
|
||||||
<p class="text-sm text-gray-500 mt-1">Rekapitulasi kehadiran siswa per jam pelajaran</p>
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
@if($type == 'daily') Rekap per jam pelajaran
|
||||||
|
@elseif($type == 'weekly') Rekap per hari (Senin - Sabtu)
|
||||||
|
@else Rekap per minggu @endif
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ route('attendances.create') }}"
|
<a href="{{ route('attendances.create') }}"
|
||||||
class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
||||||
@@ -22,11 +26,28 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Tabs --}}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
||||||
|
<div class="flex border-b border-gray-200">
|
||||||
|
<a href="{{ route('attendances.recap', ['type' => 'daily', 'class_name' => $className, 'date' => request('date', now()->toDateString())]) }}"
|
||||||
|
class="px-5 py-3 text-sm font-semibold border-b-2 transition {{ $type == 'daily' ? 'text-blue-600 border-blue-600' : 'text-gray-500 border-transparent hover:text-gray-700' }}">
|
||||||
|
<i class="fa-solid fa-clock mr-1.5"></i> Harian
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('attendances.recap', ['type' => 'weekly', 'class_name' => $className, 'week_start' => request('week_start', now()->startOfWeek()->toDateString())]) }}"
|
||||||
|
class="px-5 py-3 text-sm font-semibold border-b-2 transition {{ $type == 'weekly' ? 'text-blue-600 border-blue-600' : 'text-gray-500 border-transparent hover:text-gray-700' }}">
|
||||||
|
<i class="fa-solid fa-calendar-week mr-1.5"></i> Mingguan
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('attendances.recap', ['type' => 'monthly', 'class_name' => $className, 'month' => request('month', now()->month), 'year' => request('year', now()->year)]) }}"
|
||||||
|
class="px-5 py-3 text-sm font-semibold border-b-2 transition {{ $type == 'monthly' ? 'text-blue-600 border-blue-600' : 'text-gray-500 border-transparent hover:text-gray-700' }}">
|
||||||
|
<i class="fa-solid fa-calendar-alt mr-1.5"></i> Bulanan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Filter --}}
|
{{-- Filter --}}
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 mb-6">
|
|
||||||
<form method="GET">
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
<form method="GET">
|
||||||
|
<input type="hidden" name="type" value="{{ $type }}">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-4 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Kelas</label>
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Kelas</label>
|
||||||
<select name="class_name"
|
<select name="class_name"
|
||||||
@@ -37,96 +58,333 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if($type == 'daily')
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Tanggal</label>
|
||||||
|
<input type="date" name="date" value="{{ $date ?? now()->toDateString() }}"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
@elseif($type == 'weekly')
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Awal Minggu</label>
|
||||||
|
<input type="date" name="week_start" value="{{ $start ? $start->toDateString() : now()->startOfWeek()->toDateString() }}"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Bulan</label>
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Bulan</label>
|
||||||
<select name="month"
|
<select name="month"
|
||||||
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
@foreach(range(1, 12) as $m)
|
@foreach(range(1, 12) as $m)
|
||||||
<option value="{{ $m }}" @selected($month == $m)>{{ \Carbon\Carbon::create()->month($m)->format('F') }}</option>
|
<option value="{{ $m }}" @selected(($month ?? now()->month) == $m)>{{ \Carbon\Carbon::create()->month($m)->format('F') }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Tahun</label>
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Tahun</label>
|
||||||
<div class="flex gap-2">
|
|
||||||
<select name="year"
|
<select name="year"
|
||||||
class="flex-1 px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
@foreach(range(now()->year, now()->year - 2, -1) as $y)
|
@foreach(range(now()->year, now()->year - 2, -1) as $y)
|
||||||
<option value="{{ $y }}" @selected($year == $y)>{{ $y }}</option>
|
<option value="{{ $y }}" @selected(($year ?? now()->year) == $y)>{{ $y }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex items-end">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
class="w-full px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
||||||
<i class="fa-solid fa-filter"></i>
|
<i class="fa-solid fa-filter mr-1"></i> Tampilkan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Table --}}
|
{{-- ===== REKAP HARIAN ===== --}}
|
||||||
|
@if($type == 'daily')
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div class="px-6 py-4 border-b border-gray-100">
|
<div class="px-6 py-4 border-b border-gray-100">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-violet-600 flex items-center justify-center shadow-sm">
|
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center shadow-sm">
|
||||||
<i class="fa-solid fa-chart-simple text-white text-sm"></i>
|
<i class="fa-solid fa-clock text-white text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-gray-900">
|
<h3 class="text-sm font-semibold text-gray-900">Rekap Harian — {{ \Carbon\Carbon::parse($date ?? now())->translatedFormat('l, d F Y') }}</h3>
|
||||||
Rekap {{ \Carbon\Carbon::create()->month($month)->format('F') }} {{ $year }}
|
<p class="text-xs text-gray-400">{{ $students->count() }} siswa @if($className) • Kelas {{ $className }} @endif</p>
|
||||||
</h3>
|
|
||||||
<p class="text-xs text-gray-400">{{ count($recap) }} siswa</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(count($recap) > 0)
|
@if($students->isNotEmpty())
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-100">
|
<table class="min-w-full divide-y divide-gray-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-50/80">
|
<tr class="bg-gray-50/80">
|
||||||
<th class="px-5 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">Siswa</th>
|
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider sticky left-0 bg-gray-50/80 z-10">Siswa</th>
|
||||||
<th class="px-3 py-3 text-center text-xs font-semibold text-emerald-600 uppercase tracking-wider">✅ Hadir</th>
|
@foreach($lessonHours as $hour)
|
||||||
<th class="px-3 py-3 text-center text-xs font-semibold text-purple-600 uppercase tracking-wider">🟣 Sakit</th>
|
<th class="px-2 py-3 text-center text-xs font-semibold text-gray-400 uppercase tracking-wider">Jam {{ $hour }}</th>
|
||||||
<th class="px-3 py-3 text-center text-xs font-semibold text-blue-600 uppercase tracking-wider">🔵 Izin</th>
|
@endforeach
|
||||||
<th class="px-3 py-3 text-center text-xs font-semibold text-red-600 uppercase tracking-wider">❌ Alpha</th>
|
|
||||||
<th class="px-3 py-3 text-center text-xs font-semibold text-yellow-600 uppercase tracking-wider">🟡 Terlambat</th>
|
|
||||||
<th class="px-3 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Total</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-50">
|
<tbody class="divide-y divide-gray-50">
|
||||||
@foreach($recap as $r)
|
@foreach($students as $student)
|
||||||
<tr class="hover:bg-gray-50/50 transition">
|
<tr class="hover:bg-gray-50/50 transition">
|
||||||
<td class="px-5 py-3 whitespace-nowrap">
|
<td class="px-4 py-2.5 whitespace-nowrap sticky left-0 bg-white z-10">
|
||||||
<p class="text-sm font-semibold text-gray-900">{{ $r->student->full_name }}</p>
|
<p class="text-sm font-semibold text-gray-900">{{ $student->full_name }}</p>
|
||||||
<p class="text-xs text-gray-400">{{ $r->student->class_name }}</p>
|
<p class="text-xs text-gray-400">{{ $student->class_name }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-3 text-center text-sm font-bold text-emerald-600">{{ $r->hadir }}</td>
|
@foreach($lessonHours as $hour)
|
||||||
<td class="px-3 py-3 text-center text-sm font-bold text-purple-600">{{ $r->sakit }}</td>
|
@php
|
||||||
<td class="px-3 py-3 text-center text-sm font-bold text-blue-600">{{ $r->izin }}</td>
|
$key = $student->id . '-' . $hour;
|
||||||
<td class="px-3 py-3 text-center text-sm font-bold {{ $r->alpha > 0 ? 'text-red-600' : 'text-gray-400' }}">{{ $r->alpha }}</td>
|
$att = $attendances->get($key);
|
||||||
<td class="px-3 py-3 text-center text-sm font-bold {{ $r->terlambat > 0 ? 'text-yellow-600' : 'text-gray-400' }}">{{ $r->terlambat }}</td>
|
$status = $att ? $att->status : '-';
|
||||||
<td class="px-3 py-3 text-center text-sm font-bold text-gray-700">{{ $r->total }}</td>
|
$color = match($status) {
|
||||||
|
'hadir' => 'text-emerald-600 bg-emerald-50',
|
||||||
|
'alpha' => 'text-red-600 bg-red-50',
|
||||||
|
'sakit' => 'text-purple-600 bg-purple-50',
|
||||||
|
'izin' => 'text-blue-600 bg-blue-50',
|
||||||
|
'terlambat' => 'text-yellow-600 bg-yellow-50',
|
||||||
|
default => 'text-gray-300',
|
||||||
|
};
|
||||||
|
$icon = match($status) {
|
||||||
|
'hadir' => 'H',
|
||||||
|
'alpha' => 'A',
|
||||||
|
'sakit' => 'S',
|
||||||
|
'izin' => 'I',
|
||||||
|
'terlambat' => 'T',
|
||||||
|
default => '-',
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
<td class="px-2 py-2.5 text-center">
|
||||||
|
<span class="inline-flex items-center justify-center w-7 h-7 rounded-lg text-xs font-bold {{ $color }}">
|
||||||
|
{{ $icon }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center">
|
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
||||||
Menampilkan rekap {{ \Carbon\Carbon::create()->month($month)->format('F') }} {{ $year }}
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-emerald-50 text-emerald-600 text-center text-xs font-bold">H</span> Hadir</span>
|
||||||
@if($className) • Kelas {{ $className }} @endif
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-red-50 text-red-600 text-center text-xs font-bold">A</span> Alpha</span>
|
||||||
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-purple-50 text-purple-600 text-center text-xs font-bold">S</span> Sakit</span>
|
||||||
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-blue-50 text-blue-600 text-center text-xs font-bold">I</span> Izin</span>
|
||||||
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-yellow-50 text-yellow-600 text-center text-xs font-bold">T</span> Terlambat</span>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="px-5 py-12 text-center">
|
<div class="px-5 py-12 text-center">
|
||||||
<div class="w-14 h-14 rounded-2xl bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-3">
|
<div class="w-14 h-14 rounded-2xl bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-3">
|
||||||
<i class="fa-solid fa-chart-simple text-gray-300 text-xl"></i>
|
<i class="fa-solid fa-clock text-gray-300 text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm font-medium text-gray-500 mb-0.5">Belum Ada Data Presensi</p>
|
<p class="text-sm font-medium text-gray-500 mb-0.5">Pilih Kelas & Tanggal</p>
|
||||||
<p class="text-xs text-gray-400">Belum ada catatan presensi untuk bulan ini</p>
|
<p class="text-xs text-gray-400">Pilih kelas dan tanggal untuk melihat rekap harian</p>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- ===== REKAP MINGGUAN ===== --}}
|
||||||
|
@elseif($type == 'weekly')
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-100">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-500 to-orange-500 flex items-center justify-center shadow-sm">
|
||||||
|
<i class="fa-solid fa-calendar-week text-white text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900">
|
||||||
|
Rekap Mingguan — {{ \Carbon\Carbon::parse($start)->translatedFormat('d M') }} - {{ \Carbon\Carbon::parse($end)->translatedFormat('d M Y') }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-400">{{ $students->count() }} siswa @if($className) • Kelas {{ $className }} @endif</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('attendances.export-weekly', ['class_name' => $className, 'week_start' => request('week_start', $start->toDateString())]) }}"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold text-white bg-emerald-600 rounded-xl hover:bg-emerald-700 transition shadow-sm">
|
||||||
|
<i class="fa-solid fa-download text-xs"></i>
|
||||||
|
Export
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($students->isNotEmpty())
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-100">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50/80">
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider sticky left-0 bg-gray-50/80 border-r-2 border-gray-300 z-10" rowspan="2">Siswa</th>
|
||||||
|
@foreach($recap as $row)
|
||||||
|
@php
|
||||||
|
$sep = !$loop->last ? 'border-r-2 border-gray-300' : '';
|
||||||
|
@endphp
|
||||||
|
<th colspan="4" class="px-3 py-2 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider border-b border-gray-200 {{ $sep }}">
|
||||||
|
{{ $row['label'] }}<br>
|
||||||
|
<span class="text-[10px] text-gray-400 font-normal">{{ \Carbon\Carbon::parse($row['date'])->format('d/m') }}</span>
|
||||||
|
</th>
|
||||||
|
@endforeach
|
||||||
|
<th colspan="4" class="px-3 py-2 text-center text-xs font-semibold text-gray-800 uppercase tracking-wider border-b border-gray-200 border-l-2 border-gray-400 bg-gray-100/80">
|
||||||
|
TOTAL<br>
|
||||||
|
<span class="text-[10px] text-gray-500 font-normal">Hari</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50/80">
|
||||||
|
@foreach($recap as $row)
|
||||||
|
@php
|
||||||
|
$sep = !$loop->last ? 'border-r-2 border-gray-300' : '';
|
||||||
|
@endphp
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-600 uppercase tracking-wider">H</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-purple-600 uppercase tracking-wider">S</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-600 uppercase tracking-wider">I</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-600 uppercase tracking-wider {{ $sep }}">A</th>
|
||||||
|
@endforeach
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-700 uppercase tracking-wider bg-gray-100/80 border-l-2 border-gray-400">H</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-purple-700 uppercase tracking-wider bg-gray-100/80">S</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-700 uppercase tracking-wider bg-gray-100/80">I</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-700 uppercase tracking-wider bg-gray-100/80">A</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-50">
|
||||||
|
@foreach($students as $student)
|
||||||
|
<tr class="hover:bg-gray-50/50 transition">
|
||||||
|
<td class="px-4 py-2.5 whitespace-nowrap sticky left-0 bg-white border-r-2 border-gray-200 z-10">
|
||||||
|
<p class="text-sm font-semibold text-gray-900">{{ $student->full_name }}</p>
|
||||||
|
</td>
|
||||||
|
@foreach($recap as $row)
|
||||||
|
@php
|
||||||
|
$s = $row['students'][$student->id] ?? null;
|
||||||
|
$sep = !$loop->last ? 'border-r-2 border-gray-200' : '';
|
||||||
|
@endphp
|
||||||
|
@if($s)
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-600">{{ $s['hadir'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-purple-600">{{ $s['sakit'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-600">{{ $s['izin'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['alpha'] > 0 ? 'text-red-600' : 'text-gray-400' }} {{ $sep }}">{{ $s['alpha'] }}</td>
|
||||||
|
@else
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300 {{ $sep }}">—</td>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
{{-- TOTAL --}}
|
||||||
|
@php
|
||||||
|
$t = $totals[$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
|
||||||
|
@endphp
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-700 bg-gray-50 border-l-2 border-gray-300">{{ $t['hadir'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-purple-700 bg-gray-50">{{ $t['sakit'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-700 bg-gray-50">{{ $t['izin'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['alpha'] > 0 ? 'text-red-700' : 'text-gray-400' }} bg-gray-50">{{ $t['alpha'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
||||||
|
<span class="text-emerald-600 font-bold">H</span> Hadir
|
||||||
|
<span class="text-purple-600 font-bold">S</span> Sakit
|
||||||
|
<span class="text-blue-600 font-bold">I</span> Izin
|
||||||
|
<span class="text-red-600 font-bold">A</span> Alpha
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="px-5 py-12 text-center">
|
||||||
|
<div class="w-14 h-14 rounded-2xl bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<i class="fa-solid fa-calendar-week text-gray-300 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-medium text-gray-500 mb-0.5">Pilih Kelas & Minggu</p>
|
||||||
|
<p class="text-xs text-gray-400">Pilih kelas dan minggu untuk melihat rekap mingguan</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ===== REKAP BULANAN ===== --}}
|
||||||
|
@else
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-100">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-violet-600 flex items-center justify-center shadow-sm">
|
||||||
|
<i class="fa-solid fa-calendar-alt text-white text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900">
|
||||||
|
Rekap Bulanan — {{ \Carbon\Carbon::create()->month($month)->format('F') }} {{ $year }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-400">{{ $students->count() }} siswa @if($className) • Kelas {{ $className }} @endif</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($students->isNotEmpty())
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-100">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50/80">
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider sticky left-0 bg-gray-50/80 z-10">Siswa</th>
|
||||||
|
@foreach($recap as $row)
|
||||||
|
<th class="px-3 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||||
|
{{ $row['label'] }}<br>
|
||||||
|
<span class="text-[10px] text-gray-400 font-normal">{{ \Carbon\Carbon::parse($row['start'])->format('d/m') }}-{{ \Carbon\Carbon::parse($row['end'])->format('d/m') }}</span>
|
||||||
|
</th>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-50">
|
||||||
|
@foreach($students as $student)
|
||||||
|
<tr class="hover:bg-gray-50/50 transition">
|
||||||
|
<td class="px-4 py-2.5 whitespace-nowrap sticky left-0 bg-white z-10">
|
||||||
|
<p class="text-sm font-semibold text-gray-900">{{ $student->full_name }}</p>
|
||||||
|
</td>
|
||||||
|
@foreach($recap as $row)
|
||||||
|
@php
|
||||||
|
$s = $row['students'][$student->id] ?? null;
|
||||||
|
@endphp
|
||||||
|
<td class="px-3 py-2.5 text-center">
|
||||||
|
@if($s)
|
||||||
|
<div class="flex flex-col items-center gap-0.5 text-xs">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-emerald-600 font-bold">{{ $s['hadir'] }}</span>
|
||||||
|
@if($s['alpha'] > 0)
|
||||||
|
<span class="text-red-600 font-bold">A{{ $s['alpha'] }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($s['sakit'] > 0 || $s['izin'] > 0 || $s['terlambat'] > 0)
|
||||||
|
<div class="flex items-center gap-1.5 text-[10px]">
|
||||||
|
@if($s['sakit'] > 0)<span class="text-purple-500">S{{ $s['sakit'] }}</span>@endif
|
||||||
|
@if($s['izin'] > 0)<span class="text-blue-500">I{{ $s['izin'] }}</span>@endif
|
||||||
|
@if($s['terlambat'] > 0)<span class="text-yellow-500">T{{ $s['terlambat'] }}</span>@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-300 text-xs">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
||||||
|
<span class="text-emerald-600">H Hadir</span>
|
||||||
|
<span class="text-red-600">A Alpha</span>
|
||||||
|
<span class="text-purple-500">S Sakit</span>
|
||||||
|
<span class="text-blue-500">I Izin</span>
|
||||||
|
<span class="text-yellow-500">T Terlambat</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="px-5 py-12 text-center">
|
||||||
|
<div class="w-14 h-14 rounded-2xl bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<i class="fa-solid fa-calendar-alt text-gray-300 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-medium text-gray-500 mb-0.5">Pilih Kelas & Bulan</p>
|
||||||
|
<p class="text-xs text-gray-400">Pilih kelas dan bulan untuk melihat rekap bulanan</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::post('/attendances', [\App\Http\Controllers\AttendanceController::class, 'store'])->name('attendances.store');
|
Route::post('/attendances', [\App\Http\Controllers\AttendanceController::class, 'store'])->name('attendances.store');
|
||||||
Route::get('/attendances/recap', [\App\Http\Controllers\AttendanceController::class, 'recap'])->name('attendances.recap');
|
Route::get('/attendances/recap', [\App\Http\Controllers\AttendanceController::class, 'recap'])->name('attendances.recap');
|
||||||
Route::get('/attendances/calendar-data', [\App\Http\Controllers\AttendanceController::class, 'calendarData'])->name('attendances.calendar-data');
|
Route::get('/attendances/calendar-data', [\App\Http\Controllers\AttendanceController::class, 'calendarData'])->name('attendances.calendar-data');
|
||||||
|
Route::get('/attendances/export-weekly', [\App\Http\Controllers\AttendanceController::class, 'exportWeekly'])->name('attendances.export-weekly');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware('permission:view-sp-letters')->group(function () {
|
Route::middleware('permission:view-sp-letters')->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user