Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/page.tsx
2026-03-29 19:44:57 +02:00

382 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from 'next/link';
import {
ArrowRight,
FileText,
GraduationCap,
Mail,
Plus,
UserCheck,
UserPlus,
CalendarDays,
Activity,
BedDouble,
} from 'lucide-react';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { createMemberManagementApi } from '@kit/member-management/api';
import { createCourseManagementApi } from '@kit/course-management/api';
import { createFinanceApi } from '@kit/finance/api';
import { createNewsletterApi } from '@kit/newsletter/api';
import { createBookingManagementApi } from '@kit/booking-management/api';
import { createEventManagementApi } from '@kit/event-management/api';
import { CmsPageShell } from '~/components/cms-page-shell';
import { StatsCard } from '~/components/stats-card';
interface TeamAccountHomePageProps {
params: Promise<{ account: string }>;
}
export default async function TeamAccountHomePage({
params,
}: TeamAccountHomePageProps) {
const { account } = await params;
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
.select('id, name')
.eq('slug', account)
.single();
if (!acct) return <div>Konto nicht gefunden</div>;
// Load all stats in parallel with allSettled for resilience
const [
memberStatsResult,
courseStatsResult,
invoicesResult,
newslettersResult,
bookingsResult,
eventsResult,
] = await Promise.allSettled([
createMemberManagementApi(client).getMemberStatistics(acct.id),
createCourseManagementApi(client).getStatistics(acct.id),
createFinanceApi(client).listInvoices(acct.id, { status: 'draft' }),
createNewsletterApi(client).listNewsletters(acct.id),
createBookingManagementApi(client).listBookings(acct.id, { page: 1 }),
createEventManagementApi(client).listEvents(acct.id, { page: 1 }),
]);
const memberStats =
memberStatsResult.status === 'fulfilled'
? memberStatsResult.value
: { total: 0, active: 0, inactive: 0, pending: 0, resigned: 0 };
const courseStats =
courseStatsResult.status === 'fulfilled'
? courseStatsResult.value
: { totalCourses: 0, openCourses: 0, completedCourses: 0, totalParticipants: 0 };
const openInvoices =
invoicesResult.status === 'fulfilled' ? invoicesResult.value : [];
const newsletters =
newslettersResult.status === 'fulfilled' ? newslettersResult.value : [];
const bookings =
bookingsResult.status === 'fulfilled'
? bookingsResult.value
: { data: [], total: 0 };
const events =
eventsResult.status === 'fulfilled'
? eventsResult.value
: { data: [], total: 0 };
const accountName = acct.name ? String(acct.name) : 'Dashboard';
return (
<CmsPageShell account={account} title={accountName}>
<div className="flex w-full flex-col gap-6">
{/* Stats Row */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatsCard
title="Mitglieder"
value={memberStats.active}
icon={<UserCheck className="h-5 w-5" />}
description={`${memberStats.total} gesamt, ${memberStats.pending} ausstehend`}
/>
<StatsCard
title="Kurse"
value={courseStats.openCourses}
icon={<GraduationCap className="h-5 w-5" />}
description={`${courseStats.totalCourses} gesamt, ${courseStats.totalParticipants} Teilnehmer`}
/>
<StatsCard
title="Offene Rechnungen"
value={openInvoices.length}
icon={<FileText className="h-5 w-5" />}
description="Entwürfe zum Versenden"
/>
<StatsCard
title="Newsletter"
value={newsletters.length}
icon={<Mail className="h-5 w-5" />}
description="Erstellt"
/>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Letzte Aktivität */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Letzte Aktivität
</CardTitle>
<CardDescription>
Aktuelle Buchungen und Veranstaltungen
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Recent bookings */}
{bookings.data.slice(0, 3).map((booking: Record<string, unknown>) => (
<div
key={String(booking.id)}
className="flex items-center justify-between rounded-md border p-3"
>
<div className="flex items-center gap-3">
<div className="rounded-full bg-blue-500/10 p-2 text-blue-600">
<BedDouble className="h-4 w-4" />
</div>
<div>
<Link
href={`/home/${account}/bookings/${String(booking.id)}`}
className="text-sm font-medium hover:underline"
>
Buchung #{String(booking.id).slice(0, 8)}
</Link>
<p className="text-xs text-muted-foreground">
{booking.check_in
? new Date(
String(booking.check_in),
).toLocaleDateString('de-DE')
: '—'}{' '}
{' '}
{booking.check_out
? new Date(
String(booking.check_out),
).toLocaleDateString('de-DE')
: '—'}
</p>
</div>
</div>
<Badge variant="outline">
{String(booking.status ?? '—')}
</Badge>
</div>
))}
{/* Recent events */}
{events.data.slice(0, 3).map((event: Record<string, unknown>) => (
<div
key={String(event.id)}
className="flex items-center justify-between rounded-md border p-3"
>
<div className="flex items-center gap-3">
<div className="rounded-full bg-amber-500/10 p-2 text-amber-600">
<CalendarDays className="h-4 w-4" />
</div>
<div>
<Link
href={`/home/${account}/events/${String(event.id)}`}
className="text-sm font-medium hover:underline"
>
{String(event.name)}
</Link>
<p className="text-xs text-muted-foreground">
{event.event_date
? new Date(
String(event.event_date),
).toLocaleDateString('de-DE')
: 'Kein Datum'}
</p>
</div>
</div>
<Badge variant="outline">
{String(event.status ?? '—')}
</Badge>
</div>
))}
{bookings.data.length === 0 && events.data.length === 0 && (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Activity className="h-8 w-8 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">
Noch keine Aktivitäten vorhanden
</p>
</div>
)}
</div>
</CardContent>
</Card>
{/* Schnellaktionen */}
<Card>
<CardHeader>
<CardTitle>Schnellaktionen</CardTitle>
<CardDescription>Häufig verwendete Aktionen</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<Link href={`/home/${account}/members-cms/new`}>
<Button
variant="outline"
className="w-full justify-between"
>
<span className="flex items-center gap-2">
<UserPlus className="h-4 w-4" />
Neues Mitglied
</span>
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
<Link href={`/home/${account}/courses/new`}>
<Button
variant="outline"
className="w-full justify-between"
>
<span className="flex items-center gap-2">
<GraduationCap className="h-4 w-4" />
Neuer Kurs
</span>
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
<Link href={`/home/${account}/newsletter/new`}>
<Button
variant="outline"
className="w-full justify-between"
>
<span className="flex items-center gap-2">
<Mail className="h-4 w-4" />
Newsletter erstellen
</span>
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
<Link href={`/home/${account}/bookings/new`}>
<Button
variant="outline"
className="w-full justify-between"
>
<span className="flex items-center gap-2">
<BedDouble className="h-4 w-4" />
Neue Buchung
</span>
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
<Link href={`/home/${account}/events/new`}>
<Button
variant="outline"
className="w-full justify-between"
>
<span className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Neue Veranstaltung
</span>
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</CardContent>
</Card>
</div>
{/* Module Overview Row */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
Buchungen
</p>
<p className="text-2xl font-bold">{bookings.total}</p>
<p className="text-xs text-muted-foreground">
{bookings.data.filter(
(b: Record<string, unknown>) =>
b.status === 'confirmed' || b.status === 'checked_in',
).length}{' '}
aktiv
</p>
</div>
<Link href={`/home/${account}/bookings`}>
<Button variant="ghost" size="icon">
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
Veranstaltungen
</p>
<p className="text-2xl font-bold">{events.total}</p>
<p className="text-xs text-muted-foreground">
{events.data.filter(
(e: Record<string, unknown>) =>
e.status === 'published' ||
e.status === 'registration_open',
).length}{' '}
aktiv
</p>
</div>
<Link href={`/home/${account}/events`}>
<Button variant="ghost" size="icon">
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
Kurse abgeschlossen
</p>
<p className="text-2xl font-bold">
{courseStats.completedCourses}
</p>
<p className="text-xs text-muted-foreground">
von {courseStats.totalCourses} insgesamt
</p>
</div>
<Link href={`/home/${account}/courses`}>
<Button variant="ghost" size="icon">
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
</div>
</CmsPageShell>
);
}