'use client'; import { useState } from 'react'; import Link from 'next/link'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2, MoreHorizontal, Plus, Star, XCircle } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { EmptyState } from '@/components/shared/empty-state'; import { PermissionGate } from '@/components/shared/permission-gate'; import { apiFetch } from '@/lib/api/client'; import { AddMembershipDialog } from './add-membership-dialog'; interface MembershipRow { id: string; companyId: string; clientId: string; role: string; roleDetail: string | null; startDate: string; endDate: string | null; isPrimary: boolean; notes: string | null; } interface CompanyMembersTabProps { companyId: string; portSlug: string; } const ROLE_LABELS: Record = { director: 'Director', officer: 'Officer', broker: 'Broker', representative: 'Representative', legal_counsel: 'Legal counsel', employee: 'Employee', shareholder: 'Shareholder', other: 'Other', }; function formatDate(value: string | null): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleDateString(); } /** * Renders a client's name as a link by fetching the client record. * Memoization is handled via the TanStack Query cache, so repeat renders * for the same clientId are free. */ function ClientName({ clientId, portSlug }: { clientId: string; portSlug: string }) { const { data } = useQuery<{ fullName: string | null }>({ queryKey: ['clients', clientId, 'name-only'], queryFn: () => apiFetch<{ data: { fullName: string | null } }>(`/api/v1/clients/${clientId}`).then( (r) => r.data, ), }); return ( {data?.fullName ?? `Client ${clientId.slice(0, 8)}`} ); } export function CompanyMembersTab({ companyId, portSlug }: CompanyMembersTabProps) { const queryClient = useQueryClient(); const [activeOnly, setActiveOnly] = useState(true); const [addOpen, setAddOpen] = useState(false); const membersKey = ['companies', companyId, 'members', { activeOnly }]; const { data, isLoading } = useQuery({ queryKey: membersKey, queryFn: () => apiFetch<{ data: MembershipRow[] }>( `/api/v1/companies/${companyId}/members?activeOnly=${activeOnly ? 'true' : 'false'}`, ).then((r) => r.data), }); const endMutation = useMutation({ mutationFn: (membershipId: string) => apiFetch(`/api/v1/companies/${companyId}/members/${membershipId}`, { method: 'DELETE', }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['companies', companyId, 'members'] }); toast.success('Membership ended'); }, onError: (err: Error) => { toast.error(err.message || 'Failed to end membership'); }, }); const setPrimaryMutation = useMutation({ mutationFn: (membershipId: string) => apiFetch(`/api/v1/companies/${companyId}/members/${membershipId}/set-primary`, { method: 'POST', }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['companies', companyId, 'members'] }); toast.success('Primary contact updated'); }, onError: (err: Error) => { toast.error(err.message || 'Failed to set primary'); }, }); const members = data ?? []; return (
{isLoading ? (
) : members.length === 0 ? ( ) : (
Client Role Role Detail Start Date End Date Primary {members.map((m) => { const isActive = !m.endDate; return ( {ROLE_LABELS[m.role] ?? m.role} {m.roleDetail ?? '-'} {formatDate(m.startDate)} {m.endDate ? ( formatDate(m.endDate) ) : ( - )} {m.isPrimary ? ( Primary ) : ( - )} {isActive && !m.isPrimary && ( setPrimaryMutation.mutate(m.id)} disabled={setPrimaryMutation.isPending} > Set Primary )} {isActive && ( endMutation.mutate(m.id)} disabled={endMutation.isPending} > End Membership )} ); })}
)}
); }