From fb5e349ae6597a3af5d9288d910164d975f71755 Mon Sep 17 00:00:00 2001 From: Novy San87 Date: Thu, 23 Jul 2026 11:59:12 +0700 Subject: [PATCH] feat: add Open Graph meta tags for social preview --- app/Http/Controllers/AttendanceController.php | 180 ++++++++++++++++-- bootstrap/app.php | 1 + resources/views/attendances/recap.blade.php | 103 ++++++---- resources/views/auth/login.blade.php | 18 ++ resources/views/layouts/app.blade.php | 18 ++ routes/web.php | 1 + 6 files changed, 269 insertions(+), 52 deletions(-) diff --git a/app/Http/Controllers/AttendanceController.php b/app/Http/Controllers/AttendanceController.php index 4acee76..f11d44d 100644 --- a/app/Http/Controllers/AttendanceController.php +++ b/app/Http/Controllers/AttendanceController.php @@ -364,6 +364,7 @@ class AttendanceController extends Controller $students = collect(); $recap = collect(); + $totals = []; if ($className) { $students = Student::where('is_active', true) ->where('class_name', $className) @@ -378,35 +379,74 @@ class AttendanceController extends Controller foreach ($weeks as $weekNum => $week) { $weekAtt = $allAttendances->filter(fn($a) => $a->date >= $week['start'] && $a->date <= $week['end'] - )->groupBy('student_id'); + ); + + // 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) { - $studentAtt = $weekAtt->get($student->id, collect()); + $hCount = 0; $sCount = 0; $iCount = 0; $aCount = 0; $tCount = 0; $totalDaysForStudent = 0; - $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(); + 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: jika semua jam di hari itu alpha + $totalJam = $dayAtt->count(); + $alphaJam = $dayAtt->where('status', 'alpha')->count(); + if ($totalJam > 0) { + $aCount += round($alphaJam / $totalJam, 1); + } + } $row['students'][$student->id] = [ 'name' => $student->full_name, - 'hadir' => $hadir, - 'alpha' => $alpha, - 'sakit' => $sakit, - 'izin' => $izin, - 'terlambat' => $terlambat, - 'total' => $studentAtt->count(), + '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' + 'students', 'recap', 'weeks', 'totals' )); } @@ -555,6 +595,118 @@ class AttendanceController extends Controller ]); } + 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 = ''; diff --git a/bootstrap/app.php b/bootstrap/app.php index af2176c..3614753 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -12,6 +12,7 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->trustProxies(at: '*'); $middleware->alias([ 'role' => \App\Http\Middleware\CheckRole::class, 'permission' => \App\Http\Middleware\PermissionMiddleware::class, diff --git a/resources/views/attendances/recap.blade.php b/resources/views/attendances/recap.blade.php index f4633f4..e740ae9 100644 --- a/resources/views/attendances/recap.blade.php +++ b/resources/views/attendances/recap.blade.php @@ -304,16 +304,23 @@ @else
-
-
- -
-
-

- Rekap Bulanan — {{ \Carbon\Carbon::create()->month($month)->format('F') }} {{ $year }} -

-

{{ $students->count() }} siswa @if($className) • Kelas {{ $className }} @endif

+
+
+
+ +
+
+

+ Rekap Bulanan — {{ \Carbon\Carbon::create()->month($month)->format('F') }} {{ $year }} +

+

{{ $students->count() }} siswa @if($className) • Kelas {{ $className }} @endif

+
+ + + Export +
@@ -322,58 +329,78 @@ - + @foreach($recap as $row) - @endforeach + + + + @foreach($recap as $row) + @php + $sep = !$loop->last ? 'border-r-2 border-gray-300' : ''; + @endphp + + + + + @endforeach + + + + @foreach($students as $student) - @foreach($recap as $row) @php $s = $row['students'][$student->id] ?? null; + $sep = !$loop->last ? 'border-r-2 border-gray-200' : ''; @endphp - + @if($s) + + + + + @else + + + + + @endif @endforeach + {{-- TOTAL --}} + @php + $t = $totals[$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0]; + @endphp + + + + @endforeach
SiswaSiswa + @php + $sep = !$loop->last ? 'border-r-2 border-gray-300' : ''; + @endphp + {{ $row['label'] }}
{{ \Carbon\Carbon::parse($row['start'])->format('d/m') }}-{{ \Carbon\Carbon::parse($row['end'])->format('d/m') }}
+ TOTAL
+ Hari +
HSIAHSIA
+

{{ $student->full_name }}

- @if($s) -
-
- {{ $s['hadir'] }} - @if($s['alpha'] > 0) - A{{ $s['alpha'] }} - @endif -
- @if($s['sakit'] > 0 || $s['izin'] > 0 || $s['terlambat'] > 0) -
- @if($s['sakit'] > 0)S{{ $s['sakit'] }}@endif - @if($s['izin'] > 0)I{{ $s['izin'] }}@endif - @if($s['terlambat'] > 0)T{{ $s['terlambat'] }}@endif -
- @endif -
- @else - — - @endif -
{{ $s['hadir'] }}{{ $s['sakit'] }}{{ $s['izin'] }}{{ $s['alpha'] }}————{{ $t['hadir'] }}{{ $t['sakit'] }}{{ $t['izin'] }}{{ $t['alpha'] }}
- H Hadir - A Alpha - S Sakit - I Izin - T Terlambat + H Hadir + S Sakit + I Izin + A Alpha
@else
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 5ed0c61..9ee204e 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -11,6 +11,24 @@ $bgPath = App\Models\Setting::getValue('login_background'); @endphp Login - {{ $appName }} + + {{-- Open Graph / Social Preview --}} + + + + + + @if($logoPath) + + + + @else + + @endif + + + + @vite(['resources/css/app.css']) diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 0fb8829..a63da80 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -10,6 +10,24 @@ $logoPath = App\Models\Setting::getValue('school_logo'); @endphp {{ $schoolName }} - @yield('title', $appName) + + {{-- Open Graph / Social Preview --}} + + + + + + @if($logoPath) + + + + @else + + @endif + + + + diff --git a/routes/web.php b/routes/web.php index 5633dcc..67d6529 100644 --- a/routes/web.php +++ b/routes/web.php @@ -69,6 +69,7 @@ Route::middleware('auth')->group(function () { 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/export-weekly', [\App\Http\Controllers\AttendanceController::class, 'exportWeekly'])->name('attendances.export-weekly'); + Route::get('/attendances/export-monthly', [\App\Http\Controllers\AttendanceController::class, 'exportMonthly'])->name('attendances.export-monthly'); }); Route::middleware('permission:view-sp-letters')->group(function () {