feat: add 'mark as sent' functionality for class sessions with UI updates and notifications

This commit is contained in:
Yoga Pangestu 2026-04-07 10:43:12 +07:00
parent 2d36e961ba
commit 889e3bc510
14 changed files with 172 additions and 10 deletions

View File

@ -25,7 +25,7 @@ protected function setUp(): void
->icon(function (array $arguments, ?Assignment $record) {
$assignment = $record ?? Assignment::find($arguments['record'] ?? null);
return $assignment?->is_sent_to_lecturer ? 'heroicon-o-lock-open' : 'heroicon-o-check-circle';
return $assignment?->is_sent_to_lecturer ? 'heroicon-o-lock-open' : 'heroicon-o-paper-airplane';
})
->color(function (array $arguments, ?Assignment $record) {
$assignment = $record ?? Assignment::find($arguments['record'] ?? null);

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\Learning\Assignments\Pages;
use App\Enums\AssignmentType;
use App\Enums\NotifStyle;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Learning\Assignments\Actions\MarkAsSentAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource;
@ -181,7 +182,7 @@ public function assignmentSummary(): array
#[Computed]
public function sectionIcon(): string
{
return SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful
return SystemNotification::getNotifStyle() === NotifStyle::Cheerful
? 'heroicon-o-document-magnifying-glass'
: 'heroicon-o-document-text';
}

View File

@ -99,7 +99,14 @@ public function getSchedules()
}
return ClassSession::query()
->whereDate('date', now())
->where(function ($query) use ($student) {
$query->whereDate('date', now())
->orWhere(function ($q) use ($student) {
$q->whereDate('date', '<', now())
->where('is_sent_to_lecturer', false)
->whereDoesntHave('attendances', fn ($aq) => $aq->where('student_id', $student->id));
});
})
->with(['course', 'attendances' => function ($q) use ($student) {
$q->where('student_id', $student->id);
}])
@ -114,10 +121,11 @@ public function scheduleCards()
->map(function ($schedule) {
$attendance = $schedule->attendances?->first();
$isAttended = $attendance !== null;
$isSent = $schedule->is_sent_to_lecturer;
$now = now();
$startTime = now()->setTimeFrom($schedule->start_time);
$canAttend = $now->greaterThanOrEqualTo($startTime);
$canAttend = $now->greaterThanOrEqualTo($startTime) && ! $isSent;
$statusLabel = 'Belum Presensi';
$statusColor = 'warning';
@ -127,6 +135,10 @@ public function scheduleCards()
$statusLabel = 'Hadir';
$statusColor = 'success';
$statusIcon = 'heroicon-o-check-circle';
} elseif ($isSent) {
$statusLabel = 'Terkirim';
$statusColor = 'danger';
$statusIcon = 'heroicon-o-paper-airplane';
} elseif (! $canAttend) {
$statusLabel = 'Belum Dimulai';
$statusColor = 'gray';
@ -139,6 +151,7 @@ public function scheduleCards()
'lecturer_name' => $schedule->course?->lecturer ?? 'Belum Ditentukan',
'time_range' => $schedule->start_time?->format('H:i').' - '.$schedule->end_time?->format('H:i'),
'is_attended' => $isAttended,
'is_sent_to_lecturer' => $isSent,
'can_attend' => $canAttend && ! $isAttended,
'status_label' => $statusLabel,
'status_color' => $statusColor,
@ -210,6 +223,14 @@ public function attend(int $sessionId): void
return;
}
// Check session status
if ($session->is_sent_to_lecturer) {
SystemNotification::send('attendance_closed', type: 'danger')
->send();
return;
}
// Check time
$startTime = $session->start_time;
if (now()->lessThan($startTime)) {

View File

@ -0,0 +1,56 @@
<?php
namespace App\Filament\Resources\Learning\ClassSessions\Actions;
use App\Filament\Support\SystemNotification;
use App\Models\ClassSession;
use Filament\Actions\Action;
class MarkAsSentAction extends Action
{
public static function getDefaultName(): ?string
{
return 'markAsSentAction';
}
protected function setUp(): void
{
parent::setUp();
$this->label(function (array $arguments, ?ClassSession $record) {
$session = $record ?? ClassSession::find($arguments['record'] ?? null);
return $session?->is_sent_to_lecturer ? 'Buka Kunci' : 'Tandai Terkirim';
})
->icon(function (array $arguments, ?ClassSession $record) {
$session = $record ?? ClassSession::find($arguments['record'] ?? null);
return $session?->is_sent_to_lecturer ? 'heroicon-o-lock-open' : 'heroicon-o-paper-airplane';
})
->color(function (array $arguments, ?ClassSession $record) {
$session = $record ?? ClassSession::find($arguments['record'] ?? null);
return $session?->is_sent_to_lecturer ? 'warning' : 'success';
})
->link()
->visible(fn () => auth()->user()->hasRole(['Kosma', 'Developer']))
->action(function (array $arguments, $livewire, ?ClassSession $record) {
$session = $record ?? ClassSession::find($arguments['record'] ?? null);
if (! $session) {
return;
}
$session->update([
'is_sent_to_lecturer' => ! $session->is_sent_to_lecturer,
]);
SystemNotification::send($session->is_sent_to_lecturer ? 'session_sent' : 'session_unlocked')
->send();
if (isset($livewire->sessions)) {
unset($livewire->sessions);
}
});
}
}

View File

@ -6,6 +6,7 @@
use App\Filament\Resources\Learning\ClassSessions\Actions\DeleteSessionAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\EditSessionAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\GenerateSessionsAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\MarkAsSentAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\ShareAttendanceAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\ViewAssignmentsAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\ViewAttendanceAction;
@ -60,6 +61,7 @@ public function sessions(): Collection
'attendance_percentage' => $totalActiveStudents > 0 ? round(($session->attendances_count / $totalActiveStudents) * 100) : 0,
'materials_count' => $session->materials_count,
'assignments_count' => $session->assignments_count,
'is_sent_to_lecturer' => $session->is_sent_to_lecturer,
]);
}
@ -114,6 +116,11 @@ public function editSessionAction(): EditSessionAction
return EditSessionAction::make();
}
public function markAsSentAction(): MarkAsSentAction
{
return MarkAsSentAction::make();
}
public function deleteSessionAction(): DeleteSessionAction
{
return DeleteSessionAction::make();

View File

@ -15,6 +15,7 @@
use Filament\Forms\Contracts\HasForms;
use Filament\Resources\Pages\Page;
use Filament\Schemas\Schema;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
@ -85,13 +86,21 @@ public function todaySessions(): Collection
'attendance_percentage' => $percentage,
'materials_count' => $session->materials_count,
'assignments_count' => $session->assignments_count,
'is_sent_to_lecturer' => $session->is_sent_to_lecturer,
'url' => ClassSessionResource::getUrl('course', ['course' => $session->course]),
// Pre-calculated classes
'status_label' => $session->is_sent_to_lecturer ? 'Terkirim' : $session->time_range,
'status_icon' => $session->is_sent_to_lecturer ? 'heroicon-s-paper-airplane' : 'heroicon-s-clock',
// Pre-calculated classes
'card_classes' => 'group flex w-full rounded-xl border border-primary-300 dark:border-primary-700 bg-primary-50/30 dark:bg-primary-900/10 shadow-sm overflow-hidden relative transition-all hover:shadow-md',
'session_badge_classes' => 'flex h-12 w-12 shrink-0 flex-col items-center justify-center rounded-lg font-bold border bg-primary-100 dark:bg-primary-800 text-primary-700 dark:text-primary-300 border-primary-200 dark:border-primary-700',
'title_classes' => 'font-bold transition-colors text-gray-900 dark:text-white group-hover:text-primary-600',
'status_badge_classes' => 'flex items-center gap-1.5 rounded-full px-3 py-1 text-[11px] font-bold shadow-sm bg-primary-500 text-white animate-pulse',
'status_badge_classes' => Arr::toCssClasses([
'flex items-center gap-1.5 rounded-full px-3 py-1 text-[11px] font-bold shadow-sm text-white',
'bg-primary-500 animate-pulse' => ! $session->is_sent_to_lecturer,
'bg-danger-500' => $session->is_sent_to_lecturer,
]),
'attendance_section_classes' => 'flex flex-col items-center justify-center border-l border-primary-200 dark:border-primary-800 p-2 bg-white/30 dark:bg-black/10 transition-colors group-hover:bg-primary-500/5',
];
});

View File

@ -28,6 +28,7 @@ protected function casts(): array
'end_time' => 'datetime',
'session_number' => 'integer',
'start_time' => 'datetime',
'is_sent_to_lecturer' => 'boolean',
];
}

View File

@ -11,7 +11,7 @@
*/
public function up(): void
{
if (!Schema::hasColumn('assignments', 'is_sent_to_lecturer')) {
if (! Schema::hasColumn('assignments', 'is_sent_to_lecturer')) {
Schema::table('assignments', function (Blueprint $table) {
$table->boolean('is_sent_to_lecturer')->default(false)->after('due_date');
});

View File

@ -0,0 +1,30 @@
<?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
{
if (! Schema::hasColumn('class_sessions', 'is_sent_to_lecturer')) {
Schema::table('class_sessions', function (Blueprint $table) {
$table->boolean('is_sent_to_lecturer')->default(false)->after('end_time');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('class_sessions', function (Blueprint $table) {
$table->dropColumn('is_sent_to_lecturer');
});
}
};

View File

@ -157,6 +157,18 @@
'title' => 'Tugas Terkunci! 🔒',
'body' => 'Mohon maaf, tugas ini sudah ditandai terkirim ke dosen oleh Kosma dan tidak dapat diubah lagi. 🚫',
],
'session_sent' => [
'title' => 'Sesi Terkirim! 🚀✨',
'body' => 'Laporan presensi untuk sesi ini telah resmi ditandai terkirim ke dosen. Data aman tersimpan! ✅',
],
'session_unlocked' => [
'title' => 'Kunci Presensi Dibuka! 🔓✨',
'body' => 'Mahasiswa sekarang bisa melakukan presensi kembali untuk sesi ini. 📝',
],
'attendance_closed' => [
'title' => 'Yah, Presensi Sudah Tutup! 😭⌛',
'body' => 'Maaf ya, sesi presensi ini sudah ditutup oleh Kosma. Lain kali jangan sampai terlambat ya! 💪',
],
// UI Labels & Descriptions
'labels' => [
@ -461,6 +473,18 @@
'title' => 'Akses Pengumpulan Ditutup',
'body' => 'Tugas ini telah dalam status final (terkirim ke dosen). Perubahan data tidak lagi dimungkinkan.',
],
'session_sent' => [
'title' => 'Konfirmasi Pengiriman Laporan',
'body' => 'Laporan presensi sesi perkuliahan telah berhasil ditandai sebagai terkirim ke dosen pengampu.',
],
'session_unlocked' => [
'title' => 'Kunci Akses Presensi Dibuka',
'body' => 'Otoritas presensi telah diaktifkan kembali. Seluruh mahasiswa terkait dapat melakukan pengisian data kehadiran.',
],
'attendance_closed' => [
'title' => 'Akses Presensi Ditutup',
'body' => 'Sesi presensi untuk mata kuliah ini telah ditutup secara resmi. Anda tidak dapat melakukan pengisian data kehadiran lagi.',
],
// UI Labels & Descriptions
'labels' => [

View File

@ -89,6 +89,13 @@ class="text-[10px] font-bold text-primary-700 dark:text-primary-200 uppercase tr
Selesai
</span>
</div>
@elseif ($card->is_sent_to_lecturer)
<div class="px-2 py-1 flex items-center gap-1.5">
<x-heroicon-m-paper-airplane class="w-3 h-3 text-danger-500" />
<span
class="text-[10px] font-bold text-danger-500 uppercase tracking-widest leading-none italic">Sesi
Terkirim</span>
</div>
@else
<div class="px-2 py-1">
<span

View File

@ -34,8 +34,8 @@
</div>
<div class="{{ $session->status_badge_classes }} shrink-0 self-start">
<x-heroicon-s-clock class="w-3.5 h-3.5" />
{{ $session->time_range }}
<x-filament::icon :icon="$session->status_icon" class="w-3.5 h-3.5" />
{{ $session->status_label }}
</div>
</div>

View File

@ -40,6 +40,12 @@ class="text-[10px] opacity-60">/{{ $session->total_students }}</span>
</div>
<span
class="text-[10px] font-bold text-gray-500">{{ $session->attendance_percentage }}%</span>
@if ($session->is_sent_to_lecturer)
<span class="inline-flex items-center rounded-md bg-danger-50 dark:bg-danger-500/10 px-1.5 py-0.5 text-[10px] font-bold text-danger-700 dark:text-danger-400 ring-1 ring-inset ring-danger-600/20">
Terkirim
</span>
@endif
</div>
</div>
@ -60,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">
{{ ($this->viewAttendanceAction)(['session' => $session->id]) }}
{{ ($this->shareAttendanceAction)(['session' => $session->id]) }}
{{ ($this->markAsSentAction)(['record' => $session->id]) }}
{{ ($this->editSessionAction)(['session' => $session->id]) }}
{{ ($this->deleteSessionAction)(['session' => $session->id]) }}
</div>

View File

@ -433,4 +433,3 @@
->assertDontSeeHtml('type="file"');
});
});