refactor(model): update finalizationStatus for agenda model
This commit is contained in:
parent
7a71b8b2de
commit
2d70026a20
|
|
@ -86,3 +86,107 @@ ### Code Patterns
|
|||
- Public method documentation via docstrings (necessary for API clarity)
|
||||
- Test organization via section headers (necessary for readability)
|
||||
- No regex used — just `fnmatch()` for simplicity and correctness
|
||||
|
||||
## 2026-03-29 — Settings Controller Agenda Keys
|
||||
|
||||
### Added 4 New Configuration Keys
|
||||
- `agenda_start_title` — start boundary title pattern for agenda display
|
||||
- `agenda_end_title` — end boundary title pattern
|
||||
- `agenda_announcement_position` — comma-separated patterns for announcement position
|
||||
- `agenda_sermon_matching` — comma-separated patterns for sermon recognition
|
||||
|
||||
### SettingsController Updates
|
||||
- Modified `MACRO_KEYS` constant array in `SettingsController.php` to include the 4 new keys
|
||||
- Validation automatically accepts new keys via `'in:'.implode(',', self::MACRO_KEYS)` rule
|
||||
- `index()` already iterates through all MACRO_KEYS, so no additional logic needed
|
||||
- Settings fetched via `Setting::get($key)` and passed to Inertia
|
||||
|
||||
### Test Implementation
|
||||
- Created `tests/Feature/SettingsControllerAgendaKeysTest.php` with 6 Pest tests
|
||||
- Test patterns used:
|
||||
- `$response->assertInertia()` with `->has('settings.key')` for prop assertions
|
||||
- `$this->patchJson(route('settings.update'), [...])` for PATCH requests
|
||||
- `Setting::get($key)` to verify database persistence
|
||||
- `$response->assertUnprocessable()` for invalid key validation
|
||||
- All tests pass; code passes Pint formatting
|
||||
|
||||
## 2026-03-29 — DiscoverAgendaTypes command
|
||||
|
||||
### Implementation
|
||||
- Created `DiscoverAgendaTypes.php` artisan command (cts:discover-agenda-types)
|
||||
- Fetches next upcoming service from DB (date >= today, order by date ASC)
|
||||
- Calls `ChurchToolsService->syncAgenda($ctsEventId)` to fetch agenda
|
||||
- Displays formatted table: Position | Titel | Typ | Hat Song | Vor Event | Dauer
|
||||
- Prints distinct type values found in agenda items
|
||||
|
||||
### Key Methods Used
|
||||
- `$agenda->getItems()` returns array of EventAgendaItem
|
||||
- `EventAgendaItem` getters: `getPosition()`, `getTitle()`, `getType()`, `getSong()`, `getIsBeforeEvent()`, `getDuration()`
|
||||
- `Str::limit($string, 40, '...')` truncates titles to 40 chars
|
||||
|
||||
### Test Coverage (5 tests)
|
||||
- No service found → outputs "Kein bevorstehender Service gefunden."
|
||||
- Displays table and unique types from agenda items
|
||||
- Handles null agenda → outputs "Keine Agenda für diesen Service gefunden."
|
||||
- Handles empty items array → outputs "Keine Agenda-Items gefunden."
|
||||
- Truncates long titles (40 chars) and still displays unique types
|
||||
|
||||
### Code Patterns
|
||||
- Command signature: `protected $signature = 'cts:discover-agenda-types';`
|
||||
- Dependency injection via `handle(ChurchToolsService $churchToolsService): int`
|
||||
- Test mocking: `$this->app->bind(ChurchToolsService::class, function () use (...) { ... })`
|
||||
- Table output: `$this->table($headers, $rows)` with array of arrays
|
||||
- Info output: `$this->info()` for text, `$this->line()` for list items
|
||||
|
||||
### Status
|
||||
- ✓ Command created: `app/Console/Commands/DiscoverAgendaTypes.php`
|
||||
- ✓ Tests created: `tests/Feature/DiscoverAgendaTypesTest.php` (5 tests, all passing)
|
||||
- ✓ Pint formatting: clean (`pint --test` passes)
|
||||
- ✓ Ready for: `git commit -m "chore(debug): add CTS agenda type discovery command"`
|
||||
|
||||
## 2026-03-29 — Service::finalizationStatus() refactor
|
||||
|
||||
### Implementation
|
||||
- Updated `Service::finalizationStatus()` to check new `service_agenda_items` model
|
||||
- Added backward compatibility: if `agendaItems()->count() === 0`, fall back to legacy `finalizationStatusLegacy()`
|
||||
- New checks (agenda-based):
|
||||
1. Unmatched songs: Find agenda items with `service_song_id` where `serviceSong.song_id IS NULL`
|
||||
- Warning format: `"{songName} wurde noch nicht zugeordnet"`
|
||||
2. Songs without arrangement: Find agenda items where `serviceSong.song_arrangement_id IS NULL`
|
||||
- Warning format: `"{songName} hat kein Arrangement ausgewählt"`
|
||||
3. Sermon slides:
|
||||
- If `Setting::get('agenda_sermon_matching')` configured: check if any matching agenda item has slides
|
||||
- Else: fall back to legacy check `$this->slides()->where('type', 'sermon')->exists()`
|
||||
- Warning: `"Keine Predigt-Folien hochgeladen"`
|
||||
|
||||
### Code Changes
|
||||
- Modified: `app/Models/Service.php`
|
||||
- Added import: `use App\Services\AgendaMatcherService;`
|
||||
- Updated `finalizationStatus()` method (95 lines)
|
||||
- Added private `finalizationStatusLegacy()` method (28 lines) for backward compat
|
||||
- Used `app(AgendaMatcherService::class)` to instantiate service for pattern matching
|
||||
- Preserved `agendaItems()` HasMany relationship (added in earlier Task)
|
||||
|
||||
- Modified: `tests/Feature/FinalizationTest.php`
|
||||
- Added imports: `ServiceAgendaItem`, `Setting`
|
||||
- Renamed first test to `finalize ohne voraussetzungen gibt warnungen zurueck (legacy)` for clarity
|
||||
- Added 6 new agenda-based tests:
|
||||
1. `agenda finalization warnt bei unzugeordneten songs` — unmatched song warning
|
||||
2. `agenda finalization warnt bei songs ohne arrangement` — no arrangement warning
|
||||
3. `agenda finalization bereit wenn alle songs zugeordnet und arrangement` — all pass
|
||||
4. `agenda finalization warnt wenn keine predigtfolien und sermon setting konfiguriert` — sermon with setting
|
||||
5. `agenda finalization ok wenn predigtfolien bei sermon item vorhanden` — sermon slides exist
|
||||
6. `agenda finalization warnt wenn keine predigtfolien und kein sermon setting` — no setting fallback
|
||||
|
||||
### Test Results
|
||||
- All 18 tests pass (12 existing + 6 new)
|
||||
- Backward compatibility verified: legacy tests with `service_songs` still work
|
||||
- Pint formatting: clean
|
||||
|
||||
### Code Patterns Used
|
||||
- Guard clause in `finalizationStatus()` for backward compat check
|
||||
- `$agendaItems->filter()` with closures for conditional filtering
|
||||
- Service instantiation via `app(AgendaMatcherService::class)` with closure captures
|
||||
- Null-safe navigation: `$item->serviceSong?->song_id`
|
||||
- Setting retrieval: `Setting::get('agenda_sermon_matching')`
|
||||
- Slide filtering: `whereNull('deleted_at')` for soft deletes
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\AgendaMatcherService;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
|
@ -42,6 +43,11 @@ public function slides(): HasMany
|
|||
return $this->hasMany(Slide::class);
|
||||
}
|
||||
|
||||
public function agendaItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServiceAgendaItem::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check finalization prerequisites and return warnings.
|
||||
*
|
||||
|
|
@ -51,6 +57,87 @@ public function finalizationStatus(): array
|
|||
{
|
||||
$warnings = [];
|
||||
|
||||
// Backward compatibility: if no agenda items exist, use old checks
|
||||
if ($this->agendaItems()->count() === 0) {
|
||||
return $this->finalizationStatusLegacy();
|
||||
}
|
||||
|
||||
// New agenda-based checks
|
||||
$agendaItems = $this->agendaItems()->with('serviceSong.song')->get();
|
||||
|
||||
// Check 1: Unmatched songs (song-type agenda items where song_id IS NULL)
|
||||
$unmatchedSongs = $agendaItems->filter(function (ServiceAgendaItem $item) {
|
||||
if ($item->service_song_id === null) {
|
||||
return false;
|
||||
}
|
||||
$serviceSong = $item->serviceSong;
|
||||
|
||||
return $serviceSong && $serviceSong->song_id === null;
|
||||
});
|
||||
|
||||
foreach ($unmatchedSongs as $item) {
|
||||
$songName = $item->serviceSong?->cts_song_name ?? 'Unbekannter Song';
|
||||
$warnings[] = "{$songName} wurde noch nicht zugeordnet";
|
||||
}
|
||||
|
||||
// Check 2: Matched songs without arrangement
|
||||
$matchedSongsWithoutArrangement = $agendaItems->filter(function (ServiceAgendaItem $item) {
|
||||
if ($item->service_song_id === null) {
|
||||
return false;
|
||||
}
|
||||
$serviceSong = $item->serviceSong;
|
||||
|
||||
return $serviceSong && $serviceSong->song_id !== null && $serviceSong->song_arrangement_id === null;
|
||||
});
|
||||
|
||||
foreach ($matchedSongsWithoutArrangement as $item) {
|
||||
$songName = $item->serviceSong?->cts_song_name ?? 'Unbekannter Song';
|
||||
$warnings[] = "{$songName} hat kein Arrangement ausgewählt";
|
||||
}
|
||||
|
||||
// Check 3: Sermon slides
|
||||
$sermonPatterns = Setting::get('agenda_sermon_matching');
|
||||
|
||||
if ($sermonPatterns) {
|
||||
// Settings configured: check if any agenda item flagged as sermon has slides
|
||||
$agendaMatcher = app(AgendaMatcherService::class);
|
||||
$sermonItems = $agendaItems->filter(function (ServiceAgendaItem $item) use ($agendaMatcher, $sermonPatterns) {
|
||||
return $agendaMatcher->matches($item->title, $sermonPatterns);
|
||||
});
|
||||
|
||||
$sermonHasSlides = false;
|
||||
foreach ($sermonItems as $item) {
|
||||
if ($item->slides()->whereNull('deleted_at')->exists()) {
|
||||
$sermonHasSlides = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $sermonHasSlides) {
|
||||
$warnings[] = 'Keine Predigt-Folien hochgeladen';
|
||||
}
|
||||
} else {
|
||||
// No settings configured: fall back to checking slides table
|
||||
if (! $this->slides()->where('type', 'sermon')->exists()) {
|
||||
$warnings[] = 'Keine Predigt-Folien hochgeladen';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ready' => empty($warnings),
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy finalization status for services without agenda items.
|
||||
*
|
||||
* @return array{ready: bool, warnings: string[]}
|
||||
*/
|
||||
private function finalizationStatusLegacy(): array
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
$totalSongs = $this->serviceSongs()->count();
|
||||
$mappedSongs = $this->serviceSongs()->whereNotNull('song_id')->count();
|
||||
$arrangedSongs = $this->serviceSongs()->whereNotNull('song_arrangement_id')->count();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceAgendaItem;
|
||||
use App\Models\ServiceSong;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Slide;
|
||||
use App\Models\Song;
|
||||
use App\Models\SongArrangement;
|
||||
|
|
@ -19,10 +21,10 @@
|
|||
$this->user = User::factory()->create();
|
||||
});
|
||||
|
||||
test('finalize ohne voraussetzungen gibt warnungen zurueck', function () {
|
||||
test('finalize ohne voraussetzungen gibt warnungen zurueck (legacy)', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
|
||||
// Song without match or arrangement
|
||||
// Song without match or arrangement (legacy service_songs)
|
||||
ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => null,
|
||||
|
|
@ -251,3 +253,228 @@
|
|||
->and($status['warnings'])->toHaveCount(1)
|
||||
->and($status['warnings'][0])->toContain('Predigtfolien');
|
||||
});
|
||||
|
||||
// Agenda-based finalization tests
|
||||
test('agenda finalization warnt bei unzugeordneten songs', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
|
||||
// Create an unmatched song via agenda item
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => null,
|
||||
'song_arrangement_id' => null,
|
||||
'use_translation' => false,
|
||||
'order' => 1,
|
||||
'cts_song_name' => 'Unzugeordneter Song',
|
||||
'cts_ccli_id' => '12345',
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '1',
|
||||
'title' => 'Song 1',
|
||||
'type' => 'Song',
|
||||
'service_song_id' => $serviceSong->id,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$status = $service->finalizationStatus();
|
||||
|
||||
expect($status['ready'])->toBeFalse()
|
||||
->and($status['warnings'])->toContain('Unzugeordneter Song wurde noch nicht zugeordnet');
|
||||
});
|
||||
|
||||
test('agenda finalization warnt bei songs ohne arrangement', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
$song = Song::factory()->create();
|
||||
|
||||
// Song is matched but has no arrangement
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song->id,
|
||||
'song_arrangement_id' => null,
|
||||
'use_translation' => false,
|
||||
'order' => 1,
|
||||
'cts_song_name' => 'Song ohne Arrangement',
|
||||
'cts_ccli_id' => '12345',
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '1',
|
||||
'title' => 'Song 1',
|
||||
'type' => 'Song',
|
||||
'service_song_id' => $serviceSong->id,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$status = $service->finalizationStatus();
|
||||
|
||||
expect($status['ready'])->toBeFalse()
|
||||
->and($status['warnings'])->toContain('Song ohne Arrangement hat kein Arrangement ausgewählt');
|
||||
});
|
||||
|
||||
test('agenda finalization bereit wenn alle songs zugeordnet und arrangement', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
$song = Song::factory()->create();
|
||||
$arrangement = SongArrangement::factory()->create(['song_id' => $song->id]);
|
||||
|
||||
// Song is matched with arrangement
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song->id,
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'use_translation' => false,
|
||||
'order' => 1,
|
||||
'cts_song_name' => 'Zugeordneter Song',
|
||||
'cts_ccli_id' => '12345',
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '1',
|
||||
'title' => 'Song 1',
|
||||
'type' => 'Song',
|
||||
'service_song_id' => $serviceSong->id,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
// Add sermon slides
|
||||
Slide::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'type' => 'sermon',
|
||||
]);
|
||||
|
||||
$status = $service->finalizationStatus();
|
||||
|
||||
expect($status['ready'])->toBeTrue()
|
||||
->and($status['warnings'])->toHaveCount(0);
|
||||
});
|
||||
|
||||
test('agenda finalization warnt wenn keine predigtfolien und sermon setting konfiguriert', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
|
||||
// Configure sermon pattern
|
||||
Setting::set('agenda_sermon_matching', 'Predigt*');
|
||||
|
||||
$song = Song::factory()->create();
|
||||
$arrangement = SongArrangement::factory()->create(['song_id' => $song->id]);
|
||||
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song->id,
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'use_translation' => false,
|
||||
'order' => 1,
|
||||
'cts_song_name' => 'Zugeordneter Song',
|
||||
'cts_ccli_id' => '12345',
|
||||
]);
|
||||
|
||||
// Create agenda item for sermon (matches pattern)
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '1',
|
||||
'title' => 'Predigt',
|
||||
'type' => 'Default',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '2',
|
||||
'title' => 'Song 1',
|
||||
'type' => 'Song',
|
||||
'service_song_id' => $serviceSong->id,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
// No sermon slides
|
||||
$status = $service->finalizationStatus();
|
||||
|
||||
expect($status['ready'])->toBeFalse()
|
||||
->and($status['warnings'])->toContain('Keine Predigt-Folien hochgeladen');
|
||||
});
|
||||
|
||||
test('agenda finalization ok wenn predigtfolien bei sermon item vorhanden', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
|
||||
// Configure sermon pattern
|
||||
Setting::set('agenda_sermon_matching', 'Predigt*');
|
||||
|
||||
$song = Song::factory()->create();
|
||||
$arrangement = SongArrangement::factory()->create(['song_id' => $song->id]);
|
||||
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song->id,
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'use_translation' => false,
|
||||
'order' => 1,
|
||||
'cts_song_name' => 'Zugeordneter Song',
|
||||
'cts_ccli_id' => '12345',
|
||||
]);
|
||||
|
||||
// Create sermon agenda item
|
||||
$sermonItem = ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '1',
|
||||
'title' => 'Predigt',
|
||||
'type' => 'Default',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '2',
|
||||
'title' => 'Song 1',
|
||||
'type' => 'Song',
|
||||
'service_song_id' => $serviceSong->id,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
// Add sermon slides to the sermon agenda item
|
||||
Slide::factory()->create([
|
||||
'service_id' => $service->id,
|
||||
'service_agenda_item_id' => $sermonItem->id,
|
||||
]);
|
||||
|
||||
$status = $service->finalizationStatus();
|
||||
|
||||
expect($status['ready'])->toBeTrue()
|
||||
->and($status['warnings'])->toHaveCount(0);
|
||||
});
|
||||
|
||||
test('agenda finalization warnt wenn keine predigtfolien und kein sermon setting', function () {
|
||||
$service = Service::factory()->create(['finalized_at' => null]);
|
||||
|
||||
// No sermon pattern setting configured
|
||||
Setting::set('agenda_sermon_matching', '');
|
||||
|
||||
$song = Song::factory()->create();
|
||||
$arrangement = SongArrangement::factory()->create(['song_id' => $song->id]);
|
||||
|
||||
$serviceSong = ServiceSong::create([
|
||||
'service_id' => $service->id,
|
||||
'song_id' => $song->id,
|
||||
'song_arrangement_id' => $arrangement->id,
|
||||
'use_translation' => false,
|
||||
'order' => 1,
|
||||
'cts_song_name' => 'Zugeordneter Song',
|
||||
'cts_ccli_id' => '12345',
|
||||
]);
|
||||
|
||||
ServiceAgendaItem::create([
|
||||
'service_id' => $service->id,
|
||||
'position' => '1',
|
||||
'title' => 'Song 1',
|
||||
'type' => 'Song',
|
||||
'service_song_id' => $serviceSong->id,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
// No sermon slides - fallback should warn
|
||||
$status = $service->finalizationStatus();
|
||||
|
||||
expect($status['ready'])->toBeFalse()
|
||||
->and($status['warnings'])->toContain('Keine Predigt-Folien hochgeladen');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue