- Post-login (OAuth + dev-login) now redirects to the next upcoming service's edit page instead of /dashboard, mirroring the GET / route. - NameTagResolver now reads the real ChurchTools `responsible` shape (persons[].person.title) and resolves moderator/preacher/worship-leader by responsible ROLE ([Moderation]/[Predigt]/[Lobpreis]). This fixes missing name slides and makes the worship-leader arrangement trigger (e.g. service 12 → "Benedikt Hardt" / "Jennifer Schneider"). - NameTagSlideBuilder no longer silently drops the name slide when the configured macro id points to a missing macro; it emits the slide without a macro instead. - Song export: the "first slide" / "last slide" macro now applies only to the song's very first/last slide (global slide index across all sections), not the first slide of every section. - Export "headlines" for content-less agenda items are now emitted as proper ProPresenter playlist HEADER items instead of text presentations. - Prefix/postfix export files now also accept .probundle (unzipped: inner .pro + media embedded) in addition to .pro, both for upload validation and export injection. Full suite green (587 passed).
246 lines
7.8 KiB
PHP
246 lines
7.8 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;
|
|
}
|
|
|
|
// Prefer the person whose responsible role is "Moderation".
|
|
$byRole = $this->nameForRole($service, 'moderation');
|
|
if ($byRole !== null) {
|
|
return $byRole;
|
|
}
|
|
|
|
$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
|
|
{
|
|
return $this->nameForRole($service, 'lobpreis');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Prefer the person whose responsible role is "Predigt".
|
|
$byRole = $this->nameForRole($service, 'predigt');
|
|
if ($byRole !== null) {
|
|
return $byRole;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Find the first visible agenda item carrying a responsible person whose
|
|
* role (persons[].service, e.g. "[Lobpreis]") matches $roleNeedle, and
|
|
* return that person's display name.
|
|
*/
|
|
private function nameForRole(Service $service, string $roleNeedle): ?string
|
|
{
|
|
$items = $service->agendaItems()
|
|
->where('is_before_event', false)
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->get();
|
|
|
|
foreach ($items as $item) {
|
|
$name = $this->nameFromResponsibleByRole($item->responsible, $roleNeedle);
|
|
if ($name !== null) {
|
|
return $name;
|
|
}
|
|
}
|
|
|
|
return 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;
|
|
}
|
|
|
|
// Real CTS shape: { "text": "...", "persons": [ { "service": "[Lobpreis]", "person": { "title": "...", ... } } ] }
|
|
if (array_key_exists('persons', $responsible)) {
|
|
$entries = $responsible['persons'];
|
|
} elseif (Arr::isAssoc($responsible)) {
|
|
$entries = [$responsible];
|
|
} else {
|
|
$entries = $responsible;
|
|
}
|
|
|
|
$names = collect($entries)
|
|
->map(fn (mixed $entry) => $this->nameFromResponsibleEntry($entry))
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
return $names === [] ? null : implode(', ', $names);
|
|
}
|
|
|
|
/**
|
|
* Extract a person name from a responsible entry.
|
|
* Handles both the real CTS shape (entry has a 'person' sub-key) and the
|
|
* legacy/test shape (entry is a flat array with 'name'/'firstName'/'lastName' or a string).
|
|
*/
|
|
private function nameFromResponsibleEntry(mixed $entry): ?string
|
|
{
|
|
if (is_string($entry)) {
|
|
return $this->filledString($entry);
|
|
}
|
|
|
|
if (! is_array($entry)) {
|
|
return null;
|
|
}
|
|
|
|
// Real CTS shape: { "service": "[Lobpreis]", "person": { "title": "Kornelius Weiß", ... } }
|
|
if (isset($entry['person']) && is_array($entry['person'])) {
|
|
return $this->nameFromPersonNode($entry['person']);
|
|
}
|
|
|
|
// Legacy / test shape: { "name": "Anna Müller" } or { "firstName": "...", "lastName": "..." }
|
|
return $this->nameFromPersonNode($entry);
|
|
}
|
|
|
|
/** Extract display name from a person node (title preferred, then firstName+lastName). */
|
|
private function nameFromPersonNode(mixed $node): ?string
|
|
{
|
|
if (! is_array($node)) {
|
|
return null;
|
|
}
|
|
|
|
$title = $this->filledString($node['title'] ?? null);
|
|
if ($title !== null) {
|
|
return $title;
|
|
}
|
|
|
|
$name = $this->filledString($node['name'] ?? null);
|
|
if ($name !== null) {
|
|
return $name;
|
|
}
|
|
|
|
$domainAttrs = $node['domainAttributes'] ?? [];
|
|
$firstName = $this->filledString(
|
|
$node['firstName'] ?? $node['first_name'] ?? ($domainAttrs['firstName'] ?? null)
|
|
) ?? '';
|
|
$lastName = $this->filledString(
|
|
$node['lastName'] ?? $node['last_name'] ?? ($domainAttrs['lastName'] ?? null)
|
|
) ?? '';
|
|
$fullName = trim($firstName.' '.$lastName);
|
|
|
|
return $fullName === '' ? null : $fullName;
|
|
}
|
|
|
|
/**
|
|
* Scan a responsible array for a person whose role (persons[].service)
|
|
* contains $roleNeedle (e.g. 'lobpreis', 'predigt', 'moderation').
|
|
* Returns the person's display name, or null if none found.
|
|
*/
|
|
private function nameFromResponsibleByRole(mixed $responsible, string $roleNeedle): ?string
|
|
{
|
|
if (! is_array($responsible) || $responsible === []) {
|
|
return null;
|
|
}
|
|
|
|
// Real CTS shape: { "persons": [ { "service": "[Lobpreis]", "person": { ... } } ] }
|
|
$entries = array_key_exists('persons', $responsible) ? $responsible['persons'] : [];
|
|
|
|
foreach ($entries as $entry) {
|
|
if (! is_array($entry)) {
|
|
continue;
|
|
}
|
|
|
|
$role = $this->normalizeRole($entry['service'] ?? '');
|
|
if ($role !== '' && str_contains($role, $roleNeedle)) {
|
|
$name = $this->nameFromResponsibleEntry($entry);
|
|
if ($name !== null) {
|
|
return $name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Strip brackets, whitespace, and lowercase a role string for comparison. */
|
|
private function normalizeRole(string $role): string
|
|
{
|
|
return Str::lower(preg_replace('/[^a-zA-ZäöüÄÖÜß]/', '', $role) ?? '');
|
|
}
|
|
|
|
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 !== '',
|
|
));
|
|
}
|
|
}
|