shoc-frontend-new/src/app/(protected)/locations/_components/client-select.tsx
Alexandre Brandizzi a75b9c8be6 feat(sites): Add Site modal and site detail panel with in-place edit
- Sites page: Add Site modal with Site Code, Client, address, State/Area, optional Site Phone and Email, points of contact
- Duplicate Site Code shows inline under the field and keeps the modal open
- Row opens a non-modal detail panel; Street Address links to Google Maps; key-less map preview
- Edit switches the panel in place with Site Code read-only; Save needs a valid change, Cancel makes no call
- Site Phone is sent independently of the contacts; zip and title are carried through edits
- Retire the separate new/edit location pages
2026-09-25 11:43:30 -03:00

64 lines
2.4 KiB
TypeScript

import { useMemo, useState } from "react";
import { Controller, type Control, type FieldError } from "react-hook-form";
import { Autocomplete, TextField } from "@mui/material";
import type { AccountListItem } from "@/domain/accounts/types/account";
import { useAccountsList } from "@/domain/accounts/use-cases/use-accounts-list";
import type { LocationFormValues } from "@/domain/locations/schemas/location-schema";
import { useDebounce } from "@/hooks/use-debounce";
type ClientOption = Pick<AccountListItem, "id" | "name">;
interface ClientSelectProps {
control: Control<LocationFormValues>;
error?: FieldError;
/** Stored client, shown even when it is not in the first page of search results. */
initialClient?: ClientOption;
}
/** Searchable client (account) picker bound to `accountId`. */
export function ClientSelect({ control, error, initialClient }: ClientSelectProps) {
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
const { data, isLoading } = useAccountsList({ search: debouncedSearch, page: 1, pageSize: 50 });
const options = useMemo<ClientOption[]>(() => {
const items: ClientOption[] = data?.items ?? [];
const hasInitial =
initialClient !== undefined &&
items.some((item) => String(item.id) === String(initialClient.id));
return initialClient === undefined || hasInitial ? items : [initialClient, ...items];
}, [data?.items, initialClient]);
return (
<Controller
name="accountId"
control={control}
render={({ field }) => (
<Autocomplete<ClientOption>
options={options}
loading={isLoading}
getOptionLabel={(option) => option.name || String(option.id)}
isOptionEqualToValue={(option, value) => String(option.id) === String(value.id)}
value={options.find((option) => String(option.id) === field.value) ?? null}
onChange={(_event, option) => field.onChange(option ? String(option.id) : "")}
onBlur={field.onBlur}
onInputChange={(_event, value, reason) => {
if (reason === "input") {
setSearch(value);
}
}}
renderInput={(params) => (
<TextField
{...params}
label="Client"
required
placeholder="Search clients..."
error={Boolean(error)}
helperText={error?.message}
/>
)}
/>
)}
/>
);
}