feat(orders-ui): manual order price line item + PO upload (hybrid submit)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<File | null>(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<typeof createForm, "customer_id"> & {
|
||||
customer_id: number | null;
|
||||
};
|
||||
|
||||
const createMutation = useApiMutation<OrderCreatePayload, { id: number }>({
|
||||
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() {
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Cena za jednotku (bez DPH)">
|
||||
<input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
value={createForm.price}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, price: e.target.value })
|
||||
}
|
||||
className="admin-form-input"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="0"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Množství">
|
||||
<input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
value={createForm.quantity}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, quantity: e.target.value })
|
||||
}
|
||||
className="admin-form-input"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Předmět (scope)">
|
||||
<input
|
||||
type="text"
|
||||
@@ -583,6 +663,57 @@ export default function Orders() {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Příloha (PO zákazníka, PDF)">
|
||||
{orderAttachment ? (
|
||||
<div className="flex-row gap-2">
|
||||
<span className="text-md">
|
||||
{orderAttachment.name}{" "}
|
||||
<span className="text-tertiary">
|
||||
({(orderAttachment.size / 1024).toFixed(0)} KB)
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOrderAttachment(null)}
|
||||
className="admin-btn-icon"
|
||||
title="Odebrat"
|
||||
style={{ marginLeft: "auto" }}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.4rem",
|
||||
}}
|
||||
>
|
||||
Vybrat soubor
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(e) =>
|
||||
setOrderAttachment(e.target.files?.[0] || null)
|
||||
}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<label className="admin-form-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user