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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
<?php <?php
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceAgendaItem;
use App\Models\Slide; use App\Models\Slide;
use App\Models\User; use App\Models\User;
use App\Services\FileConversionService; use App\Services\FileConversionService;
@ -149,7 +150,7 @@
// Build zip // Build zip
$zipPath = $tempDir.'/slides.zip'; $zipPath = $tempDir.'/slides.zip';
$zip = new ZipArchive; $zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE); $zip->open($zipPath, ZipArchive::CREATE);
$zip->addFile($imgPath, 'slide1.png'); $zip->addFile($imgPath, 'slide1.png');
$zip->close(); $zip->close();
@ -171,6 +172,51 @@
@rmdir($tempDir); @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 () { test('unauthenticated user cannot upload slides', function () {
auth()->logout(); auth()->logout();
$file = makePngUploadForSlide('test.png', 400, 300); $file = makePngUploadForSlide('test.png', 400, 300);