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:
90
src/admin/ui/CustomerPicker.tsx
Normal file
90
src/admin/ui/CustomerPicker.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user