import { ReactNode, useId, useRef } from "react"; import ItemPicker from "./ItemPicker"; import LocationSelect from "./LocationSelect"; import BatchPicker from "./BatchPicker"; export interface MovementItem { key: string; item_id: number | null; item_name?: string; quantity: number; unit_price: number; location_id: number | null; batch_id: number | null; reservation_id: number | null; notes: string | null; } export type MovementRowRenderer = ( item: T, index: number, updateItem: (field: string, value: unknown) => void, removeItem: () => void, ) => ReactNode; interface WarehouseMovementTableProps { items: T[]; onChange: (items: T[]) => void; mode: "receipt" | "issue" | "inventory"; defaultItem?: () => Omit; renderRow?: MovementRowRenderer; renderHeader?: () => ReactNode; } function parseDecimal(raw: string): number { // Strip leading minus if you want negative support; here we want only positive const cleaned = raw.replace(/[eE+\-]/g, ""); const n = Number(cleaned); return Number.isFinite(n) ? n : 0; } const builtInDefaultMovementItem = (): Omit => ({ item_id: null, quantity: 0, unit_price: 0, location_id: null, batch_id: null, reservation_id: null, notes: null, }); export default function WarehouseMovementTable({ items, onChange, mode, defaultItem, renderRow, renderHeader, }: WarehouseMovementTableProps) { const baseId = useId(); const counterRef = useRef(0); const nextKey = () => `${baseId}-item-${counterRef.current++}`; const addItem = () => { const factory = defaultItem ?? (builtInDefaultMovementItem as unknown as () => Omit); onChange([...items, { ...(factory() as object), key: nextKey() } as T]); }; const removeItem = (index: number) => { onChange(items.filter((_, i) => i !== index)); }; const updateItem = (index: number, field: string, value: unknown) => { const updated = [...items]; updated[index] = { ...updated[index], [field]: value }; onChange(updated); }; // If a custom row renderer is supplied, the caller is fully in charge of // the row layout (and typically the column headers too — see inventory mode). if (renderRow) { return (
{renderHeader && ( {renderHeader()} )} {items.map((item, index) => ( {renderRow( item, index, (field, value) => updateItem(index, field, value), () => removeItem(index), )} ))}
); } // Default rendering: only valid for "receipt" | "issue" — narrow at runtime. const movementItems = items as unknown as MovementItem[]; return (
{mode === "issue" && ( )} {movementItems.map((item, index) => ( {mode === "issue" && ( )} ))}
PoložkaŠaržeMnožství Cena/ks Lokace Poznámka Akce
updateItem(index, "item_id", id)} /> { updateItem(index, "batch_id", batchId); updateItem(index, "unit_price", unitPrice); }} /> updateItem( index, "quantity", parseDecimal(e.target.value), ) } min="0" step="0.001" inputMode="decimal" pattern="[0-9]+([\.,][0-9]+)?" aria-label={`Množství, řádek ${index + 1}`} /> updateItem( index, "unit_price", parseDecimal(e.target.value), ) } min="0" step="0.01" disabled={mode === "issue"} inputMode="decimal" pattern="[0-9]+([\.,][0-9]+)?" aria-label={`Cena za kus, řádek ${index + 1}`} /> updateItem(index, "location_id", id)} /> updateItem(index, "notes", e.target.value)} />
); }