feat(ui): add slide upload on agenda items

This commit is contained in:
Thorsten Bus 2026-03-29 12:12:58 +02:00
parent f78d20fc59
commit 45955b70a2
4 changed files with 73 additions and 10 deletions

View file

@ -21,13 +21,14 @@ public function store(Request $request, FileConversionService $conversionService
{
$validated = $request->validate([
'file' => ['required', 'file', 'max:51200'],
'type' => ['required', Rule::in(['information', 'moderation', 'sermon'])],
'type' => ['required', Rule::in(['information', 'moderation', 'sermon', 'agenda_item'])],
'service_id' => ['nullable', 'exists:services,id'],
'service_agenda_item_id' => ['nullable', 'integer', 'exists:service_agenda_items,id'],
'expire_date' => ['nullable', 'date'],
]);
// moderation and sermon slides require a service_id
if (in_array($validated['type'], ['moderation', 'sermon']) && empty($validated['service_id'])) {
// moderation, sermon, and agenda_item slides require a service_id
if (in_array($validated['type'], ['moderation', 'sermon', 'agenda_item']) && empty($validated['service_id'])) {
return response()->json([
'message' => 'Moderations- und Predigtfolien benötigen einen Gottesdienst.',
'errors' => ['service_id' => ['Gottesdienst ist erforderlich.']],
@ -49,21 +50,22 @@ public function store(Request $request, FileConversionService $conversionService
$uploaderName = $request->user()?->name ?? 'Unbekannt';
$serviceId = $validated['service_id'] ?? null;
$serviceAgendaItemId = $validated['service_agenda_item_id'] ?? null;
$type = $validated['type'];
$expireDate = $validated['expire_date'] ?? null;
// Handle PowerPoint files — dispatch async job
if (in_array($extension, self::POWERPOINT_EXTENSIONS, true)) {
return $this->handlePowerPoint($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate);
return $this->handlePowerPoint($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate, $serviceAgendaItemId);
}
// Handle ZIP files — extract and process
if ($extension === 'zip') {
return $this->handleZip($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate);
return $this->handleZip($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate, $serviceAgendaItemId);
}
// Handle images — convert synchronously
return $this->handleImage($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate);
return $this->handleImage($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate, $serviceAgendaItemId);
}
public function destroy(Slide $slide): JsonResponse
@ -79,7 +81,7 @@ public function destroy(Slide $slide): JsonResponse
public function destroyBulk(Request $request): JsonResponse
{
$validated = $request->validate([
'type' => ['required', Rule::in(['information', 'moderation', 'sermon'])],
'type' => ['required', Rule::in(['information', 'moderation', 'sermon', 'agenda_item'])],
'service_id' => ['nullable', 'exists:services,id'],
]);
@ -155,6 +157,7 @@ private function handleImage(
?int $serviceId,
string $uploaderName,
?string $expireDate,
?int $serviceAgendaItemId = null,
): JsonResponse {
try {
$result = $conversionService->convertImage($file);
@ -162,6 +165,7 @@ private function handleImage(
$slide = Slide::create([
'type' => $type,
'service_id' => $serviceId,
'service_agenda_item_id' => $serviceAgendaItemId,
'original_filename' => $file->getClientOriginalName(),
'stored_filename' => $result['filename'],
'thumbnail_filename' => $result['thumbnail'],
@ -190,6 +194,7 @@ private function handlePowerPoint(
?int $serviceId,
string $uploaderName,
?string $expireDate,
?int $serviceAgendaItemId = null,
): JsonResponse {
// Store file persistently so the job can access it
$storedPath = $file->store('temp/ppt', 'local');
@ -206,6 +211,7 @@ private function handlePowerPoint(
'meta' => [
'type' => $type,
'service_id' => $serviceId,
'service_agenda_item_id' => $serviceAgendaItemId,
'uploader_name' => $uploaderName,
'expire_date' => $expireDate,
'original_filename' => $file->getClientOriginalName(),
@ -226,6 +232,7 @@ private function handleZip(
?int $serviceId,
string $uploaderName,
?string $expireDate,
?int $serviceAgendaItemId = null,
): JsonResponse {
try {
$results = $conversionService->processZip($file);
@ -242,6 +249,7 @@ private function handleZip(
$slides[] = Slide::create([
'type' => $type,
'service_id' => $serviceId,
'service_agenda_item_id' => $serviceAgendaItemId,
'original_filename' => $file->getClientOriginalName(),
'stored_filename' => $result['filename'],
'thumbnail_filename' => $result['thumbnail'],

View file

@ -92,8 +92,9 @@ function onUploaded() {
<!-- Folien-Upload (umschaltbar) -->
<div v-if="showUploader" class="mt-3 border-t pt-3">
<SlideUploader
type="sermon"
type="agenda_item"
:service-id="serviceId"
:agenda-item-id="agendaItem.id"
:show-expire-date="false"
:inline="true"
@uploaded="onUploaded"

View file

@ -9,12 +9,16 @@ const props = defineProps({
type: {
type: String,
required: true,
validator: (v) => ['information', 'moderation', 'sermon'].includes(v),
validator: (v) => ['information', 'moderation', 'sermon', 'agenda_item'].includes(v),
},
serviceId: {
type: [Number, null],
default: null,
},
agendaItemId: {
type: Number,
default: null,
},
showExpireDate: {
type: Boolean,
default: false,
@ -107,6 +111,10 @@ function uploadNextFile(index) {
formData.append('service_id', props.serviceId)
}
if (props.agendaItemId) {
formData.append('service_agenda_item_id', props.agendaItemId)
}
if (props.showExpireDate && expireDate.value) {
formData.append('expire_date', expireDate.value)
}

View file

@ -1,6 +1,7 @@
<?php
use App\Models\Service;
use App\Models\ServiceAgendaItem;
use App\Models\Slide;
use App\Models\User;
use App\Services\FileConversionService;
@ -149,7 +150,7 @@
// Build zip
$zipPath = $tempDir.'/slides.zip';
$zip = new ZipArchive;
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE);
$zip->addFile($imgPath, 'slide1.png');
$zip->close();
@ -171,6 +172,51 @@
@rmdir($tempDir);
});
test('upload agenda_item slide with service_agenda_item_id links to agenda item', function () {
$service = Service::factory()->create();
$agendaItem = ServiceAgendaItem::factory()->create(['service_id' => $service->id]);
$file = makePngUploadForSlide('agenda-slide.png', 800, 600);
$response = $this->post(route('slides.store'), [
'file' => $file,
'type' => 'agenda_item',
'service_id' => $service->id,
'service_agenda_item_id' => $agendaItem->id,
]);
$response->assertStatus(200);
$response->assertJson(['success' => true]);
$slide = Slide::first();
expect($slide)->not->toBeNull();
expect($slide->type)->toBe('agenda_item');
expect($slide->service_id)->toBe($service->id);
expect($slide->service_agenda_item_id)->toBe($agendaItem->id);
});
test('upload agenda_item slide without service_id fails', function () {
$file = makePngUploadForSlide('agenda-slide.png', 400, 300);
$response = $this->post(route('slides.store'), [
'file' => $file,
'type' => 'agenda_item',
]);
$response->assertStatus(422);
});
test('agenda_item type is accepted in validation', function () {
$file = makePngUploadForSlide('test.png', 400, 300);
$response = $this->postJson(route('slides.store'), [
'file' => $file,
'type' => 'agenda_item',
'service_id' => Service::factory()->create()->id,
]);
$response->assertStatus(200);
});
test('unauthenticated user cannot upload slides', function () {
auth()->logout();
$file = makePngUploadForSlide('test.png', 400, 300);