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}
+ >
+
+
);
}