feat: enhance API response handling and add new components for module management
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 4m50s
Workflow / ⚫️ Test (push) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-01 15:18:24 +02:00
parent f82a366a52
commit 7b078f298b
58 changed files with 1845 additions and 398 deletions

View File

@@ -1,11 +1,22 @@
'use client';
import { useState } from 'react';
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 {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import {
@@ -17,7 +28,7 @@ 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 { CreateMemberSchema } from '../schema/member.schema';
import { createMember } from '../server/actions/member-actions';
@@ -28,12 +39,19 @@ interface Props {
duesCategories: Array<{ id: string; name: string; amount: number }>;
}
interface DuplicateEntry {
field: string;
message: string;
id?: string;
}
export function CreateMemberForm({
accountId,
account,
duesCategories,
}: Props) {
const router = useRouter();
const [duplicates, setDuplicates] = useState<DuplicateEntry[]>([]);
const form = useForm({
resolver: zodResolver(CreateMemberSchema),
defaultValues: {
@@ -59,15 +77,16 @@ export function CreateMemberForm({
},
});
const { execute, isPending } = useAction(createMember, {
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Mitglied erfolgreich erstellt');
router.push(`/home/${account}/members-cms`);
}
const { execute, isPending } = useActionWithToast(createMember, {
successMessage: 'Mitglied erfolgreich erstellt',
errorMessage: 'Fehler beim Erstellen',
onSuccess: () => {
router.push(`/home/${account}/members-cms`);
},
onError: ({ error }) => {
toast.error(error.serverError ?? 'Fehler beim Erstellen');
onError: (_error, data) => {
if (data?.validationErrors) {
setDuplicates(data.validationErrors as DuplicateEntry[]);
}
},
});
@@ -586,6 +605,37 @@ export function CreateMemberForm({
</Button>
</div>
</form>
<AlertDialog
open={duplicates.length > 0}
onOpenChange={() => setDuplicates([])}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Mögliches Duplikat gefunden</AlertDialogTitle>
<AlertDialogDescription>
Es wurden ähnliche Mitglieder gefunden:
<ul className="mt-2 list-inside list-disc">
{duplicates.map((d, i) => (
<li key={d.id ?? i}>{d.message}</li>
))}
</ul>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
router.push(
`/home/${account}/members-cms/${duplicates[0]?.id}`,
);
}}
>
Zum bestehenden Mitglied
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Form>
);
}

View File

@@ -4,16 +4,20 @@ import { useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAction } from 'next-safe-action/hooks';
import { Download } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { formatDate } from '@kit/shared/dates';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Input } from '@kit/ui/input';
import { toast } from '@kit/ui/sonner';
import { useFileDownloadAction } from '@kit/ui/use-file-download-action';
import { STATUS_LABELS, getMemberStatusColor } from '../lib/member-utils';
import {
exportMembers,
exportMembersExcel,
} from '../server/actions/member-actions';
interface MembersDataTableProps {
data: Array<Record<string, unknown>>;
@@ -21,6 +25,7 @@ interface MembersDataTableProps {
page: number;
pageSize: number;
account: string;
accountId: string;
duesCategories: Array<{ id: string; name: string }>;
}
@@ -38,6 +43,7 @@ export function MembersDataTable({
page,
pageSize,
account,
accountId,
duesCategories,
}: MembersDataTableProps) {
const router = useRouter();
@@ -102,6 +108,20 @@ export function MembersDataTable({
[router, account],
);
const { execute: execCsvExport, isPending: isCsvExporting } =
useFileDownloadAction(exportMembers, {
successMessage: 'CSV-Export heruntergeladen',
errorMessage: 'CSV-Export fehlgeschlagen',
});
const { execute: execExcelExport, isPending: isExcelExporting } =
useFileDownloadAction(exportMembersExcel, {
successMessage: 'Excel-Export heruntergeladen',
errorMessage: 'Excel-Export fehlgeschlagen',
});
const isExporting = isCsvExporting || isExcelExporting;
return (
<div className="space-y-4">
{/* Toolbar */}
@@ -137,6 +157,34 @@ export function MembersDataTable({
))}
</select>
<Button
variant="outline"
size="sm"
disabled={isExporting}
onClick={() =>
execCsvExport({
accountId,
status: currentStatus || undefined,
})
}
>
<Download className="mr-1 h-4 w-4" />
CSV
</Button>
<Button
variant="outline"
size="sm"
disabled={isExporting}
onClick={() =>
execExcelExport({
accountId,
status: currentStatus || undefined,
})
}
>
<Download className="mr-1 h-4 w-4" />
Excel
</Button>
<Button
size="sm"
data-test="members-new-btn"

View File

@@ -30,6 +30,26 @@ export const createMember = authActionClient
const api = createMemberManagementApi(client);
const userId = ctx.user.id;
// Check for duplicates before creating
const duplicates = await api.checkDuplicate(
input.accountId,
input.firstName,
input.lastName,
input.dateOfBirth,
);
if (duplicates.length > 0) {
return {
success: false,
error: 'Mögliche Duplikate gefunden',
validationErrors: duplicates.map((d: Record<string, unknown>) => ({
field: 'name',
message: `${d.first_name} ${d.last_name}${d.member_number ? ` (Nr. ${d.member_number})` : ''}`,
id: String(d.id),
})),
};
}
logger.info({ name: 'member.create' }, 'Creating member...');
const result = await api.createMember(input, userId);
logger.info({ name: 'member.create' }, 'Member created');
@@ -217,7 +237,14 @@ export const exportMembers = authActionClient
const csv = await api.exportMembersCsv(input.accountId, {
status: input.status,
});
return { success: true, csv };
return {
success: true,
data: {
content: csv,
filename: `mitglieder_${new Date().toISOString().split('T')[0]}.csv`,
mimeType: 'text/csv',
},
};
});
// Gap 5: Department assignments
@@ -248,11 +275,14 @@ export const exportMembersExcel = authActionClient
const buffer = await api.exportMembersExcel(input.accountId, {
status: input.status,
});
// Return base64 for client-side download
return {
success: true,
base64: buffer.toString('base64'),
filename: `mitglieder_${new Date().toISOString().split('T')[0]}.xlsx`,
data: {
content: buffer.toString('base64'),
filename: `mitglieder_${new Date().toISOString().split('T')[0]}.xlsx`,
mimeType:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
};
});
@@ -295,8 +325,11 @@ export const generateMemberCards = authActionClient
return {
success: true,
base64: buffer.toString('base64'),
filename: `mitgliedsausweise_${new Date().toISOString().split('T')[0]}.pdf`,
data: {
content: buffer.toString('base64'),
filename: `mitgliedsausweise_${new Date().toISOString().split('T')[0]}.pdf`,
mimeType: 'application/pdf',
},
};
});