T14: Service Edit Page Layout + Routing
- ServiceController::edit() with eager-loaded relationships
- Services/Edit.vue with 4 collapsible accordion blocks
- Route: GET /services/{service}/edit
- Information slides query: global + service-specific with expire_date filtering
- Tests: 2 new (edit page render + auth guard)
T15: Information Block (Slides + Expire Dates)
- InformationBlock.vue with dynamic expire_date filtering
- Shows slides where type='information' AND expire_date >= service.date
- Global visibility across services (not service-specific)
- SlideUploader with showExpireDate=true
- SlideGrid with prominent expire date + inline editing
- Badge showing slide count + 'expiring soon' warning (within 3 days)
- Tests: 7 new (105 assertions)
T16: Moderation Block (Service-Specific)
- ModerationBlock.vue (service-specific slides)
- Filters: type='moderation' AND service_id = current_service
- No expire date field (unlike Information block)
- Service isolation (slides from Service A don't appear in Service B)
- Tests: 5 new (14 assertions)
T17: Sermon Block (Service-Specific)
- SermonBlock.vue (identical to Moderation but type='sermon')
- Service-specific slides, no expire date
- Tests: 5 new (14 assertions)
T18: Songs Block (Matching + Arrangement + Translation)
- SongsBlock.vue with conditional UI (unmatched vs matched states)
- Unmatched: 'Erstellung anfragen' button + searchable select for manual assign
- Matched: ArrangementConfigurator + translation checkbox + preview/download buttons
- ServiceSongController::update() for use_translation and song_arrangement_id
- ArrangementConfigurator emits 'arrangement-selected' for auto-save
- ServiceController::edit() provides songsCatalog for matching search
- Tests: 2 new (45 assertions)
T19: Song PDF (INCOMPLETE - timeout)
- SongPdfController.php created (partial)
- resources/views/pdf/song.blade.php created (partial)
- SongPreviewModal.vue MISSING
- Tests MISSING
- Will be completed in next commit
All tests passing: 124/124 (703 assertions)
Build: ✓ Vite production build successful
German UI: All user-facing text in German with 'Du' form
Dependencies: barryvdh/laravel-dompdf added for PDF generation
200 lines
7.9 KiB
PHP
200 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Service;
|
|
use App\Models\Song;
|
|
use App\Models\Slide;
|
|
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): RedirectResponse
|
|
{
|
|
$service->update([
|
|
'finalized_at' => now(),
|
|
]);
|
|
|
|
return redirect()
|
|
->route('services.index')
|
|
->with('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.');
|
|
}
|
|
}
|