CodingNic

Categories and Summary

Delete Categories with the Same Custom Confirmation

Categories and Summary 22 min read

Delete Categories with the Same Custom Confirmation

Open:

text
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:

javascript
const inUse = category.count > 0;

Disable the delete control when a category is in use and explain the reason through its title:

jsx
<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:

javascript
const [confirmDeleteName, setConfirmDeleteName] = useState(null);

function requestDelete(name) {
  setConfirmDeleteName(name);
}

Render the same ConfirmDialog used by transactions:

jsx
<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:

javascript
const inUse = db.transactions.some(
  (transaction) =>
    transaction.type === "expense" &&
    transaction.category === name
);

When a match exists, return a conflict response:

javascript
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.