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

@@ -1,6 +1,46 @@
import { useQuery } from "@tanstack/react-query";
import { fetchCategories } from "@/api/categories";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createCategory, deleteCategory, fetchCategories, updateCategory } from "@/api/categories";
import type { CategoryInput, CategoryUpdateInput } from "@/types";
export function useCategories() {
return useQuery({ queryKey: ["categories"], queryFn: fetchCategories });
}
// Category name/color/icon are denormalized into several read models
// (transactions, dashboard, reports, budgets, recurring templates) — any
// create/update/delete has to refresh all of them, same set an account
// mutation invalidates in useAccounts.ts.
function invalidateCategoryConsumers(queryClient: ReturnType<typeof useQueryClient>) {
queryClient.invalidateQueries({ queryKey: ["categories"] });
queryClient.invalidateQueries({ queryKey: ["transactions"] });
queryClient.invalidateQueries({ queryKey: ["dashboard-summary"] });
queryClient.invalidateQueries({ queryKey: ["category-spending-report"] });
queryClient.invalidateQueries({ queryKey: ["category-ranking"] });
queryClient.invalidateQueries({ queryKey: ["budgets"] });
queryClient.invalidateQueries({ queryKey: ["budget-status"] });
queryClient.invalidateQueries({ queryKey: ["recurring"] });
}
export function useCreateCategory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CategoryInput) => createCategory(input),
onSuccess: () => invalidateCategoryConsumers(queryClient),
});
}
export function useUpdateCategory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, input }: { id: number; input: CategoryUpdateInput }) => updateCategory(id, input),
onSuccess: () => invalidateCategoryConsumers(queryClient),
});
}
export function useDeleteCategory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteCategory(id),
onSuccess: () => invalidateCategoryConsumers(queryClient),
});
}