diff --git a/app/Http/Controllers/Api/EventPublicController.php b/app/Http/Controllers/Api/EventPublicController.php index 6b2df35..ee702b3 100644 --- a/app/Http/Controllers/Api/EventPublicController.php +++ b/app/Http/Controllers/Api/EventPublicController.php @@ -630,14 +630,22 @@ class EventPublicController extends BaseController { $guestNameParam = trim((string) $request->query('guest_name', '')); $deviceIdHeader = (string) $request->headers->get('X-Device-Id', ''); - $deviceId = substr(preg_replace('/[^A-Za-z0-9 _\-]/', '', $deviceIdHeader), 0, 120); - $candidate = $guestNameParam !== '' ? $guestNameParam : $deviceId; + $candidate = $guestNameParam !== '' ? $guestNameParam : $deviceIdHeader; + $normalized = $this->normalizeGuestIdentifier($candidate); - if ($candidate === '') { + if ($normalized === '') { return null; } - return substr(preg_replace('/[^A-Za-z0-9 _\-]/', '', $candidate), 0, 120); + return $normalized; + } + + private function normalizeGuestIdentifier(string $value): string + { + $cleaned = preg_replace('/[^\p{L}\p{N}\s_\-]/u', '', $value) ?? ''; + $trimmed = trim(mb_substr($cleaned, 0, 120)); + + return $trimmed; } private function buildAchievementsPayload(int $eventId, ?string $guestIdentifier, array $fallbacks): array @@ -1380,7 +1388,7 @@ class EventPublicController extends BaseController $photo = $shareLink->photo; $event = $photo->event; - if (! $event || $photo->status !== 'approved') { + if (! $event || in_array($photo->status, ['hidden', 'rejected'], true)) { return ApiError::response( 'photo_not_shareable', 'Photo Not Shareable', @@ -1399,14 +1407,23 @@ class EventPublicController extends BaseController } } + $emotionName = null; + if ($photo->emotion) { + $emotionName = $this->translateLocalized($photo->emotion->name, app()->getLocale(), ''); + if ($emotionName === '') { + $emotionName = is_string($photo->emotion->name) ? $photo->emotion->name : null; + } + } + $photoResource = [ 'id' => $photo->id, 'title' => $taskTitle, 'emotion' => $photo->emotion ? [ - 'name' => $photo->emotion->name, + 'name' => $emotionName, 'emoji' => $photo->emotion->emoji, ] : null, 'likes_count' => $photo->likes()->count(), + 'created_at' => $photo->created_at?->toIso8601String(), 'image_urls' => [ 'thumbnail' => $this->makeShareAssetUrl($shareLink, 'thumbnail'), 'full' => $this->makeShareAssetUrl($shareLink, 'full'), @@ -1453,7 +1470,7 @@ class EventPublicController extends BaseController $photo = $shareLink->photo; $event = $photo->event; - if (! $event || $photo->status !== 'approved') { + if (! $event || in_array($photo->status, ['hidden', 'rejected'], true)) { return ApiError::response( 'photo_not_shareable', 'Photo Not Shareable', @@ -2165,8 +2182,7 @@ class EventPublicController extends BaseController private function resolveDeviceIdentifier(Request $request): string { $deviceId = (string) $request->headers->get('X-Device-Id', ''); - $normalized = preg_replace('/[^A-Za-z0-9 _\-]/', '', $deviceId) ?? ''; - $normalized = trim(substr($normalized, 0, 120)); + $normalized = $this->normalizeGuestIdentifier($deviceId); return $normalized !== '' ? $normalized : 'anonymous'; } @@ -2634,6 +2650,7 @@ class EventPublicController extends BaseController 'thumbnail_path' => $thumbUrl, 'likes_count' => 0, 'ingest_source' => Photo::SOURCE_GUEST_PWA, + 'status' => 'approved', // Handle emotion_id: prefer explicit ID, fallback to slug lookup, then default 'emotion_id' => $this->resolveEmotionId($validated, $eventId), diff --git a/app/Http/Controllers/Api/Tenant/PhotoController.php b/app/Http/Controllers/Api/Tenant/PhotoController.php index 5176941..6bab1a7 100644 --- a/app/Http/Controllers/Api/Tenant/PhotoController.php +++ b/app/Http/Controllers/Api/Tenant/PhotoController.php @@ -244,7 +244,7 @@ class PhotoController extends Controller 'thumbnail_path' => $watermarkedThumb, 'width' => null, // Filled below 'height' => null, - 'status' => 'pending', // Requires moderation + 'status' => 'approved', 'uploader_id' => null, 'ip_address' => $request->ip(), 'user_agent' => $request->userAgent(), diff --git a/resources/css/app.css b/resources/css/app.css index 3daa6ce..d2c7566 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -537,3 +537,13 @@ h4, background-size: 400% 400%, 400% 400%; animation: aurora 20s ease infinite; } + +.guest-immersive .guest-header, +.guest-immersive .guest-bottom-nav { + display: none !important; +} + +.guest-immersive { + overscroll-behavior: contain; + background-color: #000; +} diff --git a/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx b/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx index 51552d1..11576a0 100644 --- a/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx +++ b/resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx @@ -58,7 +58,7 @@ export function DashboardEventFocusCard({ {t('empty.title', 'Leg mit deinem ersten Event los')} - {t('empty.description', 'Importiere ein Mission Pack, lege Branding fest und teile sofort den Gästelink.')} + {t('empty.description', 'Importiere ein Aufgaben-Set, lege Branding fest und teile sofort den Gästelink.')} @@ -117,7 +117,7 @@ export function DashboardEventFocusCard({ }, { key: 'tasks', - label: t('actions.tasks', 'Mission Packs & Emotionen'), + label: t('actions.tasks', 'Aufgaben-Sets & Emotionen'), description: t('actions.tasksHint', 'Kollektionen importieren und Emotionen aktivieren.'), icon: ClipboardList, handler: onOpenTasks, diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index 03070d1..9c033dd 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -1,4 +1,4 @@ -{ +{ "billing": { "title": "Pakete & Abrechnung", "subtitle": "Verwalte deine gebuchten Pakete und behalte Laufzeiten im Blick.", @@ -146,8 +146,8 @@ } }, "billingWarning": { - "title": "Handlungsbedarf", - "description": "Paketwarnungen und Limits, die du im Blick behalten solltest." + "title": "Achtung", + "description": "Paket-Hinweise und Limits, die du im Blick behalten solltest." }, "photos": { "moderation": { @@ -191,21 +191,252 @@ } }, "events": { - "list": { - "title": "Deine Events", - "subtitle": "Plane Momente, die in Erinnerung bleiben. Hier verwaltest du alles rund um deine Veranstaltungen.", - "actions": { - "create": "Neues Event", - "settings": "Einstellungen" + "errors": { + "missingSlug": "Kein Event ausgewählt.", + "loadFailed": "Event konnte nicht geladen werden.", + "notFoundTitle": "Event nicht gefunden", + "notFoundBody": "Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.", + "toggleFailed": "Status konnte nicht angepasst werden.", + "checkoutMissing": "Checkout konnte nicht gestartet werden.", + "checkoutFailed": "Add-on Checkout fehlgeschlagen." + }, + "alerts": { + "failedTitle": "Aktion fehlgeschlagen" + }, + "success": { + "addonApplied": "Add-on angewendet. Limits aktualisieren sich in Kürze." + }, + "placeholders": { + "untitled": "Unbenanntes Event" + }, + "actions": { + "backToList": "Zurück zur Liste", + "edit": "Bearbeiten", + "members": "Team & Rollen", + "tasks": "Aufgaben verwalten", + "invites": "QR-Codes & Layouts", + "photos": "Fotos moderieren", + "refresh": "Aktualisieren", + "buyMorePhotos": "Mehr Fotos freischalten", + "buyMoreGuests": "Mehr Gäste freischalten", + "extendGallery": "Galerie verlängern" + }, + "workspace": { + "detailSubtitle": "Behalte Status, Aufgaben und QR-Codes deines Events im Blick.", + "toolkitSubtitle": "Moderation, Aufgaben und QR-Codes für deinen Eventtag bündeln.", + "hero": { + "badge": "Event", + "description": "Konzentriere dich auf Aufgaben, Moderation und QR-Codes für dieses Event.", + "liveBadge": "Live?" }, - "overview": { - "title": "Übersicht", - "empty": "Noch keine Events - starte jetzt und lege dein erstes Event an.", - "count": "{{count}} {{count, plural, one {Event} other {Events}}} aktiv verwaltet.", - "badge": { - "dashboard": "Kunden-Dashboard" + "sections": { + "statusTitle": "Eventstatus & Sichtbarkeit", + "statusSubtitle": "Aktiviere dein Event für Gäste oder verstecke es vorübergehend." + }, + "fields": { + "status": "Status", + "active": "Aktiv für Gäste", + "date": "Eventdatum", + "noDate": "Kein Datum", + "eventType": "Event-Typ", + "insights": "Letzte Aktivität", + "uploadsTotal": "{{count}} Uploads gesamt", + "uploadsToday": "{{count}} Uploads (24h)", + "likesTotal": "{{count}} Likes vergeben" + }, + "actions": { + "pause": "Event pausieren", + "activate": "Event aktivieren" + }, + "activeYes": "Ja", + "activeNo": "Nein" + }, + "sections": { + "addons": { + "title": "Add-ons & Upgrades", + "description": "Zuletzt gebuchte Add-ons für dieses Event.", + "status": { + "completed": "Aktiv", + "pending": "In Bearbeitung", + "failed": "Fehlgeschlagen" + }, + "purchasedAt": "Gekauft {{date}}", + "summary": { + "photos": "+{{count}} Fotos", + "guests": "+{{count}} Gäste", + "gallery": "+{{count}} Tage Galerie" } } + }, + "status": { + "published": "Veröffentlicht", + "draft": "Entwurf", + "archived": "Archiviert" + }, + "quickActions": { + "title": "Schnellaktionen", + "subtitle": "Nutze die wichtigsten Schritte vor und während deines Events.", + "moderate": "Fotos moderieren", + "tasks": "Aufgaben bearbeiten", + "invites": "Layouts & QR verwalten", + "roles": "Team & Rollen anpassen", + "print": "Layouts als PDF drucken", + "toggle": "Status ändern" + }, + "metrics": { + "uploadsTotal": "Uploads gesamt", + "uploads24h": "Uploads (24h)", + "pending": "Fotos in Moderation", + "activeInvites": "Aktive QR-Codes" + }, + "invites": { + "badge": "QR-Codes", + "title": "QR-Codes", + "subtitle": "Behält aktive QR-Codes und Layouts im Blick.", + "activeCount": "{{count}} aktiv", + "totalCount": "{{count}} gesamt", + "empty": "Noch keine QR-Codes erstellt.", + "manage": "Layouts & QR-Codes verwalten" + }, + "tasks": { + "badge": "Aufgaben", + "title": "Aktive Aufgaben", + "subtitle": "Motiviere Gäste mit klaren Aufgaben & Highlights.", + "summary": "{{completed}} von {{total}} erledigt", + "empty": "Noch keine Aufgaben zugewiesen.", + "manage": "Aufgabenbereich öffnen", + "status": { + "completed": "Erledigt", + "open": "Offen" + } + }, + "recap": { + "badge": "Nachbereitung", + "subtitle": "Abschluss, Export und Galerie-Laufzeit verwalten.", + "galleryTitle": "Galerie-Status", + "galleryCounts": "{{photos}} Fotos, {{pending}} offen, {{likes}} Likes", + "open": "Offen", + "closed": "Geschlossen", + "openGallery": "Galerie öffnen", + "closeGallery": "Galerie schließen", + "moderate": "Uploads ansehen", + "shareGuests": "Gäste-Galerie teilen", + "shareLink": "Gäste-Link", + "noPublicUrl": "Kein Gäste-Link gesetzt. Lege den öffentlichen Link im Event-Setup fest.", + "copyLink": "Link kopieren", + "copySuccess": "Link kopiert", + "copyError": "Link konnte nicht geteilt werden.", + "qrTitle": "QR-Code teilen", + "qrDownload": "QR-Code herunterladen", + "qrShare": "Link/QR teilen", + "qrAlt": "QR-Code zur Gäste-Galerie", + "allowDownloads": "Downloads erlauben", + "allowDownloadsHint": "Gäste dürfen Fotos speichern", + "allowSharing": "Teilen erlauben", + "allowSharingHint": "Gäste dürfen Links teilen", + "galleryOpen": "Galerie geöffnet", + "galleryClosed": "Galerie geschlossen", + "exportTitle": "Export & Backup", + "exportCopy": "Alle Assets sichern", + "exportHint": "Zip/CSV Export und Backup anstoßen.", + "backup": "Backup", + "downloadAll": "Alles herunterladen", + "downloadHighlights": "Highlights herunterladen", + "highlightsHint": "„Highlights“ = als Highlight markierte Fotos in der Galerie.", + "retentionTitle": "Verlängerung / Archivierung", + "expiresAt": "Läuft ab am {{date}}", + "noExpiry": "Ablaufdatum nicht gesetzt", + "retentionHint": "Verlängere die Galerie-Laufzeit mit einem Add-on. Verlängerungen addieren sich.", + "expiry": "Ablauf", + "archive": "Archivieren/Löschen", + "extendOptions": "Alle Add-ons für dieses Event", + "extendHint": "Verlängerungen addieren sich. Checkout öffnet in einem neuen Tab.", + "priceMissing": "Preis nicht verknüpft", + "noAddons": "Aktuell keine Add-ons verfügbar.", + "archivedSuccess": "Event archiviert. Galerie ist geschlossen.", + "archiveTitle": "Galerie archivieren?", + "archiveDesc": "Das Archivieren schließt die Galerie, deaktiviert Gäste-Links und stoppt neue Uploads. Exporte vorher abschließen.", + "archiveImpact": "Auswirkungen des Archivierens", + "archiveImpactClose": "Gäste-Zugriff endet; Uploads/Downloads werden deaktiviert.", + "archiveImpactLinks": "Öffentliche Links und QR-Codes werden ungültig; Sessions laufen aus.", + "archiveImpactData": "Daten bleiben intern für Compliance/Support und können auf Anfrage gelöscht werden (DSGVO).", + "archiveConfirm": "Ich habe Exporte abgeschlossen und möchte jetzt archivieren.", + "archiveConfirmCta": "Jetzt archivieren" + }, + "branding": { + "badge": "Branding & Story", + "title": "Branding & Vorlagen / Aufgaben-Bundles", + "subtitle": "Stimme Farben, Schriftarten und Aufgabenpakete aufeinander ab.", + "brandingTitle": "Branding", + "brandingFallback": "Aktuelle Auswahl", + "brandingCopy": "Passe Farben & Schriftarten im Layout-Editor an.", + "brandingCta": "Branding anpassen", + "collectionsTitle": "Vorlagen / Aufgaben-Bundles", + "collectionsFallback": "Empfohlene Story", + "collectionsCopy": "Importiere passende Kollektionen oder aktiviere Emotionen im Aufgabenbereich.", + "collectionsActive": "{{count}} aktive Links", + "tasksCount": "{{count}} Aufgaben", + "collectionsManage": "Aufgaben bearbeiten", + "collectionsImport": "Aufgaben-Set importieren", + "emotionsTitle": "Emotionen", + "emotionsEmpty": "Aktiviere Emotionen, um Aufgaben zu kategorisieren.", + "emotionsCta": "Emotionen verwalten" + }, + "photos": { + "pendingBadge": "Moderation", + "pendingTitle": "Fotos in Moderation", + "pendingSubtitle": "Schnell prüfen, bevor Gäste live gehen.", + "pendingCount": "{{count}} Fotos offen", + "pendingEmpty": "Aktuell warten keine Fotos auf Freigabe.", + "openModeration": "Moderation öffnen", + "recentBadge": "Uploads", + "recentTitle": "Neueste Uploads", + "recentSubtitle": "Halte Ausschau nach Highlight-Momenten der Gäste.", + "recentEmpty": "Noch keine neuen Uploads.", + "toastVisible": "Foto wieder sichtbar gemacht.", + "toastHidden": "Foto ausgeblendet.", + "toastFeatured": "Foto als Highlight markiert.", + "toastUnfeatured": "Highlight entfernt.", + "errorAuth": "Session abgelaufen. Bitte erneut anmelden.", + "errorVisibility": "Sichtbarkeit konnte nicht geändert werden.", + "errorFeature": "Aktion fehlgeschlagen.", + "show": "Einblenden", + "hide": "Verstecken", + "feature": "Als Highlight markieren", + "unfeature": "Highlight entfernen" + }, + "feedback": { + "title": "Wie läuft dein Event?", + "subtitle": "Feedback hilft uns, neue Features zu priorisieren.", + "afterEventTitle": "Event beendet – kurzes Feedback?", + "afterEventCopy": "Hat alles geklappt? Deine Antwort hilft uns für kommende Events.", + "privacyHint": "Nur Admin-Feedback, keine Gastdaten", + "positive": "War super", + "neutral": "In Ordnung", + "negative": "Brauch(t)e Unterstützung", + "best": { + "uploads": "Uploads & Geschwindigkeit", + "invites": "QR-Codes & Layouts", + "moderation": "Moderation & Export", + "experience": "Allgemeine App-Erfahrung" + }, + "placeholder": "Optional: Lass uns wissen, was gut funktioniert oder wo du Unterstützung brauchst.", + "errorTitle": "Feedback konnte nicht gesendet werden.", + "authError": "Deine Session ist abgelaufen. Bitte melde dich erneut an.", + "genericError": "Feedback konnte nicht gesendet werden.", + "submit": "Feedback senden", + "submitted": "Danke!", + "afterEventThanks": "Dein Feedback ist angekommen. Wir melden uns, falls Rückfragen bestehen.", + "sendAnother": "Weiteres Feedback senden", + "supportFollowup": "Support anfragen", + "cta": "Feedback geben", + "quickSentiment": "Stimmung auswählbar (positiv/neutral/Support).", + "dialogTitle": "Kurzes After-Event Feedback", + "dialogCopy": "Wähle eine Stimmung, was am besten lief und optional, was wir verbessern sollen.", + "sentiment": "Stimmung", + "bestQuestion": "Was lief am besten?", + "improve": "Was sollen wir verbessern?", + "supportHelp": "Ich hätte gern ein kurzes Follow-up (Support)." } }, "galleryStatus": { @@ -311,7 +542,7 @@ }, "tabs": { "tasks": "Aufgaben", - "packs": "Mission Packs" + "packs": "Vorlagen / Aufgaben-Bundles" }, "eventStatus": "Status: {{status}}", "summary": { @@ -323,7 +554,7 @@ }, "library": { "hintTitle": "Weitere Vorlagen in der Aufgaben-Bibliothek", - "hintCopy": "Lege Aufgaben, Emotionen oder Mission Packs zentral an und nutze sie in mehreren Events.", + "hintCopy": "Lege Aufgaben, Emotionen oder Vorlagen / Aufgaben-Bundles zentral an und nutze sie in mehreren Events.", "open": "Aufgaben-Bibliothek öffnen" }, "sections": { @@ -354,7 +585,7 @@ "updating": "Einstellung wird gespeichert ..." }, "collections": { - "title": "Mission Packs", + "title": "Vorlagen / Aufgaben-Bundles", "subtitle": "Importiere Aufgaben-Kollektionen, die zu deinem Event passen.", "viewAll": "Alle Kollektionen ansehen", "errorTitle": "Kollektionen nicht verfügbar", @@ -365,9 +596,9 @@ "custom": "Custom", "recommended": "Empfohlen", "optional": "Optional", - "importCta": "Mission Pack importieren", + "importCta": "Aufgaben-Set importieren", "imported": "Kollektion erfolgreich importiert", - "importFailed": "Mission Pack konnte nicht importiert werden", + "importFailed": "Aufgaben-Set konnte nicht importiert werden", "error": "Kollektionen konnten nicht geladen werden." }, "toolkit": { @@ -449,15 +680,15 @@ }, "story": { "title": "Branding & Story", - "description": "Verbinde Farben, Emotionen und Mission Packs für ein stimmiges Gäste-Erlebnis.", + "description": "Verbinde Farben, Emotionen und Vorlagen / Aufgaben-Bundles für ein stimmiges Gäste-Erlebnis.", "emotionsTitle": "Emotionen", "emotionsCount": "{{count}} aktiviert", "emotionsEmpty": "Aktiviere Emotionen, um Aufgaben zu kategorisieren.", "emotionsCta": "Emotionen verwalten", - "collectionsTitle": "Mission Packs", + "collectionsTitle": "Vorlagen / Aufgaben-Bundles", "collectionsCount": "{{count}} Aufgaben", - "collectionsEmpty": "Noch keine empfohlenen Mission Packs.", - "collectionsCta": "Mission Packs anzeigen" + "collectionsEmpty": "Noch keine empfohlenen Vorlagen / Aufgaben-Bundles.", + "collectionsCta": "Aufgaben-Sets anzeigen" }, "customizer": { "title": "QR-Code anpassen", @@ -783,255 +1014,6 @@ "sourceEvent": "Quelle: Event" } }, - "events": { - "errors": { - "missingSlug": "Kein Event ausgewählt.", - "loadFailed": "Event konnte nicht geladen werden.", - "notFoundTitle": "Event nicht gefunden", - "notFoundBody": "Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.", - "toggleFailed": "Status konnte nicht angepasst werden.", - "checkoutMissing": "Checkout konnte nicht gestartet werden.", - "checkoutFailed": "Add-on Checkout fehlgeschlagen." - }, - "alerts": { - "failedTitle": "Aktion fehlgeschlagen" - }, - "success": { - "addonApplied": "Add-on angewendet. Limits aktualisieren sich in Kürze." - }, - "placeholders": { - "untitled": "Unbenanntes Event" - }, - "actions": { - "backToList": "Zurück zur Liste", - "edit": "Bearbeiten", - "members": "Team & Rollen", - "tasks": "Aufgaben verwalten", - "invites": "QR-Codes & Layouts", - "photos": "Fotos moderieren", - "refresh": "Aktualisieren", - "buyMorePhotos": "Mehr Fotos freischalten", - "buyMoreGuests": "Mehr Gäste freischalten", - "extendGallery": "Galerie verlängern" - }, - "workspace": { - "detailSubtitle": "Behalte Status, Aufgaben und QR-Codes deines Events im Blick.", - "toolkitSubtitle": "Moderation, Aufgaben und QR-Codes für deinen Eventtag bündeln.", - "hero": { - "badge": "Event", - "description": "Konzentriere dich auf Aufgaben, Moderation und QR-Codes für dieses Event.", - "liveBadge": "Live?" - }, - "sections": { - "statusTitle": "Eventstatus & Sichtbarkeit", - "statusSubtitle": "Aktiviere dein Event für Gäste oder verstecke es vorübergehend." - }, - "fields": { - "status": "Status", - "active": "Aktiv für Gäste", - "date": "Eventdatum", - "noDate": "Kein Datum", - "eventType": "Event-Typ", - "insights": "Letzte Aktivität", - "uploadsTotal": "{{count}} Uploads gesamt", - "uploadsToday": "{{count}} Uploads (24h)", - "likesTotal": "{{count}} Likes vergeben" - }, - "actions": { - "pause": "Event pausieren", - "activate": "Event aktivieren" - }, - "activeYes": "Ja", - "activeNo": "Nein" - }, - "sections": { - "addons": { - "title": "Add-ons & Upgrades", - "description": "Zuletzt gebuchte Add-ons für dieses Event.", - "status": { - "completed": "Aktiv", - "pending": "In Bearbeitung", - "failed": "Fehlgeschlagen" - }, - "purchasedAt": "Gekauft {{date}}", - "summary": { - "photos": "+{{count}} Fotos", - "guests": "+{{count}} Gäste", - "gallery": "+{{count}} Tage Galerie" - } - } - }, - "status": { - "published": "Veröffentlicht", - "draft": "Entwurf", - "archived": "Archiviert" - }, - "quickActions": { - "title": "Schnellaktionen", - "subtitle": "Nutze die wichtigsten Schritte vor und während deines Events.", - "moderate": "Fotos moderieren", - "tasks": "Aufgaben bearbeiten", - "invites": "Layouts & QR verwalten", - "roles": "Team & Rollen anpassen", - "print": "Layouts als PDF drucken", - "toggle": "Status ändern" - }, - "metrics": { - "uploadsTotal": "Uploads gesamt", - "uploads24h": "Uploads (24h)", - "pending": "Fotos in Moderation", - "activeInvites": "Aktive QR-Codes" - }, - "invites": { - "badge": "QR-Codes", - "title": "QR-Codes", - "subtitle": "Behält aktive QR-Codes und Layouts im Blick.", - "activeCount": "{{count}} aktiv", - "totalCount": "{{count}} gesamt", - "empty": "Noch keine QR-Codes erstellt.", - "manage": "Layouts & QR-Codes verwalten" - }, - "tasks": { - "badge": "Aufgaben", - "title": "Aktive Aufgaben", - "subtitle": "Motiviere Gäste mit klaren Aufgaben & Highlights.", - "summary": "{{completed}} von {{total}} erledigt", - "empty": "Noch keine Aufgaben zugewiesen.", - "manage": "Aufgabenbereich öffnen", - "status": { - "completed": "Erledigt", - "open": "Offen" - } - }, - "recap": { - "badge": "Nachbereitung", - "subtitle": "Abschluss, Export und Galerie-Laufzeit verwalten.", - "galleryTitle": "Galerie-Status", - "galleryCounts": "{{photos}} Fotos, {{pending}} offen, {{likes}} Likes", - "open": "Offen", - "closed": "Geschlossen", - "openGallery": "Galerie öffnen", - "closeGallery": "Galerie schließen", - "moderate": "Uploads ansehen", - "shareGuests": "Gäste-Galerie teilen", - "shareLink": "Gäste-Link", - "noPublicUrl": "Kein Gäste-Link gesetzt. Lege den öffentlichen Link im Event-Setup fest.", - "copyLink": "Link kopieren", - "copySuccess": "Link kopiert", - "copyError": "Link konnte nicht geteilt werden.", - "qrTitle": "QR-Code teilen", - "qrDownload": "QR-Code herunterladen", - "qrShare": "Link/QR teilen", - "qrAlt": "QR-Code zur Gäste-Galerie", - "allowDownloads": "Downloads erlauben", - "allowDownloadsHint": "Gäste dürfen Fotos speichern", - "allowSharing": "Teilen erlauben", - "allowSharingHint": "Gäste dürfen Links teilen", - "galleryOpen": "Galerie geöffnet", - "galleryClosed": "Galerie geschlossen", - "exportTitle": "Export & Backup", - "exportCopy": "Alle Assets sichern", - "exportHint": "Zip/CSV Export und Backup anstoßen.", - "backup": "Backup", - "downloadAll": "Alles herunterladen", - "downloadHighlights": "Highlights herunterladen", - "highlightsHint": "„Highlights“ = als Highlight markierte Fotos in der Galerie.", - "retentionTitle": "Verlängerung / Archivierung", - "expiresAt": "Läuft ab am {{date}}", - "noExpiry": "Ablaufdatum nicht gesetzt", - "retentionHint": "Verlängere die Galerie-Laufzeit mit einem Add-on. Verlängerungen addieren sich.", - "expiry": "Ablauf", - "archive": "Archivieren/Löschen", - "extendOptions": "Alle Add-ons für dieses Event", - "extendHint": "Verlängerungen addieren sich. Checkout öffnet in einem neuen Tab.", - "priceMissing": "Preis nicht verknüpft", - "noAddons": "Aktuell keine Add-ons verfügbar.", - "archivedSuccess": "Event archiviert. Galerie ist geschlossen.", - "archiveTitle": "Galerie archivieren?", - "archiveDesc": "Das Archivieren schließt die Galerie, deaktiviert Gäste-Links und stoppt neue Uploads. Exporte vorher abschließen.", - "archiveImpact": "Auswirkungen des Archivierens", - "archiveImpactClose": "Gäste-Zugriff endet; Uploads/Downloads werden deaktiviert.", - "archiveImpactLinks": "Öffentliche Links und QR-Codes werden ungültig; Sessions laufen aus.", - "archiveImpactData": "Daten bleiben intern für Compliance/Support und können auf Anfrage gelöscht werden (DSGVO).", - "archiveConfirm": "Ich habe Exporte abgeschlossen und möchte jetzt archivieren.", - "archiveConfirmCta": "Jetzt archivieren" - }, - "branding": { - "badge": "Branding & Story", - "title": "Branding & Mission Packs", - "subtitle": "Stimme Farben, Schriftarten und Aufgabenpakete aufeinander ab.", - "brandingTitle": "Branding", - "brandingFallback": "Aktuelle Auswahl", - "brandingCopy": "Passe Farben & Schriftarten im Layout-Editor an.", - "brandingCta": "Branding anpassen", - "collectionsTitle": "Mission Packs", - "collectionsFallback": "Empfohlene Story", - "collectionsCopy": "Importiere passende Kollektionen oder aktiviere Emotionen im Aufgabenbereich.", - "collectionsActive": "{{count}} aktive Links", - "tasksCount": "{{count}} Aufgaben", - "collectionsManage": "Aufgaben bearbeiten", - "collectionsImport": "Mission Pack importieren", - "emotionsTitle": "Emotionen", - "emotionsEmpty": "Aktiviere Emotionen, um Aufgaben zu kategorisieren.", - "emotionsCta": "Emotionen verwalten" - }, - "photos": { - "pendingBadge": "Moderation", - "pendingTitle": "Fotos in Moderation", - "pendingSubtitle": "Schnell prüfen, bevor Gäste live gehen.", - "pendingCount": "{{count}} Fotos offen", - "pendingEmpty": "Aktuell warten keine Fotos auf Freigabe.", - "openModeration": "Moderation öffnen", - "recentBadge": "Uploads", - "recentTitle": "Neueste Uploads", - "recentSubtitle": "Halte Ausschau nach Highlight-Momenten der Gäste.", - "recentEmpty": "Noch keine neuen Uploads.", - "toastVisible": "Foto wieder sichtbar gemacht.", - "toastHidden": "Foto ausgeblendet.", - "toastFeatured": "Foto als Highlight markiert.", - "toastUnfeatured": "Highlight entfernt.", - "errorAuth": "Session abgelaufen. Bitte erneut anmelden.", - "errorVisibility": "Sichtbarkeit konnte nicht geändert werden.", - "errorFeature": "Aktion fehlgeschlagen.", - "show": "Einblenden", - "hide": "Verstecken", - "feature": "Als Highlight markieren", - "unfeature": "Highlight entfernen" - }, - "feedback": { - "title": "Wie läuft dein Event?", - "subtitle": "Feedback hilft uns, neue Features zu priorisieren.", - "afterEventTitle": "Event beendet – kurzes Feedback?", - "afterEventCopy": "Hat alles geklappt? Deine Antwort hilft uns für kommende Events.", - "privacyHint": "Nur Admin-Feedback, keine Gastdaten", - "positive": "War super", - "neutral": "In Ordnung", - "negative": "Brauch(t)e Unterstützung", - "best": { - "uploads": "Uploads & Geschwindigkeit", - "invites": "QR-Codes & Layouts", - "moderation": "Moderation & Export", - "experience": "Allgemeine App-Erfahrung" - }, - "placeholder": "Optional: Lass uns wissen, was gut funktioniert oder wo du Unterstützung brauchst.", - "errorTitle": "Feedback konnte nicht gesendet werden.", - "authError": "Deine Session ist abgelaufen. Bitte melde dich erneut an.", - "genericError": "Feedback konnte nicht gesendet werden.", - "submit": "Feedback senden", - "submitted": "Danke!", - "afterEventThanks": "Dein Feedback ist angekommen. Wir melden uns, falls Rückfragen bestehen.", - "sendAnother": "Weiteres Feedback senden", - "supportFollowup": "Support anfragen", - "cta": "Feedback geben", - "quickSentiment": "Stimmung auswählbar (positiv/neutral/Support).", - "dialogTitle": "Kurzes After-Event Feedback", - "dialogCopy": "Wähle eine Stimmung, was am besten lief und optional, was wir verbessern sollen.", - "sentiment": "Stimmung", - "bestQuestion": "Was lief am besten?", - "improve": "Was sollen wir verbessern?", - "supportHelp": "Ich hätte gern ein kurzes Follow-up (Support)." - } - }, "tasks": { "actions": { "back": "Zurück zur Übersicht", @@ -1045,7 +1027,7 @@ }, "tabs": { "tasks": "Aufgaben", - "packs": "Mission Packs" + "packs": "Vorlagen / Aufgaben-Bundles" }, "eventStatus": "Status: {{status}}", "modes": { @@ -1066,7 +1048,7 @@ }, "library": { "hintTitle": "Weitere Vorlagen in der Aufgaben-Bibliothek", - "hintCopy": "Lege eigene Aufgaben, Emotionen oder Mission Packs zentral an und nutze sie in mehreren Events.", + "hintCopy": "Lege eigene Aufgaben, Emotionen oder Vorlagen / Aufgaben-Bundles zentral an und nutze sie in mehreren Events.", "open": "Aufgaben-Bibliothek öffnen" }, "sections": { @@ -1096,7 +1078,7 @@ "errorTitle": "Kollektionen nicht verfügbar", "import": "Kollektion importieren", "error": "Kollektionen konnten nicht geladen werden.", - "title": "Mission Packs", + "title": "Vorlagen / Aufgaben-Bundles", "subtitle": "Importiere Aufgaben-Kollektionen, die zu deinem Event passen.", "viewAll": "Alle Kollektionen ansehen", "empty": "Keine empfohlenen Kollektionen gefunden.", @@ -1106,9 +1088,9 @@ "custom": "Custom", "recommended": "Empfohlen", "optional": "Optional", - "importCta": "Mission Pack importieren", + "importCta": "Aufgaben-Set importieren", "imported": "Kollektion erfolgreich importiert", - "importFailed": "Mission Pack konnte nicht importiert werden" + "importFailed": "Aufgaben-Set konnte nicht importiert werden" } }, "collections": { @@ -1277,8 +1259,7 @@ } } } - } - , + }, "settings": { "hero": { "badge": "Administration", @@ -1538,14 +1519,9 @@ "cta": "Erste Task erstellen" } }, - "billingWarning": { - "title": "Achtung", - "description": "Paket-Hinweise und Limits, die du im Blick behalten solltest." - }, "eventForm": { "errors": { - "nameRequired": "Bitte gib einen Eventnamen ein.", - "typeRequired": "Bitte wähle einen Event-Typ aus." + "notice": "Hinweis" }, "titles": { "create": "Neues Event erstellen", @@ -1583,9 +1559,6 @@ "saving": "Speichert", "save": "Speichern", "cancel": "Abbrechen" - }, - "errors": { - "notice": "Hinweis" } }, "notifications": { @@ -1625,4 +1598,4 @@ "ctaFallback": "Events ansehen" } } -} +} \ No newline at end of file diff --git a/resources/js/admin/pages/EventDetailPage.tsx b/resources/js/admin/pages/EventDetailPage.tsx index 8da9fef..a528c89 100644 --- a/resources/js/admin/pages/EventDetailPage.tsx +++ b/resources/js/admin/pages/EventDetailPage.tsx @@ -884,7 +884,7 @@ function BrandingMissionCard({
@@ -908,7 +908,7 @@ function BrandingMissionCard({
-

{t('events.branding.collectionsTitle', 'Mission Packs')}

+

{t('events.branding.collectionsTitle', 'Aufgaben-Sets')}

{event.event_type?.name ?? t('events.branding.collectionsFallback', 'Empfohlene Story')}

@@ -965,7 +965,7 @@ function BrandingMissionCard({ {t('events.branding.collectionsManage', 'Aufgaben bearbeiten')}
diff --git a/resources/js/admin/pages/EventTasksPage.tsx b/resources/js/admin/pages/EventTasksPage.tsx index c6d4d6d..79dd675 100644 --- a/resources/js/admin/pages/EventTasksPage.tsx +++ b/resources/js/admin/pages/EventTasksPage.tsx @@ -70,7 +70,7 @@ export default function EventTasksPage() { const [saving, setSaving] = React.useState(false); const [modeSaving, setModeSaving] = React.useState(false); const [error, setError] = React.useState(null); - const [tab, setTab] = React.useState<'tasks' | 'packs'>('tasks'); + const [tab, setTab] = React.useState<'tasks' | 'packs' | 'emotions'>('packs'); const [taskSearch, setTaskSearch] = React.useState(''); const [debouncedTaskSearch, setDebouncedTaskSearch] = React.useState(''); const [difficultyFilter, setDifficultyFilter] = React.useState(''); @@ -88,12 +88,14 @@ export default function EventTasksPage() { const [newTaskEmotionId, setNewTaskEmotionId] = React.useState(null); const [newTaskDifficulty, setNewTaskDifficulty] = React.useState(''); const [creatingTask, setCreatingTask] = React.useState(false); + const [quickAddOpen, setQuickAddOpen] = React.useState(false); const [draggingId, setDraggingId] = React.useState(null); const [selectedAssignedIds, setSelectedAssignedIds] = React.useState([]); const [selectedAvailableIds, setSelectedAvailableIds] = React.useState([]); const [batchSaving, setBatchSaving] = React.useState(false); const [inlineSavingId, setInlineSavingId] = React.useState(null); const [emotionFilterOpen, setEmotionFilterOpen] = React.useState(false); + const libraryRef = React.useRef(null); React.useEffect(() => { const handle = window.setTimeout(() => setDebouncedTaskSearch(taskSearch.trim().toLowerCase()), 180); return () => window.clearTimeout(handle); @@ -333,6 +335,7 @@ export default function EventTasksPage() { setNewTaskDescription(''); setNewTaskEmotionId(null); setNewTaskDifficulty(''); + setQuickAddOpen(false); await hydrateTasks(event); } catch (err) { if (!isAuthError(err)) { @@ -424,14 +427,15 @@ export default function EventTasksPage() { await importTaskCollection(collection.id, slug); toast.success( t('collections.imported', { - defaultValue: 'Mission Pack "{{name}}" importiert.', + defaultValue: 'Aufgaben-Set "{{name}}" importiert.', name: collection.name, }), ); + setTab('tasks'); await hydrateTasks(event); } catch (err) { if (!isAuthError(err)) { - toast.error(t('collections.importFailed', 'Mission Pack konnte nicht importiert werden.')); + toast.error(t('collections.importFailed', 'Aufgaben-Set konnte nicht importiert werden.')); } } finally { setImportingCollectionId(null); @@ -447,30 +451,17 @@ export default function EventTasksPage() { return mode !== 'photo_only'; }, [event?.engagement_mode, event?.settings]); - const summaryBadges = !loading && event ? ( -
- - - {t('summary.assigned', 'Zugeordnete Tasks')} - - {assignedTasks.length} - - - - {t('summary.library', 'Bibliothek')} - - {availableTasks.length} - - - - {t('summary.mode', 'Aktiver Modus')} - - - {tasksEnabled ? t('summary.tasksMode', 'Mission Cards') : t('summary.photoOnly', 'Nur Fotos')} - - -
- ) : null; + const hasSelection = selectedAssignedIds.length > 0 || selectedAvailableIds.length > 0; + const tasksFirst = assignedTasks.length > 0; + const tabOrder: Array<'tasks' | 'packs' | 'emotions'> = tasksFirst ? ['tasks', 'packs', 'emotions'] : ['packs', 'tasks', 'emotions']; + const prevAssignedRef = React.useRef(assignedTasks.length); + + React.useEffect(() => { + if (prevAssignedRef.current === 0 && assignedTasks.length > 0) { + setTab('tasks'); + } + prevAssignedRef.current = assignedTasks.length; + }, [assignedTasks.length, setTab]); async function handleModeChange(checked: boolean) { if (!event || !slug) return; @@ -658,8 +649,6 @@ export default function EventTasksPage() { tabs={eventTabs} currentTabKey="tasks" > - {summaryBadges} - {error && ( {tDashboard('alerts.errorTitle', 'Fehler')} @@ -676,17 +665,33 @@ export default function EventTasksPage() { ) : ( <> - setTab(value as 'tasks' | 'packs')} className="space-y-6"> - - {t('tabs.tasks', 'Aufgaben')} - {t('tabs.packs', 'Mission Packs')} - + setTab(value as 'tasks' | 'packs' | 'emotions')} className="space-y-6"> +
+ + {tabOrder.map((key) => ( + + {key === 'packs' + ? t('tabs.packs', 'Vorlagen / Aufgaben-Bundles') + : key === 'tasks' + ? t('tabs.tasks', 'Aufgaben') + : t('tabs.emotions', 'Emotionen')} + + ))} + + +
- -
-
-
+ +
+
+

{t('modes.title', 'Aufgaben & Foto-Modus')}

@@ -695,48 +700,55 @@ export default function EventTasksPage() { ? t('modes.tasksHint', 'Aufgaben sind aktiv. Gäste sehen Mission Cards in der App.') : t('modes.photoOnlyHint', 'Der Foto-Modus ist aktiv. Gäste können Fotos hochladen, sehen aber keine Aufgaben.')}

+
+ + {t('summary.assigned', 'Zugeordnete Tasks')} · {assignedTasks.length} + + + {t('summary.library', 'Bibliothek')} · {availableTasks.length} + + + {t('summary.mode', 'Aktiver Modus')} ·{' '} + {tasksEnabled ? t('summary.tasksMode', 'Mission Cards') : t('summary.photoOnly', 'Nur Fotos')} + +
-
- - {tasksEnabled ? t('modes.tasks', 'Aufgaben aktiv') : t('modes.photoOnly', 'Foto-Modus')} - - +
+
+ + {tasksEnabled ? t('modes.tasks', 'Aufgaben aktiv') : t('modes.photoOnly', 'Foto-Modus')} + + +
+ {modeSaving ? ( +
+ + {t('modes.updating', 'Einstellung wird gespeichert ...')} +
+ ) : null} + {!tasksEnabled && assignedTasks.length === 0 ? ( +

+ {t('modes.needTasks', 'Aktiviere Aufgaben, sobald mindestens eine Aufgabe zugewiesen ist.')} +

+ ) : null}
- {modeSaving ? ( -
- - {t('modes.updating', 'Einstellung wird gespeichert ...')} -
- ) : null} +
+ +
- - - - {t('library.hintTitle', 'Weitere Vorlagen in der Aufgaben-Bibliothek')} - - - - {t('library.hintCopy', 'Lege eigene Aufgaben, Emotionen oder Mission Packs zentral an und nutze sie in mehreren Events.')} - - - - -
-

- - {t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')} -

-
-

{t('sections.library.quickCreate', 'Schnell neue Aufgabe anlegen')}

-
- setNewTaskTitle(e.target.value)} - placeholder={t('sections.library.quickTitle', 'Titel der Aufgabe')} - disabled={!tasksEnabled || creatingTask} - /> -