From fda97b3c0565c17111732d7df3bfc8dc2cc23e9c Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 23 Jan 2026 13:31:50 +0100 Subject: [PATCH] Update dashboard KPIs for live show and auto-approval --- .../js/admin/i18n/locales/de/dashboard.json | 4 + .../js/admin/i18n/locales/en/dashboard.json | 4 + resources/js/admin/mobile/DashboardPage.tsx | 85 +++++++++++++------ .../mobile/__tests__/DashboardPage.test.tsx | 25 ++++-- 4 files changed, 85 insertions(+), 33 deletions(-) diff --git a/resources/js/admin/i18n/locales/de/dashboard.json b/resources/js/admin/i18n/locales/de/dashboard.json index acd2546..9d9669d 100644 --- a/resources/js/admin/i18n/locales/de/dashboard.json +++ b/resources/js/admin/i18n/locales/de/dashboard.json @@ -36,6 +36,10 @@ "lowCredits": "Mehr Kontingent buchen empfohlen" } }, + "kpis": { + "liveShowApproved": "Live-Show freigegeben", + "likesTotal": "Likes gesamt" + }, "liveNow": { "title": "Während des Events", "description": "Direkter Zugriff, solange {{count}} Event(s) live sind.", diff --git a/resources/js/admin/i18n/locales/en/dashboard.json b/resources/js/admin/i18n/locales/en/dashboard.json index 3dd6834..08621e9 100644 --- a/resources/js/admin/i18n/locales/en/dashboard.json +++ b/resources/js/admin/i18n/locales/en/dashboard.json @@ -36,6 +36,10 @@ "lowCredits": "Add bundle soon" } }, + "kpis": { + "liveShowApproved": "Live Show approved", + "likesTotal": "Likes total" + }, "liveNow": { "title": "During the event", "description": "Quick actions while {{count}} event(s) are live.", diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx index fcd1eb3..00ac203 100644 --- a/resources/js/admin/mobile/DashboardPage.tsx +++ b/resources/js/admin/mobile/DashboardPage.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; -import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; +import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Heart, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; import { Button } from '@tamagui/button'; import { Card } from '@tamagui/card'; import { YGroup } from '@tamagui/group'; @@ -19,7 +19,7 @@ import toast from 'react-hot-toast'; import { MobileShell } from './components/MobileShell'; import { ADMIN_EVENTS_PATH, adminPath } from '../constants'; import { useEventContext } from '../context/EventContext'; -import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent } from '../api'; +import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent, getLiveShowQueue } from '../api'; import { formatEventDate } from '../lib/events'; import { useAuth } from '../auth/context'; import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme'; @@ -164,6 +164,19 @@ export default function MobileDashboardPage() { }, }); + const { data: liveShowApprovedCount = 0 } = useQuery({ + queryKey: ['mobile', 'dashboard', 'live-show-approved', activeEvent?.slug], + enabled: Boolean(activeEvent?.slug), + queryFn: async () => { + if (!activeEvent?.slug) return 0; + const result = await getLiveShowQueue(activeEvent.slug, { liveStatus: 'approved', perPage: 1 }); + if (result.photos.length === 0) { + return 0; + } + return result.meta.total ?? result.photos.length; + }, + }); + const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; React.useEffect(() => { @@ -232,7 +245,7 @@ export default function MobileDashboardPage() { {/* 2. PULSE STRIP */} - + @@ -559,36 +572,52 @@ function LifecycleHero({ ); } -function PulseStrip({ event, stats }: any) { +function PulseStrip({ event, stats, liveShowApprovedCount }: any) { const theme = useAdminTheme(); - const { t } = useTranslation('management'); + const { t } = useTranslation(['management', 'dashboard']); const uploadCount = stats?.uploads_total ?? event?.photo_count ?? 0; const guestCount = event?.active_invites_count ?? event?.total_invites_count ?? 0; const pendingCount = stats?.pending_photos ?? event?.pending_photo_count ?? 0; + const likesTotal = stats?.likes_total ?? stats?.likes ?? 0; + const showPending = (event?.settings?.guest_upload_visibility ?? 'review') !== 'immediate'; + const approvedCount = typeof liveShowApprovedCount === 'number' ? liveShowApprovedCount : 0; - return ( - 0 ? t('management:common.actionNeeded', 'Review') : undefined, - color: pendingCount > 0 ? '#8B5CF6' : theme.textMuted, - }, - ]} /> - ); + const items = [ + { + icon: ImageIcon, + value: uploadCount, + label: t('management:events.list.stats.photos', 'Photos'), + color: theme.primary, + }, + { + icon: Users, + value: guestCount, + label: t('management:events.list.stats.guests', 'Guests'), + tone: 'neutral' as const, + }, + { + icon: Tv, + value: approvedCount, + label: t('dashboard:kpis.liveShowApproved', 'Live Show approved'), + color: ADMIN_ACTION_COLORS.liveShow, + }, + showPending + ? { + icon: ShieldCheck, + value: pendingCount, + label: t('management:photos.filters.pending', 'Pending'), + note: pendingCount > 0 ? t('management:common.actionNeeded', 'Review') : undefined, + color: pendingCount > 0 ? '#8B5CF6' : theme.textMuted, + } + : { + icon: Heart, + value: likesTotal, + label: t('dashboard:kpis.likesTotal', 'Likes total'), + color: ADMIN_ACTION_COLORS.images, + }, + ]; + + return ; } function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) { diff --git a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx index 821a181..fd6f964 100644 --- a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import { ADMIN_EVENTS_PATH } from '../../constants'; const fixtures = vi.hoisted(() => ({ @@ -148,7 +148,7 @@ vi.mock('../components/Primitives', () => ({ MobileCard: ({ children }: { children: React.ReactNode }) =>
{children}
, CTAButton: ({ label }: { label: string }) => , KpiStrip: ({ items }: { items: Array<{ label: string; value: string | number }> }) => ( -
+
{items.map((item) => ( {item.label} ))} @@ -324,6 +324,7 @@ describe('MobileDashboardPage', () => { fixtures.activePackage.remaining_events = 3; fixtures.event.tasks_count = 4; fixtures.event.engagement_mode = undefined; + fixtures.event.settings = { location: 'Berlin' }; navigateMock.mockClear(); window.sessionStorage.clear(); }); @@ -368,9 +369,23 @@ describe('MobileDashboardPage', () => { it('shows the activity pulse strip', () => { render(); - expect(screen.getAllByText('Photos').length).toBeGreaterThan(0); - expect(screen.getAllByText('Guests').length).toBeGreaterThan(0); - expect(screen.getAllByText('Pending').length).toBeGreaterThan(0); + const strip = screen.getByTestId('kpi-strip'); + + expect(within(strip).getAllByText('Photos').length).toBeGreaterThan(0); + expect(within(strip).getAllByText('Guests').length).toBeGreaterThan(0); + expect(within(strip).getAllByText('Pending').length).toBeGreaterThan(0); + expect(within(strip).getAllByText('Live Show approved').length).toBeGreaterThan(0); + }); + + it('replaces pending with likes when auto-approval is enabled', () => { + fixtures.event.settings = { location: 'Berlin', guest_upload_visibility: 'immediate' }; + + render(); + + const strip = screen.getByTestId('kpi-strip'); + + expect(within(strip).queryByText('Pending')).not.toBeInTheDocument(); + expect(within(strip).getAllByText('Likes total').length).toBeGreaterThan(0); }); it('shows shortcut sections for members', () => {