Files
fotospiel-app/resources/js/admin/onboarding/pages/WelcomePackagesPage.tsx
Codex Agent d04e234ca0 - Tenant-Admin-PWA: Neues /event-admin/welcome Onboarding mit WelcomeHero, Packages-, Order-Summary- und Event-Setup-Pages, Zustandsspeicher, Routing-Guard und Dashboard-CTA für Erstnutzer; Filament-/admin-Login via Custom-View behoben.
- 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.
2025-10-10 21:31:55 +02:00

200 lines
7.9 KiB
TypeScript
Raw Blame History

import React from 'react';
import { CreditCard, ShoppingBag, ArrowRight, Loader2, AlertCircle } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
TenantWelcomeLayout,
WelcomeStepCard,
OnboardingCTAList,
useOnboardingProgress,
} from '..';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { ADMIN_BILLING_PATH, ADMIN_WELCOME_SUMMARY_PATH } from '../../constants';
import { useTenantPackages } from '../hooks/useTenantPackages';
import { Package } from '../../api';
export default function WelcomePackagesPage() {
const navigate = useNavigate();
const { markStep } = useOnboardingProgress();
const packagesState = useTenantPackages();
React.useEffect(() => {
if (packagesState.status === 'success') {
markStep({
packageSelected: Boolean(packagesState.activePackage),
lastStep: 'packages',
selectedPackage: packagesState.activePackage
? {
id: packagesState.activePackage.package_id,
name: packagesState.activePackage.package_name,
priceText: packagesState.activePackage.price
? new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: packagesState.activePackage.currency ?? 'EUR',
minimumFractionDigits: 0,
}).format(packagesState.activePackage.price)
: null,
isSubscription: false,
}
: null,
});
}
}, [packagesState, markStep]);
const handleSelectPackage = React.useCallback(
(pkg: Package) => {
const priceText =
typeof pkg.price === 'number'
? new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
}).format(pkg.price)
: 'Auf Anfrage';
markStep({
packageSelected: true,
lastStep: package-,
selectedPackage: {
id: pkg.id,
name: pkg.name,
priceText,
isSubscription: Boolean(pkg.features?.subscription),
},
});
navigate(ADMIN_WELCOME_SUMMARY_PATH, { state: { packageId: pkg.id } });
},
[markStep, navigate]
);
return (
<TenantWelcomeLayout
eyebrow="Schritt 2"
title="W<>hle dein Eventpaket"
subtitle="Fotospiel unterst<73>tzt flexible Preismodelle: einmalige Credits oder Abos, die mehrere Events abdecken."
>
<WelcomeStepCard
step={2}
totalSteps={4}
title="Aktiviere die passenden Credits"
description="Sichere dir Kapazit<69>t f<>r dein n<>chstes Event. Du kannst jederzeit upgraden <20> bezahle nur, was du wirklich brauchst."
icon={CreditCard}
>
{packagesState.status === 'loading' && (
<div className="flex items-center gap-3 rounded-2xl bg-brand-sky-soft/60 p-6 text-sm text-brand-navy/80">
<Loader2 className="size-5 animate-spin text-brand-rose" />
Pakete werden geladen <EFBFBD>
</div>
)}
{packagesState.status === 'error' && (
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>Fehler beim Laden</AlertTitle>
<AlertDescription>{packagesState.message}</AlertDescription>
</Alert>
)}
{packagesState.status === 'success' && (
<div className="grid gap-4 md:grid-cols-2">
{packagesState.catalog.map((pkg) => {
const isActive = packagesState.activePackage?.package_id === pkg.id;
const purchased = packagesState.purchasedPackages.find((tenantPkg) => tenantPkg.package_id === pkg.id);
const featureLabels = Object.entries(pkg.features ?? {})
.filter(([, enabled]) => Boolean(enabled))
.map(([key]) => key.replace(/_/g, ' '));
const isSubscription = Boolean(pkg.features?.subscription);
const priceText =
typeof pkg.price === 'number'
? new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0,
}).format(pkg.price)
: 'Auf Anfrage';
return (
<div
key={pkg.id}
className="flex flex-col gap-4 rounded-2xl border border-brand-rose-soft bg-brand-card p-6 shadow-brand-primary"
>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.25em] text-brand-rose">
{isSubscription ? 'Abo' : 'Credit-Paket'}
</p>
<h3 className="text-xl font-semibold text-brand-slate">{pkg.name}</h3>
</div>
<span className="text-lg font-medium text-brand-rose">{priceText}</span>
</div>
<p className="text-sm text-brand-navy/80">
{pkg.max_photos
? Bis zu Fotos inklusive <EFBFBD> perfekt f<EFBFBD>r lebendige Reportagen.
: 'Sofort einsatzbereit f<>r dein n<>chstes Event.'}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs uppercase tracking-wide text-brand-rose">
{pkg.max_guests && (
<span className="rounded-full bg-brand-rose-soft px-3 py-1">{${pkg.max_guests} G<EFBFBD>ste}</span>
)}
{pkg.gallery_days && (
<span className="rounded-full bg-brand-rose-soft px-3 py-1">{${pkg.gallery_days} Tage Galerie}</span>
)}
{featureLabels.map((feature) => (
<span key={feature} className="rounded-full bg-brand-rose-soft px-3 py-1">
{feature}
</span>
))}
{isActive && (
<span className="rounded-full bg-brand-sky-soft px-3 py-1 text-brand-navy">Aktives Paket</span>
)}
</div>
<Button
size="lg"
className="mt-auto rounded-full bg-brand-rose text-white shadow-lg shadow-rose-300/40 hover:bg-[var(--brand-rose-strong)]"
onClick={() => handleSelectPackage(pkg)}
>
Paket w<EFBFBD>hlen
<ArrowRight className="ml-2 size-4" />
</Button>
{purchased && (
<p className="text-xs text-brand-rose">
Bereits gekauft am{' '}
{purchased.purchased_at
? new Date(purchased.purchased_at).toLocaleDateString('de-DE')
: 'unbekanntem Datum'}
</p>
)}
</div>
);
})}
</div>
)}
</WelcomeStepCard>
<OnboardingCTAList
actions={[
{
id: 'skip-to-checkout',
label: 'Direkt zum Billing',
description:
'Falls du schon wei<65>t, welches Paket du brauchst, gelangst du hier zum bekannten Abrechnungsbereich.',
buttonLabel: 'Billing <20>ffnen',
href: ADMIN_BILLING_PATH,
icon: ShoppingBag,
variant: 'secondary',
},
{
id: 'continue',
label: 'Bestell<6C>bersicht anzeigen',
description: 'Pr<50>fe Paketdetails und entscheide, ob du direkt zahlen oder sp<73>ter fortfahren m<>chtest.',
buttonLabel: 'Weiter zur <20>bersicht',
onClick: () => navigate(ADMIN_WELCOME_SUMMARY_PATH),
icon: ArrowRight,
},
]}
/>
</TenantWelcomeLayout>
);
}