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).
152 lines
4.5 KiB
PHP
152 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Service;
|
|
use App\Models\ServiceAgendaItem;
|
|
use App\Models\Setting;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
|
|
class NameTagResolver
|
|
{
|
|
public function __construct(
|
|
private readonly AgendaMatcherService $agendaMatcherService,
|
|
) {}
|
|
|
|
public function moderatorFor(Service $service): ?string
|
|
{
|
|
$override = $this->filledString($service->moderator_name);
|
|
if ($override !== null) {
|
|
return $override;
|
|
}
|
|
|
|
$firstAgendaItem = $service->agendaItems()
|
|
->where('is_before_event', false)
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
return $firstAgendaItem ? $this->namesFromResponsible($firstAgendaItem->responsible) : null;
|
|
}
|
|
|
|
public function worshipLeaderFor(Service $service): ?string
|
|
{
|
|
$worshipItem = $service->agendaItems()
|
|
->where('is_before_event', false)
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->get()
|
|
->first(fn (ServiceAgendaItem $item) => $this->isWorshipItem($item));
|
|
|
|
return $worshipItem ? $this->namesFromResponsible($worshipItem->responsible) : null;
|
|
}
|
|
|
|
public function preacherFor(Service $service): ?string
|
|
{
|
|
$override = $this->filledString($service->preacher_name_override);
|
|
if ($override !== null) {
|
|
return $override;
|
|
}
|
|
|
|
$preacherName = $this->filledString($service->preacher_name);
|
|
if ($preacherName !== null) {
|
|
return $preacherName;
|
|
}
|
|
|
|
$sermonItem = $service->agendaItems()
|
|
->where('is_before_event', false)
|
|
->whereNull('service_song_id')
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->get()
|
|
->first(fn (ServiceAgendaItem $item) => $this->isSermonItem($item));
|
|
|
|
return $sermonItem ? $this->namesFromResponsible($sermonItem->responsible) : null;
|
|
}
|
|
|
|
private function filledString(?string $value): ?string
|
|
{
|
|
$trimmed = trim((string) $value);
|
|
|
|
return $trimmed === '' ? null : $trimmed;
|
|
}
|
|
|
|
private function namesFromResponsible(mixed $responsible): ?string
|
|
{
|
|
if (! is_array($responsible) || $responsible === []) {
|
|
return null;
|
|
}
|
|
|
|
$people = Arr::isAssoc($responsible) ? [$responsible] : $responsible;
|
|
|
|
$names = collect($people)
|
|
->map(fn (mixed $person) => $this->nameFromResponsiblePerson($person))
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
return $names === [] ? null : implode(', ', $names);
|
|
}
|
|
|
|
private function nameFromResponsiblePerson(mixed $person): ?string
|
|
{
|
|
if (is_string($person)) {
|
|
return $this->filledString($person);
|
|
}
|
|
|
|
if (! is_array($person)) {
|
|
return null;
|
|
}
|
|
|
|
$name = $this->filledString($person['name'] ?? null);
|
|
if ($name !== null) {
|
|
return $name;
|
|
}
|
|
|
|
$firstName = $this->filledString($person['firstName'] ?? $person['first_name'] ?? null) ?? '';
|
|
$lastName = $this->filledString($person['lastName'] ?? $person['last_name'] ?? null) ?? '';
|
|
$fullName = trim($firstName.' '.$lastName);
|
|
|
|
return $fullName === '' ? null : $fullName;
|
|
}
|
|
|
|
private function isWorshipItem(ServiceAgendaItem $item): bool
|
|
{
|
|
$title = Str::lower($item->title);
|
|
$type = Str::lower($item->type ?? '');
|
|
|
|
return str_contains($title, 'lobpreis')
|
|
|| str_contains($type, 'lobpreis');
|
|
}
|
|
|
|
private function isSermonItem(ServiceAgendaItem $item): bool
|
|
{
|
|
$configuredPatterns = $this->patternsFromSetting(Setting::get('agenda_sermon_matching'));
|
|
if ($configuredPatterns !== []) {
|
|
return $this->agendaMatcherService->matchesAny($item->title, $configuredPatterns);
|
|
}
|
|
|
|
$title = Str::lower($item->title);
|
|
$type = Str::lower($item->type ?? '');
|
|
|
|
return str_contains($title, 'predigt')
|
|
|| str_contains($title, 'sermon')
|
|
|| str_contains($type, 'predigt')
|
|
|| str_contains($type, 'sermon');
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function patternsFromSetting(?string $patterns): array
|
|
{
|
|
if ($patterns === null || trim($patterns) === '') {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_filter(
|
|
array_map(fn (string $pattern) => trim($pattern), explode(',', $patterns)),
|
|
fn (string $pattern) => $pattern !== '',
|
|
));
|
|
}
|
|
}
|