fix(ui): guard delete on paid invoices / ordered-or-locked offers; add guarded delete to issued orders
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1211,15 +1211,19 @@ export default function InvoiceDetail() {
|
|||||||
Zobrazit fakturu
|
Zobrazit fakturu
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{hasPermission("invoices.delete") && (
|
{/* A paid invoice is a settled financial record — never deletable
|
||||||
<Button
|
from the UI. (Backend rejection of deleting a paid invoice is a
|
||||||
onClick={() => setDeleteConfirm(true)}
|
separate hardening, out of scope here.) */}
|
||||||
variant="outlined"
|
{hasPermission("invoices.delete") &&
|
||||||
color="error"
|
invoice.status !== "paid" && (
|
||||||
>
|
<Button
|
||||||
Smazat
|
onClick={() => setDeleteConfirm(true)}
|
||||||
</Button>
|
variant="outlined"
|
||||||
)}
|
color="error"
|
||||||
|
>
|
||||||
|
Smazat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -1778,15 +1782,20 @@ export default function InvoiceDetail() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
{hasPermission("invoices.delete") && (
|
{/* A paid invoice is a settled financial record — never
|
||||||
<Button
|
deletable from the UI. A paid invoice never actually reaches
|
||||||
onClick={() => setDeleteConfirm(true)}
|
this branch (it renders the read-only view above), but the
|
||||||
variant="outlined"
|
guard documents the invariant explicitly. */}
|
||||||
color="error"
|
{hasPermission("invoices.delete") &&
|
||||||
>
|
invoice.status !== "paid" && (
|
||||||
Smazat
|
<Button
|
||||||
</Button>
|
onClick={() => setDeleteConfirm(true)}
|
||||||
)}
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
Smazat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -540,6 +540,8 @@ export default function IssuedOrderDetail() {
|
|||||||
status: string | null;
|
status: string | null;
|
||||||
}>({ show: false, status: null });
|
}>({ show: false, status: null });
|
||||||
const [pdfLoading, setPdfLoading] = useState(false);
|
const [pdfLoading, setPdfLoading] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
// ─── Queries ───
|
// ─── Queries ───
|
||||||
const customersQuery = useQuery(offerCustomersOptions());
|
const customersQuery = useQuery(offerCustomersOptions());
|
||||||
@@ -654,6 +656,12 @@ export default function IssuedOrderDetail() {
|
|||||||
invalidate: ["issued-orders"],
|
invalidate: ["issued-orders"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useApiMutation<void, unknown>({
|
||||||
|
url: () => `${API_BASE}/issued-orders/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["issued-orders"],
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Item handlers ───
|
// ─── Item handlers ───
|
||||||
const updateItem = (
|
const updateItem = (
|
||||||
index: number,
|
index: number,
|
||||||
@@ -768,6 +776,21 @@ export default function IssuedOrderDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Delete ───
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteMutation.mutateAsync(undefined);
|
||||||
|
alert.success("Objednávka byla smazána");
|
||||||
|
navigate("/orders?tab=vydane");
|
||||||
|
} catch (err) {
|
||||||
|
alert.error(err instanceof Error ? err.message : "Chyba připojení");
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
setDeleteConfirm(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ─── PDF export ───
|
// ─── PDF export ───
|
||||||
const handleViewPdf = async () => {
|
const handleViewPdf = async () => {
|
||||||
const newWindow = window.open("", "_blank");
|
const newWindow = window.open("", "_blank");
|
||||||
@@ -901,6 +924,21 @@ export default function IssuedOrderDetail() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
{/* A completed order is terminal/finalized — never deletable from the
|
||||||
|
UI. draft/sent/confirmed/cancelled may be deleted. (Backend
|
||||||
|
rejection of deleting an order is a separate hardening, out of
|
||||||
|
scope here.) */}
|
||||||
|
{isEdit &&
|
||||||
|
hasPermission("orders.delete") &&
|
||||||
|
form.status !== "completed" && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setDeleteConfirm(true)}
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
Smazat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -1280,6 +1318,21 @@ export default function IssuedOrderDetail() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete confirm */}
|
||||||
|
{isEdit && (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={deleteConfirm}
|
||||||
|
onClose={() => setDeleteConfirm(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
title="Smazat objednávku?"
|
||||||
|
message={`Opravdu chcete smazat objednávku "${poNumber}"? Tato akce je nevratná.`}
|
||||||
|
confirmText="Smazat"
|
||||||
|
cancelText="Zrušit"
|
||||||
|
confirmVariant="danger"
|
||||||
|
loading={deleting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageEnter>
|
</PageEnter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -663,6 +663,11 @@ export default function OfferDetail() {
|
|||||||
const isLockedByOther = !!lockedBy;
|
const isLockedByOther = !!lockedBy;
|
||||||
const readOnly = isInvalidated || isLockedByOther || isCompleted;
|
const readOnly = isInvalidated || isLockedByOther || isCompleted;
|
||||||
const canInvalidate = isEdit && !isInvalidated && !isCompleted && !orderInfo;
|
const canInvalidate = isEdit && !isInvalidated && !isCompleted && !orderInfo;
|
||||||
|
// An offer is deletable only when it has NOT spawned an order (orderInfo —
|
||||||
|
// deleting would orphan the linked order), is NOT invalidated, and is NOT
|
||||||
|
// locked by another user. (Backend rejection of deleting an ordered offer is
|
||||||
|
// a separate hardening, out of scope here.)
|
||||||
|
const canDelete = isEdit && !orderInfo && !isInvalidated && !isLockedByOther;
|
||||||
|
|
||||||
// Sync offer detail data to form state on first load (edit mode)
|
// Sync offer detail data to form state on first load (edit mode)
|
||||||
const formInitializedRef = useRef(false);
|
const formInitializedRef = useRef(false);
|
||||||
@@ -1190,7 +1195,7 @@ export default function OfferDetail() {
|
|||||||
{saving ? "Ukládání..." : "Uložit"}
|
{saving ? "Ukládání..." : "Uložit"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isEdit && hasPermission("offers.delete") && (
|
{canDelete && hasPermission("offers.delete") && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setDeleteConfirm(true)}
|
onClick={() => setDeleteConfirm(true)}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
Reference in New Issue
Block a user