Rebrand from Forte (music-store-specific) to LunarFront (any small business): - Package namespace @forte/* → @lunarfront/* - Database forte/forte_test → lunarfront/lunarfront_test - Docker containers, volumes, connection strings - UI branding, localStorage keys, test emails - All documentation and planning docs Generalize music-specific terminology: - instrumentDescription → itemDescription - instrumentCount → itemCount - instrumentType → itemCategory (on service templates) - New migration 0027_generalize_terminology for column renames - Seed data updated with generic examples - RBAC descriptions updated
156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { z } from 'zod'
|
|
import { AccountCreateSchema } from '@lunarfront/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: Record<string, unknown>) => void
|
|
loading?: boolean
|
|
includeFirstMember?: boolean
|
|
}
|
|
|
|
export function AccountForm({ defaultValues, onSubmit, loading, includeFirstMember }: AccountFormProps) {
|
|
const optionalNameSchema = AccountCreateSchema.extend({ name: z.string().max(255).optional() })
|
|
const schema = includeFirstMember ? optionalNameSchema : AccountCreateSchema
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
formState: { errors },
|
|
} = useForm({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: {
|
|
name: defaultValues?.name ?? (includeFirstMember ? undefined : ''),
|
|
email: defaultValues?.email ?? undefined,
|
|
phone: defaultValues?.phone ?? undefined,
|
|
billingMode: defaultValues?.billingMode ?? 'consolidated',
|
|
notes: defaultValues?.notes ?? undefined,
|
|
address: defaultValues?.address ?? undefined,
|
|
},
|
|
})
|
|
|
|
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 id="account-form" onSubmit={handleSubmit(handleFormSubmit)} noValidate className="space-y-4 max-w-lg">
|
|
<div className="space-y-2">
|
|
<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">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input id="email" type="email" {...register('email')} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="phone">Phone</Label>
|
|
<Input id="phone" {...register('phone')} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Billing Mode</Label>
|
|
<Select
|
|
value={billingMode}
|
|
onValueChange={(v) => setValue('billingMode', v as 'consolidated' | 'split')}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="consolidated">Consolidated</SelectItem>
|
|
<SelectItem value="split">Split</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Address</Label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Input placeholder="Street" {...register('address.street')} className="col-span-2" />
|
|
<Input placeholder="City" {...register('address.city')} />
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<Input placeholder="State" {...register('address.state')} />
|
|
<Input placeholder="ZIP" {...register('address.zip')} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="notes">Notes</Label>
|
|
<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>
|
|
</form>
|
|
)
|
|
}
|