behoben: ohne aufgabe kann die kamera nicht gestartet werden (offensichtlich kein fehler mit browserzugriff auf kamera!)

platz zu begrenzt im aufnahmemodus - vollbildmodus möglich? Menü und Kopfleiste ausblenden?
	Bild aus eigener galerie auswählen - Upload schlägt fehl (zu groß? evtl fehlende Rechte - aber browser hat rechte auf bilder und dateien!)
	hochgeladene bilder tauchen in der galerie nicht beim filter "Meine Bilder" auf - fotos werden auch nicht gezählt in den stats und achievements zeigen keinen fortschriftt.
	geteilte fotos: ruft man den Link auf, bekommt man die meldung "Link abgelaufen"
	der im startbildschirm gewählte name mit Umlauten (Sören) ist nach erneutem aufruf der pwa ohne umlaut (Sren).
Aufgabenseite verbessert (Zwischenstand)
This commit is contained in:
Codex Agent
2025-12-04 11:58:07 +01:00
parent 899e742c38
commit c73a3163c0
15 changed files with 776 additions and 610 deletions

View File

@@ -58,7 +58,7 @@ export function DashboardEventFocusCard({
{t('empty.title', 'Leg mit deinem ersten Event los')}
</CardTitle>
<CardDescription className="text-sm text-slate-600 dark:text-slate-300">
{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.')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -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,

View File

@@ -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"
}
}
}
}

View File

@@ -884,7 +884,7 @@ function BrandingMissionCard({
<SectionCard className="space-y-4">
<SectionHeader
eyebrow={t('events.branding.badge', 'Branding & Story')}
title={t('events.branding.title', 'Branding & Mission Packs')}
title={t('events.branding.title', 'Branding & Aufgaben-Sets')}
description={t('events.branding.subtitle', 'Stimme Farben, Schriftarten und Aufgabenpakete aufeinander ab.')}
/>
<div className="grid gap-4 md:grid-cols-2">
@@ -908,7 +908,7 @@ function BrandingMissionCard({
</Button>
</div>
<div className="rounded-2xl border border-rose-100 bg-rose-50/80 p-4 text-sm text-rose-900 shadow-inner shadow-rose-100 dark:border-rose-300/40 dark:bg-rose-500/10 dark:text-rose-100">
<p className="text-xs uppercase tracking-[0.3em]">{t('events.branding.collectionsTitle', 'Mission Packs')}</p>
<p className="text-xs uppercase tracking-[0.3em]">{t('events.branding.collectionsTitle', 'Aufgaben-Sets')}</p>
<p className="mt-1 text-base font-semibold">
{event.event_type?.name ?? t('events.branding.collectionsFallback', 'Empfohlene Story')}
</p>
@@ -965,7 +965,7 @@ function BrandingMissionCard({
{t('events.branding.collectionsManage', 'Aufgaben bearbeiten')}
</Button>
<Button size="sm" variant="ghost" className="text-rose-700 hover:bg-rose-100/80" onClick={onOpenCollections}>
{t('events.branding.collectionsImport', 'Mission Pack importieren')}
{t('events.branding.collectionsImport', 'Aufgaben-Set importieren')}
</Button>
</div>
</div>

View File

@@ -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<string | null>(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<TenantTask['difficulty'] | ''>('');
@@ -88,12 +88,14 @@ export default function EventTasksPage() {
const [newTaskEmotionId, setNewTaskEmotionId] = React.useState<number | null>(null);
const [newTaskDifficulty, setNewTaskDifficulty] = React.useState<TenantTask['difficulty'] | ''>('');
const [creatingTask, setCreatingTask] = React.useState(false);
const [quickAddOpen, setQuickAddOpen] = React.useState(false);
const [draggingId, setDraggingId] = React.useState<number | null>(null);
const [selectedAssignedIds, setSelectedAssignedIds] = React.useState<number[]>([]);
const [selectedAvailableIds, setSelectedAvailableIds] = React.useState<number[]>([]);
const [batchSaving, setBatchSaving] = React.useState(false);
const [inlineSavingId, setInlineSavingId] = React.useState<number | null>(null);
const [emotionFilterOpen, setEmotionFilterOpen] = React.useState(false);
const libraryRef = React.useRef<HTMLDivElement | null>(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 ? (
<div className="mb-4 flex flex-wrap gap-2">
<Badge className="flex items-center gap-2 rounded-full bg-slate-900 text-white">
<span className="text-xs uppercase tracking-wide text-white/80">
{t('summary.assigned', 'Zugeordnete Tasks')}
</span>
<span className="text-sm font-semibold">{assignedTasks.length}</span>
</Badge>
<Badge className="flex items-center gap-2 rounded-full bg-emerald-600/90 text-white">
<span className="text-xs uppercase tracking-wide text-white/80">
{t('summary.library', 'Bibliothek')}
</span>
<span className="text-sm font-semibold">{availableTasks.length}</span>
</Badge>
<Badge className="flex items-center gap-2 rounded-full bg-pink-500/90 text-white">
<span className="text-xs uppercase tracking-wide text-white/80">
{t('summary.mode', 'Aktiver Modus')}
</span>
<span className="text-sm font-semibold">
{tasksEnabled ? t('summary.tasksMode', 'Mission Cards') : t('summary.photoOnly', 'Nur Fotos')}
</span>
</Badge>
</div>
) : 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 && (
<Alert variant="destructive">
<AlertTitle>{tDashboard('alerts.errorTitle', 'Fehler')}</AlertTitle>
@@ -676,17 +665,33 @@ export default function EventTasksPage() {
</Alert>
) : (
<>
<Tabs value={tab} onValueChange={(value) => setTab(value as 'tasks' | 'packs')} className="space-y-6">
<TabsList className="grid gap-2 rounded-2xl bg-slate-100/80 p-1 sm:grid-cols-2">
<TabsTrigger value="tasks">{t('tabs.tasks', 'Aufgaben')}</TabsTrigger>
<TabsTrigger value="packs">{t('tabs.packs', 'Mission Packs')}</TabsTrigger>
</TabsList>
<Tabs value={tab} onValueChange={(value) => setTab(value as 'tasks' | 'packs' | 'emotions')} className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<TabsList className="grid flex-1 gap-2 rounded-2xl bg-slate-100/80 p-1 sm:grid-cols-3">
{tabOrder.map((key) => (
<TabsTrigger key={key} value={key}>
{key === 'packs'
? t('tabs.packs', 'Vorlagen / Aufgaben-Bundles')
: key === 'tasks'
? t('tabs.tasks', 'Aufgaben')
: t('tabs.emotions', 'Emotionen')}
</TabsTrigger>
))}
</TabsList>
<Button
variant="outline"
className="border-emerald-200 text-emerald-700"
onClick={() => navigate(buildEngagementTabPath('tasks'))}
>
{t('library.open', 'Aufgaben-Bibliothek öffnen')}
</Button>
</div>
<TabsContent value="tasks" className="space-y-6">
<Card className="border-0 bg-white/85 shadow-xl shadow-pink-100/60">
<CardHeader>
<div className="mt-4 flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white/70 p-4 text-sm text-slate-700">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<CardHeader className="space-y-4">
<div className="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white/70 p-4 text-sm text-slate-700">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-2">
<p className="text-sm font-semibold text-slate-900">
{t('modes.title', 'Aufgaben & Foto-Modus')}
</p>
@@ -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.')}
</p>
<div className="flex flex-wrap gap-2">
<Badge className="rounded-full bg-slate-900 text-white">
{t('summary.assigned', 'Zugeordnete Tasks')} · {assignedTasks.length}
</Badge>
<Badge className="rounded-full bg-emerald-600/90 text-white">
{t('summary.library', 'Bibliothek')} · {availableTasks.length}
</Badge>
<Badge className="rounded-full bg-pink-500/90 text-white">
{t('summary.mode', 'Aktiver Modus')} ·{' '}
{tasksEnabled ? t('summary.tasksMode', 'Mission Cards') : t('summary.photoOnly', 'Nur Fotos')}
</Badge>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs uppercase tracking-wide text-slate-500">
{tasksEnabled ? t('modes.tasks', 'Aufgaben aktiv') : t('modes.photoOnly', 'Foto-Modus')}
</span>
<Switch
checked={tasksEnabled}
onCheckedChange={handleModeChange}
disabled={modeSaving}
aria-label={t('modes.switchLabel', 'Aufgaben aktivieren/deaktivieren')}
/>
<div className="flex flex-col items-start gap-2 lg:items-end">
<div className="flex items-center gap-3">
<span className="text-xs uppercase tracking-wide text-slate-500">
{tasksEnabled ? t('modes.tasks', 'Aufgaben aktiv') : t('modes.photoOnly', 'Foto-Modus')}
</span>
<Switch
checked={tasksEnabled}
onCheckedChange={handleModeChange}
disabled={modeSaving || (!tasksEnabled && assignedTasks.length === 0)}
aria-label={t('modes.switchLabel', 'Aufgaben aktivieren/deaktivieren')}
/>
</div>
{modeSaving ? (
<div className="flex items-center gap-2 text-xs text-slate-500">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t('modes.updating', 'Einstellung wird gespeichert ...')}
</div>
) : null}
{!tasksEnabled && assignedTasks.length === 0 ? (
<p className="text-[11px] text-slate-500">
{t('modes.needTasks', 'Aktiviere Aufgaben, sobald mindestens eine Aufgabe zugewiesen ist.')}
</p>
) : null}
</div>
</div>
{modeSaving ? (
<div className="flex items-center gap-2 text-xs text-slate-500">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t('modes.updating', 'Einstellung wird gespeichert ...')}
</div>
) : null}
<div className="flex flex-wrap gap-2">
<Button
className="bg-emerald-600 text-white hover:bg-emerald-700"
onClick={() => setQuickAddOpen(true)}
>
<PlusCircle className="mr-2 h-4 w-4" />
{t('actions.addCustom', 'Eigene Aufgabe hinzufügen')}
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pb-0">
<Alert variant="default" className="rounded-2xl border border-dashed border-emerald-200 bg-emerald-50/60 text-xs text-slate-700">
<AlertTitle className="text-sm font-semibold text-slate-900">
{t('library.hintTitle', 'Weitere Vorlagen in der Aufgaben-Bibliothek')}
</AlertTitle>
<AlertDescription className="mt-1 flex flex-wrap items-center gap-2">
<span>
{t('library.hintCopy', 'Lege eigene Aufgaben, Emotionen oder Mission Packs zentral an und nutze sie in mehreren Events.')}
</span>
<Button
type="button"
variant="outline"
size="sm"
className="mt-1 rounded-full border-emerald-300 text-emerald-700 hover:bg-emerald-100"
onClick={() => navigate(buildEngagementTabPath('tasks'))}
>
{t('library.open', 'Aufgaben-Bibliothek öffnen')}
</Button>
</AlertDescription>
</Alert>
</CardContent>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
@@ -939,73 +951,17 @@ export default function EventTasksPage() {
</section>
<section className="space-y-3">
<h3 className="flex items-center gap-2 text-sm font-semibold text-slate-900">
<PlusCircle className="h-4 w-4 text-emerald-500" />
{t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')}
</h3>
<div className="rounded-2xl border border-emerald-100 bg-emerald-50/60 p-3 shadow-inner">
<p className="text-xs font-semibold text-emerald-700">{t('sections.library.quickCreate', 'Schnell neue Aufgabe anlegen')}</p>
<div className="mt-2 grid gap-2">
<Input
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder={t('sections.library.quickTitle', 'Titel der Aufgabe')}
disabled={!tasksEnabled || creatingTask}
/>
<Textarea
value={newTaskDescription}
onChange={(e) => setNewTaskDescription(e.target.value)}
placeholder={t('sections.library.quickDescription', 'Beschreibung (optional)')}
disabled={!tasksEnabled || creatingTask}
className="min-h-[70px]"
/>
<div className="flex items-center gap-2">
<label className="text-xs text-slate-700">{t('sections.library.quickEmotion', 'Emotion')}</label>
<select
className="h-9 rounded-lg border border-slate-200 bg-white px-2 text-sm"
value={newTaskEmotionId ?? ''}
onChange={(e) => setNewTaskEmotionId(e.target.value ? Number(e.target.value) : null)}
disabled={!tasksEnabled || creatingTask}
>
<option value="">{t('sections.library.quickEmotionNone', 'Keine')}</option>
{relevantEmotions.map((emotion) => (
<option key={emotion.id} value={emotion.id}>
{emotion.name}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-slate-700">{t('sections.library.quickDifficulty', 'Schwierigkeit')}</label>
<select
className="h-9 rounded-lg border border-slate-200 bg-white px-2 text-sm"
disabled={!tasksEnabled || creatingTask}
value={newTaskDifficulty}
onChange={(e) => setNewTaskDifficulty(e.target.value as TenantTask['difficulty'] | '')}
>
<option value="">{t('sections.library.quickDifficultyNone', 'Keine')}</option>
<option value="easy">{t('sections.library.difficulty.easy', 'Leicht')}</option>
<option value="medium">{t('sections.library.difficulty.medium', 'Mittel')}</option>
<option value="hard">{t('sections.library.difficulty.hard', 'Schwer')}</option>
</select>
</div>
<div className="flex justify-end">
<Button
size="sm"
onClick={() => void handleCreateQuickTask()}
disabled={!newTaskTitle.trim() || creatingTask || !tasksEnabled}
>
{creatingTask ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
t('sections.library.quickCreateCta', 'Erstellen & zuweisen')
)}
</Button>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-2" ref={libraryRef} id="library-section">
<h3 className="flex items-center gap-2 text-sm font-semibold text-slate-900">
<PlusCircle className="h-4 w-4 text-emerald-500" />
{t('sections.library.title', 'Tasks aus Bibliothek hinzufügen')}
</h3>
</div>
<p className="text-xs text-slate-600">
{t('sections.library.helper', 'Suche, filtere und füge einzelne Aufgaben hinzu. Eigene Aufgaben legst du über den Dialog an.')}
</p>
<DropZone id="library-dropzone">
<div className="space-y-2 max-h-72 overflow-y-auto">
<div className="space-y-2 max-h-80 overflow-y-auto">
{availableTasks.length === 0 ? (
<EmptyState message={t('sections.library.empty', 'Keine Tasks in der Bibliothek gefunden.')} />
) : (
@@ -1087,58 +1043,54 @@ export default function EventTasksPage() {
</DragOverlay>
</DndContext>
</Card>
<div className="sticky bottom-3 z-10 flex flex-col gap-2 rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-xl shadow-slate-200 lg:top-4 lg:bottom-auto">
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-700">
<span className="font-semibold text-slate-900">
{t('sections.bulk.title', 'Batch-Aktionen')}
</span>
<Badge variant="outline" className="border-slate-200 text-slate-700">
{t('sections.bulk.assignedSelected', {
defaultValue: '{{count}} ausgewählt (Zugeordnet)',
count: selectedAssignedIds.length,
})}
</Badge>
<Badge variant="outline" className="border-slate-200 text-slate-700">
{t('sections.bulk.librarySelected', {
defaultValue: '{{count}} ausgewählt (Bibliothek)',
count: selectedAvailableIds.length,
})}
</Badge>
{hasSelection ? (
<div className="fixed inset-x-0 bottom-4 z-30 flex justify-center px-4">
<div className="pointer-events-auto flex w-full max-w-4xl flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-white/95 p-3 shadow-2xl">
<div className="flex flex-1 flex-wrap items-center gap-2 text-sm text-slate-700">
<Badge variant="outline" className="border-slate-200 text-slate-700">
{t('sections.bulk.assignedSelected', {
defaultValue: '{{count}} ausgewählt (Zugeordnet)',
count: selectedAssignedIds.length,
})}
</Badge>
<Badge variant="outline" className="border-slate-200 text-slate-700">
{t('sections.bulk.librarySelected', {
defaultValue: '{{count}} ausgewählt (Bibliothek)',
count: selectedAvailableIds.length,
})}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => void handleAssignSelected()}
disabled={selectedAvailableIds.length === 0 || !tasksEnabled || batchSaving || saving}
>
{t('actions.assignSelected', 'Auswahl zuweisen')}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => void handleDetachSelected()}
disabled={selectedAssignedIds.length === 0 || batchSaving || saving}
>
{t('actions.removeSelected', 'Auswahl entfernen')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedAssignedIds([]);
setSelectedAvailableIds([]);
}}
>
{t('sections.bulk.clear', 'Auswahl aufheben')}
</Button>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => void handleAssignSelected()}
disabled={selectedAvailableIds.length === 0 || !tasksEnabled || batchSaving || saving}
>
{t('actions.assignSelected', 'Auswahl zuweisen')}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => void handleDetachSelected()}
disabled={selectedAssignedIds.length === 0 || batchSaving || saving}
>
{t('actions.removeSelected', 'Auswahl entfernen')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => navigate(buildEngagementTabPath('tasks'))}
>
{t('library.open', 'Aufgaben-Bibliothek öffnen')}
</Button>
</div>
</div>
<EmotionsCard
emotions={relevantEmotions}
emotionsLoading={emotionsLoading}
emotionsError={emotionsError}
onOpenEmotions={() => setEmotionsModalOpen(true)}
/>
) : null}
</TabsContent>
<TabsContent value="packs">
<MissionPackGrid
@@ -1150,10 +1102,96 @@ export default function EventTasksPage() {
onViewAll={() => navigate(buildEngagementTabPath('collections'))}
/>
</TabsContent>
<TabsContent value="emotions">
<EmotionsCard
emotions={relevantEmotions}
emotionsLoading={emotionsLoading}
emotionsError={emotionsError}
onOpenEmotions={() => setEmotionsModalOpen(true)}
/>
</TabsContent>
</Tabs>
</>
)}
<Dialog open={quickAddOpen} onOpenChange={setQuickAddOpen}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>{t('sections.library.quickCreate', 'Eigene Aufgabe hinzufügen')}</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<p className="text-xs text-slate-600">
{t('sections.library.quickHelper', 'Titel eingeben, optional beschreiben und sofort zum Event zuweisen.')}
</p>
<Input
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder={t('sections.library.quickTitle', 'Titel der Aufgabe')}
disabled={!tasksEnabled || creatingTask}
/>
<Textarea
value={newTaskDescription}
onChange={(e) => setNewTaskDescription(e.target.value)}
placeholder={t('sections.library.quickDescription', 'Beschreibung (optional)')}
disabled={!tasksEnabled || creatingTask}
className="min-h-[90px]"
/>
<div className="grid gap-2 sm:grid-cols-2">
<div className="flex items-center gap-2">
<label className="text-xs text-slate-700">{t('sections.library.quickEmotion', 'Emotion')}</label>
<select
className="h-9 w-full rounded-lg border border-slate-200 bg-white px-2 text-sm"
value={newTaskEmotionId ?? ''}
onChange={(e) => setNewTaskEmotionId(e.target.value ? Number(e.target.value) : null)}
disabled={!tasksEnabled || creatingTask}
>
<option value="">{t('sections.library.quickEmotionNone', 'Keine')}</option>
{relevantEmotions.map((emotion) => (
<option key={emotion.id} value={emotion.id}>
{emotion.name}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-slate-700">{t('sections.library.quickDifficulty', 'Schwierigkeit')}</label>
<select
className="h-9 w-full rounded-lg border border-slate-200 bg-white px-2 text-sm"
disabled={!tasksEnabled || creatingTask}
value={newTaskDifficulty}
onChange={(e) => setNewTaskDifficulty(e.target.value as TenantTask['difficulty'] | '')}
>
<option value="">{t('sections.library.quickDifficultyNone', 'Keine')}</option>
<option value="easy">{t('sections.library.difficulty.easy', 'Leicht')}</option>
<option value="medium">{t('sections.library.difficulty.medium', 'Mittel')}</option>
<option value="hard">{t('sections.library.difficulty.hard', 'Schwer')}</option>
</select>
</div>
</div>
<div className="flex justify-end gap-2">
<Button
variant="ghost"
onClick={() => {
setQuickAddOpen(false);
setNewTaskTitle('');
setNewTaskDescription('');
setNewTaskEmotionId(null);
setNewTaskDifficulty('');
}}
>
{t('actions.cancel', 'Abbrechen')}
</Button>
<Button
onClick={() => void handleCreateQuickTask()}
disabled={!newTaskTitle.trim() || creatingTask || !tasksEnabled}
>
{creatingTask ? <Loader2 className="h-4 w-4 animate-spin" /> : t('sections.library.quickCreateCta', 'Erstellen & zuweisen')}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={emotionsModalOpen} onOpenChange={setEmotionsModalOpen}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
@@ -1409,14 +1447,14 @@ function MissionPackGrid({
<div>
<CardTitle className="flex items-center gap-2 text-base text-slate-900">
<Layers className="h-5 w-5 text-pink-500" />
{t('title', 'Mission Packs')}
{t('title', 'Vorlagen / Aufgaben-Bundles')}
</CardTitle>
<CardDescription className="text-sm text-slate-600">
{t('subtitle', 'Importiere Aufgaben-Kollektionen, die zu deinem Event passen.')}
{t('subtitle', 'Importiere Aufgaben-Sets, die zu deinem Event passen.')}
</CardDescription>
</div>
<Button variant="outline" onClick={onViewAll}>
{t('viewAll', 'Alle Kollektionen ansehen')}
{t('viewAll', 'Alle Sets ansehen')}
</Button>
</CardHeader>
<CardContent className="space-y-4">
@@ -1463,7 +1501,7 @@ function MissionPackGrid({
{importingId === collection.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
t('importCta', 'Mission Pack importieren')
t('importCta', 'Aufgaben-Set importieren')
)}
</Button>
</div>