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 @@
+
+ HADIR: {{ $meeting->attended_count }}
+ / {{ $meeting->total_students }}
+
+ {{ $meeting->formatted_date }}
+
+
+
+ Belum Melakukan Absen
+ {{ $activeMeetingStats->total_students - $activeMeetingStats->attended_count }}
+
+
+
+
| + 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 + |
+ ||