Update dashboard KPIs for live show and auto-approval
This commit is contained in:
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user