Extract the shield mark into reusable brand-icon/brand-mark components and use them in the app shell, auth layout, and marketing nav/footer. Align the palette on emerald (badges, selection, dark zinc-950 body), and add a subtle grid/glow backdrop to the app layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
5.4 KiB
PHP
142 lines
5.4 KiB
PHP
<?php
|
|
|
|
use App\Models\Category;
|
|
use App\Models\Certificate;
|
|
use Flux\Flux;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
|
use Livewire\WithFileUploads;
|
|
|
|
new #[Title('Edit certificate')] class extends Component {
|
|
use WithFileUploads;
|
|
|
|
public Certificate $certificate;
|
|
|
|
public string $title = '';
|
|
|
|
public string $category_id = '';
|
|
|
|
public string $issuer = '';
|
|
|
|
public string $certificate_number = '';
|
|
|
|
public string $issued_at = '';
|
|
|
|
public string $expires_at = '';
|
|
|
|
public string $notes = '';
|
|
|
|
public ?TemporaryUploadedFile $file = null;
|
|
|
|
public function mount(Certificate $certificate): void
|
|
{
|
|
// Route-model binding respects the global scope, so a foreign
|
|
// certificate resolves to a 404.
|
|
$this->certificate = $certificate;
|
|
$this->title = $certificate->title;
|
|
$this->category_id = (string) ($certificate->category_id ?? '');
|
|
$this->issuer = (string) ($certificate->issuer ?? '');
|
|
$this->certificate_number = (string) ($certificate->certificate_number ?? '');
|
|
$this->issued_at = $certificate->issued_at?->toDateString() ?? '';
|
|
$this->expires_at = $certificate->expires_at->toDateString();
|
|
$this->notes = (string) ($certificate->notes ?? '');
|
|
}
|
|
|
|
public function save(): void
|
|
{
|
|
$validated = $this->validate([
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
|
|
'issuer' => ['nullable', 'string', 'max:255'],
|
|
'certificate_number' => ['nullable', 'string', 'max:255'],
|
|
'issued_at' => ['nullable', 'date', 'before_or_equal:expires_at'],
|
|
'expires_at' => ['required', 'date'],
|
|
'notes' => ['nullable', 'string', 'max:2000'],
|
|
'file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:10240'],
|
|
]);
|
|
|
|
if ($this->file !== null) {
|
|
Storage::disk('local')->delete($this->certificate->file_path);
|
|
|
|
$this->certificate->file_path = $this->file->storeAs(
|
|
'certificates/'.Auth::id(),
|
|
Str::uuid().'.'.$this->file->getClientOriginalExtension(),
|
|
'local'
|
|
);
|
|
}
|
|
|
|
$this->certificate->fill([
|
|
'title' => $validated['title'],
|
|
'category_id' => $validated['category_id'] ?: null,
|
|
'issuer' => $validated['issuer'] ?: null,
|
|
'certificate_number' => $validated['certificate_number'] ?: null,
|
|
'issued_at' => $validated['issued_at'] ?: null,
|
|
'expires_at' => $validated['expires_at'],
|
|
'notes' => $validated['notes'] ?: null,
|
|
])->save();
|
|
|
|
Flux::toast(variant: 'success', text: __('Certificate updated.'));
|
|
|
|
$this->redirectRoute('certificates.index', navigate: true);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, Category>
|
|
*/
|
|
#[Computed]
|
|
public function categories(): Collection
|
|
{
|
|
return Category::orderBy('name')->get();
|
|
}
|
|
}; ?>
|
|
|
|
<section class="mx-auto flex w-full max-w-xl flex-col gap-6">
|
|
<div>
|
|
<flux:heading size="xl">{{ __('Edit certificate') }}</flux:heading>
|
|
<flux:text class="mt-1">{{ __('Update the details or replace the stored document.') }}</flux:text>
|
|
</div>
|
|
|
|
<form wire:submit="save" class="space-y-6 rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-8 dark:border-zinc-800 dark:bg-zinc-900">
|
|
<flux:input wire:model="title" :label="__('Title')" required />
|
|
|
|
<flux:select wire:model="category_id" :label="__('Category')">
|
|
<flux:select.option value="">{{ __('Uncategorized') }}</flux:select.option>
|
|
@foreach ($this->categories as $category)
|
|
<flux:select.option value="{{ $category->id }}">{{ $category->name }}</flux:select.option>
|
|
@endforeach
|
|
</flux:select>
|
|
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<flux:input wire:model="issuer" :label="__('Issuing authority')" :placeholder="__('e.g. VCA, Red Cross')" />
|
|
<flux:input wire:model="certificate_number" :label="__('Certificate number')" />
|
|
<flux:input type="date" wire:model="issued_at" :label="__('Issue date')" />
|
|
<flux:input type="date" wire:model="expires_at" :label="__('Expiry date')" required />
|
|
</div>
|
|
|
|
<flux:textarea wire:model="notes" :label="__('Notes')" rows="3" :placeholder="__('Optional notes about this certificate')" />
|
|
|
|
<flux:input
|
|
type="file"
|
|
wire:model="file"
|
|
:label="__('Replace document')"
|
|
accept=".pdf,.jpg,.jpeg,.png"
|
|
:description="__('Optional. Leave empty to keep the current file.')"
|
|
/>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<flux:button type="submit" variant="primary">{{ __('Save changes') }}</flux:button>
|
|
<flux:button
|
|
:href="route('certificates.download', $certificate)"
|
|
variant="ghost"
|
|
icon="arrow-down-tray"
|
|
>{{ __('Download current file') }}</flux:button>
|
|
<flux:button :href="route('certificates.index')" wire:navigate variant="ghost">{{ __('Cancel') }}</flux:button>
|
|
</div>
|
|
</form>
|
|
</section>
|