implemented event package addons with filament resource, event-admin purchase path and notifications, showing up in purchase history
This commit is contained in:
66
resources/js/admin/components/Addons/AddonSummaryList.tsx
Normal file
66
resources/js/admin/components/Addons/AddonSummaryList.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { EventAddonSummary } from '../../api';
|
||||
|
||||
type Props = {
|
||||
addons: EventAddonSummary[];
|
||||
t: (key: string, fallback: string) => string;
|
||||
};
|
||||
|
||||
export function AddonSummaryList({ addons, t }: Props) {
|
||||
if (!addons.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{addons.map((addon) => (
|
||||
<div key={addon.id} className="flex flex-col gap-1 rounded-2xl border border-slate-200/70 bg-white/70 p-4 text-sm dark:border-white/10 dark:bg-white/5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-white">{addon.label ?? addon.key}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{buildSummary(addon, t)}
|
||||
</p>
|
||||
{addon.purchased_at ? (
|
||||
<p className="text-xs text-slate-400">
|
||||
{t('events.sections.addons.purchasedAt', `Purchased ${new Date(addon.purchased_at).toLocaleString()}`, {
|
||||
date: new Date(addon.purchased_at).toLocaleString(),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Badge variant={addon.status === 'completed' ? 'outline' : addon.status === 'pending' ? 'secondary' : 'destructive'}>
|
||||
{t(`events.sections.addons.status.${addon.status}`, addon.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildSummary(addon: EventAddonSummary, t: (key: string, fallback: string, options?: Record<string, unknown>) => string): string {
|
||||
const parts: string[] = [];
|
||||
if (addon.extra_photos > 0) {
|
||||
parts.push(
|
||||
t('events.sections.addons.summary.photos', `+${addon.extra_photos} photos`, {
|
||||
count: addon.extra_photos.toLocaleString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (addon.extra_guests > 0) {
|
||||
parts.push(
|
||||
t('events.sections.addons.summary.guests', `+${addon.extra_guests} guests`, {
|
||||
count: addon.extra_guests.toLocaleString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (addon.extra_gallery_days > 0) {
|
||||
parts.push(
|
||||
t('events.sections.addons.summary.gallery', `+${addon.extra_gallery_days} days gallery`, {
|
||||
count: addon.extra_gallery_days,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join(' · ');
|
||||
}
|
||||
64
resources/js/admin/components/Addons/AddonsPicker.tsx
Normal file
64
resources/js/admin/components/Addons/AddonsPicker.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { ShoppingCart } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { EventAddonCatalogItem } from '../../api';
|
||||
|
||||
type Props = {
|
||||
addons: EventAddonCatalogItem[];
|
||||
scope: 'photos' | 'guests' | 'gallery';
|
||||
onCheckout: (addonKey: string) => void;
|
||||
busy?: boolean;
|
||||
t: (key: string, fallback: string) => string;
|
||||
};
|
||||
|
||||
const scopeDefaults: Record<Props['scope'], string[]> = {
|
||||
photos: ['extra_photos_500', 'extra_photos_2000'],
|
||||
guests: ['extra_guests_50', 'extra_guests_100'],
|
||||
gallery: ['extend_gallery_30d', 'extend_gallery_90d'],
|
||||
};
|
||||
|
||||
export function AddonsPicker({ addons, scope, onCheckout, busy, t }: Props) {
|
||||
const options = React.useMemo(() => {
|
||||
const whitelist = scopeDefaults[scope];
|
||||
const filtered = addons.filter((addon) => whitelist.includes(addon.key));
|
||||
return filtered.length ? filtered : addons;
|
||||
}, [addons, scope]);
|
||||
|
||||
const [selected, setSelected] = React.useState<string | undefined>(() => options[0]?.key);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelected(options[0]?.key);
|
||||
}, [options]);
|
||||
|
||||
if (!options.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||
<Select value={selected} onValueChange={(value) => setSelected(value)}>
|
||||
<SelectTrigger className="w-full sm:w-64">
|
||||
<SelectValue placeholder={t('addons.selectPlaceholder', 'Add-on auswählen')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((addon) => (
|
||||
<SelectItem key={addon.key} value={addon.key} disabled={!addon.price_id}>
|
||||
{addon.label}
|
||||
{!addon.price_id ? ' (kein Preis verknüpft)' : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!selected || busy || !options.find((a) => a.key === selected)?.price_id}
|
||||
onClick={() => selected && onCheckout(selected)}
|
||||
>
|
||||
<ShoppingCart className="mr-2 h-4 w-4" />
|
||||
{t('addons.buyNow', 'Jetzt freischalten')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user