Major changes: - Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI - Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions - Sitzungsprotokolle module: meeting protocols, agenda items, task tracking - Verbandsverwaltung module: federation management, member clubs, contacts, fees - Per-account module activation via Modules page toggle - Site Builder: live CMS data in Puck blocks (courses, events, membership registration) - Public registration APIs: course signup, event registration, membership application - Document generation: PDF member cards, Excel reports, HTML labels - Landing page: real Com.BISS content (no filler text) - UX audit fixes: AccountNotFound component, shared status badges, confirm dialog, pagination, duplicate heading removal, emoji→badge replacement, a11y fixes - QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
384 lines
15 KiB
TypeScript
384 lines
15 KiB
TypeScript
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 {
|
||
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 { EmptyState } from '~/components/empty-state';
|
||
import { StatsCard } from '~/components/stats-card';
|
||
import { AccountNotFound } from '~/components/account-not-found';
|
||
|
||
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 : [];
|
||
|
||
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{' '}
|
||
{booking.check_in
|
||
? new Date(
|
||
String(booking.check_in),
|
||
).toLocaleDateString('de-DE', {
|
||
day: '2-digit',
|
||
month: 'short',
|
||
})
|
||
: '—'}
|
||
</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 && (
|
||
<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="inline-flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-medium hover:bg-muted hover:text-foreground 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="inline-flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-medium hover:bg-muted hover:text-foreground 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="inline-flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-medium hover:bg-muted hover:text-foreground 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="inline-flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-medium hover:bg-muted hover:text-foreground 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="inline-flex w-full items-center justify-between gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-medium hover:bg-muted hover:text-foreground 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-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`}
|
||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium hover:bg-muted hover:text-foreground 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-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`}
|
||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium hover:bg-muted hover:text-foreground 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-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`}
|
||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium hover:bg-muted hover:text-foreground transition-all"
|
||
>
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</CmsPageShell>
|
||
);
|
||
}
|