Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/page.tsx
T. Zehetbauer 7b078f298b
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m50s
Workflow / ⚫️ Test (push) Has been skipped
feat: enhance API response handling and add new components for module management
2026-04-01 15:18:24 +02:00

380 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 { 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>
);
}