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

@@ -1,10 +1,14 @@
import { createClient } from '@supabase/supabase-js';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Button } from '@kit/ui/button';
import { Badge } from '@kit/ui/badge';
import { FileText, Download, Shield, Receipt, FileCheck } from 'lucide-react';
import Link from 'next/link';
import { createClient } from '@supabase/supabase-js';
import { FileText, Download, Shield, Receipt, FileCheck } from 'lucide-react';
import { formatDate } from '@kit/shared/dates';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
interface Props {
params: Promise<{ slug: string }>;
}
@@ -14,77 +18,117 @@ export default async function PortalDocumentsPage({ params }: Props) {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!,
process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!,
);
const { data: account } = await supabase.from('accounts').select('id, name').eq('slug', slug).single();
if (!account) return <div className="p-8 text-center">Organisation nicht gefunden</div>;
const { data: account } = await supabase
.from('accounts')
.select('id, name')
.eq('slug', slug)
.single();
if (!account)
return <div className="p-8 text-center">Organisation nicht gefunden</div>;
// Demo documents (in production: query invoices + cms_files for this member)
const documents = [
{ id: '1', title: 'Mitgliedsbeitrag 2026', type: 'Rechnung', date: '2026-01-15', status: 'paid' },
{ id: '2', title: 'Mitgliedsbeitrag 2025', type: 'Rechnung', date: '2025-01-10', status: 'paid' },
{ id: '3', title: 'Beitrittserklärung', type: 'Dokument', date: '2020-01-15', status: 'signed' },
{
id: '1',
title: 'Mitgliedsbeitrag 2026',
type: 'Rechnung',
date: '2026-01-15',
status: 'paid',
},
{
id: '2',
title: 'Mitgliedsbeitrag 2025',
type: 'Rechnung',
date: '2025-01-10',
status: 'paid',
},
{
id: '3',
title: 'Beitrittserklärung',
type: 'Dokument',
date: '2020-01-15',
status: 'signed',
},
];
const getStatusBadge = (status: string) => {
switch (status) {
case 'paid': return <Badge variant="default">Bezahlt</Badge>;
case 'open': return <Badge variant="secondary">Offen</Badge>;
case 'signed': return <Badge variant="outline">Unterschrieben</Badge>;
default: return <Badge variant="secondary">{status}</Badge>;
case 'paid':
return <Badge variant="default">Bezahlt</Badge>;
case 'open':
return <Badge variant="secondary">Offen</Badge>;
case 'signed':
return <Badge variant="outline">Unterschrieben</Badge>;
default:
return <Badge variant="secondary">{status}</Badge>;
}
};
const getIcon = (type: string) => {
switch (type) {
case 'Rechnung': return <Receipt className="h-5 w-5 text-primary" />;
case 'Dokument': return <FileCheck className="h-5 w-5 text-primary" />;
default: return <FileText className="h-5 w-5 text-primary" />;
case 'Rechnung':
return <Receipt className="text-primary h-5 w-5" />;
case 'Dokument':
return <FileCheck className="text-primary h-5 w-5" />;
default:
return <FileText className="text-primary h-5 w-5" />;
}
};
return (
<div className="min-h-screen bg-muted/30">
<header className="border-b bg-background px-6 py-4">
<div className="flex items-center justify-between max-w-4xl mx-auto">
<div className="bg-muted/30 min-h-screen">
<header className="bg-background border-b px-6 py-4">
<div className="mx-auto flex max-w-4xl items-center justify-between">
<div className="flex items-center gap-3">
<Shield className="h-5 w-5 text-primary" />
<Shield className="text-primary h-5 w-5" />
<h1 className="text-lg font-bold">Meine Dokumente</h1>
</div>
<Link href={`/club/${slug}/portal`}>
<Button variant="ghost" size="sm"> Zurück zum Portal</Button>
<Button variant="ghost" size="sm">
Zurück zum Portal
</Button>
</Link>
</div>
</header>
<main className="max-w-3xl mx-auto py-8 px-6">
<main className="mx-auto max-w-3xl px-6 py-8">
<Card>
<CardHeader>
<CardTitle>Verfügbare Dokumente</CardTitle>
<p className="text-sm text-muted-foreground">{String(account.name)} Dokumente und Rechnungen</p>
<p className="text-muted-foreground text-sm">
{String(account.name)} Dokumente und Rechnungen
</p>
</CardHeader>
<CardContent>
{documents.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<FileText className="mx-auto h-10 w-10 mb-3" />
<div className="text-muted-foreground py-8 text-center">
<FileText className="mx-auto mb-3 h-10 w-10" />
<p>Keine Dokumente vorhanden</p>
</div>
) : (
<div className="space-y-3">
{documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between rounded-lg border p-4 hover:bg-muted/30 transition-colors">
<div
key={doc.id}
className="hover:bg-muted/30 flex items-center justify-between rounded-lg border p-4 transition-colors"
>
<div className="flex items-center gap-3">
{getIcon(doc.type)}
<div>
<p className="font-medium text-sm">{doc.title}</p>
<p className="text-xs text-muted-foreground">{doc.type} {new Date(doc.date).toLocaleDateString('de-DE')}</p>
<p className="text-sm font-medium">{doc.title}</p>
<p className="text-muted-foreground text-xs">
{doc.type} {formatDate(doc.date)}
</p>
</div>
</div>
<div className="flex items-center gap-3">
{getStatusBadge(doc.status)}
<Button size="sm" variant="outline">
<Download className="h-3 w-3 mr-1" />
<Download className="mr-1 h-3 w-3" />
PDF
</Button>
</div>