merge: Phase 5 (Rules Engine) + Phase 6 (Budget & Analytics)

Resolve additive conflicts in schema.prisma and hooks.ts — both models and all hooks retained.
This commit is contained in:
2026-03-08 17:09:57 +11:00
6 changed files with 626 additions and 3 deletions
+69
View File
@@ -431,3 +431,72 @@ export function useApplyRules() {
},
});
}
// --- Budgets & Analytics ---
export interface BudgetRow {
id: number;
category: string;
month: string;
amount_limit: number;
}
export interface MonthlyAnalyticsRow {
category: string;
spent: Record<string, number>;
budget: Record<string, number>;
txCount: Record<string, number>;
}
export interface MonthlyAnalytics {
months: string[];
rows: MonthlyAnalyticsRow[];
totals: Record<string, { spent: number; budget: number }>;
}
export function useBudgets(month: string) {
return useQuery<BudgetRow[]>({
queryKey: ["budgets", month],
queryFn: async () => {
const res = await fetch(`/api/budgets?month=${month}`);
return res.json();
},
});
}
export function useUpsertBudget() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (data: { category: string; month: string; amount_limit: number }) => {
const res = await fetch("/api/budgets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Failed to save budget");
return res.json();
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["budgets"] }),
});
}
export function useDeleteBudget() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: number) => {
await fetch(`/api/budgets/${id}`, { method: "DELETE" });
},
onSuccess: () => qc.invalidateQueries({ queryKey: ["budgets"] }),
});
}
export function useMonthlyAnalytics(months?: number) {
const m = months || 6;
return useQuery<MonthlyAnalytics>({
queryKey: ["analytics", "monthly", m],
queryFn: async () => {
const res = await fetch(`/api/analytics/monthly?months=${m}`);
return res.json();
},
});
}