Files
app/src/admin/components/warehouse/ItemPicker.tsx
BOHA 519edce373 fix: 2026-06-09 full-codebase audit hardening
Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace
the raw number|string transform idiom across every schema (the root-cause NaN bug
class); emailOrEmpty + lenient isoDateString/timeString.

Security: roles privilege-escalation closed; refresh-token family revocation on
reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices
gross VAT on all paths; orders-pdf custom-items authz.

Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse
confirm/cancel, attendance lockUserRow); uniqueness checks moved into create
transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision
timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO).

Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted
fields; dashboard invalidation gaps; stale-closure confirm bugs.

Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier;
tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit
fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes),
isolated on app_test via .env.test with a hard-throw setup guard.

Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 06:45:26 +02:00

107 lines
3.4 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import CircularProgress from "@mui/material/CircularProgress";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import useDebounce from "../../hooks/useDebounce";
import {
warehouseItemListOptions,
type WarehouseItem,
} from "../../lib/queries/warehouse";
interface ItemPickerProps {
value: number | null;
onChange: (itemId: number) => void;
itemName?: string;
}
/** Minimal option shape used to display a selected value that isn't in the
* current search results (edit/prefill via `itemName`). */
type ItemOption = Pick<WarehouseItem, "id" | "name"> & Partial<WarehouseItem>;
export default function ItemPicker({
value,
onChange,
itemName,
}: ItemPickerProps) {
const [inputValue, setInputValue] = useState(itemName ?? "");
// Debounce the search so the list query doesn't fire on every keystroke.
const debouncedSearch = useDebounce(inputValue, 300);
const { data, isFetching } = useQuery(
warehouseItemListOptions({ search: debouncedSearch, perPage: 20 }),
);
const options: ItemOption[] = data?.data ?? [];
// Show the selected value even when it isn't in the current search results
// (e.g. prefilled via `itemName`) by synthesizing a fallback option.
const selectedOption: ItemOption | null =
value == null
? null
: (options.find((o) => o.id === value) ?? {
id: value,
name: itemName ?? "",
});
return (
<Autocomplete<ItemOption>
value={selectedOption}
inputValue={inputValue}
onInputChange={(_, newInput) => setInputValue(newInput)}
onChange={(_, opt) => {
if (opt) onChange(opt.id);
}}
options={options}
loading={isFetching}
filterOptions={(x) => x}
getOptionLabel={(o) => o.name}
isOptionEqualToValue={(o, v) => o.id === v.id}
noOptionsText="Žádné položky"
renderOption={(props, option) => {
const { key, ...rest } = props;
return (
<Box component="li" key={key} {...rest}>
<Box>
<Typography variant="body2">{option.name}</Typography>
{(option.item_number ||
option.available_quantity !== undefined) && (
<Typography variant="caption" color="text.secondary">
{option.item_number}
{option.item_number && option.available_quantity !== undefined
? " · "
: ""}
{option.available_quantity !== undefined
? `${option.available_quantity} ${option.unit ?? ""}`
: ""}
</Typography>
)}
</Box>
</Box>
);
}}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder="Hledat položku..."
slotProps={{
input: {
...params.InputProps,
endAdornment: (
<>
{isFetching ? (
<CircularProgress color="inherit" size={18} />
) : null}
{params.InputProps.endAdornment}
</>
),
},
}}
/>
)}
/>
);
}