feat: add attendance export functionality for class sessions

This commit is contained in:
Yoga Pangestu 2026-04-09 19:41:00 +07:00
parent 400a0e3001
commit 9d1000a484
9 changed files with 956 additions and 237 deletions

View File

@ -0,0 +1,105 @@
<?php
namespace App\Filament\Exports;
use App\Models\Attendance;
use App\Models\ClassSession;
use App\Models\Student;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class AttendanceExport implements FromCollection, WithEvents, WithHeadings, WithMapping, WithStyles
{
protected ClassSession $session;
protected Collection $activeStudents;
protected Collection $attendanceRecords;
public function __construct(ClassSession $session)
{
$session->load('course');
$this->session = $session;
$this->activeStudents = Student::whereHas('user', fn ($q) => $q->active())
->orderBy('full_name')
->get();
$this->attendanceRecords = Attendance::query()
->whereHas('courseSchedule', function ($query) {
$query->where('course_id', $this->session->course_id);
})
->whereDate('date', $this->session->date)
->get()
->keyBy('student_id');
}
public function collection()
{
return $this->activeStudents->map(fn ($student) => (object) [
'full_name' => $student->full_name,
'student_number' => $student->student_number,
'status' => $this->attendanceRecords->has($student->id) ? 'Hadir' : 'Alpa',
'attended_at' => $this->attendanceRecords->get($student->id)?->attended_at,
]);
}
public function headings(): array
{
return [
['Data Presensi '.($this->session->course?->name ?? '').' Sesi Ke-'.$this->session->session_number.' ('.$this->session->date?->translatedFormat('d F Y').')'],
['Mahasiswa', 'NIM', 'Status', 'Waktu Presensi'],
];
}
public function map($row): array
{
return [
$row->full_name,
$row->student_number,
$row->status,
$row->attended_at ? $row->attended_at->translatedFormat('d F Y H:i') : '-',
];
}
public function styles(Worksheet $sheet)
{
return [
1 => [
'font' => ['bold' => true, 'size' => 14],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
],
2 => [
'font' => ['bold' => true],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
],
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastColumn = $sheet->getHighestColumn();
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->getStyle("A2:{$lastColumn}".$sheet->getHighestRow())
->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
foreach (range('A', $lastColumn) as $columnID) {
$sheet->getColumnDimension($columnID)->setAutoSize(true);
}
},
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\Learning\ClassSessions\Actions;
use App\Filament\Exports\AttendanceExport;
use App\Models\ClassSession;
use Filament\Actions\Action;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
class ExportSessionAttendanceAction extends Action
{
public static function getDefaultName(): ?string
{
return 'exportAttendance';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Export Presensi')
->color('success')
->icon('heroicon-o-arrow-down-tray')
->link()
->action(function (array $arguments) {
$session = ClassSession::findOrFail($arguments['session']);
$courseName = $session->course?->name ?? '';
$filename = 'rekap-presensi-'.Str::slug($courseName).'-sesi-'.$session->session_number.'-'.$session->date?->toDateString().'.xlsx';
return Excel::download(new AttendanceExport($session), $filename);
});
}
}

View File

@ -5,6 +5,7 @@
use App\Filament\Actions\BackAction; use App\Filament\Actions\BackAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\DeleteSessionAction; use App\Filament\Resources\Learning\ClassSessions\Actions\DeleteSessionAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\EditSessionAction; use App\Filament\Resources\Learning\ClassSessions\Actions\EditSessionAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\ExportSessionAttendanceAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\GenerateSessionsAction; use App\Filament\Resources\Learning\ClassSessions\Actions\GenerateSessionsAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\MarkAsSentAction; use App\Filament\Resources\Learning\ClassSessions\Actions\MarkAsSentAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\ShareAttendanceAction; use App\Filament\Resources\Learning\ClassSessions\Actions\ShareAttendanceAction;
@ -126,6 +127,11 @@ public function deleteSessionAction(): DeleteSessionAction
return DeleteSessionAction::make(); return DeleteSessionAction::make();
} }
public function exportAttendanceAction(): ExportSessionAttendanceAction
{
return ExportSessionAttendanceAction::make();
}
#[Computed] #[Computed]
public function emptyStateHeading(): string public function emptyStateHeading(): string
{ {

View File

@ -6,7 +6,7 @@
"keywords": ["laravel", "framework"], "keywords": ["laravel", "framework"],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.3", "php": "^8.2",
"achyutn/filament-log-viewer": "^2.1", "achyutn/filament-log-viewer": "^2.1",
"asmit/filament-upload": "^2.0", "asmit/filament-upload": "^2.0",
"bezhansalleh/filament-shield": "^4.1", "bezhansalleh/filament-shield": "^4.1",
@ -18,6 +18,8 @@
"joaopaulolndev/filament-pdf-viewer": "^3.0", "joaopaulolndev/filament-pdf-viewer": "^3.0",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"maatwebsite/excel": "^3.1",
"pxlrbt/filament-excel": "^3.6",
"saade/filament-facehash": "^1.0" "saade/filament-facehash": "^1.0"
}, },
"require-dev": { "require-dev": {

944
composer.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -66,6 +66,7 @@ class="text-[10px] font-bold text-gray-500">{{ $session->attendance_percentage }
class="flex flex-wrap items-center gap-3 px-4 py-3 border-t border-gray-100 dark:border-gray-700"> class="flex flex-wrap items-center gap-3 px-4 py-3 border-t border-gray-100 dark:border-gray-700">
{{ ($this->viewAttendanceAction)(['session' => $session->id]) }} {{ ($this->viewAttendanceAction)(['session' => $session->id]) }}
{{ ($this->shareAttendanceAction)(['session' => $session->id]) }} {{ ($this->shareAttendanceAction)(['session' => $session->id]) }}
{{ ($this->exportAttendanceAction)(['session' => $session->id]) }}
{{ ($this->markAsSentAction)(['record' => $session->id]) }} {{ ($this->markAsSentAction)(['record' => $session->id]) }}
{{ ($this->editSessionAction)(['session' => $session->id]) }} {{ ($this->editSessionAction)(['session' => $session->id]) }}
{{ ($this->deleteSessionAction)(['session' => $session->id]) }} {{ ($this->deleteSessionAction)(['session' => $session->id]) }}