feat(export): wire agenda export into download flow
This commit is contained in:
parent
45955b70a2
commit
18d0d6f965
|
|
@ -250,3 +250,50 @@ ### Test Infrastructure
|
|||
- `createSlideFile()` calls `Storage::fake('public')` for each slide
|
||||
- Mock playlist output: `mock-playlist:{name}\n{item1}\n{item2}` — enables position assertions via `strpos()`
|
||||
- 2 pre-existing test failures (HTTP route tests) due to empty propresenter parser src — not regressions
|
||||
|
||||
## 2026-03-29 — Finalize → Download integration tests
|
||||
|
||||
### Verification
|
||||
- `ServiceController::download()` already calls `PlaylistExportService->generatePlaylist($service)` — no controller changes needed
|
||||
- `PlaylistExportService::generatePlaylist()` already routes to agenda path or legacy fallback (Task 9)
|
||||
- Main deliverable: integration tests proving full finalize → download flow
|
||||
|
||||
### Integration Tests Added (PlaylistExportTest.php)
|
||||
1. `test_finalize_und_download_flow_mit_agenda_items` — creates service with agenda items (2 songs + sermon with slides), finalizes, downloads, verifies playlist ordering (song1 → sermon → song2)
|
||||
2. `test_finalize_und_download_flow_legacy_ohne_agenda` — creates service without agenda items, finalizes, downloads, verifies legacy path produces .proplaylist file
|
||||
|
||||
### Test Patterns
|
||||
- Both tests use `@runInSeparateProcess` + `@preserveGlobalState disabled` + `mockProPresenterClasses()` (Mockery alias)
|
||||
- Full HTTP flow: POST finalize → refresh → GET download → assert response headers
|
||||
- Agenda test verifies playlist content ordering via `strpos()` position comparison
|
||||
- Legacy test verifies backward compat: no ServiceAgendaItems → uses `generatePlaylistLegacy()`
|
||||
|
||||
## 2026-03-29 — Settings.vue Agenda Configuration UI
|
||||
|
||||
### Implementation
|
||||
- Updated `resources/js/Pages/Settings.vue` with new Agenda-Konfiguration section
|
||||
- Added 4 new fields to the `fields` array with `section: 'agenda'` property:
|
||||
1. `agenda_start_title` — Ablauf-Start pattern
|
||||
2. `agenda_end_title` — Ablauf-Ende pattern
|
||||
3. `agenda_announcement_position` — Ankündigungen-Position with helpText
|
||||
4. `agenda_sermon_matching` — Predigt-Erkennung with helpText
|
||||
- Added `section: 'macro'` to existing 4 macro fields for clear separation
|
||||
- Updated macro section template: `fields.filter(f => f.section === 'macro')`
|
||||
- Created new agenda section with identical field input structure
|
||||
- Section displays:
|
||||
- Header: "Agenda-Konfiguration"
|
||||
- Subtitle: "Diese Einstellungen steuern, wie der Gottesdienst-Ablauf angezeigt und exportiert wird."
|
||||
- Fields with auto-save on blur (same `saveField()` function)
|
||||
- Conditional helpText display (only if `field.helpText` present)
|
||||
- Identical saving/saved/error indicators as macro section
|
||||
|
||||
### Build & Commit
|
||||
- `npm run build` — ✓ 803 modules transformed, 17.94s gzip size for Edit component (expected growth)
|
||||
- Committed: `feat(ui): add agenda settings to Settings page`
|
||||
|
||||
### Vue Field Definition Pattern
|
||||
- Using `section` property to group related fields
|
||||
- Template: `fields.filter(f => f.section === '{section}')`
|
||||
- Optional `helpText` property displayed below input (agenda fields only, not macro fields)
|
||||
- All fields use same `form[key]`, `saving[key]`, `saved[key]`, `errors[key]` reactive objects
|
||||
- On blur: `saveField(field.key)` triggers PATCH to `route('settings.update')` endpoint
|
||||
|
|
|
|||
|
|
@ -616,6 +616,197 @@ public function test_agenda_export_before_event_items_ausgeschlossen(): void
|
|||
$this->cleanupTempDir($result['temp_dir']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function test_finalize_und_download_flow_mit_agenda_items(): void
|
||||
{
|
||||
$this->mockProPresenterClasses();
|
||||
|
||||
$service = Service::factory()->create([
|
||||
'finalized_at' => null,
|
||||
'title' => 'Integrations Service',
|
||||
'date' => now(),
|
||||
]);
|
||||
|
||||
// Create songs with content
|
||||
$song1 = $this->createSongWithContent('Lobpreis Lied');
|
||||
$song2 = $this->createSongWithContent('Abschluss Lied');
|
||||
$arrangement1 = $song1->arrangements()->first();
|
||||
$arrangement2 = $song2->arrangements()->first();
|
||||
|
||||
$serviceSong1 = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song1->id,
|
||||
'song_arrangement_id' => $arrangement1->id,
|
||||
'cts_song_name' => 'Lobpreis Lied',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$serviceSong2 = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song2->id,
|
||||
'song_arrangement_id' => $arrangement2->id,
|
||||
'cts_song_name' => 'Abschluss Lied',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
// Agenda: Song → Sermon (non-song with slides) → Song
|
||||
ServiceAgendaItem::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'title' => 'Lobpreis Lied',
|
||||
'service_song_id' => $serviceSong1->id,
|
||||
'sort_order' => 1,
|
||||
'is_before_event' => false,
|
||||
]);
|
||||
|
||||
$sermonItem = ServiceAgendaItem::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'title' => 'Predigt',
|
||||
'service_song_id' => null,
|
||||
'sort_order' => 2,
|
||||
'is_before_event' => false,
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'title' => 'Abschluss Lied',
|
||||
'service_song_id' => $serviceSong2->id,
|
||||
'sort_order' => 3,
|
||||
'is_before_event' => false,
|
||||
]);
|
||||
|
||||
// Upload slides to sermon agenda item
|
||||
$this->createSlideFile('sermon_slide1.jpg');
|
||||
$this->createSlide([
|
||||
'type' => 'sermon',
|
||||
'service_id' => $service->id,
|
||||
'service_agenda_item_id' => $sermonItem->id,
|
||||
'original_filename' => 'sermon_slide1.jpg',
|
||||
'stored_filename' => 'sermon_slide1.jpg',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
// Add sermon slides for finalization check (legacy)
|
||||
Slide::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'type' => 'sermon',
|
||||
]);
|
||||
|
||||
// Step 1: Finalize the service
|
||||
$finalizeResponse = $this->actingAs($this->user)
|
||||
->postJson(route('services.finalize', $service), ['confirmed' => true]);
|
||||
|
||||
$finalizeResponse->assertOk()
|
||||
->assertJson([
|
||||
'needs_confirmation' => false,
|
||||
'success' => 'Service wurde abgeschlossen.',
|
||||
]);
|
||||
|
||||
$service->refresh();
|
||||
$this->assertNotNull($service->finalized_at);
|
||||
|
||||
// Step 2: Download the finalized service
|
||||
$downloadResponse = $this->actingAs($this->user)
|
||||
->get(route('services.download', $service));
|
||||
|
||||
$downloadResponse->assertOk();
|
||||
|
||||
$disposition = $downloadResponse->headers->get('content-disposition');
|
||||
$this->assertNotNull($disposition);
|
||||
$this->assertStringContainsString('.proplaylist', $disposition);
|
||||
|
||||
// Verify playlist content includes songs and sermon in correct order
|
||||
$tempPath = null;
|
||||
foreach (glob(sys_get_temp_dir().'/playlist-export-*/*.proplaylist') as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if (str_contains($content, 'Integrations Service')) {
|
||||
$tempPath = dirname($file);
|
||||
// Verify agenda ordering: song1 → sermon → song2
|
||||
$pos1 = strpos($content, 'Lobpreis Lied');
|
||||
$posSer = strpos($content, 'Predigt');
|
||||
$pos2 = strpos($content, 'Abschluss Lied');
|
||||
$this->assertNotFalse($pos1);
|
||||
$this->assertNotFalse($posSer);
|
||||
$this->assertNotFalse($pos2);
|
||||
$this->assertLessThan($posSer, $pos1, 'Song 1 should be before sermon');
|
||||
$this->assertLessThan($pos2, $posSer, 'Sermon should be before Song 2');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($tempPath) {
|
||||
$this->cleanupTempDir($tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* @preserveGlobalState disabled
|
||||
*/
|
||||
public function test_finalize_und_download_flow_legacy_ohne_agenda(): void
|
||||
{
|
||||
$this->mockProPresenterClasses();
|
||||
|
||||
$service = Service::factory()->create([
|
||||
'finalized_at' => null,
|
||||
'title' => 'Legacy Ablauf',
|
||||
'date' => now(),
|
||||
]);
|
||||
|
||||
$song = $this->createSongWithContent('Legacy Worship');
|
||||
$arrangement = $song->arrangements()->first();
|
||||
|
||||
ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song->id,
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'cts_song_name' => 'Legacy Worship',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
Slide::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'type' => 'sermon',
|
||||
]);
|
||||
|
||||
// No ServiceAgendaItems → will use legacy path
|
||||
|
||||
// Step 1: Finalize
|
||||
$finalizeResponse = $this->actingAs($this->user)
|
||||
->postJson(route('services.finalize', $service), ['confirmed' => false]);
|
||||
|
||||
$finalizeResponse->assertOk()
|
||||
->assertJson([
|
||||
'needs_confirmation' => false,
|
||||
'success' => 'Service wurde abgeschlossen.',
|
||||
]);
|
||||
|
||||
$service->refresh();
|
||||
$this->assertNotNull($service->finalized_at);
|
||||
|
||||
// Step 2: Download — uses legacy path (no agenda items)
|
||||
$downloadResponse = $this->actingAs($this->user)
|
||||
->get(route('services.download', $service));
|
||||
|
||||
$downloadResponse->assertOk();
|
||||
|
||||
$disposition = $downloadResponse->headers->get('content-disposition');
|
||||
$this->assertNotNull($disposition);
|
||||
$this->assertStringContainsString('.proplaylist', $disposition);
|
||||
$this->assertStringContainsString('Legacy Ablauf', $disposition);
|
||||
|
||||
// Cleanup temp files
|
||||
foreach (glob(sys_get_temp_dir().'/playlist-export-*') as $dir) {
|
||||
if (is_dir($dir)) {
|
||||
$this->cleanupTempDir($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupTempDir(string $dir): void
|
||||
{
|
||||
if (is_dir($dir)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue