314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
import Link from 'next/link';
|
||
|
||
import {
|
||
ArrowRight,
|
||
FileText,
|
||
GraduationCap,
|
||
Mail,
|
||
Plus,
|
||
UserCheck,
|
||
UserPlus,
|
||
CalendarDays,
|
||
Activity,
|
||
BedDouble,
|
||
} from 'lucide-react';
|
||
import { getTranslations } from 'next-intl/server';
|
||
|
||
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 { createMemberServices } from '@kit/member-management/services';
|
||
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 t = await getTranslations('common');
|
||
|
||
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([
|
||
createMemberServices(client).query.getStatistics(acct.id),
|
||
createCourseManagementApi(client).statistics.getQuickStats(acct.id),
|
||
createFinanceApi(client).listInvoices(acct.id, { status: 'draft' }),
|
||
createNewsletterApi(client).listNewsletters(acct.id),
|
||
createBookingManagementApi(client).bookings.list(acct.id, { page: 1 }),
|
||
createEventManagementApi(client).events.list(acct.id, { page: 1 }),
|
||
]);
|
||
|
||
const memberStatsRaw =
|
||
memberStatsResult.status === 'fulfilled' ? memberStatsResult.value : {};
|
||
const memberStats = {
|
||
total: Object.values(memberStatsRaw).reduce((a, b) => a + b, 0),
|
||
active: memberStatsRaw.active ?? 0,
|
||
inactive: memberStatsRaw.inactive ?? 0,
|
||
pending: memberStatsRaw.pending ?? 0,
|
||
resigned: memberStatsRaw.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={t('dashboard.members')}
|
||
value={memberStats.active}
|
||
icon={<UserCheck className="h-5 w-5" aria-hidden="true" />}
|
||
description={t('dashboard.membersDescription', {
|
||
total: memberStats.total,
|
||
pending: memberStats.pending,
|
||
})}
|
||
/>
|
||
<StatsCard
|
||
title={t('dashboard.courses')}
|
||
value={courseStats.openCourses}
|
||
icon={<GraduationCap className="h-5 w-5" aria-hidden="true" />}
|
||
description={t('dashboard.coursesDescription', {
|
||
total: courseStats.totalCourses,
|
||
participants: courseStats.totalParticipants,
|
||
})}
|
||
/>
|
||
<StatsCard
|
||
title={t('dashboard.openInvoices')}
|
||
value={openInvoices.length}
|
||
icon={<FileText className="h-5 w-5" aria-hidden="true" />}
|
||
description={t('dashboard.openInvoicesDescription')}
|
||
/>
|
||
<StatsCard
|
||
title={t('dashboard.newsletters')}
|
||
value={newsletters.length}
|
||
icon={<Mail className="h-5 w-5" aria-hidden="true" />}
|
||
description={t('dashboard.newslettersDescription')}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||
{/* Recent Activity */}
|
||
<Card className="lg:col-span-2">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Activity className="h-5 w-5" aria-hidden="true" />
|
||
{t('dashboard.recentActivity')}
|
||
</CardTitle>
|
||
<CardDescription>
|
||
{t('dashboard.recentActivityDescription')}
|
||
</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" aria-hidden="true" />
|
||
</div>
|
||
<div>
|
||
<Link
|
||
href={`/home/${account}/bookings/${String(booking.id)}`}
|
||
className="text-sm font-medium hover:underline"
|
||
>
|
||
{t('dashboard.bookingFrom')}{' '}
|
||
{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"
|
||
aria-hidden="true"
|
||
/>
|
||
</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" aria-hidden="true" />}
|
||
title={t('dashboard.recentActivityEmpty')}
|
||
description={t('dashboard.recentActivityEmptyDescription')}
|
||
/>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Quick Actions */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>{t('dashboard.quickActions')}</CardTitle>
|
||
<CardDescription>
|
||
{t('dashboard.quickActionsDescription')}
|
||
</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" aria-hidden="true" />
|
||
{t('dashboard.newMember')}
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||
</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" aria-hidden="true" />
|
||
{t('dashboard.newCourse')}
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||
</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" />
|
||
{t('dashboard.createNewsletter')}
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||
</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" aria-hidden="true" />
|
||
{t('dashboard.newBooking')}
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||
</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" aria-hidden="true" />
|
||
{t('dashboard.newEvent')}
|
||
</span>
|
||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||
</Link>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</CmsPageShell>
|
||
);
|
||
}
|