From 86bebf97767cc2934eddb713119a6b510b39a871 Mon Sep 17 00:00:00 2001 From: BOHA Date: Fri, 29 May 2026 14:20:42 +0200 Subject: [PATCH] feat(warehouse): add shared warehouse components Co-Authored-By: Claude Opus 4.8 --- .../components/warehouse/BatchPicker.tsx | 48 ++++++ src/admin/components/warehouse/ItemPicker.tsx | 59 +++++++ .../components/warehouse/LocationSelect.tsx | 31 ++++ .../components/warehouse/SupplierSelect.tsx | 33 ++++ .../warehouse/WarehouseMovementTable.tsx | 148 ++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 src/admin/components/warehouse/BatchPicker.tsx create mode 100644 src/admin/components/warehouse/ItemPicker.tsx create mode 100644 src/admin/components/warehouse/LocationSelect.tsx create mode 100644 src/admin/components/warehouse/SupplierSelect.tsx create mode 100644 src/admin/components/warehouse/WarehouseMovementTable.tsx diff --git a/src/admin/components/warehouse/BatchPicker.tsx b/src/admin/components/warehouse/BatchPicker.tsx new file mode 100644 index 0000000..d3a7c1e --- /dev/null +++ b/src/admin/components/warehouse/BatchPicker.tsx @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { jsonQuery } from "../../lib/apiAdapter"; + +interface Batch { + id: number; + quantity: number; + unit_price: number; + received_at: string; + is_consumed: boolean; +} + +interface BatchPickerProps { + itemId: number | null; + value: number | null; + onChange: (batchId: number, unitPrice: number) => void; +} + +export default function BatchPicker({ + itemId, + value, + onChange, +}: BatchPickerProps) { + const { data: batches } = useQuery({ + queryKey: ["warehouse", "batches", itemId], + queryFn: () => + jsonQuery(`/api/admin/warehouse/items/${itemId}/batches`), + enabled: !!itemId, + }); + return ( + + ); +} diff --git a/src/admin/components/warehouse/ItemPicker.tsx b/src/admin/components/warehouse/ItemPicker.tsx new file mode 100644 index 0000000..ab7d014 --- /dev/null +++ b/src/admin/components/warehouse/ItemPicker.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { warehouseItemListOptions } from "../../lib/queries/warehouse"; + +interface ItemPickerProps { + value: number | null; + onChange: (itemId: number) => void; +} + +export default function ItemPicker({ value, onChange }: ItemPickerProps) { + const [search, setSearch] = useState(""); + const [open, setOpen] = useState(false); + const { data } = useQuery(warehouseItemListOptions({ search, perPage: 20 })); + const items = data?.data ?? []; + + return ( +
+ { + setSearch(e.target.value); + setOpen(true); + }} + onFocus={() => setOpen(true)} + onBlur={() => setTimeout(() => setOpen(false), 200)} + /> + {open && items.length > 0 && ( +
    + {items.map((item) => ( +
  • { + onChange(item.id); + setOpen(false); + setSearch(item.name); + }} + > + {item.name} + {item.item_number && ( + + {item.item_number} + + )} + {item.available_quantity !== undefined && ( + + {item.available_quantity} {item.unit} + + )} +
  • + ))} +
+ )} +
+ ); +} diff --git a/src/admin/components/warehouse/LocationSelect.tsx b/src/admin/components/warehouse/LocationSelect.tsx new file mode 100644 index 0000000..f57e51d --- /dev/null +++ b/src/admin/components/warehouse/LocationSelect.tsx @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; +import { + warehouseLocationListOptions, + type WarehouseLocation, +} from "../../lib/queries/warehouse"; + +interface LocationSelectProps { + value: number | null; + onChange: (locationId: number | null) => void; +} + +export default function LocationSelect({ + value, + onChange, +}: LocationSelectProps) { + const { data: locations } = useQuery(warehouseLocationListOptions()); + return ( + + ); +} diff --git a/src/admin/components/warehouse/SupplierSelect.tsx b/src/admin/components/warehouse/SupplierSelect.tsx new file mode 100644 index 0000000..9640339 --- /dev/null +++ b/src/admin/components/warehouse/SupplierSelect.tsx @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query"; +import { + warehouseSupplierListOptions, + type WarehouseSupplier, +} from "../../lib/queries/warehouse"; + +interface SupplierSelectProps { + value: number | null; + onChange: (supplierId: number | null) => void; +} + +export default function SupplierSelect({ + value, + onChange, +}: SupplierSelectProps) { + const { data } = useQuery(warehouseSupplierListOptions({ perPage: 100 })); + const suppliers = data?.data ?? []; + return ( + + ); +} diff --git a/src/admin/components/warehouse/WarehouseMovementTable.tsx b/src/admin/components/warehouse/WarehouseMovementTable.tsx new file mode 100644 index 0000000..8573374 --- /dev/null +++ b/src/admin/components/warehouse/WarehouseMovementTable.tsx @@ -0,0 +1,148 @@ +import type { WarehouseLocation } from "../../lib/queries/warehouse"; +import ItemPicker from "./ItemPicker"; +import LocationSelect from "./LocationSelect"; +import BatchPicker from "./BatchPicker"; + +export interface MovementLine { + 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; +} + +interface WarehouseMovementTableProps { + lines: MovementLine[]; + onChange: (lines: MovementLine[]) => void; + mode: "receipt" | "issue"; + locations: WarehouseLocation[]; +} + +let lineCounter = 0; + +export default function WarehouseMovementTable({ + lines, + onChange, + mode, +}: WarehouseMovementTableProps) { + const addLine = () => { + onChange([ + ...lines, + { + key: `line-${++lineCounter}`, + item_id: null, + quantity: 0, + unit_price: 0, + location_id: null, + batch_id: null, + reservation_id: null, + notes: null, + }, + ]); + }; + const removeLine = (index: number) => { + onChange(lines.filter((_, i) => i !== index)); + }; + const updateLine = (index: number, field: string, value: unknown) => { + const updated = [...lines]; + updated[index] = { ...updated[index], [field]: value }; + onChange(updated); + }; + + return ( +
+ + + + + {mode === "issue" && } + + + + + + + + + {lines.map((line, index) => ( + + + {mode === "issue" && ( + + )} + + + + + + + ))} + +
PoložkaŠaržeMnožstvíCena/ksLokacePoznámka
+ updateLine(index, "item_id", id)} + /> + + { + updateLine(index, "batch_id", batchId); + updateLine(index, "unit_price", unitPrice); + }} + /> + + + updateLine(index, "quantity", Number(e.target.value)) + } + min="0" + step="0.001" + /> + + + updateLine(index, "unit_price", Number(e.target.value)) + } + min="0" + step="0.01" + disabled={mode === "issue"} + /> + + updateLine(index, "location_id", id)} + /> + + updateLine(index, "notes", e.target.value)} + /> + + +
+ +
+ ); +}