diff --git a/app/Http/Controllers/Api/Tenant/PhotoboothController.php b/app/Http/Controllers/Api/Tenant/PhotoboothController.php index 331f55f..68b2fdc 100644 --- a/app/Http/Controllers/Api/Tenant/PhotoboothController.php +++ b/app/Http/Controllers/Api/Tenant/PhotoboothController.php @@ -3,12 +3,16 @@ namespace App\Http\Controllers\Api\Tenant; use App\Http\Controllers\Controller; +use App\Http\Requests\Photobooth\PhotoboothSendUploaderDownloadRequest; use App\Http\Resources\Tenant\PhotoboothStatusResource; +use App\Mail\PhotoboothUploaderDownload; use App\Models\Event; use App\Models\PhotoboothSetting; use App\Services\Photobooth\PhotoboothProvisioner; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; +use Illuminate\Validation\ValidationException; class PhotoboothController extends Controller { @@ -69,6 +73,39 @@ class PhotoboothController extends Controller ]); } + public function sendUploaderDownloadEmail(PhotoboothSendUploaderDownloadRequest $request, Event $event): JsonResponse + { + $this->assertEventBelongsToTenant($request, $event); + + $user = $request->user(); + + if (! $user || ! $user->email) { + throw ValidationException::withMessages([ + 'email' => __('No email address is configured for this account.'), + ]); + } + + $locale = $user->preferred_locale ?: app()->getLocale(); + $eventName = $this->resolveEventName($event, $locale); + $recipientName = $user->fullName ?? $user->name ?? $user->email; + + Mail::to($user->email) + ->locale($locale) + ->queue(new PhotoboothUploaderDownload( + recipientName: $recipientName, + eventName: $eventName, + links: [ + 'windows' => url('/downloads/PhotoboothUploader-win-x64.exe'), + 'macos' => url('/downloads/PhotoboothUploader-macos-x64'), + 'linux' => url('/downloads/PhotoboothUploader-linux-x64'), + ], + )); + + return response()->json([ + 'message' => __('Download links sent via email.'), + ]); + } + protected function resource(Event $event): PhotoboothStatusResource { return PhotoboothStatusResource::make([ @@ -92,4 +129,30 @@ class PhotoboothController extends Controller return in_array($mode, ['sparkbooth', 'ftp'], true) ? $mode : 'ftp'; } + + protected function resolveEventName(Event $event, ?string $locale = null): string + { + $name = $event->name; + + if (is_string($name) && trim($name) !== '') { + return $name; + } + + if (is_array($name)) { + $locale = $locale ?: app()->getLocale(); + $localized = $name[$locale] ?? null; + + if (is_string($localized) && trim($localized) !== '') { + return $localized; + } + + foreach ($name as $value) { + if (is_string($value) && trim($value) !== '') { + return $value; + } + } + } + + return $event->slug ?: __('emails.photobooth_uploader.event_fallback'); + } } diff --git a/app/Http/Requests/Photobooth/PhotoboothSendUploaderDownloadRequest.php b/app/Http/Requests/Photobooth/PhotoboothSendUploaderDownloadRequest.php new file mode 100644 index 0000000..46bfac1 --- /dev/null +++ b/app/Http/Requests/Photobooth/PhotoboothSendUploaderDownloadRequest.php @@ -0,0 +1,18 @@ + $this->eventName, + ]), + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.photobooth-uploader-download', + with: [ + 'recipientName' => $this->recipientName, + 'eventName' => $this->eventName, + 'links' => $this->links, + ], + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/lang/de/emails.php b/lang/de/emails.php index 81da26c..847af77 100644 --- a/lang/de/emails.php +++ b/lang/de/emails.php @@ -65,6 +65,25 @@ return [ 'benefit4' => 'Unterstuetzung, wenn du sie brauchst', 'footer' => 'Brauchst du Hilfe? Antworte einfach auf diese E-Mail.', ], + 'photobooth_uploader' => [ + 'subject' => 'Fotospiel Uploader App fuer :event', + 'preheader' => 'Download-Links fuer die Fotospiel Photobooth Uploader App.', + 'hero_title' => 'Hallo :name,', + 'hero_subtitle' => 'Deine Uploader App fuer :event ist bereit.', + 'body' => 'Hier findest du die Download-Links fuer die Fotospiel Photobooth Uploader App. Installiere die passende Version auf dem Photobooth-PC, bevor dein Event startet.', + 'downloads_title' => 'Download-Links', + 'downloads' => [ + 'windows' => 'Windows (x64)', + 'macos' => 'macOS (x64)', + 'linux' => 'Linux (x64)', + ], + 'cta_windows' => 'Download fuer Windows', + 'cta_macos' => 'Download fuer macOS', + 'cta_linux' => 'Download fuer Linux', + 'credentials_hint' => 'Die Zugangsdaten bleiben im Admin-Dashboard. Erstelle einen Verbindungscode, sobald du die App koppeln moechtest.', + 'footer' => 'Fragen? Antworte einfach auf diese E-Mail.', + 'event_fallback' => 'dein Event', + ], 'package_limits' => [ 'package_fallback' => 'Paket', 'team_fallback' => 'dein Team', diff --git a/lang/en/emails.php b/lang/en/emails.php index 134ae1f..d403411 100644 --- a/lang/en/emails.php +++ b/lang/en/emails.php @@ -65,6 +65,25 @@ return [ 'benefit4' => 'Friendly support whenever you need help', 'footer' => 'Need help? Reply to this email.', ], + 'photobooth_uploader' => [ + 'subject' => 'Fotospiel Uploader App for :event', + 'preheader' => 'Download links for the Fotospiel Photobooth Uploader.', + 'hero_title' => 'Hi :name,', + 'hero_subtitle' => 'Your uploader app for :event is ready.', + 'body' => 'Here are the download links for the Fotospiel Photobooth Uploader. Install the right version on the photobooth PC before your event starts.', + 'downloads_title' => 'Download links', + 'downloads' => [ + 'windows' => 'Windows (x64)', + 'macos' => 'macOS (x64)', + 'linux' => 'Linux (x64)', + ], + 'cta_windows' => 'Download for Windows', + 'cta_macos' => 'Download for macOS', + 'cta_linux' => 'Download for Linux', + 'credentials_hint' => 'Connection credentials stay in the admin dashboard. Generate a connect code when you are ready to pair the app.', + 'footer' => 'Questions? Reply to this email and we will help.', + 'event_fallback' => 'your event', + ], 'package_limits' => [ 'package_fallback' => 'package', 'team_fallback' => 'your team', diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index e7090de..7e20d44 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -2068,6 +2068,13 @@ export async function createEventPhotoboothConnectCode( }; } +export async function sendEventPhotoboothUploaderEmail(slug: string): Promise { + const response = await authorizedFetch(`${photoboothEndpoint(slug)}/uploader-email`, { + method: 'POST', + }); + await jsonOrThrow<{ message?: string }>(response, 'Failed to send photobooth uploader email'); +} + export async function submitTenantFeedback(payload: { category: string; sentiment?: 'positive' | 'neutral' | 'negative'; diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index 56f4253..0ddaf7b 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -1211,6 +1211,9 @@ "uploaderDownload": { "title": "Fotospiel Uploader App", "description": "Die Fotospiel Uploader App wird benötigt, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.", + "emailAction": "Download-Links per E-Mail senden", + "emailSuccess": "Download-Links wurden per E-Mail gesendet.", + "emailFailed": "E-Mail konnte nicht gesendet werden.", "actionWindows": "Uploader herunterladen (Windows)", "actionMac": "Uploader herunterladen (macOS)", "actionLinux": "Uploader herunterladen (Linux)" diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index f483c95..675feb2 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -924,6 +924,9 @@ "uploaderDownload": { "title": "Fotospiel Uploader App", "description": "The Fotospiel Uploader App is required so uploads stay stable, credentials remain protected, and no files are lost.", + "emailAction": "Send download links by email", + "emailSuccess": "Download links were sent by email.", + "emailFailed": "Email could not be sent.", "actionWindows": "Download uploader (Windows)", "actionMac": "Download uploader (macOS)", "actionLinux": "Download uploader (Linux)" diff --git a/resources/js/admin/mobile/EventPhotoboothPage.tsx b/resources/js/admin/mobile/EventPhotoboothPage.tsx index a4c6318..5b9e3f8 100644 --- a/resources/js/admin/mobile/EventPhotoboothPage.tsx +++ b/resources/js/admin/mobile/EventPhotoboothPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3 } from 'lucide-react'; +import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3, Mail } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Switch } from '@tamagui/switch'; @@ -15,6 +15,7 @@ import { disableEventPhotobooth, rotateEventPhotobooth, createEventPhotoboothConnectCode, + sendEventPhotoboothUploaderEmail, PhotoboothStatus, TenantEvent, } from '../api'; @@ -41,6 +42,7 @@ export default function MobileEventPhotoboothPage() { const [connectCode, setConnectCode] = React.useState(null); const [connectExpiresAt, setConnectExpiresAt] = React.useState(null); const [connectLoading, setConnectLoading] = React.useState(false); + const [sendingEmail, setSendingEmail] = React.useState(false); const [showCredentials, setShowCredentials] = React.useState(false); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); @@ -138,6 +140,23 @@ export default function MobileEventPhotoboothPage() { } }; + const handleSendDownloadEmail = async () => { + if (!slug) return; + setSendingEmail(true); + try { + await sendEventPhotoboothUploaderEmail(slug); + toast.success(t('photobooth.uploaderDownload.emailSuccess', 'Download-Links wurden per E-Mail gesendet.')); + } catch (err) { + if (!isAuthError(err)) { + toast.error( + getApiErrorMessage(err, t('photobooth.uploaderDownload.emailFailed', 'E-Mail konnte nicht gesendet werden.')) + ); + } + } finally { + setSendingEmail(false); + } + }; + const spark = status?.sparkbooth ?? null; const metrics = spark?.metrics ?? null; const expiresAt = spark?.expires_at ?? status?.expires_at; @@ -261,6 +280,17 @@ export default function MobileEventPhotoboothPage() { 'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.' )} + } + disabled={sendingEmail} + /> { diff --git a/resources/views/emails/photobooth-uploader-download.blade.php b/resources/views/emails/photobooth-uploader-download.blade.php new file mode 100644 index 0000000..6ace4f8 --- /dev/null +++ b/resources/views/emails/photobooth-uploader-download.blade.php @@ -0,0 +1,66 @@ +@extends('emails.partials.layout') + +@section('title', __('emails.photobooth_uploader.subject', ['event' => $eventName])) +@section('preheader', __('emails.photobooth_uploader.preheader', ['event' => $eventName])) +@section('hero_title', __('emails.photobooth_uploader.hero_title', ['name' => $recipientName])) +@section('hero_subtitle', __('emails.photobooth_uploader.hero_subtitle', ['event' => $eventName])) + +@section('content') +

+ {{ __('emails.photobooth_uploader.body', ['event' => $eventName]) }} +

+

+ {{ __('emails.photobooth_uploader.downloads_title') }} +

+ + + + + + + + + + + + + +
+ {{ __('emails.photobooth_uploader.downloads.windows') }} + + + {{ $links['windows'] }} + +
+ {{ __('emails.photobooth_uploader.downloads.macos') }} + + + {{ $links['macos'] }} + +
+ {{ __('emails.photobooth_uploader.downloads.linux') }} + + + {{ $links['linux'] }} + +
+

+ {{ __('emails.photobooth_uploader.credentials_hint') }} +

+@endsection + +@section('cta') + + {{ __('emails.photobooth_uploader.cta_windows') }} + + + {{ __('emails.photobooth_uploader.cta_macos') }} + + + {{ __('emails.photobooth_uploader.cta_linux') }} + +@endsection + +@section('footer') + {!! __('emails.photobooth_uploader.footer') !!} +@endsection diff --git a/routes/api.php b/routes/api.php index 37da472..5941dc4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -270,6 +270,8 @@ Route::prefix('v1')->name('api.v1.')->group(function () { Route::post('/disable', [PhotoboothController::class, 'disable'])->name('tenant.events.photobooth.disable'); Route::post('/connect-codes', [PhotoboothConnectCodeController::class, 'store']) ->name('tenant.events.photobooth.connect-codes.store'); + Route::post('/uploader-email', [PhotoboothController::class, 'sendUploaderDownloadEmail']) + ->name('tenant.events.photobooth.uploader-email'); }); Route::get('members', [EventMemberController::class, 'index']) diff --git a/tests/Feature/Photobooth/PhotoboothUploaderDownloadEmailTest.php b/tests/Feature/Photobooth/PhotoboothUploaderDownloadEmailTest.php new file mode 100644 index 0000000..49fb710 --- /dev/null +++ b/tests/Feature/Photobooth/PhotoboothUploaderDownloadEmailTest.php @@ -0,0 +1,28 @@ +for($this->tenant)->create([ + 'slug' => 'photobooth-email', + ]); + + $response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/photobooth/uploader-email"); + + $response->assertOk(); + + Mail::assertQueued(PhotoboothUploaderDownload::class); + } +}