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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user