|
|
|
|
@@ -14,7 +14,7 @@ import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge, SkeletonCard } f
|
|
|
|
|
import { MobileSheet } from './components/Sheet';
|
|
|
|
|
import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants';
|
|
|
|
|
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 { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
|
|
|
|
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 { trackOnboarding } from '../api';
|
|
|
|
|
import { useAuth } from '../auth/context';
|
|
|
|
|
import toast from 'react-hot-toast';
|
|
|
|
|
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
|
|
|
|
import { isPastEvent } from './eventDate';
|
|
|
|
|
|
|
|
|
|
@@ -51,7 +52,7 @@ export default function MobileDashboardPage() {
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const { slug: slugParam } = useParams<{ slug?: string }>();
|
|
|
|
|
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 isMember = user?.role === 'member';
|
|
|
|
|
const memberPermissions = React.useMemo(() => {
|
|
|
|
|
@@ -64,6 +65,10 @@ export default function MobileDashboardPage() {
|
|
|
|
|
() => allowPermission(memberPermissions, 'events:manage'),
|
|
|
|
|
[memberPermissions]
|
|
|
|
|
);
|
|
|
|
|
const canManageTasks = React.useMemo(
|
|
|
|
|
() => allowPermission(memberPermissions, 'tasks:manage'),
|
|
|
|
|
[memberPermissions]
|
|
|
|
|
);
|
|
|
|
|
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
|
|
|
|
|
const [fallbackLoading, setFallbackLoading] = React.useState(false);
|
|
|
|
|
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
|
|
|
|
|
@@ -72,6 +77,8 @@ export default function MobileDashboardPage() {
|
|
|
|
|
const [summaryOpen, setSummaryOpen] = React.useState(false);
|
|
|
|
|
const [summarySeenOverride, setSummarySeenOverride] = React.useState<number | null>(null);
|
|
|
|
|
const [eventSwitcherOpen, setEventSwitcherOpen] = React.useState(false);
|
|
|
|
|
const [taskDecisionOpen, setTaskDecisionOpen] = React.useState(false);
|
|
|
|
|
const [taskDecisionBusy, setTaskDecisionBusy] = React.useState(false);
|
|
|
|
|
const onboardingTrackedRef = React.useRef(false);
|
|
|
|
|
const installPrompt = useInstallPrompt();
|
|
|
|
|
const pushState = useAdminPushSubscription();
|
|
|
|
|
@@ -90,6 +97,8 @@ export default function MobileDashboardPage() {
|
|
|
|
|
});
|
|
|
|
|
const tasksEnabled =
|
|
|
|
|
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 { data: dashboardEvents } = useQuery<TenantEvent[]>({
|
|
|
|
|
@@ -114,6 +123,24 @@ export default function MobileDashboardPage() {
|
|
|
|
|
const tourTargetSlug = activeEvent?.slug ?? effectiveEvents[0]?.slug ?? null;
|
|
|
|
|
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(() => {
|
|
|
|
|
if (!slugParam || slugParam === activeEvent?.slug) {
|
|
|
|
|
return;
|
|
|
|
|
@@ -374,6 +401,17 @@ export default function MobileDashboardPage() {
|
|
|
|
|
}
|
|
|
|
|
}, [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 ? (
|
|
|
|
|
<PackageSummarySheet
|
|
|
|
|
open={summaryOpen}
|
|
|
|
|
@@ -393,6 +431,43 @@ export default function MobileDashboardPage() {
|
|
|
|
|
locale={locale}
|
|
|
|
|
/>
|
|
|
|
|
) : 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 =
|
|
|
|
|
Boolean(activePackage && summarySeenPackageId === activePackage.id) &&
|
|
|
|
|
!summaryOpen &&
|
|
|
|
|
@@ -428,6 +503,7 @@ export default function MobileDashboardPage() {
|
|
|
|
|
</YStack>
|
|
|
|
|
{tourSheet}
|
|
|
|
|
{packageSummarySheet}
|
|
|
|
|
{taskDecisionSheet}
|
|
|
|
|
</MobileShell>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@@ -450,6 +526,7 @@ export default function MobileDashboardPage() {
|
|
|
|
|
/>
|
|
|
|
|
{tourSheet}
|
|
|
|
|
{packageSummarySheet}
|
|
|
|
|
{taskDecisionSheet}
|
|
|
|
|
</MobileShell>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@@ -470,6 +547,7 @@ export default function MobileDashboardPage() {
|
|
|
|
|
<EventPickerList events={effectiveEvents} locale={locale} navigateOnSelect={false} />
|
|
|
|
|
{tourSheet}
|
|
|
|
|
{packageSummarySheet}
|
|
|
|
|
{taskDecisionSheet}
|
|
|
|
|
</MobileShell>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@@ -521,6 +599,7 @@ export default function MobileDashboardPage() {
|
|
|
|
|
/>
|
|
|
|
|
{tourSheet}
|
|
|
|
|
{packageSummarySheet}
|
|
|
|
|
{taskDecisionSheet}
|
|
|
|
|
<EventSwitcherSheet
|
|
|
|
|
open={eventSwitcherOpen}
|
|
|
|
|
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 }) {
|
|
|
|
|
const { textStrong, muted } = useAdminTheme();
|
|
|
|
|
return (
|
|
|
|
|
@@ -1333,6 +1483,7 @@ function EventManagementGrid({
|
|
|
|
|
const { textStrong, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
|
|
|
|
const slug = event?.slug ?? null;
|
|
|
|
|
const brandingAllowed = isBrandingAllowed(event ?? null);
|
|
|
|
|
const tasksNeedSetup = tasksEnabled && (event?.tasks_count ?? 0) === 0;
|
|
|
|
|
|
|
|
|
|
if (!event) {
|
|
|
|
|
return null;
|
|
|
|
|
@@ -1354,6 +1505,8 @@ function EventManagementGrid({
|
|
|
|
|
? t('events.quick.tasks', 'Tasks & Checklists')
|
|
|
|
|
: `${t('events.quick.tasks', 'Tasks & Checklists')} (${t('common:states.disabled', 'Disabled')})`,
|
|
|
|
|
color: ADMIN_ACTION_COLORS.tasks,
|
|
|
|
|
note: tasksNeedSetup ? t('mobileDashboard.tasksSetupNote', 'Setup needed') : undefined,
|
|
|
|
|
noteTone: tasksNeedSetup ? 'warning' : 'muted',
|
|
|
|
|
requiredPermission: 'tasks:manage',
|
|
|
|
|
onPress: slug ? () => onNavigate(adminPath(`/mobile/events/${slug}/tasks`)) : undefined,
|
|
|
|
|
disabled: !tasksEnabled || !slug,
|
|
|
|
|
@@ -1504,6 +1657,8 @@ function EventManagementGrid({
|
|
|
|
|
icon={tile.icon}
|
|
|
|
|
label={tile.label}
|
|
|
|
|
color={tile.color}
|
|
|
|
|
note={tile.note}
|
|
|
|
|
noteTone={tile.noteTone}
|
|
|
|
|
onPress={tile.onPress}
|
|
|
|
|
disabled={tile.disabled}
|
|
|
|
|
variant="cluster"
|
|
|
|
|
|