Delete Categories with the Same Custom Confirmation
Open:
client/src/pages/CategoriesPage.jsx
client/src/components/ConfirmDialog.jsx
server/src/routes/categories.js
A category is shared across every month, so deleting it requires one important rule: an expense must not be left pointing at a category that no longer exists.
The page already calculates a category’s transaction count. Use that value to explain when deletion is not available:
const inUse = category.count > 0;
Disable the delete control when a category is in use and explain the reason through its title:
<button
type="button"
disabled={inUse}
title={
inUse
? "In use — remove its entries first"
: "Delete category"
}
>
<Trash2 size={14} />
</button>
For an unused category, the button should only request confirmation:
const [confirmDeleteName, setConfirmDeleteName] = useState(null);
function requestDelete(name) {
setConfirmDeleteName(name);
}
Render the same ConfirmDialog used by transactions:
<ConfirmDialog
open={Boolean(confirmDeleteName)}
title={`Delete “${confirmDeleteName}” category?`}
description="This removes the category from the shared category list. No expense entries currently use it."
confirmLabel="Delete category"
cancelLabel="Keep category"
onCancel={() => setConfirmDeleteName(null)}
onConfirm={() =>
confirmDeleteName && handleDelete(confirmDeleteName)
}
/>
The server must enforce the same dependency rule because a user could call the API without using this button.
Before removing the category, search the transaction collection for an expense that uses it:
const inUse = db.transactions.some(
(transaction) =>
transaction.type === "expense" &&
transaction.category === name
);
When a match exists, return a conflict response:
if (inUse) {
const conflict = new Error(
"Category is in use — remove its entries first"
);
conflict.statusCode = 409;
throw conflict;
}
The UI rule makes the normal path clear; the server rule guarantees the data remains valid even when the API is called directly.
Test it
Try to delete Housing while the seeded rent transaction still uses it. The UI should explain that the category is in use.
Create a new unused category, open its confirmation dialog, choose Keep category, and make sure it remains.
Then confirm deletion and refresh the page. The unused category should be gone.
Checkpoint
Category deletion now follows the same interaction pattern as transaction deletion, while both the UI and server enforce the dependency rule.