feat(pdf): inline preview by default with ?download=1 override + vehicle photo fallback
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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. */
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+2
-2
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
])
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2207,8 +2207,10 @@
|
||||
"Vizitator": "Гость",
|
||||
"Vizite": "Визиты",
|
||||
"Vizual": "Визуально",
|
||||
"Vizualizare PDF": "Просмотр PDF",
|
||||
"Vizualizare dashboard": "Дашборд",
|
||||
"Vizualizează": "Просмотр",
|
||||
"Vizualizează factură (PDF)": "Просмотр счёта (PDF)",
|
||||
"Vizualizări": "Просмотры",
|
||||
"Vopsea sărită": "Скол краски",
|
||||
"Vopsire": "Покраска",
|
||||
|
||||
@@ -495,9 +495,15 @@
|
||||
</div>
|
||||
@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)
|
||||
<img class="wd-vehicle-photo" src="{{ $photoUrl }}" alt="{{ $wo->vehicle->make }}">
|
||||
@@ -683,7 +689,10 @@
|
||||
<div x-show="tab === 'docs'" x-cloak>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf') }}" target="_blank">
|
||||
📄 {{ __('Descarcă factură (PDF)') }}
|
||||
🔍 {{ __('Vizualizează factură (PDF)') }}
|
||||
</a>
|
||||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf?download=1') }}">
|
||||
⬇️ {{ __('Descarcă factură (PDF)') }}
|
||||
</a>
|
||||
@php $signed = $wo->getMedia('signed_documents'); @endphp
|
||||
@if ($signed->isNotEmpty())
|
||||
|
||||
+20
-9
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user