Add tasks setup nudge and prompt
This commit is contained in:
@@ -2222,6 +2222,18 @@
|
|||||||
"bannerSubtitle": "{{name}} ist aktiv. Prüfe Limits & Features.",
|
"bannerSubtitle": "{{name}} ist aktiv. Prüfe Limits & Features.",
|
||||||
"bannerCta": "Ansehen"
|
"bannerCta": "Ansehen"
|
||||||
},
|
},
|
||||||
|
"tasksSetupNote": "Setup nötig",
|
||||||
|
"taskDecision": {
|
||||||
|
"title": "Aufgaben einrichten?",
|
||||||
|
"body": "Dein Event ist aktiv und Aufgaben sind eingeschaltet, aber es sind noch keine Aufgaben zugewiesen. Lege jetzt Aufgaben fest oder deaktiviere Aufgaben für dieses Event.",
|
||||||
|
"promptTitle": "Nächster Schritt",
|
||||||
|
"promptBody": "Gäste sehen Missionen erst, wenn Aufgaben hinterlegt sind.",
|
||||||
|
"ctaManage": "Aufgaben hinzufügen",
|
||||||
|
"ctaDisable": "Aufgaben deaktivieren",
|
||||||
|
"dismiss": "Später",
|
||||||
|
"disabledToast": "Aufgaben wurden für dieses Event deaktiviert.",
|
||||||
|
"disableError": "Aufgaben konnten nicht deaktiviert werden."
|
||||||
|
},
|
||||||
"pickEvent": "Event auswählen",
|
"pickEvent": "Event auswählen",
|
||||||
"status": {
|
"status": {
|
||||||
"published": "Live",
|
"published": "Live",
|
||||||
|
|||||||
@@ -2226,6 +2226,18 @@
|
|||||||
"bannerSubtitle": "{{name}} is active. Review limits & features.",
|
"bannerSubtitle": "{{name}} is active. Review limits & features.",
|
||||||
"bannerCta": "View"
|
"bannerCta": "View"
|
||||||
},
|
},
|
||||||
|
"tasksSetupNote": "Setup needed",
|
||||||
|
"taskDecision": {
|
||||||
|
"title": "Set up tasks?",
|
||||||
|
"body": "Your event is live with tasks enabled, but no tasks are assigned yet. Choose to add tasks now or disable tasks for this event.",
|
||||||
|
"promptTitle": "Next step",
|
||||||
|
"promptBody": "Guests only see missions when tasks are assigned.",
|
||||||
|
"ctaManage": "Add tasks",
|
||||||
|
"ctaDisable": "Disable tasks",
|
||||||
|
"dismiss": "Later",
|
||||||
|
"disabledToast": "Tasks disabled for this event.",
|
||||||
|
"disableError": "Could not disable tasks."
|
||||||
|
},
|
||||||
"pickEvent": "Select an event",
|
"pickEvent": "Select an event",
|
||||||
"status": {
|
"status": {
|
||||||
"published": "Live",
|
"published": "Live",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge, SkeletonCard } f
|
|||||||
import { MobileSheet } from './components/Sheet';
|
import { MobileSheet } from './components/Sheet';
|
||||||
import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants';
|
import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants';
|
||||||
import { useEventContext } from '../context/EventContext';
|
import { useEventContext } from '../context/EventContext';
|
||||||
import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview, TenantPackageSummary } from '../api';
|
import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview, TenantPackageSummary, updateEvent } from '../api';
|
||||||
import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
|
import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
|
||||||
import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
||||||
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
||||||
@@ -23,6 +23,7 @@ import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from
|
|||||||
import { collectPackageFeatures, formatPackageLimit, getPackageFeatureLabel, getPackageLimitEntries } from './lib/packageSummary';
|
import { collectPackageFeatures, formatPackageLimit, getPackageFeatureLabel, getPackageLimitEntries } from './lib/packageSummary';
|
||||||
import { trackOnboarding } from '../api';
|
import { trackOnboarding } from '../api';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
||||||
import { isPastEvent } from './eventDate';
|
import { isPastEvent } from './eventDate';
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ export default function MobileDashboardPage() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { slug: slugParam } = useParams<{ slug?: string }>();
|
const { slug: slugParam } = useParams<{ slug?: string }>();
|
||||||
const { t, i18n } = useTranslation('management');
|
const { t, i18n } = useTranslation('management');
|
||||||
const { events, activeEvent, hasEvents, hasMultipleEvents, isLoading, selectEvent } = useEventContext();
|
const { events, activeEvent, hasEvents, hasMultipleEvents, isLoading, selectEvent, refetch } = useEventContext();
|
||||||
const { status, user } = useAuth();
|
const { status, user } = useAuth();
|
||||||
const isMember = user?.role === 'member';
|
const isMember = user?.role === 'member';
|
||||||
const memberPermissions = React.useMemo(() => {
|
const memberPermissions = React.useMemo(() => {
|
||||||
@@ -64,6 +65,10 @@ export default function MobileDashboardPage() {
|
|||||||
() => allowPermission(memberPermissions, 'events:manage'),
|
() => allowPermission(memberPermissions, 'events:manage'),
|
||||||
[memberPermissions]
|
[memberPermissions]
|
||||||
);
|
);
|
||||||
|
const canManageTasks = React.useMemo(
|
||||||
|
() => allowPermission(memberPermissions, 'tasks:manage'),
|
||||||
|
[memberPermissions]
|
||||||
|
);
|
||||||
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
|
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
|
||||||
const [fallbackLoading, setFallbackLoading] = React.useState(false);
|
const [fallbackLoading, setFallbackLoading] = React.useState(false);
|
||||||
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
|
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
|
||||||
@@ -72,6 +77,8 @@ export default function MobileDashboardPage() {
|
|||||||
const [summaryOpen, setSummaryOpen] = React.useState(false);
|
const [summaryOpen, setSummaryOpen] = React.useState(false);
|
||||||
const [summarySeenOverride, setSummarySeenOverride] = React.useState<number | null>(null);
|
const [summarySeenOverride, setSummarySeenOverride] = React.useState<number | null>(null);
|
||||||
const [eventSwitcherOpen, setEventSwitcherOpen] = React.useState(false);
|
const [eventSwitcherOpen, setEventSwitcherOpen] = React.useState(false);
|
||||||
|
const [taskDecisionOpen, setTaskDecisionOpen] = React.useState(false);
|
||||||
|
const [taskDecisionBusy, setTaskDecisionBusy] = React.useState(false);
|
||||||
const onboardingTrackedRef = React.useRef(false);
|
const onboardingTrackedRef = React.useRef(false);
|
||||||
const installPrompt = useInstallPrompt();
|
const installPrompt = useInstallPrompt();
|
||||||
const pushState = useAdminPushSubscription();
|
const pushState = useAdminPushSubscription();
|
||||||
@@ -90,6 +97,8 @@ export default function MobileDashboardPage() {
|
|||||||
});
|
});
|
||||||
const tasksEnabled =
|
const tasksEnabled =
|
||||||
resolveEngagementMode(activeEvent ?? undefined) !== 'photo_only';
|
resolveEngagementMode(activeEvent ?? undefined) !== 'photo_only';
|
||||||
|
const tasksNeedSetup = tasksEnabled && (activeEvent?.tasks_count ?? 0) === 0;
|
||||||
|
const taskDecisionKey = activeEvent ? `tasksDecisionPrompt:${activeEvent.id}` : null;
|
||||||
|
|
||||||
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||||
const { data: dashboardEvents } = useQuery<TenantEvent[]>({
|
const { data: dashboardEvents } = useQuery<TenantEvent[]>({
|
||||||
@@ -114,6 +123,24 @@ export default function MobileDashboardPage() {
|
|||||||
const tourTargetSlug = activeEvent?.slug ?? effectiveEvents[0]?.slug ?? null;
|
const tourTargetSlug = activeEvent?.slug ?? effectiveEvents[0]?.slug ?? null;
|
||||||
const tourStepKeys = React.useMemo(() => resolveTourStepKeys(effectiveHasEvents), [effectiveHasEvents]);
|
const tourStepKeys = React.useMemo(() => resolveTourStepKeys(effectiveHasEvents), [effectiveHasEvents]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isMember || !taskDecisionKey || !tasksNeedSetup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const status = window.sessionStorage.getItem(taskDecisionKey);
|
||||||
|
if (status === 'pending') {
|
||||||
|
setTaskDecisionOpen(true);
|
||||||
|
window.sessionStorage.setItem(taskDecisionKey, 'shown');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore storage exceptions
|
||||||
|
}
|
||||||
|
}, [isMember, taskDecisionKey, tasksNeedSetup]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!slugParam || slugParam === activeEvent?.slug) {
|
if (!slugParam || slugParam === activeEvent?.slug) {
|
||||||
return;
|
return;
|
||||||
@@ -374,6 +401,17 @@ export default function MobileDashboardPage() {
|
|||||||
}
|
}
|
||||||
}, [activePackage?.id]);
|
}, [activePackage?.id]);
|
||||||
|
|
||||||
|
const markTaskDecisionDismissed = React.useCallback(() => {
|
||||||
|
if (!taskDecisionKey || typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(taskDecisionKey, 'dismissed');
|
||||||
|
} catch {
|
||||||
|
// ignore storage exceptions
|
||||||
|
}
|
||||||
|
}, [taskDecisionKey]);
|
||||||
|
|
||||||
const packageSummarySheet = activePackage ? (
|
const packageSummarySheet = activePackage ? (
|
||||||
<PackageSummarySheet
|
<PackageSummarySheet
|
||||||
open={summaryOpen}
|
open={summaryOpen}
|
||||||
@@ -393,6 +431,43 @@ export default function MobileDashboardPage() {
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
const taskDecisionSheet = activeEvent && tasksNeedSetup ? (
|
||||||
|
<TaskDecisionSheet
|
||||||
|
open={taskDecisionOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setTaskDecisionOpen(false);
|
||||||
|
markTaskDecisionDismissed();
|
||||||
|
}}
|
||||||
|
onManageTasks={() => {
|
||||||
|
markTaskDecisionDismissed();
|
||||||
|
setTaskDecisionOpen(false);
|
||||||
|
if (activeEvent.slug) {
|
||||||
|
navigate(adminPath(`/mobile/events/${activeEvent.slug}/tasks`));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDisableTasks={async () => {
|
||||||
|
if (!activeEvent.slug || taskDecisionBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTaskDecisionBusy(true);
|
||||||
|
try {
|
||||||
|
await updateEvent(activeEvent.slug, { settings: { engagement_mode: 'photo_only' } });
|
||||||
|
void refetch();
|
||||||
|
toast.success(t('mobileDashboard.taskDecision.disabledToast', 'Tasks disabled for this event.'));
|
||||||
|
markTaskDecisionDismissed();
|
||||||
|
setTaskDecisionOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('mobileDashboard.taskDecision.disableError', 'Could not disable tasks.'));
|
||||||
|
} finally {
|
||||||
|
setTaskDecisionBusy(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
busy={taskDecisionBusy}
|
||||||
|
canDisable={canManageEvents}
|
||||||
|
canManageTasks={canManageTasks}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
const showPackageSummaryBanner =
|
const showPackageSummaryBanner =
|
||||||
Boolean(activePackage && summarySeenPackageId === activePackage.id) &&
|
Boolean(activePackage && summarySeenPackageId === activePackage.id) &&
|
||||||
!summaryOpen &&
|
!summaryOpen &&
|
||||||
@@ -428,6 +503,7 @@ export default function MobileDashboardPage() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
{tourSheet}
|
{tourSheet}
|
||||||
{packageSummarySheet}
|
{packageSummarySheet}
|
||||||
|
{taskDecisionSheet}
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -450,6 +526,7 @@ export default function MobileDashboardPage() {
|
|||||||
/>
|
/>
|
||||||
{tourSheet}
|
{tourSheet}
|
||||||
{packageSummarySheet}
|
{packageSummarySheet}
|
||||||
|
{taskDecisionSheet}
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -470,6 +547,7 @@ export default function MobileDashboardPage() {
|
|||||||
<EventPickerList events={effectiveEvents} locale={locale} navigateOnSelect={false} />
|
<EventPickerList events={effectiveEvents} locale={locale} navigateOnSelect={false} />
|
||||||
{tourSheet}
|
{tourSheet}
|
||||||
{packageSummarySheet}
|
{packageSummarySheet}
|
||||||
|
{taskDecisionSheet}
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -521,6 +599,7 @@ export default function MobileDashboardPage() {
|
|||||||
/>
|
/>
|
||||||
{tourSheet}
|
{tourSheet}
|
||||||
{packageSummarySheet}
|
{packageSummarySheet}
|
||||||
|
{taskDecisionSheet}
|
||||||
<EventSwitcherSheet
|
<EventSwitcherSheet
|
||||||
open={eventSwitcherOpen}
|
open={eventSwitcherOpen}
|
||||||
onClose={() => setEventSwitcherOpen(false)}
|
onClose={() => setEventSwitcherOpen(false)}
|
||||||
@@ -654,6 +733,77 @@ function PackageSummarySheet({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TaskDecisionSheet({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onManageTasks,
|
||||||
|
onDisableTasks,
|
||||||
|
busy,
|
||||||
|
canDisable,
|
||||||
|
canManageTasks,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onManageTasks: () => void;
|
||||||
|
onDisableTasks: () => void;
|
||||||
|
busy: boolean;
|
||||||
|
canDisable: boolean;
|
||||||
|
canManageTasks: boolean;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation('management');
|
||||||
|
const { textStrong, muted, border, surface } = useAdminTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileSheet open={open} title={t('mobileDashboard.taskDecision.title', 'Set up tasks?')} onClose={onClose}>
|
||||||
|
<YStack space="$3">
|
||||||
|
<Text fontSize="$sm" color={muted}>
|
||||||
|
{t(
|
||||||
|
'mobileDashboard.taskDecision.body',
|
||||||
|
'Your event is live with tasks enabled, but no tasks are assigned yet. Choose to add tasks now or disable tasks for this event.'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||||
|
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||||
|
{t('mobileDashboard.taskDecision.promptTitle', 'Next step')}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{t(
|
||||||
|
'mobileDashboard.taskDecision.promptBody',
|
||||||
|
'Guests only see missions when tasks are assigned.'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</MobileCard>
|
||||||
|
<XStack space="$2">
|
||||||
|
{canManageTasks ? (
|
||||||
|
<CTAButton
|
||||||
|
label={t('mobileDashboard.taskDecision.ctaManage', 'Add tasks')}
|
||||||
|
onPress={onManageTasks}
|
||||||
|
disabled={busy}
|
||||||
|
fullWidth={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{canDisable ? (
|
||||||
|
<CTAButton
|
||||||
|
label={t('mobileDashboard.taskDecision.ctaDisable', 'Disable tasks')}
|
||||||
|
tone="ghost"
|
||||||
|
onPress={onDisableTasks}
|
||||||
|
disabled={busy}
|
||||||
|
fullWidth={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<CTAButton
|
||||||
|
label={t('mobileDashboard.taskDecision.dismiss', 'Later')}
|
||||||
|
tone="ghost"
|
||||||
|
onPress={onClose}
|
||||||
|
disabled={busy}
|
||||||
|
fullWidth={false}
|
||||||
|
/>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</MobileSheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SummaryRow({ label, value }: { label: string; value: string }) {
|
function SummaryRow({ label, value }: { label: string; value: string }) {
|
||||||
const { textStrong, muted } = useAdminTheme();
|
const { textStrong, muted } = useAdminTheme();
|
||||||
return (
|
return (
|
||||||
@@ -1333,6 +1483,7 @@ function EventManagementGrid({
|
|||||||
const { textStrong, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
const { textStrong, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
||||||
const slug = event?.slug ?? null;
|
const slug = event?.slug ?? null;
|
||||||
const brandingAllowed = isBrandingAllowed(event ?? null);
|
const brandingAllowed = isBrandingAllowed(event ?? null);
|
||||||
|
const tasksNeedSetup = tasksEnabled && (event?.tasks_count ?? 0) === 0;
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1354,6 +1505,8 @@ function EventManagementGrid({
|
|||||||
? t('events.quick.tasks', 'Tasks & Checklists')
|
? t('events.quick.tasks', 'Tasks & Checklists')
|
||||||
: `${t('events.quick.tasks', 'Tasks & Checklists')} (${t('common:states.disabled', 'Disabled')})`,
|
: `${t('events.quick.tasks', 'Tasks & Checklists')} (${t('common:states.disabled', 'Disabled')})`,
|
||||||
color: ADMIN_ACTION_COLORS.tasks,
|
color: ADMIN_ACTION_COLORS.tasks,
|
||||||
|
note: tasksNeedSetup ? t('mobileDashboard.tasksSetupNote', 'Setup needed') : undefined,
|
||||||
|
noteTone: tasksNeedSetup ? 'warning' : 'muted',
|
||||||
requiredPermission: 'tasks:manage',
|
requiredPermission: 'tasks:manage',
|
||||||
onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/tasks`)) : undefined,
|
onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/tasks`)) : undefined,
|
||||||
disabled: !tasksEnabled || !slug,
|
disabled: !tasksEnabled || !slug,
|
||||||
@@ -1504,6 +1657,8 @@ function EventManagementGrid({
|
|||||||
icon={tile.icon}
|
icon={tile.icon}
|
||||||
label={tile.label}
|
label={tile.label}
|
||||||
color={tile.color}
|
color={tile.color}
|
||||||
|
note={tile.note}
|
||||||
|
noteTone={tile.noteTone}
|
||||||
onPress={tile.onPress}
|
onPress={tile.onPress}
|
||||||
disabled={tile.disabled}
|
disabled={tile.disabled}
|
||||||
variant="cluster"
|
variant="cluster"
|
||||||
|
|||||||
@@ -263,6 +263,13 @@ export default function MobileEventFormPage() {
|
|||||||
},
|
},
|
||||||
} as Parameters<typeof createEvent>[0];
|
} as Parameters<typeof createEvent>[0];
|
||||||
const { event } = await createEvent(payload);
|
const { event } = await createEvent(payload);
|
||||||
|
if (typeof window !== 'undefined' && form.tasksEnabled) {
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(`tasksDecisionPrompt:${event.id}`, 'pending');
|
||||||
|
} catch {
|
||||||
|
// ignore storage exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
selectEvent(event.slug);
|
selectEvent(event.slug);
|
||||||
void queryClient.invalidateQueries({ queryKey: ['tenant-events'] });
|
void queryClient.invalidateQueries({ queryKey: ['tenant-events'] });
|
||||||
void refetch();
|
void refetch();
|
||||||
@@ -314,6 +321,13 @@ export default function MobileEventFormPage() {
|
|||||||
...pendingPayload,
|
...pendingPayload,
|
||||||
accepted_waiver: consents.acceptedWaiver,
|
accepted_waiver: consents.acceptedWaiver,
|
||||||
});
|
});
|
||||||
|
if (typeof window !== 'undefined' && form.tasksEnabled) {
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(`tasksDecisionPrompt:${event.id}`, 'pending');
|
||||||
|
} catch {
|
||||||
|
// ignore storage exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
selectEvent(event.slug);
|
selectEvent(event.slug);
|
||||||
void queryClient.invalidateQueries({ queryKey: ['tenant-events'] });
|
void queryClient.invalidateQueries({ queryKey: ['tenant-events'] });
|
||||||
void refetch();
|
void refetch();
|
||||||
|
|||||||
@@ -149,7 +149,12 @@ vi.mock('../components/Primitives', () => ({
|
|||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
ActionTile: ({ label }: { label: string }) => <div>{label}</div>,
|
ActionTile: ({ label, note }: { label: string; note?: string }) => (
|
||||||
|
<div>
|
||||||
|
<span>{label}</span>
|
||||||
|
{note ? <span>{note}</span> : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
SkeletonCard: () => <div>Loading...</div>,
|
SkeletonCard: () => <div>Loading...</div>,
|
||||||
}));
|
}));
|
||||||
@@ -242,7 +247,25 @@ describe('MobileDashboardPage', () => {
|
|||||||
|
|
||||||
fixtures.activePackage.package_type = 'reseller';
|
fixtures.activePackage.package_type = 'reseller';
|
||||||
fixtures.activePackage.remaining_events = 3;
|
fixtures.activePackage.remaining_events = 3;
|
||||||
|
fixtures.event.tasks_count = 4;
|
||||||
|
fixtures.event.engagement_mode = undefined;
|
||||||
navigateMock.mockClear();
|
navigateMock.mockClear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a tasks setup nudge and prompt when no tasks are assigned', () => {
|
||||||
|
fixtures.event.tasks_count = 0;
|
||||||
|
fixtures.event.engagement_mode = 'tasks';
|
||||||
|
window.sessionStorage.setItem(`tasksDecisionPrompt:${fixtures.event.id}`, 'pending');
|
||||||
|
|
||||||
|
render(<MobileDashboardPage />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Setup needed')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'Your event is live with tasks enabled, but no tasks are assigned yet. Choose to add tasks now or disable tasks for this event.'
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not redirect endcustomer packages without remaining event quota', () => {
|
it('does not redirect endcustomer packages without remaining event quota', () => {
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ export function SkeletonCard({ height = 80 }: { height?: number }) {
|
|||||||
export function ActionTile({
|
export function ActionTile({
|
||||||
icon: IconCmp,
|
icon: IconCmp,
|
||||||
label,
|
label,
|
||||||
|
note,
|
||||||
|
noteTone = 'muted',
|
||||||
color,
|
color,
|
||||||
onPress,
|
onPress,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -199,13 +201,16 @@ export function ActionTile({
|
|||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<{ size?: number; color?: string }>;
|
icon: React.ComponentType<{ size?: number; color?: string }>;
|
||||||
label: string;
|
label: string;
|
||||||
|
note?: string;
|
||||||
|
noteTone?: 'warning' | 'muted';
|
||||||
color: string;
|
color: string;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
variant?: 'grid' | 'cluster';
|
variant?: 'grid' | 'cluster';
|
||||||
delayMs?: number;
|
delayMs?: number;
|
||||||
}) {
|
}) {
|
||||||
const { textStrong, glassSurface } = useAdminTheme();
|
const { textStrong, glassSurface, muted, warningText } = useAdminTheme();
|
||||||
|
const noteColor = noteTone === 'warning' ? warningText : muted;
|
||||||
const isCluster = variant === 'cluster';
|
const isCluster = variant === 'cluster';
|
||||||
const backgroundColor = withAlpha(color, 0.12);
|
const backgroundColor = withAlpha(color, 0.12);
|
||||||
const borderColor = withAlpha(color, 0.4);
|
const borderColor = withAlpha(color, 0.4);
|
||||||
@@ -254,6 +259,11 @@ export function ActionTile({
|
|||||||
<Text fontSize="$sm" fontWeight="800" color={textStrong} textAlign="center">
|
<Text fontSize="$sm" fontWeight="800" color={textStrong} textAlign="center">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
{note ? (
|
||||||
|
<Text fontSize="$xs" fontWeight="700" color={noteColor} textAlign="center">
|
||||||
|
{note}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</YStack>
|
</YStack>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user