feat: add delete functionality for leases, catch books, and permits; implement newsletter update feature
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m52s
Workflow / ⚫️ Test (push) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-01 17:53:39 +02:00
parent c6b2824da8
commit 080ec1cb47
22 changed files with 798 additions and 210 deletions

View File

@@ -0,0 +1,94 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react';
import { createSession } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
interface Props {
courseId: string;
}
export function CreateSessionDialog({ courseId }: Props) {
const router = useRouter();
const [open, setOpen] = useState(false);
const { execute, isPending } = useActionWithToast(createSession, {
successMessage: 'Termin erstellt',
errorMessage: 'Fehler beim Erstellen',
onSuccess: () => {
setOpen(false);
router.refresh();
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Neuer Termin
</Button>
</DialogTrigger>
<DialogContent>
<form
onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
execute({
courseId,
sessionDate: fd.get('sessionDate') as string,
startTime: fd.get('startTime') as string,
endTime: fd.get('endTime') as string,
notes: (fd.get('notes') as string) || undefined,
});
}}
>
<DialogHeader>
<DialogTitle>Neuen Termin erstellen</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="sessionDate">Datum *</Label>
<Input id="sessionDate" name="sessionDate" type="date" required />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startTime">Beginn *</Label>
<Input id="startTime" name="startTime" type="time" required />
</div>
<div className="space-y-2">
<Label htmlFor="endTime">Ende *</Label>
<Input id="endTime" name="endTime" type="time" required />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notizen</Label>
<Input id="notes" name="notes" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? 'Wird erstellt...' : 'Termin erstellen'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -20,6 +20,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { CreateSessionDialog } from './create-session-dialog';
import { DeleteCourseButton } from './delete-course-button';
interface PageProps {
@@ -214,11 +215,14 @@ export default async function CourseDetailPage({ params }: PageProps) {
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Termine</CardTitle>
<Link href={`/home/${account}/courses/${courseId}/attendance`}>
<Button variant="outline" size="sm">
Anwesenheit
</Button>
</Link>
<div className="flex gap-2">
<CreateSessionDialog courseId={courseId} />
<Link href={`/home/${account}/courses/${courseId}/attendance`}>
<Button variant="outline" size="sm">
Anwesenheit
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">

View File

@@ -72,6 +72,7 @@ export default async function CatchBooksPage({ params, searchParams }: Props) {
page={page}
pageSize={25}
account={account}
accountId={acct.id}
/>
</CmsPageShell>
);

View File

@@ -62,6 +62,7 @@ export default async function CompetitionsPage({
page={page}
pageSize={25}
account={account}
accountId={acct.id}
/>
</CmsPageShell>
);

View File

@@ -1,10 +1,9 @@
import { createFischereiApi } from '@kit/fischerei/api';
import { FischereiTabNavigation } from '@kit/fischerei/components';
import { LEASE_PAYMENT_LABELS } from '@kit/fischerei/lib/fischerei-constants';
import { formatDate } from '@kit/shared/dates';
import {
FischereiTabNavigation,
LeasesDataTable,
} from '@kit/fischerei/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { ListToolbar } from '@kit/ui/list-toolbar';
import { AccountNotFound } from '~/components/account-not-found';
@@ -78,84 +77,11 @@ export default async function LeasesPage({ params, searchParams }: Props) {
},
]}
/>
<Card>
<CardHeader>
<CardTitle>Pachten ({result.total})</CardTitle>
</CardHeader>
<CardContent>
{result.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 Pachten vorhanden
</h3>
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
Erstellen Sie Ihren ersten Pachtvertrag.
</p>
</div>
) : (
<div className="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">Verpächter</th>
<th className="p-3 text-left font-medium">Gewässer</th>
<th className="p-3 text-left font-medium">Beginn</th>
<th className="p-3 text-left font-medium">Ende</th>
<th className="p-3 text-right font-medium">
Jahresbetrag ()
</th>
<th className="p-3 text-left font-medium">Zahlungsart</th>
</tr>
</thead>
<tbody>
{result.data.map((lease: Record<string, unknown>) => {
const waters = lease.waters as Record<
string,
unknown
> | null;
const paymentMethod = String(
lease.payment_method ?? 'ueberweisung',
);
return (
<tr
key={String(lease.id)}
className="hover:bg-muted/30 border-b"
>
<td className="p-3 font-medium">
{String(lease.lessor_name)}
</td>
<td className="p-3">
{waters ? String(waters.name) : '—'}
</td>
<td className="p-3">
{formatDate(lease.start_date)}
</td>
<td className="p-3">
{lease.end_date
? formatDate(lease.end_date)
: 'unbefristet'}
</td>
<td className="p-3 text-right">
{lease.initial_amount != null
? `${Number(lease.initial_amount).toLocaleString('de-DE', { minimumFractionDigits: 2 })} €`
: '—'}
</td>
<td className="p-3">
<Badge variant="outline">
{LEASE_PAYMENT_LABELS[paymentMethod] ??
paymentMethod}
</Badge>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<LeasesDataTable
data={result.data}
total={result.total}
accountId={acct.id}
/>
</div>
</CmsPageShell>
);

View File

@@ -1,7 +1,9 @@
import { createFischereiApi } from '@kit/fischerei/api';
import { FischereiTabNavigation } from '@kit/fischerei/components';
import {
FischereiTabNavigation,
PermitsDataTable,
} from '@kit/fischerei/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -35,76 +37,10 @@ export default async function PermitsPage({ params }: Props) {
Erlaubnisscheine und Gewässerkarten verwalten
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Erlaubnisscheine ({permits.length})</CardTitle>
</CardHeader>
<CardContent>
{permits.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 Erlaubnisscheine vorhanden
</h3>
<p className="text-muted-foreground mt-1 max-w-sm text-sm">
Erstellen Sie Ihren ersten Erlaubnisschein.
</p>
</div>
) : (
<div className="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">Bezeichnung</th>
<th className="p-3 text-left font-medium">Kurzcode</th>
<th className="p-3 text-left font-medium">
Hauptgewässer
</th>
<th className="p-3 text-right font-medium">
Gesamtmenge
</th>
<th className="p-3 text-center font-medium">
Zum Verkauf
</th>
</tr>
</thead>
<tbody>
{permits.map((permit: Record<string, unknown>) => {
const waters = permit.waters as Record<
string,
unknown
> | null;
return (
<tr
key={String(permit.id)}
className="hover:bg-muted/30 border-b"
>
<td className="p-3 font-medium">
{String(permit.name)}
</td>
<td className="text-muted-foreground p-3">
{String(permit.short_code ?? '—')}
</td>
<td className="p-3">
{waters ? String(waters.name) : '—'}
</td>
<td className="p-3 text-right">
{permit.total_quantity != null
? String(permit.total_quantity)
: '—'}
</td>
<td className="p-3 text-center">
{permit.is_for_sale ? '✓' : '—'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
<PermitsDataTable
data={permits as Array<Record<string, unknown>>}
accountId={acct.id}
/>
</div>
</CmsPageShell>
);

View File

@@ -0,0 +1,45 @@
import { createNewsletterApi } from '@kit/newsletter/api';
import { CreateNewsletterForm } from '@kit/newsletter/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
interface Props {
params: Promise<{ account: string; campaignId: string }>;
}
export default async function EditNewsletterPage({ params }: Props) {
const { account, campaignId } = await params;
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
.select('id')
.eq('slug', account)
.single();
if (!acct) return <AccountNotFound />;
const api = createNewsletterApi(client);
const newsletter = await api.getNewsletter(campaignId);
if (!newsletter) return <AccountNotFound />;
const n = newsletter as Record<string, unknown>;
return (
<CmsPageShell account={account} title="Newsletter bearbeiten">
<CreateNewsletterForm
accountId={acct.id}
account={account}
newsletterId={campaignId}
initialData={{
subject: String(n.subject ?? ''),
bodyHtml: String(n.body_html ?? ''),
bodyText: String(n.body_text ?? ''),
scheduledAt: String(n.scheduled_at ?? ''),
}}
/>
</CmsPageShell>
);
}

View File

@@ -1,10 +1,11 @@
import Link from 'next/link';
import { ArrowLeft, Send, Users } from 'lucide-react';
import { ArrowLeft, Pencil, Send, Users } from 'lucide-react';
import { createNewsletterApi } from '@kit/newsletter/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
@@ -74,9 +75,19 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
<CardTitle>
{String(newsletter.subject ?? '(Kein Betreff)')}
</CardTitle>
<Badge variant={NEWSLETTER_STATUS_VARIANT[status] ?? 'secondary'}>
{NEWSLETTER_STATUS_LABEL[status] ?? status}
</Badge>
<div className="flex items-center gap-2">
{status === 'draft' && (
<Button asChild variant="outline" size="sm">
<Link href={`/home/${account}/newsletter/${campaignId}/edit`}>
<Pencil className="mr-2 h-4 w-4" />
Bearbeiten
</Link>
</Button>
)}
<Badge variant={NEWSLETTER_STATUS_VARIANT[status] ?? 'secondary'}>
{NEWSLETTER_STATUS_LABEL[status] ?? status}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">

View File

@@ -0,0 +1,49 @@
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import {
VerbandTabNavigation,
CreateClubForm,
} from '@kit/verbandsverwaltung/components';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
interface Props {
params: Promise<{ account: string; clubId: string }>;
}
export default async function EditClubPage({ params }: Props) {
const { account, clubId } = await params;
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
.select('id')
.eq('slug', account)
.single();
if (!acct) return <AccountNotFound />;
const api = createVerbandApi(client);
const [club, types] = await Promise.all([
api.getClub(clubId),
api.listTypes(acct.id),
]);
if (!club) return <AccountNotFound />;
return (
<CmsPageShell
account={account}
title={`${String((club as Record<string, unknown>).name)} — Bearbeiten`}
>
<VerbandTabNavigation account={account} activeTab="clubs" />
<CreateClubForm
accountId={acct.id}
account={account}
types={types.map((t) => ({ id: t.id, name: t.name }))}
club={club as Record<string, unknown>}
/>
</CmsPageShell>
);
}

View File

@@ -1,4 +1,9 @@
import Link from 'next/link';
import { Pencil } from 'lucide-react';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button';
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import {
VerbandTabNavigation,
@@ -64,6 +69,12 @@ export default async function ClubDetailPage({ params }: Props) {
)}
</div>
</div>
<Button asChild variant="outline" size="sm">
<Link href={`/home/${account}/verband/clubs/${clubId}/edit`}>
<Pencil className="mr-2 h-4 w-4" />
Bearbeiten
</Link>
</Button>
</div>
{/* Contacts */}