feat: add download support for non-PDF files in assignment submission views

This commit is contained in:
Yoga Pangestu 2026-04-18 21:51:25 +07:00
parent 9727e8d52f
commit 1f0db96179
6 changed files with 157 additions and 31 deletions

View File

@ -11,6 +11,7 @@
use App\Models\Assignment; use App\Models\Assignment;
use App\Models\AssignmentSubmission; use App\Models\AssignmentSubmission;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Pages\Page; use Filament\Resources\Pages\Page;
use Filament\Support\Enums\Width; use Filament\Support\Enums\Width;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -100,6 +101,8 @@ public function submissionSummary(): Collection
'submitted' => $isSubmitted, 'submitted' => $isSubmitted,
'submitted_at_formatted' => $isSubmitted ? $submission?->submitted_at?->translatedFormat('l, d F Y H:i') : '-', 'submitted_at_formatted' => $isSubmitted ? $submission?->submitted_at?->translatedFormat('l, d F Y H:i') : '-',
'has_file' => $isSubmitted && $submission->hasMedia('submission'), 'has_file' => $isSubmitted && $submission->hasMedia('submission'),
'is_pdf' => $isSubmitted && str_ends_with(strtolower($submission->getFirstMediaUrl('submission')), '.pdf'),
'file_url' => $isSubmitted ? $submission->getFirstMediaUrl('submission') : null,
'status_classes' => Arr::toCssClasses([ 'status_classes' => Arr::toCssClasses([
'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium', 'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium',
'bg-success-100 text-success-700 dark:bg-success-900/30 dark:text-success-400' => $isSubmitted, 'bg-success-100 text-success-700 dark:bg-success-900/30 dark:text-success-400' => $isSubmitted,
@ -129,6 +132,8 @@ public function submissionSummary(): Collection
'submitted' => $isSubmitted, 'submitted' => $isSubmitted,
'submitted_at_formatted' => $isSubmitted ? $submission->submitted_at?->translatedFormat('l, d F Y H:i') : '-', 'submitted_at_formatted' => $isSubmitted ? $submission->submitted_at?->translatedFormat('l, d F Y H:i') : '-',
'has_file' => $isSubmitted && $submission->hasMedia('submission'), 'has_file' => $isSubmitted && $submission->hasMedia('submission'),
'is_pdf' => $isSubmitted && str_ends_with(strtolower($submission->getFirstMediaUrl('submission')), '.pdf'),
'file_url' => $isSubmitted ? $submission->getFirstMediaUrl('submission') : null,
'status_classes' => Arr::toCssClasses([ 'status_classes' => Arr::toCssClasses([
'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium', 'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium',
'bg-success-100 text-success-700 dark:bg-success-900/30 dark:text-success-400' => $isSubmitted, 'bg-success-100 text-success-700 dark:bg-success-900/30 dark:text-success-400' => $isSubmitted,
@ -195,14 +200,39 @@ public function previewSubmissionAction(): Action
->modalWidth(Width::SixExtraLarge) ->modalWidth(Width::SixExtraLarge)
->infolist([ ->infolist([
PdfViewerEntry::make('submission') PdfViewerEntry::make('submission')
->visible(fn (AssignmentSubmission $record) => str_ends_with(strtolower($record->getFirstMediaUrl('submission')), '.pdf'))
->hiddenLabel() ->hiddenLabel()
->fileUrl(fn (AssignmentSubmission $record) => $record->getFirstMediaUrl('submission')) ->fileUrl(fn (AssignmentSubmission $record) => $record->getFirstMediaUrl('submission'))
->columnSpanFull(), ->columnSpanFull(),
TextEntry::make('download_file')
->label('Berkas non-PDF')
->visible(fn (AssignmentSubmission $record) => ! str_ends_with(strtolower($record->getFirstMediaUrl('submission')), '.pdf'))
->hint('Klik ikon unduh untuk melihat isi berkas.')
->default(fn (AssignmentSubmission $record) => $record->getFirstMedia('submission')?->file_name ?? 'Unduh Berkas')
->suffixAction(
Action::make('download')
->label('Unduh')
->icon('heroicon-o-arrow-down-tray')
->url(fn (AssignmentSubmission $record) => $record->getFirstMediaUrl('submission'), true)
),
]) ])
->modalSubmitAction(false) ->modalSubmitAction(false)
->modalCancelActionLabel('Tutup'); ->modalCancelActionLabel('Tutup');
} }
public function downloadMedia(int $submissionId)
{
$submission = AssignmentSubmission::findOrFail($submissionId);
$media = $submission->getFirstMedia('submission');
if (! $media) {
return;
}
return response()->download($media->getPath(), $media->file_name);
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [

View File

@ -248,6 +248,26 @@ public function getSubmissionFileUrl(): ?string
return $this->existingSubmission?->getFirstMediaUrl('submission'); return $this->existingSubmission?->getFirstMediaUrl('submission');
} }
public function downloadAttachment()
{
$media = $this->record->getFirstMedia('assignments');
if (! $media) {
return;
}
return response()->download($media->getPath(), $media->file_name);
}
public function downloadSubmission()
{
$media = $this->existingSubmission?->getFirstMedia('submission');
if (! $media) {
return;
}
return response()->download($media->getPath(), $media->file_name);
}
public function submit(): void public function submit(): void
{ {
$student = $this->student; $student = $this->student;

View File

@ -111,7 +111,7 @@ public static function configure(Schema $schema): Schema
->columnSpanFull(), ->columnSpanFull(),
AdvancedFileUpload::make('pdf') AdvancedFileUpload::make('pdf')
->label('Lampiran PDF') ->label('Lampiran Berkas')
->pdfPreviewHeight(400) ->pdfPreviewHeight(400)
->pdfDisplayPage(1) ->pdfDisplayPage(1)
->pdfToolbar(true) ->pdfToolbar(true)
@ -119,7 +119,7 @@ public static function configure(Schema $schema): Schema
->pdfFitType(PdfViewFit::FIT) ->pdfFitType(PdfViewFit::FIT)
->pdfNavPanes(true) ->pdfNavPanes(true)
->disk(config('filesystems.default')) ->disk(config('filesystems.default'))
->acceptedFileTypes(['application/pdf']) ->acceptedFileTypes(['application/pdf', 'application/zip', 'application/x-zip-compressed', 'application/x-zip', 'application/octet-stream', 'multipart/x-zip', '.zip', '.7z'])
->maxSize(1024 * 5) ->maxSize(1024 * 5)
->directory('assignments-tmp/'.now()->toDateString()) ->directory('assignments-tmp/'.now()->toDateString())
->nullable() ->nullable()

View File

@ -13,7 +13,7 @@ public static function configure(Schema $schema): Schema
return $schema return $schema
->components([ ->components([
AdvancedFileUpload::make('file') AdvancedFileUpload::make('file')
->label(fn ($get) => $get('is_resubmit') ? 'Ganti Berkas PDF (Opsional)' : 'Lampiran Berkas Tugas') ->label(fn ($get) => $get('is_resubmit') ? 'Ganti Berkas Tugas (Opsional)' : 'Lampiran Berkas Tugas')
->pdfPreviewHeight(400) ->pdfPreviewHeight(400)
->pdfDisplayPage(1) ->pdfDisplayPage(1)
->pdfToolbar(true) ->pdfToolbar(true)
@ -21,12 +21,12 @@ public static function configure(Schema $schema): Schema
->pdfFitType(PdfViewFit::FIT) ->pdfFitType(PdfViewFit::FIT)
->pdfNavPanes(true) ->pdfNavPanes(true)
->disk(config('filesystems.default')) ->disk(config('filesystems.default'))
->acceptedFileTypes(['application/pdf']) ->acceptedFileTypes(['application/pdf', 'application/zip', 'application/x-zip-compressed', 'application/x-zip', 'application/octet-stream', 'multipart/x-zip', '.zip', '.7z'])
->maxSize(1024 * 5) ->maxSize(1024 * 5)
->directory('submissions/'.now()->toDateString()) ->directory('submissions/'.now()->toDateString())
->required(fn ($get) => ! $get('is_resubmit')) ->required(fn ($get) => ! $get('is_resubmit'))
->hiddenLabel() ->hiddenLabel()
->helperText('Hanya file PDF dengan ukuran maksimal 5MB.') ->helperText('File PDF atau ZIP dengan ukuran maksimal 5MB.')
->columnSpanFull(), ->columnSpanFull(),
]); ]);
} }

View File

@ -101,12 +101,21 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
@if ($item->has_file) @if ($item->has_file)
<button type="button" @if ($item->is_pdf)
wire:click="mountAction('previewSubmission', { submissionId: {{ $item->submission_id }} })" <button type="button"
class="inline-flex items-center gap-1 text-xs text-primary-600 dark:text-primary-400 hover:underline"> wire:click="mountAction('previewSubmission', { submissionId: {{ $item->submission_id }} })"
<x-filament::icon icon="heroicon-o-eye" class="h-3.5 w-3.5" /> class="inline-flex items-center gap-1 text-xs text-primary-600 dark:text-primary-400 hover:underline">
Lihat Tugas <x-filament::icon icon="heroicon-o-eye" class="h-3.5 w-3.5" />
</button> Lihat Tugas
</button>
@else
<button type="button"
wire:click="downloadMedia({{ $item->submission_id }})"
class="inline-flex items-center gap-1 text-xs text-success-600 dark:text-success-400 hover:underline">
<x-filament::icon icon="heroicon-o-arrow-down-tray" class="h-3.5 w-3.5" />
Unduh ZIP
</button>
@endif
@else @else
<span class="text-xs text-gray-400">-</span> <span class="text-xs text-gray-400">-</span>
@endif @endif

View File

@ -35,11 +35,28 @@ class="prose prose-sm dark:prose-invert max-w-none text-gray-600 dark:text-gray-
@if ($attachmentUrl = $this->record->getFirstMediaUrl('assignments')) @if ($attachmentUrl = $this->record->getFirstMediaUrl('assignments'))
<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 @if (str_ends_with(strtolower($attachmentUrl), '.pdf'))
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> <div
<iframe src="{{ $attachmentUrl }}" width="100%" height="500" class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
class="block border-0"></iframe> <iframe src="{{ $attachmentUrl }}" width="100%" height="500"
</div> class="block border-0"></iframe>
</div>
@else
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 group hover:border-primary-500 transition-colors">
<div class="flex items-center gap-4">
<div class="p-2.5 bg-primary-100 dark:bg-primary-900/30 rounded-lg text-primary-600">
<x-filament::icon icon="heroicon-o-document-arrow-down" class="w-6 h-6" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $this->record->getFirstMedia('assignments')?->file_name ?? 'Berkas Lampiran' }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Klik tombol di samping untuk mengunduh berkas.</p>
</div>
</div>
<x-filament::button type="button" wire:click="downloadAttachment" color="gray" size="sm" icon="heroicon-o-arrow-down-tray">
Unduh
</x-filament::button>
</div>
@endif
</div> </div>
@endif @endif
</x-filament::section> </x-filament::section>
@ -63,11 +80,28 @@ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-dange
@if ($url = $this->getSubmissionFileUrl()) @if ($url = $this->getSubmissionFileUrl())
<div> <div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">File yang Terakhir Dikumpulkan</p> <p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">File yang Terakhir Dikumpulkan</p>
<div @if (str_ends_with(strtolower($url), '.pdf'))
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> <div
<iframe src="{{ $url }}" width="100%" height="400" class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
class="block border-0"></iframe> <iframe src="{{ $url }}" width="100%" height="400"
</div> class="block border-0"></iframe>
</div>
@else
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 group hover:border-primary-500 transition-colors">
<div class="flex items-center gap-4">
<div class="p-2.5 bg-primary-100 dark:bg-primary-900/30 rounded-lg text-primary-600">
<x-filament::icon icon="heroicon-o-document-arrow-down" class="w-6 h-6" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $this->existingSubmission?->getFirstMedia('submission')?->file_name ?? 'Berkas Tugas' }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Berkas non-PDF (ZIP)</p>
</div>
</div>
<x-filament::button type="button" wire:click="downloadSubmission" color="gray" size="sm" icon="heroicon-o-arrow-down-tray">
Unduh
</x-filament::button>
</div>
@endif
</div> </div>
@endif @endif
</x-filament::section> </x-filament::section>
@ -86,11 +120,27 @@ class="flex flex-col items-center justify-center py-8 text-center text-gray-500
<div class="mt-6"> <div class="mt-6">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2 italic">File yang telah <p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2 italic">File yang telah
dikumpulkan oleh kelompok Anda:</p> dikumpulkan oleh kelompok Anda:</p>
<div @if (str_ends_with(strtolower($url), '.pdf'))
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> <div
<iframe src="{{ $url }}" width="100%" height="400" class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
class="block border-0"></iframe> <iframe src="{{ $url }}" width="100%" height="400"
</div> class="block border-0"></iframe>
</div>
@else
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-4">
<div class="p-2.5 bg-primary-100 dark:bg-primary-900/30 rounded-lg text-primary-600">
<x-filament::icon icon="heroicon-o-document-arrow-down" class="w-6 h-6" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $this->existingSubmission?->getFirstMedia('submission')?->file_name ?? 'Berkas Tugas' }}</p>
</div>
</div>
<x-filament::button type="button" wire:click="downloadSubmission" color="gray" size="sm" icon="heroicon-o-arrow-down-tray">
Unduh
</x-filament::button>
</div>
@endif
</div> </div>
@endif @endif
</x-filament::section> </x-filament::section>
@ -112,11 +162,28 @@ class="block border-0"></iframe>
@if ($this->isResubmit && ($url = $this->getSubmissionFileUrl())) @if ($this->isResubmit && ($url = $this->getSubmissionFileUrl()))
<div class="mb-4"> <div class="mb-4">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">File Terkumpul Saat Ini</p> <p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">File Terkumpul Saat Ini</p>
<div @if (str_ends_with(strtolower($url), '.pdf'))
class="rounded-lg overflow-hidden border border-success-200 dark:border-success-800 bg-gray-50 dark:bg-gray-900"> <div
<iframe src="{{ $url }}" width="100%" height="400" class="rounded-lg overflow-hidden border border-success-200 dark:border-success-800 bg-gray-50 dark:bg-gray-900">
class="block border-0"></iframe> <iframe src="{{ $url }}" width="100%" height="400"
</div> class="block border-0"></iframe>
</div>
@else
<div class="flex items-center justify-between p-4 bg-success-50/50 dark:bg-success-900/10 rounded-xl border border-success-200 dark:border-success-800">
<div class="flex items-center gap-4">
<div class="p-2.5 bg-success-100 dark:bg-success-900/30 rounded-lg text-success-600">
<x-filament::icon icon="heroicon-o-check-badge" class="w-6 h-6" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $this->existingSubmission?->getFirstMedia('submission')?->file_name ?? 'Berkas Tugas' }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Berkas Terkumpul (ZIP)</p>
</div>
</div>
<x-filament::button type="button" wire:click="downloadSubmission" color="success" size="sm" icon="heroicon-o-arrow-down-tray" variant="outline">
Unduh
</x-filament::button>
</div>
@endif
</div> </div>
@endif @endif