feat: add delete functionality for leases, catch books, and permits; implement newsletter update feature
This commit is contained in:
@@ -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<Record<string, unknown>>;
|
||||
@@ -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({
|
||||
<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-left font-medium">Status</th>
|
||||
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -190,6 +204,34 @@ export function CatchBooksDataTable({
|
||||
{CATCH_BOOK_STATUS_LABELS[status] ?? status}
|
||||
</Badge>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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<Record<string, unknown>>;
|
||||
@@ -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<string, string>) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
@@ -95,6 +109,7 @@ export function CompetitionsDataTable({
|
||||
<th className="p-3 text-right font-medium">
|
||||
Max. Teilnehmer
|
||||
</th>
|
||||
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -126,6 +141,34 @@ export function CompetitionsDataTable({
|
||||
? String(comp.max_participants)
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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';
|
||||
|
||||
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 };
|
||||
});
|
||||
|
||||
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
|
||||
// =====================================================
|
||||
|
||||
@@ -670,6 +670,14 @@ export function createFischereiApi(client: SupabaseClient<Database>) {
|
||||
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<Database>) {
|
||||
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<Database>) {
|
||||
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
|
||||
// =====================================================
|
||||
|
||||
@@ -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 (
|
||||
<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"
|
||||
>
|
||||
<Card>
|
||||
@@ -146,20 +182,15 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
data-test="newsletter-cancel-btn"
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
data-test="newsletter-submit-btn"
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Newsletter erstellen'}
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending
|
||||
? 'Wird gespeichert...'
|
||||
: isEdit
|
||||
? 'Newsletter aktualisieren'
|
||||
: 'Newsletter erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -18,6 +18,11 @@ export const CreateNewsletterSchema = z.object({
|
||||
});
|
||||
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({
|
||||
accountId: z.string().uuid(),
|
||||
name: z.string().min(1),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Database>) {
|
||||
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) {
|
||||
const { data, error } = await client
|
||||
.from('newsletters')
|
||||
|
||||
@@ -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 (
|
||||
<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"
|
||||
>
|
||||
{/* Card 1: Grunddaten */}
|
||||
|
||||
Reference in New Issue
Block a user