feat: add delete functionality for leases, catch books, and permits; implement newsletter update feature
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
|||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
|
|
||||||
|
import { CreateSessionDialog } from './create-session-dialog';
|
||||||
import { DeleteCourseButton } from './delete-course-button';
|
import { DeleteCourseButton } from './delete-course-button';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
@@ -214,11 +215,14 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Termine</CardTitle>
|
<CardTitle>Termine</CardTitle>
|
||||||
<Link href={`/home/${account}/courses/${courseId}/attendance`}>
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" size="sm">
|
<CreateSessionDialog courseId={courseId} />
|
||||||
Anwesenheit
|
<Link href={`/home/${account}/courses/${courseId}/attendance`}>
|
||||||
</Button>
|
<Button variant="outline" size="sm">
|
||||||
</Link>
|
Anwesenheit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export default async function CatchBooksPage({ params, searchParams }: Props) {
|
|||||||
page={page}
|
page={page}
|
||||||
pageSize={25}
|
pageSize={25}
|
||||||
account={account}
|
account={account}
|
||||||
|
accountId={acct.id}
|
||||||
/>
|
/>
|
||||||
</CmsPageShell>
|
</CmsPageShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export default async function CompetitionsPage({
|
|||||||
page={page}
|
page={page}
|
||||||
pageSize={25}
|
pageSize={25}
|
||||||
account={account}
|
account={account}
|
||||||
|
accountId={acct.id}
|
||||||
/>
|
/>
|
||||||
</CmsPageShell>
|
</CmsPageShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createFischereiApi } from '@kit/fischerei/api';
|
import { createFischereiApi } from '@kit/fischerei/api';
|
||||||
import { FischereiTabNavigation } from '@kit/fischerei/components';
|
import {
|
||||||
import { LEASE_PAYMENT_LABELS } from '@kit/fischerei/lib/fischerei-constants';
|
FischereiTabNavigation,
|
||||||
import { formatDate } from '@kit/shared/dates';
|
LeasesDataTable,
|
||||||
|
} from '@kit/fischerei/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
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 { ListToolbar } from '@kit/ui/list-toolbar';
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
@@ -78,84 +77,11 @@ export default async function LeasesPage({ params, searchParams }: Props) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Card>
|
<LeasesDataTable
|
||||||
<CardHeader>
|
data={result.data}
|
||||||
<CardTitle>Pachten ({result.total})</CardTitle>
|
total={result.total}
|
||||||
</CardHeader>
|
accountId={acct.id}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</CmsPageShell>
|
</CmsPageShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createFischereiApi } from '@kit/fischerei/api';
|
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 { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
@@ -35,76 +37,10 @@ export default async function PermitsPage({ params }: Props) {
|
|||||||
Erlaubnisscheine und Gewässerkarten verwalten
|
Erlaubnisscheine und Gewässerkarten verwalten
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<PermitsDataTable
|
||||||
<CardHeader>
|
data={permits as Array<Record<string, unknown>>}
|
||||||
<CardTitle>Erlaubnisscheine ({permits.length})</CardTitle>
|
accountId={acct.id}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</CmsPageShell>
|
</CmsPageShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import Link from 'next/link';
|
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 { createNewsletterApi } from '@kit/newsletter/api';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
@@ -74,9 +75,19 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
|
|||||||
<CardTitle>
|
<CardTitle>
|
||||||
{String(newsletter.subject ?? '(Kein Betreff)')}
|
{String(newsletter.subject ?? '(Kein Betreff)')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Badge variant={NEWSLETTER_STATUS_VARIANT[status] ?? 'secondary'}>
|
<div className="flex items-center gap-2">
|
||||||
{NEWSLETTER_STATUS_LABEL[status] ?? status}
|
{status === 'draft' && (
|
||||||
</Badge>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Pencil } from 'lucide-react';
|
||||||
|
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
|
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
|
||||||
import {
|
import {
|
||||||
VerbandTabNavigation,
|
VerbandTabNavigation,
|
||||||
@@ -64,6 +69,12 @@ export default async function ClubDetailPage({ params }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Contacts */}
|
{/* Contacts */}
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Check } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
|
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CATCH_BOOK_STATUS_LABELS,
|
CATCH_BOOK_STATUS_LABELS,
|
||||||
CATCH_BOOK_STATUS_COLORS,
|
CATCH_BOOK_STATUS_COLORS,
|
||||||
} from '../lib/fischerei-constants';
|
} from '../lib/fischerei-constants';
|
||||||
|
import { deleteCatchBook } from '../server/actions/fischerei-actions';
|
||||||
|
import { DeleteConfirmButton } from './delete-confirm-button';
|
||||||
|
|
||||||
interface CatchBooksDataTableProps {
|
interface CatchBooksDataTableProps {
|
||||||
data: Array<Record<string, unknown>>;
|
data: Array<Record<string, unknown>>;
|
||||||
@@ -21,6 +24,7 @@ interface CatchBooksDataTableProps {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
account: string;
|
account: string;
|
||||||
|
accountId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
@@ -38,10 +42,19 @@ export function CatchBooksDataTable({
|
|||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
account,
|
account,
|
||||||
|
accountId,
|
||||||
}: CatchBooksDataTableProps) {
|
}: CatchBooksDataTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const { execute: executeDelete, isPending: isDeleting } = useActionWithToast(
|
||||||
|
deleteCatchBook,
|
||||||
|
{
|
||||||
|
successMessage: 'Fangbuch gelöscht',
|
||||||
|
onSuccess: () => router.refresh(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const currentYear = searchParams.get('year') ?? '';
|
const currentYear = searchParams.get('year') ?? '';
|
||||||
const currentStatus = searchParams.get('status') ?? '';
|
const currentStatus = searchParams.get('status') ?? '';
|
||||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||||
@@ -146,6 +159,7 @@ export function CatchBooksDataTable({
|
|||||||
<th className="p-3 text-right font-medium">Angeltage</th>
|
<th className="p-3 text-right font-medium">Angeltage</th>
|
||||||
<th className="p-3 text-right font-medium">Fänge</th>
|
<th className="p-3 text-right font-medium">Fänge</th>
|
||||||
<th className="p-3 text-left font-medium">Status</th>
|
<th className="p-3 text-left font-medium">Status</th>
|
||||||
|
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -190,6 +204,34 @@ export function CatchBooksDataTable({
|
|||||||
{CATCH_BOOK_STATUS_LABELS[status] ?? status}
|
{CATCH_BOOK_STATUS_LABELS[status] ?? status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="p-3 text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
data-test="catchbook-edit-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
router.push(
|
||||||
|
`/home/${account}/fischerei/catch-books/${String(cb.id)}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<DeleteConfirmButton
|
||||||
|
title="Fangbuch löschen"
|
||||||
|
description="Möchten Sie dieses Fangbuch wirklich löschen? Alle zugehörigen Fangeinträge werden ebenfalls gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
isPending={isDeleting}
|
||||||
|
onConfirm={() =>
|
||||||
|
executeDelete({
|
||||||
|
catchBookId: String(cb.id),
|
||||||
|
accountId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import { useCallback } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Plus } from 'lucide-react';
|
import { Pencil, Plus } from 'lucide-react';
|
||||||
|
|
||||||
import { formatDate } from '@kit/shared/dates';
|
import { formatDate } from '@kit/shared/dates';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
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 {
|
interface CompetitionsDataTableProps {
|
||||||
data: Array<Record<string, unknown>>;
|
data: Array<Record<string, unknown>>;
|
||||||
@@ -17,6 +21,7 @@ interface CompetitionsDataTableProps {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
account: string;
|
account: string;
|
||||||
|
accountId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CompetitionsDataTable({
|
export function CompetitionsDataTable({
|
||||||
@@ -25,11 +30,20 @@ export function CompetitionsDataTable({
|
|||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
account,
|
account,
|
||||||
|
accountId,
|
||||||
}: CompetitionsDataTableProps) {
|
}: CompetitionsDataTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
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(
|
const updateParams = useCallback(
|
||||||
(updates: Record<string, string>) => {
|
(updates: Record<string, string>) => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
@@ -95,6 +109,7 @@ export function CompetitionsDataTable({
|
|||||||
<th className="p-3 text-right font-medium">
|
<th className="p-3 text-right font-medium">
|
||||||
Max. Teilnehmer
|
Max. Teilnehmer
|
||||||
</th>
|
</th>
|
||||||
|
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -126,6 +141,34 @@ export function CompetitionsDataTable({
|
|||||||
? String(comp.max_participants)
|
? String(comp.max_participants)
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="p-3 text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
data-test="competition-edit-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
router.push(
|
||||||
|
`/home/${account}/fischerei/competitions/${String(comp.id)}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</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>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ export { StockingDataTable } from './stocking-data-table';
|
|||||||
export { CreateStockingForm } from './create-stocking-form';
|
export { CreateStockingForm } from './create-stocking-form';
|
||||||
export { CatchBooksDataTable } from './catch-books-data-table';
|
export { CatchBooksDataTable } from './catch-books-data-table';
|
||||||
export { CompetitionsDataTable } from './competitions-data-table';
|
export { CompetitionsDataTable } from './competitions-data-table';
|
||||||
|
export { LeasesDataTable } from './leases-data-table';
|
||||||
|
export { PermitsDataTable } from './permits-data-table';
|
||||||
|
|||||||
125
packages/features/fischerei/src/components/leases-data-table.tsx
Normal file
125
packages/features/fischerei/src/components/leases-data-table.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { formatDate } from '@kit/shared/dates';
|
||||||
|
import { Badge } from '@kit/ui/badge';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||||
|
|
||||||
|
import { LEASE_PAYMENT_LABELS } from '../lib/fischerei-constants';
|
||||||
|
import { deleteLease } from '../server/actions/fischerei-actions';
|
||||||
|
import { DeleteConfirmButton } from './delete-confirm-button';
|
||||||
|
|
||||||
|
interface LeasesDataTableProps {
|
||||||
|
data: Array<Record<string, unknown>>;
|
||||||
|
total: number;
|
||||||
|
accountId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LeasesDataTable({
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
accountId,
|
||||||
|
}: LeasesDataTableProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { execute: executeDelete, isPending: isDeleting } = useActionWithToast(
|
||||||
|
deleteLease,
|
||||||
|
{
|
||||||
|
successMessage: 'Pacht gelöscht',
|
||||||
|
onSuccess: () => router.refresh(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pachten ({total})</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{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 (EUR)
|
||||||
|
</th>
|
||||||
|
<th className="p-3 text-left font-medium">Zahlungsart</th>
|
||||||
|
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((lease) => {
|
||||||
|
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) : '\u2014'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3">
|
||||||
|
{formatDate(lease.start_date as string | null)}
|
||||||
|
</td>
|
||||||
|
<td className="p-3">
|
||||||
|
{lease.end_date
|
||||||
|
? formatDate(lease.end_date as string | null)
|
||||||
|
: 'unbefristet'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-right">
|
||||||
|
{lease.initial_amount != null
|
||||||
|
? `${Number(lease.initial_amount).toLocaleString('de-DE', { minimumFractionDigits: 2 })} \u20AC`
|
||||||
|
: '\u2014'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{LEASE_PAYMENT_LABELS[paymentMethod] ?? paymentMethod}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<DeleteConfirmButton
|
||||||
|
title="Pacht löschen"
|
||||||
|
description="Möchten Sie diesen Pachtvertrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
isPending={isDeleting}
|
||||||
|
onConfirm={() =>
|
||||||
|
executeDelete({
|
||||||
|
leaseId: String(lease.id),
|
||||||
|
accountId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||||
|
|
||||||
|
import { deletePermit } from '../server/actions/fischerei-actions';
|
||||||
|
import { DeleteConfirmButton } from './delete-confirm-button';
|
||||||
|
|
||||||
|
interface PermitsDataTableProps {
|
||||||
|
data: Array<Record<string, unknown>>;
|
||||||
|
accountId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermitsDataTable({ data, accountId }: PermitsDataTableProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { execute: executeDelete, isPending: isDeleting } = useActionWithToast(
|
||||||
|
deletePermit,
|
||||||
|
{
|
||||||
|
successMessage: 'Erlaubnisschein gelöscht',
|
||||||
|
onSuccess: () => router.refresh(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Erlaubnisscheine ({data.length})</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{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 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>
|
||||||
|
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((permit) => {
|
||||||
|
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 ?? '\u2014')}
|
||||||
|
</td>
|
||||||
|
<td className="p-3">
|
||||||
|
{waters ? String(waters.name) : '\u2014'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-right">
|
||||||
|
{permit.total_quantity != null
|
||||||
|
? String(permit.total_quantity)
|
||||||
|
: '\u2014'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-center">
|
||||||
|
{permit.is_for_sale ? '\u2713' : '\u2014'}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<DeleteConfirmButton
|
||||||
|
title="Erlaubnisschein löschen"
|
||||||
|
description="Möchten Sie diesen Erlaubnisschein wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
isPending={isDeleting}
|
||||||
|
onConfirm={() =>
|
||||||
|
executeDelete({
|
||||||
|
permitId: String(permit.id),
|
||||||
|
accountId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -291,6 +291,25 @@ export const updateLease = authActionClient
|
|||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deleteLease = authActionClient
|
||||||
|
.inputSchema(
|
||||||
|
z.object({
|
||||||
|
leaseId: z.string().uuid(),
|
||||||
|
accountId: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.action(async ({ parsedInput: input }) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const logger = await getLogger();
|
||||||
|
const api = createFischereiApi(client);
|
||||||
|
|
||||||
|
logger.info({ name: 'fischerei.lease.delete' }, 'Deleting lease...');
|
||||||
|
await api.deleteLease(input.leaseId);
|
||||||
|
logger.info({ name: 'fischerei.lease.delete' }, 'Lease deleted');
|
||||||
|
revalidatePath('/home/[account]/fischerei', 'page');
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Catch Books
|
// Catch Books
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -386,6 +405,28 @@ export const reviewCatchBook = authActionClient
|
|||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deleteCatchBook = authActionClient
|
||||||
|
.inputSchema(
|
||||||
|
z.object({
|
||||||
|
catchBookId: z.string().uuid(),
|
||||||
|
accountId: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.action(async ({ parsedInput: input }) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const logger = await getLogger();
|
||||||
|
const api = createFischereiApi(client);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{ name: 'fischerei.catchBook.delete' },
|
||||||
|
'Deleting catch book...',
|
||||||
|
);
|
||||||
|
await api.deleteCatchBook(input.catchBookId);
|
||||||
|
logger.info({ name: 'fischerei.catchBook.delete' }, 'Catch book deleted');
|
||||||
|
revalidatePath('/home/[account]/fischerei', 'page');
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Catches
|
// Catches
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -473,6 +514,25 @@ export const updatePermit = authActionClient
|
|||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deletePermit = authActionClient
|
||||||
|
.inputSchema(
|
||||||
|
z.object({
|
||||||
|
permitId: z.string().uuid(),
|
||||||
|
accountId: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.action(async ({ parsedInput: input }) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const logger = await getLogger();
|
||||||
|
const api = createFischereiApi(client);
|
||||||
|
|
||||||
|
logger.info({ name: 'fischerei.permit.delete' }, 'Deleting permit...');
|
||||||
|
await api.deletePermit(input.permitId);
|
||||||
|
logger.info({ name: 'fischerei.permit.delete' }, 'Permit deleted');
|
||||||
|
revalidatePath('/home/[account]/fischerei', 'page');
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Inspectors
|
// Inspectors
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|||||||
@@ -670,6 +670,14 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deleteLease(leaseId: string) {
|
||||||
|
const { error } = await client
|
||||||
|
.from('fishing_leases')
|
||||||
|
.delete()
|
||||||
|
.eq('id', leaseId);
|
||||||
|
if (error) throw error;
|
||||||
|
},
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Catch Books
|
// Catch Books
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -836,6 +844,14 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deleteCatchBook(catchBookId: string) {
|
||||||
|
const { error } = await client
|
||||||
|
.from('catch_books')
|
||||||
|
.delete()
|
||||||
|
.eq('id', catchBookId);
|
||||||
|
if (error) throw error;
|
||||||
|
},
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Catches
|
// Catches
|
||||||
// =====================================================
|
// =====================================================
|
||||||
@@ -1010,6 +1026,24 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getPermit(permitId: string) {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('fishing_permits')
|
||||||
|
.select('*, waters ( id, name )')
|
||||||
|
.eq('id', permitId)
|
||||||
|
.single();
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deletePermit(permitId: string) {
|
||||||
|
const { error } = await client
|
||||||
|
.from('fishing_permits')
|
||||||
|
.delete()
|
||||||
|
.eq('id', permitId);
|
||||||
|
if (error) throw error;
|
||||||
|
},
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Inspectors
|
// Inspectors
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useAction } from 'next-safe-action/hooks';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -17,45 +16,82 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@kit/ui/form';
|
} from '@kit/ui/form';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||||
|
|
||||||
import { CreateNewsletterSchema } from '../schema/newsletter.schema';
|
import {
|
||||||
import { createNewsletter } from '../server/actions/newsletter-actions';
|
CreateNewsletterSchema,
|
||||||
|
UpdateNewsletterSchema,
|
||||||
|
} from '../schema/newsletter.schema';
|
||||||
|
import {
|
||||||
|
createNewsletter,
|
||||||
|
updateNewsletter,
|
||||||
|
} from '../server/actions/newsletter-actions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
account: string;
|
account: string;
|
||||||
|
newsletterId?: string;
|
||||||
|
initialData?: {
|
||||||
|
subject: string;
|
||||||
|
bodyHtml: string;
|
||||||
|
bodyText: string;
|
||||||
|
scheduledAt: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreateNewsletterForm({ accountId, account }: Props) {
|
export function CreateNewsletterForm({
|
||||||
|
accountId,
|
||||||
|
account,
|
||||||
|
newsletterId,
|
||||||
|
initialData,
|
||||||
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isEdit = Boolean(newsletterId);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(CreateNewsletterSchema),
|
resolver: zodResolver(
|
||||||
|
isEdit ? UpdateNewsletterSchema : CreateNewsletterSchema,
|
||||||
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
accountId,
|
...(isEdit ? { newsletterId } : { accountId }),
|
||||||
subject: '',
|
subject: initialData?.subject ?? '',
|
||||||
bodyHtml: '',
|
bodyHtml: initialData?.bodyHtml ?? '',
|
||||||
bodyText: '',
|
bodyText: initialData?.bodyText ?? '',
|
||||||
scheduledAt: '',
|
scheduledAt: initialData?.scheduledAt ?? '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { execute, isPending } = useAction(createNewsletter, {
|
const { execute: execCreate, isPending: isCreating } = useActionWithToast(
|
||||||
onSuccess: ({ data }) => {
|
createNewsletter,
|
||||||
if (data?.success) {
|
{
|
||||||
toast.success('Newsletter erfolgreich erstellt');
|
successMessage: 'Newsletter erstellt',
|
||||||
router.push(`/home/${account}/newsletter`);
|
errorMessage: 'Fehler beim Erstellen',
|
||||||
}
|
onSuccess: () => router.push(`/home/${account}/newsletter`),
|
||||||
},
|
},
|
||||||
onError: ({ error }) => {
|
);
|
||||||
toast.error(error.serverError ?? 'Fehler beim Erstellen des Newsletters');
|
|
||||||
|
const { execute: execUpdate, isPending: isUpdating } = useActionWithToast(
|
||||||
|
updateNewsletter,
|
||||||
|
{
|
||||||
|
successMessage: 'Newsletter aktualisiert',
|
||||||
|
errorMessage: 'Fehler beim Aktualisieren',
|
||||||
|
onSuccess: () =>
|
||||||
|
router.push(`/home/${account}/newsletter/${newsletterId}`),
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
|
const isPending = isCreating || isUpdating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
if (isEdit && newsletterId) {
|
||||||
|
execUpdate({ ...data, newsletterId } as any);
|
||||||
|
} else {
|
||||||
|
execCreate({ ...data, accountId } as any);
|
||||||
|
}
|
||||||
|
})}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -146,20 +182,15 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
data-test="newsletter-cancel-btn"
|
|
||||||
>
|
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" disabled={isPending}>
|
||||||
type="submit"
|
{isPending
|
||||||
disabled={isPending}
|
? 'Wird gespeichert...'
|
||||||
data-test="newsletter-submit-btn"
|
: isEdit
|
||||||
>
|
? 'Newsletter aktualisieren'
|
||||||
{isPending ? 'Wird erstellt...' : 'Newsletter erstellen'}
|
: 'Newsletter erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const CreateNewsletterSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type CreateNewsletterInput = z.infer<typeof CreateNewsletterSchema>;
|
export type CreateNewsletterInput = z.infer<typeof CreateNewsletterSchema>;
|
||||||
|
|
||||||
|
export const UpdateNewsletterSchema = CreateNewsletterSchema.partial().extend({
|
||||||
|
newsletterId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
export type UpdateNewsletterInput = z.infer<typeof UpdateNewsletterSchema>;
|
||||||
|
|
||||||
export const CreateTemplateSchema = z.object({
|
export const CreateTemplateSchema = z.object({
|
||||||
accountId: z.string().uuid(),
|
accountId: z.string().uuid(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreateNewsletterSchema,
|
CreateNewsletterSchema,
|
||||||
|
UpdateNewsletterSchema,
|
||||||
CreateTemplateSchema,
|
CreateTemplateSchema,
|
||||||
} from '../../schema/newsletter.schema';
|
} from '../../schema/newsletter.schema';
|
||||||
import { createNewsletterApi } from '../api';
|
import { createNewsletterApi } from '../api';
|
||||||
@@ -26,6 +27,19 @@ export const createNewsletter = authActionClient
|
|||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateNewsletter = authActionClient
|
||||||
|
.inputSchema(UpdateNewsletterSchema)
|
||||||
|
.action(async ({ parsedInput: input, ctx }) => {
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const logger = await getLogger();
|
||||||
|
const api = createNewsletterApi(client);
|
||||||
|
|
||||||
|
logger.info({ name: 'newsletter.update' }, 'Updating newsletter...');
|
||||||
|
const result = await api.updateNewsletter(input);
|
||||||
|
logger.info({ name: 'newsletter.update' }, 'Newsletter updated');
|
||||||
|
return { success: true, data: result };
|
||||||
|
});
|
||||||
|
|
||||||
export const createTemplate = authActionClient
|
export const createTemplate = authActionClient
|
||||||
.inputSchema(
|
.inputSchema(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|||||||
|
|
||||||
import type { Database } from '@kit/supabase/database';
|
import type { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
import type { CreateNewsletterInput } from '../schema/newsletter.schema';
|
import type {
|
||||||
|
CreateNewsletterInput,
|
||||||
|
UpdateNewsletterInput,
|
||||||
|
} from '../schema/newsletter.schema';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
@@ -140,6 +143,26 @@ export function createNewsletterApi(client: SupabaseClient<Database>) {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateNewsletter(input: UpdateNewsletterInput) {
|
||||||
|
const update: Record<string, unknown> = {};
|
||||||
|
if (input.subject !== undefined) update.subject = input.subject;
|
||||||
|
if (input.bodyHtml !== undefined) update.body_html = input.bodyHtml;
|
||||||
|
if (input.bodyText !== undefined) update.body_text = input.bodyText;
|
||||||
|
if (input.templateId !== undefined)
|
||||||
|
update.template_id = input.templateId || null;
|
||||||
|
if (input.scheduledAt !== undefined)
|
||||||
|
update.scheduled_at = input.scheduledAt || null;
|
||||||
|
|
||||||
|
const { data, error } = await client
|
||||||
|
.from('newsletters')
|
||||||
|
.update(update)
|
||||||
|
.eq('id', input.newsletterId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
async getNewsletter(newsletterId: string) {
|
async getNewsletter(newsletterId: string) {
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
.from('newsletters')
|
.from('newsletters')
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ import {
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
import { useActionWithToast } from '@kit/ui/use-action-with-toast';
|
||||||
|
|
||||||
import { CreateMemberClubSchema } from '../schema/verband.schema';
|
import {
|
||||||
import { createClub } from '../server/actions/verband-actions';
|
CreateMemberClubSchema,
|
||||||
|
UpdateMemberClubSchema,
|
||||||
|
} from '../schema/verband.schema';
|
||||||
|
import { createClub, updateClub } from '../server/actions/verband-actions';
|
||||||
|
|
||||||
interface CreateClubFormProps {
|
interface CreateClubFormProps {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@@ -38,7 +41,9 @@ export function CreateClubForm({
|
|||||||
const isEdit = !!club;
|
const isEdit = !!club;
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(CreateMemberClubSchema),
|
resolver: zodResolver(
|
||||||
|
isEdit ? UpdateMemberClubSchema : CreateMemberClubSchema,
|
||||||
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
accountId,
|
accountId,
|
||||||
name: (club?.name as string) ?? '',
|
name: (club?.name as string) ?? '',
|
||||||
@@ -61,18 +66,37 @@ export function CreateClubForm({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { execute, isPending } = useActionWithToast(createClub, {
|
const { execute: execCreate, isPending: isCreating } = useActionWithToast(
|
||||||
successMessage: isEdit ? 'Verein aktualisiert' : 'Verein erstellt',
|
createClub,
|
||||||
errorMessage: 'Fehler beim Speichern',
|
{
|
||||||
onSuccess: () => {
|
successMessage: 'Verein erstellt',
|
||||||
router.push(`/home/${account}/verband/clubs`);
|
errorMessage: 'Fehler beim Erstellen',
|
||||||
|
onSuccess: () => router.push(`/home/${account}/verband/clubs`),
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
|
const { execute: execUpdate, isPending: isUpdating } = useActionWithToast(
|
||||||
|
updateClub,
|
||||||
|
{
|
||||||
|
successMessage: 'Verein aktualisiert',
|
||||||
|
errorMessage: 'Fehler beim Aktualisieren',
|
||||||
|
onSuccess: () =>
|
||||||
|
router.push(`/home/${account}/verband/clubs/${String(club?.id)}`),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPending = isCreating || isUpdating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
if (isEdit && club?.id) {
|
||||||
|
execUpdate({ ...data, clubId: String(club.id) } as any);
|
||||||
|
} else {
|
||||||
|
execCreate(data);
|
||||||
|
}
|
||||||
|
})}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
{/* Card 1: Grunddaten */}
|
{/* Card 1: Grunddaten */}
|
||||||
|
|||||||
Reference in New Issue
Block a user