From de1877695ddbae2be9db50c41c70b1ae588ac966 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 27 Mar 2026 22:08:59 +0700 Subject: [PATCH] feat: Add attendance management pages and sharing functionality, including detailed views and statistics for courses. --- .../Pages/Manage/Attendance/Detail.php | 122 ++++++++ .../Pages/Manage/Attendance/Index.php | 65 ++++ .../Controllers/ShareAttendanceController.php | 50 +++ app/Models/Course.php | 12 + app/Providers/Filament/AdminPanelProvider.php | 1 + ...022_add_sharing_token_to_courses_table.php | 33 ++ database/seeders/ShieldSeeder.php | 28 +- .../pages/manage/attendance/detail.blade.php | 145 +++++++++ .../pages/manage/attendance/index.blade.php | 102 +++++++ .../views/pages/share-attendance.blade.php | 284 ++++++++++++++++++ routes/web.php | 2 + 11 files changed, 842 insertions(+), 2 deletions(-) create mode 100644 app/Filament/Pages/Manage/Attendance/Detail.php create mode 100644 app/Filament/Pages/Manage/Attendance/Index.php create mode 100644 app/Http/Controllers/ShareAttendanceController.php create mode 100644 database/migrations/2026_03_12_114022_add_sharing_token_to_courses_table.php create mode 100644 resources/views/filament/pages/manage/attendance/detail.blade.php create mode 100644 resources/views/filament/pages/manage/attendance/index.blade.php create mode 100644 resources/views/pages/share-attendance.blade.php diff --git a/app/Filament/Pages/Manage/Attendance/Detail.php b/app/Filament/Pages/Manage/Attendance/Detail.php new file mode 100644 index 0000000..d326557 --- /dev/null +++ b/app/Filament/Pages/Manage/Attendance/Detail.php @@ -0,0 +1,122 @@ +course = $course; + } + + public function getHeaderActions(): array + { + return [ + BackAction::make() + ->url(Index::getUrl()), + ]; + } + + protected function getViewData(): array + { + return [ + 'selectedCourse' => $this->course, + 'meetingHistory' => $this->getMeetingHistory(), + 'activeMeetingStats' => $this->getActiveMeetingStats(), + ]; + } + + private function getMeetingHistory(): Collection + { + return Attendance::query() + ->whereHas('courseSchedule', function ($q) { + $q->where('course_id', $this->course->id); + }) + ->select('date') + ->distinct() + ->orderBy('date', 'desc') + ->get() + ->map(function ($att) { + $attendedCount = Attendance::whereHas('courseSchedule', function ($q) { + $q->where('course_id', $this->course->id); + }) + ->whereDate('date', $att->date) + ->count(); + + $totalStudents = Student::count(); + + return (object) [ + 'date' => [ + 'month' => $att->date->translatedFormat('M'), + 'day' => $att->date->translatedFormat('d'), + ], + 'formatted_date' => $att->date->translatedFormat('l, d F Y'), + 'attended_count' => $attendedCount, + 'total_students' => $totalStudents, + 'share_url' => route('share.attendance', [ + 'token' => $this->course->sharing_token, + 'date' => $att->date->toDateString(), + ]), + ]; + }); + } + + private function getActiveMeetingStats(): ?object + { + $today = now()->dayOfWeekIso; + $nowTime = now()->format('H:i'); + + $activeSchedule = CourseSchedule::where('course_id', $this->course->id) + ->where('day_of_week', $today) + ->where('start_time', '<=', $nowTime) + ->where('end_time', '>=', $nowTime) + ->first(); + + if (! $activeSchedule) { + return null; + } + + $attendedIds = Attendance::where('course_schedule_id', $activeSchedule->id) + ->whereDate('date', now()->toDateString()) + ->pluck('student_id') + ->toArray(); + + $totalStudents = Student::count(); + + $absentStudents = Student::whereNotIn('id', $attendedIds)->get(); + + return (object) [ + 'time_range' => $activeSchedule->start_time->format('H:i').' - '.$activeSchedule->end_time->format('H:i'), + 'attended_count' => count($attendedIds), + 'total_students' => $totalStudents, + 'absent_students' => $absentStudents, + 'percentage' => round((count($attendedIds) / $totalStudents) * 100), + ]; + } +} diff --git a/app/Filament/Pages/Manage/Attendance/Index.php b/app/Filament/Pages/Manage/Attendance/Index.php new file mode 100644 index 0000000..e5bb2dd --- /dev/null +++ b/app/Filament/Pages/Manage/Attendance/Index.php @@ -0,0 +1,65 @@ + $this->getCourses(), + ]; + } + + private function getCourses(): Collection + { + $semester = app(GeneralSettings::class)->current_semester; + $today = now()->dayOfWeekIso; + + return Course::query() + ->where('semester', $semester) + ->with(['lecturer', 'courseSchedules']) + ->get() + ->map(function ($course) use ($today) { + $isOngoing = $course->courseSchedules->contains(function ($schedule) use ($today) { + return $schedule->day_of_week == $today; + }); + + $course->lecturer_name = $course->lecturer?->full_name; + $course->is_ongoing = $isOngoing; + $course->detail_url = Detail::getUrl(['course' => $course]); + + return $course; + }); + } +} diff --git a/app/Http/Controllers/ShareAttendanceController.php b/app/Http/Controllers/ShareAttendanceController.php new file mode 100644 index 0000000..2faa4e4 --- /dev/null +++ b/app/Http/Controllers/ShareAttendanceController.php @@ -0,0 +1,50 @@ +setLocale('id'); + $course = Course::where('sharing_token', $token)->firstOrFail(); + + // Define default date: if today has no attendance, use the most recent attendance date + $latestAttendance = Attendance::whereHas('courseSchedule', function ($query) use ($course) { + $query->where('course_id', $course->id); + }) + ->latest('date') + ->first(); + + $defaultDate = $latestAttendance ? $latestAttendance->date->toDateString() : now()->toDateString(); + $date = $request->query('date', $defaultDate); + + $attendances = Attendance::with('student') + ->whereHas('courseSchedule', function ($query) use ($course) { + $query->where('course_id', $course->id); + }) + ->whereDate('date', $date) + ->get(); + + $availableDates = Attendance::whereHas('courseSchedule', function ($query) use ($course) { + $query->where('course_id', $course->id); + }) + ->select('date') + ->distinct() + ->orderBy('date', 'desc') + ->get() + ->pluck('date'); + + $assignments = Assignment::where('course_id', $course->id) + ->withCount('assignmentSubmissions') + ->latest() + ->get(); + + return view('pages.share-attendance', compact('course', 'attendances', 'date', 'availableDates', 'assignments')); + } +} diff --git a/app/Models/Course.php b/app/Models/Course.php index 96e6a2d..c558e45 100644 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -16,6 +16,13 @@ class Course extends Model protected $guarded = ['id']; + protected static function booted() + { + static::creating(function ($course) { + $course->sharing_token = \Illuminate\Support\Str::random(32); + }); + } + public function lecturer(): BelongsTo { return $this->belongsTo(Lecturer::class); @@ -37,4 +44,9 @@ public function studyGroups(): BelongsToMany { return $this->belongsToMany(StudyGroup::class, 'study_group_courses'); } + + public function courseSchedules(): HasMany + { + return $this->hasMany(CourseSchedule::class); + } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index c46ad3f..b0ac369 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -131,6 +131,7 @@ public function panel(Panel $panel): Panel ->breadcrumbs(false) ->navigationGroups([ 'Master', + 'Kelola', 'Pembelajaran', 'Informasi', 'Sistem', diff --git a/database/migrations/2026_03_12_114022_add_sharing_token_to_courses_table.php b/database/migrations/2026_03_12_114022_add_sharing_token_to_courses_table.php new file mode 100644 index 0000000..d7c4265 --- /dev/null +++ b/database/migrations/2026_03_12_114022_add_sharing_token_to_courses_table.php @@ -0,0 +1,33 @@ +string('sharing_token', 64)->nullable()->unique()->after('semester'); + }); + + // Populate existing courses with a random token + \App\Models\Course::all()->each(function ($course) { + $course->update(['sharing_token' => \Illuminate\Support\Str::random(32)]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('courses', function (Blueprint $table) { + $table->dropColumn('sharing_token'); + }); + } +}; diff --git a/database/seeders/ShieldSeeder.php b/database/seeders/ShieldSeeder.php index b75ac95..66c5357 100644 --- a/database/seeders/ShieldSeeder.php +++ b/database/seeders/ShieldSeeder.php @@ -131,7 +131,19 @@ public function run(): void "Reorder:Role", "View:LogTable", - "View:ManageSettings" + "View:ManageSettings", + + "ViewAny:AttendanceMonitoring", + "View:AttendanceMonitoring", + "Create:AttendanceMonitoring", + "Update:AttendanceMonitoring", + "Delete:AttendanceMonitoring", + "Restore:AttendanceMonitoring", + "ForceDelete:AttendanceMonitoring", + "ForceDeleteAny:AttendanceMonitoring", + "RestoreAny:AttendanceMonitoring", + "Replicate:AttendanceMonitoring", + "Reorder:AttendanceMonitoring" ] }, { @@ -195,7 +207,19 @@ public function run(): void "Update:StudyGroup", "Delete:StudyGroup", - "View:ManageSettings" + "View:ManageSettings", + + "ViewAny:AttendanceMonitoring", + "View:AttendanceMonitoring", + "Create:AttendanceMonitoring", + "Update:AttendanceMonitoring", + "Delete:AttendanceMonitoring", + "Restore:AttendanceMonitoring", + "ForceDelete:AttendanceMonitoring", + "ForceDeleteAny:AttendanceMonitoring", + "RestoreAny:AttendanceMonitoring", + "Replicate:AttendanceMonitoring", + "Reorder:AttendanceMonitoring" ] } diff --git a/resources/views/filament/pages/manage/attendance/detail.blade.php b/resources/views/filament/pages/manage/attendance/detail.blade.php new file mode 100644 index 0000000..4a11e9d --- /dev/null +++ b/resources/views/filament/pages/manage/attendance/detail.blade.php @@ -0,0 +1,145 @@ + +
+
+ +
+ + Riwayat Pertemuan + Rekapitulasi presensi tiap pertemuan. + +
+ @forelse ($meetingHistory as $meeting) +
+
+
+ {{ $meeting->date['month'] }} + {{ $meeting->date['day'] }} +
+
+

+ {{ $meeting->formatted_date }}

+

+ HADIR: {{ $meeting->attended_count }} + / {{ $meeting->total_students }} +

+
+
+ + + Salin Link + +
+ @empty + + + @endforelse +
+
+
+ + +
+ @if ($activeMeetingStats) +
+
+
+

+ + Monitoring Sesi Ini +

+
+ +
+
+ WAKTU PERKULIAHAN + {{ $activeMeetingStats->time_range }} +
+ +
+
+
+ {{ $activeMeetingStats->attended_count }} / + {{ $activeMeetingStats->total_students }} +
+ + {{ $activeMeetingStats->percentage }}% + +
+
+
+
+
+
+ +
+
+ Belum Melakukan Absen + {{ $activeMeetingStats->total_students - $activeMeetingStats->attended_count }} +
+
+ @foreach ($activeMeetingStats->absent_students as $absent) +
+
+ {{ $absent->full_name }}
+
+ {{ $absent->nim }} +
+
+ @endforeach +
+
+
+
+
+ @else + + + @endif +
+
+
+ + +
diff --git a/resources/views/filament/pages/manage/attendance/index.blade.php b/resources/views/filament/pages/manage/attendance/index.blade.php new file mode 100644 index 0000000..3c06a20 --- /dev/null +++ b/resources/views/filament/pages/manage/attendance/index.blade.php @@ -0,0 +1,102 @@ + + + diff --git a/resources/views/pages/share-attendance.blade.php b/resources/views/pages/share-attendance.blade.php new file mode 100644 index 0000000..c41a250 --- /dev/null +++ b/resources/views/pages/share-attendance.blade.php @@ -0,0 +1,284 @@ + + + + + + + Presensi {{ $course->name }} - {{ config('app.name', 'MyClass') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + +
+ +
+ +
+
+
+ {{ $course->code }} + SEMESTER + GENAP +
+

{{ $course->name }}

+

+ + {{ $course->lecturer->full_name }} +

+
+ +
+ +
+ +
+ +
+
+
+
+ + +
+ +
+
+

Daftar Kehadiran +

+ {{ $attendances->count() }} + Terdata +
+ +
+
+ + + + + + + + + + @forelse($attendances as $attendance) + + + + + + @empty + + + + @endforelse + +
+ Mahasiswa + Nomor Induk + Waktu Presensi
+
+
+ {{ collect(explode(' ', $attendance->student->full_name))->map(fn($n) => substr($n, 0, 1))->take(2)->join('') }} +
+
+ {{ $attendance->student->full_name }} +
+
+
+ + {{ $attendance->student->student_number }} + + +
+ + {{ $attendance->attended_at ? $attendance->attended_at->format('H:i') : '-' }} + WIB +
+
+

+ Belum ada data kehadiran untuk sesi ini

+
+
+
+
+ + +
+ +
+

Informasi Sesi +

+
+
+ Tanggal + Sesi + {{ \Carbon\Carbon::parse($date)->translatedFormat('l, d F Y') }} +
+
+
+
+ Partisipan + {{ $attendances->count() }} Orang +
+
+ Jam + Kelas + 08:00 - 10:30 +
+
+
+
+ + +
+

Tugas Terkait +

+
+ @forelse($assignments as $assignment) +
+
+
+

+ {{ $assignment->title }}

+ TGS +
+
+
+ + {{ $assignment->deadline ? $assignment->deadline->translatedFormat('d M') : 'N/A' }} +
+
+ + {{ $assignment->assignment_submissions_count }} Kirim +
+
+
+
+ @empty +
+ Tidak ada tugas terdaftar. +
+ @endforelse +
+
+
+
+
+
+ + + diff --git a/routes/web.php b/routes/web.php index 86a06c5..2420695 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,3 +5,5 @@ Route::get('/', function () { return view('welcome'); }); + +Route::get('/share/presensi/{token}', [\App\Http\Controllers\ShareAttendanceController::class, 'show'])->name('share.attendance');