Issued orders are purchase orders WE send - they must pick from suppliers (sklad_suppliers), not from customers. Per user decision customer_id was REPLACED (not kept alongside): migration drops issued_orders.customer_id and adds supplier_id FK -> sklad_suppliers (existing rows lose their counterparty - the feature is days old; re-point them in the UI). - service: input/filters/search (suppliers.name + ico)/enrichment/detail all supplier-based; create validates the supplier inside the transaction and update before write -> Czech 400 'Dodavatel nenalezen' instead of P2003 500; detail returns a minimal supplier field set (no internal notes leak) - routes: supplier_id on list + stats; new GET /issued-orders/suppliers lookup (orders.view/create/edit guard - orders users lack warehouse.manage which guards the warehouse suppliers CRUD), active suppliers only, name+id ordering - PDF: Dodavatel block now renders the supplier (name, newline-split address, IC/DIC), layout and both language label sets unchanged - frontend: new SupplierPicker kit component (CustomerPicker untouched), IssuedOrderDetail/IssuedOrders switched to supplier_id/supplier_name; the picker keeps a fallback option for orders whose supplier was later deactivated; WarehouseSuppliers CRUD now also invalidates issued-orders so the picker can't go stale - tests: issued-orders suite switched to supplier fixtures + new coverage (lookup shape + 403, nonexistent supplier 400, PDF supplier block) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
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 { Supplier } from "../lib/queries/issued-orders";
|
|
|
|
interface SupplierPickerProps {
|
|
suppliers: Supplier[];
|
|
/** Selected supplier 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 supplier name AND the IČO, so typing either finds the
|
|
// record. Match on a stringified "name + ico" so MUI's default
|
|
// case-insensitive substring filtering covers both.
|
|
const filterSuppliers = createFilterOptions<Supplier>({
|
|
stringify: (s) => `${s.name} ${s.ico ?? ""}`,
|
|
});
|
|
|
|
/**
|
|
* Searchable supplier picker for issued orders (purchase orders) — a
|
|
* controlled MUI Autocomplete over the sklad_suppliers lookup list, bound to
|
|
* the supplier id. Modeled on CustomerPicker (offers/invoices keep that one);
|
|
* renders bare (no label/error text); wrap it in the page's
|
|
* <Field label error> for the label + error line.
|
|
*/
|
|
export default function SupplierPicker({
|
|
suppliers,
|
|
value,
|
|
onChange,
|
|
disabled,
|
|
error,
|
|
placeholder,
|
|
id,
|
|
}: SupplierPickerProps) {
|
|
const selected = suppliers.find((s) => s.id === value) ?? null;
|
|
return (
|
|
<Autocomplete<Supplier>
|
|
options={suppliers}
|
|
value={selected}
|
|
onChange={(_, opt) => onChange(opt ? opt.id : null)}
|
|
getOptionLabel={(s) => s?.name ?? ""}
|
|
isOptionEqualToValue={(o, v) => o.id === v.id}
|
|
filterOptions={filterSuppliers}
|
|
disabled={disabled}
|
|
size="small"
|
|
fullWidth
|
|
autoHighlight
|
|
renderOption={(props, s) => {
|
|
const { key, ...rest } = props as typeof props & { key: string };
|
|
return (
|
|
<Box component="li" key={key} {...rest}>
|
|
<Box>
|
|
{s.name}
|
|
{s.ico && (
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ display: "block", color: "text.secondary" }}
|
|
>
|
|
IČ: {s.ico}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}}
|
|
renderInput={(params) => (
|
|
<MuiTextField
|
|
{...params}
|
|
id={id}
|
|
placeholder={placeholder ?? "Vyberte dodavatele…"}
|
|
error={!!error}
|
|
/>
|
|
)}
|
|
/>
|
|
);
|
|
}
|