refactor(ui): shared searchable CustomerPicker across offers/invoices/issued-orders

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 16:42:24 +02:00
parent 20177e8f87
commit e868ecf769
5 changed files with 132 additions and 286 deletions

View File

@@ -0,0 +1,90 @@
import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete";
import MuiTextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import type { Customer } from "../lib/queries/offers";
interface CustomerPickerProps {
customers: Customer[];
/** Selected customer id (FK), or null when none chosen. */
value: number | null;
onChange: (id: number | null) => void;
disabled?: boolean;
/**
* When set, draws the input in its error state (red border). The error TEXT
* is owned by the surrounding <Field> wrapper, so we deliberately do NOT
* render helperText here to avoid duplicating the message.
*/
error?: string;
placeholder?: string;
/** Optional id forwarded by <Field> for label association. */
id?: string;
}
// Search matches the customer name AND the IČO (company_id), so typing either
// finds the record. Match on a stringified "name + company_id" so MUI's default
// case-insensitive substring filtering covers both.
const filterCustomers = createFilterOptions<Customer>({
stringify: (c) => `${c.name} ${c.company_id ?? ""}`,
});
/**
* Shared searchable customer picker — a controlled MUI Autocomplete over the
* customers list, bound to the customer id. Used by Offers, Invoices and Issued
* Orders so all three FK customer selectors behave identically. Renders bare
* (no label/error text); wrap it in the page's <Field label error> for the
* label + error line.
*/
export default function CustomerPicker({
customers,
value,
onChange,
disabled,
error,
placeholder,
id,
}: CustomerPickerProps) {
const selected = customers.find((c) => c.id === value) ?? null;
return (
<Autocomplete<Customer>
options={customers}
value={selected}
onChange={(_, opt) => onChange(opt ? opt.id : null)}
getOptionLabel={(c) => c?.name ?? ""}
isOptionEqualToValue={(o, v) => o.id === v.id}
filterOptions={filterCustomers}
disabled={disabled}
size="small"
fullWidth
autoHighlight
renderOption={(props, c) => {
const { key, ...rest } = props as typeof props & { key: string };
return (
<Box component="li" key={key} {...rest}>
<Box>
{c.name}
{(c.company_id || c.city) && (
<Typography
variant="caption"
sx={{ display: "block", color: "text.secondary" }}
>
{c.company_id && `IČ: ${c.company_id}`}
{c.company_id && c.city && " · "}
{c.city}
</Typography>
)}
</Box>
</Box>
);
}}
renderInput={(params) => (
<MuiTextField
{...params}
id={id}
placeholder={placeholder ?? "Vyberte zákazníka…"}
error={!!error}
/>
)}
/>
);
}

View File

@@ -9,6 +9,7 @@ export type { DataColumn } from "./DataTable";
export { Field, SwitchField } from "./Field";
export { default as Select } from "./Select";
export type { SelectOption } from "./Select";
export { default as CustomerPicker } from "./CustomerPicker";
export { default as StatusChip } from "./StatusChip";
export { CheckboxField } from "./Checkbox";
export { default as Alert } from "./Alert";