feat(settings): add agenda configuration keys
This commit is contained in:
parent
03224ffa06
commit
1f367b6f37
|
|
@ -1,14 +1,31 @@
|
|||
{
|
||||
"active_plan": null,
|
||||
"completed_plan": "/Users/thorsten/AI/cts-work/.sisyphus/plans/cts-bugfix-features.md",
|
||||
"completed_at": "2026-03-02T11:20:00.000Z",
|
||||
"started_at": "2026-03-02T09:38:25.624Z",
|
||||
"active_plan": "/Users/thorsten/AI/cts-work/.sisyphus/plans/edit-page-restructure.md",
|
||||
"started_at": "2026-03-29T09:26:31.630Z",
|
||||
"session_ids": [
|
||||
"ses_355fcc13effe4ksRKIO611tYSD"
|
||||
"ses_2c7383fb5ffer5YzYhHHeCDk9a",
|
||||
"ses_2c7140656ffeXuVsz4aZDp9jtT",
|
||||
"ses_2c70cb563ffeuKjg4NciBBmm7R"
|
||||
],
|
||||
"plan_name": "cts-bugfix-features",
|
||||
"worktree_path": "/Users/thorsten/AI/cts-work",
|
||||
"status": "complete",
|
||||
"tasks_completed": 20,
|
||||
"tasks_total": 20
|
||||
}
|
||||
"plan_name": "edit-page-restructure",
|
||||
"agent": "atlas",
|
||||
"task_sessions": {
|
||||
"todo:1": {
|
||||
"task_key": "todo:1",
|
||||
"task_label": "1",
|
||||
"task_title": "Create `service_agenda_items` Migration, Model + Factory",
|
||||
"session_id": "ses_2c7140656ffeXuVsz4aZDp9jtT",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "quick",
|
||||
"updated_at": "2026-03-29T09:32:40.580Z"
|
||||
},
|
||||
"todo:3": {
|
||||
"task_key": "todo:3",
|
||||
"task_label": "3",
|
||||
"task_title": "Create AgendaMatcherService (Wildcard Namesmatching)",
|
||||
"session_id": "ses_2c70cb563ffeuKjg4NciBBmm7R",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "quick",
|
||||
"updated_at": "2026-03-29T09:37:26.235Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
0
.sisyphus/notepads/edit-page-restructure/issues.md
Normal file
0
.sisyphus/notepads/edit-page-restructure/issues.md
Normal file
88
.sisyphus/notepads/edit-page-restructure/learnings.md
Normal file
88
.sisyphus/notepads/edit-page-restructure/learnings.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Learnings — edit-page-restructure
|
||||
|
||||
## 2026-03-29 — Session start
|
||||
|
||||
### Code Conventions
|
||||
- Migrations: anonymous class `return new class extends Migration {`; methods `up(): void`, `down(): void`
|
||||
- Models: `$fillable` array; `casts()` method (not `$casts` property); promoted properties in constructor
|
||||
- SoftDeletes: used on Song and Slide models; `whereNull('deleted_at')` in manual queries
|
||||
- PHP string concat: no spaces around `.` operator
|
||||
- Return types always present; union types for multiple returns
|
||||
- Relationships: typed return types (`HasMany`, `BelongsTo`)
|
||||
- Error messages: German Du-form; flash `->with('success', '...')`; JSON `'message'` key
|
||||
|
||||
### Codebase Facts
|
||||
- `service_songs` table stores ONLY songs from CTS agenda (via `getSongs()`)
|
||||
- `EventAgendaItem.type` actual values: UNKNOWN — Task 5 will discover
|
||||
- `EventAgendaItem.getItems()` returns ALL agenda items (not just songs)
|
||||
- `isBeforeEvent=true` items should be excluded from visible agenda
|
||||
- Settings stored as plain strings in `settings` table (key-value)
|
||||
- Settings controller uses `MACRO_KEYS` constant array for validation
|
||||
- SlideUploader sends `type`, `service_id`, `expire_date` in FormData
|
||||
- PlaylistExportService builds: info slides → matched songs → moderation → sermon (OLD ORDER)
|
||||
- Backward compat needed: old services without agenda items fall back to block-based export
|
||||
|
||||
### Test Patterns
|
||||
- Pest v4 function-style: `test('description', function() { ... })`
|
||||
- DB reset: `use RefreshDatabase;` in class-based, or implicit in Pest
|
||||
- Inertia assertions: `$response->assertInertia(fn ($page) => $page->component('...')->has('...'))`
|
||||
- Mocked API: injectable closures in ChurchToolsService constructor
|
||||
- Storage: `Storage::fake('public')` in beforeEach for file tests
|
||||
- Carbon: `Carbon::setTestNow(...)` for deterministic dates
|
||||
|
||||
## 2026-03-29 — ServiceAgendaItem DB schema
|
||||
|
||||
### Created
|
||||
- Migration: `2026_03_29_100001_create_service_agenda_items_table.php` — contains all EventAgendaItem fields from CTS
|
||||
- Migration: `2026_03_29_100002_add_service_agenda_item_id_to_slides_table.php` — links slides to agenda items
|
||||
- Model: `ServiceAgendaItem` with `scopeVisible()`, relationships (service, serviceSong, slides)
|
||||
- Factory: `ServiceAgendaItemFactory` with `withSong()` and `nonSong()` states
|
||||
- Tests: `ServiceAgendaItemTest` — 14 tests covering schema, relationships, cascades, scopes
|
||||
|
||||
### Key Fields
|
||||
- `cts_agenda_item_id` (nullable, indexed): CTS API item ID for syncing
|
||||
- `position` (string): CTS agenda item position (e.g. "1", "2", "1.1")
|
||||
- `type` (string): CTS type value ("Song", "Default", "Header", etc.)
|
||||
- `is_before_event` (boolean): filters with `scopeVisible()` scope
|
||||
- `responsible` (json → array cast): responsible persons
|
||||
- `service_song_id` (nullable FK): links agenda item to song (nullOnDelete)
|
||||
- `sort_order` (unsignedInt): unique per service with `unique(['service_id', 'sort_order'])`
|
||||
|
||||
### Relationships
|
||||
- `service()` BelongsTo Service (cascadeOnDelete)
|
||||
- `serviceSong()` BelongsTo ServiceSong (nullable, nullOnDelete)
|
||||
- `slides()` HasMany Slide (new FK added to slides table)
|
||||
|
||||
### Test Patterns
|
||||
- Factory states: `->withSong(song)` for song items, `->nonSong()` for non-song
|
||||
- Scope: `ServiceAgendaItem::visible()` filters `is_before_event=false`
|
||||
- Migrations now always use anonymous class `return new class extends Migration {}`
|
||||
- Unique constraint throws `QueryException` when violated (tested)
|
||||
|
||||
### Slide Model Update
|
||||
- Added `service_agenda_item_id` to fillable
|
||||
- Added `serviceAgendaItem()` BelongsTo relationship (nullOnDelete)
|
||||
|
||||
## 2026-03-29 — AgendaMatcherService
|
||||
|
||||
### Implementation
|
||||
- Created `AgendaMatcherService` with 4 public methods for glob-style wildcard matching
|
||||
- Uses PHP's native `fnmatch($pattern, $title, FNM_CASEFOLD)` for * wildcard support and case-insensitive matching
|
||||
- Key methods:
|
||||
- `matches(string $itemTitle, string $pattern): bool` — single pattern match
|
||||
- `matchesAny(string $itemTitle, array $patterns): bool` — match against array
|
||||
- `findFirstMatch(array $agendaItems, string $patterns): ?ServiceAgendaItem` — comma-separated patterns, returns first match or null
|
||||
- `filterBetween(array $items, ?string $startPattern, ?string $endPattern): array` — filters items between boundaries (boundaries excluded)
|
||||
|
||||
### Test Coverage
|
||||
- 17 tests in Pest function-style covering all methods
|
||||
- Patterns tested: exact match, suffix wildcard, prefix wildcard, both sides, case-insensitive, no-match
|
||||
- `filterBetween` edge cases: both boundaries, start-only, neither, no-matching-start, empty arrays
|
||||
- Pattern parsing: comma-separated patterns with whitespace trimming
|
||||
- All tests pass; Pint clean
|
||||
|
||||
### Code Patterns
|
||||
- Service class structure matches `SongMatchingService` (constructor, public methods, no state)
|
||||
- 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
|
||||
1815
.sisyphus/plans/edit-page-restructure.md
Normal file
1815
.sisyphus/plans/edit-page-restructure.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,10 @@ class SettingsController extends Controller
|
|||
'macro_uuid',
|
||||
'macro_collection_name',
|
||||
'macro_collection_uuid',
|
||||
'agenda_start_title',
|
||||
'agenda_end_title',
|
||||
'agenda_announcement_position',
|
||||
'agenda_sermon_matching',
|
||||
];
|
||||
|
||||
public function index(): Response
|
||||
|
|
|
|||
85
tests/Feature/SettingsControllerAgendaKeysTest.php
Normal file
85
tests/Feature/SettingsControllerAgendaKeysTest.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
});
|
||||
|
||||
test('settings index includes all agenda keys in props', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->withoutVite()
|
||||
->get(route('settings.index'));
|
||||
|
||||
$response->assertInertia(
|
||||
fn ($page) => $page
|
||||
->component('Settings')
|
||||
->has('settings.agenda_start_title')
|
||||
->has('settings.agenda_end_title')
|
||||
->has('settings.agenda_announcement_position')
|
||||
->has('settings.agenda_sermon_matching')
|
||||
);
|
||||
});
|
||||
|
||||
test('patch agenda_start_title setting returns 200', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->patchJson(route('settings.update'), [
|
||||
'key' => 'agenda_start_title',
|
||||
'value' => 'Test Title',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
expect(Setting::get('agenda_start_title'))->toBe('Test Title');
|
||||
});
|
||||
|
||||
test('patch agenda_end_title setting returns 200', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->patchJson(route('settings.update'), [
|
||||
'key' => 'agenda_end_title',
|
||||
'value' => 'End Title',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
expect(Setting::get('agenda_end_title'))->toBe('End Title');
|
||||
});
|
||||
|
||||
test('patch agenda_announcement_position setting returns 200', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->patchJson(route('settings.update'), [
|
||||
'key' => 'agenda_announcement_position',
|
||||
'value' => 'pattern1,pattern2',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
expect(Setting::get('agenda_announcement_position'))->toBe('pattern1,pattern2');
|
||||
});
|
||||
|
||||
test('patch agenda_sermon_matching setting returns 200', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->patchJson(route('settings.update'), [
|
||||
'key' => 'agenda_sermon_matching',
|
||||
'value' => 'sermon,predigt',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJson(['success' => true]);
|
||||
|
||||
expect(Setting::get('agenda_sermon_matching'))->toBe('sermon,predigt');
|
||||
});
|
||||
|
||||
test('patch with unknown key returns 422', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->patchJson(route('settings.update'), [
|
||||
'key' => 'unknown_setting_key',
|
||||
'value' => 'some value',
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable();
|
||||
});
|
||||
Loading…
Reference in a new issue