diff --git a/client/api.ts b/client/api.ts index fbd82de..d35a734 100644 --- a/client/api.ts +++ b/client/api.ts @@ -1,4 +1,4 @@ -import type { Bill, Payment, TrackerResponse } from '@/types'; +import type { Bill, Category, Payment, TrackerResponse } from '@/types'; // Fetch CSRF token from the server once and cache in memory. // The cookie is httpOnly so document.cookie cannot access it directly. @@ -332,10 +332,10 @@ export const api = { abandonSnowballPlan: (id: Id) => post(`/snowball/plans/${id}/abandon`, {}), // Categories - categories: () => get('/categories'), - createCategory: (data: Body) => post('/categories', data), + categories: () => get('/categories'), + createCategory: (data: Body) => post('/categories', data), reorderCategories: (order: Body) => put('/categories/reorder', order), - updateCategory: (id: Id, data: Body) => put(`/categories/${id}`, data), + updateCategory: (id: Id, data: Body) => put(`/categories/${id}`, data), toggleCategorySpending: (id: Id, val: unknown) => patch(`/categories/${id}/spending`, { spending_enabled: val }), deleteCategory: (id: Id) => del(`/categories/${id}`), restoreCategory: (id: Id) => post(`/categories/${id}/restore`), diff --git a/client/hooks/useQueries.ts b/client/hooks/useQueries.ts index 31c713f..ee71991 100644 --- a/client/hooks/useQueries.ts +++ b/client/hooks/useQueries.ts @@ -184,8 +184,8 @@ export function useSpendingCategories() { return useQuery({ queryKey: ['spending-categories'], queryFn: async () => { - const d = await api.categories() as any; - return (d.categories || d || []).filter((c: any) => !c.deleted_at && c.spending_enabled); + const cats = await api.categories(); + return cats.filter(c => !c.deleted_at && c.spending_enabled); }, staleTime: 1000 * 60 * 5, }); diff --git a/client/types.ts b/client/types.ts index 8a035e4..cf8a0df 100644 --- a/client/types.ts +++ b/client/types.ts @@ -30,6 +30,39 @@ export interface Payment { [key: string]: unknown; } +// A category's per-bill rollup (categories list endpoint). `total_paid` here is a +// raw SQL sum (not run through fromCents), so it is deliberately NOT branded. +export interface CategoryBillSummary { + id: number; + name: string; + active: boolean; + payment_count: number; + total_paid: number; + last_paid_date: string | null; + [key: string]: unknown; +} + +// A spending/bill category. Has no money field of its own (budgets live on the +// spending endpoints). The list endpoint adds bill rollups; create/update return +// the bare row — so the rollup fields are optional. +export interface Category { + id: number; + user_id?: number; + name: string; + sort_order?: number; + spending_enabled: boolean; + group_id: number | null; + created_at?: string; + updated_at?: string; + bill_count?: number; + active_bill_count?: number; + inactive_bill_count?: number; + payment_count?: number; + bill_names?: string[]; + bills?: CategoryBillSummary[]; + [key: string]: unknown; +} + // A bill as serialized by the server (billsService.serializeBill spreads the DB // row and converts the three money columns to dollars). SQLite booleans come back // as 0/1 integers. Extra joined columns (sparkline, has_merchant_rule, …) pass