From 253239455b7e217cfe09b4d769824080fdbf9bf4 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 7 Nov 2025 13:50:55 +0100 Subject: [PATCH] feat: unify tenant admin ui and add photo moderation --- resources/js/admin/api.ts | 72 +++- resources/js/admin/components/AdminLayout.tsx | 80 ++-- .../admin/components/tenant/action-grid.tsx | 38 ++ .../components/tenant/frosted-surface.tsx | 4 +- .../js/admin/components/tenant/hero-card.tsx | 36 +- resources/js/admin/components/tenant/index.ts | 3 + .../admin/components/tenant/section-card.tsx | 41 ++ .../admin/components/tenant/stat-carousel.tsx | 35 ++ resources/js/admin/pages/BillingPage.tsx | 84 ++-- resources/js/admin/pages/DashboardPage.tsx | 290 ++++++------- resources/js/admin/pages/EngagementPage.tsx | 85 +++- resources/js/admin/pages/EventDetailPage.tsx | 408 ++++++++++-------- resources/js/admin/pages/EventsPage.tsx | 356 ++++++++++----- resources/js/admin/pages/SettingsPage.tsx | 46 +- 14 files changed, 995 insertions(+), 583 deletions(-) create mode 100644 resources/js/admin/components/tenant/action-grid.tsx create mode 100644 resources/js/admin/components/tenant/section-card.tsx create mode 100644 resources/js/admin/components/tenant/stat-carousel.tsx diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index a570c4f..264ac09 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -137,6 +137,11 @@ export type DashboardSummary = { expires_at?: string | null; remaining_events?: number | null; } | null; + engagement_totals?: { + tasks?: number; + collections?: number; + emotions?: number; + }; }; export type TenantOnboardingStatus = { @@ -620,8 +625,31 @@ function normalizeDashboard(payload: JsonValue | null): DashboardSummary | null name: String(payload.active_package.name ?? 'Aktives Package'), expires_at: payload.active_package.expires_at ?? null, remaining_events: payload.active_package.remaining_events ?? payload.active_package.remainingEvents ?? null, - } + } : null, + engagement_totals: { + tasks: + Number( + payload.tasks?.summary?.total ?? + payload.tasks_total ?? + payload.engagement?.tasks ?? + 0, + ), + collections: + Number( + payload.task_collections?.summary?.total ?? + payload.collections_total ?? + payload.engagement?.collections ?? + 0, + ), + emotions: + Number( + payload.emotions?.summary?.total ?? + payload.emotions_total ?? + payload.engagement?.emotions ?? + 0, + ), + }, }; } @@ -923,6 +951,18 @@ export async function deletePhoto(slug: string, id: number): Promise { } } +export async function updatePhotoVisibility(slug: string, id: number, visible: boolean): Promise { + const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}/visibility`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ visible }), + }); + const data = await jsonOrThrow(response, 'Failed to update photo visibility'); + return normalizePhoto(data.data); +} + export async function toggleEvent(slug: string): Promise { const response = await authorizedFetch(`${eventEndpoint(slug)}/toggle`, { method: 'POST' }); const data = await jsonOrThrow<{ message: string; data: JsonValue }>(response, 'Failed to toggle event'); @@ -1026,22 +1066,22 @@ export async function getEventToolkit(slug: string): Promise { active_invites: Number((metrics as JsonValue).active_invites ?? 0), engagement_mode: ((metrics as JsonValue).engagement_mode as 'tasks' | 'photo_only') ?? 'tasks', }, - tasks: { - summary: { - total: Number((tasks as JsonValue)?.summary?.total ?? 0), - completed: Number((tasks as JsonValue)?.summary?.completed ?? 0), - pending: Number((tasks as JsonValue)?.summary?.pending ?? 0), - }, - items: Array.isArray((tasks as JsonValue)?.items) - ? ((tasks as JsonValue).items as JsonValue[]).map((item) => ({ - id: Number(item?.id ?? 0), - title: String(item?.title ?? ''), - description: item?.description !== undefined && item?.description !== null ? String(item.description) : null, - is_completed: Boolean(item?.is_completed ?? false), - priority: item?.priority !== undefined ? String(item.priority) : null, - })) - : [], + tasks: { + summary: { + total: Number((tasks as JsonValue)?.summary?.total ?? 0), + completed: Number((tasks as JsonValue)?.summary?.completed ?? 0), + pending: Number((tasks as JsonValue)?.summary?.pending ?? 0), }, + items: Array.isArray((tasks as JsonValue)?.items) + ? ((tasks as JsonValue).items as JsonValue[]).map((item) => ({ + id: Number(item?.id ?? 0), + title: String(item?.title ?? ''), + description: item?.description !== undefined && item?.description !== null ? String(item.description) : null, + is_completed: Boolean(item?.is_completed ?? false), + priority: item?.priority !== undefined ? String(item.priority) : null, + })) + : [], + }, photos: { pending: pendingPhotosRaw.map((photo: JsonValue) => normalizePhoto(photo as TenantPhoto)), recent: recentPhotosRaw.map((photo: JsonValue) => normalizePhoto(photo as TenantPhoto)), diff --git a/resources/js/admin/components/AdminLayout.tsx b/resources/js/admin/components/AdminLayout.tsx index af054c2..53beefc 100644 --- a/resources/js/admin/components/AdminLayout.tsx +++ b/resources/js/admin/components/AdminLayout.tsx @@ -1,15 +1,6 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { cn } from '@/lib/utils'; -import toast from 'react-hot-toast'; -import { - ADMIN_HOME_PATH, - ADMIN_EVENTS_PATH, - ADMIN_SETTINGS_PATH, - ADMIN_BILLING_PATH, - ADMIN_ENGAGEMENT_PATH, -} from '../constants'; import { LayoutDashboard, CalendarDays, @@ -17,6 +8,15 @@ import { CreditCard, Settings as SettingsIcon, } from 'lucide-react'; +import toast from 'react-hot-toast'; +import { cn } from '@/lib/utils'; +import { + ADMIN_HOME_PATH, + ADMIN_EVENTS_PATH, + ADMIN_SETTINGS_PATH, + ADMIN_BILLING_PATH, + ADMIN_ENGAGEMENT_PATH, +} from '../constants'; import { LanguageSwitcher } from './LanguageSwitcher'; import { registerApiErrorListener } from '../lib/apiError'; import { getDashboardSummary, getEvents, getTenantPackagesOverview } from '../api'; @@ -92,27 +92,29 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP }, [t]); return ( -
+
-
+
-
-
-
+
+
+
+

{t('app.brand')}

-

{t('app.brand')}

-

{title}

- {subtitle ?

{subtitle}

: null} -
-
- - {actions} +

{title}

+ {subtitle ?

{subtitle}

: null}
-
+ -
+
+
-
-
{children}
+
+
{children}
- +
); } -function TenantMobileNav({ items }: { items: typeof navItems }) { +function TenantMobileNav({ + items, + onPrefetch, +}: { + items: typeof navItems; + onPrefetch: (path: string) => void; +}) { const { t } = useTranslation('common'); return (