feat(issued-orders): make line items optional (issue by sections alone)
Items are no longer required on issued orders — an order can be issued purely from its rich-text sections (Obsah). - DocumentItemsEditor: opt-in allowEmpty prop lets the list go to zero rows (default false keeps the offers/invoices at-least-one-item rule). - IssuedOrderDetail: allowEmpty on the editor; a saved order with no items reopens empty (not a blank starter row); submit guard now requires at least one item OR a non-empty section, not an item. - PDF: with no items the billing heading, items table and total row are omitted entirely — a sections-only order shows only its Obsah. - Service: finalize (draft->sent) and create-as-non-draft reject a completely blank order (no items AND no sections) with a Czech 400 (empty_document); drafts may still be saved empty as WIP. - Tests: sections-only finalize + blank-order rejection + sections-only PDF render; existing finalize fixtures given minimal content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -492,6 +492,12 @@ interface DocumentItemsEditorProps {
|
||||
itemDescriptionMaxLength?: number;
|
||||
/** Optional page-specific control (e.g. item-template Select) next to the add button. */
|
||||
templatesSlot?: ReactNode;
|
||||
/**
|
||||
* Allow removing every row so the list can be empty (issued orders may be
|
||||
* issued by sections alone). Default false keeps the offers/invoices rule of
|
||||
* at least one item.
|
||||
*/
|
||||
allowEmpty?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,6 +515,7 @@ export default function DocumentItemsEditor({
|
||||
showDiscount = false,
|
||||
itemDescriptionMaxLength = 5000,
|
||||
templatesSlot,
|
||||
allowEmpty = false,
|
||||
}: DocumentItemsEditorProps) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
@@ -538,10 +545,13 @@ export default function DocumentItemsEditor({
|
||||
const addItem = () => onChange([...items, emptyDocumentItem()]);
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (items.length <= 1) return;
|
||||
if (!allowEmpty && items.length <= 1) return;
|
||||
onChange(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// When allowEmpty, even the last row is deletable (list may go to zero).
|
||||
const canDeleteRow = allowEmpty || items.length > 1;
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
@@ -608,7 +618,7 @@ export default function DocumentItemsEditor({
|
||||
index={index}
|
||||
currency={currency}
|
||||
readOnly={readOnly}
|
||||
canDelete={items.length > 1}
|
||||
canDelete={canDeleteRow}
|
||||
showIncludedInTotal={showIncludedInTotal}
|
||||
showDiscount={showDiscount}
|
||||
itemDescriptionMaxLength={itemDescriptionMaxLength}
|
||||
@@ -666,7 +676,7 @@ export default function DocumentItemsEditor({
|
||||
index={index}
|
||||
currency={currency}
|
||||
readOnly={readOnly}
|
||||
canDelete={items.length > 1}
|
||||
canDelete={canDeleteRow}
|
||||
showIncludedInTotal={showIncludedInTotal}
|
||||
showDiscount={showDiscount}
|
||||
itemDescriptionMaxLength={itemDescriptionMaxLength}
|
||||
|
||||
Reference in New Issue
Block a user