feat: enhance assignment submission process with improved notifications, form handling, and UI updates

This commit is contained in:
Yoga Pangestu 2026-04-04 01:35:30 +07:00
parent 506bfd822f
commit ba0cadf4f9
9 changed files with 241 additions and 147 deletions

View File

@ -34,7 +34,9 @@ protected function setUp(): void
'assignment_id' => $assignment->id, 'assignment_id' => $assignment->id,
]); ]);
$livewire->js("if (navigator.clipboard) { navigator.clipboard.writeText('{$url}').catch(() => {}); }"); $escapedUrl = json_encode($url);
$livewire->js("if (navigator.clipboard) { navigator.clipboard.writeText({$escapedUrl}).catch(() => {}); }");
SystemNotification::send('link_copied') SystemNotification::send('link_copied')
->send(); ->send();

View File

@ -28,8 +28,9 @@ protected function setUp(): void
->modalDescription('Pilih metode untuk membagikan tautan publik status pengumpulan tugas ini.') ->modalDescription('Pilih metode untuk membagikan tautan publik status pengumpulan tugas ini.')
->modalSubmitActionLabel('Ke WhatsApp') ->modalSubmitActionLabel('Ke WhatsApp')
->modalCancelAction(false) ->modalCancelAction(false)
->extraModalFooterActions([ ->extraModalFooterActions(fn (Action $action): array => [
CopyAssignmentLinkAction::make(), CopyAssignmentLinkAction::make()
->arguments($action->getArguments()),
]) ])
->action(function (array $arguments, $livewire) { ->action(function (array $arguments, $livewire) {
$assignment = Assignment::with(['course'])->find($arguments['record'] ?? null); $assignment = Assignment::with(['course'])->find($arguments['record'] ?? null);

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\Learning\Assignments\Pages; namespace App\Filament\Resources\Learning\Assignments\Pages;
use App\Enums\AssignmentType; use App\Enums\AssignmentType;
use App\Enums\NotifStyle;
use App\Filament\Resources\Learning\Assignments\Actions\CreateAssignmentAction; use App\Filament\Resources\Learning\Assignments\Actions\CreateAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\DeleteAssignmentAction; use App\Filament\Resources\Learning\Assignments\Actions\DeleteAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\EditAssignmentAction; use App\Filament\Resources\Learning\Assignments\Actions\EditAssignmentAction;
@ -100,6 +101,26 @@ public function description(): string
return SystemNotification::getByKey('labels.assignment_list.description'); return SystemNotification::getByKey('labels.assignment_list.description');
} }
#[Computed]
public function emptyStateHeading(): string
{
return SystemNotification::getByKey('labels.empty_assignment.title');
}
#[Computed]
public function emptyStateDescription(): string
{
return SystemNotification::getByKey('labels.empty_assignment.description');
}
#[Computed]
public function icon(): string
{
return SystemNotification::getNotifStyle() === NotifStyle::Cheerful
? 'heroicon-o-clipboard-document-list'
: 'heroicon-o-briefcase';
}
public function pinAction(): Action public function pinAction(): Action
{ {
return PinAction::make(); return PinAction::make();
@ -245,20 +266,20 @@ public function assignmentCards()
$isNew = $assignment->created_at->diffInDays(now()) <= 3; $isNew = $assignment->created_at->diffInDays(now()) <= 3;
$isPinned = in_array($assignment->id, $pinnedIds); $isPinned = in_array($assignment->id, $pinnedIds);
$statusLabel = 'Belum Dikumpulkan'; $statusLabel = SystemNotification::getByKey('labels.assignment_status.not_submitted');
$statusColor = 'warning'; $statusColor = 'warning';
$statusIcon = 'heroicon-o-arrow-up-tray'; $statusIcon = 'heroicon-o-arrow-up-tray';
if ($isSubmitted) { if ($isSubmitted) {
$statusLabel = '✓ Sudah Dikumpulkan'; $statusLabel = SystemNotification::getByKey('labels.assignment_status.submitted');
$statusColor = 'success'; $statusColor = 'success';
$statusIcon = 'heroicon-o-check-circle'; $statusIcon = 'heroicon-o-check-circle';
} elseif ($isOverdue) { } elseif ($isOverdue) {
$statusLabel = '⏰ Waktu Habis'; $statusLabel = SystemNotification::getByKey('labels.assignment_status.overdue');
$statusColor = 'danger'; $statusColor = 'danger';
$statusIcon = 'heroicon-o-clock'; $statusIcon = 'heroicon-o-clock';
} elseif ($isGroup && ! $isLeader && ! $isSubmitted) { } elseif ($isGroup && ! $isLeader && ! $isSubmitted) {
$statusLabel = 'Menunggu Ketua'; $statusLabel = SystemNotification::getByKey('labels.assignment_status.waiting_leader');
$statusColor = 'gray'; $statusColor = 'gray';
$statusIcon = 'heroicon-o-user-group'; $statusIcon = 'heroicon-o-user-group';
} }
@ -279,6 +300,7 @@ public function assignmentCards()
'is_pinned' => $isPinned, 'is_pinned' => $isPinned,
'status_label' => $statusLabel, 'status_label' => $statusLabel,
'status_icon' => $statusIcon, 'status_icon' => $statusIcon,
'url' => AssignmentResource::getUrl('submit', ['record' => $assignment->id]),
// Pre-calculated classes // Pre-calculated classes
'card_classes' => Arr::toCssClasses([ 'card_classes' => Arr::toCssClasses([
'group flex w-full rounded-xl border transition-all overflow-hidden relative', 'group flex w-full rounded-xl border transition-all overflow-hidden relative',

View File

@ -3,23 +3,28 @@
namespace App\Filament\Resources\Learning\Assignments\Pages; namespace App\Filament\Resources\Learning\Assignments\Pages;
use App\Enums\AssignmentType; use App\Enums\AssignmentType;
use App\Enums\NotifStyle;
use App\Filament\Actions\BackAction; use App\Filament\Actions\BackAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource; use App\Filament\Resources\Learning\Assignments\AssignmentResource;
use App\Filament\Resources\Learning\Assignments\Schemas\SubmitAssignmentForm;
use App\Filament\Support\SystemNotification; use App\Filament\Support\SystemNotification;
use App\Models\Assignment; use App\Models\Assignment;
use App\Models\AssignmentSubmission; use App\Models\AssignmentSubmission;
use App\Models\Student; use App\Models\Student;
use App\Models\StudyGroup; use App\Models\StudyGroup;
use App\Models\User; use App\Models\User;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Resources\Pages\Page; use Filament\Resources\Pages\Page;
use Filament\Schemas\Schema;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\WithFileUploads; use Livewire\WithFileUploads;
class SubmitAssignmentPage extends Page class SubmitAssignmentPage extends Page implements HasForms
{ {
use WithFileUploads; use InteractsWithForms, WithFileUploads;
protected static string $resource = AssignmentResource::class; protected static string $resource = AssignmentResource::class;
@ -29,7 +34,24 @@ class SubmitAssignmentPage extends Page
public Assignment $record; public Assignment $record;
public $file = null; public array $data = [];
public $file;
public function mount(Assignment $record): void
{
$this->record = $record;
$this->form->fill([
'is_resubmit' => $this->isResubmit,
]);
}
public function form(Schema $schema): Schema
{
return SubmitAssignmentForm::configure($schema)
->statePath('data');
}
#[Computed] #[Computed]
public function statusCards(): array public function statusCards(): array
@ -135,17 +157,75 @@ public function submissionStatus(): object
return (object) [ return (object) [
'type' => 'submitted', 'type' => 'submitted',
'badge_color' => 'success', 'badge_color' => 'success',
'badge_label' => 'Sudah Dikumpulkan', 'badge_label' => SystemNotification::getByKey('assignment_status.submitted'),
]; ];
} }
return (object) [ return (object) [
'type' => 'none', 'type' => 'none',
'badge_color' => 'danger', 'badge_color' => 'danger',
'badge_label' => 'Tidak Mengumpulkan', 'badge_label' => SystemNotification::getByKey('assignment_status.not_submitted'),
]; ];
} }
#[Computed]
public function overdueHeading(): string
{
return SystemNotification::getByKey('submission_overdue.title');
}
#[Computed]
public function overdueDescription(): string
{
return SystemNotification::getByKey('submission_overdue.body');
}
#[Computed]
public function submissionHeading(): string
{
return $this->isResubmit
? SystemNotification::getByKey('submission_update.title')
: SystemNotification::getByKey('submission_create.title');
}
#[Computed]
public function submissionDescription(): string
{
return $this->isResubmit
? SystemNotification::getByKey('submission_update.body')
: SystemNotification::getByKey('submission_create.body');
}
#[Computed]
public function isCheerful(): bool
{
return SystemNotification::getNotifStyle() === NotifStyle::Cheerful;
}
#[Computed]
public function groupHintTitle(): string
{
return SystemNotification::getByKey('labels.group_submission_hint.title');
}
#[Computed]
public function groupHintDescription(): string
{
return SystemNotification::getByKey('labels.group_submission_hint.description');
}
#[Computed]
public function overdueIcon(): string
{
return $this->isCheerful ? 'heroicon-o-exclamation-triangle' : 'heroicon-o-lock-closed';
}
#[Computed]
public function submissionIcon(): string
{
return $this->isCheerful ? 'heroicon-o-paper-airplane' : 'heroicon-o-document-arrow-up';
}
public function getSubmissionFileUrl(): ?string public function getSubmissionFileUrl(): ?string
{ {
return $this->existingSubmission?->getFirstMediaUrl('submission'); return $this->existingSubmission?->getFirstMediaUrl('submission');
@ -171,32 +251,19 @@ public function submit(): void
} }
$existingSubmission = $this->existingSubmission; $existingSubmission = $this->existingSubmission;
$state = $this->form->getState();
if (! $existingSubmission && ! $this->file) { if (! $existingSubmission && empty($state['file'])) {
SystemNotification::send('submission_file_missing', type: 'warning') SystemNotification::send('submission_file_missing', type: 'warning')
->send(); ->send();
return; return;
} }
$this->validate([
'file' => [
'nullable',
'file',
'max:'.(1024 * 5),
'mimes:pdf',
],
], [
'file.max' => 'Ukuran file maksimal 5MB.',
'file.mimes' => 'Hanya file PDF yang diizinkan.',
]);
if ($existingSubmission) { if ($existingSubmission) {
if ($this->file) { if ($state['file']) {
$existingSubmission->clearMediaCollection('submission'); $existingSubmission->clearMediaCollection('submission');
$existingSubmission->addMedia($this->file->getRealPath()) $existingSubmission->addMediaFromDisk($state['file'], config('filesystems.default'))
->usingName($this->file->getClientOriginalName())
->usingFileName($this->file->getClientOriginalName())
->toMediaCollection('submission'); ->toMediaCollection('submission');
} }
@ -214,16 +281,18 @@ public function submit(): void
'submitted_at' => now(), 'submitted_at' => now(),
]); ]);
$submission->addMedia($this->file->getRealPath()) if ($state['file']) {
->usingName($this->file->getClientOriginalName()) $submission->addMediaFromDisk($state['file'], config('filesystems.default'))
->usingFileName($this->file->getClientOriginalName()) ->toMediaCollection('submission');
->toMediaCollection('submission'); }
SystemNotification::send('submission_success') SystemNotification::send('submission_success')
->send(); ->send();
} }
$this->file = null; $this->form->fill([
'is_resubmit' => $this->isResubmit,
]);
unset($this->existingSubmission, $this->isResubmit); unset($this->existingSubmission, $this->isResubmit);
$this->dispatch('submission-completed'); $this->dispatch('submission-completed');

View File

@ -0,0 +1,33 @@
<?php
namespace App\Filament\Resources\Learning\Assignments\Schemas;
use Asmit\FilamentUpload\Enums\PdfViewFit;
use Asmit\FilamentUpload\Forms\Components\AdvancedFileUpload;
use Filament\Schemas\Schema;
class SubmitAssignmentForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
AdvancedFileUpload::make('file')
->label(fn ($get) => $get('is_resubmit') ? 'Ganti Berkas PDF (Opsional)' : 'Lampiran Berkas Tugas')
->pdfPreviewHeight(400)
->pdfDisplayPage(1)
->pdfToolbar(true)
->pdfZoomLevel(100)
->pdfFitType(PdfViewFit::FIT)
->pdfNavPanes(true)
->disk(config('filesystems.default'))
->acceptedFileTypes(['application/pdf'])
->maxSize(1024 * 5)
->directory('submissions/'.now()->toDateString())
->required(fn ($get) => ! $get('is_resubmit'))
->hiddenLabel()
->helperText('Hanya file PDF dengan ukuran maksimal 5MB.')
->columnSpanFull(),
]);
}
}

View File

@ -121,6 +121,18 @@
'title' => 'Yeay! Tugas Terkumpul! 🎉', 'title' => 'Yeay! Tugas Terkumpul! 🎉',
'body' => 'Berhasil! Tugas Anda telah tercatat dengan aman di sistem. Semangat! 🎈', 'body' => 'Berhasil! Tugas Anda telah tercatat dengan aman di sistem. Semangat! 🎈',
], ],
'submission_overdue' => [
'title' => 'Yah, Batas Waktu Udah Habis! 😭⏳',
'body' => 'Maaf ya, kamu udah nggak bisa kumpulin tugas ini lagi karena waktunya udah lewat. Tetap semangat buat tugas selanjutnya! 💪',
],
'submission_update' => [
'title' => 'Kuy Perbarui Tugasmu! 🔄✨',
'body' => 'Ada yang mau dirubah? Tenang, kamu masih bisa update file tugasmu selama belum lewat deadline kok! 🚀✨',
],
'submission_create' => [
'title' => 'Kumpulkan Tugas Sekarang! 🚀📚',
'body' => 'Ayo, jangan sampai telat ya! Segera upload file tugas terbaikmu biar tenang dan nilai maksimal! 💪🎨',
],
'user_activated' => [ 'user_activated' => [
'title' => 'Pengguna Diaktifkan! ✅✨', 'title' => 'Pengguna Diaktifkan! ✅✨',
'body' => 'Status akun pengguna berhasil diaktifkan kembali. Siap beraksi! 🚀', 'body' => 'Status akun pengguna berhasil diaktifkan kembali. Siap beraksi! 🚀',
@ -193,6 +205,10 @@
'title' => 'Daftar Tugas Bikin Lemes! 🤣📒', 'title' => 'Daftar Tugas Bikin Lemes! 🤣📒',
'description' => 'Yuk, cek dan kumpulin tugasmu biar tenang hidupnya! Klik aja di tugasnya ya. 🚀👨‍💻', 'description' => 'Yuk, cek dan kumpulin tugasmu biar tenang hidupnya! Klik aja di tugasnya ya. 🚀👨‍💻',
], ],
'empty_assignment' => [
'title' => 'Hore, Belum Ada Tugas! 🎊✨',
'description' => 'Belum ada tugas baru yang perlu dikerjakan. Hidup tenang tanpa beban tugas itu asik ya! 😊🌈',
],
'changelog' => [ 'changelog' => [
'title' => 'Catatan Rilis 📜✨', 'title' => 'Catatan Rilis 📜✨',
'description' => 'Kepoin apa aja yang baru, fitur kece, dan perbaikan bug biar nggak kudet! 🚀🔥', 'description' => 'Kepoin apa aja yang baru, fitur kece, dan perbaikan bug biar nggak kudet! 🚀🔥',
@ -221,6 +237,16 @@
'title' => 'Belum Ada Mata Kuliah Nih! 📚😴', 'title' => 'Belum Ada Mata Kuliah Nih! 📚😴',
'description' => 'Semester ini kayanya masih kosong. Coba hubungi admin kalau ada yang salah ya! ✨', 'description' => 'Semester ini kayanya masih kosong. Coba hubungi admin kalau ada yang salah ya! ✨',
], ],
'assignment_status' => [
'not_submitted' => 'Belum Dikumpulkan',
'submitted' => '✓ Sudah Dikumpulkan',
'overdue' => '⏰ Waktu Habis',
'waiting_leader' => 'Menunggu Ketua',
],
'group_submission_hint' => [
'title' => 'Tugas Kelompok',
'description' => 'Hanya <strong>Ketua Kelompok</strong> yang dapat mengumpulkan atau memperbarui tugas ini. Silakan hubungi ketua kelompok Anda.',
],
], ],
// Icons // Icons
@ -359,6 +385,18 @@
'title' => 'Konfirmasi Pengumpulan Berhasil', 'title' => 'Konfirmasi Pengumpulan Berhasil',
'body' => 'Seluruh berkas tugas Anda telah berhasil diverifikasi dan disimpan oleh sistem.', 'body' => 'Seluruh berkas tugas Anda telah berhasil diverifikasi dan disimpan oleh sistem.',
], ],
'submission_overdue' => [
'title' => 'Batas Waktu Telah Berakhir',
'body' => 'Masa pengumpulan tugas ini telah berakhir. Sistem tidak lagi menerima unggahan baru maupun pembaruan untuk penugasan ini.',
],
'submission_update' => [
'title' => 'Perbarui Pengumpulan',
'body' => 'Anda diperbolehkan untuk mengunggah ulang atau memperbarui berkas tugas selama masa pengumpulan masih aktif.',
],
'submission_create' => [
'title' => 'Kumpulkan Tugas',
'body' => 'Gunakan formulir ini untuk mengunggah berkas tugas Anda sesuai dengan ketentuan yang berlaku.',
],
'user_activated' => [ 'user_activated' => [
'title' => 'Aktivasi Akun Berhasil', 'title' => 'Aktivasi Akun Berhasil',
'body' => 'Status akun pengguna terpilih telah diubah menjadi aktif.', 'body' => 'Status akun pengguna terpilih telah diubah menjadi aktif.',
@ -431,6 +469,10 @@
'title' => 'Tugas Saya', 'title' => 'Tugas Saya',
'description' => 'Klik pada tugas untuk melihat detail dan mengumpulkan file.', 'description' => 'Klik pada tugas untuk melihat detail dan mengumpulkan file.',
], ],
'empty_assignment' => [
'title' => 'Tidak Ada Tugas Terdaftar',
'description' => 'Belum terdapat tugas baru yang perlu Anda kerjakan untuk saat ini.',
],
'changelog' => [ 'changelog' => [
'title' => 'Catatan Rilis', 'title' => 'Catatan Rilis',
'description' => 'Lihat riwayat perbaikan bug, pembaruan aplikasi, dan rilis fitur terbaru secara mendalam.', 'description' => 'Lihat riwayat perbaikan bug, pembaruan aplikasi, dan rilis fitur terbaru secara mendalam.',
@ -459,6 +501,16 @@
'title' => 'Mata Kuliah Belum Terdaftar', 'title' => 'Mata Kuliah Belum Terdaftar',
'description' => 'Belum ada mata kuliah yang terdaftar untuk semester aktif ini.', 'description' => 'Belum ada mata kuliah yang terdaftar untuk semester aktif ini.',
], ],
'assignment_status' => [
'not_submitted' => 'Belum Dikumpulkan',
'submitted' => 'Sudah Dikumpulkan',
'overdue' => 'Waktu Habis',
'waiting_leader' => 'Menunggu Ketua',
],
'group_submission_hint' => [
'title' => 'Tugas Kelompok',
'description' => 'Hanya <strong>Ketua Kelompok</strong> yang memiliki otoritas untuk memperbarui atau mengumpulkan tugas kelompok.',
],
], ],
// Icons // Icons

View File

@ -1,6 +1,6 @@
<x-filament-panels::page> <x-filament-panels::page>
<x-filament::section <x-filament::section
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-clipboard-document-list' : 'heroicon-o-briefcase' }}" icon="{{ $this->icon }}"
icon-color="primary"> icon-color="primary">
<x-slot name="heading">{{ $this->heading }}</x-slot> <x-slot name="heading">{{ $this->heading }}</x-slot>
<x-slot name="description">{{ $this->description }}</x-slot> <x-slot name="description">{{ $this->description }}</x-slot>
@ -15,10 +15,9 @@
@forelse ($this->assignmentCards as $card) @forelse ($this->assignmentCards as $card)
<div class="{{ $card->card_classes }} flex-col"> <div class="{{ $card->card_classes }} flex-col">
<a href="{{ \App\Filament\Resources\Learning\Assignments\AssignmentResource::getUrl('submit', ['record' => $card->id]) }}" <a href="{{ $card->url }}"
class="flex flex-1 items-start gap-4 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 }} h-12 w-12 mt-1 shrink-0"> <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" /> <x-filament::icon :icon="$card->status_icon" class="h-6 w-6" />
</div> </div>
@ -32,7 +31,6 @@ class="flex flex-1 items-start gap-4 p-5 text-left min-w-0 hover:bg-primary-500/
{{ $card->course_name }} {{ $card->course_name }}
</p> </p>
<div class="flex flex-wrap items-center gap-2 mt-3"> <div class="flex flex-wrap items-center gap-2 mt-3">
@if ($card->is_pinned) @if ($card->is_pinned)
<span <span
@ -68,7 +66,6 @@ class="inline-flex items-center gap-1.5 rounded-full bg-danger-500 text-white px
@endif @endif
</div> </div>
<div class="flex flex-wrap gap-x-4 gap-y-2 mt-4 text-xs text-gray-500 dark:text-gray-400"> <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"> <span class="flex items-center gap-1.5">
<x-filament::icon icon="heroicon-o-clock" <x-filament::icon icon="heroicon-o-clock"
@ -86,7 +83,6 @@ class="flex items-center gap-1.5 text-success-600 dark:text-success-400 font-bol
</div> </div>
</a> </a>
<div <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"> 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->pinAction)(['record' => $card->id]) }}
@ -98,14 +94,12 @@ class="flex flex-wrap items-center gap-3 px-5 py-4 border-t border-gray-100 dark
@endcanAny @endcanAny
</div> </div>
</div> </div>
@empty @empty
<div class="col-span-full"> <div class="col-span-full">
<x-filament::empty-state icon="heroicon-o-clipboard-document-list" <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') }}" heading="{{ $this->emptyStateHeading }}"
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!') }}" description="{{ $this->emptyStateDescription }}"
iconColor="gray"> iconColor="gray">
</x-filament::empty-state> </x-filament::empty-state>
</div> </div>

View File

@ -32,17 +32,12 @@ class="prose prose-sm dark:prose-invert max-w-none text-gray-600 dark:text-gray-
</div> </div>
@endif @endif
@if ($this->record->hasMedia('assignments')) @if ($attachmentUrl = $this->record->getFirstMediaUrl('assignments'))
@php
$attachmentUrl = $this->record->getFirstMediaUrl('assignments');
$googleViewerUrl =
'https://docs.google.com/viewer?url=' . urlencode($attachmentUrl) . '&embedded=true';
@endphp
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700"> <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Lampiran Tugas</p> <p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Lampiran Tugas</p>
<div <div
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<iframe src="{{ $this->record->getFirstMediaUrl('assignments') }}" width="100%" height="500" <iframe src="{{ $attachmentUrl }}" width="100%" height="500"
class="block border-0"></iframe> class="block border-0"></iframe>
</div> </div>
</div> </div>
@ -50,13 +45,9 @@ class="block border-0"></iframe>
</x-filament::section> </x-filament::section>
@if ($this->isOverdue) @if ($this->isOverdue)
<x-filament::section <x-filament::section :icon="$this->overdueIcon" icon-color="danger">
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-exclamation-triangle' : 'heroicon-o-lock-closed' }}" <x-slot name="heading">{{ $this->overdueHeading }}</x-slot>
icon-color="danger"> <x-slot name="description">{{ $this->overdueDescription }}</x-slot>
<x-slot
name="heading">{{ \App\Filament\Support\SystemNotification::getMessage('Yah, Batas Waktu Udah Habis! 😭⏳', 'Batas Waktu Telah Berakhir') }}</x-slot>
<x-slot
name="description">{{ \App\Filament\Support\SystemNotification::getMessage('Maaf ya, kamu udah nggak bisa kumpulin tugas ini lagi karena waktunya udah lewat. Tetap semangat buat tugas selanjutnya! 💪', 'Proses pengumpulan tugas ini telah ditutup karena melewati batas waktu.') }}</x-slot>
<div class="flex items-center gap-3 mb-5"> <div class="flex items-center gap-3 mb-5">
<div <div
@ -86,10 +77,9 @@ class="block border-0"></iframe>
<div <div
class="flex flex-col items-center justify-center py-8 text-center text-gray-500 dark:text-gray-400"> class="flex flex-col items-center justify-center py-8 text-center text-gray-500 dark:text-gray-400">
<x-filament::icon icon="heroicon-o-user-group" class="w-12 h-12 mb-3" /> <x-filament::icon icon="heroicon-o-user-group" class="w-12 h-12 mb-3" />
<p class="font-semibold text-gray-700 dark:text-gray-300">Tugas Kelompok</p> <p class="font-semibold text-gray-700 dark:text-gray-300">{{ $this->groupHintTitle }}</p>
<p class="text-sm mt-1 max-w-sm"> <p class="text-sm mt-1 max-w-sm">
Hanya <strong>Ketua Kelompok</strong> yang dapat mengumpulkan atau memperbarui tugas ini. {!! $this->groupHintDescription !!}
Silakan hubungi ketua kelompok Anda.
</p> </p>
</div> </div>
@ -106,11 +96,9 @@ class="block border-0"></iframe>
@endif @endif
</x-filament::section> </x-filament::section>
@else @else
<x-filament::section <x-filament::section :icon="$this->submissionIcon" icon-color="primary">
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-paper-airplane' : 'heroicon-o-document-arrow-up' }}" <x-slot name="heading">{{ $this->submissionHeading }}</x-slot>
icon-color="primary"> <x-slot name="description">{{ $this->submissionDescription }}</x-slot>
<x-slot
name="heading">{{ $this->isResubmit ? \App\Filament\Support\SystemNotification::getMessage('Kuy Perbarui Tugasmu! 🔄✨', 'Perbarui Pengumpulan') : \App\Filament\Support\SystemNotification::getMessage('Kumpulkan Tugas Sekarang! 🚀📚', 'Kumpulkan Tugas') }}</x-slot>
@if ($this->isResubmit && ($url = $this->getSubmissionFileUrl())) @if ($this->isResubmit && ($url = $this->getSubmissionFileUrl()))
<div class="mb-4"> <div class="mb-4">
@ -124,87 +112,18 @@ class="block border-0"></iframe>
@endif @endif
<div class="space-y-4"> <div class="space-y-4">
<div> {{ $this->form }}
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
{{ $this->isResubmit ? 'Ganti File PDF (opsional)' : 'File Tugas' }}
@if (!$this->isResubmit)
<span class="text-danger-500">*</span>
@endif
</label>
<input wire:model="file" type="file" id="assignment-file" accept=".pdf"
class="block w-full text-sm text-gray-700 dark:text-gray-300
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0
file:text-sm file:font-medium file:cursor-pointer
file:bg-primary-50 file:text-primary-700
dark:file:bg-primary-900/20 dark:file:text-primary-400
hover:file:bg-primary-100 dark:hover:file:bg-primary-900/30
border border-gray-300 dark:border-gray-600 rounded-lg
bg-white dark:bg-gray-800 p-2 transition-colors" />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Hanya file PDF. Maksimal 5MB.
</p>
@error('file')
<p class="mt-1 text-xs text-danger-600 dark:text-danger-400">
{{ $message }}</p>
@enderror
</div>
<div id="pdf-submission-preview" class="hidden" wire:ignore>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Preview</p>
<div
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<iframe id="pdf-submission-frame" width="100%" height="400"
class="block border-0"></iframe>
</div>
</div>
</div>
<div class="flex justify-end mt-5 pt-4 border-t border-gray-200 dark:border-gray-700">
<x-filament::button wire:click="submit" wire:loading.attr="disabled" color="primary">
<span wire:loading.remove wire:target="submit">
{{ $this->isResubmit ? 'Perbarui Pengumpulan' : 'Kumpulkan Sekarang' }}
</span>
<span wire:loading wire:target="submit">Mengupload...</span>
</x-filament::button>
</div> </div>
</x-filament::section> </x-filament::section>
<div class="flex justify-start">
<x-filament::button wire:click="submit" wire:loading.attr="disabled" color="primary">
<span wire:loading.remove wire:target="submit">
{{ $this->isResubmit ? 'Perbarui' : 'Kumpulkan' }}
</span>
<span wire:loading wire:target="submit">Mengupload...</span>
</x-filament::button>
</div>
@endif @endif
</div> </div>
</x-filament-panels::page> </x-filament-panels::page>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to reset preview
function resetPreview() {
var preview = document.getElementById('pdf-submission-preview');
var frame = document.getElementById('pdf-submission-frame');
if (preview) preview.classList.add('hidden');
if (frame) frame.src = '';
}
// Delegation for change event
document.addEventListener('change', function(e) {
if (e.target && e.target.id === 'assignment-file') {
var file = e.target.files[0];
var preview = document.getElementById('pdf-submission-preview');
var frame = document.getElementById('pdf-submission-frame');
if (file && preview && frame) {
frame.src = URL.createObjectURL(file);
preview.classList.remove('hidden');
} else {
resetPreview();
}
}
});
// Listen for the Livewire event after submission
window.addEventListener('submission-completed', function() {
var input = document.getElementById('assignment-file');
if (input) input.value = '';
resetPreview();
});
});
</script>
@endpush

View File

@ -142,9 +142,11 @@
$this->actingAs($user); $this->actingAs($user);
Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment]) Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment])
->assertSuccessful() ->assertSuccessful()
->set('file', $file) ->fillForm([
'file' => $file,
])
->call('submit') ->call('submit')
->assertHasNoErrors(); ->assertHasNoFormErrors();
$this->assertDatabaseHas('assignment_submissions', [ $this->assertDatabaseHas('assignment_submissions', [
'assignment_id' => $assignment->id, 'assignment_id' => $assignment->id,