diff --git a/app/Filament/Resources/InviteLayouts/Schemas/InviteLayoutForm.php b/app/Filament/Resources/InviteLayouts/Schemas/InviteLayoutForm.php index ff183c0..4ea81e9 100644 --- a/app/Filament/Resources/InviteLayouts/Schemas/InviteLayoutForm.php +++ b/app/Filament/Resources/InviteLayouts/Schemas/InviteLayoutForm.php @@ -4,12 +4,12 @@ namespace App\Filament\Resources\InviteLayouts\Schemas; use Filament\Forms\Components\ColorPicker; use Filament\Forms\Components\Repeater; -use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Schemas\Schema; +use Filament\Schemas\Components\Section; use Illuminate\Support\Str; class InviteLayoutForm diff --git a/app/Http/Controllers/Api/Tenant/EventController.php b/app/Http/Controllers/Api/Tenant/EventController.php index b3752ef..6ed3581 100644 --- a/app/Http/Controllers/Api/Tenant/EventController.php +++ b/app/Http/Controllers/Api/Tenant/EventController.php @@ -39,10 +39,26 @@ class EventController extends Controller $query = Event::where('tenant_id', $tenantId) ->with([ 'eventType', - 'photos', 'eventPackages.package', 'eventPackage.package', ]) + ->withCount([ + 'photos', + 'photos as pending_photos_count' => fn ($photoQuery) => $photoQuery->where('status', 'pending'), + 'tasks as tasks_count', + 'joinTokens as total_join_tokens_count', + 'joinTokens as active_join_tokens_count' => fn ($tokenQuery) => $tokenQuery + ->whereNull('revoked_at') + ->where(function ($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->where(function ($query) { + $query->whereNull('usage_limit') + ->orWhereColumn('usage_limit', '>', 'usage_count'); + }), + ]) + ->withSum('photos as likes_sum', 'likes_count') ->orderBy('created_at', 'desc'); if ($request->has('status')) { diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index f2323c5..26e2bb0 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -265,14 +265,6 @@ class MarketingController extends Controller $converter = new MarkdownConverter($environment); $contentHtml = (string) $converter->convert($markdown); - // Debug log for content_html - \Log::info('BlogShow Debug: content_html type and preview', [ - 'type' => gettype($contentHtml), - 'is_string' => is_string($contentHtml), - 'length' => strlen($contentHtml ?? ''), - 'preview' => substr((string) $contentHtml, 0, 200).'...', - ]); - $post = [ 'id' => $postModel->id, 'title' => $postModel->getTranslation('title', $locale) ?? $postModel->getTranslation('title', 'de') ?? '', @@ -287,13 +279,6 @@ class MarketingController extends Controller ] : null, ]; - // Debug log for final postArray - \Log::info('BlogShow Debug: Final post content_html', [ - 'type' => gettype($post['content_html']), - 'is_string' => is_string($post['content_html']), - 'length' => strlen($post['content_html'] ?? ''), - ]); - return Inertia::render('marketing/BlogShow', compact('post')); } diff --git a/app/Http/Resources/Tenant/EventJoinTokenResource.php b/app/Http/Resources/Tenant/EventJoinTokenResource.php index 9259cd1..84844ba 100644 --- a/app/Http/Resources/Tenant/EventJoinTokenResource.php +++ b/app/Http/Resources/Tenant/EventJoinTokenResource.php @@ -20,11 +20,11 @@ class EventJoinTokenResource extends JsonResource $eventContext = $eventFromRoute instanceof Event ? $eventFromRoute : ($this->resource->event ?? null); $layouts = []; - if ($eventContext && Route::has('tenant.events.join-tokens.layouts.download')) { + if ($eventContext && Route::has('api.v1.tenant.events.join-tokens.layouts.download')) { $layouts = JoinTokenLayoutRegistry::toResponse(function (string $layoutId, string $format) use ($eventContext) { - return route('tenant.events.join-tokens.layouts.download', [ + return route('api.v1.tenant.events.join-tokens.layouts.download', [ 'event' => $eventContext, - 'joinToken' => $this->resource, + 'joinToken' => $eventContext instanceof Event ? $this->resource->id : $this->resource, 'layout' => $layoutId, 'format' => $format, ]); @@ -32,10 +32,10 @@ class EventJoinTokenResource extends JsonResource } $layoutsUrl = null; - if ($eventContext && Route::has('tenant.events.join-tokens.layouts.index')) { - $layoutsUrl = route('tenant.events.join-tokens.layouts.index', [ + if ($eventContext && Route::has('api.v1.tenant.events.join-tokens.layouts.index')) { + $layoutsUrl = route('api.v1.tenant.events.join-tokens.layouts.index', [ 'event' => $eventContext, - 'joinToken' => $this->resource, + 'joinToken' => $eventContext instanceof Event ? $this->resource->id : $this->resource, ]); } diff --git a/app/Http/Resources/Tenant/EventResource.php b/app/Http/Resources/Tenant/EventResource.php index 9dcbae2..59fb2c1 100644 --- a/app/Http/Resources/Tenant/EventResource.php +++ b/app/Http/Resources/Tenant/EventResource.php @@ -49,8 +49,14 @@ class EventResource extends JsonResource 'event_type_id' => $this->event_type_id, 'created_at' => $this->created_at?->toISOString(), 'updated_at' => $this->updated_at?->toISOString(), - 'photo_count' => $this->photos_count ?? 0, - 'like_count' => $this->whenLoaded('photos', fn () => $this->photos->sum('likes_count'), 0), + 'photo_count' => (int) ($this->photos_count ?? 0), + 'pending_photo_count' => isset($this->pending_photos_count) ? (int) $this->pending_photos_count : null, + 'like_count' => isset($this->likes_sum) + ? (int) $this->likes_sum + : $this->whenLoaded('photos', fn () => (int) $this->photos->sum('likes_count'), 0), + 'tasks_count' => isset($this->tasks_count) ? (int) $this->tasks_count : null, + 'active_invites_count' => isset($this->active_join_tokens_count) ? (int) $this->active_join_tokens_count : null, + 'total_invites_count' => isset($this->total_join_tokens_count) ? (int) $this->total_join_tokens_count : null, 'is_public' => $this->status === 'published', 'public_share_url' => null, 'qr_code_url' => null, diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index cfb8e74..61a789d 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -1,7 +1,7 @@ import { authorizedFetch } from './auth/tokens'; import i18n from './i18n'; -type JsonValue = Record; +type JsonValue = Record; export type EventQrInviteLayout = { id: string; @@ -40,7 +40,11 @@ export type TenantEvent = { is_active?: boolean; description?: string | null; photo_count?: number; + pending_photo_count?: number; like_count?: number; + tasks_count?: number; + active_invites_count?: number; + total_invites_count?: number; engagement_mode?: 'tasks' | 'photo_only'; settings?: Record & { engagement_mode?: 'tasks' | 'photo_only' }; package?: { @@ -442,7 +446,26 @@ function normalizeEvent(event: JsonValue): TenantEvent { is_active: typeof event.is_active === 'boolean' ? event.is_active : undefined, description: event.description ?? null, photo_count: event.photo_count !== undefined ? Number(event.photo_count ?? 0) : undefined, - like_count: event.like_count !== undefined ? Number(event.like_count ?? 0) : undefined, + pending_photo_count: event.pending_photo_count !== undefined ? Number(event.pending_photo_count ?? 0) : undefined, + like_count: + event.like_count !== undefined + ? Number(event.like_count ?? 0) + : event.likes_sum !== undefined + ? Number(event.likes_sum ?? 0) + : undefined, + tasks_count: event.tasks_count !== undefined ? Number(event.tasks_count ?? 0) : undefined, + active_invites_count: + event.active_invites_count !== undefined + ? Number(event.active_invites_count ?? 0) + : event.active_join_tokens_count !== undefined + ? Number(event.active_join_tokens_count ?? 0) + : undefined, + total_invites_count: + event.total_invites_count !== undefined + ? Number(event.total_invites_count ?? 0) + : event.total_join_tokens_count !== undefined + ? Number(event.total_join_tokens_count ?? 0) + : undefined, engagement_mode: engagementMode, settings, package: event.package ?? null, @@ -638,9 +661,9 @@ function normalizeMember(member: JsonValue): EventMember { } function normalizeQrInvite(raw: JsonValue): EventQrInvite { - const rawLayouts = Array.isArray(raw.layouts) ? raw.layouts : []; + const rawLayouts = Array.isArray(raw.layouts) ? (raw.layouts as JsonValue[]) : []; const layouts: EventQrInviteLayout[] = rawLayouts - .map((layout: any) => { + .map((layout: JsonValue) => { const formats = Array.isArray(layout.formats) ? layout.formats.map((format: unknown) => String(format ?? '')).filter((format: string) => format.length > 0) : []; diff --git a/resources/js/admin/components/AdminLayout.tsx b/resources/js/admin/components/AdminLayout.tsx index 0fde78f..5bab523 100644 --- a/resources/js/admin/components/AdminLayout.tsx +++ b/resources/js/admin/components/AdminLayout.tsx @@ -6,19 +6,15 @@ import { ADMIN_HOME_PATH, ADMIN_EVENTS_PATH, ADMIN_SETTINGS_PATH, - ADMIN_TASKS_PATH, ADMIN_BILLING_PATH, - ADMIN_TASK_COLLECTIONS_PATH, - ADMIN_EMOTIONS_PATH, + ADMIN_ENGAGEMENT_PATH, } from '../constants'; import { LanguageSwitcher } from './LanguageSwitcher'; const navItems = [ { to: ADMIN_HOME_PATH, labelKey: 'navigation.dashboard', end: true }, { to: ADMIN_EVENTS_PATH, labelKey: 'navigation.events' }, - { to: ADMIN_TASKS_PATH, labelKey: 'navigation.tasks' }, - { to: ADMIN_TASK_COLLECTIONS_PATH, labelKey: 'navigation.collections' }, - { to: ADMIN_EMOTIONS_PATH, labelKey: 'navigation.emotions' }, + { to: ADMIN_ENGAGEMENT_PATH, labelKey: 'navigation.engagement' }, { to: ADMIN_BILLING_PATH, labelKey: 'navigation.billing' }, { to: ADMIN_SETTINGS_PATH, labelKey: 'navigation.settings' }, ]; diff --git a/resources/js/admin/constants.ts b/resources/js/admin/constants.ts index ad6fb17..3787c3f 100644 --- a/resources/js/admin/constants.ts +++ b/resources/js/admin/constants.ts @@ -8,9 +8,9 @@ export const ADMIN_LOGIN_PATH = adminPath('/login'); export const ADMIN_AUTH_CALLBACK_PATH = adminPath('/auth/callback'); export const ADMIN_EVENTS_PATH = adminPath('/events'); export const ADMIN_SETTINGS_PATH = adminPath('/settings'); -export const ADMIN_TASKS_PATH = adminPath('/tasks'); -export const ADMIN_TASK_COLLECTIONS_PATH = adminPath('/task-collections'); -export const ADMIN_EMOTIONS_PATH = adminPath('/emotions'); +export const ADMIN_ENGAGEMENT_PATH = adminPath('/engagement'); +export const buildEngagementTabPath = (tab: 'tasks' | 'collections' | 'emotions'): string => + `${ADMIN_ENGAGEMENT_PATH}?tab=${encodeURIComponent(tab)}`; export const ADMIN_BILLING_PATH = adminPath('/billing'); export const ADMIN_PHOTOS_PATH = adminPath('/photos'); export const ADMIN_WELCOME_BASE_PATH = adminPath('/welcome'); @@ -25,3 +25,4 @@ export const ADMIN_EVENT_PHOTOS_PATH = (slug: string): string => adminPath(`/eve export const ADMIN_EVENT_MEMBERS_PATH = (slug: string): string => adminPath(`/events/${encodeURIComponent(slug)}/members`); export const ADMIN_EVENT_TASKS_PATH = (slug: string): string => adminPath(`/events/${encodeURIComponent(slug)}/tasks`); export const ADMIN_EVENT_TOOLKIT_PATH = (slug: string): string => adminPath(`/events/${encodeURIComponent(slug)}/toolkit`); +export const ADMIN_EVENT_INVITES_PATH = (slug: string): string => adminPath(`/events/${encodeURIComponent(slug)}/invites`); diff --git a/resources/js/admin/i18n/locales/de/common.json b/resources/js/admin/i18n/locales/de/common.json index 71843fa..5e4f61d 100644 --- a/resources/js/admin/i18n/locales/de/common.json +++ b/resources/js/admin/i18n/locales/de/common.json @@ -9,6 +9,7 @@ "tasks": "Aufgaben", "collections": "Aufgabenvorlagen", "emotions": "Emotionen", + "engagement": "Aufgaben & Co.", "billing": "Abrechnung", "settings": "Einstellungen" }, diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index e47620e..9e5505a 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -314,6 +314,83 @@ } } }, + "invites": { + "cardTitle": "QR-Einladungen & Layouts", + "cardDescription": "Erzeuge Einladungen, passe Layouts an und stelle druckfertige Vorlagen bereit.", + "subtitle": "Manage QR-Einladungen, Drucklayouts und Branding für deine Gäste.", + "summary": { + "active": "Aktive Einladungen", + "total": "Gesamt" + }, + "actions": { + "refresh": "Aktualisieren", + "create": "Neue Einladung erstellen", + "backToList": "Zurück zur Übersicht", + "backToEvent": "Event öffnen", + "copy": "Link kopieren", + "copied": "Kopiert!", + "deactivate": "Deaktivieren" + }, + "labels": { + "usage": "Nutzung", + "layout": "Layout", + "layoutFallback": "Standard", + "selected": "Aktuell ausgewählt", + "tapToEdit": "Zum Anpassen auswählen", + "noPrintSource": "Keine druckbare Version verfügbar." + }, + "empty": { + "title": "Noch keine Einladungen", + "copy": "Erstelle eine Einladung, um druckfertige QR-Layouts zu erhalten." + }, + "errorTitle": "Aktion fehlgeschlagen", + "customizer": { + "heading": "Layout anpassen", + "copy": "Verleihe der Einladung euren Ton: Texte, Farben und Logo lassen sich live bearbeiten.", + "actions": { + "save": "Layout speichern", + "reset": "Zurücksetzen", + "print": "Drucken", + "removeLogo": "Logo entfernen", + "uploadLogo": "Logo hochladen (max. 1 MB)", + "addInstruction": "Punkt hinzufügen" + }, + "sections": { + "layouts": "Layouts", + "layoutsHint": "Wähle eine Vorlage als Basis aus. Du kannst jederzeit wechseln.", + "text": "Texte", + "instructions": "Schritt-für-Schritt", + "instructionsHint": "Helft euren Gästen mit klaren Aufgaben. Maximal fünf Punkte.", + "branding": "Branding" + }, + "fields": { + "headline": "Überschrift", + "subtitle": "Unterzeile", + "description": "Beschreibung", + "badge": "Badge-Label", + "cta": "Call-to-Action", + "linkHeading": "Link-Überschrift", + "linkLabel": "Link/Begleittext", + "instructionsHeading": "Abschnittsüberschrift", + "instructionPlaceholder": "Beschreibung des Schritts", + "accentColor": "Akzentfarbe", + "textColor": "Textfarbe", + "backgroundColor": "Hintergrund", + "badgeColor": "Badge", + "logo": "Logo" + }, + "preview": { + "title": "Live-Vorschau", + "subtitle": "So sieht dein Layout beim Export aus." + }, + "placeholderTitle": "Kein Layout verfügbar", + "placeholderCopy": "Erstelle eine Einladung, damit du Texte, Farben und Drucklayouts bearbeiten kannst.", + "loadingTitle": "Layouts werden geladen", + "loadingDescription": "Bitte warte einen Moment, wir bereiten die Drucklayouts vor.", + "loadingError": "Layouts konnten nicht geladen werden.", + "layoutFallback": "Layout" + } + }, "collections": { "title": "Aufgabenvorlagen", "subtitle": "Durchstöbere kuratierte Vorlagen oder aktiviere sie für deine Events.", @@ -365,6 +442,9 @@ "page": "Seite {{current}} von {{total}}" } }, + "engagement": { + "subtitle": "Plane Aufgaben, Vorlagen und Emotionen gebündelt für deine Events." + }, "emotions": { "title": "Emotionen", "subtitle": "Verwalte Stimmungen und Icons, die deine Events begleiten.", diff --git a/resources/js/admin/i18n/locales/en/common.json b/resources/js/admin/i18n/locales/en/common.json index 5715d92..642330b 100644 --- a/resources/js/admin/i18n/locales/en/common.json +++ b/resources/js/admin/i18n/locales/en/common.json @@ -9,6 +9,7 @@ "tasks": "Tasks", "collections": "Collections", "emotions": "Emotions", + "engagement": "Tasks & More", "billing": "Billing", "settings": "Settings" }, diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index b1c4ffd..eb4b4b0 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -314,6 +314,83 @@ } } }, + "invites": { + "cardTitle": "QR invites & layouts", + "cardDescription": "Create invite links, customise layouts, and prepare print-ready PDFs.", + "subtitle": "Manage invite links, layouts, and branding for your guests.", + "summary": { + "active": "Active invites", + "total": "Total" + }, + "actions": { + "refresh": "Refresh", + "create": "Create invite", + "backToList": "Back to list", + "backToEvent": "Open event", + "copy": "Copy link", + "copied": "Copied!", + "deactivate": "Deactivate" + }, + "labels": { + "usage": "Usage", + "layout": "Layout", + "layoutFallback": "Default", + "selected": "Currently selected", + "tapToEdit": "Select to edit", + "noPrintSource": "No printable version available." + }, + "empty": { + "title": "No invites yet", + "copy": "Create an invite to generate ready-to-print QR layouts." + }, + "errorTitle": "Action failed", + "customizer": { + "heading": "Customise layout", + "copy": "Make the invite your own – adjust copy, colours, and logos in real time.", + "actions": { + "save": "Save layout", + "reset": "Reset", + "print": "Print", + "removeLogo": "Remove logo", + "uploadLogo": "Upload logo (max. 1 MB)", + "addInstruction": "Add step" + }, + "sections": { + "layouts": "Layouts", + "layoutsHint": "Pick a starting template. You can switch at any time.", + "text": "Text", + "instructions": "Step-by-step", + "instructionsHint": "Guide guests with clear steps. Maximum of five.", + "branding": "Branding" + }, + "fields": { + "headline": "Headline", + "subtitle": "Subheading", + "description": "Description", + "badge": "Badge label", + "cta": "Call-to-action", + "linkHeading": "Link heading", + "linkLabel": "Link/short URL", + "instructionsHeading": "Section heading", + "instructionPlaceholder": "Describe this step", + "accentColor": "Accent colour", + "textColor": "Text colour", + "backgroundColor": "Background", + "badgeColor": "Badge colour", + "logo": "Logo" + }, + "preview": { + "title": "Live preview", + "subtitle": "See the export-ready version instantly." + }, + "placeholderTitle": "No layout available", + "placeholderCopy": "Create an invite first to customise copy, colours, and print layouts.", + "loadingTitle": "Loading layouts", + "loadingDescription": "One moment – we are preparing the available layouts.", + "loadingError": "Layouts could not be loaded.", + "layoutFallback": "Layout" + } + }, "collections": { "title": "Task collections", "subtitle": "Browse curated task bundles or activate them for your events.", @@ -362,8 +439,11 @@ "pagination": { "prev": "Previous", "next": "Next", - "page": "Page {{current}} of {{total}}" - } + "page": "Page {{current}} of {{total}}" + } +}, + "engagement": { + "subtitle": "Manage tasks, collections, and emotions from a single workspace." }, "emotions": { "title": "Emotions", diff --git a/resources/js/admin/pages/AuthCallbackPage.tsx b/resources/js/admin/pages/AuthCallbackPage.tsx index dc8b50f..15a307f 100644 --- a/resources/js/admin/pages/AuthCallbackPage.tsx +++ b/resources/js/admin/pages/AuthCallbackPage.tsx @@ -26,7 +26,7 @@ export default function AuthCallbackPage() { if (isAuthError(err) && err.code === 'token_exchange_failed') { setError('Anmeldung fehlgeschlagen. Bitte versuche es erneut.'); } else if (isAuthError(err) && err.code === 'invalid_state') { - setError('Ungueltiger Login-Vorgang. Bitte starte die Anmeldung erneut.'); + setError('Ungültiger Login-Vorgang. Bitte starte die Anmeldung erneut.'); } else { setError('Unbekannter Fehler beim Login.'); } diff --git a/resources/js/admin/pages/DashboardPage.tsx b/resources/js/admin/pages/DashboardPage.tsx index 546fe17..89bacbb 100644 --- a/resources/js/admin/pages/DashboardPage.tsx +++ b/resources/js/admin/pages/DashboardPage.tsx @@ -27,8 +27,6 @@ import { getDashboardSummary, getEvents, getTenantPackagesOverview, - getEventTasks, - getEventQrInvites, TenantEvent, TenantPackageSummary, } from '../api'; @@ -39,11 +37,11 @@ import { ADMIN_EVENT_VIEW_PATH, ADMIN_EVENTS_PATH, ADMIN_EVENT_TASKS_PATH, - ADMIN_TASKS_PATH, ADMIN_BILLING_PATH, ADMIN_SETTINGS_PATH, ADMIN_WELCOME_BASE_PATH, ADMIN_EVENT_CREATE_PATH, + buildEngagementTabPath, } from '../constants'; import { useOnboardingProgress } from '../onboarding'; @@ -122,12 +120,18 @@ export default function DashboardPage() { setReadiness({ hasEvent: events.length > 0, - hasTasks: false, - hasQrInvites: false, + hasTasks: primaryEvent ? (Number(primaryEvent.tasks_count ?? 0) > 0) : false, + hasQrInvites: primaryEvent + ? Number( + primaryEvent.active_invites_count ?? + primaryEvent.active_join_tokens_count ?? + 0 + ) > 0 + : false, hasPackage: Boolean(packages.activePackage), primaryEventSlug: primaryEvent?.slug ?? null, primaryEventName, - loading: Boolean(primaryEvent), + loading: false, }); setState({ @@ -138,28 +142,7 @@ export default function DashboardPage() { errorKey: null, }); - if (primaryEvent) { - try { - const [eventTasks, qrInvites] = await Promise.all([ - getEventTasks(primaryEvent.id, 1), - getEventQrInvites(primaryEvent.slug), - ]); - - if (!cancelled) { - setReadiness((prev) => ({ - ...prev, - hasTasks: (eventTasks.data ?? []).length > 0, - hasQrInvites: qrInvites.length > 0, - loading: false, - })); - } - } catch (readinessError) { - if (!cancelled) { - console.warn('Failed to load readiness checklist', readinessError); - setReadiness((prev) => ({ ...prev, loading: false })); - } - } - } else if (!cancelled) { + if (!primaryEvent && !cancelled) { setReadiness((prev) => ({ ...prev, hasTasks: false, @@ -338,7 +321,7 @@ export default function DashboardPage() { icon={} label={translate('quickActions.organiseTasks.label')} description={translate('quickActions.organiseTasks.description')} - onClick={() => navigate(ADMIN_TASKS_PATH)} + onClick={() => navigate(buildEngagementTabPath('tasks'))} /> } @@ -385,7 +368,7 @@ export default function DashboardPage() { onOpenTasks={() => readiness.primaryEventSlug ? navigate(ADMIN_EVENT_TASKS_PATH(readiness.primaryEventSlug)) - : navigate(ADMIN_TASKS_PATH) + : navigate(buildEngagementTabPath('tasks')) } onOpenQr={() => readiness.primaryEventSlug diff --git a/resources/js/admin/pages/EmotionsPage.tsx b/resources/js/admin/pages/EmotionsPage.tsx index 4476a46..562adcc 100644 --- a/resources/js/admin/pages/EmotionsPage.tsx +++ b/resources/js/admin/pages/EmotionsPage.tsx @@ -3,34 +3,20 @@ import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; import { de, enGB } from 'date-fns/locale'; import type { Locale } from 'date-fns'; -import { Palette, Plus, Power, Smile } from 'lucide-react'; +import { Loader2, Palette, Plus, Power, Smile } from 'lucide-react'; -import { AdminLayout } from '../components/AdminLayout'; -import { - getEmotions, - createEmotion, - updateEmotion, - TenantEmotion, - EmotionPayload, -} from '../api'; -import { isAuthError } from '../auth/tokens'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; -const DEFAULT_COLOR = '#6366f1'; +import { AdminLayout } from '../components/AdminLayout'; +import { getEmotions, createEmotion, updateEmotion, TenantEmotion, EmotionPayload } from '../api'; +import { isAuthError } from '../auth/tokens'; type EmotionFormState = { name: string; @@ -41,6 +27,7 @@ type EmotionFormState = { sort_order: number; }; +const DEFAULT_COLOR = '#6366f1'; const INITIAL_FORM_STATE: EmotionFormState = { name: '', description: '', @@ -50,7 +37,11 @@ const INITIAL_FORM_STATE: EmotionFormState = { sort_order: 0, }; -export default function EmotionsPage(): JSX.Element { +export type EmotionsSectionProps = { + embedded?: boolean; +}; + +export function EmotionsSection({ embedded = false }: EmotionsSectionProps): JSX.Element { const { t, i18n } = useTranslation('management'); const [emotions, setEmotions] = React.useState([]); @@ -89,10 +80,10 @@ export default function EmotionsPage(): JSX.Element { }; }, [t]); - function openCreateDialog() { + const openCreateDialog = React.useCallback(() => { setForm(INITIAL_FORM_STATE); setDialogOpen(true); - } + }, []); async function handleCreate(event: React.FormEvent) { event.preventDefault(); @@ -137,21 +128,13 @@ export default function EmotionsPage(): JSX.Element { } const locale = i18n.language.startsWith('en') ? enGB : de; + const title = embedded ? t('emotions.title') : t('emotions.title'); + const subtitle = embedded + ? t('emotions.subtitle') + : t('emotions.subtitle'); return ( - - - {t('emotions.actions.create')} - - } - > +
{error && ( {t('emotions.errors.genericTitle')} @@ -160,15 +143,27 @@ export default function EmotionsPage(): JSX.Element { )} - - {t('emotions.title')} - {t('emotions.subtitle')} + +
+ + + {title} + + {subtitle} +
+
{loading ? ( ) : emotions.length === 0 ? ( - + ) : (
{emotions.map((emotion) => ( @@ -177,7 +172,6 @@ export default function EmotionsPage(): JSX.Element { emotion={emotion} onToggle={() => toggleEmotion(emotion)} locale={locale} - canToggle={!emotion.is_global} /> ))}
@@ -193,6 +187,15 @@ export default function EmotionsPage(): JSX.Element { saving={saving} onSubmit={handleCreate} /> +
+ ); +} + +export default function EmotionsPage(): JSX.Element { + const { t } = useTranslation('management'); + return ( + + ); } @@ -201,95 +204,60 @@ function EmotionCard({ emotion, onToggle, locale, - canToggle, }: { emotion: TenantEmotion; onToggle: () => void; locale: Locale; - canToggle: boolean; }) { const { t } = useTranslation('management'); - const updatedLabel = emotion.updated_at - ? format(new Date(emotion.updated_at), 'PP', { locale }) - : null; - + const updated = emotion.updated_at ? format(new Date(emotion.updated_at), 'Pp', { locale }) : null; return ( - - + +
- - {emotion.is_global ? t('emotions.scope.global') : t('emotions.scope.tenant')} - - - {emotion.event_types.map((type) => type.name).join(', ') || t('emotions.labels.noEventType')} - + +
+
+ {emotion.name} + {emotion.description ? ( + {emotion.description} + ) : null} +
- - - {emotion.name} - - {emotion.description && ( - {emotion.description} - )} -
- -
- - {emotion.color} -
- {updatedLabel && {t('emotions.labels.updated', { date: updatedLabel })}} -
- - + {emotion.is_active ? t('emotions.status.active') : t('emotions.status.inactive')} - - +
); } -function EmptyEmotionsState() { - const { t } = useTranslation('management'); - return ( -
-
- -
-
-

{t('emotions.empty.title')}

-

{t('emotions.empty.description')}

-
-
- ); -} - -function EmotionSkeleton() { - return ( -
- {Array.from({ length: 4 }).map((_, index) => ( -
-
-
-
-
- ))} -
- ); -} - function EmotionDialog({ open, onOpenChange, @@ -308,12 +276,11 @@ function EmotionDialog({ const { t } = useTranslation('management'); return ( - + {t('emotions.dialogs.createTitle')} - -
+
-
-