216 lines
8.4 KiB
TypeScript
216 lines
8.4 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { CheckCircle2, Package as PackageIcon } from 'lucide-react';
|
|
import { YStack, XStack } from '@tamagui/stacks';
|
|
import { SizableText as Text } from '@tamagui/text';
|
|
import { OnboardingShell } from '../components/OnboardingShell';
|
|
import { MobileCard, CTAButton, PillBadge } from '../components/Primitives';
|
|
import { getPackages, getTenantPackagesOverview } from '../../api';
|
|
import { ADMIN_WELCOME_BASE_PATH, ADMIN_WELCOME_EVENT_PATH, ADMIN_WELCOME_PACKAGES_PATH, adminPath } from '../../constants';
|
|
import { getSelectedPackageId } from '../lib/onboardingSelection';
|
|
import { ADMIN_COLORS } from '../theme';
|
|
|
|
type SummaryPackage = {
|
|
id: number;
|
|
name: string;
|
|
max_photos: number | null;
|
|
max_guests: number | null;
|
|
gallery_days: number | null;
|
|
active: boolean;
|
|
remaining_events?: number | null;
|
|
};
|
|
|
|
export default function WelcomeSummaryPage() {
|
|
const navigate = useNavigate();
|
|
const { t } = useTranslation('onboarding');
|
|
const selectedId = getSelectedPackageId();
|
|
|
|
const { data: catalog, isLoading: catalogLoading } = useQuery({
|
|
queryKey: ['mobile', 'onboarding', 'packages-list'],
|
|
queryFn: () => getPackages('endcustomer'),
|
|
staleTime: 60_000,
|
|
});
|
|
|
|
const { data: overview, isLoading: overviewLoading } = useQuery({
|
|
queryKey: ['mobile', 'onboarding', 'packages-overview'],
|
|
queryFn: () => getTenantPackagesOverview({ force: true }),
|
|
staleTime: 60_000,
|
|
});
|
|
|
|
const selectedPackage = catalog?.find((pkg) => pkg.id === selectedId) ?? null;
|
|
const activePackage = overview?.activePackage ?? null;
|
|
const hasActivePackage =
|
|
Boolean(activePackage) || Boolean(overview?.packages?.some((pkg) => pkg.active));
|
|
|
|
const resolvedPackage: SummaryPackage | null = selectedPackage
|
|
? {
|
|
id: selectedPackage.id,
|
|
name: selectedPackage.name,
|
|
max_photos: selectedPackage.max_photos ?? null,
|
|
max_guests: selectedPackage.max_guests ?? null,
|
|
gallery_days: selectedPackage.gallery_days ?? null,
|
|
active: false,
|
|
}
|
|
: activePackage
|
|
? {
|
|
id: activePackage.id,
|
|
name: activePackage.package_name ?? 'Package',
|
|
max_photos: (activePackage.package_limits as any)?.max_photos ?? null,
|
|
max_guests: (activePackage.package_limits as any)?.max_guests ?? null,
|
|
gallery_days: (activePackage.package_limits as any)?.gallery_days ?? null,
|
|
active: true,
|
|
remaining_events: activePackage.remaining_events ?? null,
|
|
}
|
|
: null;
|
|
|
|
const loading = catalogLoading || overviewLoading;
|
|
const backTarget = selectedPackage ? ADMIN_WELCOME_PACKAGES_PATH : ADMIN_WELCOME_BASE_PATH;
|
|
|
|
return (
|
|
<OnboardingShell
|
|
eyebrow={t('summary.layout.eyebrow', 'Step 3')}
|
|
title={t('summary.layout.title', 'Order summary')}
|
|
subtitle={t('summary.layout.subtitle', 'Review package, price, and payment before proceeding to the event setup.')}
|
|
onBack={() => navigate(backTarget)}
|
|
onSkip={() => navigate(adminPath('/mobile/billing#packages'))}
|
|
skipLabel={t('summary.cta.billing.button', 'Go to billing')}
|
|
>
|
|
{loading ? (
|
|
<MobileCard>
|
|
<Text fontSize="$sm" color={ADMIN_COLORS.textMuted}>
|
|
{t('summary.state.loading', 'Checking available packages …')}
|
|
</Text>
|
|
</MobileCard>
|
|
) : !resolvedPackage ? (
|
|
<MobileCard>
|
|
<Text fontSize="$sm" fontWeight="800">
|
|
{t('summary.state.missingTitle', 'No package selected')}
|
|
</Text>
|
|
<Text fontSize="$sm" color={ADMIN_COLORS.textMuted}>
|
|
{t('summary.state.missingDescription', 'Select a package first or refresh if data changed.')}
|
|
</Text>
|
|
<CTAButton label={t('summary.footer.back', 'Back to package selection')} onPress={() => navigate(ADMIN_WELCOME_PACKAGES_PATH)} />
|
|
</MobileCard>
|
|
) : (
|
|
<MobileCard space="$3">
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<XStack alignItems="center" space="$2">
|
|
<XStack
|
|
width={36}
|
|
height={36}
|
|
borderRadius={12}
|
|
backgroundColor={ADMIN_COLORS.accentSoft}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
>
|
|
<PackageIcon size={18} color={ADMIN_COLORS.primary} />
|
|
</XStack>
|
|
<YStack>
|
|
<Text fontSize="$sm" fontWeight="800">
|
|
{resolvedPackage.name}
|
|
</Text>
|
|
<Text fontSize="$xs" color={ADMIN_COLORS.textMuted}>
|
|
{resolvedPackage.active
|
|
? t('summary.details.section.statusActive', 'Already purchased')
|
|
: t('summary.details.section.statusInactive', 'Not purchased yet')}
|
|
</Text>
|
|
</YStack>
|
|
</XStack>
|
|
<PillBadge tone={resolvedPackage.active ? 'success' : 'muted'}>
|
|
{resolvedPackage.active ? t('summary.details.section.statusActive', 'Already purchased') : t('packages.card.select', 'Select package')}
|
|
</PillBadge>
|
|
</XStack>
|
|
|
|
<YStack space="$2">
|
|
<SummaryRow
|
|
label={t('summary.details.section.photosTitle', 'Photos & gallery')}
|
|
value={t('summary.details.section.photosValue', {
|
|
count: resolvedPackage.max_photos ?? 0,
|
|
days: resolvedPackage.gallery_days ?? 0,
|
|
defaultValue: 'Unlimited photos for {{days}} days',
|
|
} as any) as string}
|
|
/>
|
|
<SummaryRow
|
|
label={t('summary.details.section.guestsTitle', 'Guests & team')}
|
|
value={t('summary.details.section.guestsValue', {
|
|
count: resolvedPackage.max_guests ?? 0,
|
|
defaultValue: 'Unlimited guests',
|
|
} as any) as string}
|
|
/>
|
|
{resolvedPackage.remaining_events !== undefined && resolvedPackage.remaining_events !== null ? (
|
|
<SummaryRow
|
|
label={t('summary.details.section.statusTitle', 'Status')}
|
|
value={t('summary.details.section.statusActive', 'Already purchased')}
|
|
/>
|
|
) : null}
|
|
</YStack>
|
|
|
|
{resolvedPackage.active ? (
|
|
<XStack alignItems="center" space="$2">
|
|
<CheckCircle2 size={18} color={ADMIN_COLORS.success} />
|
|
<Text fontSize="$sm" color={ADMIN_COLORS.success} fontWeight="700">
|
|
{t('summary.details.section.statusActive', 'Already purchased')}
|
|
</Text>
|
|
</XStack>
|
|
) : null}
|
|
</MobileCard>
|
|
)}
|
|
|
|
<MobileCard space="$2">
|
|
<Text fontSize="$sm" fontWeight="800">
|
|
{t('summary.nextStepsTitle', 'Next steps')}
|
|
</Text>
|
|
<YStack space="$1">
|
|
{(t('summary.nextSteps', {
|
|
returnObjects: true,
|
|
defaultValue: [
|
|
'Optional: finish billing via Paddle inside the billing area.',
|
|
'Complete the event setup and configure tasks, team, and gallery.',
|
|
'Check your event slots before go-live and share your guest link.',
|
|
],
|
|
}) as string[]).map((item) => (
|
|
<XStack key={item} space="$2">
|
|
<Text fontSize="$xs" color={ADMIN_COLORS.textMuted}>
|
|
•
|
|
</Text>
|
|
<Text fontSize="$sm" color={ADMIN_COLORS.textMuted}>
|
|
{item}
|
|
</Text>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
</MobileCard>
|
|
|
|
<XStack space="$2">
|
|
<CTAButton
|
|
label={t('summary.cta.billing.button', 'Go to billing')}
|
|
tone="ghost"
|
|
onPress={() => navigate(adminPath('/mobile/billing#packages'))}
|
|
fullWidth={false}
|
|
/>
|
|
<CTAButton
|
|
label={t('summary.cta.setup.button', 'Continue to setup')}
|
|
onPress={() => navigate(ADMIN_WELCOME_EVENT_PATH)}
|
|
disabled={!resolvedPackage && !hasActivePackage}
|
|
fullWidth={false}
|
|
/>
|
|
</XStack>
|
|
</OnboardingShell>
|
|
);
|
|
}
|
|
|
|
function SummaryRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<Text fontSize="$sm" color={ADMIN_COLORS.text}>
|
|
{label}
|
|
</Text>
|
|
<Text fontSize="$sm" color={ADMIN_COLORS.textMuted}>
|
|
{value}
|
|
</Text>
|
|
</XStack>
|
|
);
|
|
}
|