feat: Add attendance management pages and sharing functionality, including detailed views and statistics for courses.

This commit is contained in:
Yoga Pangestu 2026-03-27 22:08:59 +07:00
parent f7efd30b2a
commit de1877695d
11 changed files with 842 additions and 2 deletions

View File

@ -0,0 +1,122 @@
<?php
namespace App\Filament\Pages\Manage\Attendance;
use App\Filament\Actions\BackAction;
use App\Models\Attendance;
use App\Models\Course;
use App\Models\CourseSchedule;
use App\Models\Student;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Pages\Page;
use Illuminate\Support\Collection;
class Detail extends Page
{
use HasPageShield;
public static function getPagePermission(): string
{
return 'View:AttendanceMonitoring';
}
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Detail Monitoring Presensi';
protected static ?string $slug = 'manage/attendance-monitoring/{course}';
protected string $view = 'filament.pages.manage.attendance.detail';
public Course $course;
public function mount(Course $course): void
{
$this->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),
];
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Filament\Pages\Manage\Attendance;
use App\Models\Course;
use App\Settings\GeneralSettings;
use BackedEnum;
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Collection;
use UnitEnum;
class Index extends Page
{
use HasPageShield;
public static function getPagePermission(): string
{
return 'View:AttendanceMonitoring';
}
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPresentationChartLine;
protected static ?string $navigationLabel = 'Monitoring Presensi';
protected static ?string $title = 'Monitoring Presensi';
protected static ?string $slug = 'manage/attendance-monitoring';
protected static ?int $navigationSort = 5;
protected string $view = 'filament.pages.manage.attendance.index';
protected function getViewData(): array
{
return [
'courses' => $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;
});
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use App\Models\Assignment;
use App\Models\Attendance;
use App\Models\Course;
use Illuminate\Http\Request;
class ShareAttendanceController extends Controller
{
public function show(Request $request, string $token)
{
app()->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'));
}
}

View File

@ -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);
}
}

View File

@ -131,6 +131,7 @@ public function panel(Panel $panel): Panel
->breadcrumbs(false)
->navigationGroups([
'Master',
'Kelola',
'Pembelajaran',
'Informasi',
'Sistem',

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('courses', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -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"
]
}

View File

@ -0,0 +1,145 @@
<x-filament-panels::page>
<div class="space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left: History -->
<div class="lg:col-span-2 space-y-6">
<x-filament::section>
<x-slot name="heading">Riwayat Pertemuan</x-slot>
<x-slot name="description">Rekapitulasi presensi tiap pertemuan.</x-slot>
<div class="space-y-4">
@forelse ($meetingHistory as $meeting)
<div
class="fi-card flex items-center justify-between p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm transition duration-200 hover:shadow-md hover:border-primary-500">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-lg bg-gray-50 dark:bg-gray-800 flex flex-col items-center justify-center border border-gray-100 dark:border-gray-700">
<span
class="text-[10px] font-bold uppercase text-gray-400">{{ $meeting->date['month'] }}</span>
<span
class="text-xl font-bold text-gray-700 dark:text-gray-200">{{ $meeting->date['day'] }}</span>
</div>
<div>
<h4 class="font-bold text-sm text-gray-950 dark:text-white">
{{ $meeting->formatted_date }}</h4>
<p
class="text-[11px] font-medium text-gray-500 uppercase tracking-tight mt-0.5">
HADIR: <span
class="text-primary-600 dark:text-primary-400">{{ $meeting->attended_count }}
/ {{ $meeting->total_students }}</span>
</p>
</div>
</div>
<x-filament::button
x-on:click="window.navigator.clipboard.writeText('{{ $meeting->share_url }}'); new FilamentNotification().title('Tautan Berhasil Disalin ✨').body('Tautan presensi telah berhasil disalin ke papan klip.').success().send()"
color="gray" size="xs" icon="heroicon-m-share"
class="rounded-lg shadow-sm font-bold uppercase tracking-tight text-[10px]">
Salin Link
</x-filament::button>
</div>
@empty
<x-filament::empty-state icon="heroicon-o-calendar" heading="Belum Ada Riwayat Pertemuan"
description="Riwayat pertemuan untuk mata kuliah ini belum tersedia. Data presensi akan muncul setelah sesi perkuliahan dilakukan."
iconColor="gray">
</x-filament::empty-state>
@endforelse
</div>
</x-filament::section>
</div>
<!-- Right: Ongoing Stats -->
<div class="space-y-6">
@if ($activeMeetingStats)
<div
class="fi-card flex flex-col justify-between rounded-xl border transition duration-200 relative bg-primary-50/30 dark:bg-primary-900/10 border-primary-500 shadow-md ring-1 ring-primary-500">
<div class="p-6 space-y-4">
<div class="space-y-1">
<h3
class="text-lg font-bold leading-tight flex items-center gap-2 text-primary-900 dark:text-primary-100">
<x-heroicon-o-presentation-chart-line class="w-5 h-5 shrink-0 text-primary-500" />
Monitoring Sesi Ini
</h3>
</div>
<div class="space-y-4 pt-4 border-t border-primary-200 dark:border-primary-800">
<div
class="flex justify-between items-center text-[10px] font-bold uppercase tracking-widest text-primary-700 dark:text-primary-300">
<span>WAKTU PERKULIAHAN</span>
<span
class="font-mono bg-primary-100 dark:bg-primary-800 px-2 py-0.5 rounded text-primary-600 dark:text-primary-400">{{ $activeMeetingStats->time_range }}</span>
</div>
<div class="space-y-3">
<div class="flex justify-between items-end">
<div
class="text-3xl font-black text-primary-950 dark:text-white tracking-tight">
{{ $activeMeetingStats->attended_count }} <span
class="text-sm font-normal text-primary-600/70 italic">/
{{ $activeMeetingStats->total_students }}</span>
</div>
<span
class="text-xs font-bold text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-800 px-2 py-1 rounded-lg">
{{ $activeMeetingStats->percentage }}%
</span>
</div>
<div
class="w-full h-3 bg-white/50 dark:bg-gray-800/50 rounded-full overflow-hidden border border-primary-200 dark:border-primary-700 p-0.5">
<div class="h-full bg-primary-500 rounded-full transition-all duration-1000 shadow-[0_0_12px_rgba(var(--primary-500),0.4)]"
style="width: {{ $activeMeetingStats->percentage }}%">
</div>
</div>
</div>
<div class="pt-4 border-t border-primary-200 dark:border-primary-800">
<h5
class="text-[10px] font-bold text-primary-700 dark:text-primary-300 uppercase tracking-widest mb-3 flex items-center justify-between">
<span>Belum Melakukan Absen</span>
<span
class="bg-warning-500 text-white px-2 py-0.5 rounded text-[10px] shadow-sm font-bold">{{ $activeMeetingStats->total_students - $activeMeetingStats->attended_count }}</span>
</h5>
<div class="max-h-[350px] overflow-y-auto space-y-2 thin-scrollbar pr-1">
@foreach ($activeMeetingStats->absent_students as $absent)
<div
class="p-3 rounded-xl bg-white/60 dark:bg-gray-900/60 border border-primary-200/50 dark:border-primary-800/50 text-xs shadow-sm group hover:border-warning-500 transition">
<div class="font-bold text-gray-950 dark:text-white truncate">
{{ $absent->full_name }}</div>
<div class="text-[10px] text-gray-500 font-mono mt-0.5">
{{ $absent->nim }}
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
@else
<x-filament::empty-state icon="heroicon-o-clock" heading="Tidak Ada Sesi Perkuliahan Aktif"
description="Saat ini belum ada sesi perkuliahan yang berlangsung. Silakan lihat daftar riwayat pertemuan di sebelah kiri."
iconColor="gray">
</x-filament::empty-state>
@endif
</div>
</div>
</div>
<style>
.thin-scrollbar::-webkit-scrollbar {
width: 4px;
}
.thin-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.thin-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
}
.dark .thin-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.05);
}
</style>
</x-filament-panels::page>

View File

@ -0,0 +1,102 @@
<x-filament-panels::page>
<div class="space-y-6">
<x-filament::section>
<x-slot name="heading">Monitoring Presensi</x-slot>
<x-slot name="description">Pilih mata kuliah untuk memantau kehadiran mahasiswa per pertemuan.</x-slot>
<div class="columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6">
@foreach ($courses as $course)
<a href="{{ $course->detail_url }}" class="break-inside-avoid mb-6 block group">
<div @class([
'fi-card flex flex-col justify-between rounded-xl border transition duration-200 group relative',
'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md hover:border-primary-500' => !$course->is_ongoing,
'bg-primary-50/30 dark:bg-primary-900/10 border-primary-500 shadow-md ring-1 ring-primary-500' =>
$course->is_ongoing,
])>
@if ($course->is_ongoing)
<div class="absolute top-0 right-0 -translate-x-1 -translate-y-1/2 z-10">
<span
class="inline-flex items-center gap-1 rounded-full bg-primary-600 px-3 py-1 text-[10px] font-bold text-white shadow-lg uppercase tracking-widest">
<div class="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
Hari Ini
</span>
</div>
@endif
<div class="p-6 space-y-4 flex-1">
<div class="space-y-1">
<h3 @class([
'text-lg font-bold leading-tight flex items-center gap-2 transition-colors',
'text-gray-950 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400' => !$course->is_ongoing,
'text-primary-900 dark:text-primary-100' => $course->is_ongoing,
])>
<x-heroicon-o-folder @class([
'w-5 h-5 shrink-0',
'opacity-40' => !$course->is_ongoing,
'text-primary-500' => $course->is_ongoing,
]) />
{{ $course->name }}
</h3>
<div @class([
'flex items-center gap-1.5',
'text-gray-500 dark:text-gray-400' => !$course->is_ongoing,
'text-primary-600 dark:text-primary-400' => $course->is_ongoing,
])>
<x-heroicon-m-hashtag @class(['w-3.5 h-3.5 shrink-0', 'opacity-50' => !$course->is_ongoing]) />
<span class="text-[11px] font-medium leading-none uppercase tracking-widest">
{{ $course->code }}
</span>
</div>
</div>
<div @class([
'pt-4 border-t',
'border-gray-100 dark:border-gray-800' => !$course->is_ongoing,
'border-primary-200 dark:border-primary-800' => $course->is_ongoing,
])>
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
<div @class([
'w-8 h-8 rounded-full flex items-center justify-center mr-3 shrink-0 border',
'bg-primary-100 dark:bg-primary-900/40 border-primary-200 dark:border-primary-800' => !$course->is_ongoing,
'bg-white dark:bg-primary-800 border-primary-300 dark:border-primary-600' =>
$course->is_ongoing,
])>
<x-heroicon-m-user @class([
'w-4 h-4',
'text-primary-600 dark:text-primary-300' => !$course->is_ongoing,
'text-primary-700 dark:text-primary-200' => $course->is_ongoing,
]) />
</div>
<div class="flex flex-col min-w-0">
<span
class="text-[10px] font-bold uppercase text-gray-400 tracking-widest leading-none mb-1">Dosen
Pengampu</span>
<span @class([
'truncate font-medium',
'text-gray-900 dark:text-gray-200' => !$course->is_ongoing,
'text-primary-900 dark:text-primary-50' => $course->is_ongoing,
])>
{{ $course->lecturer_name }}
</span>
</div>
</div>
</div>
</div>
<div @class([
'flex items-center justify-end gap-2 p-4 pt-0 rounded-b-xl',
'bg-primary-100/30 dark:bg-primary-900/10 pt-4' => $course->is_ongoing,
])>
<span
class="text-[10px] font-bold text-primary-600 dark:text-primary-400 uppercase tracking-tight group-hover:underline">
Buka &rarr;
</span>
</div>
</div>
</a>
@endforeach
</div>
</x-filament::section>
</div>
</x-filament-panels::page>

View File

@ -0,0 +1,284 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full antialiased font-sans">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Presensi {{ $course->name }} - {{ config('app.name', 'MyClass') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap" rel="stylesheet" />
<!-- Styles -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
[x-cloak] {
display: none !important;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #fafafa;
color: #18181b;
}
.dark body {
background-color: #09090b;
color: #fafafa;
}
.card {
background-color: #ffffff;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
}
.dark .card {
background-color: #09090b;
border-color: #27272a;
}
.badge {
display: inline-flex;
align-items: center;
border-radius: 9999px;
border: 1px solid #e4e4e7;
padding-left: 0.625rem;
padding-right: 0.625rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
font-size: 0.75rem;
font-weight: 600;
}
.dark .badge {
border-color: #27272a;
}
.select-trigger {
display: flex;
height: 2.5rem;
width: 100%;
align-items: center;
justify-content: space-between;
border-radius: 0.375rem;
border: 1px solid #e4e4e7;
background-color: transparent;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
font-weight: 500;
}
.dark .select-trigger {
border-color: #27272a;
}
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-thumb {
background: #d4d4d8;
border-radius: 4px;
}
.dark ::-webkit-scrollbar-thumb {
background: #3f3f46;
}
</style>
</head>
<body class="min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="space-y-8 tracking-tight">
<!-- Header -->
<div
class="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-zinc-200 dark:border-zinc-800">
<div class="space-y-1">
<div class="flex items-center gap-2 mb-2">
<span
class="badge bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 capitalize tracking-tighter">{{ $course->code }}</span>
<span class="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">SEMESTER
GENAP</span>
</div>
<h1 class="text-3xl font-bold tracking-tighter">{{ $course->name }}</h1>
<p class="text-zinc-500 flex items-center gap-2 tracking-tight text-sm font-medium">
<x-heroicon-o-user class="w-4 h-4 opacity-50" />
{{ $course->lecturer->full_name }}
</p>
</div>
<div class="flex flex-col gap-1.5 min-w-[240px]">
<label for="date-selector"
class="text-[10px] font-bold text-zinc-500 ml-1 uppercase tracking-wider">Pilih Tanggal
Sesi</label>
<div x-data="{ date: '{{ $date }}' }" class="relative">
<select id="date-selector" x-model="date" @change="window.location.href = '?date=' + date"
class="select-trigger cursor-pointer appearance-none pr-10 hover:bg-zinc-50 dark:hover:bg-zinc-900/50 transition-colors">
@foreach ($availableDates as $availableDate)
<option value="{{ $availableDate->toDateString() }}"
{{ $date == $availableDate->toDateString() ? 'selected' : '' }}>
Pertemuan: {{ $availableDate->translatedFormat('d F Y') }}
</option>
@endforeach
@if ($availableDates->isEmpty())
<option value="{{ now()->toDateString() }}">Tidak ada riwayat</option>
@endif
</select>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-400">
<x-heroicon-m-chevron-up-down class="w-4 h-4" />
</div>
</div>
</div>
</div>
<!-- Main Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
<!-- Attendance Content -->
<div class="lg:col-span-8 space-y-6">
<div class="flex items-center justify-between mb-2 px-1">
<h2 class="text-lg font-bold tracking-tight uppercase tracking-tighter">Daftar Kehadiran
</h2>
<span
class="text-[11px] font-black text-zinc-400 uppercase tracking-widest">{{ $attendances->count() }}
Terdata</span>
</div>
<div class="card overflow-hidden shadow-sm">
<div class="overflow-x-auto">
<table class="w-full text-left text-sm whitespace-nowrap">
<thead class="border-b border-zinc-200 dark:border-zinc-800">
<tr>
<th
class="px-6 py-4 font-semibold text-zinc-900 dark:text-zinc-100 tracking-tight">
Mahasiswa</th>
<th
class="px-6 py-4 font-semibold text-zinc-900 dark:text-zinc-100 tracking-tight">
Nomor Induk</th>
<th
class="px-6 py-4 font-semibold text-zinc-900 dark:text-zinc-100 tracking-tight text-right">
Waktu Presensi</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800/50">
@forelse($attendances as $attendance)
<tr class="hover:bg-zinc-50/50 dark:hover:bg-zinc-900/10 transition-colors">
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<div
class="w-7 h-7 rounded-sm bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 flex items-center justify-center text-[9px] font-bold text-zinc-500 dark:text-zinc-400 shrink-0 uppercase">
{{ collect(explode(' ', $attendance->student->full_name))->map(fn($n) => substr($n, 0, 1))->take(2)->join('') }}
</div>
<div class="flex flex-col gap-0.5">
<span
class="font-semibold text-zinc-950 dark:text-zinc-50 tracking-tight">{{ $attendance->student->full_name }}</span>
</div>
</div>
</td>
<td class="px-6 py-5">
<span class="text-zinc-500 font-mono text-xs tracking-tight">
{{ $attendance->student->student_number }}
</span>
</td>
<td class="px-6 py-5 text-right">
<div
class="inline-flex items-center gap-2 px-2.5 py-1 rounded-md bg-emerald-50 dark:bg-emerald-950/30 text-emerald-700 dark:text-emerald-400 font-bold text-[10px] tracking-widest uppercase">
<span class="w-1 h-1 rounded-full bg-emerald-500"></span>
{{ $attendance->attended_at ? $attendance->attended_at->format('H:i') : '-' }}
WIB
</div>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-6 py-20 text-center">
<p
class="text-xs font-medium text-zinc-400 uppercase tracking-widest opacity-60">
Belum ada data kehadiran untuk sesi ini</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<!-- Sidebar Panels -->
<div class="lg:col-span-4 space-y-8">
<!-- Class Status -->
<div class="space-y-3">
<h3 class="text-xs font-bold text-zinc-400 uppercase tracking-widest pl-1">Informasi Sesi
</h3>
<div class="card p-6 space-y-5 shadow-sm">
<div class="flex flex-col gap-1">
<span class="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">Tanggal
Sesi</span>
<span
class="text-sm font-bold tracking-tight">{{ \Carbon\Carbon::parse($date)->translatedFormat('l, d F Y') }}</span>
</div>
<div class="h-px bg-zinc-100 dark:bg-zinc-800"></div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-1">
<span
class="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">Partisipan</span>
<span class="text-sm font-bold">{{ $attendances->count() }} Orang</span>
</div>
<div class="flex flex-col gap-1 text-right">
<span class="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">Jam
Kelas</span>
<span class="text-sm font-bold whitespace-nowrap">08:00 - 10:30</span>
</div>
</div>
</div>
</div>
<!-- Assignments -->
<div class="space-y-3">
<h3 class="text-xs font-bold text-zinc-400 uppercase tracking-widest pl-1">Tugas Terkait
</h3>
<div class="space-y-3">
@forelse($assignments as $assignment)
<div
class="card p-5 hover:bg-zinc-50 dark:hover:bg-zinc-900/40 transition-colors shadow-sm cursor-default">
<div class="flex flex-col gap-3">
<div class="flex items-start justify-between gap-3">
<h4
class="text-xs font-bold leading-tight tracking-tight uppercase line-clamp-2">
{{ $assignment->title }}</h4>
<span
class="shrink-0 text-[8px] font-black px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-800 rounded border border-zinc-200 dark:border-zinc-700 uppercase tracking-widest text-zinc-400">TGS</span>
</div>
<div
class="flex items-center gap-4 text-[10px] text-zinc-400 font-bold uppercase tracking-widest">
<div class="flex items-center gap-1.5">
<x-heroicon-m-calendar class="w-3 h-3 opacity-40 text-rose-500" />
<span>{{ $assignment->deadline ? $assignment->deadline->translatedFormat('d M') : 'N/A' }}</span>
</div>
<div class="flex items-center gap-1.5">
<x-heroicon-m-document-check
class="w-3 h-3 opacity-40 text-blue-500" />
<span>{{ $assignment->assignment_submissions_count }} Kirim</span>
</div>
</div>
</div>
</div>
@empty
<div
class="text-[10px] text-zinc-400 font-bold italic pl-1 uppercase tracking-widest opacity-50">
Tidak ada tugas terdaftar.
</div>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -5,3 +5,5 @@
Route::get('/', function () {
return view('welcome');
});
Route::get('/share/presensi/{token}', [\App\Http\Controllers\ShareAttendanceController::class, 'show'])->name('share.attendance');