diff --git a/app/Http/Controllers/ArrangementController.php b/app/Http/Controllers/ArrangementController.php index 3de74b1..e449876 100644 --- a/app/Http/Controllers/ArrangementController.php +++ b/app/Http/Controllers/ArrangementController.php @@ -23,19 +23,18 @@ public function store(Request $request, Song $song): RedirectResponse 'is_default' => false, ]); - $source = $song->arrangements() - ->where('is_default', true) - ->with('arrangementGroups') - ->first(); + $groups = $song->groups()->orderBy('order')->get(); + $rows = $groups->map(fn ($group, $index) => [ + 'song_arrangement_id' => $arrangement->id, + 'song_group_id' => $group->id, + 'order' => $index + 1, + 'created_at' => now(), + 'updated_at' => now(), + ])->all(); - if ($source === null) { - $source = $song->arrangements() - ->with('arrangementGroups') - ->orderBy('id') - ->first(); + if ($rows !== []) { + $arrangement->arrangementGroups()->insert($rows); } - - $this->cloneGroups($source, $arrangement); }); return back()->with('success', 'Arrangement wurde hinzugefügt.'); diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index 9c927f4..25a68a9 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -111,6 +111,7 @@ public function edit(Service $service): Response $q->whereNull('expire_date') ->orWhereDate('expire_date', '>=', $service->date); }) + ->whereDate('uploaded_at', '<=', $service->date) ) ->orderByDesc('uploaded_at') ->get(); diff --git a/app/Models/ServiceSong.php b/app/Models/ServiceSong.php index 2f485ab..a60b441 100644 --- a/app/Models/ServiceSong.php +++ b/app/Models/ServiceSong.php @@ -18,6 +18,7 @@ class ServiceSong extends Model 'order', 'cts_song_name', 'cts_ccli_id', + 'cts_song_id', 'matched_at', 'request_sent_at', ]; diff --git a/app/Models/Song.php b/app/Models/Song.php index 175405e..bd0d33b 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -15,6 +15,7 @@ class Song extends Model protected $fillable = [ 'ccli_id', + 'cts_song_id', 'title', 'author', 'copyright_text', diff --git a/app/Services/ChurchToolsService.php b/app/Services/ChurchToolsService.php index 948c113..f9f8469 100644 --- a/app/Services/ChurchToolsService.php +++ b/app/Services/ChurchToolsService.php @@ -108,20 +108,36 @@ public function syncSongs(): int foreach ($songs as $song) { $ccliId = $this->normalizeCcli($song->getCcli() ?? null); - if ($ccliId === null) { - continue; + $ctsSongId = (string) ($song->getId() ?? ''); + + if ($ccliId !== null) { + DB::table('songs')->updateOrInsert( + ['ccli_id' => $ccliId], + [ + 'cts_song_id' => $ctsSongId !== '' ? $ctsSongId : null, + 'title' => (string) ($song->getName() ?? ''), + 'updated_at' => Carbon::now(), + 'created_at' => Carbon::now(), + ] + ); + + $count++; } - DB::table('songs')->updateOrInsert( - ['ccli_id' => $ccliId], - [ - 'title' => (string) ($song->getName() ?? ''), - 'updated_at' => Carbon::now(), - 'created_at' => Carbon::now(), - ] - ); + if ($ctsSongId !== '' && $ccliId === null) { + $existing = DB::table('songs')->where('cts_song_id', $ctsSongId)->first(); - $count++; + if (! $existing) { + DB::table('songs')->insert([ + 'cts_song_id' => $ctsSongId, + 'title' => (string) ($song->getName() ?? ''), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]); + } + + $count++; + } } return $count; @@ -335,6 +351,7 @@ private function syncServiceAgendaSongs(int $serviceId, int $eventId): array [ 'cts_song_name' => (string) ($song->getName() ?? ''), 'cts_ccli_id' => $ccliId, + 'cts_song_id' => $ctsSongId, 'updated_at' => Carbon::now(), 'created_at' => Carbon::now(), ] @@ -346,7 +363,11 @@ private function syncServiceAgendaSongs(int $serviceId, int $eventId): array $matched = false; - if ($serviceSong !== null && $serviceSong->cts_ccli_id && ! $serviceSong->song_id) { + if ( + $serviceSong !== null + && ! $serviceSong->song_id + && ($serviceSong->cts_ccli_id || $serviceSong->cts_song_id) + ) { $matched = $songMatchingService->autoMatch($serviceSong); } elseif ($serviceSong !== null && $serviceSong->song_id) { $matched = true; diff --git a/app/Services/SongMatchingService.php b/app/Services/SongMatchingService.php index 6768030..8316084 100644 --- a/app/Services/SongMatchingService.php +++ b/app/Services/SongMatchingService.php @@ -11,23 +11,21 @@ class SongMatchingService { - /** - * Auto-match a service song to a song in the DB by CCLI ID. - * - * Returns true if a match was found and assigned, false otherwise. - * Skips if already matched or no CCLI ID present. - */ public function autoMatch(ServiceSong $serviceSong): bool { if ($serviceSong->song_id !== null) { return false; } - if ($serviceSong->cts_ccli_id === null || $serviceSong->cts_ccli_id === '') { - return false; + $song = null; + + if ($serviceSong->cts_ccli_id !== null && $serviceSong->cts_ccli_id !== '') { + $song = Song::where('ccli_id', $serviceSong->cts_ccli_id)->first(); } - $song = Song::where('ccli_id', $serviceSong->cts_ccli_id)->first(); + if ($song === null && $serviceSong->cts_song_id) { + $song = Song::where('cts_song_id', $serviceSong->cts_song_id)->first(); + } if ($song === null) { return false; @@ -36,6 +34,7 @@ public function autoMatch(ServiceSong $serviceSong): bool $serviceSong->update([ 'song_id' => $song->id, 'matched_at' => Carbon::now(), + 'use_translation' => $song->has_translation, ]); return true; @@ -50,6 +49,7 @@ public function manualAssign(ServiceSong $serviceSong, Song $song): void $serviceSong->update([ 'song_id' => $song->id, 'matched_at' => Carbon::now(), + 'use_translation' => $song->has_translation, ]); } diff --git a/database/migrations/2026_03_02_130249_add_cts_song_id_to_songs_and_service_songs_tables.php b/database/migrations/2026_03_02_130249_add_cts_song_id_to_songs_and_service_songs_tables.php new file mode 100644 index 0000000..3f4720c --- /dev/null +++ b/database/migrations/2026_03_02_130249_add_cts_song_id_to_songs_and_service_songs_tables.php @@ -0,0 +1,29 @@ +string('cts_song_id')->nullable()->index()->after('ccli_id'); + }); + + Schema::table('service_songs', function (Blueprint $table) { + $table->string('cts_song_id')->nullable()->after('cts_ccli_id'); + }); + } + + public function down(): void + { + Schema::table('service_songs', function (Blueprint $table) { + $table->dropColumn('cts_song_id'); + }); + + Schema::table('songs', function (Blueprint $table) { + $table->dropColumn('cts_song_id'); + }); + } +}; diff --git a/tests/Feature/ArrangementControllerTest.php b/tests/Feature/ArrangementControllerTest.php index 0439d09..7d4b066 100644 --- a/tests/Feature/ArrangementControllerTest.php +++ b/tests/Feature/ArrangementControllerTest.php @@ -15,7 +15,7 @@ class ArrangementControllerTest extends TestCase { use RefreshDatabase; - public function test_create_arrangement_clones_groups_from_default_arrangement(): void + public function test_create_arrangement_uses_default_song_group_ordering(): void { [$song, $normal] = $this->createSongWithDefaultArrangement(); $user = User::factory()->create(); @@ -34,10 +34,10 @@ public function test_create_arrangement_clones_groups_from_default_arrangement() $this->assertNotNull($newArrangement); - $normalGroups = SongArrangementGroup::query() - ->where('song_arrangement_id', $normal->id) + $defaultGroupOrder = SongGroup::query() + ->where('song_id', $song->id) ->orderBy('order') - ->pluck('song_group_id') + ->pluck('id') ->all(); $newGroups = SongArrangementGroup::query() @@ -46,7 +46,7 @@ public function test_create_arrangement_clones_groups_from_default_arrangement() ->pluck('song_group_id') ->all(); - $this->assertSame($normalGroups, $newGroups); + $this->assertSame($defaultGroupOrder, $newGroups); } public function test_clone_arrangement_duplicates_current_arrangement_groups(): void diff --git a/tests/Feature/InformationBlockTest.php b/tests/Feature/InformationBlockTest.php index 25b7ff6..74e182f 100644 --- a/tests/Feature/InformationBlockTest.php +++ b/tests/Feature/InformationBlockTest.php @@ -209,6 +209,35 @@ ); }); +test('information slides uploaded after service date are not shown', function () { + Carbon::setTestNow('2026-03-01 10:00:00'); + + $service = Service::factory()->create(['date' => '2026-03-10']); + + $visibleSlide = Slide::factory()->create([ + 'type' => 'information', + 'service_id' => null, + 'expire_date' => '2026-03-20', + 'uploaded_at' => '2026-03-09 10:00:00', + ]); + + Slide::factory()->create([ + 'type' => 'information', + 'service_id' => null, + 'expire_date' => '2026-03-20', + 'uploaded_at' => '2026-03-12 10:00:00', + ]); + + $response = $this->get(route('services.edit', $service)); + + $response->assertOk(); + $response->assertInertia( + fn ($page) => $page + ->has('informationSlides', 1) + ->where('informationSlides.0.id', $visibleSlide->id) + ); +}); + test('information slides ordered by uploaded_at descending', function () { Carbon::setTestNow('2026-03-01 10:00:00'); diff --git a/tests/Feature/SongMatchingTest.php b/tests/Feature/SongMatchingTest.php index 8bab693..b7d5389 100644 --- a/tests/Feature/SongMatchingTest.php +++ b/tests/Feature/SongMatchingTest.php @@ -15,10 +15,11 @@ */ test('autoMatch ordnet Song per CCLI-ID zu', function () { - $song = Song::factory()->create(['ccli_id' => '7115744']); + $song = Song::factory()->create(['ccli_id' => '7115744', 'has_translation' => true]); $serviceSong = ServiceSong::factory()->create([ 'cts_ccli_id' => '7115744', 'song_id' => null, + 'use_translation' => false, 'matched_at' => null, ]); @@ -29,6 +30,31 @@ $serviceSong->refresh(); expect($serviceSong->song_id)->toBe($song->id); expect($serviceSong->matched_at)->not->toBeNull(); + expect($serviceSong->use_translation)->toBeTrue(); +}); + +test('autoMatch nutzt CTS-Song-ID als Fallback wenn keine CCLI passt', function () { + $song = Song::factory()->create([ + 'ccli_id' => '7115744', + 'cts_song_id' => 'cts-123', + 'has_translation' => true, + ]); + + $serviceSong = ServiceSong::factory()->create([ + 'cts_ccli_id' => '0000000', + 'cts_song_id' => 'cts-123', + 'song_id' => null, + 'use_translation' => false, + 'matched_at' => null, + ]); + + $service = app(SongMatchingService::class); + $result = $service->autoMatch($serviceSong); + + expect($result)->toBeTrue(); + $serviceSong->refresh(); + expect($serviceSong->song_id)->toBe($song->id); + expect($serviceSong->use_translation)->toBeTrue(); }); test('autoMatch gibt false zurück wenn kein CCLI-ID vorhanden', function () { @@ -79,9 +105,10 @@ }); test('manualAssign ordnet Song manuell zu', function () { - $song = Song::factory()->create(); + $song = Song::factory()->create(['has_translation' => true]); $serviceSong = ServiceSong::factory()->create([ 'song_id' => null, + 'use_translation' => false, 'matched_at' => null, ]); @@ -91,6 +118,7 @@ $serviceSong->refresh(); expect($serviceSong->song_id)->toBe($song->id); expect($serviceSong->matched_at)->not->toBeNull(); + expect($serviceSong->use_translation)->toBeTrue(); }); test('manualAssign überschreibt bestehende Zuordnung', function () {