'use client'; import { useCallback, useState } from 'react'; import { useRouter } from 'next/navigation'; 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 { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { toast } from '@kit/ui/sonner'; import { useActionWithToast } from '@kit/ui/use-action-with-toast'; import { STATUS_LABELS, getMemberStatusColor, formatAddress, formatIban, computeAge, computeMembershipYears, } from '../lib/member-utils'; import { deleteMember, updateMember, createMemberRole, deleteMemberRole, createMemberHonor, deleteMemberHonor, createMandate, revokeMandate, } from '../server/actions/member-actions'; interface MemberRole { id: string; role_name: string; from_date: string | null; until_date: string | null; is_active: boolean; } interface MemberHonor { id: string; honor_name: string; honor_date: string | null; description: string | null; } interface SepaMandate { id: string; mandate_reference: string; iban: string; bic: string | null; account_holder: string; mandate_date: string; status: string; is_primary: boolean; sequence: string; } interface MemberDetailViewProps { member: Record; account: string; accountId: string; roles?: MemberRole[]; honors?: MemberHonor[]; mandates?: SepaMandate[]; } function DetailRow({ label, value, }: { label: string; value: React.ReactNode; }) { return (
{label} {value ?? '—'}
); } export function MemberDetailView({ member, account, accountId, roles = [], honors = [], mandates = [], }: MemberDetailViewProps) { const router = useRouter(); const memberId = String(member.id ?? ''); const status = String(member.status ?? 'active'); const firstName = String(member.first_name ?? ''); const lastName = String(member.last_name ?? ''); const fullName = `${firstName} ${lastName}`.trim(); 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'); }, }, ); 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'); }, }, ); const handleDelete = useCallback(() => { if ( !window.confirm( `Möchten Sie ${fullName} wirklich kündigen? Diese Aktion kann nicht rückgängig gemacht werden.`, ) ) { return; } executeDelete({ memberId, accountId }); }, [executeDelete, memberId, accountId, fullName]); const handleArchive = useCallback(() => { if (!window.confirm(`Möchten Sie ${fullName} wirklich archivieren?`)) { return; } executeUpdate({ memberId, accountId, isArchived: true, }); }, [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 address = formatAddress(member); const iban = formatIban(member.iban as string | null | undefined); return (
{/* Header */}

{fullName}

{STATUS_LABELS[status] ?? status}

Mitgliedsnr. {String(member.member_number ?? '—')}

{/* Detail Cards */}
{/* Persönliche Daten */} Persönliche Daten {/* Kontakt */} Kontakt {/* Adresse */} Adresse {/* Mitgliedschaft */} Mitgliedschaft {STATUS_LABELS[status] ?? status} } /> 0 ? `${membershipYears} Jahre` : '—'} />
{/* Roles Section */} {/* Honors Section */} {/* Mandates Section */} {/* Back */}
); } /* ─── Roles Section ─── */ function RolesSection({ roles, memberId, accountId, }: { roles: MemberRole[]; memberId: string; accountId: string; }) { const router = useRouter(); const [showForm, setShowForm] = useState(false); const [roleName, setRoleName] = useState(''); const [fromDate, setFromDate] = useState(''); const [untilDate, setUntilDate] = useState(''); const { execute: executeCreate, isPending: isCreating } = useActionWithToast( createMemberRole, { successMessage: 'Funktion erstellt', onSuccess: () => { setShowForm(false); setRoleName(''); setFromDate(''); setUntilDate(''); router.refresh(); }, }, ); const { execute: executeDeleteRole, isPending: isDeletingRole } = useActionWithToast(deleteMemberRole, { successMessage: 'Funktion gelöscht', onSuccess: () => router.refresh(), }); const handleCreate = useCallback(() => { if (!roleName.trim()) return; executeCreate({ memberId, accountId, roleName: roleName.trim(), fromDate: fromDate || undefined, untilDate: untilDate || undefined, }); }, [executeCreate, memberId, accountId, roleName, fromDate, untilDate]); const handleDeleteRole = useCallback( (roleId: string) => { if (!window.confirm('Funktion wirklich löschen?')) return; executeDeleteRole({ roleId }); }, [executeDeleteRole], ); return (
Funktionen ({roles.length})
{showForm && (
setRoleName(e.target.value)} placeholder="z.B. Kassier" data-test="role-name-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setFromDate(e.target.value)} data-test="role-from-date" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setUntilDate(e.target.value)} data-test="role-until-date" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
)} {roles.length === 0 && !showForm ? (

Keine Funktionen vorhanden.

) : ( roles.length > 0 && (
{roles.map((role) => ( ))}
Bezeichnung Von Bis Aktionen
{role.role_name} {role.from_date ? formatDate(role.from_date) : '—'} {role.until_date ? formatDate(role.until_date) : '—'}
) )}
); } /* ─── Honors Section ─── */ function HonorsSection({ honors, memberId, accountId, }: { honors: MemberHonor[]; memberId: string; accountId: string; }) { const router = useRouter(); const [showForm, setShowForm] = useState(false); const [honorName, setHonorName] = useState(''); const [honorDate, setHonorDate] = useState(''); const [description, setDescription] = useState(''); const { execute: executeCreate, isPending: isCreating } = useActionWithToast( createMemberHonor, { successMessage: 'Ehrung erstellt', onSuccess: () => { setShowForm(false); setHonorName(''); setHonorDate(''); setDescription(''); router.refresh(); }, }, ); const { execute: executeDeleteHonor, isPending: isDeletingHonor } = useActionWithToast(deleteMemberHonor, { successMessage: 'Ehrung gelöscht', onSuccess: () => router.refresh(), }); const handleCreate = useCallback(() => { if (!honorName.trim()) return; executeCreate({ memberId, accountId, honorName: honorName.trim(), honorDate: honorDate || undefined, description: description || undefined, }); }, [executeCreate, memberId, accountId, honorName, honorDate, description]); const handleDeleteHonor = useCallback( (honorId: string) => { if (!window.confirm('Ehrung wirklich löschen?')) return; executeDeleteHonor({ honorId }); }, [executeDeleteHonor], ); return (
Ehrungen ({honors.length})
{showForm && (
setHonorName(e.target.value)} placeholder="z.B. Ehrennadel in Gold" data-test="honor-name-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setHonorDate(e.target.value)} data-test="honor-date-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setDescription(e.target.value)} placeholder="Optional" data-test="honor-description-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
)} {honors.length === 0 && !showForm ? (

Keine Ehrungen vorhanden.

) : ( honors.length > 0 && (
{honors.map((honor) => ( ))}
Bezeichnung Datum Beschreibung Aktionen
{honor.honor_name} {honor.honor_date ? formatDate(honor.honor_date) : '—'} {honor.description ?? '—'}
) )}
); } /* ─── Mandates Section ─── */ function MandatesSection({ mandates, memberId, accountId, }: { mandates: SepaMandate[]; memberId: string; accountId: string; }) { const router = useRouter(); const [showForm, setShowForm] = useState(false); const [mandateRef, setMandateRef] = useState(''); const [mandateIban, setMandateIban] = useState(''); const [mandateBic, setMandateBic] = useState(''); const [mandateHolder, setMandateHolder] = useState(''); const [mandateDate, setMandateDate] = useState(''); const [mandateSequence, setMandateSequence] = useState< 'FRST' | 'RCUR' | 'FNAL' | 'OOFF' >('RCUR'); const MANDATE_STATUS_LABELS: Record = { active: 'Aktiv', revoked: 'Widerrufen', expired: 'Abgelaufen', }; const MANDATE_STATUS_COLORS: Record< string, 'default' | 'secondary' | 'destructive' | 'outline' > = { active: 'default', revoked: 'destructive', expired: 'outline', }; const { execute: executeCreate, isPending: isCreating } = useActionWithToast( createMandate, { successMessage: 'Mandat erstellt', onSuccess: () => { setShowForm(false); setMandateRef(''); setMandateIban(''); setMandateBic(''); setMandateHolder(''); setMandateDate(''); setMandateSequence('RCUR'); router.refresh(); }, }, ); const { execute: executeRevoke, isPending: isRevoking } = useActionWithToast( revokeMandate, { successMessage: 'Mandat widerrufen', onSuccess: () => router.refresh(), }, ); const handleCreate = useCallback(() => { if ( !mandateRef.trim() || !mandateIban.trim() || !mandateHolder.trim() || !mandateDate ) return; executeCreate({ memberId, accountId, mandateReference: mandateRef.trim(), iban: mandateIban.trim(), bic: mandateBic.trim() || undefined, accountHolder: mandateHolder.trim(), mandateDate, sequence: mandateSequence, }); }, [ executeCreate, memberId, accountId, mandateRef, mandateIban, mandateBic, mandateHolder, mandateDate, mandateSequence, ]); const handleRevoke = useCallback( (mandateId: string) => { if (!window.confirm('Mandat wirklich widerrufen?')) return; executeRevoke({ mandateId }); }, [executeRevoke], ); return (
SEPA-Mandate ({mandates.length})
{showForm && (
setMandateRef(e.target.value)} placeholder="z.B. MAND-001" data-test="mandate-ref-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setMandateIban(e.target.value)} placeholder="DE..." data-test="mandate-iban-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setMandateBic(e.target.value)} placeholder="Optional" data-test="mandate-bic-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setMandateHolder(e.target.value)} placeholder="Name des Kontoinhabers" data-test="mandate-holder-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
setMandateDate(e.target.value)} data-test="mandate-date-input" className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm" />
)} {mandates.length === 0 && !showForm ? (

Keine SEPA-Mandate vorhanden.

) : ( mandates.length > 0 && (
{mandates.map((mandate) => ( ))}
Referenz IBAN Kontoinhaber Datum Status Aktionen
{mandate.mandate_reference} {mandate.is_primary && ( Primär )} {formatIban(mandate.iban)} {mandate.account_holder} {formatDate(mandate.mandate_date)} {MANDATE_STATUS_LABELS[mandate.status] ?? mandate.status} {mandate.status === 'active' && ( )}
) )}
); }