feat(ts): type the shared Category API response (A4)
Adds Category (+ CategoryBillSummary) to @/types and wires categories/ createCategory/updateCategory. Category has no money field of its own (budgets live on the spending endpoints); the nested per-bill total_paid is a raw SQL sum, so it's deliberately left unbranded. With categories now typed, useSpending Categories drops its `any` cast and dead d.categories branch. Category, Bill, Payment, and the Tracker envelope are the cross-cutting types used by many components — worth defining upfront. The remaining single-consumer page responses (summary, analytics, spending, snowball, subscriptions, bank ledger) are typed alongside their page conversions in phase B, where the exact fields consumed are visible (more accurate than a speculative interface). typecheck 0, build green, 48 client tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
337ad95a3e
commit
4af738f947
|
|
@ -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<Category[]>('/categories'),
|
||||
createCategory: (data: Body) => post<Category>('/categories', data),
|
||||
reorderCategories: (order: Body) => put('/categories/reorder', order),
|
||||
updateCategory: (id: Id, data: Body) => put(`/categories/${id}`, data),
|
||||
updateCategory: (id: Id, data: Body) => put<Category>(`/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`),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue