From 3d784adf5df79288513bc5a5543cb81c56b0e994 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 16 Jun 2026 20:20:51 +0200 Subject: [PATCH] feat(documents): per-document custom-field print picker on offer/issued-order/invoice detail Co-Authored-By: Claude Opus 4.8 (1M context) --- src/admin/pages/InvoiceDetail.tsx | 37 ++++++++++++++++++++++++--- src/admin/pages/IssuedOrderDetail.tsx | 16 ++++++++++++ src/admin/pages/OfferDetail.tsx | 16 ++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index ce020d9..9010252 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -27,6 +27,7 @@ import RichEditor from "../components/RichEditor"; import SectionsEditor, { type DocumentSection, } from "../components/document/SectionsEditor"; +import CustomFieldsPrintPicker from "../components/document/CustomFieldsPrintPicker"; import { DndContext, closestCenter, @@ -611,6 +612,9 @@ export default function InvoiceDetail() { // Rich-text CZ/EN "Obsah" sections (printed after the items on the PDF — // shared editor with offers/issued orders). const [sections, setSections] = useState([]); + const [selectedCustomFields, setSelectedCustomFields] = useState( + [], + ); const [items, setItems] = useState(() => [ { _key: "inv-1", @@ -821,11 +825,15 @@ export default function InvoiceDetail() { : []; setSections(mappedSections); + const mappedCustomFields = inv.selected_custom_fields ?? []; + setSelectedCustomFields(mappedCustomFields); + // Capture initial snapshot for dirty-checking initialSnapshotRef.current = JSON.stringify({ form: formData, items: mappedItems, sections: mappedSections, + selectedCustomFields: mappedCustomFields, }); setDataReady(true); @@ -923,15 +931,21 @@ export default function InvoiceDetail() { // Edit mode: captured inside the sync effect from raw query data. // Create mode: captured on the first render after sync effects populate the form. if (dataReady && !initialSnapshotRef.current) { - initialSnapshotRef.current = JSON.stringify({ form, items, sections }); + initialSnapshotRef.current = JSON.stringify({ + form, + items, + sections, + selectedCustomFields, + }); } const isDirty = useMemo(() => { if (!initialSnapshotRef.current) return false; return ( - JSON.stringify({ form, items, sections }) !== initialSnapshotRef.current + JSON.stringify({ form, items, sections, selectedCustomFields }) !== + initialSnapshotRef.current ); - }, [form, items, sections]); + }, [form, items, sections, selectedCustomFields]); useEffect(() => { if (!isDirty) return; @@ -1095,6 +1109,7 @@ export default function InvoiceDetail() { content: s.content, position: i, })), + selected_custom_fields: selectedCustomFields, }; // Only set status when a target is given (create-as-draft / create-as-live // / finalize). Editing a live invoice sends no status — the backend keeps @@ -1107,7 +1122,12 @@ export default function InvoiceDetail() { const data = await saveMutation.mutateAsync(payload); alert.success(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena"); - initialSnapshotRef.current = JSON.stringify({ form, items, sections }); + initialSnapshotRef.current = JSON.stringify({ + form, + items, + sections, + selectedCustomFields, + }); if (!isEdit) { navigate(`/invoices/${data.invoice_id}`); } @@ -2209,6 +2229,15 @@ export default function InvoiceDetail() { ]} /> + + + + {/* Items */} diff --git a/src/admin/pages/IssuedOrderDetail.tsx b/src/admin/pages/IssuedOrderDetail.tsx index 870d017..fd8ac17 100644 --- a/src/admin/pages/IssuedOrderDetail.tsx +++ b/src/admin/pages/IssuedOrderDetail.tsx @@ -23,6 +23,7 @@ import SectionsEditor, { type DocumentSection, } from "../components/document/SectionsEditor"; import LockBanner from "../components/document/LockBanner"; +import CustomFieldsPrintPicker from "../components/document/CustomFieldsPrintPicker"; import { useDocumentLock } from "../hooks/useDocumentLock"; import { useUnsavedChangesGuard } from "../hooks/useUnsavedChangesGuard"; import { useDocumentPdf } from "../hooks/useDocumentPdf"; @@ -137,6 +138,7 @@ interface IssuedOrderSavePayload { internal_notes: string; items: IssuedOrderItemPayload[]; sections: IssuedOrderSectionPayload[]; + selected_custom_fields: number[]; status?: string; } @@ -164,6 +166,9 @@ export default function IssuedOrderDetail() { emptyDocumentItem(), ]); const [sections, setSections] = useState([]); + const [selectedCustomFields, setSelectedCustomFields] = useState( + [], + ); const [errors, setErrors] = useState>({}); const [saving, setSaving] = useState(false); // Which save action is in flight ("draft" | the live status | "save"), so @@ -308,6 +313,7 @@ export default function IssuedOrderDetail() { })) : []; setSections(mappedSections); + setSelectedCustomFields(d.selected_custom_fields ?? []); setLockedBy(d.locked_by ?? null); // Acquire the edit lock when nobody holds it, the order is still @@ -430,6 +436,7 @@ export default function IssuedOrderDetail() { content: s.content, position: i, })), + selected_custom_fields: selectedCustomFields, }; // Only set status when a target is given (create-as-draft / create-as-live // / finalize). Editing a live order sends no status — the backend keeps @@ -874,6 +881,15 @@ export default function IssuedOrderDetail() { placeholder="Objednáváme si u Vás: (ponechte prázdné pro výchozí)" /> + + + + {/* Items */} diff --git a/src/admin/pages/OfferDetail.tsx b/src/admin/pages/OfferDetail.tsx index 86286c5..681ce67 100644 --- a/src/admin/pages/OfferDetail.tsx +++ b/src/admin/pages/OfferDetail.tsx @@ -33,6 +33,7 @@ import SectionsEditor, { type DocumentSection, } from "../components/document/SectionsEditor"; import LockBanner from "../components/document/LockBanner"; +import CustomFieldsPrintPicker from "../components/document/CustomFieldsPrintPicker"; import { useDocumentLock } from "../hooks/useDocumentLock"; import { useUnsavedChangesGuard } from "../hooks/useUnsavedChangesGuard"; import { useDocumentPdf } from "../hooks/useDocumentPdf"; @@ -114,6 +115,7 @@ interface OfferSavePayload { language: string; items: OfferItemPayload[]; sections: OfferSectionPayload[]; + selected_custom_fields: number[]; status?: string; } @@ -178,6 +180,9 @@ export default function OfferDetail() { emptyDocumentItem(), ]); const [sections, setSections] = useState([]); + const [selectedCustomFields, setSelectedCustomFields] = useState( + [], + ); const [orderInfo, setOrderInfo] = useState(null); const [offerStatus, setOfferStatus] = useState(""); const [quotationNumber, setQuotationNumber] = useState(""); @@ -318,6 +323,7 @@ export default function OfferDetail() { })) : []; setSections(mappedSections); + setSelectedCustomFields(d.selected_custom_fields ?? []); markClean({ form: formData, items: mappedItems, sections: mappedSections }); setOfferStatus(d.status || ""); setOrderInfo(d.order ?? null); @@ -445,6 +451,7 @@ export default function OfferDetail() { content: s.content, position: i, })), + selected_custom_fields: selectedCustomFields, }; // Only set status when a target is given (create-as-draft / create-as-live // / finalize). Editing a live offer sends no status — the backend keeps @@ -929,6 +936,15 @@ export default function OfferDetail() { /> + + + + {/* Items (drag-and-drop, offers carry the "V ceně" column) */}