# Form System

This document covers the generic form infrastructure used by all resource forms in the dashboard.

---

## Layer Overview

```
<CustomerForm>                     ← Feature-specific form component
  └─ useResourceForm(...)          ← State: RHF form + optional fetch for edit mode
  └─ useMutation(...)              ← Create or update mutation with toast
  └─ <Rhform form onSubmit>        ← FormProvider + <form> wrapper
       └─ <RhfTextField>           ← RHF-connected text input
       └─ <RhfSelectField>         ← RHF-connected static select
       └─ <RhfAsyncSelectField>    ← RHF-connected async combobox (fetches options)
       └─ <Button type="submit">
```

---

## `useResourceForm` — Form Initialization Hook

**File:** `shared/hooks/use-resource-form.ts`

Manages the react-hook-form instance with Zod validation and handles pre-filling the form when editing an existing item.

```ts
const { form, isEditing, isInitializing } = useResourceForm<TFormValues, TApiData>({
  schema,           // Zod schema → resolver
  defaultValues,    // Default form values for create mode
  resourceId,       // null → create, "5" → edit
  initialData,      // Optional: pre-fetched data (e.g. from table row)
  mapToFormValues,  // Maps API data shape → form values shape
  initialize,       // Optional: fetch fn called when resourceId is set (re-fetch from server)
  queryKey,         // Optional: React Query key for the initialize query
})
```

### Behavior

| Scenario | Result |
|---|---|
| `resourceId` is null | `isEditing = false`, form uses `defaultValues` |
| `resourceId` set, `initialData` provided, no `initialize` | Form pre-filled from `initialData` |
| `resourceId` set, `initialize` provided | `useQuery` calls `initialize(resourceId)`; form pre-filled from response |
| `resourceId` set, both provided | `useQuery` result takes precedence over `initialData` |

### `mapToFormValues`

Transforms the API response shape into the form's internal value shape. Field names, null handling, and relation objects are all resolved here:

```ts
function mapCustomerToFormValues(data: unknown): CustomerFormValues {
  const c = (data as any)?.data ?? data ?? {}
  return {
    first_name: c.first_name || "",
    customer_type: toRelation(c.customer_type_id, c.customer_type_name),
    // ...
  }
}
```

### `toRelation` / `toId` Helpers

**File:** `shared/lib/utils.ts`

Relation fields (foreign keys) are stored in the form as `{ value: string, label: string } | null` objects (combobox-compatible), not raw IDs.

```ts
// API data → form object
toRelation(id, name)  // → { value: String(id), label: name } or null

// Form object → API payload
toId(relation)        // → relation?.value ?? null
```

---

## `Rhform` — Form Provider Wrapper

**File:** `shared/components/form/rhform.tsx`

Wraps `react-hook-form`'s `FormProvider` and a `<form>` element. Avoids passing the `form` instance manually through every field.

```tsx
<Rhform form={form} onSubmit={handleSubmit}>
  {/* children have access to form context via useFormContext */}
</Rhform>
```

---

## `RhfField` — Generic RHF Controller Connector

**File:** `shared/components/form/rhf-field.tsx`

Low-level generic component that connects any field control to react-hook-form. Used internally by all `Rhf*` field wrappers. You rarely need to use this directly.

```tsx
<RhfField
  name="email"
  label="Email"
  required
  component={TextInputField}   // Any BaseFieldControlProps-compatible control
  placeholder="john@example.com"
  type="email"
/>
```

---

## `FieldShell` — Label + Error Layout

**File:** `shared/components/form/field-shell.tsx`

Renders the `FieldLabel`, `FieldDescription`, and `FieldError` around a control. Used by `RhfField` and `RhfAsyncSelectField` directly.

---

## Ready-Made `Rhf*` Field Wrappers

All wrappers follow the same pattern: they accept `name`, `label`, `description`, `required`, `disabled` plus any control-specific props.

### `RhfTextField`

```tsx
<RhfTextField name="first_name" label="First Name" placeholder="John" required />
<RhfTextField name="email" label="Email" type="email" />
<RhfTextField name="phone" label="Phone" type="tel" />
```

### `RhfTextareaField`

```tsx
<RhfTextareaField name="notes" label="Notes" rows={4} />
```

### `RhfCheckboxField`

```tsx
<RhfCheckboxField name="is_active" label="Active" />
```

### `RhfSelectField` — Static Options

```tsx
const options = [
  { value: "Mr.", label: "Mr." },
  { value: "Mrs.", label: "Mrs." },
]

<RhfSelectField name="salutation" label="Salutation" options={options} />
```

### `RhfAsyncSelectField` — Remote Options (Single)

Fetches options via React Query and renders a searchable combobox.

```tsx
<RhfAsyncSelectField
  name="customer_type"
  label="Customer Type"
  placeholder="Select customer type"
  queryKey={["customer-types"]}
  listFn={() => api.customers.listCustomerTypes()}
  mapOption={(item) => ({ value: String(item.id), label: item.name })}
  getOptionValue={(o) => o}       // store the full object, not just the string value
  getOptionLabel={(o) => o.label}
/>
```

**Data source options** (choose one):

| Prop | Description |
|---|---|
| `listFn` | Calls an API method; response is unwrapped automatically via `extractItems` |
| `loadOptions` | Returns `Promise<any[]>` directly (custom logic) |

The `mapOption` prop transforms raw API items to `{ value, label }` objects.

**`staleTime`** defaults to 5 minutes. Override for highly dynamic lookups.

### `RhfAsyncMultiSelectField` — Remote Options (Multi)

Same as `RhfAsyncSelectField` but stores an array of values:

```tsx
<RhfAsyncMultiSelectField
  name="tags"
  multiple
  label="Tags"
  queryKey={["tags"]}
  listFn={() => api.tags.list()}
  mapOption={(item) => ({ value: String(item.id), label: item.name })}
  getOptionValue={(o) => o}
  getOptionLabel={(o) => o.label}
/>
```

---

## Anatomy of a Feature Form

```tsx
// modules/my-feature/my-feature-form.tsx

const DEFAULT_VALUES: MyFormValues = { name: "", ... }

function mapToFormValues(data: unknown): MyFormValues { ... }
function mapFormToPayload(values: MyFormValues) { ... }

export function MyFeatureForm({ resourceId, initialData, onSuccess }: ResourceFormProps) {
  const api = useAuthApi()

  // 1. Form initialization
  const { form, isEditing } = useResourceForm<MyFormValues>({
    schema: myFormSchema,
    defaultValues: DEFAULT_VALUES,
    resourceId,
    initialData,
    mapToFormValues,
  })

  // 2. Mutation
  const { mutate, error, isPending } = useMutation({
    mutationFn: (values: MyFormValues) => {
      const payload = mapFormToPayload(values)
      const promise = isEditing
        ? api.myResources.update(resourceId!, payload)
        : api.myResources.create(payload)
      toast.promise(promise, { loading: "Saving...", success: "Saved!", error: "Failed." })
      return promise
    },
    onSuccess: () => { form.reset(); onSuccess?.() },
    onError: (err) => {
      if (err instanceof ApiError && err.validationErrors) {
        Object.entries(err.validationErrors).forEach(([field, msgs]) => {
          form.setError(field as any, { message: msgs[0] })
        })
      }
    },
  })

  // 3. Render
  return (
    <Rhform form={form} onSubmit={(v) => mutate(v)}>
      {error && <Alert variant="destructive">...</Alert>}
      <FieldGroup>
        <RhfTextField name="name" label="Name" required />
        <Button type="submit" disabled={isPending}>
          {isPending ? "Saving..." : isEditing ? "Update" : "Create"}
        </Button>
      </FieldGroup>
    </Rhform>
  )
}
```

---

## Zod Schema Conventions

**File:** `modules/<feature>/<feature>.schema.ts`

### Relation Fields

Relation fields (foreign-key selects) use a shared `relationFieldSchema`:

```ts
import { z } from "zod"

const relationFieldSchema = z
  .object({ value: z.string(), label: z.string() })
  .nullable()

// In the schema:
const mySchema = z.object({
  category: relationFieldSchema,  // → { value: "3", label: "Electronics" } | null
  name: z.string().min(1, "Name is required"),
})
```

### Email Validation Pattern

Use union to allow empty strings:

```ts
email: z.union([
  z.string().email("Enter a valid email address"),
  z.literal(""),
]).optional(),
```

---

## `extractItems` — Response Unwrapper

Used internally by `RhfAsyncSelectField` to normalize different API response shapes:

```ts
// Handles all of:
extractItems([{ id: 1, name: "A" }])                          // → same array
extractItems({ data: [{ id: 1, name: "A" }] })                // → data array
extractItems({ data: { data: [{ id: 1, name: "A" }] } })      // → nested data
```

This handles both plain arrays, standard Laravel list responses, and nested pagination wrappers.
