380 lines
14 KiB
TypeScript
380 lines
14 KiB
TypeScript
import Link from 'next/link';
|
||
|
||
import {
|
||
ArrowRight,
|
||
FileText,
|
||
GraduationCap,
|
||
Mail,
|
||
Plus,
|
||
UserCheck,
|
||
UserPlus,
|
||
CalendarDays,
|
||
Activity,
|
||
BedDouble,
|
||
} from 'lucide-react';
|
||
|
||
import { createBookingManagementApi } from '@kit/booking-management/api';
|
||
import { createCourseManagementApi } from '@kit/course-management/api';
|
||
import { createEventManagementApi } from '@kit/event-management/api';
|
||
import { createFinanceApi } from '@kit/finance/api';
|
||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||
import { createNewsletterApi } from '@kit/newsletter/api';
|
||
import { formatDate } from '@kit/shared/dates';
|
||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||
import { Badge } from '@kit/ui/badge';
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from '@kit/ui/card';
|
||
|
||
import { AccountNotFound } from '~/components/account-not-found';
|
||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||
import { EmptyState } from '~/components/empty-state';
|
||
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 <AccountNotFound />;
|
||
|
||
// 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.data : [];
|
||
|
||
const newsletters =
|
||
newslettersResult.status === 'fulfilled'
|
||
? newslettersResult.value.data
|
||
: [];
|
||
|
||
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{' '}
|
||
{booking.check_in
|
||
? formatDate(booking.check_in as string)
|
||
: '—'}
|
||
</Link>
|
||
<p className="text-muted-foreground text-xs">
|
||
{formatDate(booking.check_in as string)} –{' '}
|
||
{formatDate(booking.check_out as string)}
|
||
</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-muted-foreground text-xs">
|
||
{formatDate(event.event_date as string)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Badge variant="outline">
|
||
{String(event.status ?? '—')}
|
||
</Badge>
|
||
</div>
|
||
))}
|
||
|
||
{bookings.data.length === 0 && events.data.length === 0 && (
|
||
<EmptyState
|
||
icon={<Activity className="h-8 w-8" />}
|
||
title="Noch keine Aktivitäten"
|
||
description="Aktuelle Buchungen und Veranstaltungen werden hier angezeigt."
|
||
/>
|
||
)}
|
||
</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`}
|
||
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<UserPlus className="h-4 w-4" />
|
||
Neues Mitglied
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
|
||
<Link
|
||
href={`/home/${account}/courses/new`}
|
||
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<GraduationCap className="h-4 w-4" />
|
||
Neuer Kurs
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
|
||
<Link
|
||
href={`/home/${account}/newsletter/new`}
|
||
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<Mail className="h-4 w-4" />
|
||
Newsletter erstellen
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
|
||
<Link
|
||
href={`/home/${account}/bookings/new`}
|
||
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<BedDouble className="h-4 w-4" />
|
||
Neue Buchung
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
|
||
<Link
|
||
href={`/home/${account}/events/new`}
|
||
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<Plus className="h-4 w-4" />
|
||
Neue Veranstaltung
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</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-muted-foreground text-sm font-medium">
|
||
Buchungen
|
||
</p>
|
||
<p className="text-2xl font-bold">{bookings.total}</p>
|
||
<p className="text-muted-foreground text-xs">
|
||
{
|
||
bookings.data.filter(
|
||
(b: Record<string, unknown>) =>
|
||
b.status === 'confirmed' || b.status === 'checked_in',
|
||
).length
|
||
}{' '}
|
||
aktiv
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href={`/home/${account}/bookings`}
|
||
className="hover:bg-muted hover:text-foreground inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium transition-all"
|
||
>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-muted-foreground text-sm font-medium">
|
||
Veranstaltungen
|
||
</p>
|
||
<p className="text-2xl font-bold">{events.total}</p>
|
||
<p className="text-muted-foreground text-xs">
|
||
{
|
||
events.data.filter(
|
||
(e: Record<string, unknown>) =>
|
||
e.status === 'published' ||
|
||
e.status === 'registration_open',
|
||
).length
|
||
}{' '}
|
||
aktiv
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href={`/home/${account}/events`}
|
||
className="hover:bg-muted hover:text-foreground inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium transition-all"
|
||
>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-muted-foreground text-sm font-medium">
|
||
Kurse abgeschlossen
|
||
</p>
|
||
<p className="text-2xl font-bold">
|
||
{courseStats.completedCourses}
|
||
</p>
|
||
<p className="text-muted-foreground text-xs">
|
||
von {courseStats.totalCourses} insgesamt
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href={`/home/${account}/courses`}
|
||
className="hover:bg-muted hover:text-foreground inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium transition-all"
|
||
>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</CmsPageShell>
|
||
);
|
||
}
|