269 lines
9.2 KiB
TypeScript
269 lines
9.2 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Building2,
|
|
Users,
|
|
CalendarDays,
|
|
BookOpen,
|
|
Euro,
|
|
TrendingUp,
|
|
} from 'lucide-react';
|
|
|
|
import { formatNumber, formatCurrencyAmount } from '@kit/shared/formatters';
|
|
import { Badge } from '@kit/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
|
|
|
interface HierarchyReportProps {
|
|
summary: {
|
|
total_orgs: number;
|
|
total_active_members: number;
|
|
total_members: number;
|
|
new_members_this_year: number;
|
|
total_upcoming_events: number;
|
|
total_active_courses: number;
|
|
total_open_invoices: number;
|
|
total_open_invoice_amount: number;
|
|
total_sepa_batches_this_year: number;
|
|
};
|
|
report: Array<{
|
|
org_id: string;
|
|
org_name: string;
|
|
org_slug: string | null;
|
|
depth: number;
|
|
active_members: number;
|
|
inactive_members: number;
|
|
total_members: number;
|
|
new_members_this_year: number;
|
|
active_courses: number;
|
|
upcoming_events: number;
|
|
open_invoices: number;
|
|
open_invoice_amount: number;
|
|
sepa_batches_this_year: number;
|
|
}>;
|
|
}
|
|
|
|
function getDepthLabel(depth: number) {
|
|
switch (depth) {
|
|
case 0:
|
|
return 'Verband';
|
|
case 1:
|
|
return 'Unterverband';
|
|
default:
|
|
return 'Verein';
|
|
}
|
|
}
|
|
|
|
function getDepthVariant(depth: number) {
|
|
switch (depth) {
|
|
case 0:
|
|
return 'default' as const;
|
|
case 1:
|
|
return 'secondary' as const;
|
|
default:
|
|
return 'outline' as const;
|
|
}
|
|
}
|
|
|
|
export function HierarchyReport({ summary, report }: HierarchyReportProps) {
|
|
return (
|
|
<div className="flex w-full flex-col gap-6">
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Organisationen
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{formatNumber(summary.total_orgs)}
|
|
</p>
|
|
</div>
|
|
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
<Building2 className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Aktive Mitglieder
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{formatNumber(summary.total_active_members)}
|
|
</p>
|
|
<p className="text-muted-foreground text-xs">
|
|
von {formatNumber(summary.total_members)} gesamt
|
|
</p>
|
|
</div>
|
|
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
<Users className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Neue Mitglieder (Jahr)
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{formatNumber(summary.new_members_this_year)}
|
|
</p>
|
|
</div>
|
|
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
<TrendingUp className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Anstehende Termine
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{formatNumber(summary.total_upcoming_events)}
|
|
</p>
|
|
</div>
|
|
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
<CalendarDays className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Aktive Kurse
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{formatNumber(summary.total_active_courses)}
|
|
</p>
|
|
</div>
|
|
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
<BookOpen className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
Offene Rechnungen
|
|
</p>
|
|
<p className="text-2xl font-bold">
|
|
{formatCurrencyAmount(summary.total_open_invoice_amount)}
|
|
</p>
|
|
<p className="text-muted-foreground text-xs">
|
|
{formatNumber(summary.total_open_invoices)} Rechnungen
|
|
</p>
|
|
</div>
|
|
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
<Euro className="h-5 w-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Per-Org Report Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Bericht pro Organisation</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{report.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
|
<h3 className="text-lg font-semibold">
|
|
Keine Organisationen vorhanden
|
|
</h3>
|
|
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
|
Die Hierarchie enthält noch keine Organisationen.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto rounded-md border">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-muted/50 border-b">
|
|
<th className="p-3 text-left font-medium">Name</th>
|
|
<th className="p-3 text-left font-medium">Ebene</th>
|
|
<th className="p-3 text-right font-medium">
|
|
Aktive Mitgl.
|
|
</th>
|
|
<th className="p-3 text-right font-medium">Gesamt</th>
|
|
<th className="p-3 text-right font-medium">Neu (Jahr)</th>
|
|
<th className="p-3 text-right font-medium">Kurse</th>
|
|
<th className="p-3 text-right font-medium">Termine</th>
|
|
<th className="p-3 text-right font-medium">
|
|
Offene Rechn.
|
|
</th>
|
|
<th className="p-3 text-right font-medium">
|
|
Offener Betrag
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{report.map((row) => (
|
|
<tr key={row.org_id} className="hover:bg-muted/30 border-b">
|
|
<td className="p-3 font-medium">
|
|
<span style={{ paddingLeft: `${row.depth * 1.25}rem` }}>
|
|
{row.org_name}
|
|
</span>
|
|
</td>
|
|
<td className="p-3">
|
|
<Badge variant={getDepthVariant(row.depth)}>
|
|
{getDepthLabel(row.depth)}
|
|
</Badge>
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatNumber(row.active_members)}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatNumber(row.total_members)}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatNumber(row.new_members_this_year)}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatNumber(row.active_courses)}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatNumber(row.upcoming_events)}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatNumber(row.open_invoices)}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{formatCurrencyAmount(row.open_invoice_amount)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|