feat(issued-orders): counterparty is now a supplier (dodavatel), not a customer
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>
This commit is contained in:
88
src/admin/ui/SupplierPicker.tsx
Normal file
88
src/admin/ui/SupplierPicker.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user