From 080ec1cb4753b9c1b0a7a3d826e8ca7f77e42470 Mon Sep 17 00:00:00 2001 From: "T. Zehetbauer" <125989630+4thTomost@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:53:39 +0200 Subject: [PATCH] feat: add delete functionality for leases, catch books, and permits; implement newsletter update feature --- .../[courseId]/create-session-dialog.tsx | 94 +++++++++++++ .../[account]/courses/[courseId]/page.tsx | 14 +- .../[account]/fischerei/catch-books/page.tsx | 1 + .../[account]/fischerei/competitions/page.tsx | 1 + .../home/[account]/fischerei/leases/page.tsx | 92 ++----------- .../home/[account]/fischerei/permits/page.tsx | 80 ++--------- .../newsletter/[campaignId]/edit/page.tsx | 45 +++++++ .../newsletter/[campaignId]/page.tsx | 19 ++- .../verband/clubs/[clubId]/edit/page.tsx | 49 +++++++ .../[account]/verband/clubs/[clubId]/page.tsx | 11 ++ .../src/components/catch-books-data-table.tsx | 44 +++++- .../components/competitions-data-table.tsx | 45 ++++++- .../fischerei/src/components/index.ts | 2 + .../src/components/leases-data-table.tsx | 125 ++++++++++++++++++ .../src/components/permits-data-table.tsx | 107 +++++++++++++++ .../src/server/actions/fischerei-actions.ts | 60 +++++++++ packages/features/fischerei/src/server/api.ts | 34 +++++ .../src/components/create-newsletter-form.tsx | 97 +++++++++----- .../src/schema/newsletter.schema.ts | 5 + .../src/server/actions/newsletter-actions.ts | 14 ++ .../features/newsletter/src/server/api.ts | 25 +++- .../src/components/create-club-form.tsx | 44 ++++-- 22 files changed, 798 insertions(+), 210 deletions(-) create mode 100644 apps/web/app/[locale]/home/[account]/courses/[courseId]/create-session-dialog.tsx create mode 100644 apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/edit/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/edit/page.tsx create mode 100644 packages/features/fischerei/src/components/leases-data-table.tsx create mode 100644 packages/features/fischerei/src/components/permits-data-table.tsx diff --git a/apps/web/app/[locale]/home/[account]/courses/[courseId]/create-session-dialog.tsx b/apps/web/app/[locale]/home/[account]/courses/[courseId]/create-session-dialog.tsx new file mode 100644 index 000000000..0d01250a0 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/courses/[courseId]/create-session-dialog.tsx @@ -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 ( + + + + + +
{ + 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, + }); + }} + > + + Neuen Termin erstellen + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+
+
+ ); +} diff --git a/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx b/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx index f491506db..aeea7ca5d 100644 --- a/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx @@ -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) { Termine - - - +
+ + + + +
diff --git a/apps/web/app/[locale]/home/[account]/fischerei/catch-books/page.tsx b/apps/web/app/[locale]/home/[account]/fischerei/catch-books/page.tsx index ebbb7181d..26064e9df 100644 --- a/apps/web/app/[locale]/home/[account]/fischerei/catch-books/page.tsx +++ b/apps/web/app/[locale]/home/[account]/fischerei/catch-books/page.tsx @@ -72,6 +72,7 @@ export default async function CatchBooksPage({ params, searchParams }: Props) { page={page} pageSize={25} account={account} + accountId={acct.id} /> ); diff --git a/apps/web/app/[locale]/home/[account]/fischerei/competitions/page.tsx b/apps/web/app/[locale]/home/[account]/fischerei/competitions/page.tsx index 611f64df0..d00c00479 100644 --- a/apps/web/app/[locale]/home/[account]/fischerei/competitions/page.tsx +++ b/apps/web/app/[locale]/home/[account]/fischerei/competitions/page.tsx @@ -62,6 +62,7 @@ export default async function CompetitionsPage({ page={page} pageSize={25} account={account} + accountId={acct.id} /> ); diff --git a/apps/web/app/[locale]/home/[account]/fischerei/leases/page.tsx b/apps/web/app/[locale]/home/[account]/fischerei/leases/page.tsx index 129400243..6c4cf5746 100644 --- a/apps/web/app/[locale]/home/[account]/fischerei/leases/page.tsx +++ b/apps/web/app/[locale]/home/[account]/fischerei/leases/page.tsx @@ -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) { }, ]} /> - - - Pachten ({result.total}) - - - {result.data.length === 0 ? ( -
-

- Keine Pachten vorhanden -

-

- Erstellen Sie Ihren ersten Pachtvertrag. -

-
- ) : ( -
- - - - - - - - - - - - - {result.data.map((lease: Record) => { - const waters = lease.waters as Record< - string, - unknown - > | null; - const paymentMethod = String( - lease.payment_method ?? 'ueberweisung', - ); - - return ( - - - - - - - - - ); - })} - -
VerpächterGewässerBeginnEnde - Jahresbetrag (€) - Zahlungsart
- {String(lease.lessor_name)} - - {waters ? String(waters.name) : '—'} - - {formatDate(lease.start_date)} - - {lease.end_date - ? formatDate(lease.end_date) - : 'unbefristet'} - - {lease.initial_amount != null - ? `${Number(lease.initial_amount).toLocaleString('de-DE', { minimumFractionDigits: 2 })} €` - : '—'} - - - {LEASE_PAYMENT_LABELS[paymentMethod] ?? - paymentMethod} - -
-
- )} -
-
+
); diff --git a/apps/web/app/[locale]/home/[account]/fischerei/permits/page.tsx b/apps/web/app/[locale]/home/[account]/fischerei/permits/page.tsx index 613df69d1..8bcb1ea8e 100644 --- a/apps/web/app/[locale]/home/[account]/fischerei/permits/page.tsx +++ b/apps/web/app/[locale]/home/[account]/fischerei/permits/page.tsx @@ -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

- - - Erlaubnisscheine ({permits.length}) - - - {permits.length === 0 ? ( -
-

- Keine Erlaubnisscheine vorhanden -

-

- Erstellen Sie Ihren ersten Erlaubnisschein. -

-
- ) : ( -
- - - - - - - - - - - - {permits.map((permit: Record) => { - const waters = permit.waters as Record< - string, - unknown - > | null; - - return ( - - - - - - - - ); - })} - -
BezeichnungKurzcode - Hauptgewässer - - Gesamtmenge - - Zum Verkauf -
- {String(permit.name)} - - {String(permit.short_code ?? '—')} - - {waters ? String(waters.name) : '—'} - - {permit.total_quantity != null - ? String(permit.total_quantity) - : '—'} - - {permit.is_for_sale ? '✓' : '—'} -
-
- )} -
-
+ >} + accountId={acct.id} + /> ); diff --git a/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/edit/page.tsx b/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/edit/page.tsx new file mode 100644 index 000000000..27243d6d9 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/edit/page.tsx @@ -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 ; + + const api = createNewsletterApi(client); + const newsletter = await api.getNewsletter(campaignId); + if (!newsletter) return ; + + const n = newsletter as Record; + + return ( + + + + ); +} diff --git a/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/page.tsx b/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/page.tsx index dd40fe410..8dbff903c 100644 --- a/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/page.tsx @@ -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) { {String(newsletter.subject ?? '(Kein Betreff)')} - - {NEWSLETTER_STATUS_LABEL[status] ?? status} - +
+ {status === 'draft' && ( + + )} + + {NEWSLETTER_STATUS_LABEL[status] ?? status} + +
diff --git a/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/edit/page.tsx b/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/edit/page.tsx new file mode 100644 index 000000000..390f9d029 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/edit/page.tsx @@ -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 ; + + const api = createVerbandApi(client); + const [club, types] = await Promise.all([ + api.getClub(clubId), + api.listTypes(acct.id), + ]); + + if (!club) return ; + + return ( + ).name)} — Bearbeiten`} + > + + ({ id: t.id, name: t.name }))} + club={club as Record} + /> + + ); +} diff --git a/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx b/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx index d83c19832..1558929b6 100644 --- a/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx @@ -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) { )}
+ {/* Contacts */} diff --git a/packages/features/fischerei/src/components/catch-books-data-table.tsx b/packages/features/fischerei/src/components/catch-books-data-table.tsx index e83eeb72c..16914b7bd 100644 --- a/packages/features/fischerei/src/components/catch-books-data-table.tsx +++ b/packages/features/fischerei/src/components/catch-books-data-table.tsx @@ -4,16 +4,19 @@ import { useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { Check } from 'lucide-react'; +import { Pencil } from 'lucide-react'; import { Badge } from '@kit/ui/badge'; 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 { CATCH_BOOK_STATUS_LABELS, CATCH_BOOK_STATUS_COLORS, } from '../lib/fischerei-constants'; +import { deleteCatchBook } from '../server/actions/fischerei-actions'; +import { DeleteConfirmButton } from './delete-confirm-button'; interface CatchBooksDataTableProps { data: Array>; @@ -21,6 +24,7 @@ interface CatchBooksDataTableProps { page: number; pageSize: number; account: string; + accountId: string; } const STATUS_OPTIONS = [ @@ -38,10 +42,19 @@ export function CatchBooksDataTable({ page, pageSize, account, + accountId, }: CatchBooksDataTableProps) { const router = useRouter(); const searchParams = useSearchParams(); + const { execute: executeDelete, isPending: isDeleting } = useActionWithToast( + deleteCatchBook, + { + successMessage: 'Fangbuch gelöscht', + onSuccess: () => router.refresh(), + }, + ); + const currentYear = searchParams.get('year') ?? ''; const currentStatus = searchParams.get('status') ?? ''; const totalPages = Math.max(1, Math.ceil(total / pageSize)); @@ -146,6 +159,7 @@ export function CatchBooksDataTable({ Angeltage Fänge Status + Aktionen @@ -190,6 +204,34 @@ export function CatchBooksDataTable({ {CATCH_BOOK_STATUS_LABELS[status] ?? status} + +
+ + + executeDelete({ + catchBookId: String(cb.id), + accountId, + }) + } + /> +
+ ); })} diff --git a/packages/features/fischerei/src/components/competitions-data-table.tsx b/packages/features/fischerei/src/components/competitions-data-table.tsx index 8b5e7f2c6..bc84af5e4 100644 --- a/packages/features/fischerei/src/components/competitions-data-table.tsx +++ b/packages/features/fischerei/src/components/competitions-data-table.tsx @@ -5,11 +5,15 @@ import { useCallback } from 'react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; -import { Plus } from 'lucide-react'; +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>; @@ -17,6 +21,7 @@ interface CompetitionsDataTableProps { page: number; pageSize: number; account: string; + accountId: string; } export function CompetitionsDataTable({ @@ -25,11 +30,20 @@ export function CompetitionsDataTable({ 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) => { const params = new URLSearchParams(searchParams.toString()); @@ -95,6 +109,7 @@ export function CompetitionsDataTable({ Max. Teilnehmer + Aktionen @@ -126,6 +141,34 @@ export function CompetitionsDataTable({ ? String(comp.max_participants) : '—'} + +
+ + + executeDelete({ + competitionId: String(comp.id), + accountId, + }) + } + /> +
+ ); })} diff --git a/packages/features/fischerei/src/components/index.ts b/packages/features/fischerei/src/components/index.ts index 63370d4dd..66659cfa2 100644 --- a/packages/features/fischerei/src/components/index.ts +++ b/packages/features/fischerei/src/components/index.ts @@ -8,3 +8,5 @@ export { StockingDataTable } from './stocking-data-table'; export { CreateStockingForm } from './create-stocking-form'; export { CatchBooksDataTable } from './catch-books-data-table'; export { CompetitionsDataTable } from './competitions-data-table'; +export { LeasesDataTable } from './leases-data-table'; +export { PermitsDataTable } from './permits-data-table'; diff --git a/packages/features/fischerei/src/components/leases-data-table.tsx b/packages/features/fischerei/src/components/leases-data-table.tsx new file mode 100644 index 000000000..519f9130f --- /dev/null +++ b/packages/features/fischerei/src/components/leases-data-table.tsx @@ -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>; + 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 ( + + + Pachten ({total}) + + + {data.length === 0 ? ( +
+

Keine Pachten vorhanden

+

+ Erstellen Sie Ihren ersten Pachtvertrag. +

+
+ ) : ( +
+ + + + + + + + + + + + + + {data.map((lease) => { + const waters = lease.waters as Record | null; + const paymentMethod = String( + lease.payment_method ?? 'ueberweisung', + ); + + return ( + + + + + + + + + + ); + })} + +
VerpächterGewässerBeginnEnde + Jahresbetrag (EUR) + ZahlungsartAktionen
+ {String(lease.lessor_name)} + + {waters ? String(waters.name) : '\u2014'} + + {formatDate(lease.start_date as string | null)} + + {lease.end_date + ? formatDate(lease.end_date as string | null) + : 'unbefristet'} + + {lease.initial_amount != null + ? `${Number(lease.initial_amount).toLocaleString('de-DE', { minimumFractionDigits: 2 })} \u20AC` + : '\u2014'} + + + {LEASE_PAYMENT_LABELS[paymentMethod] ?? paymentMethod} + + +
+ + executeDelete({ + leaseId: String(lease.id), + accountId, + }) + } + /> +
+
+
+ )} +
+
+ ); +} diff --git a/packages/features/fischerei/src/components/permits-data-table.tsx b/packages/features/fischerei/src/components/permits-data-table.tsx new file mode 100644 index 000000000..938161245 --- /dev/null +++ b/packages/features/fischerei/src/components/permits-data-table.tsx @@ -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>; + 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 ( + + + Erlaubnisscheine ({data.length}) + + + {data.length === 0 ? ( +
+

+ Keine Erlaubnisscheine vorhanden +

+

+ Erstellen Sie Ihren ersten Erlaubnisschein. +

+
+ ) : ( +
+ + + + + + + + + + + + + {data.map((permit) => { + const waters = permit.waters as Record< + string, + unknown + > | null; + + return ( + + + + + + + + + ); + })} + +
BezeichnungKurzcodeHauptgewässerGesamtmengeZum VerkaufAktionen
{String(permit.name)} + {String(permit.short_code ?? '\u2014')} + + {waters ? String(waters.name) : '\u2014'} + + {permit.total_quantity != null + ? String(permit.total_quantity) + : '\u2014'} + + {permit.is_for_sale ? '\u2713' : '\u2014'} + +
+ + executeDelete({ + permitId: String(permit.id), + accountId, + }) + } + /> +
+
+
+ )} +
+
+ ); +} diff --git a/packages/features/fischerei/src/server/actions/fischerei-actions.ts b/packages/features/fischerei/src/server/actions/fischerei-actions.ts index c0b0c3f43..c94d0f870 100644 --- a/packages/features/fischerei/src/server/actions/fischerei-actions.ts +++ b/packages/features/fischerei/src/server/actions/fischerei-actions.ts @@ -291,6 +291,25 @@ export const updateLease = authActionClient 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 // ===================================================== @@ -386,6 +405,28 @@ export const reviewCatchBook = authActionClient 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 // ===================================================== @@ -473,6 +514,25 @@ export const updatePermit = authActionClient 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 // ===================================================== diff --git a/packages/features/fischerei/src/server/api.ts b/packages/features/fischerei/src/server/api.ts index 98a3072e9..536b0eab6 100644 --- a/packages/features/fischerei/src/server/api.ts +++ b/packages/features/fischerei/src/server/api.ts @@ -670,6 +670,14 @@ export function createFischereiApi(client: SupabaseClient) { return data; }, + async deleteLease(leaseId: string) { + const { error } = await client + .from('fishing_leases') + .delete() + .eq('id', leaseId); + if (error) throw error; + }, + // ===================================================== // Catch Books // ===================================================== @@ -836,6 +844,14 @@ export function createFischereiApi(client: SupabaseClient) { return data; }, + async deleteCatchBook(catchBookId: string) { + const { error } = await client + .from('catch_books') + .delete() + .eq('id', catchBookId); + if (error) throw error; + }, + // ===================================================== // Catches // ===================================================== @@ -1010,6 +1026,24 @@ export function createFischereiApi(client: SupabaseClient) { 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 // ===================================================== diff --git a/packages/features/newsletter/src/components/create-newsletter-form.tsx b/packages/features/newsletter/src/components/create-newsletter-form.tsx index 6ef995dd6..aebfbe06a 100644 --- a/packages/features/newsletter/src/components/create-newsletter-form.tsx +++ b/packages/features/newsletter/src/components/create-newsletter-form.tsx @@ -3,7 +3,6 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useAction } from 'next-safe-action/hooks'; import { useForm } from 'react-hook-form'; import { Button } from '@kit/ui/button'; @@ -17,45 +16,82 @@ import { FormMessage, } from '@kit/ui/form'; 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 { createNewsletter } from '../server/actions/newsletter-actions'; +import { + CreateNewsletterSchema, + UpdateNewsletterSchema, +} from '../schema/newsletter.schema'; +import { + createNewsletter, + updateNewsletter, +} from '../server/actions/newsletter-actions'; interface Props { accountId: 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 isEdit = Boolean(newsletterId); + const form = useForm({ - resolver: zodResolver(CreateNewsletterSchema), + resolver: zodResolver( + isEdit ? UpdateNewsletterSchema : CreateNewsletterSchema, + ), defaultValues: { - accountId, - subject: '', - bodyHtml: '', - bodyText: '', - scheduledAt: '', + ...(isEdit ? { newsletterId } : { accountId }), + subject: initialData?.subject ?? '', + bodyHtml: initialData?.bodyHtml ?? '', + bodyText: initialData?.bodyText ?? '', + scheduledAt: initialData?.scheduledAt ?? '', }, }); - const { execute, isPending } = useAction(createNewsletter, { - onSuccess: ({ data }) => { - if (data?.success) { - toast.success('Newsletter erfolgreich erstellt'); - router.push(`/home/${account}/newsletter`); - } + const { execute: execCreate, isPending: isCreating } = useActionWithToast( + createNewsletter, + { + successMessage: 'Newsletter erstellt', + 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 (
execute(data))} + onSubmit={form.handleSubmit((data) => { + if (isEdit && newsletterId) { + execUpdate({ ...data, newsletterId } as any); + } else { + execCreate({ ...data, accountId } as any); + } + })} className="space-y-6" > @@ -146,20 +182,15 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
- -
diff --git a/packages/features/newsletter/src/schema/newsletter.schema.ts b/packages/features/newsletter/src/schema/newsletter.schema.ts index c908aea42..e86be99cf 100644 --- a/packages/features/newsletter/src/schema/newsletter.schema.ts +++ b/packages/features/newsletter/src/schema/newsletter.schema.ts @@ -18,6 +18,11 @@ export const CreateNewsletterSchema = z.object({ }); export type CreateNewsletterInput = z.infer; +export const UpdateNewsletterSchema = CreateNewsletterSchema.partial().extend({ + newsletterId: z.string().uuid(), +}); +export type UpdateNewsletterInput = z.infer; + export const CreateTemplateSchema = z.object({ accountId: z.string().uuid(), name: z.string().min(1), diff --git a/packages/features/newsletter/src/server/actions/newsletter-actions.ts b/packages/features/newsletter/src/server/actions/newsletter-actions.ts index 7b537ef66..482c85fc3 100644 --- a/packages/features/newsletter/src/server/actions/newsletter-actions.ts +++ b/packages/features/newsletter/src/server/actions/newsletter-actions.ts @@ -8,6 +8,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { CreateNewsletterSchema, + UpdateNewsletterSchema, CreateTemplateSchema, } from '../../schema/newsletter.schema'; import { createNewsletterApi } from '../api'; @@ -26,6 +27,19 @@ export const createNewsletter = authActionClient 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 .inputSchema( z.object({ diff --git a/packages/features/newsletter/src/server/api.ts b/packages/features/newsletter/src/server/api.ts index a87fc9e8c..c9663e64c 100644 --- a/packages/features/newsletter/src/server/api.ts +++ b/packages/features/newsletter/src/server/api.ts @@ -2,7 +2,10 @@ import type { SupabaseClient } from '@supabase/supabase-js'; 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 */ @@ -140,6 +143,26 @@ export function createNewsletterApi(client: SupabaseClient) { return data; }, + async updateNewsletter(input: UpdateNewsletterInput) { + const update: Record = {}; + 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) { const { data, error } = await client .from('newsletters') diff --git a/packages/features/verbandsverwaltung/src/components/create-club-form.tsx b/packages/features/verbandsverwaltung/src/components/create-club-form.tsx index 335988ab7..958acc94f 100644 --- a/packages/features/verbandsverwaltung/src/components/create-club-form.tsx +++ b/packages/features/verbandsverwaltung/src/components/create-club-form.tsx @@ -18,8 +18,11 @@ import { import { Input } from '@kit/ui/input'; import { useActionWithToast } from '@kit/ui/use-action-with-toast'; -import { CreateMemberClubSchema } from '../schema/verband.schema'; -import { createClub } from '../server/actions/verband-actions'; +import { + CreateMemberClubSchema, + UpdateMemberClubSchema, +} from '../schema/verband.schema'; +import { createClub, updateClub } from '../server/actions/verband-actions'; interface CreateClubFormProps { accountId: string; @@ -38,7 +41,9 @@ export function CreateClubForm({ const isEdit = !!club; const form = useForm({ - resolver: zodResolver(CreateMemberClubSchema), + resolver: zodResolver( + isEdit ? UpdateMemberClubSchema : CreateMemberClubSchema, + ), defaultValues: { accountId, name: (club?.name as string) ?? '', @@ -61,18 +66,37 @@ export function CreateClubForm({ }, }); - const { execute, isPending } = useActionWithToast(createClub, { - successMessage: isEdit ? 'Verein aktualisiert' : 'Verein erstellt', - errorMessage: 'Fehler beim Speichern', - onSuccess: () => { - router.push(`/home/${account}/verband/clubs`); + const { execute: execCreate, isPending: isCreating } = useActionWithToast( + createClub, + { + successMessage: 'Verein erstellt', + 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 (
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" > {/* Card 1: Grunddaten */}