feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
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
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user