feat: allow deleting default categories once they have no transactions

Раньше стандартные категории нельзя было удалить вообще. Теперь можно —
если у категории нет ни одной обычной транзакции, ни строки сплита с
ней. Пользовательские категории удаляются как и раньше, без проверок.

- delete_category() проверяет Transaction.category_id и
  TransactionSplit.category_id для стандартных категорий перед
  удалением.
- Кнопка удаления в списке категорий больше не блокируется заранее для
  стандартных — бэкенд решает, бейдж «стандартная» остался чисто
  информационным.
- При отказе (400) показывается понятное сообщение вместо тихого
  игнорирования клика.
This commit is contained in:
ZProger
2026-08-30 19:49:21 +03:00
parent dbae7938da
commit da1c7b5d63
5 changed files with 99 additions and 15 deletions

View File

@@ -49,10 +49,8 @@ function CategoryRow({ category, indented, onEdit, onDelete }: RowProps) {
<button
type="button"
aria-label={t("common.delete")}
title={category.is_default ? t("category.defaultCannotDelete") : undefined}
disabled={category.is_default}
onClick={() => onDelete(category)}
className="rounded-md p-1.5 text-text-muted hover:bg-surface-2 hover:text-danger disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-text-muted"
className="rounded-md p-1.5 text-text-muted hover:bg-surface-2 hover:text-danger"
>
<Trash2 size={15} />
</button>

View File

@@ -428,7 +428,7 @@ const ru = {
"category.incomeSectionTitle": "Категории доходов",
"category.empty": "Категорий пока нет.",
"category.defaultBadge": "стандартная",
"category.defaultCannotDelete": "Стандартные категории нельзя удалить",
"category.defaultDeleteBlocked": "Стандартную категорию можно удалить только после того, как у неё не останется транзакций.",
"category.confirmDelete": "Удалить категорию «{{name}}»?\n\nЕё транзакции останутся без категории.",
"category.kind.expense": "Расход",
"category.kind.income": "Доход",
@@ -910,7 +910,7 @@ const en: Record<keyof typeof ru, string> = {
"category.incomeSectionTitle": "Income categories",
"category.empty": "No categories yet.",
"category.defaultBadge": "default",
"category.defaultCannotDelete": "Default categories cannot be deleted",
"category.defaultDeleteBlocked": "A default category can only be deleted once it has no transactions left.",
"category.confirmDelete": "Delete the category “{{name}}”?\n\nIts transactions will be left uncategorized.",
"category.kind.expense": "Expense",
"category.kind.income": "Income",

View File

@@ -38,12 +38,15 @@ export function CategoriesPage() {
setModalOpen(true);
}
function handleDelete(category: Category) {
// Default categories are protected server-side too (400) — the list
// already disables the delete button for them, this is just a guard.
if (category.is_default) return;
if (window.confirm(t("category.confirmDelete", { name: category.name }))) {
deleteCategory.mutate(category.id);
async function handleDelete(category: Category) {
if (!window.confirm(t("category.confirmDelete", { name: category.name }))) return;
try {
await deleteCategory.mutateAsync(category.id);
} catch {
// The only way a delete 400s is a default category that still has
// transactions pointing at it (api/routes/categories.py) — a custom
// category has no such guard and always succeeds.
window.alert(t("category.defaultDeleteBlocked"));
}
}