feat: add share and copy assignment link actions to enhance assignment sharing capabilities

This commit is contained in:
Yoga Pangestu 2026-04-01 14:22:05 +07:00
parent 93085269e9
commit a4f615ad20
8 changed files with 76 additions and 56 deletions

View File

@ -1,9 +1,9 @@
<?php
namespace App\Filament\Resources\Learning\ClassSessions\Actions;
namespace App\Filament\Resources\Learning\Assignments\Actions;
use App\Filament\Support\SystemNotification;
use App\Models\ClassSession;
use App\Models\Assignment;
use Filament\Actions\Action;
use Illuminate\Support\Facades\URL;
@ -22,12 +22,16 @@ protected function setUp(): void
->color('gray')
->icon('heroicon-o-clipboard-document')
->action(function (array $arguments, $livewire) {
$course = $livewire->course;
$session = ClassSession::find($arguments['session'] ?? null);
$assignment = Assignment::find($arguments['record'] ?? null);
$course = $assignment ? $assignment->course : null;
if (! $course || ! $assignment) {
return;
}
$url = URL::temporarySignedRoute('share.assignment', now()->addHour(), [
'course' => $course->id,
'session_id' => $session ? $session->id : null,
'assignment_id' => $assignment->id,
]);
$livewire->js("if (navigator.clipboard) { navigator.clipboard.writeText('{$url}').catch(() => {}); }");

View File

@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\Learning\ClassSessions\Actions;
namespace App\Filament\Resources\Learning\Assignments\Actions;
use App\Models\ClassSession;
use App\Models\Assignment;
use Filament\Actions\Action;
use Filament\Support\Colors\Color;
use Illuminate\Support\Facades\URL;
@ -25,22 +25,26 @@ protected function setUp(): void
->requiresConfirmation()
->modalIcon('heroicon-o-paper-airplane')
->modalHeading('Bagikan Info Tugas')
->modalDescription('Pilih metode untuk membagikan tautan publik status pengumpulan tugas sesi ini.')
->modalDescription('Pilih metode untuk membagikan tautan publik status pengumpulan tugas ini.')
->modalSubmitActionLabel('Ke WhatsApp')
->modalCancelAction(false)
->extraModalFooterActions([
CopyAssignmentLinkAction::make(),
])
->action(function (array $arguments, $livewire) {
$course = $livewire->course;
$session = ClassSession::find($arguments['session'] ?? null);
$assignment = Assignment::with(['course'])->find($arguments['record'] ?? null);
$course = $assignment ? $assignment->course : null;
if (! $course || ! $assignment) {
return;
}
$url = URL::temporarySignedRoute('share.assignment', now()->addHour(), [
'course' => $course->id,
'session_id' => $session ? $session->id : null,
'assignment_id' => $assignment->id,
]);
$text = "*Info Tugas Kelas {$course->name}*\nSesi ke-".($session->session_number ?? '-').' ('.($session->date->translatedFormat('d M Y') ?? '').")\n\nSilakan cek status pengumpulan tugas melalui tautan publik berikut:\n\n{$url}";
$text = "*Info Tugas Kelas {$course->name}*\nJudul: {$assignment->title}\n\nSilakan cek status pengumpulan tugas melalui tautan publik berikut:\n\n{$url}";
$escapedText = json_encode($text);

View File

@ -7,6 +7,7 @@
use App\Filament\Resources\Learning\Assignments\Actions\DeleteAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\EditAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\PinAction;
use App\Filament\Resources\Learning\Assignments\Actions\ShareAssignmentAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource;
use App\Filament\Support\SystemNotification;
use App\Models\Assignment;
@ -96,6 +97,11 @@ public function deleteAssignmentAction(): Action
->visible(fn () => auth()->user()->can('Delete:Assignment'));
}
public function shareAssignmentAction(): Action
{
return ShareAssignmentAction::make();
}
#[Computed]
public function assignments(): Collection
{

View File

@ -6,7 +6,6 @@
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\ShareAssignmentAction;
use App\Filament\Resources\Learning\ClassSessions\Actions\ShareAttendanceAction;
use App\Filament\Resources\Learning\ClassSessions\ClassSessionResource;
use App\Models\ClassSession;
@ -140,7 +139,6 @@ protected function getActions(): array
{
return [
$this->shareAttendanceAction(),
$this->shareAssignmentAction(),
$this->viewAttendanceAction(),
$this->viewMaterialsAction(),
$this->viewAssignmentsAction(),
@ -155,11 +153,6 @@ public function shareAttendanceAction(): Action
return ShareAttendanceAction::make();
}
public function shareAssignmentAction(): Action
{
return ShareAssignmentAction::make();
}
public function viewAttendanceAction(): Action
{
return Action::make('viewAttendance')

View File

@ -12,74 +12,74 @@
</div>
</div>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
@forelse ($this->assignmentCards as $card)
<div class="{{ $card->card_classes }} flex-col sm:flex-row">
<div class="{{ $card->card_classes }} flex-col">
<a href="{{ \App\Filament\Resources\Learning\Assignments\AssignmentResource::getUrl('submit', ['record' => $card->id]) }}"
class="flex flex-1 items-start gap-3 p-4 sm:p-5 text-left min-w-0 hover:bg-primary-500/5 transition-colors cursor-pointer">
class="flex flex-1 items-start gap-4 p-5 text-left min-w-0 hover:bg-primary-500/5 transition-colors cursor-pointer">
<div class="{{ $card->icon_wrapper_classes }} mt-0.5 shrink-0">
<x-filament::icon :icon="$card->status_icon" class="h-5 w-5 sm:h-6 sm:w-6" />
<div class="{{ $card->icon_wrapper_classes }} h-12 w-12 mt-1 shrink-0">
<x-filament::icon :icon="$card->status_icon" class="h-6 w-6" />
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-gray-900 dark:text-white text-sm sm:text-base leading-snug">
<p class="font-bold text-gray-900 dark:text-white text-lg leading-tight">
{{ $card->title }}
</p>
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400 mt-0.5 truncate">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mt-1 truncate">
{{ $card->course_name }}
</p>
<div class="flex flex-wrap items-center gap-1.5 mt-2">
<div class="flex flex-wrap items-center gap-2 mt-3">
@if ($card->is_pinned)
<span
class="inline-flex items-center gap-1 rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400 px-2 py-0.5 text-xs font-semibold whitespace-nowrap">
class="inline-flex items-center gap-1.5 rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400 px-2.5 py-1 text-xs font-bold whitespace-nowrap border border-primary-200/50 dark:border-primary-500/20">
📌 Dipinned
</span>
@endif
@if ($card->is_new)
<span
class="inline-flex items-center gap-1 rounded-full bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400 px-2 py-0.5 text-xs font-semibold whitespace-nowrap">
class="inline-flex items-center gap-1.5 rounded-full bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400 px-2.5 py-1 text-xs font-bold whitespace-nowrap border border-violet-200/50 dark:border-violet-500/20">
Baru
</span>
@endif
@if ($card->is_group)
<span
class="inline-flex items-center gap-1 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 px-2 py-0.5 text-xs font-semibold whitespace-nowrap">
class="inline-flex items-center gap-1.5 rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 px-2.5 py-1 text-xs font-bold whitespace-nowrap border border-blue-200/50 dark:border-blue-500/20">
👥 Kelompok
</span>
@endif
<span class="{{ $card->status_badge_classes }} whitespace-nowrap">
<span class="{{ $card->status_badge_classes }} px-2.5 py-1 whitespace-nowrap">
@if ($card->status_icon)
<x-filament::icon :icon="$card->status_icon" class="h-3 w-3" />
<x-filament::icon :icon="$card->status_icon" class="h-4 w-4" />
@endif
{{ $card->status_label }}
</span>
@if ($card->is_urgent)
<span
class="inline-flex items-center gap-1 rounded-full bg-danger-500 text-white px-2 py-0.5 text-xs font-semibold animate-pulse whitespace-nowrap">
class="inline-flex items-center gap-1.5 rounded-full bg-danger-500 text-white px-2.5 py-1 text-xs font-bold animate-pulse whitespace-nowrap shadow-sm shadow-danger-300 dark:shadow-none">
🔥 Segera!
</span>
@endif
</div>
<div class="flex flex-wrap gap-x-3 gap-y-1 mt-2 text-xs text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-1">
<x-filament::icon icon="heroicon-o-clock" class="h-3.5 w-3.5 shrink-0" />
<span>Batas: {{ $card->due_date_formatted }}</span>
<div class="flex flex-wrap gap-x-4 gap-y-2 mt-4 text-xs text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-1.5">
<x-filament::icon icon="heroicon-o-clock" class="h-4 w-4 shrink-0 text-primary-500" />
<span class="font-medium">Batas: {{ $card->due_date_formatted }}</span>
</span>
@if ($card->submitted_at_formatted)
<span class="flex items-center gap-1 text-success-600 dark:text-success-400">
<span class="flex items-center gap-1.5 text-success-600 dark:text-success-400 font-bold bg-success-50 dark:bg-success-900/25 px-2 py-0.5 rounded-lg">
<x-filament::icon icon="heroicon-o-arrow-up-tray"
class="h-3.5 w-3.5 shrink-0" />
class="h-4 w-4 shrink-0" />
<span>Dikumpulkan: {{ $card->submitted_at_formatted }}</span>
</span>
@endif
@ -88,9 +88,9 @@ class="h-3.5 w-3.5 shrink-0" />
</a>
<div
class="flex items-center justify-start gap-2 px-4 py-2 border-t border-gray-100 dark:border-gray-700 sm:flex-col sm:items-start sm:justify-center sm:border-t-0 sm:border-l sm:px-2 sm:py-3 sm:shrink-0">
<div class="flex flex-wrap items-center gap-3 px-5 py-4 border-t border-gray-100 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/20">
{{ ($this->pinAction)(['record' => $card->id]) }}
{{ ($this->shareAssignmentAction)(['record' => $card->id]) }}
@canAny(['Update:Assignment', 'Delete:Assignment'])
{{ ($this->editAssignmentAction)(['record' => $card->id]) }}
@ -102,11 +102,13 @@ class="flex items-center justify-start gap-2 px-4 py-2 border-t border-gray-100
</div>
@empty
<x-filament::empty-state icon="heroicon-o-clipboard-document-list"
heading="{{ \App\Filament\Support\SystemNotification::getMessage('Hore, Belum Ada Tugas! 🎊✨', 'Tidak ada data yang ditemukan') }}"
description="{{ \App\Filament\Support\SystemNotification::getMessage('Belum ada tugas baru yang perlu dikerjakan. Hidup tenang tanpa beban tugas itu asik ya! 😊🌈', 'Belum ada tugas baru yang perlu dikerjakan untuk saat ini. Tetap semangat!') }}"
iconColor="gray">
</x-filament::empty-state>
<div class="col-span-full">
<x-filament::empty-state icon="heroicon-o-clipboard-document-list"
heading="{{ \App\Filament\Support\SystemNotification::getMessage('Hore, Belum Ada Tugas! 🎊✨', 'Tidak ada data yang ditemukan') }}"
description="{{ \App\Filament\Support\SystemNotification::getMessage('Belum ada tugas baru yang perlu dikerjakan. Hidup tenang tanpa beban tugas itu asik ya! 😊🌈', 'Belum ada tugas baru yang perlu dikerjakan untuk saat ini. Tetap semangat!') }}"
iconColor="gray">
</x-filament::empty-state>
</div>
@endforelse
</div>
</x-filament::section>

View File

@ -55,7 +55,6 @@
<div class="flex flex-wrap items-center gap-3 px-4 py-3 border-t border-gray-100 dark:border-gray-700">
{{ ($this->shareAttendanceAction)(['session' => $session->id]) }}
{{ ($this->shareAssignmentAction)(['session' => $session->id]) }}
{{ ($this->editSessionAction)(['session' => $session->id]) }}
{{ ($this->deleteSessionAction)(['session' => $session->id]) }}
</div>

View File

@ -300,6 +300,27 @@
});
});
describe('Assignment Sharing', function () {
beforeEach(function () {
$user = User::factory()->create();
$user->assignRole(RoleEnum::Student);
$user->givePermissionTo(['ViewAny:Assignment', 'View:CourseSchedule']);
$this->studentProfile = Student::factory()->create(['user_id' => $user->id]);
$this->actingAs($user);
$this->currentSemester = app(GeneralSettings::class)->current_semester;
});
it('can share assignment information', function () {
$course = Course::factory()->create(['semester' => $this->currentSemester]);
$assignment = Assignment::factory()->create(['course_id' => $course->id]);
$assignment->students()->attach($this->studentProfile->id);
Livewire::test(ListAssignments::class)
->callAction('shareAssignment', [], ['record' => $assignment->id])
->assertHasNoActionErrors();
});
});
describe('Assignment Details and Preview', function () {
beforeEach(function () {
$user = User::factory()->create();

View File

@ -158,15 +158,6 @@
->assertHasNoActionErrors();
});
it('can share assignment information', function () {
$course = Course::factory()->create(['semester' => $this->currentSemester]);
$session = ClassSession::factory()->create(['course_id' => $course->id]);
Livewire::test(ListCourseSessions::class, ['courseId' => $course->id])
->callAction('shareAssignment', [], ['session' => $session->id])
->assertHasNoActionErrors();
});
it('can show modals for attendance, materials and assignments', function () {
$course = Course::factory()->create(['semester' => $this->currentSemester]);
$session = ClassSession::factory()->create(['course_id' => $course->id]);