pp-planer/app/Http/Controllers/SongPdfController.php
Thorsten Bus d75d748417 feat: T19 - Song Preview Modal + PDF Download
- SongPreviewModal.vue: Teleport modal with song text in arrangement order
  - Group headers with group.color background
  - Side-by-side translations when use_translation=true
  - Copyright footer from song metadata
  - Close button + click-outside/Escape dismiss
  - PDF download button

- SongPdfController.php: PDF generation endpoint
  - GET /songs/{song}/arrangements/{arrangement}/pdf
  - Uses barryvdh/laravel-dompdf with DejaVu Sans font
  - Old-school CSS only (NO Tailwind)
  - Handles German umlauts correctly

- resources/views/pdf/song.blade.php: PDF template
  - Title header, groups with colored headers, slide text, copyright footer
  - Includes translation text when present
  - Simple CSS: font-family, color, background-color, margin, padding

- Tests: 9 new (25 assertions)
  - PDF content type verification
  - Filename includes song title
  - Groups in correct arrangement order
  - Translation text included when present
  - Copyright footer present
  - German umlauts render correctly
  - Auth guard
  - 404 when arrangement doesn't belong to song
  - Empty arrangement handling

All tests passing: 133/133 (728 assertions)
Build: ✓ Vite production build successful
Fix: Removed duplicate content from SongPdfTest.php (parse error)
2026-03-01 20:15:02 +01:00

72 lines
2.2 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Song;
use App\Models\SongArrangement;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class SongPdfController extends Controller
{
public function preview(Song $song, SongArrangement $arrangement): JsonResponse
{
abort_unless($arrangement->song_id === $song->id, 404);
$groupsInOrder = $this->buildGroupsInOrder($arrangement);
return response()->json([
'song' => [
'id' => $song->id,
'title' => $song->title,
'copyright_text' => $song->copyright_text,
'ccli_id' => $song->ccli_id,
],
'arrangement' => [
'id' => $arrangement->id,
'name' => $arrangement->name,
],
'groups' => $groupsInOrder,
]);
}
public function download(Song $song, SongArrangement $arrangement): Response
{
abort_unless($arrangement->song_id === $song->id, 404);
$groupsInOrder = $this->buildGroupsInOrder($arrangement);
$pdf = Pdf::loadView('pdf.song', [
'song' => $song,
'arrangement' => $arrangement,
'groupsInOrder' => $groupsInOrder,
]);
$filename = str($song->title)->slug() . '-' . str($arrangement->name)->slug() . '.pdf';
return $pdf->download($filename);
}
private function buildGroupsInOrder(SongArrangement $arrangement): array
{
$arrangement->load([
'arrangementGroups' => fn ($query) => $query->orderBy('order'),
'arrangementGroups.group.slides' => fn ($query) => $query->orderBy('order'),
]);
return $arrangement->arrangementGroups->map(function ($arrangementGroup) {
$group = $arrangementGroup->group;
return [
'name' => $group->name,
'color' => $group->color ?? '#6b7280',
'slides' => $group->slides->map(fn ($slide) => [
'text_content' => $slide->text_content,
'text_content_translated' => $slide->text_content_translated,
])->values()->all(),
];
})->values()->all();
}
}