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
211 lines
7.3 KiB
TypeScript
211 lines
7.3 KiB
TypeScript
'use client';
|
||
|
||
import Link from 'next/link';
|
||
|
||
import {
|
||
FileText,
|
||
Calendar,
|
||
ListChecks,
|
||
AlertTriangle,
|
||
} from 'lucide-react';
|
||
|
||
import { Badge } from '@kit/ui/badge';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||
|
||
import { MEETING_TYPE_LABELS } from '../lib/meetings-constants';
|
||
|
||
interface DashboardStats {
|
||
totalProtocols: number;
|
||
thisYearProtocols: number;
|
||
openTasks: number;
|
||
overdueTasks: number;
|
||
}
|
||
|
||
interface RecentProtocol {
|
||
id: string;
|
||
title: string;
|
||
meeting_date: string;
|
||
meeting_type: string;
|
||
is_published: boolean;
|
||
}
|
||
|
||
interface OverdueTask {
|
||
id: string;
|
||
title: string;
|
||
responsible_person: string | null;
|
||
due_date: string | null;
|
||
status: string;
|
||
meeting_protocols: {
|
||
id: string;
|
||
title: string;
|
||
};
|
||
}
|
||
|
||
interface MeetingsDashboardProps {
|
||
stats: DashboardStats;
|
||
recentProtocols: RecentProtocol[];
|
||
overdueTasks: OverdueTask[];
|
||
account: string;
|
||
}
|
||
|
||
export function MeetingsDashboard({
|
||
stats,
|
||
recentProtocols,
|
||
overdueTasks,
|
||
account,
|
||
}: MeetingsDashboardProps) {
|
||
return (
|
||
<div className="flex w-full flex-col gap-6">
|
||
{/* Header */}
|
||
<div>
|
||
<h1 className="text-2xl font-bold">Sitzungsprotokolle – Übersicht</h1>
|
||
<p className="text-muted-foreground">
|
||
Protokolle, Tagesordnungspunkte und Aufgaben verwalten
|
||
</p>
|
||
</div>
|
||
|
||
{/* Stats Grid */}
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||
<Link href={`/home/${account}/meetings/protocols`}>
|
||
<Card className="cursor-pointer transition-shadow hover:shadow-md">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-muted-foreground">Protokolle gesamt</p>
|
||
<p className="text-2xl font-bold">{stats.totalProtocols}</p>
|
||
</div>
|
||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||
<FileText className="h-5 w-5" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
|
||
<Link href={`/home/${account}/meetings/protocols`}>
|
||
<Card className="cursor-pointer transition-shadow hover:shadow-md">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-muted-foreground">Protokolle dieses Jahr</p>
|
||
<p className="text-2xl font-bold">{stats.thisYearProtocols}</p>
|
||
</div>
|
||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||
<Calendar className="h-5 w-5" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
|
||
<Link href={`/home/${account}/meetings/tasks`}>
|
||
<Card className="cursor-pointer transition-shadow hover:shadow-md">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-muted-foreground">Offene Aufgaben</p>
|
||
<p className="text-2xl font-bold">{stats.openTasks}</p>
|
||
</div>
|
||
<div className="rounded-full bg-primary/10 p-3 text-primary">
|
||
<ListChecks className="h-5 w-5" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
|
||
<Link href={`/home/${account}/meetings/tasks`}>
|
||
<Card className="cursor-pointer transition-shadow hover:shadow-md">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-muted-foreground">Überfällige Aufgaben</p>
|
||
<p className="text-2xl font-bold">{stats.overdueTasks}</p>
|
||
</div>
|
||
<div className="rounded-full bg-destructive/10 p-3 text-destructive">
|
||
<AlertTriangle className="h-5 w-5" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Recent + Overdue Sections */}
|
||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">Letzte Protokolle</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{recentProtocols.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">
|
||
Noch keine Protokolle vorhanden.
|
||
</p>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{recentProtocols.map((protocol) => (
|
||
<Link
|
||
key={protocol.id}
|
||
href={`/home/${account}/meetings/protocols/${protocol.id}`}
|
||
className="flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50"
|
||
>
|
||
<div className="min-w-0 flex-1">
|
||
<p className="truncate text-sm font-medium">{protocol.title}</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{new Date(protocol.meeting_date).toLocaleDateString('de-DE')}
|
||
{' · '}
|
||
{MEETING_TYPE_LABELS[protocol.meeting_type] ?? protocol.meeting_type}
|
||
</p>
|
||
</div>
|
||
{protocol.is_published && (
|
||
<Badge variant="default" className="ml-2 shrink-0">Veröffentlicht</Badge>
|
||
)}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">Überfällige Aufgaben</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{overdueTasks.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">
|
||
Keine überfälligen Aufgaben.
|
||
</p>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{overdueTasks.map((task) => (
|
||
<div
|
||
key={task.id}
|
||
className="rounded-lg border border-destructive/20 bg-destructive/5 p-3"
|
||
>
|
||
<p className="text-sm font-medium">{task.title}</p>
|
||
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
|
||
{task.responsible_person && (
|
||
<span>Zuständig: {task.responsible_person}</span>
|
||
)}
|
||
{task.due_date && (
|
||
<span>
|
||
Fällig: {new Date(task.due_date).toLocaleDateString('de-DE')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="mt-1 text-xs text-muted-foreground">
|
||
Protokoll: {task.meeting_protocols.title}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|