import { useState, useEffect, useRef, useCallback, useId } from "react"; import { useParams, useNavigate, Link as RouterLink } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import CircularProgress from "@mui/material/CircularProgress"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import ItemPicker from "../components/warehouse/ItemPicker"; import apiFetch from "../utils/api"; import { warehouseReceiptDetailOptions, warehouseSupplierListOptions, warehouseLocationListOptions, type WarehouseSupplier, type WarehouseLocation, } from "../lib/queries/warehouse"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, TextField, Select, DateField, Field, LoadingState, PageEnter, } from "../ui"; interface ReceiptForm { supplier_id: number | null; delivery_note_number: string; delivery_note_date: string; 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; } 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 = ( ); const UploadIcon = ( ); export default function WarehouseReceiptForm() { const { id } = useParams(); const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const queryClient = useQueryClient(); const isEdit = id !== undefined && id !== "new"; const baseId = useId(); const counterRef = useRef(0); const nextKey = useCallback( () => `${baseId}-item-${counterRef.current++}`, [baseId], ); const [form, setForm] = useState({ supplier_id: null, delivery_note_number: "", delivery_note_date: "", notes: "", }); const [items, setItems] = useState(() => isEdit ? [] : [ { key: nextKey(), item_id: null, quantity: 0, unit_price: 0, location_id: null, batch_id: null, reservation_id: null, notes: null, }, ], ); const [errors, setErrors] = useState>({}); const [saving, setSaving] = useState(false); const [uploadingFiles, setUploadingFiles] = useState(false); const formInitialized = useRef(false); const { data: receipt, isPending } = useQuery( warehouseReceiptDetailOptions(isEdit ? id : undefined), ); const { data: suppliersData } = useQuery( warehouseSupplierListOptions({ perPage: 100 }), ); const suppliers: WarehouseSupplier[] = suppliersData?.data ?? []; const { data: locations } = useQuery(warehouseLocationListOptions()); useEffect(() => { formInitialized.current = false; }, [id]); useEffect(() => { if (isEdit && receipt && !formInitialized.current) { setForm({ supplier_id: receipt.supplier_id, delivery_note_number: receipt.delivery_note_number || "", delivery_note_date: receipt.delivery_note_date ? receipt.delivery_note_date.split("T")[0] : "", notes: receipt.notes || "", }); if (receipt.items && receipt.items.length > 0) { setItems( receipt.items.map((item) => ({ key: nextKey(), item_id: item.item_id, item_name: item.item?.name, quantity: Number(item.quantity), unit_price: Number(item.unit_price), location_id: item.location_id, batch_id: null, reservation_id: null, notes: item.notes, })), ); } formInitialized.current = true; } }, [isEdit, receipt, nextKey]); const saveMutation = useApiMutation< ReturnType, { id?: number } >({ url: () => isEdit ? `/api/admin/warehouse/receipts/${id}` : "/api/admin/warehouse/receipts", method: () => (isEdit ? "PUT" : "POST"), invalidate: ["warehouse"], }); const confirmMutation = useApiMutation({ url: (receiptId) => `/api/admin/warehouse/receipts/${receiptId}/confirm`, method: () => "POST", invalidate: ["warehouse"], onSuccess: () => { alert.success("Doklad byl potvrzen"); }, }); const handleFileUpload = useCallback( async (files: FileList | File[]) => { if (!isEdit || !id) return; setUploadingFiles(true); try { for (const file of files) { const formData = new FormData(); formData.append("file", file); const response = await apiFetch( `/api/admin/warehouse/receipts/${id}/attachments`, { method: "POST", body: formData, }, ); const result = await response.json(); if (!result.success) { alert.error( result.error || `Nepodařilo se nahrát soubor ${file.name}`, ); } } queryClient.invalidateQueries({ queryKey: ["warehouse"] }); alert.success("Soubory byly nahrány"); } catch { alert.error("Chyba připojení"); } finally { setUploadingFiles(false); } }, [id, isEdit, alert, queryClient], ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); if (e.dataTransfer.files.length > 0) { handleFileUpload(e.dataTransfer.files); } }, [handleFileUpload], ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); }, []); // All hooks above this line — permission gate is the last thing before render. if (!hasPermission("warehouse.operate")) return ; const addItem = () => { setItems((prev) => [ ...prev, { key: nextKey(), item_id: null, quantity: 0, unit_price: 0, location_id: null, batch_id: null, reservation_id: null, notes: null, }, ]); }; 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 = {}; const validItems = items.filter((l) => l.item_id !== null); if (validItems.length === 0) { newErrors.items = "Přidejte alespoň jednu položku"; } for (const item of validItems) { if (item.quantity <= 0) { newErrors.items = "Množství musí být větší než 0"; break; } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const buildPayload = () => ({ supplier_id: form.supplier_id, delivery_note_number: form.delivery_note_number.trim() || null, delivery_note_date: form.delivery_note_date || null, notes: form.notes.trim() || null, items: items .filter((l) => l.item_id !== null) .map((l) => ({ item_id: l.item_id!, quantity: l.quantity, unit_price: l.unit_price, location_id: l.location_id, notes: l.notes?.trim() || null, })), }); const saveReceipt = async (): Promise => { try { const data = await saveMutation.mutateAsync(buildPayload()); return data?.id ?? Number(id); } catch (e) { alert.error( e instanceof Error ? e.message : "Nepodařilo se uložit doklad", ); return null; } }; const confirmReceipt = async (receiptId: number) => { try { await confirmMutation.mutateAsync(receiptId); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleSaveDraft = async () => { if (!validate()) return; setSaving(true); try { const savedId = await saveReceipt(); if (savedId) { alert.success("Doklad byl uložen jako návrh"); navigate(`/warehouse/receipts/${savedId}`); } } finally { setSaving(false); } }; const handleSaveAndConfirm = async () => { if (!validate()) return; setSaving(true); try { const savedId = await saveReceipt(); if (savedId) { await confirmReceipt(savedId); navigate(`/warehouse/receipts/${savedId}`); } } finally { setSaving(false); } }; if (isEdit && isPending) { return ; } const supplierOptions = [ { value: "", label: "-- bez dodavatele --" }, ...suppliers.map((s) => ({ value: String(s.id), label: `${s.name}${s.ico ? ` (${s.ico})` : ""}`, })), ]; const locationOptions = [ { value: "", label: "-- bez lokace --" }, ...(locations ?? []).map((loc: WarehouseLocation) => ({ value: String(loc.id), label: `${loc.code} - ${loc.name}`, })), ]; return ( {/* Header */} {BackIcon} {isEdit ? "Upravit příjmový doklad" : "Nový příjmový doklad"} {/* Header fields */} Základní údaje 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} ))} {/* File upload — only in edit mode for existing receipts */} {isEdit && ( Přílohy { const input = document.createElement("input"); input.type = "file"; input.multiple = true; input.onchange = () => { if (input.files && input.files.length > 0) { handleFileUpload(input.files); } }; input.click(); }} sx={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1, p: 4, border: "2px dashed", borderColor: "divider", borderRadius: 1, color: "text.secondary", cursor: "pointer", opacity: uploadingFiles ? 0.6 : 1, transition: "border-color .2s, background-color .2s", "&:hover": { borderColor: "primary.main", bgcolor: "action.hover", }, }} > {uploadingFiles ? ( <> Nahrávání... ) : ( <> {UploadIcon} Přetáhněte soubory sem nebo klikněte pro výběr Dodací listy, faktury a další dokumenty )} )} ); }