From 5a5f43f2def14b2308fee6d1ffb7befa96599e21 Mon Sep 17 00:00:00 2001 From: Vasyka Date: Wed, 5 Aug 2026 20:56:24 +0000 Subject: [PATCH] feat(pdf): inline preview by default with ?download=1 override + vehicle photo fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkOrder and InjectorProtocol PDF routes now respond with Content-Disposition: inline (opens in browser tab so the user can read/print/save from the built-in PDF viewer). ?download=1 forces the classic attachment download. - CalendarBoard exportPdf() switched to the same inline default. - WO edit action + WO table row action + injector protocol actions now use ->url(...)->openUrlInNewTab() instead of streaming the PDF inline into the current tab. - Dashboard Docs tab has both a preview link and a "Descarcă" link. - Vehicle card photo falls back to WO's first uploaded photo when Vehicle model has no MediaLibrary integration of its own. Test CalendarEnhancementsTest updated to assert the new inline response headers. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Filament/Tenant/Pages/CalendarBoard.php | 12 ++++---- .../Resources/InjectorProtocolResource.php | 2 +- .../Pages/EditInjectorProtocol.php | 4 +-- .../Tenant/Resources/WorkOrderResource.php | 13 ++------- .../WorkOrderResource/Pages/EditWorkOrder.php | 17 +++-------- lang/en.json | 2 ++ lang/ru.json | 2 ++ .../pages/work-order-dashboard.blade.php | 11 ++++++- routes/web.php | 29 +++++++++++++------ tests/Feature/CalendarEnhancementsTest.php | 7 +++-- 10 files changed, 55 insertions(+), 44 deletions(-) diff --git a/app/Filament/Tenant/Pages/CalendarBoard.php b/app/Filament/Tenant/Pages/CalendarBoard.php index e9dff6e..77aad27 100644 --- a/app/Filament/Tenant/Pages/CalendarBoard.php +++ b/app/Filament/Tenant/Pages/CalendarBoard.php @@ -437,11 +437,13 @@ class CalendarBoard extends Page 'generatedAt' => now()->format('d.m.Y H:i'), ])->setPaper('a4', 'portrait'); - return response()->streamDownload( - fn () => print $pdf->output(), - 'programari_' . $firstDate . '_' . $lastDate . '.pdf', - ['Content-Type' => 'application/pdf'] - ); + $filename = 'programari_' . $firstDate . '_' . $lastDate . '.pdf'; + $disposition = request()->boolean('download') ? 'attachment' : 'inline'; + return response($pdf->output(), 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => $disposition . '; filename="' . $filename . '"', + 'Cache-Control' => 'private, no-store', + ]); } /** Flat list of appointments for the visible period — used by list view. */ diff --git a/app/Filament/Tenant/Resources/InjectorProtocolResource.php b/app/Filament/Tenant/Resources/InjectorProtocolResource.php index 39a8a32..a693fd2 100644 --- a/app/Filament/Tenant/Resources/InjectorProtocolResource.php +++ b/app/Filament/Tenant/Resources/InjectorProtocolResource.php @@ -264,7 +264,7 @@ class InjectorProtocolResource extends Resource ->actions([ Actions\Action::make('pdf') ->label(__('PDF')) - ->icon('heroicon-m-document-arrow-down') + ->icon('heroicon-m-document-magnifying-glass') ->color('gray') ->url(fn (InjectorProtocol $r) => route('tenant.injector-protocols.pdf', ['record' => $r->id])) ->openUrlInNewTab(), diff --git a/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/EditInjectorProtocol.php b/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/EditInjectorProtocol.php index a70eb40..0cb1943 100644 --- a/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/EditInjectorProtocol.php +++ b/app/Filament/Tenant/Resources/InjectorProtocolResource/Pages/EditInjectorProtocol.php @@ -15,8 +15,8 @@ class EditInjectorProtocol extends EditRecord { return [ Actions\Action::make('pdf') - ->label(__('Descarcă PDF')) - ->icon('heroicon-m-document-arrow-down') + ->label(__('Vizualizare PDF')) + ->icon('heroicon-m-document-magnifying-glass') ->color('gray') ->url(fn (InjectorProtocol $record) => route('tenant.injector-protocols.pdf', ['record' => $record->id])) ->openUrlInNewTab(), diff --git a/app/Filament/Tenant/Resources/WorkOrderResource.php b/app/Filament/Tenant/Resources/WorkOrderResource.php index e7581bc..0cbb2df 100644 --- a/app/Filament/Tenant/Resources/WorkOrderResource.php +++ b/app/Filament/Tenant/Resources/WorkOrderResource.php @@ -186,17 +186,10 @@ class WorkOrderResource extends Resource ->actions([ Actions\Action::make('pdf') ->label(__('PDF')) - ->icon('heroicon-m-document-arrow-down') + ->icon('heroicon-m-document-magnifying-glass') ->color('gray') - ->action(function (WorkOrder $r) { - $svc = app(\App\Services\WorkOrderPdfService::class); - $pdf = $svc->generate($r); - $filename = $svc->filename($r); - return response()->streamDownload( - fn () => print($pdf->output()), - $filename - ); - }), + ->url(fn (WorkOrder $r) => url('/app/work-orders/' . $r->id . '/pdf')) + ->openUrlInNewTab(), Actions\EditAction::make(), Actions\DeleteAction::make(), ]) diff --git a/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php b/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php index 19b5c78..5e5e0ef 100644 --- a/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php +++ b/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php @@ -4,7 +4,6 @@ namespace App\Filament\Tenant\Resources\WorkOrderResource\Pages; use App\Filament\Tenant\Resources\WorkOrderResource; use App\Models\Tenant\WorkOrder; -use App\Services\WorkOrderPdfService; use Filament\Actions; use Filament\Resources\Pages\EditRecord; @@ -85,19 +84,11 @@ class EditWorkOrder extends EditRecord 'wo' => $this->record, ])), Actions\Action::make('pdf') - ->label(__('Descarcă PDF')) - ->icon('heroicon-m-document-arrow-down') + ->label(__('Vizualizare PDF')) + ->icon('heroicon-m-document-magnifying-glass') ->color('gray') - ->action(function () { - /** @var WorkOrder $wo */ - $wo = $this->record; - $svc = app(WorkOrderPdfService::class); - $pdf = $svc->generate($wo); - return response()->streamDownload( - fn () => print($pdf->output()), - $svc->filename($wo) - ); - }), + ->url(fn () => url('/app/work-orders/' . $this->record->id . '/pdf')) + ->openUrlInNewTab(), Actions\DeleteAction::make(), ]; } diff --git a/lang/en.json b/lang/en.json index e023f8f..37d190b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2207,8 +2207,10 @@ "Vizitator": "Viewer", "Vizite": "Visits", "Vizual": "Visual", + "Vizualizare PDF": "View PDF", "Vizualizare dashboard": "Dashboard view", "Vizualizează": "View", + "Vizualizează factură (PDF)": "View invoice (PDF)", "Vizualizări": "Views", "Vopsea sărită": "Paint chip", "Vopsire": "Painting", diff --git a/lang/ru.json b/lang/ru.json index d291458..0e9fed2 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -2207,8 +2207,10 @@ "Vizitator": "Гость", "Vizite": "Визиты", "Vizual": "Визуально", + "Vizualizare PDF": "Просмотр PDF", "Vizualizare dashboard": "Дашборд", "Vizualizează": "Просмотр", + "Vizualizează factură (PDF)": "Просмотр счёта (PDF)", "Vizualizări": "Просмотры", "Vopsea sărită": "Скол краски", "Vopsire": "Покраска", diff --git a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php index 695ed0f..aebdffc 100644 --- a/resources/views/filament/tenant/pages/work-order-dashboard.blade.php +++ b/resources/views/filament/tenant/pages/work-order-dashboard.blade.php @@ -495,9 +495,15 @@ @if ($wo->vehicle) @php + // 1) Vehicle's own photo (if the model uses MediaLibrary), else + // 2) fall back to the first WO photo (uploaded via edit form). $photoUrl = method_exists($wo->vehicle, 'getFirstMediaUrl') ? ($wo->vehicle->getFirstMediaUrl('photos') ?: null) : null; + if (! $photoUrl) { + $woFirst = $wo->getFirstMedia('photos'); + $photoUrl = $woFirst?->getFullUrl(); + } @endphp @if ($photoUrl) {{ $wo->vehicle->make }} @@ -683,7 +689,10 @@
- 📄 {{ __('Descarcă factură (PDF)') }} + 🔍 {{ __('Vizualizează factură (PDF)') }} + + + ⬇️ {{ __('Descarcă factură (PDF)') }} @php $signed = $wo->getMedia('signed_documents'); @endphp @if ($signed->isNotEmpty()) diff --git a/routes/web.php b/routes/web.php index fac7140..28691c8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -47,13 +47,21 @@ Route::post('/payments/paypal/webhook', [\App\Http\Controllers\PaymentController Route::post('/payments/paynet/webhook', [\App\Http\Controllers\PaymentController::class, 'paynetWebhook']) ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]); -// PDF download for a work order (used from dashboard's Documents tab). -Route::get('/app/work-orders/{workOrder}/pdf', function (int $workOrder) { +// PDF for a work order. Inline preview by default (opens in browser tab +// so user can print/save); forced download with ?download=1. +Route::get('/app/work-orders/{workOrder}/pdf', function (Request $request, int $workOrder) { abort_unless(auth('web')->check(), 403); $wo = \App\Models\Tenant\WorkOrder::findOrFail($workOrder); $svc = app(\App\Services\WorkOrderPdfService::class); $pdf = $svc->generate($wo); - return response()->streamDownload(fn () => print($pdf->output()), $svc->filename($wo)); + $bytes = $pdf->output(); + $filename = $svc->filename($wo); + $disposition = $request->boolean('download') ? 'attachment' : 'inline'; + return response($bytes, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => $disposition . '; filename="' . $filename . '"', + 'Cache-Control' => 'private, no-store', + ]); })->name('work-orders.pdf'); // User invitation accept flow (no auth required — token is the credential). @@ -82,17 +90,20 @@ Route::middleware(['web', 'auth'])->group(function () { Route::post('/push/unsubscribe', [\App\Http\Controllers\PushSubscriptionController::class, 'unsubscribe']) ->name('push.unsubscribe'); - Route::get('/app/injector-protocols/{record}/pdf', function (\App\Models\Tenant\InjectorProtocol $record) { + Route::get('/app/injector-protocols/{record}/pdf', function (Request $request, \App\Models\Tenant\InjectorProtocol $record) { abort_unless( auth()->user()?->canDo(\App\Auth\Permissions::INJECTOR_PROTOCOLS_VIEW), 403 ); $svc = app(\App\Services\InjectorProtocolPdfService::class); - return response()->streamDownload( - fn () => print($svc->render($record)), - $svc->filename($record), - ['Content-Type' => 'application/pdf'] - ); + $bytes = $svc->render($record); + $filename = $svc->filename($record); + $disposition = $request->boolean('download') ? 'attachment' : 'inline'; + return response($bytes, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => $disposition . '; filename="' . $filename . '"', + 'Cache-Control' => 'private, no-store', + ]); })->name('tenant.injector-protocols.pdf'); }); diff --git a/tests/Feature/CalendarEnhancementsTest.php b/tests/Feature/CalendarEnhancementsTest.php index 70630d9..1379b9e 100644 --- a/tests/Feature/CalendarEnhancementsTest.php +++ b/tests/Feature/CalendarEnhancementsTest.php @@ -168,10 +168,11 @@ class CalendarEnhancementsTest extends TestCase $response = $page->exportPdf(); - // It's a StreamedResponse - $this->assertInstanceOf(\Symfony\Component\HttpFoundation\StreamedResponse::class, $response); + // Inline PDF response (was StreamedResponse before we switched to preview-by-default). + $this->assertSame('application/pdf', $response->headers->get('content-type')); $cd = $response->headers->get('content-disposition'); + $this->assertStringStartsWith('inline;', $cd); $this->assertStringContainsString('programari_', $cd); - $this->assertStringEndsWith('.pdf', $cd); + $this->assertStringContainsString('.pdf', $cd); } }