- Brand/Theming: Marketing-Farb- und Typographievariablen in `resources/css/app.css` eingeführt, AdminLayout, Dashboardkarten und Onboarding-Komponenten entsprechend angepasst; Dokumentation (`docs/todo/tenant-admin-onboarding-fusion.md`, `docs/changes/...`) aktualisiert. - Checkout & Payments: Checkout-, PayPal-Controller und Tests für integrierte Stripe/PayPal-Flows sowie Paket-Billing-Abläufe überarbeitet; neue PayPal SDK-Factory und Admin-API-Helper (`resources/js/admin/api.ts`) schaffen Grundlage für Billing/Members/Tasks-Seiten. - DX & Tests: Neue Playwright/E2E-Struktur (docs/testing/e2e.md, `tests/e2e/tenant-onboarding-flow.test.ts`, Utilities), E2E-Tenant-Seeder und zusätzliche Übersetzungen/Factories zur Unterstützung der neuen Flows. - Marketing-Kommunikation: Automatische Kontakt-Bestätigungsmail (`ContactConfirmation` + Blade-Template) implementiert; Guest-PWA unter `/event` erreichbar. - Nebensitzung: Blogsystem gefixt und umfassenden BlogPostSeeder für Beispielinhalte angelegt.
580 lines
22 KiB
TypeScript
580 lines
22 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { Receipt, ShieldCheck, ArrowRight, ArrowLeft, CreditCard, AlertTriangle, Loader2 } from 'lucide-react';
|
|
import {
|
|
TenantWelcomeLayout,
|
|
WelcomeStepCard,
|
|
OnboardingCTAList,
|
|
useOnboardingProgress,
|
|
} from '..';
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ADMIN_BILLING_PATH, ADMIN_WELCOME_EVENT_PATH, ADMIN_WELCOME_PACKAGES_PATH } from '../../constants';
|
|
import { useTenantPackages } from '../hooks/useTenantPackages';
|
|
import {
|
|
assignFreeTenantPackage,
|
|
completeTenantPackagePurchase,
|
|
createTenantPackagePaymentIntent,
|
|
createTenantPayPalOrder,
|
|
captureTenantPayPalOrder,
|
|
} from '../../api';
|
|
import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
import { loadStripe } from '@stripe/stripe-js';
|
|
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
|
|
|
|
const stripePublishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ?? '';
|
|
const paypalClientId = import.meta.env.VITE_PAYPAL_CLIENT_ID ?? '';
|
|
const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : null;
|
|
|
|
type StripeCheckoutProps = {
|
|
clientSecret: string;
|
|
packageId: number;
|
|
onSuccess: () => void;
|
|
};
|
|
|
|
function StripeCheckoutForm({ clientSecret, packageId, onSuccess }: StripeCheckoutProps) {
|
|
const stripe = useStripe();
|
|
const elements = useElements();
|
|
const [submitting, setSubmitting] = React.useState(false);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
const handleSubmit = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
if (!stripe || !elements) {
|
|
setError('Zahlungsmodul noch nicht bereit. Bitte lade die Seite neu.');
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
setError(null);
|
|
|
|
const result = await stripe.confirmPayment({
|
|
elements,
|
|
confirmParams: {
|
|
return_url: window.location.href,
|
|
},
|
|
redirect: 'if_required',
|
|
});
|
|
|
|
if (result.error) {
|
|
setError(result.error.message ?? 'Zahlung fehlgeschlagen. Bitte erneut versuchen.');
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
const paymentIntent = result.paymentIntent;
|
|
const paymentMethodId =
|
|
typeof paymentIntent?.payment_method === 'string'
|
|
? paymentIntent.payment_method
|
|
: typeof paymentIntent?.id === 'string'
|
|
? paymentIntent.id
|
|
: null;
|
|
|
|
if (!paymentMethodId) {
|
|
setError('Zahlung konnte nicht bestätigt werden (fehlende Zahlungs-ID).');
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await completeTenantPackagePurchase({
|
|
packageId,
|
|
paymentMethodId,
|
|
});
|
|
onSuccess();
|
|
} catch (purchaseError) {
|
|
console.error('[Onboarding] Purchase completion failed', purchaseError);
|
|
setError(
|
|
purchaseError instanceof Error
|
|
? purchaseError.message
|
|
: 'Der Kauf wurde bei uns noch nicht verbucht. Bitte kontaktiere den Support mit deiner Zahlungsbestätigung.'
|
|
);
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6 rounded-2xl border border-brand-rose-soft bg-brand-card p-6 shadow-brand-primary">
|
|
<div className="space-y-3">
|
|
<p className="text-sm font-medium text-brand-slate">Kartenzahlung</p>
|
|
<PaymentElement id="payment-element" />
|
|
</div>
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>Zahlung fehlgeschlagen</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
<Button
|
|
type="submit"
|
|
disabled={submitting || !stripe || !elements}
|
|
className="w-full rounded-full bg-brand-rose text-white shadow-lg shadow-rose-400/40 hover:bg-[var(--brand-rose-strong)] disabled:opacity-60"
|
|
>
|
|
{submitting ? (
|
|
<>
|
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
Zahlung wird bestätigt ...
|
|
</>
|
|
) : (
|
|
<>
|
|
<CreditCard className="mr-2 size-4" />
|
|
Jetzt bezahlen
|
|
</>
|
|
)}
|
|
</Button>
|
|
<p className="text-xs text-brand-navy/70">
|
|
Sicherer Checkout über Stripe. Du erhältst eine Bestätigung, sobald der Kauf verbucht wurde.
|
|
</p>
|
|
<input type="hidden" value={clientSecret} />
|
|
</form>
|
|
);
|
|
}
|
|
|
|
type PayPalCheckoutProps = {
|
|
packageId: number;
|
|
onSuccess: () => void;
|
|
currency?: string;
|
|
};
|
|
|
|
function PayPalCheckout({ packageId, onSuccess, currency = 'EUR' }: PayPalCheckoutProps) {
|
|
const [status, setStatus] = React.useState<'idle' | 'creating' | 'capturing' | 'error' | 'success'>('idle');
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
const handleCreateOrder = React.useCallback(async () => {
|
|
try {
|
|
setStatus('creating');
|
|
const orderId = await createTenantPayPalOrder(packageId);
|
|
setStatus('idle');
|
|
setError(null);
|
|
return orderId;
|
|
} catch (err) {
|
|
console.error('[Onboarding] PayPal create order failed', err);
|
|
setStatus('error');
|
|
setError(
|
|
err instanceof Error
|
|
? err.message
|
|
: 'PayPal-Bestellung konnte nicht erstellt werden. Bitte versuche es erneut.'
|
|
);
|
|
throw err;
|
|
}
|
|
}, [packageId]);
|
|
|
|
const handleApprove = React.useCallback(
|
|
async (orderId: string) => {
|
|
try {
|
|
setStatus('capturing');
|
|
await captureTenantPayPalOrder(orderId);
|
|
setStatus('success');
|
|
setError(null);
|
|
onSuccess();
|
|
} catch (err) {
|
|
console.error('[Onboarding] PayPal capture failed', err);
|
|
setStatus('error');
|
|
setError(
|
|
err instanceof Error
|
|
? err.message
|
|
: 'PayPal-Zahlung konnte nicht abgeschlossen werden. Bitte kontaktiere den Support, falls der Betrag bereits abgebucht wurde.'
|
|
);
|
|
throw err;
|
|
}
|
|
},
|
|
[onSuccess]
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-3 rounded-2xl border border-brand-rose-soft bg-white/85 p-6 shadow-inner">
|
|
<p className="text-sm font-medium text-brand-slate">PayPal</p>
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>PayPal-Fehler</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
<PayPalButtons
|
|
style={{ layout: 'vertical' }}
|
|
forceReRender={[packageId, currency]}
|
|
createOrder={async () => handleCreateOrder()}
|
|
onApprove={async (data) => {
|
|
if (!data.orderID) {
|
|
setError('PayPal hat keine Order-ID geliefert.');
|
|
setStatus('error');
|
|
return;
|
|
}
|
|
await handleApprove(data.orderID);
|
|
}}
|
|
onError={(err) => {
|
|
console.error('[Onboarding] PayPal onError', err);
|
|
setStatus('error');
|
|
setError('PayPal hat ein Problem gemeldet. Bitte versuche es in wenigen Minuten erneut.');
|
|
}}
|
|
onCancel={() => {
|
|
setStatus('idle');
|
|
setError('PayPal-Zahlung wurde abgebrochen.');
|
|
}}
|
|
disabled={status === 'creating' || status === 'capturing'}
|
|
/>
|
|
<p className="text-xs text-brand-navy/70">
|
|
PayPal leitet dich ggf. kurz weiter, um die Zahlung zu bestätigen. Anschließend wirst du automatisch zum Setup
|
|
zurückgebracht.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function WelcomeOrderSummaryPage() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { progress, markStep } = useOnboardingProgress();
|
|
const packagesState = useTenantPackages();
|
|
|
|
const packageIdFromState = typeof location.state === 'object' ? (location.state as any)?.packageId : undefined;
|
|
const selectedPackageId = progress.selectedPackage?.id ?? packageIdFromState ?? null;
|
|
|
|
React.useEffect(() => {
|
|
if (!selectedPackageId && packagesState.status !== 'loading') {
|
|
navigate(ADMIN_WELCOME_PACKAGES_PATH, { replace: true });
|
|
}
|
|
}, [selectedPackageId, packagesState.status, navigate]);
|
|
|
|
React.useEffect(() => {
|
|
markStep({ lastStep: 'summary' });
|
|
}, [markStep]);
|
|
|
|
const packageDetails =
|
|
packagesState.status === 'success' && selectedPackageId
|
|
? packagesState.catalog.find((pkg) => pkg.id === selectedPackageId)
|
|
: null;
|
|
|
|
const activePackage =
|
|
packagesState.status === 'success'
|
|
? packagesState.purchasedPackages.find((pkg) => pkg.package_id === selectedPackageId)
|
|
: null;
|
|
|
|
const isSubscription = Boolean(packageDetails?.features?.subscription);
|
|
const requiresPayment = Boolean(packageDetails && packageDetails.price > 0);
|
|
|
|
const [clientSecret, setClientSecret] = React.useState<string | null>(null);
|
|
const [intentStatus, setIntentStatus] = React.useState<'idle' | 'loading' | 'error' | 'ready'>('idle');
|
|
const [intentError, setIntentError] = React.useState<string | null>(null);
|
|
const [freeAssignStatus, setFreeAssignStatus] = React.useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
const [freeAssignError, setFreeAssignError] = React.useState<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!requiresPayment || !packageDetails) {
|
|
setClientSecret(null);
|
|
setIntentStatus('idle');
|
|
setIntentError(null);
|
|
return;
|
|
}
|
|
|
|
if (!stripePromise) {
|
|
setIntentError('Stripe Publishable Key fehlt. Bitte konfiguriere VITE_STRIPE_PUBLISHABLE_KEY.');
|
|
setIntentStatus('error');
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
setIntentStatus('loading');
|
|
setIntentError(null);
|
|
createTenantPackagePaymentIntent(packageDetails.id)
|
|
.then((secret) => {
|
|
if (cancelled) return;
|
|
setClientSecret(secret);
|
|
setIntentStatus('ready');
|
|
})
|
|
.catch((error) => {
|
|
console.error('[Onboarding] Failed to create payment intent', error);
|
|
if (cancelled) return;
|
|
setIntentError(
|
|
error instanceof Error
|
|
? error.message
|
|
: 'PaymentIntent konnte nicht erstellt werden. Bitte später erneut versuchen.'
|
|
);
|
|
setIntentStatus('error');
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [requiresPayment, packageDetails?.id]);
|
|
|
|
const priceText =
|
|
progress.selectedPackage?.priceText ??
|
|
(packageDetails && typeof packageDetails.price === 'number'
|
|
? new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
minimumFractionDigits: 0,
|
|
}).format(packageDetails.price)
|
|
: null);
|
|
|
|
return (
|
|
<TenantWelcomeLayout
|
|
eyebrow="Schritt 3"
|
|
title="Bestellübersicht"
|
|
subtitle="Prüfe Paket, Preis und Abrechnung bevor du zum Event-Setup wechselst."
|
|
footer={
|
|
<button
|
|
type="button"
|
|
className="inline-flex items-center gap-2 text-sm font-medium text-brand-rose hover:text-rose-600"
|
|
onClick={() => navigate(ADMIN_WELCOME_PACKAGES_PATH)}
|
|
>
|
|
<ArrowLeft className="size-4" />
|
|
Zurück zur Paketauswahl
|
|
</button>
|
|
}
|
|
>
|
|
<WelcomeStepCard
|
|
step={3}
|
|
totalSteps={4}
|
|
title="Deine Auswahl im Überblick"
|
|
description="Du kannst sofort an die Abrechnung übergeben oder das Setup fortsetzen und später bezahlen."
|
|
icon={Receipt}
|
|
>
|
|
{packagesState.status === 'loading' && (
|
|
<div className="flex items-center gap-3 rounded-2xl bg-slate-50 p-6 text-sm text-brand-navy/80">
|
|
<CreditCard className="size-5 text-brand-rose" />
|
|
Wir prüfen verfügbare Pakete …
|
|
</div>
|
|
)}
|
|
|
|
{packagesState.status === 'error' && (
|
|
<Alert variant="destructive">
|
|
<AlertTriangle className="size-4" />
|
|
<AlertTitle>Paketdaten derzeit nicht verfügbar</AlertTitle>
|
|
<AlertDescription>
|
|
{packagesState.message} Du kannst das Event trotzdem vorbereiten und später zurückkehren.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{packagesState.status === 'success' && !packageDetails && (
|
|
<Alert>
|
|
<AlertTitle>Keine Paketauswahl gefunden</AlertTitle>
|
|
<AlertDescription>
|
|
Bitte wähle zuerst ein Paket aus oder aktualisiere die Seite, falls sich die Daten geändert haben.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{packagesState.status === 'success' && packageDetails && (
|
|
<div className="grid gap-4">
|
|
<div className="rounded-3xl border border-brand-rose-soft bg-brand-card p-6 shadow-md shadow-rose-100/40">
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<p className="text-xs uppercase tracking-[0.35em] text-brand-rose">
|
|
{isSubscription ? 'Abo' : 'Credit-Paket'}
|
|
</p>
|
|
<h3 className="text-2xl font-semibold text-brand-slate">{packageDetails.name}</h3>
|
|
</div>
|
|
{priceText && (
|
|
<Badge className="rounded-full bg-rose-100 px-4 py-2 text-base font-semibold text-rose-600">
|
|
{priceText}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<dl className="mt-6 grid gap-3 text-sm text-brand-navy/80 md:grid-cols-2">
|
|
<div>
|
|
<dt className="font-semibold text-brand-slate">Fotos & Galerie</dt>
|
|
<dd>
|
|
{packageDetails.max_photos
|
|
? `Bis zu ${packageDetails.max_photos} Fotos, Galerie ${packageDetails.gallery_days ?? '∞'} Tage`
|
|
: 'Unbegrenzte Fotos, flexible Galerie'}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="font-semibold text-brand-slate">Gäste & Team</dt>
|
|
<dd>
|
|
{packageDetails.max_guests
|
|
? `${packageDetails.max_guests} Gäste inklusive, Co-Hosts frei planbar`
|
|
: 'Unbegrenzte Gästeliste'}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="font-semibold text-brand-slate">Highlights</dt>
|
|
<dd>
|
|
{Object.entries(packageDetails.features ?? {})
|
|
.filter(([, enabled]) => enabled)
|
|
.map(([feature]) => feature.replace(/_/g, ' '))
|
|
.join(', ') || 'Standard'}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="font-semibold text-brand-slate">Status</dt>
|
|
<dd>{activePackage ? 'Bereits gebucht' : 'Noch nicht gebucht'}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
{!activePackage && (
|
|
<Alert className="rounded-2xl border-brand-rose-soft bg-brand-rose-soft/60 text-rose-700">
|
|
<ShieldCheck className="size-4" />
|
|
<AlertTitle>Abrechnung steht noch aus</AlertTitle>
|
|
<AlertDescription>
|
|
Du kannst das Event bereits vorbereiten. Spätestens zur Veröffentlichung benötigst du ein aktives Paket.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{packageDetails.price === 0 && (
|
|
<div className="rounded-2xl border border-emerald-200 bg-emerald-50/40 p-6">
|
|
<p className="text-sm text-emerald-700">
|
|
Dieses Paket ist kostenlos. Du kannst es sofort deinem Tenant zuweisen und direkt mit dem Setup
|
|
weitermachen.
|
|
</p>
|
|
{freeAssignStatus === 'success' ? (
|
|
<Alert className="mt-4 border-emerald-200 bg-white text-emerald-600">
|
|
<AlertTitle>Gratis-Paket aktiviert</AlertTitle>
|
|
<AlertDescription>
|
|
Deine Credits wurden hinzugefügt. Weiter geht's mit dem Event-Setup.
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : (
|
|
<Button
|
|
onClick={async () => {
|
|
if (!packageDetails) {
|
|
return;
|
|
}
|
|
setFreeAssignStatus('loading');
|
|
setFreeAssignError(null);
|
|
try {
|
|
await assignFreeTenantPackage(packageDetails.id);
|
|
setFreeAssignStatus('success');
|
|
markStep({ packageSelected: true });
|
|
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
|
|
} catch (error) {
|
|
console.error('[Onboarding] Free package assignment failed', error);
|
|
setFreeAssignStatus('error');
|
|
setFreeAssignError(
|
|
error instanceof Error ? error.message : 'Kostenloses Paket konnte nicht aktiviert werden.'
|
|
);
|
|
}
|
|
}}
|
|
disabled={freeAssignStatus === 'loading'}
|
|
className="mt-4 rounded-full bg-brand-teal text-white shadow-md shadow-emerald-300/40 hover:opacity-90"
|
|
>
|
|
{freeAssignStatus === 'loading' ? (
|
|
<>
|
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
Aktivierung läuft ...
|
|
</>
|
|
) : (
|
|
'Gratis-Paket aktivieren'
|
|
)}
|
|
</Button>
|
|
)}
|
|
{freeAssignStatus === 'error' && freeAssignError && (
|
|
<Alert variant="destructive" className="mt-3">
|
|
<AlertTitle>Aktivierung fehlgeschlagen</AlertTitle>
|
|
<AlertDescription>{freeAssignError}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{requiresPayment && (
|
|
<div className="space-y-6">
|
|
<div className="space-y-4">
|
|
<h4 className="text-lg font-semibold text-brand-slate">Kartenzahlung (Stripe)</h4>
|
|
{intentStatus === 'loading' && (
|
|
<div className="flex items-center gap-3 rounded-2xl border border-brand-rose-soft bg-brand-card p-6 text-sm text-brand-navy/80">
|
|
<Loader2 className="size-4 animate-spin text-brand-rose" />
|
|
Zahlungsdetails werden geladen …
|
|
</div>
|
|
)}
|
|
{intentStatus === 'error' && (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>Stripe nicht verfügbar</AlertTitle>
|
|
<AlertDescription>{intentError}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
{intentStatus === 'ready' && clientSecret && stripePromise && (
|
|
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
|
<StripeCheckoutForm
|
|
clientSecret={clientSecret}
|
|
packageId={packageDetails.id}
|
|
onSuccess={() => {
|
|
markStep({ packageSelected: true });
|
|
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
|
|
}}
|
|
/>
|
|
</Elements>
|
|
)}
|
|
</div>
|
|
|
|
{paypalClientId ? (
|
|
<div className="space-y-4">
|
|
<h4 className="text-lg font-semibold text-brand-slate">PayPal</h4>
|
|
<PayPalScriptProvider
|
|
options={{ 'client-id': paypalClientId, currency: 'EUR', intent: 'CAPTURE' }}
|
|
>
|
|
<PayPalCheckout
|
|
packageId={packageDetails.id}
|
|
onSuccess={() => {
|
|
markStep({ packageSelected: true });
|
|
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
|
|
}}
|
|
/>
|
|
</PayPalScriptProvider>
|
|
</div>
|
|
) : (
|
|
<Alert variant="default" className="border-amber-200 bg-amber-50 text-amber-700">
|
|
<AlertTitle>PayPal nicht konfiguriert</AlertTitle>
|
|
<AlertDescription>
|
|
Hinterlege `VITE_PAYPAL_CLIENT_ID`, damit Gastgeber optional mit PayPal bezahlen können.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-3 rounded-2xl bg-brand-card p-6 shadow-inner shadow-rose-100/40">
|
|
<h4 className="text-lg font-semibold text-brand-slate">Nächste Schritte</h4>
|
|
<ol className="list-decimal space-y-2 pl-5 text-sm text-brand-navy/80">
|
|
<li>Optional: Abrechnung abschließen (Stripe/PayPal) im Billing-Bereich.</li>
|
|
<li>Event-Setup durchlaufen und Aufgaben, Team & Galerie konfigurieren.</li>
|
|
<li>Vor dem Go-Live Credits prüfen und Gäste-Link teilen.</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</WelcomeStepCard>
|
|
|
|
<OnboardingCTAList
|
|
actions={[
|
|
{
|
|
id: 'checkout',
|
|
label: 'Abrechnung starten',
|
|
description: 'Öffnet den Billing-Bereich mit allen Kaufoptionen (Stripe, PayPal, Credits).',
|
|
buttonLabel: 'Zu Billing & Zahlung',
|
|
href: ADMIN_BILLING_PATH,
|
|
icon: CreditCard,
|
|
variant: 'secondary',
|
|
},
|
|
{
|
|
id: 'continue-to-setup',
|
|
label: 'Mit Event-Setup fortfahren',
|
|
description: 'Du kannst später jederzeit zur Abrechnung zurückkehren.',
|
|
buttonLabel: 'Weiter zum Setup',
|
|
onClick: () => {
|
|
markStep({ lastStep: 'event-setup' });
|
|
navigate(ADMIN_WELCOME_EVENT_PATH);
|
|
},
|
|
icon: ArrowRight,
|
|
},
|
|
]}
|
|
/>
|
|
</TenantWelcomeLayout>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|