feat: add category management page (create/edit/delete)

The backend already had full category CRUD but nothing in the UI exposed
it — categories could only be viewed inside charts. Adds a Categories nav
item and page with icon/color pickers, grouped by expense/income, backed
by new create/update/delete mutations. Also expands icons.tsx's ICON_MAP,
which was missing 14 icon keys already in use by real category data and
silently falling back to a wallet glyph everywhere they render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ZProger
2026-08-24 23:43:06 +03:00
parent 6cc532378b
commit 9f5ea00227
13 changed files with 512 additions and 5 deletions

View File

@@ -0,0 +1,88 @@
import { useState } from "react";
import { Plus } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { CategoryFormModal } from "@/components/categories/CategoryFormModal";
import { CategoryList } from "@/components/categories/CategoryList";
import { useCategories, useDeleteCategory } from "@/hooks/useCategories";
import { useTranslation } from "@/lib/i18n";
import type { Category, CategoryKind } from "@/types";
export function CategoriesPage() {
const { t } = useTranslation();
const { data: categories, isLoading } = useCategories();
const deleteCategory = useDeleteCategory();
const [modalOpen, setModalOpen] = useState(false);
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [modalKind, setModalKind] = useState<CategoryKind>("expense");
function openCreateModal(kind: CategoryKind) {
setEditingCategory(null);
setModalKind(kind);
setModalOpen(true);
}
function openEditModal(category: Category) {
setEditingCategory(category);
setModalKind(category.kind);
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);
}
}
const expenseCategories = (categories ?? []).filter((category) => category.kind === "expense");
const incomeCategories = (categories ?? []).filter((category) => category.kind === "income");
return (
<div className="space-y-5">
<Card>
<CardHeader>
<CardTitle>{t("category.expenseSectionTitle")}</CardTitle>
<Button onClick={() => openCreateModal("expense")}>
<Plus size={16} />
{t("common.add")}
</Button>
</CardHeader>
<CardContent>
{isLoading ? (
<p className="py-10 text-center text-sm text-text-muted">{t("common.loading")}</p>
) : (
<CategoryList items={expenseCategories} onEdit={openEditModal} onDelete={handleDelete} />
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t("category.incomeSectionTitle")}</CardTitle>
<Button onClick={() => openCreateModal("income")}>
<Plus size={16} />
{t("common.add")}
</Button>
</CardHeader>
<CardContent>
{isLoading ? (
<p className="py-10 text-center text-sm text-text-muted">{t("common.loading")}</p>
) : (
<CategoryList items={incomeCategories} onEdit={openEditModal} onDelete={handleDelete} />
)}
</CardContent>
</Card>
<CategoryFormModal
open={modalOpen}
onClose={() => setModalOpen(false)}
category={editingCategory}
defaultKind={modalKind}
/>
</div>
);
}