Admin package summary sheet
This commit is contained in:
@@ -11,7 +11,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 { getEventStats, EventStats, TenantEvent, getEvents } from '../api';
|
||||
import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview } from '../api';
|
||||
import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
|
||||
import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
||||
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
||||
@@ -39,6 +39,8 @@ export default function MobileDashboardPage() {
|
||||
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
|
||||
const [tourOpen, setTourOpen] = React.useState(false);
|
||||
const [tourStep, setTourStep] = React.useState(0);
|
||||
const [summaryOpen, setSummaryOpen] = React.useState(false);
|
||||
const [summarySeenOverride, setSummarySeenOverride] = React.useState<number | null>(null);
|
||||
const onboardingTrackedRef = React.useRef(false);
|
||||
const installPrompt = useInstallPrompt();
|
||||
const pushState = useAdminPushSubscription();
|
||||
@@ -64,6 +66,16 @@ export default function MobileDashboardPage() {
|
||||
queryFn: () => getEvents({ force: true }),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const { data: onboardingStatus, isLoading: onboardingLoading } = useQuery({
|
||||
queryKey: ['mobile', 'onboarding', 'status'],
|
||||
queryFn: fetchOnboardingStatus,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const { data: packagesOverview, isLoading: packagesLoading, isError: packagesError } = useQuery({
|
||||
queryKey: ['mobile', 'onboarding', 'packages-overview'],
|
||||
queryFn: () => getTenantPackagesOverview({ force: true }),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const effectiveEvents = events.length ? events : dashboardEvents?.length ? dashboardEvents : fallbackEvents;
|
||||
const effectiveHasEvents = hasEvents || Boolean(dashboardEvents?.length) || fallbackEvents.length > 0;
|
||||
const effectiveMultiple =
|
||||
@@ -115,6 +127,41 @@ export default function MobileDashboardPage() {
|
||||
setTourOpen(true);
|
||||
}, [forceTour, location.pathname, navigate]);
|
||||
|
||||
const activePackage =
|
||||
packagesOverview?.activePackage ?? packagesOverview?.packages?.find((pkg) => pkg.active) ?? null;
|
||||
const remainingEvents = activePackage?.remaining_events ?? null;
|
||||
const summarySeenPackageId =
|
||||
summarySeenOverride ?? onboardingStatus?.steps?.summary_seen_package_id ?? null;
|
||||
const hasSummaryPackage =
|
||||
Boolean(activePackage?.id && activePackage.id !== summarySeenPackageId);
|
||||
const shouldRedirectToBilling =
|
||||
!packagesLoading &&
|
||||
!packagesError &&
|
||||
!effectiveHasEvents &&
|
||||
(activePackage === null || (remainingEvents !== null && remainingEvents <= 0));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (packagesLoading || !shouldRedirectToBilling) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(adminPath('/mobile/billing#packages'), { replace: true });
|
||||
}, [navigate, packagesLoading, shouldRedirectToBilling]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (packagesLoading || packagesError || onboardingLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldRedirectToBilling) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSummaryPackage) {
|
||||
setSummaryOpen(true);
|
||||
}
|
||||
}, [hasSummaryPackage, onboardingLoading, packagesLoading, shouldRedirectToBilling]);
|
||||
|
||||
const markTourSeen = React.useCallback(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -278,6 +325,32 @@ export default function MobileDashboardPage() {
|
||||
</MobileSheet>
|
||||
) : null;
|
||||
|
||||
const handleSummaryClose = React.useCallback(() => {
|
||||
setSummaryOpen(false);
|
||||
if (activePackage?.id) {
|
||||
setSummarySeenOverride(activePackage.id);
|
||||
void trackOnboarding('summary_seen', { package_id: activePackage.id });
|
||||
}
|
||||
}, [activePackage?.id]);
|
||||
|
||||
const packageSummarySheet = activePackage ? (
|
||||
<PackageSummarySheet
|
||||
open={summaryOpen}
|
||||
onClose={handleSummaryClose}
|
||||
onContinue={() => {
|
||||
handleSummaryClose();
|
||||
navigate(adminPath('/mobile/events/new'));
|
||||
}}
|
||||
packageName={activePackage.package_name ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary')}
|
||||
remainingEvents={remainingEvents}
|
||||
purchasedAt={activePackage.purchased_at}
|
||||
expiresAt={activePackage.expires_at}
|
||||
limits={activePackage.package_limits ?? null}
|
||||
features={activePackage.features ?? null}
|
||||
locale={locale}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (events.length || isLoading || fallbackLoading || fallbackAttempted) {
|
||||
return;
|
||||
@@ -306,6 +379,7 @@ export default function MobileDashboardPage() {
|
||||
))}
|
||||
</YStack>
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
@@ -320,6 +394,7 @@ export default function MobileDashboardPage() {
|
||||
onOpenSettings={() => navigate(adminPath('/mobile/settings'))}
|
||||
/>
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
@@ -333,6 +408,7 @@ export default function MobileDashboardPage() {
|
||||
>
|
||||
<EventPickerList events={effectiveEvents} locale={locale} text={text} muted={muted} border={border} />
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
@@ -374,10 +450,125 @@ export default function MobileDashboardPage() {
|
||||
|
||||
<AlertsAndHints event={activeEvent} stats={stats} tasksEnabled={tasksEnabled} />
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
function PackageSummarySheet({
|
||||
open,
|
||||
onClose,
|
||||
onContinue,
|
||||
packageName,
|
||||
remainingEvents,
|
||||
purchasedAt,
|
||||
expiresAt,
|
||||
limits,
|
||||
features,
|
||||
locale,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onContinue: () => void;
|
||||
packageName: string;
|
||||
remainingEvents: number | null | undefined;
|
||||
purchasedAt: string | null | undefined;
|
||||
expiresAt: string | null | undefined;
|
||||
limits: Record<string, unknown> | null;
|
||||
features: string[] | null;
|
||||
locale: string;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme();
|
||||
const text = textStrong;
|
||||
const maxPhotos = (limits as Record<string, number | null> | null)?.max_photos ?? null;
|
||||
const maxGuests = (limits as Record<string, number | null> | null)?.max_guests ?? null;
|
||||
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
|
||||
const hasFeatures = Array.isArray(features) && features.length > 0;
|
||||
|
||||
const formatLimit = (value: number | null) =>
|
||||
value === null || value === undefined
|
||||
? t('mobileDashboard.packageSummary.unlimited', 'Unlimited')
|
||||
: String(value);
|
||||
|
||||
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
|
||||
|
||||
return (
|
||||
<MobileSheet open={open} title={t('mobileDashboard.packageSummary.title', 'Your package summary')} onClose={onClose}>
|
||||
<YStack space="$3">
|
||||
<MobileCard space="$2.5" borderColor={border} backgroundColor={surface}>
|
||||
<YStack space="$1.5">
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{packageName}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('mobileDashboard.packageSummary.subtitle', 'Here is a quick overview of your active package.')}
|
||||
</Text>
|
||||
</YStack>
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatLimit(maxPhotos)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatLimit(maxGuests)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatLimit(galleryDays)} />
|
||||
<SummaryRow
|
||||
label={t('mobileDashboard.packageSummary.remaining', 'Remaining events')}
|
||||
value={remainingEvents === null || remainingEvents === undefined ? t('mobileDashboard.packageSummary.unlimited', 'Unlimited') : String(remainingEvents)}
|
||||
/>
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.purchased', 'Purchased')} value={formatDate(purchasedAt)} />
|
||||
{expiresAt ? (
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.expires', 'Expires')} value={formatDate(expiresAt)} />
|
||||
) : null}
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
{hasFeatures ? (
|
||||
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
|
||||
</Text>
|
||||
<YStack space="$1.5" marginTop="$2">
|
||||
{features?.map((feature) => (
|
||||
<XStack key={feature} alignItems="center" space="$2">
|
||||
<XStack width={24} height={24} borderRadius={8} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
|
||||
<Sparkles size={14} color={primary} />
|
||||
</XStack>
|
||||
<Text fontSize="$xs" color={text}>
|
||||
{t(`mobileDashboard.packageSummary.feature.${feature}`, feature)}
|
||||
</Text>
|
||||
</XStack>
|
||||
))}
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('mobileDashboard.packageSummary.hint', 'You can revisit billing any time to review or upgrade your package.')}
|
||||
</Text>
|
||||
</MobileCard>
|
||||
|
||||
<XStack space="$2">
|
||||
<CTAButton label={t('mobileDashboard.packageSummary.continue', 'Continue to event setup')} onPress={onContinue} fullWidth={false} />
|
||||
<CTAButton label={t('mobileDashboard.packageSummary.dismiss', 'Close')} tone="ghost" onPress={onClose} fullWidth={false} />
|
||||
</XStack>
|
||||
</YStack>
|
||||
</MobileSheet>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryRow({ label, value }: { label: string; value: string }) {
|
||||
const { textStrong, muted } = useAdminTheme();
|
||||
return (
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$xs" color={textStrong}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{value}
|
||||
</Text>
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) {
|
||||
const { t } = useTranslation('management');
|
||||
const { textStrong, muted, border, primary, accentSoft } = useAdminTheme();
|
||||
|
||||
Reference in New Issue
Block a user