Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { approveApplication, rejectApplication } from '../server/actions/member-actions';
|
||||
import {
|
||||
APPLICATION_STATUS_VARIANT,
|
||||
APPLICATION_STATUS_LABEL,
|
||||
} from '../lib/member-utils';
|
||||
import {
|
||||
approveApplication,
|
||||
rejectApplication,
|
||||
} from '../server/actions/member-actions';
|
||||
|
||||
interface ApplicationWorkflowProps {
|
||||
applications: Array<Record<string, unknown>>;
|
||||
@@ -58,11 +65,7 @@ export function ApplicationWorkflow({
|
||||
|
||||
const handleApprove = useCallback(
|
||||
(applicationId: string) => {
|
||||
if (
|
||||
!window.confirm(
|
||||
'Mitglied wird automatisch erstellt. Fortfahren?',
|
||||
)
|
||||
) {
|
||||
if (!window.confirm('Mitglied wird automatisch erstellt. Fortfahren?')) {
|
||||
return;
|
||||
}
|
||||
executeApprove({ applicationId, accountId });
|
||||
@@ -91,7 +94,7 @@ export function ApplicationWorkflow({
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Aufnahmeanträge</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{applications.length} Antrag{applications.length !== 1 ? 'e' : ''}
|
||||
</p>
|
||||
</div>
|
||||
@@ -99,7 +102,7 @@ export function ApplicationWorkflow({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="px-4 py-3 text-left font-medium">Name</th>
|
||||
<th className="px-4 py-3 text-left font-medium">E-Mail</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Datum</th>
|
||||
@@ -112,7 +115,7 @@ export function ApplicationWorkflow({
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
className="px-4 py-8 text-center text-muted-foreground"
|
||||
className="text-muted-foreground px-4 py-8 text-center"
|
||||
>
|
||||
Keine Aufnahmeanträge vorhanden.
|
||||
</td>
|
||||
@@ -130,18 +133,18 @@ export function ApplicationWorkflow({
|
||||
{String(app.last_name ?? '')},{' '}
|
||||
{String(app.first_name ?? '')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground px-4 py-3">
|
||||
{String(app.email ?? '—')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{app.created_at
|
||||
? new Date(String(app.created_at)).toLocaleDateString(
|
||||
'de-DE',
|
||||
)
|
||||
: '—'}
|
||||
<td className="text-muted-foreground px-4 py-3">
|
||||
{formatDate(app.created_at as string)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={APPLICATION_STATUS_VARIANT[appStatus] ?? 'secondary'}>
|
||||
<Badge
|
||||
variant={
|
||||
APPLICATION_STATUS_VARIANT[appStatus] ?? 'secondary'
|
||||
}
|
||||
>
|
||||
{APPLICATION_STATUS_LABEL[appStatus] ?? appStatus}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
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';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { CreateMemberSchema } from '../schema/member.schema';
|
||||
import { createMember } from '../server/actions/member-actions';
|
||||
|
||||
@@ -18,16 +28,34 @@ interface Props {
|
||||
duesCategories: Array<{ id: string; name: string; amount: number }>;
|
||||
}
|
||||
|
||||
export function CreateMemberForm({ accountId, account, duesCategories }: Props) {
|
||||
export function CreateMemberForm({
|
||||
accountId,
|
||||
account,
|
||||
duesCategories,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
resolver: zodResolver(CreateMemberSchema),
|
||||
defaultValues: {
|
||||
accountId,
|
||||
firstName: '', lastName: '', email: '', phone: '', mobile: '',
|
||||
street: '', houseNumber: '', postalCode: '', city: '', country: 'DE',
|
||||
memberNumber: '', status: 'active' as const, entryDate: new Date().toISOString().split('T')[0]!,
|
||||
iban: '', bic: '', accountHolder: '', gdprConsent: false, notes: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
street: '',
|
||||
houseNumber: '',
|
||||
postalCode: '',
|
||||
city: '',
|
||||
country: 'DE',
|
||||
memberNumber: '',
|
||||
status: 'active' as const,
|
||||
entryDate: new Date().toISOString().split('T')[0]!,
|
||||
iban: '',
|
||||
bic: '',
|
||||
accountHolder: '',
|
||||
gdprConsent: false,
|
||||
notes: '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,197 +73,517 @@ export function CreateMemberForm({ accountId, account, duesCategories }: Props)
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit((data) => execute(data))} className="space-y-6">
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
className="space-y-6"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Persönliche Daten</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Persönliche Daten</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField control={form.control} name="firstName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Vorname *</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="lastName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Nachname *</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="dateOfBirth" render={({ field }) => (
|
||||
<FormItem><FormLabel>Geburtsdatum</FormLabel><FormControl><Input type="date" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="gender" render={({ field }) => (
|
||||
<FormItem><FormLabel>Geschlecht</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="">— Bitte wählen —</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Vorname *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nachname *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dateOfBirth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Geburtsdatum</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gender"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Geschlecht</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Bitte wählen —</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Kontakt</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Kontakt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="email" render={({ field }) => (
|
||||
<FormItem><FormLabel>E-Mail</FormLabel><FormControl><Input type="email" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="phone" render={({ field }) => (
|
||||
<FormItem><FormLabel>Telefon</FormLabel><FormControl><Input type="tel" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="mobile" render={({ field }) => (
|
||||
<FormItem><FormLabel>Mobil</FormLabel><FormControl><Input type="tel" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-Mail</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="tel" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mobile"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mobil</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="tel" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Adresse</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Adresse</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField control={form.control} name="street" render={({ field }) => (
|
||||
<FormItem><FormLabel>Straße</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="houseNumber" render={({ field }) => (
|
||||
<FormItem><FormLabel>Hausnummer</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="postalCode" render={({ field }) => (
|
||||
<FormItem><FormLabel>PLZ</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="city" render={({ field }) => (
|
||||
<FormItem><FormLabel>Ort</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="street"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Straße</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="houseNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Hausnummer</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="postalCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>PLZ</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Ort</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Mitgliedschaft</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Mitgliedschaft</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="memberNumber" render={({ field }) => (
|
||||
<FormItem><FormLabel>Mitgliedsnr.</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="status" render={({ field }) => (
|
||||
<FormItem><FormLabel>Status</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="pending">Ausstehend</option>
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="entryDate" render={({ field }) => (
|
||||
<FormItem><FormLabel>Eintrittsdatum</FormLabel><FormControl><Input type="date" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memberNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mitgliedsnr.</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="pending">Ausstehend</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="entryDate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Eintrittsdatum</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{duesCategories.length > 0 && (
|
||||
<FormField control={form.control} name="duesCategoryId" render={({ field }) => (
|
||||
<FormItem><FormLabel>Beitragskategorie</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="">— Keine —</option>
|
||||
{duesCategories.map(c => <option key={c.id} value={c.id}>{c.name} ({c.amount} €)</option>)}
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="duesCategoryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Beitragskategorie</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Keine —</option>
|
||||
{duesCategories.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name} ({c.amount} €)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>SEPA-Bankdaten</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>SEPA-Bankdaten</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="iban" render={({ field }) => (
|
||||
<FormItem><FormLabel>IBAN</FormLabel><FormControl><Input placeholder="DE89 3704 0044 0532 0130 00" {...field} onChange={(e) => field.onChange(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="bic" render={({ field }) => (
|
||||
<FormItem><FormLabel>BIC</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="accountHolder" render={({ field }) => (
|
||||
<FormItem><FormLabel>Kontoinhaber</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="iban"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IBAN</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="DE89 3704 0044 0532 0130 00"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, ''),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>BIC</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accountHolder"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Kontoinhaber</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Guardian (Gap 4) */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Erziehungsberechtigte (Jugend)</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Erziehungsberechtigte (Jugend)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="guardianName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Name Erziehungsberechtigte/r</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="guardianPhone" render={({ field }) => (
|
||||
<FormItem><FormLabel>Telefon</FormLabel><FormControl><Input type="tel" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="guardianEmail" render={({ field }) => (
|
||||
<FormItem><FormLabel>E-Mail</FormLabel><FormControl><Input type="email" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guardianName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name Erziehungsberechtigte/r</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guardianPhone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="tel" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guardianEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-Mail</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Lifecycle flags (Gap 4) */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Mitgliedschaftsmerkmale</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Mitgliedschaftsmerkmale</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{([
|
||||
['isHonorary', 'Ehrenmitglied'],
|
||||
['isFoundingMember', 'Gründungsmitglied'],
|
||||
['isYouth', 'Jugendmitglied'],
|
||||
['isRetiree', 'Rentner/Senior'],
|
||||
['isProbationary', 'Probejahr'],
|
||||
] as const).map(([name, label]) => (
|
||||
<FormField key={name} control={form.control} name={name} render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input type="checkbox" checked={field.value as boolean} onChange={field.onChange} className="h-4 w-4 rounded border-input" />
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)} />
|
||||
{(
|
||||
[
|
||||
['isHonorary', 'Ehrenmitglied'],
|
||||
['isFoundingMember', 'Gründungsmitglied'],
|
||||
['isYouth', 'Jugendmitglied'],
|
||||
['isRetiree', 'Rentner/Senior'],
|
||||
['isProbationary', 'Probejahr'],
|
||||
] as const
|
||||
).map(([name, label]) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value as boolean}
|
||||
onChange={field.onChange}
|
||||
className="border-input h-4 w-4 rounded"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* GDPR granular (Gap 4) */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Datenschutz-Einwilligungen</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Datenschutz-Einwilligungen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{([
|
||||
['gdprConsent', 'Allgemeine Einwilligung'],
|
||||
['gdprNewsletter', 'Newsletter'],
|
||||
['gdprInternet', 'Internet/Homepage'],
|
||||
['gdprPrint', 'Vereinszeitung'],
|
||||
['gdprBirthdayInfo', 'Geburtstagsinfo'],
|
||||
] as const).map(([name, label]) => (
|
||||
<FormField key={name} control={form.control} name={name} render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input type="checkbox" checked={field.value as boolean} onChange={field.onChange} className="h-4 w-4 rounded border-input" />
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)} />
|
||||
{(
|
||||
[
|
||||
['gdprConsent', 'Allgemeine Einwilligung'],
|
||||
['gdprNewsletter', 'Newsletter'],
|
||||
['gdprInternet', 'Internet/Homepage'],
|
||||
['gdprPrint', 'Vereinszeitung'],
|
||||
['gdprBirthdayInfo', 'Geburtstagsinfo'],
|
||||
] as const
|
||||
).map(([name, label]) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value as boolean}
|
||||
onChange={field.onChange}
|
||||
className="border-input h-4 w-4 rounded"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Sonstiges</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Sonstiges</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="salutation" render={({ field }) => (
|
||||
<FormItem><FormLabel>Anrede</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="">— Keine —</option>
|
||||
<option value="Herr">Herr</option>
|
||||
<option value="Frau">Frau</option>
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="birthplace" render={({ field }) => (
|
||||
<FormItem><FormLabel>Geburtsort</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="street2" render={({ field }) => (
|
||||
<FormItem><FormLabel>Adresszusatz</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="salutation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Anrede</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Keine —</option>
|
||||
<option value="Herr">Herr</option>
|
||||
<option value="Frau">Frau</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="birthplace"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Geburtsort</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="street2"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Adresszusatz</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField control={form.control} name="notes" render={({ field }) => (
|
||||
<FormItem><FormLabel>Notizen</FormLabel><FormControl><textarea {...field} className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm" /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notizen</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>Abbrechen</Button>
|
||||
<Button type="submit" disabled={isPending}>{isPending ? 'Wird erstellt...' : 'Mitglied erstellen'}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Wird erstellt...' : 'Mitglied erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { formatCurrencyAmount } from '@kit/shared/formatters';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import {
|
||||
createDuesCategory,
|
||||
@@ -90,7 +94,11 @@ export function DuesCategoryManager({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
amount: Number(values.amount),
|
||||
interval: values.interval as 'monthly' | 'quarterly' | 'half_yearly' | 'yearly',
|
||||
interval: values.interval as
|
||||
| 'monthly'
|
||||
| 'quarterly'
|
||||
| 'half_yearly'
|
||||
| 'yearly',
|
||||
isDefault: values.isDefault,
|
||||
});
|
||||
},
|
||||
@@ -100,9 +108,7 @@ export function DuesCategoryManager({
|
||||
const handleDelete = useCallback(
|
||||
(categoryId: string, categoryName: string) => {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Beitragskategorie "${categoryName}" wirklich löschen?`,
|
||||
)
|
||||
!window.confirm(`Beitragskategorie "${categoryName}" wirklich löschen?`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -158,7 +164,7 @@ export function DuesCategoryManager({
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Intervall</label>
|
||||
<select
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
{...form.register('interval')}
|
||||
>
|
||||
<option value="monthly">Monatlich</option>
|
||||
@@ -171,7 +177,7 @@ export function DuesCategoryManager({
|
||||
<label className="flex items-center gap-2 text-sm font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-input"
|
||||
className="border-input rounded"
|
||||
{...form.register('isDefault')}
|
||||
/>
|
||||
Standard
|
||||
@@ -191,7 +197,7 @@ export function DuesCategoryManager({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="px-4 py-3 text-left font-medium">Name</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Beschreibung</th>
|
||||
<th className="px-4 py-3 text-right font-medium">Betrag</th>
|
||||
@@ -205,7 +211,7 @@ export function DuesCategoryManager({
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="px-4 py-8 text-center text-muted-foreground"
|
||||
className="text-muted-foreground px-4 py-8 text-center"
|
||||
>
|
||||
Keine Beitragskategorien vorhanden.
|
||||
</td>
|
||||
@@ -221,14 +227,11 @@ export function DuesCategoryManager({
|
||||
return (
|
||||
<tr key={catId} className="border-b">
|
||||
<td className="px-4 py-3 font-medium">{catName}</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground px-4 py-3">
|
||||
{String(cat.description ?? '—')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono">
|
||||
{amount.toLocaleString('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
})}
|
||||
{formatCurrencyAmount(amount)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{INTERVAL_LABELS[interval] ?? interval}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
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';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { UpdateMemberSchema } from '../schema/member.schema';
|
||||
@@ -78,131 +86,382 @@ export function EditMemberForm({ member, account, accountId }: Props) {
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit((data) => execute(data))} className="space-y-6">
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
className="space-y-6"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Persönliche Daten</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Persönliche Daten</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField control={form.control} name="salutation" render={({ field }) => (
|
||||
<FormItem><FormLabel>Anrede</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="">—</option><option value="Herr">Herr</option><option value="Frau">Frau</option>
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="salutation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Anrede</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="Herr">Herr</option>
|
||||
<option value="Frau">Frau</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div />
|
||||
<FormField control={form.control} name="firstName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Vorname *</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="lastName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Nachname *</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="memberNumber" render={({ field }) => (
|
||||
<FormItem><FormLabel>Mitgliedsnr.</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="birthplace" render={({ field }) => (
|
||||
<FormItem><FormLabel>Geburtsort</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Vorname *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nachname *</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memberNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mitgliedsnr.</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="birthplace"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Geburtsort</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Kontakt</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Kontakt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="email" render={({ field }) => (
|
||||
<FormItem><FormLabel>E-Mail</FormLabel><FormControl><Input type="email" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="phone" render={({ field }) => (
|
||||
<FormItem><FormLabel>Telefon</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="mobile" render={({ field }) => (
|
||||
<FormItem><FormLabel>Mobil</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-Mail</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mobile"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mobil</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Adresse</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Adresse</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField control={form.control} name="street" render={({ field }) => (
|
||||
<FormItem><FormLabel>Straße</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="houseNumber" render={({ field }) => (
|
||||
<FormItem><FormLabel>Hausnummer</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="street2" render={({ field }) => (
|
||||
<FormItem><FormLabel>Adresszusatz</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="street"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Straße</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="houseNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Hausnummer</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="street2"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Adresszusatz</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div />
|
||||
<FormField control={form.control} name="postalCode" render={({ field }) => (
|
||||
<FormItem><FormLabel>PLZ</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="city" render={({ field }) => (
|
||||
<FormItem><FormLabel>Ort</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="postalCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>PLZ</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Ort</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>SEPA-Bankdaten</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>SEPA-Bankdaten</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="iban" render={({ field }) => (
|
||||
<FormItem><FormLabel>IBAN</FormLabel><FormControl><Input {...field} onChange={(e) => field.onChange(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="bic" render={({ field }) => (
|
||||
<FormItem><FormLabel>BIC</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="accountHolder" render={({ field }) => (
|
||||
<FormItem><FormLabel>Kontoinhaber</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="iban"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IBAN</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, ''),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>BIC</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accountHolder"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Kontoinhaber</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Erziehungsberechtigte</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Erziehungsberechtigte</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<FormField control={form.control} name="guardianName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Name</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="guardianPhone" render={({ field }) => (
|
||||
<FormItem><FormLabel>Telefon</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="guardianEmail" render={({ field }) => (
|
||||
<FormItem><FormLabel>E-Mail</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guardianName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guardianPhone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="guardianEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-Mail</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Merkmale & Datenschutz</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Merkmale & Datenschutz</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
{([
|
||||
['isHonorary', 'Ehrenmitglied'], ['isFoundingMember', 'Gründungsmitglied'],
|
||||
['isYouth', 'Jugend'], ['isRetiree', 'Rentner'],
|
||||
['isProbationary', 'Probejahr'],
|
||||
] as const).map(([name, label]) => (
|
||||
<FormField key={name} control={form.control} name={name} render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl><input type="checkbox" checked={field.value as boolean} onChange={field.onChange} className="h-4 w-4" /></FormControl>
|
||||
<FormLabel className="!mt-0 text-sm">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)} />
|
||||
{(
|
||||
[
|
||||
['isHonorary', 'Ehrenmitglied'],
|
||||
['isFoundingMember', 'Gründungsmitglied'],
|
||||
['isYouth', 'Jugend'],
|
||||
['isRetiree', 'Rentner'],
|
||||
['isProbationary', 'Probejahr'],
|
||||
] as const
|
||||
).map(([name, label]) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value as boolean}
|
||||
onChange={field.onChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0 text-sm">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2">DSGVO-Einwilligungen</p>
|
||||
<p className="text-muted-foreground mb-2 text-xs font-medium">
|
||||
DSGVO-Einwilligungen
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
{([
|
||||
['gdprConsent', 'Allgemein'], ['gdprNewsletter', 'Newsletter'],
|
||||
['gdprInternet', 'Internet'], ['gdprPrint', 'Zeitung'],
|
||||
['gdprBirthdayInfo', 'Geburtstag'],
|
||||
] as const).map(([name, label]) => (
|
||||
<FormField key={name} control={form.control} name={name} render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl><input type="checkbox" checked={field.value as boolean} onChange={field.onChange} className="h-4 w-4" /></FormControl>
|
||||
<FormLabel className="!mt-0 text-sm">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)} />
|
||||
{(
|
||||
[
|
||||
['gdprConsent', 'Allgemein'],
|
||||
['gdprNewsletter', 'Newsletter'],
|
||||
['gdprInternet', 'Internet'],
|
||||
['gdprPrint', 'Zeitung'],
|
||||
['gdprBirthdayInfo', 'Geburtstag'],
|
||||
] as const
|
||||
).map(([name, label]) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value as boolean}
|
||||
onChange={field.onChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0 text-sm">{label}</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,17 +469,36 @@ export function EditMemberForm({ member, account, accountId }: Props) {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Notizen</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Notizen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FormField control={form.control} name="notes" render={({ field }) => (
|
||||
<FormItem><FormControl><textarea {...field} rows={4} className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm" /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
rows={4}
|
||||
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>Abbrechen</Button>
|
||||
<Button type="submit" disabled={isPending}>{isPending ? 'Wird gespeichert...' : 'Änderungen speichern'}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Wird gespeichert...' : 'Änderungen speichern'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { formatIban } from '../lib/member-utils';
|
||||
import { createMandate, revokeMandate } from '../server/actions/member-actions';
|
||||
@@ -119,7 +123,7 @@ export function MandateManager({
|
||||
bic: values.bic,
|
||||
accountHolder: values.accountHolder,
|
||||
mandateDate: values.mandateDate,
|
||||
sequence: values.sequence as "FRST" | "RCUR" | "FNAL" | "OOFF",
|
||||
sequence: values.sequence as 'FRST' | 'RCUR' | 'FNAL' | 'OOFF',
|
||||
});
|
||||
},
|
||||
[executeCreate, memberId, accountId],
|
||||
@@ -127,11 +131,7 @@ export function MandateManager({
|
||||
|
||||
const handleRevoke = useCallback(
|
||||
(mandateId: string, reference: string) => {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Mandat "${reference}" wirklich widerrufen?`,
|
||||
)
|
||||
) {
|
||||
if (!window.confirm(`Mandat "${reference}" wirklich widerrufen?`)) {
|
||||
return;
|
||||
}
|
||||
executeRevoke({ mandateId });
|
||||
@@ -185,10 +185,7 @@ export function MandateManager({
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">BIC</label>
|
||||
<Input
|
||||
placeholder="COBADEFFXXX"
|
||||
{...form.register('bic')}
|
||||
/>
|
||||
<Input placeholder="COBADEFFXXX" {...form.register('bic')} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Kontoinhaber *</label>
|
||||
@@ -207,7 +204,7 @@ export function MandateManager({
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Sequenz</label>
|
||||
<select
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
{...form.register('sequence')}
|
||||
>
|
||||
<option value="FRST">FRST – Erstlastschrift</option>
|
||||
@@ -230,7 +227,7 @@ export function MandateManager({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="px-4 py-3 text-left font-medium">Referenz</th>
|
||||
<th className="px-4 py-3 text-left font-medium">IBAN</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Kontoinhaber</th>
|
||||
@@ -245,7 +242,7 @@ export function MandateManager({
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="px-4 py-8 text-center text-muted-foreground"
|
||||
className="text-muted-foreground px-4 py-8 text-center"
|
||||
>
|
||||
Keine SEPA-Mandate vorhanden.
|
||||
</td>
|
||||
@@ -261,21 +258,15 @@ export function MandateManager({
|
||||
|
||||
return (
|
||||
<tr key={mandateId} className="border-b">
|
||||
<td className="px-4 py-3 font-mono text-xs">
|
||||
{reference}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs">{reference}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs">
|
||||
{formatIban(mandate.iban as string | null | undefined)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{String(mandate.account_holder ?? '—')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{mandate.mandate_date
|
||||
? new Date(
|
||||
String(mandate.mandate_date),
|
||||
).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
<td className="text-muted-foreground px-4 py-3">
|
||||
{formatDate(mandate.mandate_date as string)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={getMandateStatusColor(mandateStatus)}>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import {
|
||||
STATUS_LABELS,
|
||||
@@ -25,16 +29,26 @@ interface MemberDetailViewProps {
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
function DetailRow({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 border-b py-2 last:border-b-0">
|
||||
<span className="text-sm font-medium text-muted-foreground">{label}</span>
|
||||
<span className="text-sm text-right">{value ?? '—'}</span>
|
||||
<span className="text-muted-foreground text-sm font-medium">{label}</span>
|
||||
<span className="text-right text-sm">{value ?? '—'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MemberDetailView({ member, account, accountId }: MemberDetailViewProps) {
|
||||
export function MemberDetailView({
|
||||
member,
|
||||
account,
|
||||
accountId,
|
||||
}: MemberDetailViewProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const memberId = String(member.id ?? '');
|
||||
@@ -45,32 +59,42 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
|
||||
const form = useForm();
|
||||
|
||||
const { execute: executeDelete, isPending: isDeleting } = useAction(deleteMember, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success('Mitglied wurde gekündigt');
|
||||
router.push(`/home/${account}/members-cms`);
|
||||
}
|
||||
const { execute: executeDelete, isPending: isDeleting } = useAction(
|
||||
deleteMember,
|
||||
{
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success('Mitglied wurde gekündigt');
|
||||
router.push(`/home/${account}/members-cms`);
|
||||
}
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
toast.error(error.serverError ?? 'Fehler beim Kündigen');
|
||||
},
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
toast.error(error.serverError ?? 'Fehler beim Kündigen');
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const { execute: executeUpdate, isPending: isUpdating } = useAction(updateMember, {
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success('Mitglied wurde archiviert');
|
||||
router.refresh();
|
||||
}
|
||||
const { execute: executeUpdate, isPending: isUpdating } = useAction(
|
||||
updateMember,
|
||||
{
|
||||
onSuccess: ({ data }) => {
|
||||
if (data?.success) {
|
||||
toast.success('Mitglied wurde archiviert');
|
||||
router.refresh();
|
||||
}
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
toast.error(error.serverError ?? 'Fehler beim Archivieren');
|
||||
},
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
toast.error(error.serverError ?? 'Fehler beim Archivieren');
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!window.confirm(`Möchten Sie ${fullName} wirklich kündigen? Diese Aktion kann nicht rückgängig gemacht werden.`)) {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Möchten Sie ${fullName} wirklich kündigen? Diese Aktion kann nicht rückgängig gemacht werden.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
executeDelete({ memberId, accountId });
|
||||
@@ -88,7 +112,9 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
}, [executeUpdate, memberId, accountId, fullName]);
|
||||
|
||||
const age = computeAge(member.date_of_birth as string | null | undefined);
|
||||
const membershipYears = computeMembershipYears(member.entry_date as string | null | undefined);
|
||||
const membershipYears = computeMembershipYears(
|
||||
member.entry_date as string | null | undefined,
|
||||
);
|
||||
const address = formatAddress(member);
|
||||
const iban = formatIban(member.iban as string | null | undefined);
|
||||
|
||||
@@ -103,7 +129,7 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
{STATUS_LABELS[status] ?? status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Mitgliedsnr. {String(member.member_number ?? '—')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -147,12 +173,18 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
label="Geburtsdatum"
|
||||
value={
|
||||
member.date_of_birth
|
||||
? `${new Date(String(member.date_of_birth)).toLocaleDateString('de-DE')}${age !== null ? ` (${age} Jahre)` : ''}`
|
||||
? `${formatDate(member.date_of_birth as string)}${age !== null ? ` (${age} Jahre)` : ''}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<DetailRow label="Geschlecht" value={String(member.gender ?? '—')} />
|
||||
<DetailRow label="Anrede" value={String(member.salutation ?? '—')} />
|
||||
<DetailRow
|
||||
label="Geschlecht"
|
||||
value={String(member.gender ?? '—')}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Anrede"
|
||||
value={String(member.salutation ?? '—')}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -187,7 +219,10 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
<CardTitle>Mitgliedschaft</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DetailRow label="Mitgliedsnr." value={String(member.member_number ?? '—')} />
|
||||
<DetailRow
|
||||
label="Mitgliedsnr."
|
||||
value={String(member.member_number ?? '—')}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Status"
|
||||
value={
|
||||
@@ -198,11 +233,7 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
/>
|
||||
<DetailRow
|
||||
label="Eintrittsdatum"
|
||||
value={
|
||||
member.entry_date
|
||||
? new Date(String(member.entry_date)).toLocaleDateString('de-DE')
|
||||
: '—'
|
||||
}
|
||||
value={formatDate(member.entry_date as string)}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Mitgliedsjahre"
|
||||
@@ -210,7 +241,10 @@ export function MemberDetailView({ member, account, accountId }: MemberDetailVie
|
||||
/>
|
||||
<DetailRow label="IBAN" value={iban} />
|
||||
<DetailRow label="BIC" value={String(member.bic ?? '—')} />
|
||||
<DetailRow label="Kontoinhaber" value={String(member.account_holder ?? '—')} />
|
||||
<DetailRow
|
||||
label="Kontoinhaber"
|
||||
value={String(member.account_holder ?? '—')}
|
||||
/>
|
||||
<DetailRow label="Notizen" value={String(member.notes ?? '—')} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import {
|
||||
Upload,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { Upload, ArrowRight, ArrowLeft, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||
|
||||
import { createMember } from '../server/actions/member-actions';
|
||||
|
||||
@@ -46,45 +54,56 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
const [rawData, setRawData] = useState<string[][]>([]);
|
||||
const [headers, setHeaders] = useState<string[]>([]);
|
||||
const [mapping, setMapping] = useState<Record<string, string>>({});
|
||||
const [importResults, setImportResults] = useState<{ success: number; errors: string[] }>({ success: 0, errors: [] });
|
||||
const [importResults, setImportResults] = useState<{
|
||||
success: number;
|
||||
errors: string[];
|
||||
}>({ success: 0, errors: [] });
|
||||
|
||||
const { execute: executeCreate } = useAction(createMember);
|
||||
|
||||
// Step 1: Parse file
|
||||
const handleFileUpload = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const handleFileUpload = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
Papa.parse(file, {
|
||||
delimiter: ';',
|
||||
encoding: 'UTF-8',
|
||||
complete: (result) => {
|
||||
const data = result.data as string[][];
|
||||
if (data.length < 2) {
|
||||
toast.error('Datei enthält keine Daten');
|
||||
return;
|
||||
}
|
||||
setHeaders(data[0]!);
|
||||
setRawData(data.slice(1).filter(row => row.some(cell => cell?.trim())));
|
||||
|
||||
// Auto-map by header name similarity
|
||||
const autoMap: Record<string, string> = {};
|
||||
for (const field of MEMBER_FIELDS) {
|
||||
const match = data[0]!.findIndex(h =>
|
||||
h.toLowerCase().includes(field.label.toLowerCase().replace('.', '')) ||
|
||||
h.toLowerCase().includes(field.key.toLowerCase())
|
||||
Papa.parse(file, {
|
||||
delimiter: ';',
|
||||
encoding: 'UTF-8',
|
||||
complete: (result) => {
|
||||
const data = result.data as string[][];
|
||||
if (data.length < 2) {
|
||||
toast.error('Datei enthält keine Daten');
|
||||
return;
|
||||
}
|
||||
setHeaders(data[0]!);
|
||||
setRawData(
|
||||
data.slice(1).filter((row) => row.some((cell) => cell?.trim())),
|
||||
);
|
||||
if (match >= 0) autoMap[field.key] = String(match);
|
||||
}
|
||||
setMapping(autoMap);
|
||||
setStep('mapping');
|
||||
toast.success(`${data.length - 1} Zeilen erkannt`);
|
||||
},
|
||||
error: (err) => {
|
||||
toast.error(`Fehler beim Lesen: ${err.message}`);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Auto-map by header name similarity
|
||||
const autoMap: Record<string, string> = {};
|
||||
for (const field of MEMBER_FIELDS) {
|
||||
const match = data[0]!.findIndex(
|
||||
(h) =>
|
||||
h
|
||||
.toLowerCase()
|
||||
.includes(field.label.toLowerCase().replace('.', '')) ||
|
||||
h.toLowerCase().includes(field.key.toLowerCase()),
|
||||
);
|
||||
if (match >= 0) autoMap[field.key] = String(match);
|
||||
}
|
||||
setMapping(autoMap);
|
||||
setStep('mapping');
|
||||
toast.success(`${data.length - 1} Zeilen erkannt`);
|
||||
},
|
||||
error: (err) => {
|
||||
toast.error(`Fehler beim Lesen: ${err.message}`);
|
||||
},
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Step 3: Execute import
|
||||
const executeImport = useCallback(async () => {
|
||||
@@ -111,7 +130,9 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
await executeCreate(memberData as any);
|
||||
success++;
|
||||
} catch (err) {
|
||||
errors.push(`Zeile ${i + 2}: ${err instanceof Error ? err.message : 'Unbekannter Fehler'}`);
|
||||
errors.push(
|
||||
`Zeile ${i + 2}: ${err instanceof Error ? err.message : 'Unbekannter Fehler'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,15 +152,35 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
{/* Step indicator */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{(['upload', 'mapping', 'preview', 'done'] as const).map((s, i) => {
|
||||
const labels = ['Datei hochladen', 'Spalten zuordnen', 'Vorschau & Import', 'Fertig'];
|
||||
const isActive = ['upload', 'mapping', 'preview', 'importing', 'done'].indexOf(step) >= i;
|
||||
const labels = [
|
||||
'Datei hochladen',
|
||||
'Spalten zuordnen',
|
||||
'Vorschau & Import',
|
||||
'Fertig',
|
||||
];
|
||||
const isActive =
|
||||
['upload', 'mapping', 'preview', 'importing', 'done'].indexOf(
|
||||
step,
|
||||
) >= i;
|
||||
return (
|
||||
<div key={s} className="flex items-center gap-2">
|
||||
<div className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold ${
|
||||
isActive ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'
|
||||
}`}>{i + 1}</div>
|
||||
<span className={`text-sm ${isActive ? 'font-semibold' : 'text-muted-foreground'}`}>{labels[i]}</span>
|
||||
{i < 3 && <ArrowRight className="h-4 w-4 text-muted-foreground" />}
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold ${
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm ${isActive ? 'font-semibold' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{labels[i]}
|
||||
</span>
|
||||
{i < 3 && (
|
||||
<ArrowRight className="text-muted-foreground h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -148,14 +189,25 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
{/* Step 1: Upload */}
|
||||
{step === 'upload' && (
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" />Datei hochladen</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Datei hochladen
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-12 text-center">
|
||||
<Upload className="mb-4 h-10 w-10 text-muted-foreground" />
|
||||
<Upload className="text-muted-foreground mb-4 h-10 w-10" />
|
||||
<p className="text-lg font-semibold">CSV-Datei auswählen</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Semikolon-getrennt (;), UTF-8</p>
|
||||
<input type="file" accept=".csv" onChange={handleFileUpload}
|
||||
className="mt-4 block w-full max-w-xs text-sm file:mr-4 file:rounded-md file:border-0 file:bg-primary file:px-4 file:py-2 file:text-sm file:font-semibold file:text-primary-foreground" />
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
Semikolon-getrennt (;), UTF-8
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={handleFileUpload}
|
||||
className="file:bg-primary file:text-primary-foreground mt-4 block w-full max-w-xs text-sm file:mr-4 file:rounded-md file:border-0 file:px-4 file:py-2 file:text-sm file:font-semibold"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -164,33 +216,54 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
{/* Step 2: Column mapping */}
|
||||
{step === 'mapping' && (
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Spalten zuordnen</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Spalten zuordnen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-muted-foreground">{rawData.length} Zeilen erkannt. Ordnen Sie die CSV-Spalten den Mitgliedsfeldern zu.</p>
|
||||
<p className="text-muted-foreground mb-4 text-sm">
|
||||
{rawData.length} Zeilen erkannt. Ordnen Sie die CSV-Spalten den
|
||||
Mitgliedsfeldern zu.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{MEMBER_FIELDS.map(field => (
|
||||
{MEMBER_FIELDS.map((field) => (
|
||||
<div key={field.key} className="flex items-center gap-4">
|
||||
<span className="w-40 text-sm font-medium">{field.label}</span>
|
||||
<span className="w-40 text-sm font-medium">
|
||||
{field.label}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<select
|
||||
value={mapping[field.key] ?? ''}
|
||||
onChange={(e) => setMapping(prev => ({ ...prev, [field.key]: e.target.value }))}
|
||||
className="flex h-9 w-64 rounded-md border border-input bg-background px-3 py-1 text-sm"
|
||||
onChange={(e) =>
|
||||
setMapping((prev) => ({
|
||||
...prev,
|
||||
[field.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="border-input bg-background flex h-9 w-64 rounded-md border px-3 py-1 text-sm"
|
||||
>
|
||||
<option value="">— Nicht zuordnen —</option>
|
||||
{headers.map((h, i) => (
|
||||
<option key={i} value={String(i)}>{h}</option>
|
||||
<option key={i} value={String(i)}>
|
||||
{h}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{mapping[field.key] !== undefined && rawData[0] && (
|
||||
<span className="text-xs text-muted-foreground">z.B. "{rawData[0][Number(mapping[field.key])]}"</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
z.B. "{rawData[0][Number(mapping[field.key])]}"
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button variant="outline" onClick={() => setStep('upload')}><ArrowLeft className="mr-2 h-4 w-4" />Zurück</Button>
|
||||
<Button onClick={() => setStep('preview')}>Vorschau <ArrowRight className="ml-2 h-4 w-4" /></Button>
|
||||
<Button variant="outline" onClick={() => setStep('upload')}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Zurück
|
||||
</Button>
|
||||
<Button onClick={() => setStep('preview')}>
|
||||
Vorschau <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -199,26 +272,46 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
{/* Step 3: Preview + execute */}
|
||||
{step === 'preview' && (
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Vorschau ({rawData.length} Einträge)</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Vorschau ({rawData.length} Einträge)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-auto rounded-md border max-h-96">
|
||||
<div className="max-h-96 overflow-auto rounded-md border">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-2 text-left">#</th>
|
||||
{MEMBER_FIELDS.filter(f => mapping[f.key] !== undefined).map(f => (
|
||||
<th key={f.key} className="p-2 text-left">{f.label}</th>
|
||||
{MEMBER_FIELDS.filter(
|
||||
(f) => mapping[f.key] !== undefined,
|
||||
).map((f) => (
|
||||
<th key={f.key} className="p-2 text-left">
|
||||
{f.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rawData.slice(0, 20).map((_, i) => {
|
||||
const hasName = getMappedValue(i, 'firstName') && getMappedValue(i, 'lastName');
|
||||
const hasName =
|
||||
getMappedValue(i, 'firstName') &&
|
||||
getMappedValue(i, 'lastName');
|
||||
return (
|
||||
<tr key={i} className={`border-b ${hasName ? '' : 'bg-red-50 dark:bg-red-950'}`}>
|
||||
<td className="p-2">{i + 1} {!hasName && <AlertTriangle className="inline h-3 w-3 text-destructive" />}</td>
|
||||
{MEMBER_FIELDS.filter(f => mapping[f.key] !== undefined).map(f => (
|
||||
<td key={f.key} className="p-2 max-w-32 truncate">{getMappedValue(i, f.key) || '—'}</td>
|
||||
<tr
|
||||
key={i}
|
||||
className={`border-b ${hasName ? '' : 'bg-red-50 dark:bg-red-950'}`}
|
||||
>
|
||||
<td className="p-2">
|
||||
{i + 1}{' '}
|
||||
{!hasName && (
|
||||
<AlertTriangle className="text-destructive inline h-3 w-3" />
|
||||
)}
|
||||
</td>
|
||||
{MEMBER_FIELDS.filter(
|
||||
(f) => mapping[f.key] !== undefined,
|
||||
).map((f) => (
|
||||
<td key={f.key} className="max-w-32 truncate p-2">
|
||||
{getMappedValue(i, f.key) || '—'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
@@ -226,9 +319,16 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{rawData.length > 20 && <p className="mt-2 text-xs text-muted-foreground">... und {rawData.length - 20} weitere Einträge</p>}
|
||||
{rawData.length > 20 && (
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
... und {rawData.length - 20} weitere Einträge
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button variant="outline" onClick={() => setStep('mapping')}><ArrowLeft className="mr-2 h-4 w-4" />Zurück</Button>
|
||||
<Button variant="outline" onClick={() => setStep('mapping')}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Zurück
|
||||
</Button>
|
||||
<Button onClick={executeImport}>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
{rawData.length} Mitglieder importieren
|
||||
@@ -242,9 +342,13 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
{step === 'importing' && (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center p-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<p className="mt-4 text-lg font-semibold">Importiere Mitglieder...</p>
|
||||
<p className="text-sm text-muted-foreground">Bitte warten Sie, bis der Import abgeschlossen ist.</p>
|
||||
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent" />
|
||||
<p className="mt-4 text-lg font-semibold">
|
||||
Importiere Mitglieder...
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Bitte warten Sie, bis der Import abgeschlossen ist.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -256,20 +360,28 @@ export function MemberImportWizard({ accountId, account }: Props) {
|
||||
<CheckCircle className="mx-auto h-12 w-12 text-green-500" />
|
||||
<h3 className="mt-4 text-xl font-semibold">Import abgeschlossen</h3>
|
||||
<div className="mt-4 flex justify-center gap-4">
|
||||
<Badge variant="default">{importResults.success} erfolgreich</Badge>
|
||||
<Badge variant="default">
|
||||
{importResults.success} erfolgreich
|
||||
</Badge>
|
||||
{importResults.errors.length > 0 && (
|
||||
<Badge variant="destructive">{importResults.errors.length} Fehler</Badge>
|
||||
<Badge variant="destructive">
|
||||
{importResults.errors.length} Fehler
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{importResults.errors.length > 0 && (
|
||||
<div className="mt-4 max-h-40 overflow-auto rounded-md border p-3 text-left text-xs">
|
||||
{importResults.errors.map((err, i) => (
|
||||
<p key={i} className="text-destructive">{err}</p>
|
||||
<p key={i} className="text-destructive">
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<Button onClick={() => router.push(`/home/${account}/members-cms`)}>
|
||||
<Button
|
||||
onClick={() => router.push(`/home/${account}/members-cms`)}
|
||||
>
|
||||
Zur Mitgliederliste
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
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 { STATUS_LABELS, getMemberStatusColor } from '../lib/member-utils';
|
||||
|
||||
@@ -117,7 +121,7 @@ export function MembersDataTable({
|
||||
<select
|
||||
value={currentStatus}
|
||||
onChange={handleStatusChange}
|
||||
className="flex h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
|
||||
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
{STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
@@ -128,9 +132,7 @@ export function MembersDataTable({
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
router.push(`/home/${account}/members-cms/new`)
|
||||
}
|
||||
onClick={() => router.push(`/home/${account}/members-cms/new`)}
|
||||
>
|
||||
Neues Mitglied
|
||||
</Button>
|
||||
@@ -141,7 +143,7 @@ export function MembersDataTable({
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="px-4 py-3 text-left font-medium">Nr</th>
|
||||
<th className="px-4 py-3 text-left font-medium">Name</th>
|
||||
<th className="px-4 py-3 text-left font-medium">E-Mail</th>
|
||||
@@ -153,7 +155,10 @@ export function MembersDataTable({
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">
|
||||
<td
|
||||
colSpan={6}
|
||||
className="text-muted-foreground px-4 py-8 text-center"
|
||||
>
|
||||
Keine Mitglieder gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -165,7 +170,7 @@ export function MembersDataTable({
|
||||
<tr
|
||||
key={memberId}
|
||||
onClick={() => handleRowClick(memberId)}
|
||||
className="cursor-pointer border-b transition-colors hover:bg-muted/50"
|
||||
className="hover:bg-muted/50 cursor-pointer border-b transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3 font-mono text-xs">
|
||||
{String(member.member_number ?? '—')}
|
||||
@@ -174,21 +179,17 @@ export function MembersDataTable({
|
||||
{String(member.last_name ?? '')},{' '}
|
||||
{String(member.first_name ?? '')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground px-4 py-3">
|
||||
{String(member.email ?? '—')}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{String(member.city ?? '—')}
|
||||
</td>
|
||||
<td className="px-4 py-3">{String(member.city ?? '—')}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={getMemberStatusColor(status)}>
|
||||
{STATUS_LABELS[status] ?? status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{member.entry_date
|
||||
? new Date(String(member.entry_date)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
<td className="text-muted-foreground px-4 py-3">
|
||||
{formatDate(member.entry_date as string)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -200,7 +201,7 @@ export function MembersDataTable({
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{total} Mitglied{total !== 1 ? 'er' : ''} insgesamt
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user