'use client'; import { useCallback, useState } from 'react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { ArrowRightLeft, Search, Users } from 'lucide-react'; 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@kit/ui/dialog'; import { Input } from '@kit/ui/input'; import { Textarea } from '@kit/ui/textarea'; import { useActionWithToast } from '@kit/ui/use-action-with-toast'; import { getTransferPreview, transferMember, } from '../server/actions/hierarchy-actions'; import type { CrossOrgMember, TransferPreview } from '../server/api'; const STATUS_LABELS: Record = { active: 'Aktiv', inactive: 'Inaktiv', pending: 'Ausstehend', resigned: 'Ausgetreten', excluded: 'Ausgeschlossen', deceased: 'Verstorben', }; const STATUS_COLORS: Record = { active: 'default', inactive: 'secondary', pending: 'outline', resigned: 'secondary', excluded: 'destructive', deceased: 'secondary', }; interface CrossOrgMemberSearchProps { account: string; members: CrossOrgMember[]; total: number; page: number; pageSize: number; childAccounts: Array<{ id: string; name: string }>; } export function CrossOrgMemberSearch({ account, members, total, page, pageSize, childAccounts, }: CrossOrgMemberSearchProps) { const router = useRouter(); const searchParams = useSearchParams(); const [transferTarget, setTransferTarget] = useState( null, ); const [targetAccountId, setTargetAccountId] = useState(''); const [transferReason, setTransferReason] = useState(''); const [keepSepa, setKeepSepa] = useState(true); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const currentSearch = searchParams.get('q') ?? ''; const currentStatus = searchParams.get('status') ?? ''; const currentAccount = searchParams.get('accountId') ?? ''; const totalPages = Math.ceil(total / pageSize); const form = useForm({ defaultValues: { search: currentSearch }, }); const { execute: executePreview } = useAction(getTransferPreview, { onSuccess: ({ data }) => { if (data?.data) setPreview(data.data); setPreviewLoading(false); }, onError: () => { setPreviewLoading(false); }, }); const { execute: executeTransfer, isPending: isTransferring } = useActionWithToast(transferMember, { successMessage: 'Mitglied erfolgreich transferiert', errorMessage: 'Fehler beim Transfer', onSuccess: () => { setTransferTarget(null); setTargetAccountId(''); setTransferReason(''); setKeepSepa(true); setPreview(null); }, }); const buildUrl = useCallback( (params: Record) => { const urlSearchParams = new URLSearchParams(searchParams.toString()); for (const [key, val] of Object.entries(params)) { if (val === null || val === '' || val === undefined) { urlSearchParams.delete(key); } else { urlSearchParams.set(key, String(val)); } } return `/home/${account}/verband/members?${urlSearchParams.toString()}`; }, [account, searchParams], ); const handleSearch = useCallback( (values: { search: string }) => { router.push(buildUrl({ q: values.search || null, page: null })); }, [router, buildUrl], ); const handleStatusChange = useCallback( (e: React.ChangeEvent) => { router.push(buildUrl({ status: e.target.value || null, page: null })); }, [router, buildUrl], ); const handleAccountChange = useCallback( (e: React.ChangeEvent) => { router.push(buildUrl({ accountId: e.target.value || null, page: null })); }, [router, buildUrl], ); const handlePageChange = useCallback( (newPage: number) => { router.push(buildUrl({ page: newPage > 1 ? newPage : null })); }, [router, buildUrl], ); function handleTransferConfirm() { if (!transferTarget || !targetAccountId) return; executeTransfer({ memberId: transferTarget.id, targetAccountId, reason: transferReason || undefined, keepSepa, }); } // Accounts available as transfer targets (exclude member's current account) const transferTargetAccounts = transferTarget ? childAccounts.filter((a) => a.id !== transferTarget.account_id) : []; return (
{/* Search + Filters */}
{childAccounts.length > 0 && ( )}
{/* Results */} Mitglieder ({total}) {members.length === 0 ? (

Keine Mitglieder gefunden

{currentSearch ? 'Versuchen Sie einen anderen Suchbegriff.' : 'In den verknüpften Organisationen sind noch keine Mitglieder vorhanden.'}

) : (
{members.map((member) => ( ))}
Name Organisation E-Mail Ort Status Eintritt Aktion
{member.last_name}, {member.first_name} {member.member_number && ( #{member.member_number} )} {member.account_slug ? ( {member.account_name} ) : ( {member.account_name} )} {member.email ?? '—'} {member.city ?? '—'} {STATUS_LABELS[member.status] ?? member.status} {formatDate(member.entry_date)}
)} {/* Pagination */} {totalPages > 1 && (

Seite {page} von {totalPages} ({total} Einträge)

)}
{/* Transfer Dialog */} { if (!open) setTransferTarget(null); }} > Mitglied transferieren {transferTarget && ( <> {transferTarget.first_name} {transferTarget.last_name} {' '} wird von {transferTarget.account_name} in eine andere Organisation verschoben. )}
{/* Preview: side effects */} {previewLoading && (
Lade Transfervorschau...
)} {preview && !previewLoading && (
{/* Active courses */} {preview.courses.length > 0 && (

{preview.courses.length} aktive Kurseinschreibung(en)

    {preview.courses.map((course) => (
  • {course.name} ({course.accountName}) bleibt erhalten
  • ))}
)} {/* Open invoices */} {preview.invoices.length > 0 && (

{preview.invoices.length} offene Rechnung(en)

    {preview.invoices.map((inv) => (
  • {inv.invoiceNumber} — {inv.amount.toFixed(2)} EUR ( {inv.accountName}) verbleibt beim Quellverein
  • ))}
)} {/* SEPA mandates */} {preview.mandates.length > 0 && (

{preview.mandates.length} aktive(s) SEPA-Mandat(e)

    {preview.mandates.map((mandate) => (
  • {mandate.reference} (Status: {mandate.status}) wird zurückgesetzt
  • ))}
)} {/* Newsletters */} {preview.newsletters.length > 0 && (

{preview.newsletters.length} Newsletter-Abonnement(s)

    {preview.newsletters.map((newsletter) => (
  • {newsletter.name} ({newsletter.accountName}) bleibt erhalten
  • ))}
)} {/* What gets cleared */}

Wird zurückgesetzt:

    {preview.member.memberNumber && (
  • Mitgliedsnr.{' '} #{preview.member.memberNumber} — Neuvergabe im Zielverein nötig
  • )} {preview.member.hasDuesCategory && (
  • Beitragskategorie — muss im Zielverein neu zugewiesen werden
  • )}
  • SEPA-Mandatstatus → "ausstehend" (Neubestätigung nötig)
{/* No side effects */} {preview.courses.length === 0 && preview.invoices.length === 0 && preview.mandates.length === 0 && preview.newsletters.length === 0 && (

Keine aktiven Verknüpfungen gefunden

Transfer kann ohne Seiteneffekte durchgeführt werden.

)}
)}

Bankverbindung wird übernommen, Mandat muss im Zielverein neu bestätigt werden.