implemented event package addons with filament resource, event-admin purchase path and notifications, showing up in purchase history
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
RefreshCw,
|
||||
Smile,
|
||||
Sparkles,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
toggleEvent,
|
||||
submitTenantFeedback,
|
||||
updatePhotoVisibility,
|
||||
createEventAddonCheckout,
|
||||
} from '../api';
|
||||
import { buildLimitWarnings } from '../lib/limitWarnings';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
@@ -54,6 +56,9 @@ import {
|
||||
ActionGrid,
|
||||
TenantHeroCard,
|
||||
} from '../components/tenant';
|
||||
import { AddonsPicker } from '../components/Addons/AddonsPicker';
|
||||
import { AddonSummaryList } from '../components/Addons/AddonSummaryList';
|
||||
import { EventAddonCatalogItem, getAddonCatalog } from '../api';
|
||||
import { GuestBroadcastCard } from '../components/GuestBroadcastCard';
|
||||
|
||||
type EventDetailPageProps = {
|
||||
@@ -76,6 +81,7 @@ type WorkspaceState = {
|
||||
|
||||
export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProps) {
|
||||
const { slug: slugParam } = useParams<{ slug?: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('management');
|
||||
const { t: tCommon } = useTranslation('common');
|
||||
@@ -91,6 +97,9 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
});
|
||||
|
||||
const [toolkit, setToolkit] = React.useState<ToolkitState>({ data: null, loading: true, error: null });
|
||||
const [addonBusyId, setAddonBusyId] = React.useState<string | null>(null);
|
||||
const [addonRefreshCount, setAddonRefreshCount] = React.useState(0);
|
||||
const [addonsCatalog, setAddonsCatalog] = React.useState<EventAddonCatalogItem[]>([]);
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
if (!slug) {
|
||||
@@ -103,8 +112,9 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
setToolkit((prev) => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const [eventData, statsData] = await Promise.all([getEvent(slug), getEventStats(slug)]);
|
||||
const [eventData, statsData, addonOptions] = await Promise.all([getEvent(slug), getEventStats(slug), getAddonCatalog()]);
|
||||
setState((prev) => ({ ...prev, event: eventData, stats: statsData, loading: false }));
|
||||
setAddonsCatalog(addonOptions);
|
||||
} catch (error) {
|
||||
if (!isAuthError(error)) {
|
||||
setState((prev) => ({
|
||||
@@ -181,7 +191,43 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
[event?.limits, tCommon],
|
||||
);
|
||||
|
||||
const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
const shownWarningToasts = React.useRef<Set<string>>(new Set());
|
||||
const [addonBusyId, setAddonBusyId] = React.useState<string | null>(null);
|
||||
|
||||
const handleAddonPurchase = React.useCallback(
|
||||
async (scope: 'photos' | 'guests' | 'gallery', addonKeyOverride?: string) => {
|
||||
if (!slug) return;
|
||||
|
||||
const defaultAddons: Record<typeof scope, string> = {
|
||||
photos: 'extra_photos_500',
|
||||
guests: 'extra_guests_100',
|
||||
gallery: 'extend_gallery_30d',
|
||||
};
|
||||
|
||||
const addonKey = addonKeyOverride ?? defaultAddons[scope];
|
||||
setAddonBusyId(scope);
|
||||
try {
|
||||
const currentUrl = window.location.origin + window.location.pathname;
|
||||
const successUrl = `${currentUrl}?addon_success=1`;
|
||||
const checkout = await createEventAddonCheckout(slug, {
|
||||
addon_key: addonKey,
|
||||
quantity: 1,
|
||||
success_url: successUrl,
|
||||
cancel_url: currentUrl,
|
||||
});
|
||||
if (checkout.checkout_url) {
|
||||
window.location.href = checkout.checkout_url;
|
||||
} else {
|
||||
toast(t('events.errors.checkoutMissing', 'Checkout konnte nicht gestartet werden.'));
|
||||
}
|
||||
} catch (err) {
|
||||
toast(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Add-on Checkout fehlgeschlagen.')));
|
||||
} finally {
|
||||
setAddonBusyId(null);
|
||||
}
|
||||
},
|
||||
[slug, t],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
limitWarnings.forEach((warning) => {
|
||||
@@ -198,6 +244,30 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
});
|
||||
}, [limitWarnings]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const success = searchParams.get('addon_success');
|
||||
if (success && slug) {
|
||||
toast(t('events.success.addonApplied', 'Add-on angewendet. Limits aktualisieren sich in Kürze.'));
|
||||
void load();
|
||||
setAddonRefreshCount(3);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.delete('addon_success');
|
||||
const search = params.toString();
|
||||
navigate(search ? `${window.location.pathname}?${search}` : window.location.pathname, { replace: true });
|
||||
}
|
||||
}, [searchParams, slug, load, navigate, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (addonRefreshCount <= 0) {
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
void load();
|
||||
setAddonRefreshCount((count) => count - 1);
|
||||
}, 8000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [addonRefreshCount, load]);
|
||||
|
||||
if (!slug) {
|
||||
return (
|
||||
<AdminLayout
|
||||
@@ -230,10 +300,39 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
variant={warning.tone === 'danger' ? 'destructive' : 'default'}
|
||||
className={warning.tone === 'warning' ? 'border-amber-400/40 bg-amber-50 text-amber-900' : undefined}
|
||||
>
|
||||
<AlertDescription className="flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{warning.message}
|
||||
</AlertDescription>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<AlertDescription className="flex items-center gap-2 text-sm">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{warning.message}
|
||||
</AlertDescription>
|
||||
{(['photos', 'guests', 'gallery'] as const).includes(warning.scope) ? (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => { void handleAddonPurchase(warning.scope as 'photos' | 'guests' | 'gallery'); }}
|
||||
disabled={addonBusyId === warning.scope}
|
||||
className="justify-start"
|
||||
>
|
||||
<ShoppingCart className="mr-2 h-4 w-4" />
|
||||
{warning.scope === 'photos'
|
||||
? t('events.actions.buyMorePhotos', 'Mehr Fotos freischalten')
|
||||
: warning.scope === 'guests'
|
||||
? t('events.actions.buyMoreGuests', 'Mehr Gäste freischalten')
|
||||
: t('events.actions.extendGallery', 'Galerie verlängern')}
|
||||
</Button>
|
||||
{addonsCatalog.length > 0 ? (
|
||||
<AddonsPicker
|
||||
addons={addonsCatalog}
|
||||
scope={warning.scope as 'photos' | 'guests' | 'gallery'}
|
||||
onCheckout={(key) => { void handleAddonPurchase(warning.scope as 'photos' | 'guests' | 'gallery', key); }}
|
||||
busy={addonBusyId === warning.scope}
|
||||
t={(key, fallback) => t(key as any, fallback)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
@@ -257,7 +356,17 @@ export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProp
|
||||
navigate={navigate}
|
||||
/>
|
||||
|
||||
{(toolkitData?.alerts?.length ?? 0) > 0 && <AlertList alerts={toolkitData?.alerts ?? []} />}
|
||||
{(toolkitData?.alerts?.length ?? 0) > 0 && <AlertList alerts={toolkitData?.alerts ?? []} />}
|
||||
|
||||
{state.event?.addons?.length ? (
|
||||
<SectionCard>
|
||||
<SectionHeader
|
||||
title={t('events.sections.addons.title', 'Add-ons & Upgrades')}
|
||||
description={t('events.sections.addons.description', 'Zusätzliche Kontingente für dieses Event.')}
|
||||
/>
|
||||
<AddonSummaryList addons={state.event.addons} t={(key, fallback) => t(key, fallback)} />
|
||||
</SectionCard>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.6fr)_minmax(0,0.6fr)]">
|
||||
<StatusCard event={event} stats={stats} busy={busy} onToggle={handleToggle} />
|
||||
|
||||
Reference in New Issue
Block a user