diff --git a/src/components/companies/add-membership-dialog.tsx b/src/components/companies/add-membership-dialog.tsx new file mode 100644 index 0000000..8795729 --- /dev/null +++ b/src/components/companies/add-membership-dialog.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Loader2 } from 'lucide-react'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { ClientPicker } from '@/components/shared/client-picker'; +import { apiFetch } from '@/lib/api/client'; +import { ROLES } from '@/lib/validators/company-memberships'; + +type RoleEnum = (typeof ROLES)[number]; + +type FormValues = { + clientId: string | null; + role: RoleEnum; + roleDetail?: string; + startDate: string; // YYYY-MM-DD + isPrimary: boolean; + notes?: string; +}; + +interface AddMembershipDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + companyId: string; +} + +const todayIso = (): string => new Date().toISOString().slice(0, 10); + +const ROLE_LABEL: Record = { + director: 'Director', + officer: 'Officer', + broker: 'Broker', + representative: 'Representative', + legal_counsel: 'Legal counsel', + employee: 'Employee', + shareholder: 'Shareholder', + other: 'Other', +}; + +export function AddMembershipDialog({ open, onOpenChange, companyId }: AddMembershipDialogProps) { + const queryClient = useQueryClient(); + const [formError, setFormError] = useState(null); + + const { + register, + handleSubmit, + watch, + setValue, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + clientId: null, + role: 'director', + roleDetail: '', + startDate: todayIso(), + isPrimary: false, + notes: '', + }, + }); + + useEffect(() => { + if (open) { + setFormError(null); + reset({ + clientId: null, + role: 'director', + roleDetail: '', + startDate: todayIso(), + isPrimary: false, + notes: '', + }); + } + }, [open, reset]); + + const clientId = watch('clientId'); + const role = watch('role'); + const isPrimary = watch('isPrimary'); + + const mutation = useMutation({ + mutationFn: async (data: FormValues) => { + if (!data.clientId) { + throw new Error('Please select a client'); + } + await apiFetch(`/api/v1/companies/${companyId}/members`, { + method: 'POST', + body: { + clientId: data.clientId, + role: data.role, + roleDetail: data.roleDetail?.trim() || undefined, + startDate: data.startDate, + isPrimary: data.isPrimary, + notes: data.notes?.trim() || undefined, + }, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['companies', companyId, 'members'] }); + onOpenChange(false); + }, + onError: (err: unknown) => { + let msg = err instanceof Error ? err.message : 'Failed to add membership'; + // Detect 409 — service returns a "membership already exists" message + if (/already exists/i.test(msg)) { + msg = 'This membership already exists (same client + role + start date).'; + } + setFormError(msg); + }, + }); + + return ( + + + + Add member + + Associate a client with this company in a specific role. + + + +
{ + setFormError(null); + mutation.mutate(data); + })} + className="space-y-4" + > +
+ + setValue('clientId', id)} /> + {!clientId && errors.clientId &&

Required

} +
+ +
+ + +
+ +
+ + +
+ +
+ + + {errors.startDate &&

Required

} +
+ +
+ setValue('isPrimary', v === true)} + /> + +
+ +
+ +