diff --git a/.sisyphus/notepads/edit-page-restructure/learnings.md b/.sisyphus/notepads/edit-page-restructure/learnings.md index 593aad0..ee38c52 100644 --- a/.sisyphus/notepads/edit-page-restructure/learnings.md +++ b/.sisyphus/notepads/edit-page-restructure/learnings.md @@ -297,3 +297,22 @@ ### Vue Field Definition Pattern - 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 + +## 2026-03-29 — Service list status columns update + +### Implementation +- Updated `ServiceController::index()` to compute and pass `agenda_slides_count` +- `agenda_slides_count` uses correlated subquery on `service_agenda_items`: counts non-song items (`service_song_id IS NULL`) that have at least 1 non-deleted slide via `whereExists()` +- Updated `has_sermon_slides` logic: if `Setting::get('agenda_sermon_matching')` is configured, checks agenda items matching the pattern for slides (using `AgendaMatcherService`); otherwise falls back to legacy `slides.type = 'sermon'` subquery +- Eager-loads `agendaItems` (non-song only) with slides for the sermon check in the map callback +- Updated `Index.vue` to show `{count} weitere Folien` when `agenda_slides_count > 0`, with `data-testid="agenda-slides-count"` + +### Test Coverage (3 new tests) +1. `services_index_zaehlt_agenda_slides_fuer_nicht_song_items` — verifies non-song agenda items with slides count correctly (song items excluded) +2. `services_index_sermon_check_nutzt_agenda_matching_wenn_konfiguriert` — sermon slides via agenda item with setting +3. `services_index_sermon_ohne_agenda_slides_zeigt_false` — no slides on sermon agenda item → false + +### Key Patterns +- `addSelect` with correlated subquery using `ServiceAgendaItem::query()` + `whereExists(Slide::query()...)` +- Sermon check done in PHP (map callback) because `fnmatch()` pattern matching can't be done in SQL +- `->with(['agendaItems' => fn ($q) => $q->whereNull('service_song_id')])` for efficient eager loading of only non-song agenda items diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index 4d489a9..95d2d77 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\Service; +use App\Models\ServiceAgendaItem; use App\Models\Setting; use App\Models\Slide; use App\Models\Song; @@ -32,6 +33,9 @@ public function index(): Response ->orderBy('date'); } + $sermonPatterns = Setting::get('agenda_sermon_matching'); + $matcher = $sermonPatterns ? app(AgendaMatcherService::class) : null; + $services = $query ->withCount([ 'serviceSongs as songs_total_count', @@ -61,24 +65,55 @@ public function index(): Response fn ($q) => $q ->whereColumn(DB::raw('DATE(slides.uploaded_at)'), '<=', 'services.date') ), + 'agenda_slides_count' => ServiceAgendaItem::query() + ->selectRaw('COUNT(*)') + ->whereColumn('service_agenda_items.service_id', 'services.id') + ->whereNull('service_agenda_items.service_song_id') + ->whereExists( + Slide::query() + ->selectRaw('1') + ->whereColumn('slides.service_agenda_item_id', 'service_agenda_items.id') + ->whereNull('slides.deleted_at') + ), ]) + ->with(['agendaItems' => fn ($q) => $q->whereNull('service_song_id')]) + ->with(['agendaItems.slides' => fn ($q) => $q->whereNull('deleted_at')]) ->get() - ->map(fn (Service $service) => [ - 'id' => $service->id, - 'cts_event_id' => $service->cts_event_id, - 'title' => $service->title, - 'date' => $service->date?->toDateString(), - 'preacher_name' => $service->preacher_name, - 'beamer_tech_name' => $service->beamer_tech_name, - 'last_synced_at' => $service->last_synced_at?->toJSON(), - 'updated_at' => $service->updated_at?->toJSON(), - 'finalized_at' => $service->finalized_at?->toJSON(), - 'songs_total_count' => (int) $service->songs_total_count, - 'songs_mapped_count' => (int) $service->songs_mapped_count, - 'songs_arranged_count' => (int) $service->songs_arranged_count, - 'has_sermon_slides' => (bool) $service->has_sermon_slides, - 'info_slides_count' => (int) $service->info_slides_count, - ]) + ->map(function (Service $service) use ($matcher, $sermonPatterns) { + // Determine sermon slides status + $hasSermonSlides = (bool) $service->has_sermon_slides; + if ($matcher && $sermonPatterns) { + $sermonItems = $service->agendaItems->filter( + fn (ServiceAgendaItem $item) => $matcher->matchesAny( + $item->title, + array_map('trim', explode(',', $sermonPatterns)) + ) + ); + if ($sermonItems->isNotEmpty()) { + $hasSermonSlides = $sermonItems->contains( + fn (ServiceAgendaItem $item) => $item->slides->isNotEmpty() + ); + } + } + + return [ + 'id' => $service->id, + 'cts_event_id' => $service->cts_event_id, + 'title' => $service->title, + 'date' => $service->date?->toDateString(), + 'preacher_name' => $service->preacher_name, + 'beamer_tech_name' => $service->beamer_tech_name, + 'last_synced_at' => $service->last_synced_at?->toJSON(), + 'updated_at' => $service->updated_at?->toJSON(), + 'finalized_at' => $service->finalized_at?->toJSON(), + 'songs_total_count' => (int) $service->songs_total_count, + 'songs_mapped_count' => (int) $service->songs_mapped_count, + 'songs_arranged_count' => (int) $service->songs_arranged_count, + 'has_sermon_slides' => $hasSermonSlides, + 'info_slides_count' => (int) $service->info_slides_count, + 'agenda_slides_count' => (int) $service->agenda_slides_count, + ]; + }) ->values(); return Inertia::render('Services/Index', [ diff --git a/resources/js/Pages/Services/Index.vue b/resources/js/Pages/Services/Index.vue index 87b6f03..7922c77 100644 --- a/resources/js/Pages/Services/Index.vue +++ b/resources/js/Pages/Services/Index.vue @@ -338,6 +338,15 @@ function stateIconClass(isDone) { {{ service.info_slides_count }} Infofolien +