Add top-level members list, primary member on account, member move, combined create flows

- GET /v1/members with search across all members (includes account name)
- POST /members/:id/move with optional accountId (creates new account if omitted)
- primary_member_id on account table, auto-set when first member added
- isMinor flag on member create (manual override when no DOB provided)
- Account search now includes member names
- New account form includes primary contact fields, auto-generates name
- Members page in sidebar with global search
This commit is contained in:
Ryan Moon
2026-03-28 09:08:06 -05:00
parent 7c64a928e1
commit 572af05a3f
16 changed files with 796 additions and 77 deletions

View File

@@ -1,29 +1,33 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { AccountCreateSchema } from '@forte/shared/schemas'
import type { AccountCreateInput } from '@forte/shared/schemas'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import type { Account } from '@/types/account'
interface AccountFormProps {
defaultValues?: Partial<Account>
onSubmit: (data: AccountCreateInput) => void
onSubmit: (data: Record<string, unknown>) => void
loading?: boolean
includeFirstMember?: boolean
}
export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormProps) {
export function AccountForm({ defaultValues, onSubmit, loading, includeFirstMember }: AccountFormProps) {
const optionalNameSchema = AccountCreateSchema.extend({ name: AccountCreateSchema.shape.name.optional() })
const schema = includeFirstMember ? optionalNameSchema : AccountCreateSchema
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<AccountCreateInput>({
resolver: zodResolver(AccountCreateSchema),
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: defaultValues?.name ?? '',
email: defaultValues?.email ?? undefined,
@@ -36,12 +40,31 @@ export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormPro
const billingMode = watch('billingMode')
function handleFormSubmit(accountData: Record<string, unknown>) {
if (includeFirstMember) {
// Grab member fields from the native form
const form = document.getElementById('account-form') as HTMLFormElement
const formData = new FormData(form)
onSubmit({
...accountData,
memberFirstName: formData.get('memberFirstName') || undefined,
memberLastName: formData.get('memberLastName') || undefined,
memberEmail: formData.get('memberEmail') || undefined,
memberPhone: formData.get('memberPhone') || undefined,
memberDateOfBirth: formData.get('memberDateOfBirth') || undefined,
memberIsMinor: formData.get('memberIsMinor') === 'on' || undefined,
})
} else {
onSubmit(accountData)
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-lg">
<form id="account-form" onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4 max-w-lg">
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input id="name" {...register('name')} />
{errors.name && <p className="text-sm text-destructive">{errors.name.message}</p>}
<Label htmlFor="name">Account Name {includeFirstMember ? '' : '*'}</Label>
<Input id="name" {...register('name')} placeholder={includeFirstMember ? 'Auto-generated from member name if blank' : 'e.g. Smith Family, Lincoln Elementary'} />
{errors.name && !includeFirstMember && <p className="text-sm text-destructive">{String(errors.name.message)}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
@@ -88,6 +111,41 @@ export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormPro
<Textarea id="notes" {...register('notes')} />
</div>
{includeFirstMember && (
<>
<Separator />
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Primary Contact</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="memberFirstName">First Name</Label>
<Input id="memberFirstName" name="memberFirstName" />
</div>
<div className="space-y-2">
<Label htmlFor="memberLastName">Last Name</Label>
<Input id="memberLastName" name="memberLastName" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="memberEmail">Email</Label>
<Input id="memberEmail" name="memberEmail" type="email" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="memberDateOfBirth">Date of Birth</Label>
<Input id="memberDateOfBirth" name="memberDateOfBirth" type="date" />
</div>
<div className="space-y-2">
<Label htmlFor="memberPhone">Phone</Label>
<Input id="memberPhone" name="memberPhone" />
</div>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="memberIsMinor" name="memberIsMinor" className="rounded" />
<Label htmlFor="memberIsMinor" className="font-normal">This person is a minor</Label>
</div>
</>
)}
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : defaultValues ? 'Update Account' : 'Create Account'}
</Button>