Update dashboard KPIs for live show and auto-approval
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-23 13:31:50 +01:00
parent 55608c311d
commit fda97b3c05
4 changed files with 85 additions and 33 deletions

View File

@@ -36,6 +36,10 @@
"lowCredits": "Mehr Kontingent buchen empfohlen" "lowCredits": "Mehr Kontingent buchen empfohlen"
} }
}, },
"kpis": {
"liveShowApproved": "Live-Show freigegeben",
"likesTotal": "Likes gesamt"
},
"liveNow": { "liveNow": {
"title": "Während des Events", "title": "Während des Events",
"description": "Direkter Zugriff, solange {{count}} Event(s) live sind.", "description": "Direkter Zugriff, solange {{count}} Event(s) live sind.",

View File

@@ -36,6 +36,10 @@
"lowCredits": "Add bundle soon" "lowCredits": "Add bundle soon"
} }
}, },
"kpis": {
"liveShowApproved": "Live Show approved",
"likesTotal": "Likes total"
},
"liveNow": { "liveNow": {
"title": "During the event", "title": "During the event",
"description": "Quick actions while {{count}} event(s) are live.", "description": "Quick actions while {{count}} event(s) are live.",

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; 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 { Button } from '@tamagui/button';
import { Card } from '@tamagui/card'; import { Card } from '@tamagui/card';
import { YGroup } from '@tamagui/group'; import { YGroup } from '@tamagui/group';
@@ -19,7 +19,7 @@ import toast from 'react-hot-toast';
import { MobileShell } from './components/MobileShell'; import { MobileShell } from './components/MobileShell';
import { ADMIN_EVENTS_PATH, adminPath } from '../constants'; import { ADMIN_EVENTS_PATH, adminPath } from '../constants';
import { useEventContext } from '../context/EventContext'; 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 { formatEventDate } from '../lib/events';
import { useAuth } from '../auth/context'; import { useAuth } from '../auth/context';
import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme'; 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'; const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
React.useEffect(() => { React.useEffect(() => {
@@ -232,7 +245,7 @@ export default function MobileDashboardPage() {
{/* 2. PULSE STRIP */} {/* 2. PULSE STRIP */}
<Separator backgroundColor={theme.border} opacity={0.6} /> <Separator backgroundColor={theme.border} opacity={0.6} />
<PulseStrip event={activeEvent} stats={stats} /> <PulseStrip event={activeEvent} stats={stats} liveShowApprovedCount={liveShowApprovedCount} />
</YStack> </YStack>
</DashboardCard> </DashboardCard>
@@ -559,15 +572,17 @@ function LifecycleHero({
); );
} }
function PulseStrip({ event, stats }: any) { function PulseStrip({ event, stats, liveShowApprovedCount }: any) {
const theme = useAdminTheme(); const theme = useAdminTheme();
const { t } = useTranslation('management'); const { t } = useTranslation(['management', 'dashboard']);
const uploadCount = stats?.uploads_total ?? event?.photo_count ?? 0; const uploadCount = stats?.uploads_total ?? event?.photo_count ?? 0;
const guestCount = event?.active_invites_count ?? event?.total_invites_count ?? 0; const guestCount = event?.active_invites_count ?? event?.total_invites_count ?? 0;
const pendingCount = stats?.pending_photos ?? event?.pending_photo_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 ( const items = [
<KpiStrip items={[
{ {
icon: ImageIcon, icon: ImageIcon,
value: uploadCount, value: uploadCount,
@@ -578,17 +593,31 @@ function PulseStrip({ event, stats }: any) {
icon: Users, icon: Users,
value: guestCount, value: guestCount,
label: t('management:events.list.stats.guests', 'Guests'), label: t('management:events.list.stats.guests', 'Guests'),
tone: 'neutral', tone: 'neutral' as const,
}, },
{ {
icon: Tv,
value: approvedCount,
label: t('dashboard:kpis.liveShowApproved', 'Live Show approved'),
color: ADMIN_ACTION_COLORS.liveShow,
},
showPending
? {
icon: ShieldCheck, icon: ShieldCheck,
value: pendingCount, value: pendingCount,
label: t('management:photos.filters.pending', 'Pending'), label: t('management:photos.filters.pending', 'Pending'),
note: pendingCount > 0 ? t('management:common.actionNeeded', 'Review') : undefined, note: pendingCount > 0 ? t('management:common.actionNeeded', 'Review') : undefined,
color: pendingCount > 0 ? '#8B5CF6' : theme.textMuted, color: pendingCount > 0 ? '#8B5CF6' : theme.textMuted,
}
: {
icon: Heart,
value: likesTotal,
label: t('dashboard:kpis.likesTotal', 'Likes total'),
color: ADMIN_ACTION_COLORS.images,
}, },
]} /> ];
);
return <KpiStrip items={items} />;
} }
function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) { function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest'; 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'; import { ADMIN_EVENTS_PATH } from '../../constants';
const fixtures = vi.hoisted(() => ({ const fixtures = vi.hoisted(() => ({
@@ -148,7 +148,7 @@ vi.mock('../components/Primitives', () => ({
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>, MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>, CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
KpiStrip: ({ items }: { items: Array<{ label: string; value: string | number }> }) => ( KpiStrip: ({ items }: { items: Array<{ label: string; value: string | number }> }) => (
<div> <div data-testid="kpi-strip">
{items.map((item) => ( {items.map((item) => (
<span key={item.label}>{item.label}</span> <span key={item.label}>{item.label}</span>
))} ))}
@@ -324,6 +324,7 @@ describe('MobileDashboardPage', () => {
fixtures.activePackage.remaining_events = 3; fixtures.activePackage.remaining_events = 3;
fixtures.event.tasks_count = 4; fixtures.event.tasks_count = 4;
fixtures.event.engagement_mode = undefined; fixtures.event.engagement_mode = undefined;
fixtures.event.settings = { location: 'Berlin' };
navigateMock.mockClear(); navigateMock.mockClear();
window.sessionStorage.clear(); window.sessionStorage.clear();
}); });
@@ -368,9 +369,23 @@ describe('MobileDashboardPage', () => {
it('shows the activity pulse strip', () => { it('shows the activity pulse strip', () => {
render(<MobileDashboardPage />); render(<MobileDashboardPage />);
expect(screen.getAllByText('Photos').length).toBeGreaterThan(0); const strip = screen.getByTestId('kpi-strip');
expect(screen.getAllByText('Guests').length).toBeGreaterThan(0);
expect(screen.getAllByText('Pending').length).toBeGreaterThan(0); 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(<MobileDashboardPage />);
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', () => { it('shows shortcut sections for members', () => {