Resolves a batch of bugs and feature requests across songs, services, settings and export: Songs & sections - Every song now carries permanent, empty, locked PREFIX (COPYRIGHT) and POSTFIX (BLANK) sections, deduplicated on import; locked sections cannot be edited or deleted via UI or API. - Song edit modal: explicit Speichern/Schließen with dirty-tracking, editable section headline (combobox + custom values), and a fix for the 419 CSRF errors after CCLI "Importieren & Bearbeiten" (token read fresh per request). - CCLI bookmarklet "Importieren & Bearbeiten" now opens the edit dialog. Service schedule & arrangements - Fixed assigned songs showing no sections (slides loaded for all arrangements, not just the default). - Added "Song entfernen / neu zuordnen" to reassign an assigned song. - Worship-leader arrangement is created/selected lazily when the arrangement dialog opens (only when not user-overridden); the leader is resolved from the "Lobpreis" agenda item, and manual create/clone names are prefixed with the leader name. Navigation - "/" redirects to the next upcoming service's edit page (or the list). - Service titles link to the edit page. Settings - Renamed "Makro-Import"/"Label-Import" menu items; fixed drag-and-drop imports (were downloading the dropped file); added label-import hint; made the panel scrollable. - Nametag now uses a single MacroPicker; added song prefix/postfix label defaults (COPYRIGHT #24B34C / BLANK #000000); new "Export-Dateien" menu to upload prefix/postfix .pro files added to every export. Export - Filenames/playlist names are date-first ("YYYY-MM-DD <Title>"). - Keyvisual slide only for the first content-less item after real content; all other content-less items render as headlines. - New "Vorschau herunterladen" for non-finalized services (filename and import name prefixed "Vorschau" with export timestamp). - Uploaded prefix/postfix .pro files wrap every export. Tests updated to the new behavior; full suite green (569 passed).
134 lines
4.1 KiB
PHP
134 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Label;
|
|
use App\Models\Setting;
|
|
use App\Models\Song;
|
|
use App\Models\SongArrangement;
|
|
use App\Models\SongArrangementSection;
|
|
use App\Models\SongSection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class SongPrefixPostfixService
|
|
{
|
|
public function ensure(Song $song): void
|
|
{
|
|
DB::transaction(function () use ($song): void {
|
|
$prefixLabelId = $this->resolvePrefixLabelId();
|
|
$postfixLabelId = $this->resolvePostfixLabelId();
|
|
|
|
$prefixSection = $this->ensureLockedSection($song, $prefixLabelId, 0);
|
|
$postfixSection = $this->ensureLockedSection($song, $postfixLabelId, PHP_INT_MAX);
|
|
|
|
$arrangement = $this->resolveDefaultArrangement($song);
|
|
|
|
$this->ensureSectionInArrangement($arrangement, $prefixSection, 'first');
|
|
$this->ensureSectionInArrangement($arrangement, $postfixSection, 'last');
|
|
});
|
|
}
|
|
|
|
private function resolvePrefixLabelId(): int
|
|
{
|
|
$id = Setting::get('song_prefix_label_id');
|
|
|
|
if ($id !== null && $id !== '') {
|
|
return (int) $id;
|
|
}
|
|
|
|
$label = Label::firstOrCreate(
|
|
['name' => 'COPYRIGHT'],
|
|
['color' => '#24B34C'],
|
|
);
|
|
|
|
Setting::set('song_prefix_label_id', (string) $label->id);
|
|
|
|
return $label->id;
|
|
}
|
|
|
|
private function resolvePostfixLabelId(): int
|
|
{
|
|
$id = Setting::get('song_postfix_label_id');
|
|
|
|
if ($id !== null && $id !== '') {
|
|
return (int) $id;
|
|
}
|
|
|
|
$label = Label::firstOrCreate(
|
|
['name' => 'BLANK'],
|
|
['color' => '#000000'],
|
|
);
|
|
|
|
Setting::set('song_postfix_label_id', (string) $label->id);
|
|
|
|
return $label->id;
|
|
}
|
|
|
|
private function ensureLockedSection(Song $song, int $labelId, int $orderHint): SongSection
|
|
{
|
|
$section = SongSection::firstOrCreate(
|
|
['song_id' => $song->id, 'label_id' => $labelId],
|
|
['order' => $orderHint, 'locked' => true],
|
|
);
|
|
|
|
$section->update(['locked' => true]);
|
|
$section->slides()->delete();
|
|
|
|
return $section;
|
|
}
|
|
|
|
private function resolveDefaultArrangement(Song $song): SongArrangement
|
|
{
|
|
return SongArrangement::firstOrCreate(
|
|
['song_id' => $song->id, 'name' => 'normal'],
|
|
['is_default' => true],
|
|
);
|
|
}
|
|
|
|
private function ensureSectionInArrangement(
|
|
SongArrangement $arrangement,
|
|
SongSection $section,
|
|
string $position,
|
|
): void {
|
|
$existing = SongArrangementSection::where('song_arrangement_id', $arrangement->id)
|
|
->where('song_section_id', $section->id)
|
|
->first();
|
|
|
|
if ($position === 'first') {
|
|
if ($existing === null) {
|
|
SongArrangementSection::where('song_arrangement_id', $arrangement->id)
|
|
->increment('order');
|
|
|
|
SongArrangementSection::create([
|
|
'song_arrangement_id' => $arrangement->id,
|
|
'song_section_id' => $section->id,
|
|
'order' => 0,
|
|
]);
|
|
} else {
|
|
if ($existing->order !== 0) {
|
|
SongArrangementSection::where('song_arrangement_id', $arrangement->id)
|
|
->where('id', '!=', $existing->id)
|
|
->where('order', '<', $existing->order)
|
|
->increment('order');
|
|
|
|
$existing->update(['order' => 0]);
|
|
}
|
|
}
|
|
} else {
|
|
$maxOrder = SongArrangementSection::where('song_arrangement_id', $arrangement->id)
|
|
->where('song_section_id', '!=', $section->id)
|
|
->max('order') ?? 0;
|
|
|
|
if ($existing === null) {
|
|
SongArrangementSection::create([
|
|
'song_arrangement_id' => $arrangement->id,
|
|
'song_section_id' => $section->id,
|
|
'order' => $maxOrder + 1,
|
|
]);
|
|
} else {
|
|
$existing->update(['order' => $maxOrder + 1]);
|
|
}
|
|
}
|
|
}
|
|
}
|