diff --git a/src/admin/pages/WarehouseIssueForm.tsx b/src/admin/pages/WarehouseIssueForm.tsx index 11b469e..a4035fb 100644 --- a/src/admin/pages/WarehouseIssueForm.tsx +++ b/src/admin/pages/WarehouseIssueForm.tsx @@ -1,24 +1,163 @@ import { useState, useEffect, useRef, useId } from "react"; -import { useParams, useNavigate, useLocation, Link } from "react-router-dom"; +import { + useParams, + useNavigate, + useLocation, + Link as RouterLink, +} from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; +import { motion } from "framer-motion"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import IconButton from "@mui/material/IconButton"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; -import { motion } from "framer-motion"; -import FormField from "../components/FormField"; -import WarehouseMovementTable, { - type MovementItem, -} from "../components/warehouse/WarehouseMovementTable"; +import ItemPicker from "../components/warehouse/ItemPicker"; -import { warehouseIssueDetailOptions } from "../lib/queries/warehouse"; +import { jsonQuery } from "../lib/apiAdapter"; +import { + warehouseIssueDetailOptions, + warehouseLocationListOptions, + type WarehouseLocation, +} from "../lib/queries/warehouse"; import { projectListOptions, type Project } from "../lib/queries/projects"; import { useApiMutation } from "../lib/queries/mutations"; +import { Button, Card, TextField, Select, Field, LoadingState } from "../ui"; interface IssueForm { project_id: number | null; notes: string; } +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; +} + +interface Batch { + id: number; + quantity: number; + unit_price: number; + received_at: string; + is_consumed: boolean; +} + +interface BatchesResponse { + batches: Batch[]; + total_stock: number; + reserved_quantity: number; + available_quantity: number; +} + +function parseDecimal(raw: string): number { + const cleaned = raw.replace(/[eE+\-]/g, ""); + const n = Number(cleaned); + return Number.isFinite(n) ? n : 0; +} + +const BackIcon = ( + + + +); + +const RemoveIcon = ( + + + + +); + +const PlusIcon = ( + + + + +); + +/** Inline batch dropdown: lists FIFO batches for an item, sets batch + unit price. */ +function BatchSelect({ + itemId, + value, + onChange, +}: { + itemId: number | null; + value: number | null; + onChange: (batchId: number, unitPrice: number) => void; +}) { + const { data: response } = useQuery({ + queryKey: ["warehouse", "batches", itemId], + queryFn: () => + jsonQuery( + `/api/admin/warehouse/items/${itemId}/batches`, + ), + enabled: !!itemId, + }); + + const batches = response?.batches ?? []; + + return ( + + {response && response.reserved_quantity > 0 && ( + + Dostupné: {response.available_quantity} ks (z toho rezervováno:{" "} + {response.reserved_quantity} ks) + + )} + { + const batchId = Number(val); + const batch = batches.find((b) => b.id === batchId); + if (batch) onChange(batch.id, Number(batch.unit_price)); + }} + options={[ + { value: "", label: "-- auto FIFO --" }, + ...batches.map((b) => ({ + value: String(b.id), + label: `${new Date(b.received_at).toLocaleDateString("cs-CZ")} | ${b.quantity} ks | ${Number(b.unit_price).toFixed(2)} Kč`, + })), + ]} + /> + + ); +} + export default function WarehouseIssueForm() { const { id } = useParams(); const alert = useAlert(); @@ -87,6 +226,8 @@ export default function WarehouseIssueForm() { const { data: projectsData } = useQuery(projectListOptions({ perPage: 100 })); const projects = projectsData?.data ?? []; + const { data: locations } = useQuery(warehouseLocationListOptions()); + useEffect(() => { formInitialized.current = false; }, [id]); @@ -134,6 +275,22 @@ export default function WarehouseIssueForm() { ]); }; + const removeItem = (index: number) => { + setItems((prev) => prev.filter((_, i) => i !== index)); + }; + + const updateItem = ( + index: number, + field: keyof MovementItem, + value: unknown, + ) => { + setItems((prev) => { + const updated = [...prev]; + updated[index] = { ...updated[index], [field]: value }; + return updated; + }); + }; + const validate = (): boolean => { const newErrors: Record = {}; if (!form.project_id) { @@ -245,138 +402,219 @@ export default function WarehouseIssueForm() { }; if (isEdit && isPending) { - return ( - - - - ); + return ; } + const projectOptions = [ + { value: "", label: "Vyberte projekt..." }, + ...projects.map((p: Project) => ({ + value: String(p.id), + label: `${p.project_number} – ${p.name}`, + })), + ]; + + const locationOptions = [ + { value: "", label: "-- bez lokace --" }, + ...(locations ?? []).map((loc: WarehouseLocation) => ({ + value: String(loc.id), + label: `${loc.code} - ${loc.name}`, + })), + ]; + return ( - + {/* Header */} - - - + + - - - - - + {BackIcon} + + {isEdit ? "Upravit výdejku" : "Nová výdejka"} - - - - - - {saving ? "Ukládání..." : "Uložit jako návrh"} - - - {saving ? "Ukládání..." : "Uložit a potvrdit"} - - + + + + + {saving ? "Ukládání..." : "Uložit jako návrh"} + + + {saving ? "Ukládání..." : "Uložit a potvrdit"} + + + {/* Header fields */} - - Základní údaje - - - - setForm((prev) => ({ - ...prev, - project_id: - e.target.value === "" ? null : Number(e.target.value), - })) - } - className="admin-form-select" - > - Vyberte projekt... - {projects.map((p: Project) => ( - - {p.project_number} – {p.name} - - ))} - - {errors.project_id && ( - {errors.project_id} - )} - - - - setForm((prev) => ({ ...prev, notes: e.target.value })) - } - className="admin-form-input" - rows={3} - placeholder="Volitelné poznámky" - /> - - - + + + Základní údaje + + + + setForm((prev) => ({ + ...prev, + project_id: val === "" ? null : Number(val), + })) + } + options={projectOptions} + /> + + + + setForm((prev) => ({ ...prev, notes: e.target.value })) + } + placeholder="Volitelné poznámky" + /> + + {/* Lines */} - - Položky + + + Položky + {errors.items && ( - {errors.items} - + )} - - + + {items.map((item, index) => ( + + updateItem(index, "item_id", id)} + /> + { + updateItem(index, "batch_id", batchId); + updateItem(index, "unit_price", unitPrice); + }} + /> + + updateItem(index, "quantity", parseDecimal(e.target.value)) + } + inputProps={{ + min: 0, + step: 0.001, + inputMode: "decimal", + pattern: "[0-9]+([\\.,][0-9]+)?", + "aria-label": `Množství, řádek ${index + 1}`, + }} + placeholder="Množství" + /> + + + updateItem(index, "location_id", val ? Number(val) : null) + } + options={locationOptions} + /> + updateItem(index, "notes", e.target.value)} + placeholder="Poznámka" + /> + removeItem(index)} + title="Odebrat řádek" + aria-label="Odebrat řádek" + sx={{ justifySelf: { xs: "start", md: "center" } }} + > + {RemoveIcon} + + + ))} + + + Přidat položku + + - + ); }