feat(packages): implement package-based business model
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft, Loader2, Save, Sparkles } from 'lucide-react';
|
||||
import { ArrowLeft, Loader2, Save, Sparkles, Package as PackageIcon } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -8,15 +9,18 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
|
||||
import { AdminLayout } from '../components/AdminLayout';
|
||||
import { createEvent, getEvent, updateEvent } from '../api';
|
||||
import { createEvent, getEvent, updateEvent, getPackages } from '../api';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
|
||||
interface EventFormState {
|
||||
name: string;
|
||||
slug: string;
|
||||
date: string;
|
||||
package_id: number;
|
||||
isPublished: boolean;
|
||||
}
|
||||
|
||||
@@ -30,6 +34,7 @@ export default function EventFormPage() {
|
||||
name: '',
|
||||
slug: '',
|
||||
date: '',
|
||||
package_id: 1, // Default Free package
|
||||
isPublished: false,
|
||||
});
|
||||
const [autoSlug, setAutoSlug] = React.useState(true);
|
||||
@@ -38,6 +43,11 @@ export default function EventFormPage() {
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const { data: packages, isLoading: packagesLoading } = useQuery({
|
||||
queryKey: ['packages', 'endcustomer'],
|
||||
queryFn: () => getPackages('endcustomer'),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!isEdit || !slugParam) {
|
||||
@@ -109,6 +119,7 @@ export default function EventFormPage() {
|
||||
const payload = {
|
||||
name: trimmedName,
|
||||
slug: trimmedSlug,
|
||||
package_id: form.package_id,
|
||||
date: form.date || undefined,
|
||||
status: form.isPublished ? 'published' : 'draft',
|
||||
};
|
||||
@@ -199,6 +210,50 @@ export default function EventFormPage() {
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, date: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="package_id">Package</Label>
|
||||
<Select value={form.package_id.toString()} onValueChange={(value) => setForm((prev) => ({ ...prev, package_id: parseInt(value) }))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Wählen Sie ein Package" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{packagesLoading ? (
|
||||
<SelectItem value="">Laden...</SelectItem>
|
||||
) : (
|
||||
packages?.map((pkg) => (
|
||||
<SelectItem key={pkg.id} value={pkg.id.toString()}>
|
||||
{pkg.name} - {pkg.price} € ({pkg.max_photos} Fotos)
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">Package-Details</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Package auswählen</DialogTitle>
|
||||
<DialogDescription>Wählen Sie das Package für Ihr Event. Höhere Packages bieten mehr Limits und Features.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
{packages?.map((pkg) => (
|
||||
<div key={pkg.id} className="p-4 border rounded">
|
||||
<h3 className="font-semibold">{pkg.name}</h3>
|
||||
<p>{pkg.price} €</p>
|
||||
<ul className="text-sm">
|
||||
<li>Max Fotos: {pkg.max_photos}</li>
|
||||
<li>Max Gäste: {pkg.max_guests}</li>
|
||||
<li>Galerie: {pkg.gallery_days} Tage</li>
|
||||
<li>Features: {Object.keys(pkg.features).filter(k => pkg.features[k]).join(', ')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-xl bg-pink-50/60 p-4">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { ArrowRight, CalendarDays, Plus, Settings, Sparkles } from 'lucide-react';
|
||||
import { ArrowRight, CalendarDays, Plus, Settings, Sparkles, Package as PackageIcon } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -8,7 +9,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
import { AdminLayout } from '../components/AdminLayout';
|
||||
import { getEvents, TenantEvent } from '../api';
|
||||
import { getEvents, TenantEvent, getPackages } from '../api';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
|
||||
export default function EventsPage() {
|
||||
@@ -17,6 +18,11 @@ export default function EventsPage() {
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: tenantPackages } = useQuery({
|
||||
queryKey: ['tenant-packages'],
|
||||
queryFn: () => getPackages('reseller'), // or separate endpoint
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -53,6 +59,33 @@ export default function EventsPage() {
|
||||
subtitle="Plane Momente, die in Erinnerung bleiben. Hier verwaltest du alles rund um deine Veranstaltungen."
|
||||
actions={actions}
|
||||
>
|
||||
{tenantPackages && tenantPackages.length > 0 && (
|
||||
<Card className="mb-6 border-0 bg-white/80 shadow-xl shadow-pink-100/60">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<PackageIcon className="h-5 w-5 text-pink-500" />
|
||||
Aktuelles Package
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Ihr aktuelles Reseller-Package und verbleibende Limits.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid md:grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold">Aktives Package</h3>
|
||||
<p className="text-lg font-bold">{tenantPackages.find(p => p.active)?.package?.name || 'Kein aktives Package'}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold">Verbleibende Events</h3>
|
||||
<p className="text-lg font-bold">{tenantPackages.find(p => p.active)?.remaining_events || 0}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold">Ablauf</h3>
|
||||
<p className="text-lg font-bold">{tenantPackages.find(p => p.active)?.expires_at || 'Kein Package'}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Fehler beim Laden</AlertTitle>
|
||||
|
||||
Reference in New Issue
Block a user