feat(documents): per-document custom-field print picker on offer/issued-order/invoice detail
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import RichEditor from "../components/RichEditor";
|
|||||||
import SectionsEditor, {
|
import SectionsEditor, {
|
||||||
type DocumentSection,
|
type DocumentSection,
|
||||||
} from "../components/document/SectionsEditor";
|
} from "../components/document/SectionsEditor";
|
||||||
|
import CustomFieldsPrintPicker from "../components/document/CustomFieldsPrintPicker";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -611,6 +612,9 @@ export default function InvoiceDetail() {
|
|||||||
// Rich-text CZ/EN "Obsah" sections (printed after the items on the PDF —
|
// Rich-text CZ/EN "Obsah" sections (printed after the items on the PDF —
|
||||||
// shared editor with offers/issued orders).
|
// shared editor with offers/issued orders).
|
||||||
const [sections, setSections] = useState<DocumentSection[]>([]);
|
const [sections, setSections] = useState<DocumentSection[]>([]);
|
||||||
|
const [selectedCustomFields, setSelectedCustomFields] = useState<number[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const [items, setItems] = useState<InvoiceItem[]>(() => [
|
const [items, setItems] = useState<InvoiceItem[]>(() => [
|
||||||
{
|
{
|
||||||
_key: "inv-1",
|
_key: "inv-1",
|
||||||
@@ -821,11 +825,15 @@ export default function InvoiceDetail() {
|
|||||||
: [];
|
: [];
|
||||||
setSections(mappedSections);
|
setSections(mappedSections);
|
||||||
|
|
||||||
|
const mappedCustomFields = inv.selected_custom_fields ?? [];
|
||||||
|
setSelectedCustomFields(mappedCustomFields);
|
||||||
|
|
||||||
// Capture initial snapshot for dirty-checking
|
// Capture initial snapshot for dirty-checking
|
||||||
initialSnapshotRef.current = JSON.stringify({
|
initialSnapshotRef.current = JSON.stringify({
|
||||||
form: formData,
|
form: formData,
|
||||||
items: mappedItems,
|
items: mappedItems,
|
||||||
sections: mappedSections,
|
sections: mappedSections,
|
||||||
|
selectedCustomFields: mappedCustomFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
setDataReady(true);
|
setDataReady(true);
|
||||||
@@ -923,15 +931,21 @@ export default function InvoiceDetail() {
|
|||||||
// Edit mode: captured inside the sync effect from raw query data.
|
// Edit mode: captured inside the sync effect from raw query data.
|
||||||
// Create mode: captured on the first render after sync effects populate the form.
|
// Create mode: captured on the first render after sync effects populate the form.
|
||||||
if (dataReady && !initialSnapshotRef.current) {
|
if (dataReady && !initialSnapshotRef.current) {
|
||||||
initialSnapshotRef.current = JSON.stringify({ form, items, sections });
|
initialSnapshotRef.current = JSON.stringify({
|
||||||
|
form,
|
||||||
|
items,
|
||||||
|
sections,
|
||||||
|
selectedCustomFields,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirty = useMemo(() => {
|
const isDirty = useMemo(() => {
|
||||||
if (!initialSnapshotRef.current) return false;
|
if (!initialSnapshotRef.current) return false;
|
||||||
return (
|
return (
|
||||||
JSON.stringify({ form, items, sections }) !== initialSnapshotRef.current
|
JSON.stringify({ form, items, sections, selectedCustomFields }) !==
|
||||||
|
initialSnapshotRef.current
|
||||||
);
|
);
|
||||||
}, [form, items, sections]);
|
}, [form, items, sections, selectedCustomFields]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDirty) return;
|
if (!isDirty) return;
|
||||||
@@ -1095,6 +1109,7 @@ export default function InvoiceDetail() {
|
|||||||
content: s.content,
|
content: s.content,
|
||||||
position: i,
|
position: i,
|
||||||
})),
|
})),
|
||||||
|
selected_custom_fields: selectedCustomFields,
|
||||||
};
|
};
|
||||||
// Only set status when a target is given (create-as-draft / create-as-live
|
// 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
|
// / 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);
|
const data = await saveMutation.mutateAsync(payload);
|
||||||
alert.success(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena");
|
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) {
|
if (!isEdit) {
|
||||||
navigate(`/invoices/${data.invoice_id}`);
|
navigate(`/invoices/${data.invoice_id}`);
|
||||||
}
|
}
|
||||||
@@ -2209,6 +2229,15 @@ export default function InvoiceDetail() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CustomFieldsPrintPicker
|
||||||
|
fields={companySettings?.custom_fields ?? []}
|
||||||
|
selected={selectedCustomFields}
|
||||||
|
disabled={false}
|
||||||
|
onChange={setSelectedCustomFields}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Items */}
|
{/* Items */}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import SectionsEditor, {
|
|||||||
type DocumentSection,
|
type DocumentSection,
|
||||||
} from "../components/document/SectionsEditor";
|
} from "../components/document/SectionsEditor";
|
||||||
import LockBanner from "../components/document/LockBanner";
|
import LockBanner from "../components/document/LockBanner";
|
||||||
|
import CustomFieldsPrintPicker from "../components/document/CustomFieldsPrintPicker";
|
||||||
import { useDocumentLock } from "../hooks/useDocumentLock";
|
import { useDocumentLock } from "../hooks/useDocumentLock";
|
||||||
import { useUnsavedChangesGuard } from "../hooks/useUnsavedChangesGuard";
|
import { useUnsavedChangesGuard } from "../hooks/useUnsavedChangesGuard";
|
||||||
import { useDocumentPdf } from "../hooks/useDocumentPdf";
|
import { useDocumentPdf } from "../hooks/useDocumentPdf";
|
||||||
@@ -137,6 +138,7 @@ interface IssuedOrderSavePayload {
|
|||||||
internal_notes: string;
|
internal_notes: string;
|
||||||
items: IssuedOrderItemPayload[];
|
items: IssuedOrderItemPayload[];
|
||||||
sections: IssuedOrderSectionPayload[];
|
sections: IssuedOrderSectionPayload[];
|
||||||
|
selected_custom_fields: number[];
|
||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +166,9 @@ export default function IssuedOrderDetail() {
|
|||||||
emptyDocumentItem(),
|
emptyDocumentItem(),
|
||||||
]);
|
]);
|
||||||
const [sections, setSections] = useState<DocumentSection[]>([]);
|
const [sections, setSections] = useState<DocumentSection[]>([]);
|
||||||
|
const [selectedCustomFields, setSelectedCustomFields] = useState<number[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
// Which save action is in flight ("draft" | the live status | "save"), so
|
// Which save action is in flight ("draft" | the live status | "save"), so
|
||||||
@@ -308,6 +313,7 @@ export default function IssuedOrderDetail() {
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
setSections(mappedSections);
|
setSections(mappedSections);
|
||||||
|
setSelectedCustomFields(d.selected_custom_fields ?? []);
|
||||||
|
|
||||||
setLockedBy(d.locked_by ?? null);
|
setLockedBy(d.locked_by ?? null);
|
||||||
// Acquire the edit lock when nobody holds it, the order is still
|
// Acquire the edit lock when nobody holds it, the order is still
|
||||||
@@ -430,6 +436,7 @@ export default function IssuedOrderDetail() {
|
|||||||
content: s.content,
|
content: s.content,
|
||||||
position: i,
|
position: i,
|
||||||
})),
|
})),
|
||||||
|
selected_custom_fields: selectedCustomFields,
|
||||||
};
|
};
|
||||||
// Only set status when a target is given (create-as-draft / create-as-live
|
// 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
|
// / 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í)"
|
placeholder="Objednáváme si u Vás: (ponechte prázdné pro výchozí)"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CustomFieldsPrintPicker
|
||||||
|
fields={companySettings?.custom_fields ?? []}
|
||||||
|
selected={selectedCustomFields}
|
||||||
|
disabled={!editable}
|
||||||
|
onChange={setSelectedCustomFields}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Items */}
|
{/* Items */}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import SectionsEditor, {
|
|||||||
type DocumentSection,
|
type DocumentSection,
|
||||||
} from "../components/document/SectionsEditor";
|
} from "../components/document/SectionsEditor";
|
||||||
import LockBanner from "../components/document/LockBanner";
|
import LockBanner from "../components/document/LockBanner";
|
||||||
|
import CustomFieldsPrintPicker from "../components/document/CustomFieldsPrintPicker";
|
||||||
import { useDocumentLock } from "../hooks/useDocumentLock";
|
import { useDocumentLock } from "../hooks/useDocumentLock";
|
||||||
import { useUnsavedChangesGuard } from "../hooks/useUnsavedChangesGuard";
|
import { useUnsavedChangesGuard } from "../hooks/useUnsavedChangesGuard";
|
||||||
import { useDocumentPdf } from "../hooks/useDocumentPdf";
|
import { useDocumentPdf } from "../hooks/useDocumentPdf";
|
||||||
@@ -114,6 +115,7 @@ interface OfferSavePayload {
|
|||||||
language: string;
|
language: string;
|
||||||
items: OfferItemPayload[];
|
items: OfferItemPayload[];
|
||||||
sections: OfferSectionPayload[];
|
sections: OfferSectionPayload[];
|
||||||
|
selected_custom_fields: number[];
|
||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +180,9 @@ export default function OfferDetail() {
|
|||||||
emptyDocumentItem(),
|
emptyDocumentItem(),
|
||||||
]);
|
]);
|
||||||
const [sections, setSections] = useState<DocumentSection[]>([]);
|
const [sections, setSections] = useState<DocumentSection[]>([]);
|
||||||
|
const [selectedCustomFields, setSelectedCustomFields] = useState<number[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const [orderInfo, setOrderInfo] = useState<OfferOrderInfo | null>(null);
|
const [orderInfo, setOrderInfo] = useState<OfferOrderInfo | null>(null);
|
||||||
const [offerStatus, setOfferStatus] = useState<string>("");
|
const [offerStatus, setOfferStatus] = useState<string>("");
|
||||||
const [quotationNumber, setQuotationNumber] = useState("");
|
const [quotationNumber, setQuotationNumber] = useState("");
|
||||||
@@ -318,6 +323,7 @@ export default function OfferDetail() {
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
setSections(mappedSections);
|
setSections(mappedSections);
|
||||||
|
setSelectedCustomFields(d.selected_custom_fields ?? []);
|
||||||
markClean({ form: formData, items: mappedItems, sections: mappedSections });
|
markClean({ form: formData, items: mappedItems, sections: mappedSections });
|
||||||
setOfferStatus(d.status || "");
|
setOfferStatus(d.status || "");
|
||||||
setOrderInfo(d.order ?? null);
|
setOrderInfo(d.order ?? null);
|
||||||
@@ -445,6 +451,7 @@ export default function OfferDetail() {
|
|||||||
content: s.content,
|
content: s.content,
|
||||||
position: i,
|
position: i,
|
||||||
})),
|
})),
|
||||||
|
selected_custom_fields: selectedCustomFields,
|
||||||
};
|
};
|
||||||
// Only set status when a target is given (create-as-draft / create-as-live
|
// 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
|
// / finalize). Editing a live offer sends no status — the backend keeps
|
||||||
@@ -929,6 +936,15 @@ export default function OfferDetail() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<CustomFieldsPrintPicker
|
||||||
|
fields={companySettings?.custom_fields ?? []}
|
||||||
|
selected={selectedCustomFields}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={setSelectedCustomFields}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Items (drag-and-drop, offers carry the "V ceně" column) */}
|
{/* Items (drag-and-drop, offers carry the "V ceně" column) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user