Files
myeasycms-v2/packages/features/member-management/src/components/members-list-view.tsx
T. Zehetbauer 5c5aaabae5
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped
refactor: remove obsolete member management API module
2026-04-03 14:08:31 +02:00

275 lines
9.0 KiB
TypeScript

'use client';
import { useCallback, useState } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import {
type RowSelectionState,
getCoreRowModel,
useReactTable,
flexRender,
} from '@tanstack/react-table';
import { ChevronLeft, ChevronRight, Plus, Users } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { MemberQuickPreview } from './member-quick-preview';
import { createMembersColumns, type MemberRow } from './members-table-columns';
import { MembersToolbar } from './members-toolbar';
interface MembersListViewProps {
data: Array<Record<string, unknown>>;
total: number;
page: number;
pageSize: number;
account: string;
accountId: string;
duesCategories: Array<{ id: string; name: string }>;
departments: Array<{ id: string; name: string; memberCount: number }>;
tags?: Array<{ id: string; name: string; color: string }>;
memberTags?: Record<
string,
Array<{ id: string; name: string; color: string }>
>;
}
export function MembersListView({
data,
total,
page,
pageSize,
account,
accountId,
duesCategories,
departments,
tags = [],
memberTags = {},
}: MembersListViewProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [previewMember, setPreviewMember] = useState<MemberRow | null>(null);
const members: MemberRow[] = data.map((m) => ({
id: String(m.id),
firstName: String(m.first_name ?? ''),
lastName: String(m.last_name ?? ''),
email: String(m.email ?? ''),
phone: String(m.phone ?? ''),
mobile: String(m.mobile ?? ''),
memberNumber: String(m.member_number ?? ''),
city: String(m.city ?? ''),
status: String(m.status ?? 'active'),
entryDate: String(m.entry_date ?? ''),
isHonorary: Boolean(m.is_honorary),
isFoundingMember: Boolean(m.is_founding_member),
isYouth: Boolean(m.is_youth),
tags: memberTags[String(m.id)] ?? [],
}));
const columns = createMembersColumns({
account,
onPreview: setPreviewMember,
onNavigate: (path) => router.push(path),
});
const table = useReactTable({
data: members,
columns,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
state: { rowSelection },
getRowId: (row) => row.id,
manualPagination: true,
pageCount: Math.ceil(total / pageSize),
});
const selectedIds = Object.keys(rowSelection).filter(
(key) => rowSelection[key],
);
const updateSearchParam = useCallback(
(key: string, value: string | null) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
if (key !== 'page') params.delete('page');
router.push(`?${params.toString()}`);
},
[router, searchParams],
);
const hasFilters = searchParams.get('q') || searchParams.get('status');
const totalPages = Math.ceil(total / pageSize);
return (
<div className="space-y-4">
<MembersToolbar
account={account}
accountId={accountId}
searchValue={searchParams.get('q') ?? ''}
statusFilter={searchParams.get('status') ?? ''}
onSearchChange={(value) => updateSearchParam('q', value || null)}
onStatusChange={(value) => updateSearchParam('status', value || null)}
selectedCount={selectedIds.length}
selectedIds={selectedIds}
departments={departments}
duesCategories={duesCategories}
tags={tags}
/>
{/* Table or empty state */}
{total === 0 && !hasFilters ? (
<EmptyState account={account} />
) : (
<>
<div className="overflow-auto rounded-md border">
<Table>
<TableHeader className="bg-muted/40 sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="whitespace-nowrap">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() ? 'selected' : undefined}
className="cursor-pointer transition-colors"
onClick={() => setPreviewMember(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-muted-foreground h-32 text-center"
>
<div className="space-y-1">
<p className="font-medium">Keine Mitglieder gefunden</p>
<p className="text-sm">
Versuchen Sie andere Suchbegriffe oder Filter.
</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<p className="text-muted-foreground text-sm tabular-nums">
{total} Mitglieder
{selectedIds.length > 0 && `${selectedIds.length} ausgewählt`}
</p>
{totalPages > 1 && (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={page <= 1}
onClick={() => updateSearchParam('page', String(page - 1))}
data-test="members-prev-page"
>
<ChevronLeft className="size-4" />
</Button>
<span className="text-muted-foreground text-sm tabular-nums">
Seite {page} von {totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages}
onClick={() => updateSearchParam('page', String(page + 1))}
data-test="members-next-page"
>
<ChevronRight className="size-4" />
</Button>
</div>
)}
</div>
</>
)}
{/* Quick preview sheet */}
<MemberQuickPreview
member={previewMember}
account={account}
open={!!previewMember}
onOpenChange={(open) => {
if (!open) setPreviewMember(null);
}}
/>
</div>
);
}
/* ─── Empty State ─── */
function EmptyState({ account }: { account: string }) {
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16">
<div className="bg-muted mb-4 flex size-16 items-center justify-center rounded-full">
<Users className="text-muted-foreground size-8" />
</div>
<h3 className="text-lg font-semibold">Noch keine Mitglieder</h3>
<p className="text-muted-foreground mt-1 max-w-sm text-center text-sm">
Erstellen Sie Ihr erstes Mitglied oder importieren Sie bestehende
Mitgliederdaten aus einer CSV-Datei.
</p>
<div className="mt-6 flex gap-3">
<Link
href={`/home/${account}/members-cms/new`}
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-8 items-center gap-1.5 rounded-lg px-3 text-sm font-medium transition-colors"
data-test="empty-create-member"
>
<Plus className="size-4" />
Mitglied erstellen
</Link>
<Link
href={`/home/${account}/members-cms/import`}
className="border-border bg-background hover:bg-muted inline-flex h-8 items-center gap-1.5 rounded-lg border px-3 text-sm font-medium transition-colors"
>
CSV importieren
</Link>
</div>
</div>
);
}