feat: add cross-organization member search and template cloning functionality
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user