shoc-frontend-new/src/app/(protected)/locations/_components/site-form-fields.tsx
Alexandre Brandizzi 3f3a3701c3 feat(sites): suggest addresses as Street Address is typed
SH-331 requires Street Address suggestions after 3 typed characters,
with a pick filling Street, City and State and unpicked typing kept as
free text. SH-272, which SH-331 consolidates, specifies those
suggestions as mocked, and the shared AddressAutocompleteField that
Vendors already uses (SH-271) was built for Sites to reuse. No Places
key is involved.

The Add Site modal and the detail panel's edit mode now render that
field. A pick writes all three values through their own controllers,
so the form marks them dirty and Save changes enables in edit mode.
The field gains an opt-in requiredMarker prop so Sites keeps its label
asterisk while Vendors stays without one.
2026-09-25 13:10:03 -03:00

111 lines
4.1 KiB
TypeScript

import { Controller, useController, useWatch, type UseFormReturn } from "react-hook-form";
import { Stack, TextField } from "@mui/material";
import { AddressAutocompleteField } from "@/components/common/address-autocomplete-field";
import { normalizeStateCode } from "@/data/areas";
import type { AccountListItem } from "@/domain/accounts/types/account";
import type { LocationFormValues } from "@/domain/locations/schemas/location-schema";
import { ClientSelect } from "./client-select";
import { LocationContactsFieldArray } from "./location-contacts-field-array";
import { SiteMapPreview } from "./site-map-preview";
import { StateSelect } from "./state-select";
interface SiteFormFieldsProps {
form: UseFormReturn<LocationFormValues>;
/** Edit mode: Site Code is immutable after creation. */
siteCodeReadOnly?: boolean;
initialClient?: Pick<AccountListItem, "id" | "name">;
}
/** Shared Add Site / edit-mode fields, in the prototype order. */
export function SiteFormFields({
form,
siteCodeReadOnly = false,
initialClient,
}: SiteFormFieldsProps) {
const {
control,
register,
formState: { errors },
} = form;
const [address, city, state] = useWatch({ control, name: ["address", "city", "state"] });
const { field: addressField } = useController({ control, name: "address" });
const { field: cityField } = useController({ control, name: "city" });
const { field: stateField } = useController({ control, name: "state" });
return (
<Stack spacing={2}>
<Controller
name="name"
control={control}
render={({ field }) => (
<TextField
{...field}
onChange={(event) => field.onChange(event.target.value.toUpperCase())}
label="Site Code"
placeholder="e.g. AB12"
required={!siteCodeReadOnly}
disabled={siteCodeReadOnly}
error={Boolean(errors.name)}
helperText={
errors.name?.message ?? (siteCodeReadOnly ? "Site Code can't be changed" : undefined)
}
slotProps={{ htmlInput: { maxLength: 50, readOnly: siteCodeReadOnly } }}
fullWidth
/>
)}
/>
<ClientSelect control={control} error={errors.accountId} initialClient={initialClient} />
<AddressAutocompleteField
value={addressField.value}
onInputChange={addressField.onChange}
onSelect={(parts) => {
// One pick fills all three, each through its own controller so the
// form marks them dirty and revalidates (edit mode's Save depends on it).
addressField.onChange(parts.street);
cityField.onChange(parts.city);
stateField.onChange(normalizeStateCode(parts.state) ?? "");
}}
label="Street Address"
required
requiredMarker
error={Boolean(errors.address)}
helperText={
errors.address?.message ?? "Pick a suggestion to auto-fill City/State, or type your own."
}
/>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<TextField
{...cityField}
label="City"
required
error={Boolean(errors.city)}
helperText={errors.city?.message}
fullWidth
/>
<StateSelect control={control} error={errors.state} />
</Stack>
<SiteMapPreview address={address} city={city} state={state} />
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<TextField
{...register("sitePhone")}
label="Site Phone (optional)"
placeholder="(000) 000-0000"
error={Boolean(errors.sitePhone)}
helperText={errors.sitePhone?.message}
fullWidth
slotProps={{ htmlInput: { maxLength: 20 } }}
/>
<TextField
{...register("contactEmail")}
label="Site Email (optional)"
placeholder="site@company.com"
type="email"
error={Boolean(errors.contactEmail)}
helperText={errors.contactEmail?.message}
fullWidth
/>
</Stack>
<LocationContactsFieldArray control={control} errors={errors} register={register} />
</Stack>
);
}