validate([ 'file' => ['required', 'file', 'max:51200'], '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, 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.']], ], 422); } /** @var UploadedFile $file */ $file = $request->file('file'); $extension = strtolower($file->getClientOriginalExtension()); // Validate file extension $allowedExtensions = [...self::IMAGE_EXTENSIONS, ...self::POWERPOINT_EXTENSIONS, 'zip']; if (! in_array($extension, $allowedExtensions, true)) { return response()->json([ 'message' => 'Dateityp nicht erlaubt.', 'errors' => ['file' => ['Nur PNG, JPG, PPT, PPTX und ZIP-Dateien sind erlaubt.']], ], 422); } $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, $serviceAgendaItemId); } // Handle ZIP files — extract and process if ($extension === 'zip') { return $this->handleZip($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate, $serviceAgendaItemId); } // Handle images — convert synchronously return $this->handleImage($file, $conversionService, $type, $serviceId, $uploaderName, $expireDate, $serviceAgendaItemId); } public function destroy(Slide $slide): JsonResponse { $slide->delete(); return response()->json([ 'success' => true, 'message' => 'Folie wurde gelöscht.', ]); } public function destroyBulk(Request $request): JsonResponse { $validated = $request->validate([ 'type' => ['required', Rule::in(['information', 'moderation', 'sermon', 'agenda_item'])], 'service_id' => ['nullable', 'exists:services,id'], ]); $query = Slide::where('type', $validated['type']); if ($validated['service_id']) { $query->where('service_id', $validated['service_id']); } else { // Information slides without service_id (global) $query->whereNull('service_id'); } $count = $query->count(); $query->delete(); // soft-delete via SoftDeletes trait return response()->json([ 'success' => true, 'message' => $count.' Folien wurden gelöscht.', 'count' => $count, ]); } public function reorder(Request $request): JsonResponse { $validated = $request->validate([ 'slides' => ['required', 'array', 'min:1'], 'slides.*.id' => ['required', 'integer', 'exists:slides,id'], 'slides.*.sort_order' => ['required', 'integer', 'min:0'], ]); foreach ($validated['slides'] as $item) { Slide::where('id', $item['id'])->update(['sort_order' => $item['sort_order']]); } return response()->json([ 'success' => true, 'message' => 'Reihenfolge wurde aktualisiert.', ]); } public function updateExpireDate(Request $request, Slide $slide): JsonResponse { if ($slide->type !== 'information') { return response()->json([ 'message' => 'Ablaufdatum kann nur für Informationsfolien gesetzt werden.', 'errors' => ['type' => ['Nur Informationsfolien haben ein Ablaufdatum.']], ], 422); } $validated = $request->validate([ 'expire_date' => ['nullable', 'date'], ]); $slide->update(['expire_date' => $validated['expire_date']]); return response()->json([ 'success' => true, 'slide' => $slide->fresh(), ]); } private function nextSortOrder(string $type, ?int $serviceId): int { return (int) Slide::where('type', $type) ->when($serviceId, fn ($q) => $q->where('service_id', $serviceId), fn ($q) => $q->whereNull('service_id')) ->max('sort_order') + 1; } private function handleImage( UploadedFile $file, FileConversionService $conversionService, string $type, ?int $serviceId, string $uploaderName, ?string $expireDate, ?int $serviceAgendaItemId = null, ): JsonResponse { try { $result = $conversionService->convertImage($file); $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'], 'expire_date' => $expireDate, 'uploader_name' => $uploaderName, 'uploaded_at' => now(), 'sort_order' => $this->nextSortOrder($type, $serviceId), ]); return response()->json([ 'success' => true, 'slide' => $slide, ]); } catch (InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 422); } } private function handlePowerPoint( UploadedFile $file, FileConversionService $conversionService, string $type, ?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'); try { $jobId = $conversionService->convertPowerPoint( storage_path('app/'.$storedPath) ); return response()->json([ 'success' => true, 'job_id' => $jobId, 'message' => 'PowerPoint wird verarbeitet...', 'meta' => [ 'type' => $type, 'service_id' => $serviceId, 'service_agenda_item_id' => $serviceAgendaItemId, 'uploader_name' => $uploaderName, 'expire_date' => $expireDate, 'original_filename' => $file->getClientOriginalName(), ], ]); } catch (InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 422); } } private function handleZip( UploadedFile $file, FileConversionService $conversionService, string $type, ?int $serviceId, string $uploaderName, ?string $expireDate, ?int $serviceAgendaItemId = null, ): JsonResponse { try { $results = $conversionService->processZip($file); $slides = []; $sortOrder = $this->nextSortOrder($type, $serviceId); foreach ($results as $result) { // Skip PPT job results (they are handled asynchronously) if (isset($result['job_id'])) { continue; } $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'], 'expire_date' => $expireDate, 'uploader_name' => $uploaderName, 'uploaded_at' => now(), 'sort_order' => $sortOrder++, ]); } return response()->json([ 'success' => true, 'slides' => $slides, 'count' => count($slides), ]); } catch (InvalidArgumentException $e) { Log::warning('ZIP-Verarbeitung fehlgeschlagen', ['error' => $e->getMessage()]); return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 422); } } }