Files
lunarfront-app/packages/admin/src/components/accounts/account-form.tsx
Ryan Moon 9400828f62 Rename Forte to LunarFront, generalize for any small business
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
2026-03-30 08:51:54 -05:00

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>
)
}