feat(dashboard): implement transparent setup roadmap and fix translations
- Added SetupChecklist component for clear progress visualization - Refactored LifecycleHero to show readiness state - Fixed remaining untranslated keys in tool grid and readiness hook
This commit is contained in:
@@ -7,6 +7,7 @@ import { YStack, XStack } from '@tamagui/stacks';
|
|||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
import { Image } from '@tamagui/image';
|
import { Image } from '@tamagui/image';
|
||||||
|
import { Progress } from '@tamagui/progress';
|
||||||
import { isSameDay, isPast, isFuture, parseISO, differenceInDays, startOfDay } from 'date-fns';
|
import { isSameDay, isPast, isFuture, parseISO, differenceInDays, startOfDay } from 'date-fns';
|
||||||
|
|
||||||
import { MobileShell } from './components/MobileShell';
|
import { MobileShell } from './components/MobileShell';
|
||||||
@@ -19,6 +20,7 @@ import { useAdminTheme } from './theme';
|
|||||||
import { buildLimitWarnings } from '../lib/limitWarnings';
|
import { buildLimitWarnings } from '../lib/limitWarnings';
|
||||||
import { withAlpha } from './components/colors';
|
import { withAlpha } from './components/colors';
|
||||||
import { useEventReadiness } from './hooks/useEventReadiness';
|
import { useEventReadiness } from './hooks/useEventReadiness';
|
||||||
|
import { SetupChecklist } from './components/SetupChecklist';
|
||||||
|
|
||||||
// --- HELPERS ---
|
// --- HELPERS ---
|
||||||
|
|
||||||
@@ -156,6 +158,10 @@ export default function MobileDashboardPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calculate Readiness for Setup Checklist
|
||||||
|
const readiness = useEventReadiness(activeEvent, t as any);
|
||||||
|
const phase = activeEvent ? getEventPhase(activeEvent) : 'setup';
|
||||||
|
|
||||||
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -194,8 +200,17 @@ export default function MobileDashboardPage() {
|
|||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
onSwitch={() => setEventSwitcherOpen(true)}
|
onSwitch={() => setEventSwitcherOpen(true)}
|
||||||
canSwitch={hasMultipleEvents}
|
canSwitch={hasMultipleEvents}
|
||||||
|
readiness={readiness}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 1b. SETUP CHECKLIST (Only in Setup Phase) */}
|
||||||
|
{phase === 'setup' && !readiness.isReady && (
|
||||||
|
<SetupChecklist
|
||||||
|
steps={readiness.steps}
|
||||||
|
title={t('management:photobooth.checklist.title', 'Checklist')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 2. PULSE STRIP */}
|
{/* 2. PULSE STRIP */}
|
||||||
<PulseStrip event={activeEvent} stats={stats} />
|
<PulseStrip event={activeEvent} stats={stats} />
|
||||||
|
|
||||||
@@ -234,10 +249,9 @@ function getEventPhase(event: TenantEvent): EventPhase {
|
|||||||
return 'setup';
|
return 'setup';
|
||||||
}
|
}
|
||||||
|
|
||||||
function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch }: any) {
|
function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch, readiness }: any) {
|
||||||
const theme = useAdminTheme();
|
const theme = useAdminTheme();
|
||||||
const { t } = useTranslation(['management', 'dashboard']);
|
const { t } = useTranslation(['management', 'dashboard']);
|
||||||
const { completedSteps, totalSteps, nextStep } = useEventReadiness(event, t as any);
|
|
||||||
|
|
||||||
if (!event) return null;
|
if (!event) return null;
|
||||||
const phase = getEventPhase(event);
|
const phase = getEventPhase(event);
|
||||||
@@ -320,9 +334,9 @@ function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch }:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SETUP
|
// SETUP PHASE
|
||||||
const ctaLabel = nextStep ? nextStep.ctaLabel : t('dashboard:onboarding.hero.cta', 'Setup Complete');
|
// We removed the big button. We show high-level status.
|
||||||
const ctaAction = nextStep ? () => navigate(adminPath(nextStep.targetPath)) : undefined;
|
const progressPercent = (readiness.progress ?? 0) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack>
|
<YStack>
|
||||||
@@ -344,20 +358,17 @@ function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch }:
|
|||||||
|
|
||||||
<YStack height={1} backgroundColor={theme.border} />
|
<YStack height={1} backgroundColor={theme.border} />
|
||||||
|
|
||||||
|
<YStack space="$1.5">
|
||||||
<XStack alignItems="center" justifyContent="space-between">
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
<Text fontSize="$sm" color={theme.textStrong} fontWeight="600">
|
<Text fontSize="$sm" color={theme.textStrong} fontWeight="600">
|
||||||
{t('management:photobooth.checklist.title', 'Setup Status')}
|
{t('management:photobooth.checklist.title', 'Setup Status')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="$xs" color={theme.muted}>{completedSteps}/{totalSteps} Schritte</Text>
|
<Text fontSize="$xs" color={theme.muted}>{readiness.completedSteps}/{readiness.totalSteps}</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
<Progress value={progressPercent} max={100} size="$2" backgroundColor={theme.surfaceMuted} borderWidth={0}>
|
||||||
<ModernButton
|
<Progress.Indicator backgroundColor={theme.primary} />
|
||||||
label={ctaLabel}
|
</Progress>
|
||||||
tone={nextStep ? 'primary' : 'ghost'}
|
</YStack>
|
||||||
icon={nextStep ? <ArrowRight size={16} color="white" /> : <CheckCircle2 size={16} color={theme.primary} />}
|
|
||||||
onPress={ctaAction}
|
|
||||||
disabled={!nextStep}
|
|
||||||
/>
|
|
||||||
</ModernCard>
|
</ModernCard>
|
||||||
</YStack>
|
</YStack>
|
||||||
);
|
);
|
||||||
@@ -407,7 +418,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember }: any) {
|
|||||||
items: [
|
items: [
|
||||||
{ label: t('management:photos.gallery.title', 'Photos'), icon: ImageIcon, path: `/mobile/events/${slug}/control-room`, color: theme.primary },
|
{ label: t('management:photos.gallery.title', 'Photos'), icon: ImageIcon, path: `/mobile/events/${slug}/control-room`, color: theme.primary },
|
||||||
{ label: t('management:events.quick.liveShowSettings', 'Slide Show'), icon: Tv, path: `/mobile/events/${slug}/live-show/settings`, color: '#F59E0B' },
|
{ label: t('management:events.quick.liveShowSettings', 'Slide Show'), icon: Tv, path: `/mobile/events/${slug}/live-show/settings`, color: '#F59E0B' },
|
||||||
{ label: t('management:events.quick.tasks', 'Tasks'), icon: ListTodo, path: `/mobile/events/${slug}/tasks`, color: theme.accent },
|
{ label: t('management:tasks.badge', 'Tasks'), icon: ListTodo, path: `/mobile/events/${slug}/tasks`, color: theme.accent },
|
||||||
{ label: t('management:events.quick.photobooth', 'Photobooth'), icon: Camera, path: `/mobile/events/${slug}/photobooth`, color: '#8B5CF6' },
|
{ label: t('management:events.quick.photobooth', 'Photobooth'), icon: Camera, path: `/mobile/events/${slug}/photobooth`, color: '#8B5CF6' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -417,7 +428,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember }: any) {
|
|||||||
{ label: t('management:invites.badge', 'QR Codes'), icon: QrCode, path: `/mobile/events/${slug}/qr`, color: '#10B981' },
|
{ label: t('management:invites.badge', 'QR Codes'), icon: QrCode, path: `/mobile/events/${slug}/qr`, color: '#10B981' },
|
||||||
{ label: t('management:events.quick.guests', 'Guests'), icon: Users, path: `/mobile/events/${slug}/members`, color: theme.text },
|
{ label: t('management:events.quick.guests', 'Guests'), icon: Users, path: `/mobile/events/${slug}/members`, color: theme.text },
|
||||||
{ label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: theme.text },
|
{ label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: theme.text },
|
||||||
{ label: t('management:events.branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: theme.text },
|
{ label: t('management:branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: theme.text },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
91
resources/js/admin/mobile/components/SetupChecklist.tsx
Normal file
91
resources/js/admin/mobile/components/SetupChecklist.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
|
import { CheckCircle2, Circle, ChevronRight, ArrowRight } from 'lucide-react';
|
||||||
|
import { useAdminTheme } from '../theme';
|
||||||
|
import { ReadinessStep } from '../hooks/useEventReadiness';
|
||||||
|
import { adminPath } from '../../constants';
|
||||||
|
|
||||||
|
export function SetupChecklist({ steps, title }: { steps: ReadinessStep[]; title: string }) {
|
||||||
|
const theme = useAdminTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (steps.every(s => s.isComplete)) {
|
||||||
|
return null; // Don't show if all done
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack
|
||||||
|
backgroundColor={theme.surface}
|
||||||
|
borderRadius={16}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={theme.border}
|
||||||
|
padding="$0"
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ boxShadow: `0 2px 8px ${theme.shadow}` }}
|
||||||
|
>
|
||||||
|
<YStack padding="$3.5" paddingBottom="$2">
|
||||||
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
|
||||||
|
<YStack>
|
||||||
|
{steps.map((step, index) => {
|
||||||
|
const isNext = !step.isComplete && steps.slice(0, index).every(s => s.isComplete);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={step.id}
|
||||||
|
onPress={() => navigate(adminPath(step.targetPath))}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isNext ? theme.surface : 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XStack
|
||||||
|
paddingHorizontal="$3.5"
|
||||||
|
paddingVertical="$3"
|
||||||
|
alignItems="center"
|
||||||
|
space="$3"
|
||||||
|
borderTopWidth={1}
|
||||||
|
borderColor={theme.border}
|
||||||
|
>
|
||||||
|
{step.isComplete ? (
|
||||||
|
<CheckCircle2 size={20} color={theme.successText} />
|
||||||
|
) : isNext ? (
|
||||||
|
<Circle size={20} color={theme.primary} strokeWidth={2.5} />
|
||||||
|
) : (
|
||||||
|
<Circle size={20} color={theme.border} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<YStack flex={1} space="$0.5">
|
||||||
|
<Text
|
||||||
|
fontSize="$sm"
|
||||||
|
fontWeight={isNext ? "700" : "500"}
|
||||||
|
color={step.isComplete ? theme.muted : theme.textStrong}
|
||||||
|
textDecorationLine={step.isComplete ? 'line-through' : 'none'}
|
||||||
|
>
|
||||||
|
{step.label}
|
||||||
|
</Text>
|
||||||
|
{step.description && !step.isComplete && (
|
||||||
|
<Text fontSize="$xs" color={theme.muted}>
|
||||||
|
{step.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
|
||||||
|
{isNext && (
|
||||||
|
<YStack backgroundColor={theme.primary} borderRadius={999} paddingHorizontal="$2.5" paddingVertical="$1.5">
|
||||||
|
<Text fontSize="$xs" color="white" fontWeight="700">Start</Text>
|
||||||
|
</YStack>
|
||||||
|
)}
|
||||||
|
</XStack>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</YStack>
|
||||||
|
</YStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,22 +4,22 @@ import { adminPath } from '../../constants';
|
|||||||
export type ReadinessStep = {
|
export type ReadinessStep = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
description?: string; // New: Contextual Why
|
||||||
isComplete: boolean;
|
isComplete: boolean;
|
||||||
ctaLabel: string;
|
ctaLabel: string;
|
||||||
targetPath: string;
|
targetPath: string;
|
||||||
priority: number; // Lower is higher priority
|
priority: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReadinessStatus = {
|
export type ReadinessStatus = {
|
||||||
steps: ReadinessStep[];
|
steps: ReadinessStep[];
|
||||||
totalSteps: number;
|
totalSteps: number;
|
||||||
completedSteps: number;
|
completedSteps: number;
|
||||||
progress: number; // 0 to 1
|
progress: number;
|
||||||
nextStep: ReadinessStep | null;
|
nextStep: ReadinessStep | null;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// We pass `t` (translation function) to localise the return values
|
|
||||||
export function useEventReadiness(event: TenantEvent | null, t: (key: string, fallback?: string) => string): ReadinessStatus {
|
export function useEventReadiness(event: TenantEvent | null, t: (key: string, fallback?: string) => string): ReadinessStatus {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return { steps: [], totalSteps: 0, completedSteps: 0, progress: 0, nextStep: null, isReady: false };
|
return { steps: [], totalSteps: 0, completedSteps: 0, progress: 0, nextStep: null, isReady: false };
|
||||||
@@ -27,7 +27,6 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
|
|||||||
|
|
||||||
const settings = (event.settings ?? {}) as Record<string, unknown>;
|
const settings = (event.settings ?? {}) as Record<string, unknown>;
|
||||||
|
|
||||||
// 1. Basics: Date & Location
|
|
||||||
const hasDate = Boolean(event.event_date);
|
const hasDate = Boolean(event.event_date);
|
||||||
const hasLocation = Boolean(
|
const hasLocation = Boolean(
|
||||||
(settings.location as string) ||
|
(settings.location as string) ||
|
||||||
@@ -35,17 +34,16 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
|
|||||||
(settings.city as string)
|
(settings.city as string)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Engagement: Tasks (only if tasks are enabled)
|
|
||||||
const tasksEnabled = event.engagement_mode !== 'photo_only';
|
const tasksEnabled = event.engagement_mode !== 'photo_only';
|
||||||
const hasTasks = (event.tasks_count ?? 0) > 0;
|
const hasTasks = (event.tasks_count ?? 0) > 0;
|
||||||
|
|
||||||
// 3. Access: QR / Invites
|
|
||||||
const hasInvite = (event.active_invites_count ?? 0) > 0 || (event.total_invites_count ?? 0) > 0;
|
const hasInvite = (event.active_invites_count ?? 0) > 0 || (event.total_invites_count ?? 0) > 0;
|
||||||
|
|
||||||
const steps: ReadinessStep[] = [
|
const steps: ReadinessStep[] = [
|
||||||
{
|
{
|
||||||
id: 'basics',
|
id: 'basics',
|
||||||
label: t('management:events.form.date', 'Datum & Ort'),
|
label: t('management:events.form.date', 'Datum & Ort'),
|
||||||
|
description: 'Grundlage für die Gäste-Info.',
|
||||||
isComplete: hasDate && hasLocation,
|
isComplete: hasDate && hasLocation,
|
||||||
ctaLabel: t('management:events.actions.edit', 'Bearbeiten'),
|
ctaLabel: t('management:events.actions.edit', 'Bearbeiten'),
|
||||||
targetPath: `/mobile/events/${event.slug}/edit`,
|
targetPath: `/mobile/events/${event.slug}/edit`,
|
||||||
@@ -54,6 +52,7 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
|
|||||||
{
|
{
|
||||||
id: 'access',
|
id: 'access',
|
||||||
label: t('management:invites.badge', 'QR-Codes'),
|
label: t('management:invites.badge', 'QR-Codes'),
|
||||||
|
description: 'Der Schlüssel für deine Gäste.',
|
||||||
ctaLabel: t('management:invites.actions.create', 'QR-Code erstellen'),
|
ctaLabel: t('management:invites.actions.create', 'QR-Code erstellen'),
|
||||||
isComplete: hasInvite,
|
isComplete: hasInvite,
|
||||||
targetPath: `/mobile/events/${event.slug}/qr`,
|
targetPath: `/mobile/events/${event.slug}/qr`,
|
||||||
@@ -65,6 +64,7 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
|
|||||||
steps.push({
|
steps.push({
|
||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
label: t('management:tasks.badge', 'Aufgaben'),
|
label: t('management:tasks.badge', 'Aufgaben'),
|
||||||
|
description: 'Sorgt für 3x mehr Interaktion.',
|
||||||
isComplete: hasTasks,
|
isComplete: hasTasks,
|
||||||
ctaLabel: t('management:tasks.actions.assign', 'Aufgaben hinzufügen'),
|
ctaLabel: t('management:tasks.actions.assign', 'Aufgaben hinzufügen'),
|
||||||
targetPath: `/mobile/events/${event.slug}/tasks`,
|
targetPath: `/mobile/events/${event.slug}/tasks`,
|
||||||
@@ -72,7 +72,6 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by priority
|
|
||||||
steps.sort((a, b) => a.priority - b.priority);
|
steps.sort((a, b) => a.priority - b.priority);
|
||||||
|
|
||||||
const completedSteps = steps.filter(s => s.isComplete).length;
|
const completedSteps = steps.filter(s => s.isComplete).length;
|
||||||
|
|||||||
Reference in New Issue
Block a user