Fix demo task readiness and gate event creation
This commit is contained in:
@@ -16,11 +16,13 @@ export function EventSwitcherSheet({
|
||||
onClose,
|
||||
events,
|
||||
activeSlug,
|
||||
canCreateEvent,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
events: TenantEvent[];
|
||||
activeSlug: string | null;
|
||||
canCreateEvent: boolean;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(['management', 'mobile']);
|
||||
const navigate = useNavigate();
|
||||
@@ -73,13 +75,15 @@ export function EventSwitcherSheet({
|
||||
);
|
||||
})}
|
||||
|
||||
<Pressable onPress={() => { onClose(); navigate(adminPath('/mobile/events/new')); }}>
|
||||
<XStack padding="$3" justifyContent="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.primary}>
|
||||
{t('mobile:header.createEvent', 'Create Event')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
{canCreateEvent ? (
|
||||
<Pressable onPress={() => { onClose(); navigate(adminPath('/mobile/events/new')); }}>
|
||||
<XStack padding="$3" justifyContent="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.primary}>
|
||||
{t('mobile:header.createEvent', 'Create Event')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</YStack>
|
||||
</MobileSheet>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChevronLeft, Bell, QrCode, ChevronsUpDown, Search } from 'lucide-react';
|
||||
import { YStack, XStack, SizableText as Text, Image } from 'tamagui';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
@@ -12,7 +13,7 @@ import { MobileCard, CTAButton } from './Primitives';
|
||||
import { useNotificationsBadge } from '../hooks/useNotificationsBadge';
|
||||
import { useOnlineStatus } from '../hooks/useOnlineStatus';
|
||||
import { resolveEventDisplayName } from '../../lib/events';
|
||||
import { TenantEvent, getEvents } from '../../api';
|
||||
import { TenantEvent, getEvents, getTenantPackagesOverview, type TenantPackageSummary } from '../../api';
|
||||
import { setTabHistory } from '../lib/tabHistory';
|
||||
import { loadPhotoQueue } from '../lib/photoModerationQueue';
|
||||
import { countQueuedPhotoActions } from '../lib/queueStatus';
|
||||
@@ -31,6 +32,59 @@ type MobileShellProps = {
|
||||
headerActions?: React.ReactNode;
|
||||
};
|
||||
|
||||
function collectActivePackages(
|
||||
overview: { packages: TenantPackageSummary[]; activePackage: TenantPackageSummary | null } | null
|
||||
): TenantPackageSummary[] {
|
||||
if (!overview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packages = overview.packages ?? [];
|
||||
const activePackage = overview.activePackage;
|
||||
const activeList = packages.filter((pkg) => pkg.active);
|
||||
|
||||
if (activePackage && !activeList.some((pkg) => pkg.id === activePackage.id) && activePackage.active) {
|
||||
return [activePackage, ...activeList];
|
||||
}
|
||||
|
||||
return activeList;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
|
||||
if (pkg.package_type !== 'reseller') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const remaining = toNumber(pkg.remaining_events);
|
||||
if (remaining !== null) {
|
||||
return remaining > 0;
|
||||
}
|
||||
|
||||
const limits = (pkg.package_limits ?? {}) as Record<string, unknown>;
|
||||
const limitMaxEvents = toNumber(limits.max_events_per_year);
|
||||
if (limitMaxEvents === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const usedEvents = toNumber(pkg.used_events) ?? 0;
|
||||
return limitMaxEvents > usedEvents;
|
||||
}
|
||||
|
||||
export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) {
|
||||
const { events, activeEvent, selectEvent } = useEventContext();
|
||||
const { user } = useAuth();
|
||||
@@ -40,6 +94,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
const { t } = useTranslation('mobile');
|
||||
const { count: notificationCount } = useNotificationsBadge();
|
||||
const online = useOnlineStatus();
|
||||
const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin';
|
||||
|
||||
useDocumentTitle(title);
|
||||
|
||||
@@ -144,6 +199,29 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
};
|
||||
const showQr = Boolean(effectiveActive?.slug) && allowPermission('join-tokens:manage');
|
||||
|
||||
const { data: packagesOverview, isLoading: packagesLoading } = useQuery({
|
||||
queryKey: ['mobile', 'header', 'packages-overview'],
|
||||
enabled: !isMember && !isSuperAdmin,
|
||||
queryFn: () => getTenantPackagesOverview({ force: true }),
|
||||
});
|
||||
|
||||
const canCreateEvent = React.useMemo(() => {
|
||||
if (isMember) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packagesLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activePackages = collectActivePackages(packagesOverview ?? null);
|
||||
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
|
||||
}, [isMember, isSuperAdmin, packagesLoading, packagesOverview]);
|
||||
|
||||
// --- CONTEXT PILL ---
|
||||
const EventContextPill = () => {
|
||||
if (!effectiveActive || isEventsIndex || isCompactHeader) {
|
||||
@@ -387,10 +465,11 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
<BottomNav active={activeTab} onNavigate={go} />
|
||||
|
||||
<EventSwitcherSheet
|
||||
open={switcherOpen}
|
||||
onClose={() => setSwitcherOpen(false)}
|
||||
events={effectiveEvents}
|
||||
activeSlug={effectiveActive?.slug ?? null}
|
||||
open={switcherOpen}
|
||||
onClose={() => setSwitcherOpen(false)}
|
||||
events={effectiveEvents}
|
||||
activeSlug={effectiveActive?.slug ?? null}
|
||||
canCreateEvent={canCreateEvent}
|
||||
/>
|
||||
<UserMenuSheet
|
||||
open={userMenuOpen}
|
||||
|
||||
@@ -112,6 +112,11 @@ vi.mock('../../hooks/useOnlineStatus', () => ({
|
||||
|
||||
vi.mock('../../../api', () => ({
|
||||
getEvents: vi.fn().mockResolvedValue([]),
|
||||
getTenantPackagesOverview: vi.fn().mockResolvedValue({ packages: [], activePackage: null }),
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: () => ({ data: { packages: [], activePackage: null }, isLoading: false }),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/tabHistory', () => ({
|
||||
|
||||
Reference in New Issue
Block a user