1816 lines
83 KiB
Markdown
1816 lines
83 KiB
Markdown
# Service Edit Page Restructure — Agenda-Based View
|
||
|
||
## TL;DR
|
||
|
||
> **Quick Summary**: Replace the 4-block accordion service edit page (Information, Moderation, Sermon, Songs) with a full CTS agenda view showing all agenda items in order, with inline slide uploads, song arrangement management, and agenda-ordered presentation export.
|
||
>
|
||
> **Deliverables**:
|
||
> - New `service_agenda_items` DB table storing ALL CTS agenda items (not just songs)
|
||
> - Restructured edit page showing full agenda with slide upload per item
|
||
> - Redesigned arrangement dialog (two-column: pill list + lyric preview)
|
||
> - Three new global settings (announcement position, sermon recognition, agenda boundaries)
|
||
> - Agenda-ordered .proplaylist export
|
||
> - Updated service list status columns
|
||
> - PHP Feature tests + Playwright E2E tests for all changes
|
||
>
|
||
> **Estimated Effort**: Large
|
||
> **Parallel Execution**: YES — 5 waves
|
||
> **Critical Path**: Task 1 → Task 4 → Task 7 → Task 12 → Task 16 → Task 18 → F1-F4
|
||
|
||
---
|
||
|
||
## Context
|
||
|
||
### Original Request
|
||
Restructure the service edit page to show the complete CTS agenda instead of 4 separate blocks. Keep announcements at top; replace moderation/sermon/songs blocks with a unified agenda view. Every agenda element gets slide upload capability. Songs show arrangements inline. New arrangement dialog with two-column layout. Export follows agenda order.
|
||
|
||
### Interview Summary
|
||
**Key Discussions**:
|
||
- **Titel-Konfiguration**: Global in App-Settings (two separate fields for start/end with * wildcard)
|
||
- **Slide-Zuordnung**: Service-specific (bound to service + agenda item)
|
||
- **Announcement-Position**: Embedded in agenda at matched position during export
|
||
- **Nicht-Song Items**: Skipped in export if no slides or songs
|
||
- **Moderation**: Now a normal agenda item with "+" slide upload
|
||
- **Predigt**: Recognized via namesmatching setting (like announcements), with special upload area
|
||
- **Export-Format**: .proplaylist stays, but order follows agenda
|
||
- **Service-Liste**: Adapt status — songs + predigt stay, add count of additional items with slides
|
||
- **Arrangement '+' Button**: Quick-select showing ALL song groups (including already used ones)
|
||
- **2x Button**: Duplicates element directly below
|
||
- **Arrangement Preview**: Shows actual lyric text, translation in gray/italic/small below
|
||
- **Tests**: PHP Feature + Playwright E2E tests for all changes (TDD)
|
||
|
||
### Metis Review
|
||
**Identified Gaps** (addressed):
|
||
- **CTS API type values**: The actual `EventAgendaItem.type` values need real-world verification. Plan includes a diagnostic task to dump actual type values from the user's CTS instance before implementing filtering logic.
|
||
- **Data migration**: Existing finalized services must remain downloadable. Plan keeps backward compatibility — old export path preserved for services finalized before migration.
|
||
- **Slide type enum**: Instead of expanding the `slides.type` enum, plan uses a new `service_agenda_item_id` FK on slides table.
|
||
- **isBeforeEvent items**: Items with `isBeforeEvent=true` will be excluded from the visible agenda display.
|
||
- **Re-sync behavior**: If agenda items change in CTS, existing uploaded slides are preserved (orphaned but accessible).
|
||
- **Agenda item with song AND slides**: Song .pro file takes precedence; uploaded slides are appended as additional slides within the same presentation item.
|
||
|
||
---
|
||
|
||
## Work Objectives
|
||
|
||
### Core Objective
|
||
Replace the rigid 4-block service edit page with a dynamic agenda-based view that mirrors the actual CTS service plan, enabling slide uploads on any agenda item and exporting presentations in the correct agenda order.
|
||
|
||
### Concrete Deliverables
|
||
- Migration: `service_agenda_items` table
|
||
- Migration: `service_agenda_item_id` FK on `slides` table
|
||
- Model: `ServiceAgendaItem` with relationships
|
||
- Service: `AgendaMatcherService` (wildcard namesmatching)
|
||
- Updated: `ChurchToolsService::syncServiceAgendaItems()` storing ALL items
|
||
- Updated: `SettingsController` + `Settings.vue` with 4 new setting fields
|
||
- Updated: `PlaylistExportService` for agenda-ordered export
|
||
- Updated: `ServiceController::edit()` passing agenda items
|
||
- Updated: `ServiceController::index()` with new status counts
|
||
- New Vue: `AgendaItemRow.vue`, `SongAgendaItem.vue`, `AnnouncementPositionIndicator.vue`
|
||
- Redesigned Vue: `ArrangementDialog.vue` (two-column with scroll-sync)
|
||
- Restructured Vue: `Edit.vue` (agenda list replacing accordion)
|
||
- Updated: `Service::finalizationStatus()` for new model
|
||
- Tests: PHP Feature tests for all backend changes
|
||
- Tests: Playwright E2E tests for all frontend changes
|
||
|
||
### Definition of Done
|
||
- [x] `php artisan test` — all tests pass (existing + new)
|
||
- [x] `npx playwright test` — all E2E tests pass
|
||
- [x] `./vendor/bin/pint --test` — no formatting issues
|
||
- [x] Service edit page shows full CTS agenda
|
||
- [x] Songs show arrangement pills inline
|
||
- [x] Arrangement dialog has two-column layout with lyric preview
|
||
- [x] Export .proplaylist follows agenda order with announcements at matched position
|
||
- [x] Settings page has 4 new fields for agenda configuration
|
||
- [x] Service list shows updated status columns
|
||
|
||
### Must Have
|
||
- Full CTS agenda display (all item types, not just songs)
|
||
- Slide upload on any agenda item via "+" button
|
||
- Song arrangement pills (Vers1, Chorus, etc.) shown inline
|
||
- Redesigned arrangement dialog with lyric preview
|
||
- Announcement slides embedded at matched agenda position in export
|
||
- Sermon recognition via namesmatching
|
||
- Configurable agenda start/end boundaries
|
||
- Backward compatibility for existing finalized services
|
||
|
||
### Must NOT Have (Guardrails)
|
||
- MUST NOT change the CTS API integration fetcher pattern (injectable closures for testing)
|
||
- MUST NOT modify the propresenter-parser package
|
||
- MUST NOT touch SongDB page, auth/login, or file conversion logic
|
||
- MUST NOT allow drag-and-drop reordering of agenda items (order comes from CTS API, immutable)
|
||
- MUST NOT write to the CTS API (READ-ONLY constraint)
|
||
- MUST NOT remove existing `service_songs` data — migrate gracefully
|
||
- MUST NOT change Slide file storage pattern (public disk, filenames)
|
||
- MUST NOT add English strings to UI — all German Du-form
|
||
- MUST NOT use `as any`, `@ts-ignore`, or `@ts-expect-error`
|
||
- MUST NOT add "save" buttons — all actions persist immediately
|
||
- MUST NOT use Sie-form — always Du-form
|
||
|
||
---
|
||
|
||
## Verification Strategy (MANDATORY)
|
||
|
||
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions.
|
||
|
||
### Test Decision
|
||
- **Infrastructure exists**: YES
|
||
- **Automated tests**: TDD (tests first)
|
||
- **Framework**: Pest v4 / PHPUnit for PHP, Playwright for E2E
|
||
- **TDD**: Each task follows RED (failing test) → GREEN (minimal impl) → REFACTOR
|
||
|
||
### QA Policy
|
||
Every task MUST include agent-executed QA scenarios.
|
||
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||
|
||
- **Frontend/UI**: Use Playwright — Navigate, interact, assert DOM, screenshot
|
||
- **Backend/API**: Use Bash (curl/artisan) — Run commands, assert output
|
||
- **DB**: Use `php artisan test` — Assert migrations, model relationships
|
||
- **Export**: Use PHP tests — Assert file content and order
|
||
|
||
---
|
||
|
||
## Execution Strategy
|
||
|
||
### Parallel Execution Waves
|
||
|
||
```
|
||
Wave 1 (Foundation — no UI changes, all independent):
|
||
├── Task 1: service_agenda_items migration + model + factory [quick]
|
||
├── Task 2: Slides table FK migration (service_agenda_item_id) [quick]
|
||
├── Task 3: AgendaMatcherService (wildcard namesmatching logic) [quick]
|
||
├── Task 4: Settings keys + SettingsController extension [quick]
|
||
├── Task 5: CTS API type discovery diagnostic [quick]
|
||
|
||
Wave 2 (Backend services — depends on Wave 1):
|
||
├── Task 6: ChurchToolsService agenda sync (all items, not just songs) [deep] (depends: 1)
|
||
├── Task 7: ServiceController.edit() — pass agenda items to frontend [unspecified-high] (depends: 1, 2, 4)
|
||
├── Task 8: Service.finalizationStatus() update [quick] (depends: 1)
|
||
├── Task 9: PlaylistExportService — agenda-ordered export [deep] (depends: 1, 2, 3)
|
||
|
||
Wave 3 (Frontend components — depends on Wave 2):
|
||
├── Task 10: ArrangementDialog.vue redesign (two-column + lyric preview) [visual-engineering] (depends: 7)
|
||
├── Task 11: AgendaItemRow.vue + SongAgendaItem.vue components [visual-engineering] (depends: 7)
|
||
├── Task 12: Edit.vue restructure (agenda list + announcements section) [visual-engineering] (depends: 10, 11)
|
||
├── Task 13: Slide upload on agenda items (+ button + drag-and-drop) [visual-engineering] (depends: 2, 11)
|
||
|
||
Wave 4 (Settings UI + Service List + Export wiring):
|
||
├── Task 14: Settings.vue — add agenda configuration fields [quick] (depends: 4)
|
||
├── Task 15: ServiceController.index() + Index.vue — updated status columns [unspecified-high] (depends: 1, 8)
|
||
├── Task 16: Wire agenda-ordered export into download flow [unspecified-high] (depends: 9, 12)
|
||
|
||
Wave 5 (Tests + Cleanup):
|
||
├── Task 17: Update existing PHP tests for new model [unspecified-high] (depends: 6, 7, 8, 9)
|
||
├── Task 18: Playwright E2E tests for restructured edit page [unspecified-high] (depends: 12, 13, 14, 16)
|
||
├── Task 19: Remove deprecated components (ModerationBlock, SermonBlock) [quick] (depends: 17, 18)
|
||
|
||
Wave FINAL (After ALL tasks — 4 parallel reviews, then user okay):
|
||
├── Task F1: Plan compliance audit (oracle)
|
||
├── Task F2: Code quality review (unspecified-high)
|
||
├── Task F3: Real manual QA (unspecified-high)
|
||
└── Task F4: Scope fidelity check (deep)
|
||
-> Present results -> Get explicit user okay
|
||
|
||
Critical Path: Task 1 → Task 6 → Task 7 → Task 12 → Task 16 → Task 18 → F1-F4 → user okay
|
||
Parallel Speedup: ~65% faster than sequential
|
||
Max Concurrent: 5 (Wave 1)
|
||
```
|
||
|
||
### Dependency Matrix
|
||
|
||
| Task | Depends On | Blocks |
|
||
|------|-----------|--------|
|
||
| 1 | — | 6, 7, 8, 9, 15 |
|
||
| 2 | — | 7, 9, 13 |
|
||
| 3 | — | 9 |
|
||
| 4 | — | 7, 14 |
|
||
| 5 | — | — |
|
||
| 6 | 1 | 17 |
|
||
| 7 | 1, 2, 4 | 10, 11, 12 |
|
||
| 8 | 1 | 15, 17 |
|
||
| 9 | 1, 2, 3 | 16, 17 |
|
||
| 10 | 7 | 12 |
|
||
| 11 | 7 | 12, 13 |
|
||
| 12 | 10, 11 | 16, 18 |
|
||
| 13 | 2, 11 | 18 |
|
||
| 14 | 4 | 18 |
|
||
| 15 | 1, 8 | — |
|
||
| 16 | 9, 12 | 18 |
|
||
| 17 | 6, 7, 8, 9 | 19 |
|
||
| 18 | 12, 13, 14, 16 | 19 |
|
||
| 19 | 17, 18 | — |
|
||
|
||
### Agent Dispatch Summary
|
||
|
||
- **Wave 1**: **5** — T1→`quick`, T2→`quick`, T3→`quick`, T4→`quick`, T5→`quick`
|
||
- **Wave 2**: **4** — T6→`deep`, T7→`unspecified-high`, T8→`quick`, T9→`deep`
|
||
- **Wave 3**: **4** — T10→`visual-engineering`, T11→`visual-engineering`, T12→`visual-engineering`, T13→`visual-engineering`
|
||
- **Wave 4**: **3** — T14→`quick`, T15→`unspecified-high`, T16→`unspecified-high`
|
||
- **Wave 5**: **3** — T17→`unspecified-high`, T18→`unspecified-high`, T19→`quick`
|
||
- **FINAL**: **4** — F1→`oracle`, F2→`unspecified-high`, F3→`unspecified-high`, F4→`deep`
|
||
|
||
---
|
||
|
||
## TODOs
|
||
|
||
- [x] 1. Create `service_agenda_items` Migration, Model + Factory
|
||
|
||
**What to do**:
|
||
- Create migration `create_service_agenda_items_table` with columns:
|
||
- `id` (bigIncrements)
|
||
- `service_id` (FK to services, cascadeOnDelete)
|
||
- `cts_agenda_item_id` (string, nullable) — CTS API item ID
|
||
- `position` (string) — CTS position value (e.g. "1", "2", "1.1")
|
||
- `title` (string) — item title from CTS
|
||
- `type` (string) — CTS type value (e.g. "Song", "Default", "Header" — actual values TBD from Task 5)
|
||
- `note` (text, nullable) — item note
|
||
- `duration` (string, nullable) — planned duration
|
||
- `start` (string, nullable) — planned start time
|
||
- `is_before_event` (boolean, default false)
|
||
- `responsible` (json, nullable)
|
||
- `service_song_id` (FK to service_songs, nullable, nullOnDelete) — links to existing service_song if this is a song item
|
||
- `sort_order` (unsignedInteger) — display order (derived from position)
|
||
- `created_at`, `updated_at`
|
||
- Unique index: `[service_id, sort_order]`
|
||
- Create `ServiceAgendaItem` model with:
|
||
- `$fillable` array with all columns
|
||
- Relationships: `belongsTo` service, `belongsTo` serviceSong (nullable), `hasMany` slides
|
||
- `casts()` method: `responsible` → array, `is_before_event` → boolean
|
||
- Scope: `scopeVisible($query)` — filters `is_before_event = false`
|
||
- Scope: `scopeBetweenTitles($query, $startTitle, $endTitle)` — filters items between matching header items using AgendaMatcherService
|
||
- Create factory `ServiceAgendaItemFactory` with reasonable defaults
|
||
- Write Pest tests:
|
||
- Test migration creates table with all columns
|
||
- Test model relationships (service, serviceSong, slides)
|
||
- Test factory creates valid records
|
||
- Test scopeVisible excludes is_before_event=true items
|
||
- Test unique constraint on [service_id, sort_order]
|
||
|
||
**Must NOT do**:
|
||
- Do NOT modify `service_songs` table
|
||
- Do NOT add any UI code
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 2, 3, 4, 5)
|
||
- **Blocks**: Tasks 6, 7, 8, 9, 15
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `database/migrations/2026_03_01_100700_create_service_songs_table.php` — Migration pattern with FK constraints and unique indices
|
||
- `app/Models/ServiceSong.php` — Model pattern with fillable, casts, relationships
|
||
- `app/Models/Slide.php` — SoftDeletes and nullable FK pattern
|
||
- `database/factories/` — Factory pattern for existing models
|
||
|
||
**API/Type References**:
|
||
- `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgendaItem.php` — CTS API item fields: position, title, type, note, duration, start, isBeforeEvent, responsible, song
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `service_songs migration` — Follow exact FK constraint style and anonymous class migration pattern
|
||
- `ServiceSong model` — Copy the promoted constructor, fillable, casts, and relationship patterns
|
||
- `EventAgendaItem.php` — Source of truth for all fields the model must store
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Migration runs: `php artisan migrate` → no errors
|
||
- [ ] `php artisan test --filter=ServiceAgendaItemTest` → all tests pass
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Migration creates table correctly
|
||
Tool: Bash (php artisan)
|
||
Preconditions: Fresh database
|
||
Steps:
|
||
1. Run `php artisan migrate:fresh`
|
||
2. Run `php artisan test --filter=test_service_agenda_items_table_has_all_columns`
|
||
Expected Result: Test passes, asserting all columns exist
|
||
Evidence: .sisyphus/evidence/task-1-migration.txt
|
||
|
||
Scenario: Model relationships work
|
||
Tool: Bash (php artisan test)
|
||
Preconditions: Migrated database with factories
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_service_agenda_item_belongs_to_service`
|
||
2. Run `php artisan test --filter=test_service_agenda_item_belongs_to_service_song`
|
||
Expected Result: Both tests pass
|
||
Evidence: .sisyphus/evidence/task-1-relationships.txt
|
||
|
||
Scenario: Unique constraint prevents duplicate sort_order per service
|
||
Tool: Bash (php artisan test)
|
||
Preconditions: Migrated database
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_unique_sort_order_per_service`
|
||
Expected Result: Test verifies that inserting duplicate [service_id, sort_order] throws exception
|
||
Evidence: .sisyphus/evidence/task-1-unique-constraint.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(db): add service_agenda_items table and model`
|
||
- Files: `database/migrations/*_create_service_agenda_items_table.php`, `app/Models/ServiceAgendaItem.php`, `database/factories/ServiceAgendaItemFactory.php`, `tests/Feature/ServiceAgendaItemTest.php`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 2. Add `service_agenda_item_id` FK to Slides Table
|
||
|
||
**What to do**:
|
||
- Create migration `add_service_agenda_item_id_to_slides_table`:
|
||
- Add nullable `service_agenda_item_id` FK column (nullOnDelete)
|
||
- Add index on `service_agenda_item_id`
|
||
- Update `Slide` model:
|
||
- Add `service_agenda_item_id` to `$fillable`
|
||
- Add `agendaItem()` belongsTo relationship → `ServiceAgendaItem`
|
||
- Update `ServiceAgendaItem` model (from Task 1):
|
||
- Verify `slides()` hasMany relationship exists
|
||
- Write Pest tests:
|
||
- Test migration adds column
|
||
- Test Slide.agendaItem() relationship returns correct ServiceAgendaItem
|
||
- Test slides can be created with service_agenda_item_id
|
||
- Test nullOnDelete: when agenda item is deleted, slide.service_agenda_item_id becomes null
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change existing slide type enum
|
||
- Do NOT modify existing slide upload logic
|
||
- Do NOT remove existing service_id FK
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 1, 3, 4, 5)
|
||
- **Blocks**: Tasks 7, 9, 13
|
||
- **Blocked By**: None (Task 1 is parallel but this migration can run independently)
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `database/migrations/2026_03_01_100800_create_slides_table.php` — Existing slides migration pattern
|
||
- `app/Models/Slide.php` — Current Slide model with fillable, relationships, SoftDeletes
|
||
- `database/migrations/2026_03_02_130249_add_cts_song_id_to_songs_and_service_songs_tables.php` — Pattern for adding columns to existing tables
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `slides migration` — See current column structure to ensure new FK is compatible
|
||
- `Slide model` — Must add to existing $fillable array and add new relationship method
|
||
- `add_cts_song_id migration` — Follow same pattern for "add column to existing table" migration
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Migration runs: `php artisan migrate` → no errors
|
||
- [ ] `php artisan test --filter=SlideAgendaItemRelationTest` → passes
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Slide links to agenda item correctly
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_slide_belongs_to_agenda_item`
|
||
Expected Result: Test creates a Slide with service_agenda_item_id, asserts ->agendaItem returns correct model
|
||
Evidence: .sisyphus/evidence/task-2-slide-relationship.txt
|
||
|
||
Scenario: Null on delete works
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_slide_agenda_item_id_nulled_on_delete`
|
||
Expected Result: Deleting agenda item sets slide.service_agenda_item_id to null (not cascade delete)
|
||
Evidence: .sisyphus/evidence/task-2-null-on-delete.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(db): add service_agenda_item_id FK to slides`
|
||
- Files: `database/migrations/*_add_service_agenda_item_id_to_slides_table.php`, `app/Models/Slide.php`, test file
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 3. Create AgendaMatcherService (Wildcard Namesmatching)
|
||
|
||
**What to do**:
|
||
- Create `app/Services/AgendaMatcherService.php` with:
|
||
- `matches(string $itemTitle, string $pattern): bool` — glob-style wildcard matching (`*` matches any chars, case-insensitive)
|
||
- `matchesAny(string $itemTitle, array $patterns): bool` — returns true if any pattern matches
|
||
- `findFirstMatch(array $agendaItems, array $patterns): ?ServiceAgendaItem` — returns first item whose title matches any pattern
|
||
- `filterBetween(array $items, ?string $startPattern, ?string $endPattern): array` — filters items between first match of start and end patterns
|
||
- Write comprehensive unit tests:
|
||
- Exact match: `"Predigt"` matches `"Predigt"`
|
||
- Wildcard suffix: `"Predigt*"` matches `"Predigt – Thema XY"`
|
||
- Wildcard prefix: `"*Predigt"` matches `"Die Predigt"`
|
||
- Wildcard both: `"*Predigt*"` matches `"Die Predigt heute"`
|
||
- Case insensitive: `"predigt*"` matches `"Predigt – Thema"`
|
||
- No match: `"Predigt*"` does NOT match `"Lobpreis"`
|
||
- Multiple patterns: `["Predigt*", "Sermon*"]` matches `"Predigt – Thema"`
|
||
- findFirstMatch: returns first matching item from array
|
||
- filterBetween with start only, end only, both, neither
|
||
- filterBetween excludes the boundary items themselves
|
||
|
||
**Must NOT do**:
|
||
- Do NOT use regex — use simple glob-style `*` wildcard only
|
||
- Do NOT add any UI or controller code
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 1, 2, 4, 5)
|
||
- **Blocks**: Task 9
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Services/SongMatchingService.php` — Existing service class pattern (constructor injection, single responsibility)
|
||
- `app/Services/ChurchToolsService.php:454-462` — normalizeCcli() for input sanitization pattern
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `SongMatchingService` — Follow same class structure (namespace, constructor, public methods)
|
||
- `normalizeCcli` — Same trim/normalize approach for pattern input sanitization
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=AgendaMatcherServiceTest` → all tests pass (10+ test cases)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Wildcard matching works correctly
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=AgendaMatcherServiceTest`
|
||
Expected Result: All wildcard matching test cases pass (exact, prefix*, *suffix, *both*, case-insensitive)
|
||
Evidence: .sisyphus/evidence/task-3-matching.txt
|
||
|
||
Scenario: filterBetween returns correct subset
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_filter_between_start_and_end`
|
||
Expected Result: Given items [Header:Start, Item1, Item2, Header:End, Item3], returns [Item1, Item2]
|
||
Evidence: .sisyphus/evidence/task-3-filter-between.txt
|
||
|
||
Scenario: Empty patterns return all items
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_filter_between_no_patterns`
|
||
Expected Result: When both start and end patterns are null/empty, returns all non-header items
|
||
Evidence: .sisyphus/evidence/task-3-no-filter.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(service): add AgendaMatcherService with wildcard namesmatching`
|
||
- Files: `app/Services/AgendaMatcherService.php`, `tests/Feature/AgendaMatcherServiceTest.php`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 4. Add Agenda Settings Keys to SettingsController
|
||
|
||
**What to do**:
|
||
- Add 4 new setting keys to `SettingsController::MACRO_KEYS` (rename to `SETTING_KEYS`):
|
||
- `agenda_announcement_position` — comma-separated patterns for announcement position matching (e.g. `"Information*,Hinweis*"`)
|
||
- `agenda_sermon_matching` — comma-separated patterns for sermon recognition (e.g. `"Predigt*,Sermon*"`)
|
||
- `agenda_start_title` — single pattern for agenda start boundary (e.g. `"Ablauf*"`)
|
||
- `agenda_end_title` — single pattern for agenda end boundary (e.g. `"Ende*"`)
|
||
- Update validation in `update()` to accept the new keys
|
||
- Update `index()` to pass all settings including new ones
|
||
- Write Pest tests:
|
||
- Test settings page returns new keys
|
||
- Test updating each new key
|
||
- Test validation rejects unknown keys
|
||
- Test comma-separated values stored correctly
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change the Settings.vue yet (Task 14)
|
||
- Do NOT change the Setting model
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 5)
|
||
- **Blocks**: Tasks 7, 14
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Http/Controllers/SettingsController.php:13-18` — Existing MACRO_KEYS constant array pattern
|
||
- `app/Http/Controllers/SettingsController.php:32-37` — Validation with Rule::in pattern
|
||
- `app/Models/Setting.php` — Setting::get() and Setting::set() API
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `SettingsController` — Add to existing constant array and follow same validation pattern
|
||
- `Setting model` — Understand get/set API for new keys
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=SettingsControllerTest` → all tests pass including new key tests
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: New setting keys are returned by index
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_settings_page_includes_agenda_keys`
|
||
Expected Result: Inertia page props include agenda_announcement_position, agenda_sermon_matching, agenda_start_title, agenda_end_title
|
||
Evidence: .sisyphus/evidence/task-4-settings-keys.txt
|
||
|
||
Scenario: Updating agenda setting works
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_update_agenda_announcement_position`
|
||
Expected Result: PATCH request with key=agenda_announcement_position, value="Information*,Hinweis*" returns 200
|
||
Evidence: .sisyphus/evidence/task-4-update-setting.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(settings): add agenda configuration keys`
|
||
- Files: `app/Http/Controllers/SettingsController.php`, test file
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 5. CTS Agenda Type Discovery Command
|
||
|
||
**What to do**:
|
||
- Create artisan command `app/Console/Commands/DiscoverAgendaTypes.php`:
|
||
- Command: `cts:discover-agenda-types`
|
||
- Fetches agenda for the next upcoming service via `ChurchToolsService::syncAgenda()`
|
||
- Iterates `agenda.getItems()` and prints a table:
|
||
- Position | Title | Type | Has Song | Duration | IsBeforeEvent
|
||
- This is a diagnostic tool to discover actual CTS `EventAgendaItem.type` values
|
||
- Write basic test:
|
||
- Test command runs with mocked API data
|
||
- Test output contains expected columns
|
||
|
||
**Must NOT do**:
|
||
- Do NOT use this to modify any data
|
||
- Do NOT make this part of the sync flow
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 4)
|
||
- **Blocks**: None (informational only)
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Services/ChurchToolsService.php:145-159` — syncAgenda method with injectable fetcher
|
||
- `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgendaItem.php` — Item getters: getPosition(), getTitle(), getType(), getSong(), getIsBeforeEvent()
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `ChurchToolsService` — Reuse syncAgenda() method for fetching
|
||
- `EventAgendaItem` — Know which getter methods to call for output
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan cts:discover-agenda-types` runs without error (with valid API config)
|
||
- [ ] `php artisan test --filter=DiscoverAgendaTypesTest` → passes with mocked data
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Command outputs agenda items
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_discover_command_outputs_table`
|
||
Expected Result: Test verifies command outputs table with Position, Title, Type columns using mocked agenda data
|
||
Evidence: .sisyphus/evidence/task-5-discover-command.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `chore(debug): add CTS agenda type discovery command`
|
||
- Files: `app/Console/Commands/DiscoverAgendaTypes.php`, test file
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 6. Extend ChurchToolsService to Sync ALL Agenda Items
|
||
|
||
**What to do**:
|
||
- Rename `syncServiceAgendaSongs()` to `syncServiceAgendaItems()` (keep old method as deprecated wrapper for backward compat)
|
||
- New `syncServiceAgendaItems()`:
|
||
- Call `$agenda->getItems()` instead of `$agenda->getSongs()`
|
||
- For EACH `EventAgendaItem`:
|
||
- Upsert into `service_agenda_items` table using `[service_id, sort_order]` unique key
|
||
- Store all fields: cts_agenda_item_id, position, title, type, note, duration, start, is_before_event, responsible
|
||
- If item has a `getSong()` that is not null:
|
||
- Continue the existing `service_songs` upsert logic (create/update service_song row)
|
||
- Link: set `service_agenda_item.service_song_id` → the service_song ID
|
||
- Auto-match via `SongMatchingService` (existing behavior)
|
||
- Sort order: derive from item position or array index
|
||
- Remove orphaned agenda items: delete `service_agenda_items` where `service_id` matches but `sort_order` not in current sync batch
|
||
- Preserve slides: when removing orphaned agenda items, set their slides' `service_agenda_item_id` to null (don't delete slides)
|
||
- Update sync summary to include `agenda_items_count`
|
||
- Write comprehensive tests:
|
||
- Test all agenda items (songs + non-songs) are stored
|
||
- Test song items create both service_song AND service_agenda_item records with correct link
|
||
- Test non-song items don't create service_song records
|
||
- Test re-sync updates existing items
|
||
- Test orphaned items are cleaned up
|
||
- Test slides are preserved when orphaned items are removed
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change the `syncAgenda()` API call method
|
||
- Do NOT remove existing `service_songs` table or data
|
||
- Do NOT change the injectable closure pattern
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 2 (with Tasks 7, 8, 9)
|
||
- **Blocks**: Task 17
|
||
- **Blocked By**: Task 1
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Services/ChurchToolsService.php:324-385` — Current `syncServiceAgendaSongs()` — the code being extended
|
||
- `app/Services/ChurchToolsService.php:145-159` — `syncAgenda()` returns agenda object
|
||
- `app/Services/SongMatchingService.php` — `autoMatch()` method used for song matching
|
||
- `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgenda.php:43-52` — `getSongs()` vs `getItems()` difference
|
||
- `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgendaItem.php` — Full item API
|
||
|
||
**Test References**:
|
||
- `tests/Feature/ChurchToolsSyncTest.php` — Existing sync tests with mocked API data and fake event objects
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `syncServiceAgendaSongs` — This IS the method being refactored; understand every line
|
||
- `EventAgenda.getSongs()` — Shows current filtering; need to switch to `getItems()`
|
||
- `EventAgendaItem` — API surface for extracting all fields
|
||
- `ChurchToolsSyncTest` — Follow same mocking pattern for new tests
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=ChurchToolsSyncTest` → all existing tests still pass
|
||
- [ ] `php artisan test --filter=test_sync_stores_all_agenda_item_types` → passes
|
||
- [ ] `php artisan test --filter=test_sync_links_song_items_to_service_songs` → passes
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Sync stores all agenda item types
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_sync_stores_all_agenda_item_types`
|
||
Expected Result: Given mock agenda with 8 items (3 songs, 2 headers, 3 default), all 8 appear in service_agenda_items table
|
||
Evidence: .sisyphus/evidence/task-6-all-types.txt
|
||
|
||
Scenario: Song items link to service_songs
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_sync_links_song_items_to_service_songs`
|
||
Expected Result: Song agenda items have non-null service_song_id pointing to correct service_songs row
|
||
Evidence: .sisyphus/evidence/task-6-song-links.txt
|
||
|
||
Scenario: Orphaned agenda items cleaned up on re-sync
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_resync_removes_orphaned_agenda_items`
|
||
Expected Result: After re-sync with fewer items, old items are removed but their slides retain null FK
|
||
Evidence: .sisyphus/evidence/task-6-orphan-cleanup.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(sync): sync all CTS agenda items (not just songs)`
|
||
- Files: `app/Services/ChurchToolsService.php`, `tests/Feature/ChurchToolsSyncTest.php`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 7. Update ServiceController.edit() to Pass Agenda Items
|
||
|
||
**What to do**:
|
||
- Update `ServiceController::edit()` to:
|
||
- Load `service.agendaItems` (ordered by sort_order) with eager-loaded relationships:
|
||
- `agendaItems.serviceSong.song.groups.slides` (for song items)
|
||
- `agendaItems.serviceSong.song.arrangements.arrangementGroups.group`
|
||
- `agendaItems.serviceSong.arrangement.arrangementGroups.group` (selected arrangement)
|
||
- `agendaItems.slides` (uploaded slides per agenda item)
|
||
- Load settings: `agenda_announcement_position`, `agenda_sermon_matching`, `agenda_start_title`, `agenda_end_title`
|
||
- Pass to Inertia: `agendaItems`, `agendaSettings`, plus existing `informationSlides`, `songsCatalog`
|
||
- Keep existing `serviceSongs` prop for backward compatibility during transition
|
||
- Apply `filterBetween` using `AgendaMatcherService` on the agenda items based on start/end title settings
|
||
- Mark items: add computed properties `is_announcement_position` (matches announcement pattern), `is_sermon` (matches sermon pattern) to each item
|
||
- Write tests:
|
||
- Test edit page returns agendaItems prop
|
||
- Test agendaItems are filtered by start/end titles
|
||
- Test is_announcement_position and is_sermon flags are set correctly
|
||
- Test settings are passed to frontend
|
||
|
||
**Must NOT do**:
|
||
- Do NOT remove existing props yet (backward compat)
|
||
- Do NOT change the route definition
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 2 (with Tasks 6, 8, 9)
|
||
- **Blocks**: Tasks 10, 11, 12
|
||
- **Blocked By**: Tasks 1, 2, 4
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Http/Controllers/ServiceController.php:88-224` — Current edit() method with Eloquent eager loading and Inertia::render
|
||
- `app/Models/Setting.php:15-20` — Setting::get() for retrieving settings
|
||
- `app/Services/AgendaMatcherService.php` — filterBetween() and findFirstMatch() (from Task 3)
|
||
|
||
**Test References**:
|
||
- `tests/Feature/ServiceControllerTest.php` — Existing edit page tests with assertInertia
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `ServiceController::edit()` — This method is being modified; need to understand existing eager loading
|
||
- `Setting::get()` — How to retrieve the new settings for passing to frontend
|
||
- `AgendaMatcherService` — Used for filtering and marking items
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=ServiceControllerTest` → all existing tests pass
|
||
- [ ] `php artisan test --filter=test_edit_returns_agenda_items` → passes
|
||
- [ ] `php artisan test --filter=test_edit_filters_agenda_by_title_settings` → passes
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Edit page returns agenda items
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_edit_returns_agenda_items`
|
||
Expected Result: Inertia page has 'agendaItems' prop with correct structure including type, title, serviceSong relation
|
||
Evidence: .sisyphus/evidence/task-7-agenda-items-prop.txt
|
||
|
||
Scenario: Agenda items filtered by start/end title settings
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_edit_filters_agenda_by_title_settings`
|
||
Expected Result: With agenda_start_title="Ablauf*" and agenda_end_title="Ende*", only items between those headers are returned
|
||
Evidence: .sisyphus/evidence/task-7-filtered-items.txt
|
||
|
||
Scenario: Sermon and announcement flags set correctly
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_edit_marks_sermon_and_announcement_items`
|
||
Expected Result: Items matching agenda_sermon_matching have is_sermon=true, items matching agenda_announcement_position have is_announcement_position=true
|
||
Evidence: .sisyphus/evidence/task-7-item-flags.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(controller): pass agenda items to edit page`
|
||
- Files: `app/Http/Controllers/ServiceController.php`, test file
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 8. Update Service.finalizationStatus() for Agenda Model
|
||
|
||
**What to do**:
|
||
- Update `Service::finalizationStatus()` to check:
|
||
- All song-type agenda items have their service_song matched (song_id not null)
|
||
- All matched songs have an arrangement selected (song_arrangement_id not null)
|
||
- At least one agenda item flagged as sermon has slides uploaded (replaces "sermon slides exist" check)
|
||
- Announce if announcement position matching found a match (informational, not blocking)
|
||
- Keep the same return shape: `{ready: bool, warnings: string[]}`
|
||
- Warning messages in German Du-form
|
||
- Write tests:
|
||
- Test all songs matched → no warning
|
||
- Test unmatched song → warning "Song X wurde noch nicht zugeordnet"
|
||
- Test song without arrangement → warning "Song X hat kein Arrangement"
|
||
- Test no sermon slides → warning "Keine Predigt-Folien hochgeladen"
|
||
- Test all checks pass → ready=true
|
||
|
||
**Must NOT do**:
|
||
- Do NOT remove existing checks — replace them to work with new model
|
||
- Do NOT change the finalization flow/endpoint
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 2 (with Tasks 6, 7, 9)
|
||
- **Blocks**: Tasks 15, 17
|
||
- **Blocked By**: Task 1
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Models/Service.php` — Current finalizationStatus() method
|
||
- `tests/Feature/FinalizationTest.php` — Existing finalization tests
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `Service::finalizationStatus()` — The method being modified; understand current checks
|
||
- `FinalizationTest` — Update existing tests to use new model
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=FinalizationTest` → all tests pass (updated for new model)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Finalization warns about unmatched songs
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_finalization_warns_unmatched_songs`
|
||
Expected Result: Service with unmatched song agenda item returns warning
|
||
Evidence: .sisyphus/evidence/task-8-unmatched-warning.txt
|
||
|
||
Scenario: All checks pass allows finalization
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_finalization_ready_when_all_checks_pass`
|
||
Expected Result: ready=true, warnings=[]
|
||
Evidence: .sisyphus/evidence/task-8-ready.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `refactor(model): update finalizationStatus for agenda model`
|
||
- Files: `app/Models/Service.php`, `tests/Feature/FinalizationTest.php`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 9. PlaylistExportService — Agenda-Ordered Export
|
||
|
||
**What to do**:
|
||
- Refactor `PlaylistExportService::generatePlaylist()` to:
|
||
- Load service with `agendaItems` ordered by `sort_order`
|
||
- Build playlist in agenda order:
|
||
1. For each agenda item in order:
|
||
- If item `is_announcement_position` → insert information slides as .pro presentation at this position
|
||
- If item has a matched song with arrangement → generate .pro via ProExportService
|
||
- If item has uploaded slides (via agendaItem.slides) → generate .pro presentation from those slides
|
||
- If item has both song AND slides → song .pro first, then slides .pro
|
||
- If item has nothing → skip
|
||
2. If NO agenda item matched announcement position → insert information slides at the beginning (fallback)
|
||
- Keep the .proplaylist format (ProPlaylistGenerator)
|
||
- Handle backward compatibility: if service has no agenda items (old data), fall back to existing block-based export
|
||
- Write comprehensive tests:
|
||
- Test export follows agenda order
|
||
- Test announcements embedded at matched position
|
||
- Test announcements at beginning if no match
|
||
- Test items with slides are included
|
||
- Test items without slides or songs are skipped
|
||
- Test backward compat: old services use block-based export
|
||
- Test song + slides combination
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change the ProExportService (individual .pro generation)
|
||
- Do NOT change the propresenter-parser package
|
||
- Do NOT change the .proplaylist format
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 2 (with Tasks 6, 7, 8)
|
||
- **Blocks**: Tasks 16, 17
|
||
- **Blocked By**: Tasks 1, 2, 3
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Services/PlaylistExportService.php` — Current generatePlaylist() — the method being refactored
|
||
- `app/Services/ProExportService.php` — generateProFile() for individual song .pro files
|
||
- `app/Services/ProBundleExportService.php` — generateBundle() for slides-to-.pro conversion pattern
|
||
- `app/Services/AgendaMatcherService.php` — findFirstMatch() for announcement position detection
|
||
|
||
**Test References**:
|
||
- `tests/Feature/PlaylistExportTest.php` — Existing export tests
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `PlaylistExportService` — The service being refactored; understand current item ordering and embedding
|
||
- `ProExportService` — Used unchanged for generating individual song .pro files
|
||
- `ProBundleExportService` — Pattern for converting uploaded slides into .pro presentation
|
||
- `AgendaMatcherService` — Used to find announcement insertion point
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=PlaylistExportTest` → all tests pass
|
||
- [ ] `php artisan test --filter=test_export_follows_agenda_order` → passes
|
||
- [ ] `php artisan test --filter=test_export_embeds_announcements_at_matched_position` → passes
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Export follows agenda order
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_export_follows_agenda_order`
|
||
Expected Result: Given agenda [Begrüßung, Song1, Information, Predigt, Song2], export order is [Song1.pro, InfoSlides.pro, Predigt-Slides.pro, Song2.pro]
|
||
Evidence: .sisyphus/evidence/task-9-agenda-order.txt
|
||
|
||
Scenario: Announcements at matched position
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_export_embeds_announcements_at_matched_position`
|
||
Expected Result: With announcement setting "Information*", announcements .pro is embedded right at the "Informationen" agenda item position
|
||
Evidence: .sisyphus/evidence/task-9-announcement-position.txt
|
||
|
||
Scenario: Backward compat for old services
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_export_falls_back_to_block_order`
|
||
Expected Result: Service without agenda items exports in old block order (info→songs→moderation→sermon)
|
||
Evidence: .sisyphus/evidence/task-9-backward-compat.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(export): agenda-ordered playlist export`
|
||
- Files: `app/Services/PlaylistExportService.php`, `tests/Feature/PlaylistExportTest.php`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 10. Redesign ArrangementDialog — Two-Column with Lyric Preview
|
||
|
||
**What to do**:
|
||
- Create new `resources/js/Components/ArrangementDialog.vue` (replaces `ArrangementConfigurator.vue` in the arrangement popup context):
|
||
- **Top section**: Arrangement selection dropdown + "Neu" (create) + "Duplizieren" (clone) buttons
|
||
- **Two-column layout**:
|
||
- **Left column** (pills): Vertical list of arrangement group names as colored pills
|
||
- Drag-and-drop reorderable (VueDraggable)
|
||
- Each pill shows group name + group color as background/border
|
||
- Right side of each pill: "2x" button (duplicate this group element directly below) + "+" button (quick-select popup showing ALL available song groups to insert below)
|
||
- "+" button shows a dropdown/popup with all song groups (Vers1, Vers2, Chorus, Bridge, etc.) — clicking one inserts it below the current pill
|
||
- **Right column** (lyric preview): Same elements as left, scroll-synced
|
||
- Each element shows:
|
||
- Colored left border matching group color
|
||
- Small pill-style title (e.g. "Vers 1") inside the top-left of the element
|
||
- Actual lyric text from `SongSlide.text_content` — one line per slide
|
||
- If song has translation: `SongSlide.text_content_translated` shown below each original line in gray, italic, smaller font
|
||
- If a group has multiple slides: show all slides together with a thin gray line separator between them
|
||
- Scroll position synced with left column (when user scrolls left, right scrolls to matching position and vice versa)
|
||
- **Delete arrangement**: Button to delete selected arrangement (prevent deleting last one)
|
||
- API calls (reuse existing endpoints):
|
||
- POST `/songs/{songId}/arrangements` — create new
|
||
- POST `/arrangements/{arrangementId}/clone` — clone
|
||
- PUT `/arrangements/{arrangementId}` — update groups order (on drag-end, on 2x, on + add)
|
||
- DELETE `/arrangements/{arrangementId}` — delete
|
||
- Emits: `arrangement-selected(arrangementId)`, `close`
|
||
- Props: `songId`, `arrangements`, `selectedArrangementId`, `groups` (all song groups with slides)
|
||
- This is a modal/dialog component, opened by edit button on song agenda items
|
||
|
||
**Must NOT do**:
|
||
- Do NOT delete `ArrangementConfigurator.vue` yet (used by SongEditModal — removed in Task 19)
|
||
- Do NOT change the API endpoints (reuse existing)
|
||
- Do NOT add English text — all German Du-form
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 3 (with Tasks 11, 13)
|
||
- **Blocks**: Task 12
|
||
- **Blocked By**: Task 7
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `resources/js/Components/ArrangementConfigurator.vue` — Current arrangement UI with VueDraggable, create/clone/delete buttons (270 lines). Reuse API call patterns and VueDraggable configuration.
|
||
- `resources/js/Components/SongEditModal.vue` — Modal dialog pattern with auto-save
|
||
- `resources/js/Components/Blocks/SongsBlock.vue:1-100` — How arrangements are currently displayed and selected
|
||
|
||
**API/Type References**:
|
||
- `app/Models/SongGroup.php` — Group fields: name, color, order
|
||
- `app/Models/SongSlide.php` — Slide fields: text_content, text_content_translated, order
|
||
- `app/Models/SongArrangement.php` — Arrangement: name, is_default
|
||
- `app/Models/SongArrangementGroup.php` — ArrangementGroup: song_arrangement_id, song_group_id, order
|
||
|
||
**External References**:
|
||
- VueDraggable: already used in project, see existing import pattern in ArrangementConfigurator.vue
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `ArrangementConfigurator.vue` — Core logic being redesigned; reuse API calls, VueDraggable config, error handling
|
||
- `SongGroup/SongSlide` — Data structure for rendering lyric preview and group colors
|
||
- `SongEditModal` — Follow same modal dialog UX patterns (overlay, close on escape, etc.)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npm run build` → no errors
|
||
- [ ] ArrangementDialog renders with two columns
|
||
- [ ] Drag-and-drop reordering saves via PUT endpoint
|
||
- [ ] "2x" duplicates element below
|
||
- [ ] "+" shows quick-select of all groups
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Two-column layout renders correctly
|
||
Tool: Playwright
|
||
Preconditions: Dev server running, authenticated, service with matched song that has groups/slides
|
||
Steps:
|
||
1. Navigate to service edit page
|
||
2. Find song agenda item, click edit button (data-testid="song-edit-arrangement")
|
||
3. Assert dialog is visible
|
||
4. Assert left column has pill elements (data-testid="arrangement-pill-list")
|
||
5. Assert right column has lyric content (data-testid="arrangement-lyric-preview")
|
||
6. Take screenshot
|
||
Expected Result: Dialog shows with two columns, pills on left, lyrics on right
|
||
Failure Indicators: Dialog not opening, missing columns, no content
|
||
Evidence: .sisyphus/evidence/task-10-two-column-layout.png
|
||
|
||
Scenario: Drag-and-drop reorders elements
|
||
Tool: Playwright
|
||
Preconditions: Dialog open with 3+ arrangement elements
|
||
Steps:
|
||
1. Note current order of pills
|
||
2. Drag first pill to third position
|
||
3. Wait for save indicator (auto-save)
|
||
4. Assert pills are in new order
|
||
Expected Result: Pills reorder and save persists (PUT request sent)
|
||
Evidence: .sisyphus/evidence/task-10-drag-reorder.png
|
||
|
||
Scenario: 2x button duplicates element
|
||
Tool: Playwright
|
||
Preconditions: Dialog open with arrangement elements
|
||
Steps:
|
||
1. Count current pills (e.g. 4)
|
||
2. Click "2x" button on second pill (data-testid="arrangement-duplicate-btn")
|
||
3. Assert pill count is now 5
|
||
4. Assert new pill is same group name as the one duplicated
|
||
5. Assert it appears directly after the duplicated pill
|
||
Expected Result: Duplicate appears below, same group name
|
||
Evidence: .sisyphus/evidence/task-10-duplicate.png
|
||
|
||
Scenario: Lyric preview shows translation in gray/italic
|
||
Tool: Playwright
|
||
Preconditions: Song with has_translation=true and text_content_translated populated
|
||
Steps:
|
||
1. Open arrangement dialog for translated song
|
||
2. Assert original text is visible in normal font
|
||
3. Assert translated text is visible with classes for gray, italic, small (check computed styles)
|
||
Expected Result: Translation text displayed below original in gray italic small font
|
||
Evidence: .sisyphus/evidence/task-10-translation-preview.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(ui): redesign ArrangementDialog with lyric preview`
|
||
- Files: `resources/js/Components/ArrangementDialog.vue`
|
||
- Pre-commit: `npm run build`
|
||
|
||
---
|
||
|
||
- [x] 11. Create AgendaItemRow and SongAgendaItem Components
|
||
|
||
**What to do**:
|
||
- Create `resources/js/Components/AgendaItemRow.vue`:
|
||
- Generic agenda item display row with:
|
||
- Left: position number, title, optional note
|
||
- Center: slide thumbnails (if any uploaded)
|
||
- Right: "+" button for slide upload (opens drag-and-drop area below)
|
||
- When "+" clicked: toggles a `SlideUploader` area below the row (service-specific, type='agenda_item')
|
||
- Highlight behavior:
|
||
- Items with uploaded slides: success-style highlight (green border/bg)
|
||
- Items matching announcement position: special indicator/icon
|
||
- Items matching sermon pattern: sermon label/indicator
|
||
- Props: `agendaItem` (full object with slides, flags), `serviceId`, `serviceDate`
|
||
- Emits: `slides-updated` (triggers page reload)
|
||
- Create `resources/js/Components/SongAgendaItem.vue`:
|
||
- Extends/wraps AgendaItemRow with song-specific features:
|
||
- Song name from CTS + matched song name from DB
|
||
- If NOT matched: search dropdown to assign from catalog + "Erstellung anfragen" button (reuse from SongsBlock)
|
||
- If matched:
|
||
- Arrangement name shown as label
|
||
- Element pills (Vers1, Chorus, Bridge, etc.) from selected arrangement's groups — shown as small colored pills in a row
|
||
- Edit button → opens ArrangementDialog (Task 10) as modal
|
||
- Translation checkbox (if has_translation)
|
||
- Highlight: success (has arrangement), warning (no arrangement), neutral (not matched)
|
||
- Props: `agendaItem` (with serviceSong relation), `songsCatalog`, `serviceId`
|
||
- Emits: `arrangement-selected`, `slides-updated`
|
||
- Both components use `data-testid` attributes on all interactive elements
|
||
|
||
**Must NOT do**:
|
||
- Do NOT implement the full upload logic (reuse existing `SlideUploader`)
|
||
- Do NOT change existing API endpoints
|
||
- Do NOT use Sie-form
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 3 (with Tasks 10, 13)
|
||
- **Blocks**: Tasks 12, 13
|
||
- **Blocked By**: Task 7
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `resources/js/Components/Blocks/SongsBlock.vue` — Song matching UI (search dropdown, request button, arrangement selector, translation checkbox). Reuse the matching/assign/request logic.
|
||
- `resources/js/Components/SlideUploader.vue` — Reusable uploader component (already built)
|
||
- `resources/js/Components/SlideGrid.vue` — Thumbnail display for uploaded slides
|
||
|
||
**API/Type References**:
|
||
- `routes/api.php` — Service-song assign/request/update endpoints
|
||
- `app/Http/Controllers/ServiceSongController.php` — API for song matching operations
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `SongsBlock.vue` — Has the complete song matching UX; extract/reuse the match/assign/request logic
|
||
- `SlideUploader/SlideGrid` — Compose these into the agenda item row for slide management
|
||
- `ServiceSongController` — API endpoints to reuse for song operations
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npm run build` → no errors
|
||
- [ ] AgendaItemRow renders with title, position, "+" button
|
||
- [ ] SongAgendaItem shows arrangement pills for matched songs
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Non-song agenda item shows with "+" upload button
|
||
Tool: Playwright
|
||
Preconditions: Service with non-song agenda items (e.g. "Gebet")
|
||
Steps:
|
||
1. Navigate to service edit page
|
||
2. Find non-song agenda item row (data-testid="agenda-item-row")
|
||
3. Assert title "Gebet" is visible
|
||
4. Assert "+" button exists (data-testid="agenda-item-add-slides")
|
||
Expected Result: Row displays with title and "+" button
|
||
Evidence: .sisyphus/evidence/task-11-agenda-item-row.png
|
||
|
||
Scenario: Song shows arrangement pills
|
||
Tool: Playwright
|
||
Preconditions: Service with matched song that has arrangement with groups
|
||
Steps:
|
||
1. Navigate to service edit page
|
||
2. Find song agenda item (data-testid="song-agenda-item")
|
||
3. Assert arrangement name label is visible
|
||
4. Assert group pills (data-testid="arrangement-pill") exist with correct names (e.g. "Vers 1", "Chorus")
|
||
Expected Result: Song row shows arrangement name + colored pills for each group
|
||
Evidence: .sisyphus/evidence/task-11-song-pills.png
|
||
|
||
Scenario: Unmatched song shows search dropdown
|
||
Tool: Playwright
|
||
Preconditions: Service with unmatched song (no song_id)
|
||
Steps:
|
||
1. Navigate to service edit page
|
||
2. Find unmatched song row
|
||
3. Assert search dropdown (data-testid="song-search-select") is visible
|
||
4. Assert "Erstellung anfragen" button (data-testid="request-song-btn") exists
|
||
Expected Result: Unmatched song shows assignment controls
|
||
Evidence: .sisyphus/evidence/task-11-unmatched-song.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(ui): add AgendaItemRow and SongAgendaItem components`
|
||
- Files: `resources/js/Components/AgendaItemRow.vue`, `resources/js/Components/SongAgendaItem.vue`
|
||
- Pre-commit: `npm run build`
|
||
|
||
---
|
||
|
||
- [x] 12. Restructure Edit.vue — Agenda View Replacing Accordion
|
||
|
||
**What to do**:
|
||
- Major restructure of `resources/js/Pages/Services/Edit.vue`:
|
||
- **Top section** (keep): Announcements/Information block
|
||
- Keep the existing `InformationBlock` component at the top
|
||
- This is unchanged: global slides with expire dates, upload, delete, download bundle
|
||
- **Agenda section** (new): Replace the 3 block accordion (Moderation, Sermon, Songs) with:
|
||
- Header: "Ablauf" (Agenda)
|
||
- List of all agenda items in order (from `agendaItems` prop)
|
||
- For each item:
|
||
- If item is a header/title type: render as section divider (styled differently, no "+" button)
|
||
- If item is a song: render `SongAgendaItem` component
|
||
- If item is flagged as sermon (`is_sermon`): render `AgendaItemRow` with sermon-specific upload area and label
|
||
- If item is flagged as announcement position (`is_announcement_position`): render `AgendaItemRow` with announcement indicator
|
||
- Otherwise: render `AgendaItemRow` (generic with "+" button)
|
||
- Each item with uploaded slides or song arrangement: highlighted (success/green border)
|
||
- Song without arrangement: warning highlight (amber/orange border)
|
||
- **Footer** (keep, adapted): Finalization status + action buttons
|
||
- Keep Finalize, Reopen, Download buttons
|
||
- Update finalization status display for new model
|
||
- Remove: ModerationBlock, SermonBlock, SongsBlock imports and usage
|
||
- Add: imports for AgendaItemRow, SongAgendaItem, ArrangementDialog
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change InformationBlock (announcements stay as-is)
|
||
- Do NOT change finalization/reopen/download endpoints
|
||
- Do NOT add English strings
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: NO
|
||
- **Parallel Group**: Wave 3 (after Tasks 10, 11)
|
||
- **Blocks**: Tasks 16, 18
|
||
- **Blocked By**: Tasks 10, 11
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `resources/js/Pages/Services/Edit.vue` — Current 662-line edit page (the file being restructured)
|
||
- `resources/js/Components/Blocks/InformationBlock.vue` — Keep this component unchanged
|
||
- `resources/js/Layouts/AuthenticatedLayout.vue` — Layout pattern
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `Edit.vue` — Must understand current structure to preserve header, footer, navigation, and toast functionality while replacing the middle section
|
||
- `InformationBlock` — Must continue to work as-is in the new layout
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npm run build` → no errors
|
||
- [ ] Edit page shows announcements section at top
|
||
- [ ] Edit page shows agenda items list below
|
||
- [ ] Songs show inline arrangement pills
|
||
- [ ] Finalize/Download buttons still work
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Edit page shows agenda instead of accordion
|
||
Tool: Playwright
|
||
Preconditions: Service with synced agenda items (mix of songs, default items, headers)
|
||
Steps:
|
||
1. Navigate to /services/{id}/edit
|
||
2. Assert InformationBlock (data-testid="information-block") is visible at top
|
||
3. Assert agenda section (data-testid="agenda-section") is visible below
|
||
4. Assert NO accordion blocks (data-testid="moderation-block") exist
|
||
5. Assert agenda items are listed in order
|
||
6. Take screenshot
|
||
Expected Result: Page shows announcements + agenda list, no accordion
|
||
Evidence: .sisyphus/evidence/task-12-edit-page-structure.png
|
||
|
||
Scenario: Header items render as dividers
|
||
Tool: Playwright
|
||
Preconditions: Agenda with header-type items
|
||
Steps:
|
||
1. Navigate to edit page
|
||
2. Find header items (data-testid="agenda-header-item")
|
||
3. Assert they have divider styling (larger text, different background)
|
||
4. Assert they do NOT have a "+" upload button
|
||
Expected Result: Headers are visual dividers without interaction
|
||
Evidence: .sisyphus/evidence/task-12-header-dividers.png
|
||
|
||
Scenario: Highlighted items show correct status
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Navigate to edit page with: 1 song with arrangement, 1 song without arrangement, 1 item with slides, 1 item without slides
|
||
2. Assert song with arrangement has success highlight (data-testid contains class with green/emerald)
|
||
3. Assert song without arrangement has warning highlight (amber/orange)
|
||
4. Assert item with slides has success highlight
|
||
5. Assert plain item has no highlight
|
||
Expected Result: Correct visual status for each item type
|
||
Evidence: .sisyphus/evidence/task-12-highlights.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(ui): restructure Edit.vue with agenda view`
|
||
- Files: `resources/js/Pages/Services/Edit.vue`
|
||
- Pre-commit: `npm run build`
|
||
|
||
---
|
||
|
||
- [x] 13. Slide Upload on Agenda Items — "+" Button + Drag-and-Drop
|
||
|
||
**What to do**:
|
||
- Enhance `AgendaItemRow.vue` (from Task 11):
|
||
- When "+" button clicked: show/hide a `SlideUploader` component below the row
|
||
- SlideUploader configured with: `type='agenda_item'`, `serviceId`, and new prop `agendaItemId`
|
||
- After upload: reload page to show new slide thumbnails
|
||
- Update `SlideController::store()`:
|
||
- Accept optional `service_agenda_item_id` parameter
|
||
- When provided: set `service_agenda_item_id` on created Slide record
|
||
- Keep existing type parameter for backward compat, but allow new type value `'agenda_item'`
|
||
- Update `SlideUploader.vue`:
|
||
- Add optional `agendaItemId` prop
|
||
- When set: include `service_agenda_item_id` in upload FormData
|
||
- No expire-date picker for agenda item slides (they're service-specific)
|
||
- Show uploaded slides as small thumbnails inline in the agenda item row (reuse SlideGrid patterns)
|
||
- Update slide deletion to work for agenda-item slides
|
||
- Write tests:
|
||
- Test uploading slide with service_agenda_item_id
|
||
- Test slide appears linked to correct agenda item
|
||
- Test deletion works
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change file conversion logic (images, PPT processing)
|
||
- Do NOT add expire dates to agenda item slides
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 3 (with Tasks 10, 11 — but after 11 creates base component)
|
||
- **Blocks**: Task 18
|
||
- **Blocked By**: Tasks 2, 11
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `resources/js/Components/SlideUploader.vue` — Existing uploader component to extend
|
||
- `app/Http/Controllers/SlideController.php` — store() method handling uploads
|
||
- `resources/js/Components/SlideGrid.vue` — Thumbnail display pattern
|
||
|
||
**Test References**:
|
||
- `tests/Feature/SlideControllerTest.php` — Existing slide upload tests
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `SlideUploader` — Adding agendaItemId prop, minimal change to existing component
|
||
- `SlideController::store()` — Add service_agenda_item_id to the validation and storage logic
|
||
- `SlideGrid` — Reuse for inline thumbnail display in agenda rows
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npm run build` → no errors
|
||
- [ ] `php artisan test --filter=SlideControllerTest` → all tests pass
|
||
- [ ] Slides uploaded via agenda item "+" button are linked to correct agenda item
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Upload slide on agenda item
|
||
Tool: Playwright
|
||
Preconditions: Service with agenda items, dev server running
|
||
Steps:
|
||
1. Navigate to service edit page
|
||
2. Find a non-song agenda item (e.g. "Gebet")
|
||
3. Click "+" button (data-testid="agenda-item-add-slides")
|
||
4. Assert upload area appears below the row
|
||
5. Upload a test image file
|
||
6. Wait for upload to complete and page reload
|
||
7. Assert thumbnail appears in the agenda item row
|
||
Expected Result: Slide uploaded and visible as thumbnail in the agenda item
|
||
Evidence: .sisyphus/evidence/task-13-upload-slide.png
|
||
|
||
Scenario: Delete slide from agenda item
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Navigate to edit page with agenda item that has slides
|
||
2. Click delete button on slide thumbnail
|
||
3. Confirm deletion
|
||
4. Assert slide is removed
|
||
Expected Result: Slide deleted (soft-delete), no longer visible
|
||
Evidence: .sisyphus/evidence/task-13-delete-slide.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(ui): add slide upload on agenda items`
|
||
- Files: `resources/js/Components/AgendaItemRow.vue`, `resources/js/Components/SlideUploader.vue`, `app/Http/Controllers/SlideController.php`, test file
|
||
- Pre-commit: `npm run build && php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 14. Settings.vue — Add Agenda Configuration Fields
|
||
|
||
**What to do**:
|
||
- Update `resources/js/Pages/Settings.vue`:
|
||
- Add new section below existing "ProPresenter Makro-Konfiguration":
|
||
- Section title: "Agenda-Konfiguration"
|
||
- Description: "Diese Einstellungen steuern, wie der Gottesdienst-Ablauf angezeigt und exportiert wird."
|
||
- Add 4 new fields (same pattern as existing macro fields):
|
||
- `agenda_start_title` — Label: "Ablauf-Start", Placeholder: "z.B. Ablauf*"
|
||
- `agenda_end_title` — Label: "Ablauf-Ende", Placeholder: "z.B. Ende*"
|
||
- `agenda_announcement_position` — Label: "Ankündigungen-Position", Placeholder: "z.B. Information*,Hinweis*"
|
||
- Add help text: "Komma-getrennte Liste. Das erste passende Element im Ablauf bestimmt die Position der Ankündigungen im Export. * als Platzhalter."
|
||
- `agenda_sermon_matching` — Label: "Predigt-Erkennung", Placeholder: "z.B. Predigt*,Sermon*"
|
||
- Add help text: "Komma-getrennte Liste. Erkannte Elemente bekommen einen speziellen Upload-Bereich für Predigt-Folien. * als Platzhalter."
|
||
- Reuse existing `saveField()` auto-save on blur pattern
|
||
- Add `data-testid` attributes on all new inputs
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change existing macro settings section
|
||
- Do NOT add validation beyond what SettingsController provides
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 4 (with Tasks 15, 16)
|
||
- **Blocks**: Task 18
|
||
- **Blocked By**: Task 4
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `resources/js/Pages/Settings.vue` — Existing settings page (155 lines) with field rendering and auto-save pattern. Add new section following same structure.
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `Settings.vue` — The file being modified. Follow exact same field rendering pattern (v-for, auto-save on blur, saving/saved/error states)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npm run build` → no errors
|
||
- [ ] Settings page shows "Agenda-Konfiguration" section with 4 fields
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Agenda settings fields visible and functional
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Navigate to /settings
|
||
2. Assert section "Agenda-Konfiguration" is visible
|
||
3. Assert input (data-testid="setting-agenda_start_title") exists with placeholder "z.B. Ablauf*"
|
||
4. Assert input (data-testid="setting-agenda_announcement_position") exists
|
||
5. Type "Predigt*" into sermon matching field
|
||
6. Click outside (blur) to trigger save
|
||
7. Assert saved indicator (green checkmark) appears
|
||
8. Reload page
|
||
9. Assert value "Predigt*" persists
|
||
Expected Result: All 4 fields visible, auto-save works, values persist
|
||
Evidence: .sisyphus/evidence/task-14-agenda-settings.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(ui): add agenda settings to Settings page`
|
||
- Files: `resources/js/Pages/Settings.vue`
|
||
- Pre-commit: `npm run build`
|
||
|
||
---
|
||
|
||
- [x] 15. Update Service List — Status Columns
|
||
|
||
**What to do**:
|
||
- Update `ServiceController::index()`:
|
||
- Add count of agenda items with uploaded slides (non-song items that have slides)
|
||
- Keep songs_mapped_count and songs_arranged_count
|
||
- Keep sermon slides check (now: "any agenda item matching sermon pattern has slides")
|
||
- Add: `agenda_slides_count` — count of non-song agenda items that have at least one slide
|
||
- Update `resources/js/Pages/Services/Index.vue`:
|
||
- Replace moderation/sermon column with new status indicators:
|
||
- Songs: "x/y Lieder zugeordnet" (keep)
|
||
- Arrangements: "x/y Arrangements" (keep)
|
||
- Predigt: "Predigt-Folien ✓/✗" (keep, but source changes to agenda item matching)
|
||
- New: "N weitere Folien" — count of non-song agenda items with slides
|
||
- Remove references to moderation slides count
|
||
- Write tests:
|
||
- Test index returns correct agenda_slides_count
|
||
- Test sermon detection works via agenda item matching
|
||
|
||
**Must NOT do**:
|
||
- Do NOT redesign the service list layout
|
||
- Do NOT change finalize/reopen/download from list view
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 4 (with Tasks 14, 16)
|
||
- **Blocks**: None
|
||
- **Blocked By**: Tasks 1, 8
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Http/Controllers/ServiceController.php:30-86` — Current index() method with status counts
|
||
- `resources/js/Pages/Services/Index.vue` — Current list page (512 lines) with status columns
|
||
|
||
**Test References**:
|
||
- `tests/Feature/ServiceControllerTest.php` — Existing index tests
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `ServiceController::index()` — Modify count queries for new model
|
||
- `Index.vue` — Update display columns
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=ServiceControllerTest` → all tests pass
|
||
- [ ] `npm run build` → no errors
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Service list shows updated status columns
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Navigate to /services
|
||
2. Find a service row with known data
|
||
3. Assert songs count is visible (e.g. "3/3 Lieder")
|
||
4. Assert predigt status is visible
|
||
5. Assert "N weitere Folien" count is visible
|
||
6. Assert NO "Moderation" column exists
|
||
Expected Result: List shows songs, predigt, and additional slides count
|
||
Evidence: .sisyphus/evidence/task-15-service-list-status.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(ui): update service list status columns`
|
||
- Files: `resources/js/Pages/Services/Index.vue`, `app/Http/Controllers/ServiceController.php`, test file
|
||
- Pre-commit: `npm run build && php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 16. Wire Agenda-Ordered Export into Download Flow
|
||
|
||
**What to do**:
|
||
- Update `ServiceController::download()`:
|
||
- Use the updated `PlaylistExportService::generatePlaylist()` (from Task 9) which now follows agenda order
|
||
- Add agenda items and settings loading before calling export
|
||
- Pass announcement slides and their position to the export service
|
||
- Handle the fallback: if service has no agenda items, use old block-based export path
|
||
- Update `ServiceController::finalize()`:
|
||
- Use updated `finalizationStatus()` (from Task 8)
|
||
- Keep confirmation dialog behavior unchanged
|
||
- Write integration test:
|
||
- Test full flow: create service with agenda items → upload slides → finalize → download → verify .proplaylist content order
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change the download endpoint route
|
||
- Do NOT change the .proplaylist format
|
||
- Do NOT break existing downloadBundle() endpoint
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: NO
|
||
- **Parallel Group**: Wave 4 (after Tasks 9, 12)
|
||
- **Blocks**: Task 18
|
||
- **Blocked By**: Tasks 9, 12
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `app/Http/Controllers/ServiceController.php:226-260` — Current download() and finalize() methods
|
||
- `app/Services/PlaylistExportService.php` — Updated service from Task 9
|
||
|
||
**Test References**:
|
||
- `tests/Feature/FinalizationTest.php` — Existing finalize/download integration tests
|
||
- `tests/Feature/PlaylistExportTest.php` — Export tests
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `ServiceController` — The methods being wired up to use the new export
|
||
- `PlaylistExportService` — The updated service that now supports agenda ordering
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=FinalizationTest` → passes
|
||
- [ ] `php artisan test --filter=PlaylistExportTest` → passes
|
||
- [ ] `php artisan test --filter=test_download_follows_agenda_order` → passes
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Full finalize → download flow with agenda order
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_full_finalize_download_flow`
|
||
Expected Result: Service with agenda items finalizes and downloads .proplaylist with presentations in agenda order
|
||
Evidence: .sisyphus/evidence/task-16-full-flow.txt
|
||
|
||
Scenario: Old service without agenda items still downloadable
|
||
Tool: Bash (php artisan test)
|
||
Steps:
|
||
1. Run `php artisan test --filter=test_old_service_download_backward_compat`
|
||
Expected Result: Service finalized before migration downloads correctly using old block order
|
||
Evidence: .sisyphus/evidence/task-16-backward-compat.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(export): wire agenda export into download flow`
|
||
- Files: `app/Http/Controllers/ServiceController.php`, test file
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 17. Update Existing PHP Tests for Agenda Model
|
||
|
||
**What to do**:
|
||
- Review and update ALL existing PHP tests that reference the old model:
|
||
- `tests/Feature/ServiceControllerTest.php` — Update edit page assertions for agendaItems prop
|
||
- `tests/Feature/FinalizationTest.php` — Already updated in Task 8, verify integration
|
||
- `tests/Feature/PlaylistExportTest.php` — Already updated in Task 9, verify integration
|
||
- `tests/Feature/ChurchToolsSyncTest.php` — Already updated in Task 6, verify integration
|
||
- `tests/Feature/ModerationBlockTest.php` — Mark tests as covering deprecated feature or update
|
||
- `tests/Feature/SermonBlockTest.php` — Mark tests as covering deprecated feature or update
|
||
- `tests/Feature/SongsBlockTest.php` — Update to test SongAgendaItem behavior
|
||
- `tests/Feature/SlideControllerTest.php` — Add tests for agenda_item slide type
|
||
- `tests/Feature/InformationBlockTest.php` — Verify still passes unchanged
|
||
- Run full test suite: `php artisan test` — ALL tests must pass
|
||
- Do NOT delete old tests yet — only update them to work with new model
|
||
|
||
**Must NOT do**:
|
||
- Do NOT delete any test files (deprecation in Task 19)
|
||
- Do NOT skip tests — update them
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 5 (with Tasks 18)
|
||
- **Blocks**: Task 19
|
||
- **Blocked By**: Tasks 6, 7, 8, 9
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- All test files in `tests/Feature/` — each file listed above needs review
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Every test file may reference old props (moderationSlides, sermonSlides) or old model structure
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test` → ALL tests pass (0 failures, 0 errors)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Full PHP test suite passes
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run `php artisan test`
|
||
Expected Result: All tests pass with 0 failures and 0 errors
|
||
Evidence: .sisyphus/evidence/task-17-full-test-suite.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `test(php): update existing tests for agenda model`
|
||
- Files: `tests/Feature/*.php` (multiple)
|
||
- Pre-commit: `php artisan test`
|
||
|
||
---
|
||
|
||
- [x] 18. Playwright E2E Tests for Restructured Edit Page
|
||
|
||
**What to do**:
|
||
- Update/create E2E test files:
|
||
- `tests/e2e/service-edit-agenda.spec.ts` (NEW):
|
||
- Test agenda items display in correct order
|
||
- Test song items show arrangement pills
|
||
- Test non-song items show "+" upload button
|
||
- Test header items render as dividers
|
||
- Test slide upload on agenda item
|
||
- Test arrangement dialog opens from song edit button
|
||
- Test arrangement drag-and-drop
|
||
- Test arrangement 2x duplicate
|
||
- Test announcement position indicator
|
||
- Test sermon item detection
|
||
- Test highlight states (success/warning/neutral)
|
||
- Update `tests/e2e/service-edit-information.spec.ts`:
|
||
- Verify information block still at top, working as before
|
||
- Update `tests/e2e/service-finalization.spec.ts`:
|
||
- Verify finalize/download flow with new agenda model
|
||
- Deprecate/remove:
|
||
- `tests/e2e/service-edit-moderation.spec.ts` — no longer relevant
|
||
- `tests/e2e/service-edit-sermon.spec.ts` — sermon is now part of agenda
|
||
- `tests/e2e/service-edit-songs.spec.ts` — songs are now part of agenda
|
||
- Update `tests/e2e/sync-and-pro.spec.ts`:
|
||
- Verify sync creates agenda items
|
||
- Run: `npx playwright test` — all tests pass
|
||
|
||
**Must NOT do**:
|
||
- Do NOT change auth setup
|
||
- Do NOT change base URL configuration
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 5 (with Task 17)
|
||
- **Blocks**: Task 19
|
||
- **Blocked By**: Tasks 12, 13, 14, 16
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `tests/e2e/service-edit-songs.spec.ts` — Existing song E2E tests with authentication and Playwright patterns
|
||
- `tests/e2e/auth.setup.ts` — Auth setup for E2E tests
|
||
- `tests/e2e/service-finalization.spec.ts` — Finalization E2E flow
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Existing E2E tests — Follow same patterns (authentication, selectors, assertions, German text)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npx playwright test` → all tests pass
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: E2E test suite passes
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run `npx playwright test`
|
||
Expected Result: All E2E tests pass
|
||
Evidence: .sisyphus/evidence/task-18-e2e-results.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `test(e2e): Playwright tests for restructured edit page`
|
||
- Files: `tests/e2e/*.spec.ts`
|
||
- Pre-commit: `npx playwright test`
|
||
|
||
---
|
||
|
||
- [x] 19. Remove Deprecated Block Components
|
||
|
||
**What to do**:
|
||
- Remove Vue components no longer used:
|
||
- `resources/js/Components/Blocks/ModerationBlock.vue` — replaced by AgendaItemRow
|
||
- `resources/js/Components/Blocks/SermonBlock.vue` — replaced by sermon-flagged AgendaItemRow
|
||
- `resources/js/Components/Blocks/SongsBlock.vue` — replaced by SongAgendaItem
|
||
- Update `ArrangementConfigurator.vue`:
|
||
- If still used by `SongEditModal.vue` (SongDB page): keep it
|
||
- If `SongEditModal.vue` now uses `ArrangementDialog.vue`: remove `ArrangementConfigurator.vue`
|
||
- Remove deprecated test files:
|
||
- `tests/Feature/ModerationBlockTest.php` — if fully replaced by agenda tests
|
||
- `tests/Feature/SermonBlockTest.php` — if fully replaced
|
||
- `tests/e2e/service-edit-moderation.spec.ts`
|
||
- `tests/e2e/service-edit-sermon.spec.ts`
|
||
- `tests/e2e/service-edit-songs.spec.ts`
|
||
- Run full test suite to verify nothing breaks:
|
||
- `php artisan test` → pass
|
||
- `npx playwright test` → pass
|
||
- `npm run build` → pass
|
||
|
||
**Must NOT do**:
|
||
- Do NOT remove InformationBlock (still in use)
|
||
- Do NOT remove SlideGrid, SlideUploader (still in use)
|
||
- Do NOT remove ArrangementConfigurator if SongEditModal still uses it
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: NO
|
||
- **Parallel Group**: Wave 5 (after Tasks 17, 18)
|
||
- **Blocks**: None
|
||
- **Blocked By**: Tasks 17, 18
|
||
|
||
**References**:
|
||
**Pattern References**:
|
||
- `resources/js/Components/Blocks/` — Directory containing block components to review
|
||
- `resources/js/Components/SongEditModal.vue` — Check if it still uses ArrangementConfigurator
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `npm run build` → no errors (no missing imports)
|
||
- [ ] `php artisan test` → all tests pass
|
||
- [ ] `npx playwright test` → all tests pass
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Build succeeds after component removal
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run `npm run build`
|
||
2. Run `php artisan test`
|
||
3. Run `npx playwright test`
|
||
Expected Result: All three commands succeed with zero errors
|
||
Evidence: .sisyphus/evidence/task-19-cleanup-verification.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `refactor(cleanup): remove deprecated block components`
|
||
- Files: deleted Vue components, deleted test files
|
||
- Pre-commit: `npm run build && php artisan test`
|
||
|
||
---
|
||
|
||
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
||
|
||
> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
|
||
|
||
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||
Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
|
||
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
|
||
|
||
- [x] F2. **Code Quality Review** — `unspecified-high`
|
||
Run `./vendor/bin/pint --test` + `php artisan test`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names. Verify German Du-form in all UI text.
|
||
Output: `Pint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT`
|
||
|
||
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill)
|
||
Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration: create service → sync agenda → upload slides on agenda items → configure arrangement → finalize → download .proplaylist → verify order. Edge cases: empty agenda, no songs, no matching settings.
|
||
Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`
|
||
|
||
- [x] F4. **Scope Fidelity Check** — `deep`
|
||
For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination. Flag unaccounted changes.
|
||
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`
|
||
|
||
---
|
||
|
||
## Commit Strategy
|
||
|
||
| Task | Commit Message | Files | Pre-commit |
|
||
|------|---------------|-------|------------|
|
||
| 1 | `feat(db): add service_agenda_items table and model` | migration, model, factory, test | `php artisan test` |
|
||
| 2 | `feat(db): add service_agenda_item_id FK to slides` | migration, Slide model, test | `php artisan test` |
|
||
| 3 | `feat(service): add AgendaMatcherService with wildcard namesmatching` | service, test | `php artisan test` |
|
||
| 4 | `feat(settings): add agenda configuration keys` | controller, test | `php artisan test` |
|
||
| 5 | `chore(debug): add CTS agenda type discovery command` | artisan command | `php artisan test` |
|
||
| 6 | `feat(sync): sync all CTS agenda items (not just songs)` | ChurchToolsService, test | `php artisan test` |
|
||
| 7 | `feat(controller): pass agenda items to edit page` | ServiceController, test | `php artisan test` |
|
||
| 8 | `refactor(model): update finalizationStatus for agenda model` | Service model, test | `php artisan test` |
|
||
| 9 | `feat(export): agenda-ordered playlist export` | PlaylistExportService, test | `php artisan test` |
|
||
| 10 | `feat(ui): redesign ArrangementDialog with lyric preview` | Vue component | `npm run build` |
|
||
| 11 | `feat(ui): add AgendaItemRow and SongAgendaItem components` | Vue components | `npm run build` |
|
||
| 12 | `feat(ui): restructure Edit.vue with agenda view` | Edit.vue | `npm run build` |
|
||
| 13 | `feat(ui): add slide upload on agenda items` | Vue component, SlideController | `npm run build && php artisan test` |
|
||
| 14 | `feat(ui): add agenda settings to Settings page` | Settings.vue | `npm run build` |
|
||
| 15 | `feat(ui): update service list status columns` | Index.vue, ServiceController | `npm run build && php artisan test` |
|
||
| 16 | `feat(export): wire agenda export into download flow` | ServiceController | `php artisan test` |
|
||
| 17 | `test(php): update existing tests for agenda model` | test files | `php artisan test` |
|
||
| 18 | `test(e2e): Playwright tests for restructured edit page` | E2E test files | `npx playwright test` |
|
||
| 19 | `refactor(cleanup): remove deprecated block components` | Vue components, test files | `npm run build && php artisan test` |
|
||
|
||
---
|
||
|
||
## Success Criteria
|
||
|
||
### Verification Commands
|
||
```bash
|
||
php artisan test # Expected: all tests pass
|
||
npx playwright test # Expected: all E2E tests pass
|
||
./vendor/bin/pint --test # Expected: no formatting issues
|
||
npm run build # Expected: no build errors
|
||
```
|
||
|
||
### Final Checklist
|
||
- [x] All "Must Have" present
|
||
- [x] All "Must NOT Have" absent
|
||
- [x] All tests pass (PHP + E2E)
|
||
- [x] Service edit page shows CTS agenda
|
||
- [x] Arrangement dialog has two-column layout
|
||
- [x] Export follows agenda order
|
||
- [x] Settings page has agenda configuration
|
||
- [x] Existing finalized services still downloadable
|