pp-planer/app/Http/Controllers/ServiceController.php
Thorsten Bus b2d230e991 feat: Wave 3 (partial) - Service Edit page + 4 blocks (T14-T18)
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
2026-03-01 20:09:47 +01:00

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.');
}
}