T14: Service Edit Page Layout + Routing
- ServiceController::edit() with eager-loaded relationships
- Services/Edit.vue with 4 collapsible accordion blocks
- Route: GET /services/{service}/edit
- Information slides query: global + service-specific with expire_date filtering
- Tests: 2 new (edit page render + auth guard)
T15: Information Block (Slides + Expire Dates)
- InformationBlock.vue with dynamic expire_date filtering
- Shows slides where type='information' AND expire_date >= service.date
- Global visibility across services (not service-specific)
- SlideUploader with showExpireDate=true
- SlideGrid with prominent expire date + inline editing
- Badge showing slide count + 'expiring soon' warning (within 3 days)
- Tests: 7 new (105 assertions)
T16: Moderation Block (Service-Specific)
- ModerationBlock.vue (service-specific slides)
- Filters: type='moderation' AND service_id = current_service
- No expire date field (unlike Information block)
- Service isolation (slides from Service A don't appear in Service B)
- Tests: 5 new (14 assertions)
T17: Sermon Block (Service-Specific)
- SermonBlock.vue (identical to Moderation but type='sermon')
- Service-specific slides, no expire date
- Tests: 5 new (14 assertions)
T18: Songs Block (Matching + Arrangement + Translation)
- SongsBlock.vue with conditional UI (unmatched vs matched states)
- Unmatched: 'Erstellung anfragen' button + searchable select for manual assign
- Matched: ArrangementConfigurator + translation checkbox + preview/download buttons
- ServiceSongController::update() for use_translation and song_arrangement_id
- ArrangementConfigurator emits 'arrangement-selected' for auto-save
- ServiceController::edit() provides songsCatalog for matching search
- Tests: 2 new (45 assertions)
T19: Song PDF (INCOMPLETE - timeout)
- SongPdfController.php created (partial)
- resources/views/pdf/song.blade.php created (partial)
- SongPreviewModal.vue MISSING
- Tests MISSING
- Will be completed in next commit
All tests passing: 124/124 (703 assertions)
Build: ✓ Vite production build successful
German UI: All user-facing text in German with 'Du' form
Dependencies: barryvdh/laravel-dompdf added for PDF generation
123 lines
10 KiB
Markdown
123 lines
10 KiB
Markdown
- 2026-03-01: Fuer 1920x1080 Slide-Output ohne Upscaling funktioniert in Intervention Image v3 die Kombination aus schwarzer Canvas (`create()->fill('000000')`), `scaleDown(width: 1920, height: 1080)` und zentriertem `place(...)` stabil.
|
|
- 2026-03-01: Bei Fake-Storage in Tests muessen Zielordner vor direktem Intervention-`save()` explizit erstellt werden (`makeDirectory`/`mkdir`), sonst wirft Intervention `NotWritableException`.
|
|
- 2026-03-01: Fuer Testverifikation von Letterbox/Pillarbox sind farbige PNG-Testbilder sinnvoller als `UploadedFile::fake()->image(...)`, weil Fake-Bilder sonst komplett schwarz sein koennen.
|
|
- 2026-03-01: CTS-Sync laeuft stabil mit `EventRequest::where("from", heute)` + `EventAgendaRequest::fromEvent(...)->get()`, wenn Services per `cts_event_id` und Agenda-Songs per (`service_id`,`order`) upserted werden; CCLI-Matching bleibt strikt auf `songs.ccli_id` und setzt nur dann `song_id`/`matched_at`.
|
|
- 2026-03-01: SongController CRUD nutzt `auth:sanctum` Middleware; `actingAs()` in Tests funktioniert damit problemlos (Sanctum unterstuetzt Session-Auth in Tests).
|
|
- 2026-03-01: SQLite gibt `date`-Spalten als `YYYY-MM-DD 00:00:00` zurueck statt `YYYY-MM-DD` — Accessor muss `substr($date, 0, 10)` nutzen fuer saubere Date-Only Werte.
|
|
- 2026-03-01: `Attribute::get()` in Laravel 12 fuer berechnete Accessors statt altem `get{Name}Attribute()` Pattern. Snake_case `last_used_in_service` mapped automatisch auf `lastUsedInService()` Methode.
|
|
- 2026-03-01: Default-Gruppen (Strophe 1=#3B82F6, Refrain=#10B981, Bridge=#F59E0B) und Default-Arrangement 'Normal' werden automatisch bei Song-Erstellung via SongService erzeugt.
|
|
- 2026-03-01: `Rule::unique('songs', 'ccli_id')->ignore($songId)->whereNull('deleted_at')` stellt sicher, dass Soft-Deleted Songs die Unique-Constraint nicht blockieren.
|
|
- 2026-03-01: `bootstrap/app.php` braucht explizit `api: __DIR__.'/../routes/api.php'` in `withRouting()` — ist nicht automatisch registriert in Laravel 12.
|
|
- 2026-03-01: Service-Listenstatus laesst sich performant in einem Query aggregieren via `withCount(...)` fuer Song-Metriken plus `addSelect`-Subqueries fuer `has_sermon_slides` und datumsabhaengige `info_slides_count` (inkl. globaler `information`-Slides mit `service_id = null`).
|
|
- 2026-03-01: TranslationService line-count distribution: iterate groups (by order) → slides (by order), for each slide count lines in `text_content`, then slice that many lines from the translated text array. `array_slice` + offset tracking works cleanly.
|
|
- 2026-03-01: URL scraping is best-effort only: `Http::timeout(10)->get($url)` + `strip_tags()` + `trim()`. Return null on any failure — no exceptions bubble up. PHP 8.1+ allows `catch (\Exception)` without variable capture.
|
|
- 2026-03-01: Translation routes: `POST /api/translation/fetch-url` (preview), `POST /api/songs/{song}/translation/import` (save), `DELETE /api/songs/{song}/translation` (remove). All under `auth:sanctum` middleware.
|
|
- 2026-03-01: `removeTranslation` uses a two-step approach: collect slide IDs via `SongSlide::whereIn('song_group_id', $song->groups()->pluck('id'))` then bulk-update `text_content_translated = null`, avoiding N+1 queries.
|
|
- 2026-03-01: Der Arrangement-Konfigurator bleibt stabil bei mehrfachen Gruppeninstanzen, wenn die Sequenz mit Vue-Keys im Muster `${group.id}-${index}` gerendert und die Reihenfolge nach jedem Drag-End sofort per `router.put(..., { preserveScroll: true })` gespeichert wird.
|
|
|
|
## [2026-03-01] Wave 2 Complete — T8-T13
|
|
|
|
### T11: Arrangement Configurator
|
|
- ArrangementController: store (clone from default), clone (duplicate existing), update (reorder groups), destroy (prevent last)
|
|
- ArrangementConfigurator.vue: vue-draggable-plus with clone mode for group pills
|
|
- **CRITICAL**: Vue key for repeating groups MUST be `${group.id}-${index}` NOT just `group.id` (groups repeat in arrangements)
|
|
- Color picker integration: group_colors array in update payload, applied to SongGroup records
|
|
- Default arrangement protection: if deleting is_default=true, promote next arrangement to default
|
|
- Tests: 4 passing (17 assertions)
|
|
|
|
### T12: Song Matching Service
|
|
- SongMatchingService: autoMatch (CCLI), manualAssign, requestCreation (email), unassign
|
|
- ServiceSongController: API endpoints for /api/service-songs/{id}/assign, /request, /unassign
|
|
- Auto-match runs during CTS sync (ChurchToolsService updated to call autoMatch)
|
|
- MissingSongRequest mailable already existed from T7, reused here
|
|
- matched_at timestamp tracks when assignment occurred
|
|
- Tests: 14 passing (33 assertions)
|
|
|
|
### T13: Translation Service
|
|
- TranslationService: fetchFromUrl (HTTP + strip_tags), importTranslation (line-count distribution), removeTranslation
|
|
- TranslationController: POST /translation/fetch-url, POST /songs/{song}/translation/import, DELETE /songs/{song}/translation
|
|
- Line-count algorithm: for each slide (ordered by group.order, slide.order), take N lines from translated text where N = original slide line count
|
|
- Best-effort URL scraping: Http::timeout(10), catch all exceptions, return null on failure
|
|
- has_translation flag on Song model tracks translation state
|
|
- Tests: 18 passing (18 assertions)
|
|
|
|
### Wave 2 Summary
|
|
- All 6 tasks completed in parallel delegation
|
|
- T11 timed out during polling but work completed successfully
|
|
- Total: 103 tests passing (488 assertions)
|
|
- Vite build: ✓ successful
|
|
- Commit: d915f8c (27 files, +3951/-25 lines)
|
|
|
|
### Next: Wave 3 (T14-T19)
|
|
- Service Edit page layout + 4 blocks (Information, Moderation, Sermon, Songs)
|
|
- Song preview modal + PDF download
|
|
- All blocks integrate SlideUploader/SlideGrid from T10
|
|
- ArrangementConfigurator from T11 embedded in Songs block
|
|
|
|
## [2026-03-01] T16: Moderation Block (Service-Specific Slides)
|
|
|
|
- ModerationBlock.vue: Simple wrapper around SlideUploader + SlideGrid with `showExpireDate=false`
|
|
- Filtering: `type='moderation' AND service_id = current_service` via computed property
|
|
- SlideUploader/SlideGrid already support `showExpireDate` prop (from T10)
|
|
- Service-specific filtering ensures moderation slides from Service A don't appear in Service B
|
|
- No expire_date field anywhere in UI (unlike Information block)
|
|
- Tests: 5 passing (14 assertions) — verifies service-specific isolation
|
|
- Vite build: ✓ successful
|
|
|
|
### T17: Sermon Block (Service-Specific Slides)
|
|
- SermonBlock.vue: Identical to ModerationBlock but with `type='sermon'`
|
|
- Filters slides: `type='sermon' AND service_id = current_service`
|
|
- Uses SlideUploader with `type="sermon"`, `serviceId={current}`, `showExpireDate={false}`
|
|
- Uses SlideGrid with `showExpireDate={false}`
|
|
- Tests: 5 passing (14 assertions) — verifies service-specific filtering, type isolation, no expire_date
|
|
- Build: ✓ successful
|
|
- 2026-03-01: Songs-Block nutzt fuer unmatched CTS-Songs eine lokale Such-Eingabe plus gefiltertes Select (Titel/CCLI), danach `POST /api/service-songs/{id}/assign` und soft reload nur von `serviceSongs`.
|
|
- 2026-03-01: Arrangement-Auswahl wird ueber ein neues `arrangement-selected` Event aus dem ArrangementConfigurator nach oben gemeldet und per `PATCH /api/service-songs/{id}` als `song_arrangement_id` sofort gespeichert.
|
|
- 2026-03-01: Translation-Toggle im Songs-Block speichert direkt per `PATCH /api/service-songs/{id}` (`use_translation`) und bleibt so ohne separaten Save-Button konsistent mit dem Auto-Save-Prinzip.
|
|
|
|
## [2026-03-01] T15: Information Block (Slides + Expire Dates)
|
|
|
|
- InformationBlock.vue: Wraps SlideUploader + SlideGrid with `showExpireDate=true` and `serviceId=null` (global slides)
|
|
- Server-side filtering in ServiceController.edit(): `type='information' AND expire_date >= service.date AND (service_id IS NULL OR service_id = current)`
|
|
- Information slides are GLOBAL — not tied to a specific service, appear in all services where `expire_date >= service.date`
|
|
- The `whereNull('deleted_at')` in edit() query is redundant with SoftDeletes trait but harmless
|
|
- Slides without expire_date are excluded from information block (require scheduling)
|
|
- SlideUploader passes `serviceId=null` for information slides (they're global, not service-specific)
|
|
- ExpiringSoonCount computed: badges warn when slides expire within 3 days of service date
|
|
- Edit.vue updated: replaced placeholder for 'information' block key with actual InformationBlock component
|
|
- `router.reload({ preserveScroll: true })` used for refreshing page after slide upload/delete/update
|
|
- Tests: 7 passing (105 assertions) — covers expire date filtering, global visibility, soft-delete exclusion, type isolation, null expire_date, ordering
|
|
- Full suite: 122 tests passing (658 assertions)
|
|
- Vite build: ✓ successful
|
|
|
|
|
|
## [2026-03-01] T14: Service Edit Page Layout + Routing
|
|
|
|
### ServiceController::edit()
|
|
- Eager-loads `serviceSongs` (ordered), `serviceSongs.song`, `serviceSongs.arrangement`, `slides`
|
|
- Information slides query is complex: global (service_id=null) + service-specific, filtered by expire_date >= service.date
|
|
- Moderation/sermon slides filtered from loaded `$service->slides` collection via `->where('type', ...)`
|
|
- Service data returned as explicit array (not full model) to control frontend shape
|
|
- serviceSongs mapped to include nested song/arrangement data as null-safe arrays
|
|
|
|
### Edit.vue Page Pattern
|
|
- Uses collapsible accordion blocks (expandedBlocks ref with boolean per key)
|
|
- 4 blocks: Information, Moderation, Predigt, Songs — each with colored gradient icon, badge count, chevron toggle
|
|
- Block content area is placeholder div (T15-T18 will replace with actual components)
|
|
- Vue Transition with max-h trick for collapse animation
|
|
- `router.get(route('services.index'))` for back navigation
|
|
- German date format: `toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' })`
|
|
|
|
### Index.vue Integration
|
|
- "Bearbeiten" button now uses `router.get(route('services.edit', service.id))` instead of showComingSoon
|
|
|
|
### Route
|
|
- `GET /services/{service}/edit` → `services.edit` named route, uses implicit model binding
|
|
- Placed after finalize/reopen POST routes in web.php
|
|
|
|
### Test Pattern
|
|
- PHPUnit style (not Pest) in this project's ServiceControllerTest
|
|
- `$this->withoutVite()` required for Inertia page assertion tests
|
|
- `assertInertia(fn ($page) => $page->component(...)->has(...)->where(...))` for deep prop assertions
|
|
- Auth test: unauthenticated GET redirects to login route
|