Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/page.tsx
Zaid Marzguioui ebd0fd4638
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m26s
Workflow / ⚫️ Test (push) Has been skipped
feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
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
2026-03-31 16:35:46 +02:00

384 lines
15 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 {
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>
);
}