feat(pdf): preview in Filament modal (iframe) instead of new tab

All "Vizualizare PDF" actions now open a wide (7xl) Filament modal
containing an iframe pointing at the inline PDF endpoint, with a
top-right "Descarcă" and "Deschide în tab nou" fallback.

- WorkOrder edit action + WO table row action
- InjectorProtocol edit action + protocols table row action
- Dashboard Docs tab preview button (Alpine.js overlay since it's
  a plain Blade view, not a Filament action)

The underlying /pdf routes stay unchanged (Content-Disposition: inline,
?download=1 for attachment) — the iframe just loads the same URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 05:47:07 +00:00
parent e289fbc068
commit c9d400fd6e
8 changed files with 68 additions and 11 deletions
@@ -266,8 +266,14 @@ class InjectorProtocolResource extends Resource
->label(__('PDF')) ->label(__('PDF'))
->icon('heroicon-m-document-magnifying-glass') ->icon('heroicon-m-document-magnifying-glass')
->color('gray') ->color('gray')
->url(fn (InjectorProtocol $r) => route('tenant.injector-protocols.pdf', ['record' => $r->id])) ->modalHeading(fn (InjectorProtocol $r) => __('Protocol') . ' ' . $r->protocol_number)
->openUrlInNewTab(), ->modalSubmitAction(false)
->modalCancelActionLabel(__('Închide'))
->modalWidth('7xl')
->modalContent(fn (InjectorProtocol $r) => view('filament.tenant.pdf-preview', [
'pdfUrl' => route('tenant.injector-protocols.pdf', ['record' => $r->id]),
'downloadUrl' => route('tenant.injector-protocols.pdf', ['record' => $r->id, 'download' => 1]),
])),
Actions\EditAction::make(), Actions\EditAction::make(),
Actions\DeleteAction::make(), Actions\DeleteAction::make(),
]) ])
@@ -18,8 +18,14 @@ class EditInjectorProtocol extends EditRecord
->label(__('Vizualizare PDF')) ->label(__('Vizualizare PDF'))
->icon('heroicon-m-document-magnifying-glass') ->icon('heroicon-m-document-magnifying-glass')
->color('gray') ->color('gray')
->url(fn (InjectorProtocol $record) => route('tenant.injector-protocols.pdf', ['record' => $record->id])) ->modalHeading(fn (InjectorProtocol $record) => __('Protocol') . ' ' . $record->protocol_number)
->openUrlInNewTab(), ->modalSubmitAction(false)
->modalCancelActionLabel(__('Închide'))
->modalWidth('7xl')
->modalContent(fn (InjectorProtocol $record) => view('filament.tenant.pdf-preview', [
'pdfUrl' => route('tenant.injector-protocols.pdf', ['record' => $record->id]),
'downloadUrl' => route('tenant.injector-protocols.pdf', ['record' => $record->id, 'download' => 1]),
])),
Actions\DeleteAction::make(), Actions\DeleteAction::make(),
]; ];
} }
@@ -188,8 +188,14 @@ class WorkOrderResource extends Resource
->label(__('PDF')) ->label(__('PDF'))
->icon('heroicon-m-document-magnifying-glass') ->icon('heroicon-m-document-magnifying-glass')
->color('gray') ->color('gray')
->url(fn (WorkOrder $r) => url('/app/work-orders/' . $r->id . '/pdf')) ->modalHeading(fn (WorkOrder $r) => __('Factură') . ' — WO #' . $r->number)
->openUrlInNewTab(), ->modalSubmitAction(false)
->modalCancelActionLabel(__('Închide'))
->modalWidth('7xl')
->modalContent(fn (WorkOrder $r) => view('filament.tenant.pdf-preview', [
'pdfUrl' => url('/app/work-orders/' . $r->id . '/pdf'),
'downloadUrl' => url('/app/work-orders/' . $r->id . '/pdf?download=1'),
])),
Actions\EditAction::make(), Actions\EditAction::make(),
Actions\DeleteAction::make(), Actions\DeleteAction::make(),
]) ])
@@ -87,8 +87,14 @@ class EditWorkOrder extends EditRecord
->label(__('Vizualizare PDF')) ->label(__('Vizualizare PDF'))
->icon('heroicon-m-document-magnifying-glass') ->icon('heroicon-m-document-magnifying-glass')
->color('gray') ->color('gray')
->url(fn () => url('/app/work-orders/' . $this->record->id . '/pdf')) ->modalHeading(fn () => __('Factură') . ' — WO #' . $this->record->number)
->openUrlInNewTab(), ->modalSubmitAction(false)
->modalCancelActionLabel(__('Închide'))
->modalWidth('7xl')
->modalContent(fn () => view('filament.tenant.pdf-preview', [
'pdfUrl' => url('/app/work-orders/' . $this->record->id . '/pdf'),
'downloadUrl' => url('/app/work-orders/' . $this->record->id . '/pdf?download=1'),
])),
Actions\DeleteAction::make(), Actions\DeleteAction::make(),
]; ];
} }
+2
View File
@@ -670,6 +670,7 @@
"Deschide fișă nouă": "Open new order", "Deschide fișă nouă": "Open new order",
"Deschide panoul pe telefon și acceptă notificările întâi.": "Open the panel on the phone and accept notifications first.", "Deschide panoul pe telefon și acceptă notificările întâi.": "Open the panel on the phone and accept notifications first.",
"Deschide tenantul": "Deschide tenantul", "Deschide tenantul": "Deschide tenantul",
"Deschide în tab nou": "Open in new tab",
"Deschide →": "Open →", "Deschide →": "Open →",
"Deschidere ÎNAINTE": "Opening BEFORE", "Deschidere ÎNAINTE": "Opening BEFORE",
"Deschis": "Opened", "Deschis": "Opened",
@@ -1623,6 +1624,7 @@
"Progres": "Progress", "Progres": "Progress",
"Progres etapă": "Stage progress", "Progres etapă": "Stage progress",
"Proprietar": "Owner", "Proprietar": "Owner",
"Protocol": "Protocol",
"Protocol nou": "New protocol", "Protocol nou": "New protocol",
"Provider implicit": "Default provider", "Provider implicit": "Default provider",
"Provider:": "Provider:", "Provider:": "Provider:",
+2
View File
@@ -670,6 +670,7 @@
"Deschide fișă nouă": "Открыть новый наряд", "Deschide fișă nouă": "Открыть новый наряд",
"Deschide panoul pe telefon și acceptă notificările întâi.": "Сначала откройте панель на телефоне и разрешите уведомления.", "Deschide panoul pe telefon și acceptă notificările întâi.": "Сначала откройте панель на телефоне и разрешите уведомления.",
"Deschide tenantul": "Deschide tenantul", "Deschide tenantul": "Deschide tenantul",
"Deschide în tab nou": "Открыть в новой вкладке",
"Deschide →": "Открыть →", "Deschide →": "Открыть →",
"Deschidere ÎNAINTE": "Открытие ДО", "Deschidere ÎNAINTE": "Открытие ДО",
"Deschis": "Открыт", "Deschis": "Открыт",
@@ -1623,6 +1624,7 @@
"Progres": "Прогресс", "Progres": "Прогресс",
"Progres etapă": "Прогресс этапа", "Progres etapă": "Прогресс этапа",
"Proprietar": "Владелец", "Proprietar": "Владелец",
"Protocol": "Протокол",
"Protocol nou": "Новый протокол", "Protocol nou": "Новый протокол",
"Provider implicit": "Провайдер по умолчанию", "Provider implicit": "Провайдер по умолчанию",
"Provider:": "Provider:", "Provider:": "Provider:",
@@ -687,13 +687,28 @@
{{-- ── DOCUMENTS TAB ── --}} {{-- ── DOCUMENTS TAB ── --}}
<div x-show="tab === 'docs'" x-cloak> <div x-show="tab === 'docs'" x-cloak>
<div style="display:flex;flex-direction:column;gap:8px;"> <div style="display:flex;flex-direction:column;gap:8px;" x-data="{ pdfOpen: false }">
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf') }}" target="_blank"> <button type="button" class="wd-btn" @click="pdfOpen = true">
🔍 {{ __('Vizualizează factură (PDF)') }} 🔍 {{ __('Vizualizează factură (PDF)') }}
</a> </button>
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf?download=1') }}"> <a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf?download=1') }}">
⬇️ {{ __('Descarcă factură (PDF)') }} ⬇️ {{ __('Descarcă factură (PDF)') }}
</a> </a>
{{-- PDF preview modal (Alpine.js overlay) --}}
<div x-show="pdfOpen" x-cloak
style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center;padding:24px;"
@click.self="pdfOpen = false"
@keydown.escape.window="pdfOpen = false">
<div style="background:#fff;border-radius:8px;width:100%;max-width:1000px;height:90vh;display:flex;flex-direction:column;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #e5e7eb;">
<b style="font-size:14px;">{{ __('Factură') }} WO #{{ $wo->number }}</b>
<button type="button" @click="pdfOpen = false" style="background:none;border:none;font-size:20px;cursor:pointer;color:#6b7280;"></button>
</div>
<iframe x-bind:src="pdfOpen ? '{{ url('/app/work-orders/' . $wo->id . '/pdf') }}#toolbar=1&navpanes=0' : ''"
style="flex:1;width:100%;border:none;" title="PDF"></iframe>
</div>
</div>
@php $signed = $wo->getMedia('signed_documents'); @endphp @php $signed = $wo->getMedia('signed_documents'); @endphp
@if ($signed->isNotEmpty()) @if ($signed->isNotEmpty())
<div style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin-top:12px;">{{ __('Documente semnate') }}</div> <div style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin-top:12px;">{{ __('Documente semnate') }}</div>
@@ -0,0 +1,14 @@
{{-- Reusable PDF preview iframe (loaded inside a Filament action modal).
$pdfUrl inline PDF endpoint (Content-Disposition: inline).
$downloadUrl same URL with ?download=1, shown as a fallback link. --}}
<div style="display:flex;flex-direction:column;gap:10px;">
<div style="display:flex;justify-content:flex-end;gap:8px;">
<a href="{{ $downloadUrl }}" class="text-primary-600 hover:underline text-sm font-medium">
⬇️ {{ __('Descarcă') }}
</a>
<a href="{{ $pdfUrl }}" target="_blank" class="text-primary-600 hover:underline text-sm font-medium">
↗️ {{ __('Deschide în tab nou') }}
</a>
</div>
<iframe src="{{ $pdfUrl }}#toolbar=1&navpanes=0" style="width:100%;height:75vh;border:1px solid var(--fi-color-gray-200,#e5e7eb);border-radius:6px;" title="PDF"></iframe>
</div>