mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 11:06:40 +07:00
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>
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|