Commits all remaining uncommitted local work: - apps/web: fischerei, verband, modules, members-cms, documents, newsletter, meetings, site-builder, courses, bookings, events, finance pages and components - apps/web: marketing page updates, layout, paths config, next.config.mjs, styles/makerkit.css - apps/web/i18n: documents, fischerei, marketing, verband (de+en) - packages/features: finance, fischerei, member-management, module-builder, newsletter, sitzungsprotokolle, verbandsverwaltung server APIs and components - packages/ui: button.tsx updates - pnpm-lock.yaml
219 lines
7.6 KiB
TypeScript
219 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback } from 'react';
|
|
|
|
import Link from 'next/link';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
|
|
import { Pencil, Plus } from 'lucide-react';
|
|
|
|
import { formatDate } from '@kit/shared/dates';
|
|
import { Button } from '@kit/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
|
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
|
|
|
import { deleteCompetition } from '../server/actions/fischerei-actions';
|
|
import { DeleteConfirmButton } from './delete-confirm-button';
|
|
|
|
interface CompetitionsDataTableProps {
|
|
data: Array<Record<string, unknown>>;
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
account: string;
|
|
accountId: string;
|
|
}
|
|
|
|
export function CompetitionsDataTable({
|
|
data,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
account,
|
|
accountId,
|
|
}: CompetitionsDataTableProps) {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
|
|
const { execute: executeDelete, isPending: isDeleting } = useActionWithToast(
|
|
deleteCompetition,
|
|
{
|
|
successMessage: 'Wettbewerb gelöscht',
|
|
onSuccess: () => router.refresh(),
|
|
},
|
|
);
|
|
|
|
const updateParams = useCallback(
|
|
(updates: Record<string, string>) => {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
if (value) {
|
|
params.set(key, value);
|
|
} else {
|
|
params.delete(key);
|
|
}
|
|
}
|
|
if (!('page' in updates)) params.delete('page');
|
|
router.push(`?${params.toString()}`);
|
|
},
|
|
[router, searchParams],
|
|
);
|
|
|
|
const handlePageChange = useCallback(
|
|
(newPage: number) => {
|
|
updateParams({ page: String(newPage) });
|
|
},
|
|
[updateParams],
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Toolbar */}
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold">Wettbewerbe ({total})</h2>
|
|
<Link href={`/home/${account}/fischerei/competitions/new`}>
|
|
<Button size="sm" data-test="competitions-new-btn">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Neuer Wettbewerb
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
{data.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 Wettbewerbe vorhanden
|
|
</h3>
|
|
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
|
|
Erstellen Sie Ihren ersten Wettbewerb.
|
|
</p>
|
|
<Link
|
|
href={`/home/${account}/fischerei/competitions/new`}
|
|
className="mt-4"
|
|
>
|
|
<Button>Neuer Wettbewerb</Button>
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-muted/50 border-b">
|
|
<th scope="col" className="p-3 text-left font-medium">
|
|
Name
|
|
</th>
|
|
<th scope="col" className="p-3 text-left font-medium">
|
|
Datum
|
|
</th>
|
|
<th scope="col" className="p-3 text-left font-medium">
|
|
Gewässer
|
|
</th>
|
|
<th scope="col" className="p-3 text-right font-medium">
|
|
Max. Teilnehmer
|
|
</th>
|
|
<th scope="col" className="p-3 text-right font-medium">
|
|
Aktionen
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.map((comp) => {
|
|
const waters = comp.waters as Record<
|
|
string,
|
|
unknown
|
|
> | null;
|
|
|
|
return (
|
|
<tr
|
|
key={String(comp.id)}
|
|
className="hover:bg-muted/30 cursor-pointer border-b"
|
|
onClick={() =>
|
|
router.push(
|
|
`/home/${account}/fischerei/competitions/${String(comp.id)}`,
|
|
)
|
|
}
|
|
>
|
|
<td className="p-3 font-medium">{String(comp.name)}</td>
|
|
<td className="p-3">
|
|
{formatDate(comp.competition_date as string | null)}
|
|
</td>
|
|
<td className="p-3">
|
|
{waters ? String(waters.name) : '—'}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
{comp.max_participants != null
|
|
? String(comp.max_participants)
|
|
: '—'}
|
|
</td>
|
|
<td className="p-3 text-right">
|
|
<div className="flex items-center justify-end gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
aria-label="Wettkampf bearbeiten"
|
|
data-test="competition-edit-btn"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
router.push(
|
|
`/home/${account}/fischerei/competitions/${String(comp.id)}`,
|
|
);
|
|
}}
|
|
>
|
|
<Pencil className="h-4 w-4" aria-hidden="true" />
|
|
</Button>
|
|
<DeleteConfirmButton
|
|
title="Wettbewerb löschen"
|
|
description="Möchten Sie diesen Wettbewerb wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden."
|
|
isPending={isDeleting}
|
|
onConfirm={() =>
|
|
executeDelete({
|
|
competitionId: String(comp.id),
|
|
accountId,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{totalPages > 1 && (
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<p className="text-muted-foreground text-sm">
|
|
Seite {page} von {totalPages} ({total} Einträge)
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={page <= 1}
|
|
onClick={() => handlePageChange(page - 1)}
|
|
>
|
|
Zurück
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={page >= totalPages}
|
|
onClick={() => handlePageChange(page + 1)}
|
|
>
|
|
Weiter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|