From 337feb8b3a169372372c88cc8c1fd1f4261e2477 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sat, 6 Jun 2026 13:58:21 +0200 Subject: [PATCH] feat(orders-ui): manual order price line item + PO upload (hybrid submit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds price/quantity fields (→ one line item) and a PDF attachment picker to the manual-order create modal. Submit now uses apiFetch directly: FormData (multipart) when a file is attached, JSON otherwise. Navigates to the new order on success. Removes the old useApiMutation-based createMutation; deleteMutation is untouched. Co-Authored-By: Claude Sonnet 4.6 --- src/admin/pages/Orders.tsx | 185 +++++++++++++++++++++++++++++++------ 1 file changed, 158 insertions(+), 27 deletions(-) diff --git a/src/admin/pages/Orders.tsx b/src/admin/pages/Orders.tsx index 8a649c8..3ee4427 100644 --- a/src/admin/pages/Orders.tsx +++ b/src/admin/pages/Orders.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import apiFetch from "../utils/api"; import Forbidden from "../components/Forbidden"; import { motion } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; @@ -89,8 +90,13 @@ export default function Orders() { scope_description: "", notes: "", create_project: true, + price: "", + quantity: "1", }); const [creating, setCreating] = useState(false); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const [orderAttachment, setOrderAttachment] = useState(null); const customersQuery = useQuery({ ...offerCustomersOptions(), @@ -101,23 +107,6 @@ export default function Orders() { enabled: showCreate, }); - // The payload normalises customer_id: "" → null. CreateOrderSchema's - // customer_id coercion does NOT special-case "", so a raw "" would become - // Number("") = 0 and hit a foreign-key violation. - type OrderCreatePayload = Omit & { - customer_id: number | null; - }; - - const createMutation = useApiMutation({ - url: () => `${API_BASE}/orders`, - method: () => "POST", - invalidate: ["orders", "projects", "offers", "invoices"], - onSuccess: () => { - setShowCreate(false); - alert.success("Objednávka byla vytvořena"); - }, - }); - const openCreate = () => { setCreateForm({ customer_id: "", @@ -129,21 +118,82 @@ export default function Orders() { scope_description: "", notes: "", create_project: true, + price: "", + quantity: "1", }); + setOrderAttachment(null); setShowCreate(true); }; const handleCreate = async () => { setCreating(true); try { - await createMutation.mutateAsync({ - ...createForm, - customer_id: createForm.customer_id - ? Number(createForm.customer_id) - : null, - }); - } catch (e) { - alert.error(e instanceof Error ? e.message : "Chyba připojení"); + const priceNum = createForm.price ? Number(createForm.price) : 0; + const qtyNum = createForm.quantity ? Number(createForm.quantity) : 1; + const items = + priceNum > 0 + ? [ + { + description: createForm.scope_title || "Položka", + quantity: qtyNum, + unit_price: priceNum, + is_included_in_total: true, + }, + ] + : undefined; + + let fetchOptions: RequestInit; + if (orderAttachment) { + const fd = new FormData(); + if (createForm.customer_id) + fd.append("customer_id", createForm.customer_id); + fd.append("customer_order_number", createForm.customer_order_number); + fd.append("currency", createForm.currency); + fd.append("vat_rate", createForm.vat_rate); + fd.append("apply_vat", createForm.apply_vat ? "1" : "0"); + fd.append("scope_title", createForm.scope_title); + fd.append("scope_description", createForm.scope_description); + fd.append("notes", createForm.notes); + fd.append("create_project", createForm.create_project ? "1" : "0"); + if (items) fd.append("items", JSON.stringify(items)); + fd.append("attachment", orderAttachment); + fetchOptions = { method: "POST", body: fd }; + } else { + fetchOptions = { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + customer_id: createForm.customer_id + ? Number(createForm.customer_id) + : null, + customer_order_number: createForm.customer_order_number, + currency: createForm.currency, + vat_rate: createForm.vat_rate, + apply_vat: createForm.apply_vat, + scope_title: createForm.scope_title, + scope_description: createForm.scope_description, + notes: createForm.notes, + create_project: createForm.create_project, + ...(items ? { items } : {}), + }), + }; + } + + const response = await apiFetch(`${API_BASE}/orders`, fetchOptions); + const result = await response.json(); + if (result.success) { + setShowCreate(false); + alert.success(result.message || "Objednávka byla vytvořena"); + await queryClient.invalidateQueries({ queryKey: ["orders"] }); + await queryClient.invalidateQueries({ queryKey: ["projects"] }); + await queryClient.invalidateQueries({ queryKey: ["offers"] }); + await queryClient.invalidateQueries({ queryKey: ["invoices"] }); + if (result.data?.id) navigate(`/orders/${result.data.id}`); + } else { + alert.error(result.error || "Nepodařilo se vytvořit objednávku"); + } + } catch { + alert.error("Chyba připojení"); } finally { setCreating(false); } @@ -547,6 +597,36 @@ export default function Orders() { +
+ + + setCreateForm({ ...createForm, price: e.target.value }) + } + className="admin-form-input" + min="0" + step="0.01" + placeholder="0" + /> + + + + setCreateForm({ ...createForm, quantity: e.target.value }) + } + className="admin-form-input" + min="0" + step="1" + /> + +
+ + + {orderAttachment ? ( +
+ + {orderAttachment.name}{" "} + + ({(orderAttachment.size / 1024).toFixed(0)} KB) + + + +
+ ) : ( + + )} +
+