import { useState, useCallback, useEffect } from "react"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; import { Modal, Button, TextField, Field } from "../ui"; import { useAlert } from "../context/AlertContext"; // Editable line-item. quantity/unit_price are held as the raw typed string // while editing (so a field can be cleared — empty renders fine in a // type="number" input) and are coerced to numbers in handleEditGenerate before // being handed to onGenerate (the parent posts them verbatim). interface ConfirmationItem { description: string; quantity: string | number; unit: string; unit_price: string | number; is_included_in_total: boolean; } // Numeric shape handed to the parent (the raw editing strings are coerced in // handleEditGenerate). Distinct from the editable ConfirmationItem. interface GeneratedItem { description: string; quantity: number; unit: string; unit_price: number; is_included_in_total: boolean; } interface OrderConfirmationModalProps { isOpen: boolean; onClose: () => void; onGenerate: (lang: string, items?: GeneratedItem[]) => Promise; initialItems: ConfirmationItem[]; orderNumber: string; } export default function OrderConfirmationModal({ isOpen, onClose, onGenerate, initialItems, orderNumber, }: OrderConfirmationModalProps) { const alert = useAlert(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const [step, setStep] = useState<"choose" | "edit">("choose"); const [lang, setLang] = useState("cs"); const [items, setItems] = useState(initialItems); const [loading, setLoading] = useState(false); // The modal is permanently mounted and merely toggled via `isOpen`, so its // local state survives between opens and the `useState(initialItems)` snapshot // goes stale. Re-seed everything from the current props each time the modal // (re)opens so a fresh open always reflects the latest order data and starts // on the "choose" step. useEffect(() => { if (!isOpen) return; setStep("choose"); setLang("cs"); setItems(initialItems); // initialItems are captured at open time; intentionally not in the dep // array so an unrelated parent re-render doesn't clobber the user's edits. // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); const handleUseExisting = async () => { setLoading(true); try { await onGenerate(lang, undefined); // Only close on success — a generation error must keep the modal open. onClose(); } catch (err) { console.error("Chyba při generování potvrzení:", err); alert.error("Nepodařilo se vygenerovat potvrzení"); } finally { setLoading(false); } }; const handleEditGenerate = async () => { setLoading(true); try { // Coerce the raw typed strings back to numbers so an empty/partial field // never reaches the server as NaN (the parent posts these verbatim). const coercedItems: GeneratedItem[] = items.map((it) => ({ description: it.description, quantity: Number(it.quantity) || 0, unit: it.unit, unit_price: Number(it.unit_price) || 0, is_included_in_total: it.is_included_in_total, })); await onGenerate(lang, coercedItems); // Only close on success — on error keep the user's edited items intact. onClose(); } catch (err) { console.error("Chyba při generování potvrzení:", err); alert.error("Nepodařilo se vygenerovat potvrzení"); } finally { setLoading(false); } }; const updateItem = useCallback( ( index: number, field: keyof ConfirmationItem, value: string | number | boolean, ) => { setItems((prev) => { const next = [...prev]; next[index] = { ...next[index], [field]: value }; return next; }); }, [], ); const removeItem = useCallback((index: number) => { setItems((prev) => prev.filter((_, i) => i !== index)); }, []); const addItem = useCallback(() => { setItems((prev) => [ ...prev, { description: "", quantity: 1, unit: "ks", unit_price: 0, is_included_in_total: true, }, ]); }, []); return ( {step === "choose" ? ( Jak chcete připravit potvrzení objednávky? ) : ( {isMobile ? ( {items.map((item, i) => ( Položka {i + 1} removeItem(i)} title="Odstranit" aria-label="Odstranit" > updateItem(i, "description", e.target.value) } /> updateItem(i, "quantity", e.target.value) } slotProps={{ htmlInput: { step: "0.001" } }} /> updateItem(i, "unit", e.target.value)} /> updateItem(i, "unit_price", e.target.value) } slotProps={{ htmlInput: { step: "0.01" } }} /> ))} ) : ( Popis Mn. Jedn. Cena {items.map((item, i) => ( updateItem(i, "description", e.target.value) } sx={{ minWidth: 200 }} /> updateItem(i, "quantity", e.target.value) } sx={{ width: 112 }} slotProps={{ htmlInput: { step: "0.001" } }} /> updateItem(i, "unit", e.target.value) } sx={{ width: 70 }} /> updateItem(i, "unit_price", e.target.value) } sx={{ width: 110 }} slotProps={{ htmlInput: { step: "0.01" } }} /> removeItem(i)} title="Odstranit" aria-label="Odstranit" > ))} )} )} ); }