feat: enhance member management features; add quick stats and search capabilities

This commit is contained in:
T. Zehetbauer
2026-04-02 22:56:04 +02:00
parent 0932c57fa1
commit f43770999f
35 changed files with 4370 additions and 159 deletions

View File

@@ -0,0 +1,178 @@
'use client';
import type { ReactNode } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import {
FileDown,
FileUp,
IdCard,
KeyRound,
LayoutList,
Settings,
Users,
} from 'lucide-react';
import {
MemberStatsBar,
MemberCommandPalette,
} from '@kit/member-management/components';
import { Badge } from '@kit/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { PageBody } from '@kit/ui/page';
import { cn } from '@kit/ui/utils';
interface MembersCmsLayoutClientProps {
header: ReactNode;
children: ReactNode;
account: string;
accountId: string;
stats: {
total: number;
active: number;
pending: number;
newThisYear: number;
pendingApplications: number;
};
}
export function MembersCmsLayoutClient({
header,
children,
account,
accountId,
stats,
}: MembersCmsLayoutClientProps) {
const pathname = usePathname();
const basePath = `/home/${account}/members-cms`;
const isOnMembersTab =
pathname.endsWith('/members-cms') ||
pathname.includes('/members-cms/new') ||
/\/members-cms\/[^/]+$/.test(pathname);
const isOnApplicationsTab = pathname.includes('/applications');
const isOnSubPage =
pathname.includes('/import') ||
pathname.includes('/edit') ||
(/\/members-cms\/[^/]+$/.test(pathname) &&
!pathname.endsWith('/members-cms'));
return (
<>
{header}
<PageBody>
<div className="space-y-4">
{/* Stats bar — only on main views */}
{!isOnSubPage && <MemberStatsBar stats={stats} />}
{/* Tab navigation + settings */}
{!isOnSubPage && (
<div className="flex items-center justify-between border-b">
<nav className="-mb-px flex gap-4">
<TabLink
href={basePath}
active={isOnMembersTab && !isOnApplicationsTab}
>
<Users className="size-4" />
Mitglieder
</TabLink>
<TabLink
href={`${basePath}/applications`}
active={isOnApplicationsTab}
>
<FileUp className="size-4" />
Aufnahmeanträge
{stats.pendingApplications > 0 && (
<Badge
variant="destructive"
className="ml-1 h-5 min-w-5 px-1 text-xs"
>
{stats.pendingApplications}
</Badge>
)}
</TabLink>
</nav>
<SettingsMenu basePath={basePath} />
</div>
)}
{children}
</div>
<MemberCommandPalette account={account} accountId={accountId} />
</PageBody>
</>
);
}
function TabLink({
href,
active,
children,
}: {
href: string;
active: boolean;
children: ReactNode;
}) {
return (
<Link
href={href}
className={cn(
'inline-flex items-center gap-1.5 border-b-2 px-1 pb-2 text-sm font-medium transition-colors',
active
? 'border-primary text-foreground'
: 'text-muted-foreground hover:text-foreground border-transparent',
)}
>
{children}
</Link>
);
}
function SettingsMenu({ basePath }: { basePath: string }) {
const router = useRouter();
const navigate = (path: string) => () => router.push(path);
return (
<DropdownMenu>
<DropdownMenuTrigger
className="hover:bg-muted inline-flex size-7 items-center justify-center rounded-lg"
data-test="members-settings-menu"
>
<Settings className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem onClick={navigate(`${basePath}/dues`)}>
<LayoutList className="mr-2 size-4" />
Beitragskategorien
</DropdownMenuItem>
<DropdownMenuItem onClick={navigate(`${basePath}/departments`)}>
<Users className="mr-2 size-4" />
Abteilungen
</DropdownMenuItem>
<DropdownMenuItem onClick={navigate(`${basePath}/cards`)}>
<IdCard className="mr-2 size-4" />
Mitgliedsausweise
</DropdownMenuItem>
<DropdownMenuItem onClick={navigate(`${basePath}/invitations`)}>
<KeyRound className="mr-2 size-4" />
Portal-Einladungen
</DropdownMenuItem>
<DropdownMenuItem onClick={navigate(`${basePath}/import`)}>
<FileDown className="mr-2 size-4" />
Import
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}