T20: Song DB Page - Songs/Index.vue with search, action buttons, pagination - Upload area for .pro files (calls T23 placeholder) - Song-Datenbank nav link added to AuthenticatedLayout - Tests: 9 new (44 assertions) T21: Song DB Edit Popup - SongEditModal.vue with metadata + ArrangementConfigurator - Auto-save with fetch (500ms debounce for text, immediate on blur) - Tests: 11 new (53 assertions) T22: Song DB Translate Page - Songs/Translate.vue with two-column editor - URL fetch or manual paste, line-count constraints - Group headers with colors, save marks has_translation=true - Tests: 1 new (12 assertions) T23: .pro File Placeholders - ProParserNotImplementedException with HTTP 501 - ProFileController with importPro/downloadPro placeholders - German error messages - Tests: 5 new (7 assertions) T24: Service Finalization + Status - Two-step finalization with warnings (unmatched songs, missing slides) - Download placeholder toast - isReadyToFinalize accessor on Service model - Tests: 11 new (30 assertions) All tests passing: 174/174 (905 assertions) Build: ✓ Vite production build successful German UI: All user-facing text in German with 'Du' form
220 lines
8.5 KiB
PHP
220 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Service;
|
|
use App\Models\Song;
|
|
use App\Models\Slide;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Support\Carbon;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class ServiceController extends Controller
|
|
{
|
|
public function index(): Response
|
|
{
|
|
$services = Service::query()
|
|
->whereDate('date', '>=', Carbon::today())
|
|
->orderBy('date')
|
|
->withCount([
|
|
'serviceSongs as songs_total_count',
|
|
'serviceSongs as songs_mapped_count' => fn ($query) => $query->whereNotNull('song_id'),
|
|
'serviceSongs as songs_arranged_count' => fn ($query) => $query->whereNotNull('song_arrangement_id'),
|
|
])
|
|
->addSelect([
|
|
'has_sermon_slides' => Slide::query()
|
|
->selectRaw('CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END')
|
|
->whereColumn('slides.service_id', 'services.id')
|
|
->where('slides.type', 'sermon'),
|
|
'info_slides_count' => Slide::query()
|
|
->selectRaw('COUNT(*)')
|
|
->where('slides.type', 'information')
|
|
->where(function ($query) {
|
|
$query
|
|
->whereNull('slides.service_id')
|
|
->orWhereColumn('slides.service_id', 'services.id');
|
|
})
|
|
->whereNotNull('slides.expire_date')
|
|
->whereColumn('slides.expire_date', '>=', 'services.date'),
|
|
])
|
|
->get()
|
|
->map(fn (Service $service) => [
|
|
'id' => $service->id,
|
|
'title' => $service->title,
|
|
'date' => $service->date?->toDateString(),
|
|
'preacher_name' => $service->preacher_name,
|
|
'beamer_tech_name' => $service->beamer_tech_name,
|
|
'last_synced_at' => $service->last_synced_at?->toJSON(),
|
|
'updated_at' => $service->updated_at?->toJSON(),
|
|
'finalized_at' => $service->finalized_at?->toJSON(),
|
|
'songs_total_count' => (int) $service->songs_total_count,
|
|
'songs_mapped_count' => (int) $service->songs_mapped_count,
|
|
'songs_arranged_count' => (int) $service->songs_arranged_count,
|
|
'has_sermon_slides' => (bool) $service->has_sermon_slides,
|
|
'info_slides_count' => (int) $service->info_slides_count,
|
|
])
|
|
->values();
|
|
|
|
return Inertia::render('Services/Index', [
|
|
'services' => $services,
|
|
]);
|
|
}
|
|
|
|
public function edit(Service $service): Response
|
|
{
|
|
$service->load([
|
|
'serviceSongs' => fn ($query) => $query->orderBy('order'),
|
|
'serviceSongs.song',
|
|
'serviceSongs.song.groups',
|
|
'serviceSongs.song.arrangements.arrangementGroups.group',
|
|
'serviceSongs.arrangement',
|
|
'slides',
|
|
]);
|
|
|
|
$songsCatalog = Song::query()
|
|
->orderBy('title')
|
|
->get(['id', 'title', 'ccli_id', 'has_translation'])
|
|
->map(fn (Song $song) => [
|
|
'id' => $song->id,
|
|
'title' => $song->title,
|
|
'ccli_id' => $song->ccli_id,
|
|
'has_translation' => $song->has_translation,
|
|
])
|
|
->values();
|
|
|
|
$informationSlides = Slide::query()
|
|
->where('type', 'information')
|
|
->whereNull('deleted_at')
|
|
->where(function ($query) use ($service) {
|
|
$query
|
|
->whereNull('service_id')
|
|
->orWhere('service_id', $service->id);
|
|
})
|
|
->when(
|
|
$service->date,
|
|
fn ($query) => $query
|
|
->whereNotNull('expire_date')
|
|
->whereDate('expire_date', '>=', $service->date)
|
|
)
|
|
->orderByDesc('uploaded_at')
|
|
->get();
|
|
|
|
$moderationSlides = $service->slides
|
|
->where('type', 'moderation')
|
|
->sortByDesc('uploaded_at')
|
|
->values();
|
|
|
|
$sermonSlides = $service->slides
|
|
->where('type', 'sermon')
|
|
->sortByDesc('uploaded_at')
|
|
->values();
|
|
|
|
return Inertia::render('Services/Edit', [
|
|
'service' => [
|
|
'id' => $service->id,
|
|
'title' => $service->title,
|
|
'date' => $service->date?->toDateString(),
|
|
'preacher_name' => $service->preacher_name,
|
|
'beamer_tech_name' => $service->beamer_tech_name,
|
|
'finalized_at' => $service->finalized_at?->toJSON(),
|
|
'last_synced_at' => $service->last_synced_at?->toJSON(),
|
|
],
|
|
'serviceSongs' => $service->serviceSongs->map(fn ($ss) => [
|
|
'id' => $ss->id,
|
|
'order' => $ss->order,
|
|
'cts_song_name' => $ss->cts_song_name,
|
|
'cts_ccli_id' => $ss->cts_ccli_id,
|
|
'use_translation' => $ss->use_translation,
|
|
'song_id' => $ss->song_id,
|
|
'song_arrangement_id' => $ss->song_arrangement_id,
|
|
'matched_at' => $ss->matched_at?->toJSON(),
|
|
'request_sent_at' => $ss->request_sent_at?->toJSON(),
|
|
'song' => $ss->song ? [
|
|
'id' => $ss->song->id,
|
|
'title' => $ss->song->title,
|
|
'ccli_id' => $ss->song->ccli_id,
|
|
'has_translation' => $ss->song->has_translation,
|
|
'groups' => $ss->song->groups
|
|
->sortBy('order')
|
|
->values()
|
|
->map(fn ($group) => [
|
|
'id' => $group->id,
|
|
'name' => $group->name,
|
|
'color' => $group->color,
|
|
'order' => $group->order,
|
|
]),
|
|
'arrangements' => $ss->song->arrangements
|
|
->sortBy(fn ($arrangement) => $arrangement->is_default ? 0 : 1)
|
|
->values()
|
|
->map(fn ($arrangement) => [
|
|
'id' => $arrangement->id,
|
|
'name' => $arrangement->name,
|
|
'is_default' => $arrangement->is_default,
|
|
'groups' => $arrangement->arrangementGroups
|
|
->sortBy('order')
|
|
->values()
|
|
->map(fn ($arrangementGroup) => [
|
|
'id' => $arrangementGroup->group?->id,
|
|
'name' => $arrangementGroup->group?->name,
|
|
'color' => $arrangementGroup->group?->color,
|
|
])
|
|
->filter(fn ($group) => $group['id'] !== null)
|
|
->values(),
|
|
]),
|
|
] : null,
|
|
'arrangement' => $ss->arrangement ? [
|
|
'id' => $ss->arrangement->id,
|
|
'name' => $ss->arrangement->name,
|
|
] : null,
|
|
]),
|
|
'informationSlides' => $informationSlides,
|
|
'moderationSlides' => $moderationSlides,
|
|
'sermonSlides' => $sermonSlides,
|
|
'songsCatalog' => $songsCatalog,
|
|
]);
|
|
}
|
|
|
|
public function finalize(Service $service): JsonResponse
|
|
{
|
|
$status = $service->finalizationStatus();
|
|
|
|
$confirmed = request()->boolean('confirmed');
|
|
|
|
if (! $status['ready'] && ! $confirmed) {
|
|
return response()->json([
|
|
'needs_confirmation' => true,
|
|
'warnings' => $status['warnings'],
|
|
]);
|
|
}
|
|
|
|
$service->update([
|
|
'finalized_at' => now(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'needs_confirmation' => false,
|
|
'success' => 'Service wurde abgeschlossen.',
|
|
]);
|
|
}
|
|
|
|
public function reopen(Service $service): RedirectResponse
|
|
{
|
|
$service->update([
|
|
'finalized_at' => null,
|
|
]);
|
|
|
|
return redirect()
|
|
->route('services.index')
|
|
->with('success', 'Service wurde wieder geoeffnet.');
|
|
}
|
|
|
|
public function download(Service $service): JsonResponse
|
|
{
|
|
return response()->json([
|
|
'message' => 'Die Show-Erstellung wird in einem zukünftigen Update verfügbar sein.',
|
|
]);
|
|
}
|
|
}
|