From 30dd5a3f3bbb33ac977836d16011e598a6398443 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sat, 6 Jun 2026 11:17:53 +0200 Subject: [PATCH] feat(orders-ui): add manual order create modal (header only, optional project) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Projects.tsx pattern: useQuery-driven customers + next-number fetches (enabled only when modal is open), OrderCreatePayload type normalises customer_id "" → null so the server's CreateOrderSchema never receives 0. Co-Authored-By: Claude Sonnet 4.6 --- src/admin/pages/Orders.tsx | 244 ++++++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 1 deletion(-) diff --git a/src/admin/pages/Orders.tsx b/src/admin/pages/Orders.tsx index cbecfe4..8a649c8 100644 --- a/src/admin/pages/Orders.tsx +++ b/src/admin/pages/Orders.tsx @@ -1,16 +1,23 @@ import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { Link } from "react-router-dom"; import Forbidden from "../components/Forbidden"; import { motion } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; +import FormModal from "../components/FormModal"; +import FormField from "../components/FormField"; import { formatCurrency, formatDate, czechPlural } from "../utils/formatters"; import SortIcon from "../components/SortIcon"; import useTableSort from "../hooks/useTableSort"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; -import { orderListOptions } from "../lib/queries/orders"; +import { + orderListOptions, + orderNextNumberOptions, +} from "../lib/queries/orders"; +import { offerCustomersOptions } from "../lib/queries/offers"; import { useApiMutation } from "../lib/queries/mutations"; import Pagination from "../components/Pagination"; @@ -71,6 +78,77 @@ export default function Orders() { }, }); + const [showCreate, setShowCreate] = useState(false); + const [createForm, setCreateForm] = useState({ + customer_id: "", + customer_order_number: "", + currency: "CZK", + vat_rate: "21", + apply_vat: true, + scope_title: "", + scope_description: "", + notes: "", + create_project: true, + }); + const [creating, setCreating] = useState(false); + + const customersQuery = useQuery({ + ...offerCustomersOptions(), + enabled: showCreate, + }); + const nextNumberQuery = useQuery({ + ...orderNextNumberOptions(), + 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: "", + customer_order_number: "", + currency: "CZK", + vat_rate: "21", + apply_vat: true, + scope_title: "", + scope_description: "", + notes: "", + create_project: true, + }); + 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í"); + } finally { + setCreating(false); + } + }; + const { items: orders, pagination, @@ -120,6 +198,27 @@ export default function Orders() { )}

+ {hasPermission("orders.create") && ( +
+ +
+ )} + + setShowCreate(false)} + onSubmit={handleCreate} + title="Vytvořit objednávku bez nabídky" + submitLabel="Vytvořit" + loading={creating} + > +
+
+ + + + + + setCreateForm({ + ...createForm, + customer_order_number: e.target.value, + }) + } + className="admin-form-input" + /> + +
+ +
+ + + + + + setCreateForm({ ...createForm, vat_rate: e.target.value }) + } + className="admin-form-input" + min="0" + /> + +
+ + + + setCreateForm({ ...createForm, scope_title: e.target.value }) + } + className="admin-form-input" + /> + + + +