Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -13,9 +13,15 @@ import {
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,
@@ -24,17 +30,10 @@ import {
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 { 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';
import { AccountNotFound } from '~/components/account-not-found';
interface TeamAccountHomePageProps {
params: Promise<{ account: string }>;
@@ -79,7 +78,12 @@ export default async function TeamAccountHomePage({
const courseStats =
courseStatsResult.status === 'fulfilled'
? courseStatsResult.value
: { totalCourses: 0, openCourses: 0, completedCourses: 0, totalParticipants: 0 };
: {
totalCourses: 0,
openCourses: 0,
completedCourses: 0,
totalParticipants: 0,
};
const openInvoices =
invoicesResult.status === 'fulfilled' ? invoicesResult.value : [];
@@ -145,82 +149,68 @@ export default async function TeamAccountHomePage({
<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>
{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>
<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>
{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>
<Badge variant="outline">
{String(event.status ?? '—')}
</Badge>
</div>
))}
))}
{bookings.data.length === 0 && events.data.length === 0 && (
<EmptyState
@@ -242,7 +232,7 @@ export default async function TeamAccountHomePage({
<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"
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" />
@@ -253,7 +243,7 @@ export default async function TeamAccountHomePage({
<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"
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" />
@@ -264,7 +254,7 @@ export default async function TeamAccountHomePage({
<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"
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" />
@@ -275,7 +265,7 @@ export default async function TeamAccountHomePage({
<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"
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" />
@@ -286,7 +276,7 @@ export default async function TeamAccountHomePage({
<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"
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" />
@@ -304,21 +294,23 @@ export default async function TeamAccountHomePage({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
<p className="text-muted-foreground text-sm font-medium">
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}{' '}
<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="inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium hover:bg-muted hover:text-foreground transition-all"
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>
@@ -330,22 +322,24 @@ export default async function TeamAccountHomePage({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
<p className="text-muted-foreground text-sm font-medium">
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}{' '}
<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="inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium hover:bg-muted hover:text-foreground transition-all"
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>
@@ -357,19 +351,19 @@ export default async function TeamAccountHomePage({
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
<p className="text-muted-foreground text-sm font-medium">
Kurse abgeschlossen
</p>
<p className="text-2xl font-bold">
{courseStats.completedCourses}
</p>
<p className="text-xs text-muted-foreground">
<p className="text-muted-foreground text-xs">
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"
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>