@pfm-platform/budgets-data-access 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2,11 +2,6 @@
2
2
 
3
3
  var reactQuery = require('@tanstack/react-query');
4
4
  var shared = require('@pfm-platform/shared');
5
- var axios = require('axios');
6
-
7
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
-
9
- var axios__default = /*#__PURE__*/_interopDefault(axios);
10
5
 
11
6
  // src/queries/useBudgets.ts
12
7
 
@@ -18,33 +13,6 @@ var budgetKeys = {
18
13
  details: () => [...budgetKeys.all, "detail"],
19
14
  detail: (userId, budgetId) => [...budgetKeys.details(), userId, budgetId]
20
15
  };
21
- var api = axios__default.default.create({
22
- baseURL: "/api",
23
- headers: {
24
- "Content-Type": "application/json"
25
- },
26
- timeout: 1e4
27
- });
28
- api.interceptors.request.use(
29
- (config) => {
30
- const token = localStorage.getItem("auth_token");
31
- if (token) {
32
- config.headers.Authorization = `Bearer ${token}`;
33
- }
34
- return config;
35
- },
36
- (error) => Promise.reject(error)
37
- );
38
- api.interceptors.response.use(
39
- (response) => response,
40
- (error) => {
41
- if (error.response?.status === 401) {
42
- localStorage.removeItem("auth_token");
43
- window.location.href = "/login";
44
- }
45
- return Promise.reject(error);
46
- }
47
- );
48
16
 
49
17
  // src/queries/useBudgets.ts
50
18
  function useBudgets(params, options) {
@@ -52,20 +20,18 @@ function useBudgets(params, options) {
52
20
  return reactQuery.useQuery({
53
21
  queryKey: budgetKeys.list(userId, filters),
54
22
  queryFn: async () => {
55
- const queryParams = new URLSearchParams();
56
- if (filters.start_date) {
57
- queryParams.append("start_date", filters.start_date);
23
+ let query = shared.supabase.from("budgets").select("*, budget_tags(*), budget_accounts(*)").eq("user_id", userId);
24
+ if (filters.month !== void 0) {
25
+ query = query.eq("month", filters.month);
58
26
  }
59
- if (filters.end_date) {
60
- queryParams.append("end_date", filters.end_date);
27
+ if (filters.year !== void 0) {
28
+ query = query.eq("year", filters.year);
61
29
  }
62
- const queryString = queryParams.toString();
63
- const url = `/users/${userId}/budgets${queryString ? `?${queryString}` : ""}`;
64
- const response = await api.get(url);
65
- return shared.BudgetsResponseSchema.parse(response.data);
30
+ const { data, error } = await query.order("name");
31
+ if (error) throw error;
32
+ return { budgets: shared.BudgetsResponseSchema.parse(data) };
66
33
  },
67
34
  staleTime: 1e3 * 60 * 5,
68
- // 5 minutes - budgets change less frequently than transactions
69
35
  ...options
70
36
  });
71
37
  }
@@ -74,23 +40,41 @@ function useBudget(params, options) {
74
40
  return reactQuery.useQuery({
75
41
  queryKey: budgetKeys.detail(userId, budgetId),
76
42
  queryFn: async () => {
77
- const response = await api.get(`/users/${userId}/budgets/${budgetId}`);
78
- return shared.BudgetSchema.parse(response.data);
43
+ const { data, error } = await shared.supabase.from("budgets").select("*, budget_tags(*), budget_accounts(*)").eq("id", budgetId).single();
44
+ if (error) throw error;
45
+ return shared.BudgetWithRelationsSchema.parse(data);
79
46
  },
80
47
  staleTime: 1e3 * 60 * 5,
81
- // 5 minutes
82
48
  ...options
83
49
  });
84
50
  }
85
51
  function useCreateBudget(options) {
86
52
  const queryClient = reactQuery.useQueryClient();
87
53
  return reactQuery.useMutation({
88
- mutationFn: async ({ userId, data }) => {
54
+ mutationFn: async ({ data, tagNames, accountIds }) => {
89
55
  const validated = shared.BudgetCreateSchema.parse(data);
90
- const response = await api.post(`/users/${userId}/budgets`, {
91
- budget: validated
92
- });
93
- return shared.BudgetSchema.parse(response.data);
56
+ const { data: created, error } = await shared.supabase.from("budgets").insert(validated).select().single();
57
+ if (error) throw error;
58
+ const budgetId = created.id;
59
+ if (tagNames && tagNames.length > 0) {
60
+ const tagRows = tagNames.map((tag_name) => ({
61
+ budget_id: budgetId,
62
+ tag_name
63
+ }));
64
+ const { error: tagError } = await shared.supabase.from("budget_tags").insert(tagRows);
65
+ if (tagError) throw tagError;
66
+ }
67
+ if (accountIds && accountIds.length > 0) {
68
+ const accountRows = accountIds.map((account_id) => ({
69
+ budget_id: budgetId,
70
+ account_id
71
+ }));
72
+ const { error: accountError } = await shared.supabase.from("budget_accounts").insert(accountRows);
73
+ if (accountError) throw accountError;
74
+ }
75
+ const { data: full, error: fetchError } = await shared.supabase.from("budgets").select("*, budget_tags(*), budget_accounts(*)").eq("id", budgetId).single();
76
+ if (fetchError) throw fetchError;
77
+ return shared.BudgetWithRelationsSchema.parse(full);
94
78
  },
95
79
  onSuccess: (_, { userId }) => {
96
80
  queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });
@@ -101,12 +85,40 @@ function useCreateBudget(options) {
101
85
  function useUpdateBudget(options) {
102
86
  const queryClient = reactQuery.useQueryClient();
103
87
  return reactQuery.useMutation({
104
- mutationFn: async ({ userId, budgetId, data }) => {
88
+ mutationFn: async ({ budgetId, data, tagNames, accountIds }) => {
105
89
  const validated = shared.BudgetUpdateSchema.parse(data);
106
- const response = await api.put(`/users/${userId}/budgets/${budgetId}`, {
107
- budget: validated
108
- });
109
- return shared.BudgetSchema.parse(response.data);
90
+ const { id: _, ...updateData } = validated;
91
+ if (Object.keys(updateData).length > 0) {
92
+ const { error } = await shared.supabase.from("budgets").update(updateData).eq("id", budgetId);
93
+ if (error) throw error;
94
+ }
95
+ if (tagNames !== void 0) {
96
+ const { error: deleteTagError } = await shared.supabase.from("budget_tags").delete().eq("budget_id", budgetId);
97
+ if (deleteTagError) throw deleteTagError;
98
+ if (tagNames.length > 0) {
99
+ const tagRows = tagNames.map((tag_name) => ({
100
+ budget_id: budgetId,
101
+ tag_name
102
+ }));
103
+ const { error: insertTagError } = await shared.supabase.from("budget_tags").insert(tagRows);
104
+ if (insertTagError) throw insertTagError;
105
+ }
106
+ }
107
+ if (accountIds !== void 0) {
108
+ const { error: deleteAccError } = await shared.supabase.from("budget_accounts").delete().eq("budget_id", budgetId);
109
+ if (deleteAccError) throw deleteAccError;
110
+ if (accountIds.length > 0) {
111
+ const accountRows = accountIds.map((account_id) => ({
112
+ budget_id: budgetId,
113
+ account_id
114
+ }));
115
+ const { error: insertAccError } = await shared.supabase.from("budget_accounts").insert(accountRows);
116
+ if (insertAccError) throw insertAccError;
117
+ }
118
+ }
119
+ const { data: full, error: fetchError } = await shared.supabase.from("budgets").select("*, budget_tags(*), budget_accounts(*)").eq("id", budgetId).single();
120
+ if (fetchError) throw fetchError;
121
+ return shared.BudgetWithRelationsSchema.parse(full);
110
122
  },
111
123
  onSuccess: (_, { userId, budgetId }) => {
112
124
  queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });
@@ -120,12 +132,15 @@ function useUpdateBudget(options) {
120
132
  function useDeleteBudget(options) {
121
133
  const queryClient = reactQuery.useQueryClient();
122
134
  return reactQuery.useMutation({
123
- mutationFn: async ({ userId, budgetId }) => {
124
- await api.delete(`/users/${userId}/budgets/${budgetId}`);
135
+ mutationFn: async ({ budgetId }) => {
136
+ const { error } = await shared.supabase.from("budgets").delete().eq("id", budgetId);
137
+ if (error) throw error;
125
138
  },
126
139
  onSuccess: (_, { userId, budgetId }) => {
127
- queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });
128
140
  queryClient.invalidateQueries({
141
+ queryKey: budgetKeys.list(userId)
142
+ });
143
+ queryClient.removeQueries({
129
144
  queryKey: budgetKeys.detail(userId, budgetId)
130
145
  });
131
146
  },
@@ -133,7 +148,6 @@ function useDeleteBudget(options) {
133
148
  });
134
149
  }
135
150
 
136
- exports.api = api;
137
151
  exports.budgetKeys = budgetKeys;
138
152
  exports.useBudget = useBudget;
139
153
  exports.useBudgets = useBudgets;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/keys.ts","../src/client.ts","../src/queries/useBudgets.ts","../src/queries/useBudget.ts","../src/mutations/useCreateBudget.ts","../src/mutations/useUpdateBudget.ts","../src/mutations/useDeleteBudget.ts"],"names":["axios","useQuery","BudgetsResponseSchema","BudgetSchema","useQueryClient","useMutation","BudgetCreateSchema","BudgetUpdateSchema"],"mappings":";;;;;;;;;;;;;AAUO,IAAM,UAAA,GAAa;AAAA,EACxB,GAAA,EAAK,CAAC,SAAS,CAAA;AAAA,EACf,OAAO,MAAM,CAAC,GAAG,UAAA,CAAW,KAAK,MAAM,CAAA;AAAA,EACvC,IAAA,EAAM,CAAC,MAAA,EAAgB,OAAA,KACrB,CAAC,GAAG,UAAA,CAAW,KAAA,EAAM,EAAG,MAAA,EAAQ,OAAA,IAAW,EAAE,CAAA;AAAA,EAC/C,SAAS,MAAM,CAAC,GAAG,UAAA,CAAW,KAAK,QAAQ,CAAA;AAAA,EAC3C,MAAA,EAAQ,CAAC,MAAA,EAAgB,QAAA,KACvB,CAAC,GAAG,UAAA,CAAW,OAAA,EAAQ,EAAG,MAAA,EAAQ,QAAQ;AAC9C;ACXO,IAAM,GAAA,GAAMA,uBAAM,MAAA,CAAO;AAAA,EAC9B,OAAA,EAAS,MAAA;AAAA,EACT,OAAA,EAAS;AAAA,IACP,cAAA,EAAgB;AAAA,GAClB;AAAA,EACA,OAAA,EAAS;AACX,CAAC;AAGD,GAAA,CAAI,aAAa,OAAA,CAAQ,GAAA;AAAA,EACvB,CAAC,MAAA,KAAW;AACV,IAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,OAAA,CAAQ,YAAY,CAAA;AAC/C,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAA,CAAO,OAAA,CAAQ,aAAA,GAAgB,CAAA,OAAA,EAAU,KAAK,CAAA,CAAA;AAAA,IAChD;AACA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AAAA,EACA,CAAC,KAAA,KAAU,OAAA,CAAQ,MAAA,CAAO,KAAK;AACjC,CAAA;AAGA,GAAA,CAAI,aAAa,QAAA,CAAS,GAAA;AAAA,EACxB,CAAC,QAAA,KAAa,QAAA;AAAA,EACd,CAAC,KAAA,KAAU;AACT,IAAA,IAAI,KAAA,CAAM,QAAA,EAAU,MAAA,KAAW,GAAA,EAAK;AAElC,MAAA,YAAA,CAAa,WAAW,YAAY,CAAA;AACpC,MAAA,MAAA,CAAO,SAAS,IAAA,GAAO,QAAA;AAAA,IACzB;AACA,IAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,EAC7B;AACF,CAAA;;;ACeO,SAAS,UAAA,CACd,QACA,OAAA,EACA;AACA,EAAA,MAAM,EAAE,MAAA,EAAQ,OAAA,GAAU,IAAG,GAAI,MAAA;AAEjC,EAAA,OAAOC,mBAAA,CAAS;AAAA,IACd,QAAA,EAAU,UAAA,CAAW,IAAA,CAAK,MAAA,EAAQ,OAAO,CAAA;AAAA,IACzC,SAAS,YAAY;AAEnB,MAAA,MAAM,WAAA,GAAc,IAAI,eAAA,EAAgB;AAExC,MAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,QAAA,WAAA,CAAY,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,UAAU,CAAA;AAAA,MACrD;AAEA,MAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,QAAA,WAAA,CAAY,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,QAAQ,CAAA;AAAA,MACjD;AAEA,MAAA,MAAM,WAAA,GAAc,YAAY,QAAA,EAAS;AACzC,MAAA,MAAM,GAAA,GAAM,UAAU,MAAM,CAAA,QAAA,EAC1B,cAAc,CAAA,CAAA,EAAI,WAAW,KAAK,EACpC,CAAA,CAAA;AAEA,MAAA,MAAM,QAAA,GAAW,MAAM,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAGlC,MAAA,OAAOC,4BAAA,CAAsB,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA;AAAA,IAClD,CAAA;AAAA,IACA,SAAA,EAAW,MAAO,EAAA,GAAK,CAAA;AAAA;AAAA,IACvB,GAAG;AAAA,GACJ,CAAA;AACH;ACzCO,SAAS,SAAA,CACd,QACA,OAAA,EACA;AACA,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,MAAA;AAE7B,EAAA,OAAOD,mBAAAA,CAAS;AAAA,IACd,QAAA,EAAU,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,QAAQ,CAAA;AAAA,IAC5C,SAAS,YAAY;AACnB,MAAA,MAAM,QAAA,GAAW,MAAM,GAAA,CAAI,GAAA,CAAI,UAAU,MAAM,CAAA,SAAA,EAAY,QAAQ,CAAA,CAAE,CAAA;AAGrE,MAAA,OAAOE,mBAAA,CAAa,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA;AAAA,IACzC,CAAA;AAAA,IACA,SAAA,EAAW,MAAO,EAAA,GAAK,CAAA;AAAA;AAAA,IACvB,GAAG;AAAA,GACJ,CAAA;AACH;ACAO,SAAS,gBACd,OAAA,EACA;AACA,EAAA,MAAM,cAAcC,yBAAA,EAAe;AAEnC,EAAA,OAAOC,sBAAA,CAAY;AAAA,IACjB,UAAA,EAAY,OAAO,EAAE,MAAA,EAAQ,MAAK,KAA0B;AAE1D,MAAA,MAAM,SAAA,GAAYC,yBAAA,CAAmB,KAAA,CAAM,IAAI,CAAA;AAE/C,MAAA,MAAM,WAAW,MAAM,GAAA,CAAI,IAAA,CAAK,CAAA,OAAA,EAAU,MAAM,CAAA,QAAA,CAAA,EAAY;AAAA,QAC1D,MAAA,EAAQ;AAAA,OACT,CAAA;AAGD,MAAA,OAAOH,mBAAAA,CAAa,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA;AAAA,IACzC,CAAA;AAAA,IACA,SAAA,EAAW,CAAC,CAAA,EAAG,EAAE,QAAO,KAAM;AAE5B,MAAA,WAAA,CAAY,kBAAkB,EAAE,QAAA,EAAU,UAAA,CAAW,KAAA,IAAS,CAAA;AAAA,IAChE,CAAA;AAAA,IACA,GAAG;AAAA,GACJ,CAAA;AACH;ACtBO,SAAS,gBACd,OAAA,EACA;AACA,EAAA,MAAM,cAAcC,yBAAAA,EAAe;AAEnC,EAAA,OAAOC,sBAAAA,CAAY;AAAA,IACjB,YAAY,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,MAAK,KAA0B;AAEpE,MAAA,MAAM,SAAA,GAAYE,yBAAA,CAAmB,KAAA,CAAM,IAAI,CAAA;AAE/C,MAAA,MAAM,QAAA,GAAW,MAAM,GAAA,CAAI,GAAA,CAAI,UAAU,MAAM,CAAA,SAAA,EAAY,QAAQ,CAAA,CAAA,EAAI;AAAA,QACrE,MAAA,EAAQ;AAAA,OACT,CAAA;AAGD,MAAA,OAAOJ,mBAAAA,CAAa,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA;AAAA,IACzC,CAAA;AAAA,IACA,WAAW,CAAC,CAAA,EAAG,EAAE,MAAA,EAAQ,UAAS,KAAM;AAEtC,MAAA,WAAA,CAAY,kBAAkB,EAAE,QAAA,EAAU,UAAA,CAAW,KAAA,IAAS,CAAA;AAE9D,MAAA,WAAA,CAAY,iBAAA,CAAkB;AAAA,QAC5B,QAAA,EAAU,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,QAAQ;AAAA,OAC7C,CAAA;AAAA,IACH,CAAA;AAAA,IACA,GAAG;AAAA,GACJ,CAAA;AACH;AC/BO,SAAS,gBACd,OAAA,EACA;AACA,EAAA,MAAM,cAAcC,yBAAAA,EAAe;AAEnC,EAAA,OAAOC,sBAAAA,CAAY;AAAA,IACjB,UAAA,EAAY,OAAO,EAAE,MAAA,EAAQ,UAAS,KAA0B;AAC9D,MAAA,MAAM,IAAI,MAAA,CAAO,CAAA,OAAA,EAAU,MAAM,CAAA,SAAA,EAAY,QAAQ,CAAA,CAAE,CAAA;AAAA,IAEzD,CAAA;AAAA,IACA,WAAW,CAAC,CAAA,EAAG,EAAE,MAAA,EAAQ,UAAS,KAAM;AAEtC,MAAA,WAAA,CAAY,kBAAkB,EAAE,QAAA,EAAU,UAAA,CAAW,KAAA,IAAS,CAAA;AAE9D,MAAA,WAAA,CAAY,iBAAA,CAAkB;AAAA,QAC5B,QAAA,EAAU,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,QAAQ;AAAA,OAC7C,CAAA;AAAA,IACH,CAAA;AAAA,IACA,GAAG;AAAA,GACJ,CAAA;AACH","file":"index.cjs","sourcesContent":["/**\n * Query key factory for Budget domain\n * Hierarchical structure for efficient cache invalidation\n */\n\nexport interface BudgetDateRangeFilters {\n start_date?: string; // RFC 3339 format\n end_date?: string; // RFC 3339 format\n}\n\nexport const budgetKeys = {\n all: ['budgets'] as const,\n lists: () => [...budgetKeys.all, 'list'] as const,\n list: (userId: string, filters?: BudgetDateRangeFilters) =>\n [...budgetKeys.lists(), userId, filters ?? {}] as const,\n details: () => [...budgetKeys.all, 'detail'] as const,\n detail: (userId: string, budgetId: number) =>\n [...budgetKeys.details(), userId, budgetId] as const,\n};\n","/**\n * Axios client for Budget API calls\n * Reuses pattern from accounts/transactions data-access\n */\n\nimport axios from 'axios';\n\nexport const api = axios.create({\n baseURL: '/api',\n headers: {\n 'Content-Type': 'application/json',\n },\n timeout: 10000,\n});\n\n// Request interceptor - Add auth token\napi.interceptors.request.use(\n (config) => {\n const token = localStorage.getItem('auth_token');\n if (token) {\n config.headers.Authorization = `Bearer ${token}`;\n }\n return config;\n },\n (error) => Promise.reject(error)\n);\n\n// Response interceptor - Handle 401 unauthorized\napi.interceptors.response.use(\n (response) => response,\n (error) => {\n if (error.response?.status === 401) {\n // Clear token and redirect to login\n localStorage.removeItem('auth_token');\n window.location.href = '/login';\n }\n return Promise.reject(error);\n }\n);\n","/**\n * Query hook for fetching budgets list\n * Legacy API: GET /users/{userId}/budgets\n * Optional filters: start_date, end_date (RFC 3339 format)\n */\n\nimport { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { BudgetsResponseSchema, BudgetsResponse } from '@pfm-platform/shared';\nimport { budgetKeys, BudgetDateRangeFilters } from '../keys';\nimport { api } from '../client';\n\nexport interface UseBudgetsParams {\n userId: string;\n filters?: BudgetDateRangeFilters;\n}\n\n/**\n * Fetch budgets with optional date range filters\n * Uses Zod validation to ensure API response matches expected schema\n *\n * @param params - Query parameters with userId and optional date range filters\n * @param options - TanStack Query options\n * @returns Query result with validated budgets data\n *\n * @example\n * ```tsx\n * // Fetch all budgets\n * function BudgetsList() {\n * const { data, isLoading } = useBudgets({ userId: 'user123' });\n *\n * return (\n * <ul>\n * {data?.budgets.map(budget => (\n * <li key={budget.id}>{budget.name}</li>\n * ))}\n * </ul>\n * );\n * }\n *\n * // Fetch budgets for specific date range\n * function MonthBudgets() {\n * const { data } = useBudgets({\n * userId: 'user123',\n * filters: {\n * start_date: '2025-01-01T00:00:00Z',\n * end_date: '2025-01-31T23:59:59Z',\n * }\n * });\n *\n * return <BudgetList budgets={data?.budgets} />;\n * }\n * ```\n */\nexport function useBudgets(\n params: UseBudgetsParams,\n options?: Omit<UseQueryOptions<BudgetsResponse>, 'queryKey' | 'queryFn'>\n) {\n const { userId, filters = {} } = params;\n\n return useQuery({\n queryKey: budgetKeys.list(userId, filters),\n queryFn: async () => {\n // Build query parameters\n const queryParams = new URLSearchParams();\n\n if (filters.start_date) {\n queryParams.append('start_date', filters.start_date);\n }\n\n if (filters.end_date) {\n queryParams.append('end_date', filters.end_date);\n }\n\n const queryString = queryParams.toString();\n const url = `/users/${userId}/budgets${\n queryString ? `?${queryString}` : ''\n }`;\n\n const response = await api.get(url);\n\n // Validate response with Zod schema from Mini-Arc 1\n return BudgetsResponseSchema.parse(response.data);\n },\n staleTime: 1000 * 60 * 5, // 5 minutes - budgets change less frequently than transactions\n ...options,\n });\n}\n","/**\n * Query hook for fetching a single budget\n * Legacy API: GET /users/{userId}/budgets/{id}\n */\n\nimport { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { BudgetSchema, Budget } from '@pfm-platform/shared';\nimport { budgetKeys } from '../keys';\nimport { api } from '../client';\n\nexport interface UseBudgetParams {\n userId: string;\n budgetId: number;\n}\n\n/**\n * Fetch a single budget by ID\n * Uses Zod validation to ensure API response matches expected schema\n *\n * @param params - Query parameters with userId and budgetId\n * @param options - TanStack Query options\n * @returns Query result with validated budget data\n *\n * @example\n * ```tsx\n * function BudgetDetail({ budgetId }: { budgetId: number }) {\n * const { data, isLoading, error } = useBudget({\n * userId: 'user123',\n * budgetId\n * });\n *\n * if (isLoading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return (\n * <div>\n * <h1>{data.name}</h1>\n * <p>Amount: ${data.budget_amount}</p>\n * <p>Spent: ${data.spent}</p>\n * <p>State: {data.state}</p>\n * </div>\n * );\n * }\n * ```\n */\nexport function useBudget(\n params: UseBudgetParams,\n options?: Omit<UseQueryOptions<Budget>, 'queryKey' | 'queryFn'>\n) {\n const { userId, budgetId } = params;\n\n return useQuery({\n queryKey: budgetKeys.detail(userId, budgetId),\n queryFn: async () => {\n const response = await api.get(`/users/${userId}/budgets/${budgetId}`);\n\n // Validate response with Zod schema from Mini-Arc 1\n return BudgetSchema.parse(response.data);\n },\n staleTime: 1000 * 60 * 5, // 5 minutes\n ...options,\n });\n}\n","/**\n * Mutation hook for creating a budget\n * Legacy API: POST /users/{userId}/budgets\n */\n\nimport {\n useMutation,\n UseMutationOptions,\n useQueryClient,\n} from '@tanstack/react-query';\nimport {\n BudgetCreateSchema,\n BudgetSchema,\n Budget,\n BudgetCreate,\n} from '@pfm-platform/shared';\nimport { budgetKeys } from '../keys';\nimport { api } from '../client';\n\nexport interface CreateBudgetParams {\n userId: string;\n data: BudgetCreate;\n}\n\n/**\n * Create a new budget\n * Validates input with Zod schema and invalidates related queries on success\n *\n * @param options - TanStack Mutation options\n * @returns Mutation result\n *\n * @example\n * ```tsx\n * function BudgetCreateForm() {\n * const createBudget = useCreateBudget();\n *\n * const handleSubmit = (formData) => {\n * createBudget.mutate({\n * userId: 'user123',\n * data: {\n * name: formData.name,\n * budget_amount: formData.amount,\n * tag_names: formData.tags,\n * account_list: formData.accounts,\n * show_on_dashboard: true,\n * other: {}\n * }\n * }, {\n * onSuccess: (newBudget) => {\n * toast.success('Budget created!');\n * router.push(`/budgets/${newBudget.id}`);\n * },\n * onError: (error) => {\n * toast.error(`Failed: ${error.message}`);\n * }\n * });\n * };\n *\n * return <form onSubmit={handleSubmit}>...</form>;\n * }\n * ```\n */\nexport function useCreateBudget(\n options?: Omit<UseMutationOptions<Budget, Error, CreateBudgetParams>, 'mutationFn'>\n) {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ userId, data }: CreateBudgetParams) => {\n // Validate input with Zod schema from Mini-Arc 1\n const validated = BudgetCreateSchema.parse(data);\n\n const response = await api.post(`/users/${userId}/budgets`, {\n budget: validated,\n });\n\n // Validate response\n return BudgetSchema.parse(response.data);\n },\n onSuccess: (_, { userId }) => {\n // Invalidate budget list queries for this user\n queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });\n },\n ...options,\n });\n}\n","/**\n * Mutation hook for updating a budget\n * Legacy API: PUT /users/{userId}/budgets/{id}\n */\n\nimport {\n useMutation,\n UseMutationOptions,\n useQueryClient,\n} from '@tanstack/react-query';\nimport {\n BudgetUpdateSchema,\n BudgetSchema,\n Budget,\n BudgetUpdate,\n} from '@pfm-platform/shared';\nimport { budgetKeys } from '../keys';\nimport { api } from '../client';\n\nexport interface UpdateBudgetParams {\n userId: string;\n budgetId: number;\n data: BudgetUpdate;\n}\n\n/**\n * Update an existing budget\n * Validates input with Zod schema and invalidates related queries on success\n *\n * @param options - TanStack Mutation options\n * @returns Mutation result\n *\n * @example\n * ```tsx\n * function BudgetEditForm({ budget }: { budget: Budget }) {\n * const updateBudget = useUpdateBudget();\n *\n * const handleSubmit = (formData) => {\n * updateBudget.mutate({\n * userId: 'user123',\n * budgetId: budget.id,\n * data: {\n * name: formData.name,\n * budget_amount: formData.amount,\n * tag_names: formData.tags,\n * account_list: formData.accounts,\n * show_on_dashboard: formData.showOnDashboard,\n * other: formData.metadata\n * }\n * }, {\n * onSuccess: () => {\n * toast.success('Budget updated!');\n * },\n * onError: (error) => {\n * toast.error(`Failed: ${error.message}`);\n * }\n * });\n * };\n *\n * return <form onSubmit={handleSubmit}>...</form>;\n * }\n * ```\n */\nexport function useUpdateBudget(\n options?: Omit<UseMutationOptions<Budget, Error, UpdateBudgetParams>, 'mutationFn'>\n) {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ userId, budgetId, data }: UpdateBudgetParams) => {\n // Validate input with Zod schema from Mini-Arc 1\n const validated = BudgetUpdateSchema.parse(data);\n\n const response = await api.put(`/users/${userId}/budgets/${budgetId}`, {\n budget: validated,\n });\n\n // Validate response\n return BudgetSchema.parse(response.data);\n },\n onSuccess: (_, { userId, budgetId }) => {\n // Invalidate list queries\n queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });\n // Invalidate this specific budget\n queryClient.invalidateQueries({\n queryKey: budgetKeys.detail(userId, budgetId),\n });\n },\n ...options,\n });\n}\n","/**\n * Mutation hook for deleting a budget\n * Legacy API: DELETE /users/{userId}/budgets/{id}\n */\n\nimport {\n useMutation,\n UseMutationOptions,\n useQueryClient,\n} from '@tanstack/react-query';\nimport { budgetKeys } from '../keys';\nimport { api } from '../client';\n\nexport interface DeleteBudgetParams {\n userId: string;\n budgetId: number;\n}\n\n/**\n * Delete a budget\n * Removes budget from user's budgets list\n * Invalidates related queries on success\n *\n * @param options - TanStack Mutation options\n * @returns Mutation result\n *\n * @example\n * ```tsx\n * function BudgetDeleteButton({ budgetId }: { budgetId: number }) {\n * const deleteBudget = useDeleteBudget();\n *\n * const handleDelete = () => {\n * if (confirm('Are you sure you want to delete this budget?')) {\n * deleteBudget.mutate({\n * userId: 'user123',\n * budgetId\n * }, {\n * onSuccess: () => {\n * toast.success('Budget deleted');\n * router.push('/budgets');\n * },\n * onError: (error) => {\n * toast.error(`Failed: ${error.message}`);\n * }\n * });\n * }\n * };\n *\n * return (\n * <button\n * onClick={handleDelete}\n * disabled={deleteBudget.isPending}\n * >\n * {deleteBudget.isPending ? 'Deleting...' : 'Delete'}\n * </button>\n * );\n * }\n * ```\n */\nexport function useDeleteBudget(\n options?: Omit<UseMutationOptions<void, Error, DeleteBudgetParams>, 'mutationFn'>\n) {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ userId, budgetId }: DeleteBudgetParams) => {\n await api.delete(`/users/${userId}/budgets/${budgetId}`);\n // DELETE returns 204 No Content - no response body to validate\n },\n onSuccess: (_, { userId, budgetId }) => {\n // Invalidate list queries\n queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });\n // Invalidate this specific budget\n queryClient.invalidateQueries({\n queryKey: budgetKeys.detail(userId, budgetId),\n });\n },\n ...options,\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/keys.ts","../src/queries/useBudgets.ts","../src/queries/useBudget.ts","../src/mutations/useCreateBudget.ts","../src/mutations/useUpdateBudget.ts","../src/mutations/useDeleteBudget.ts"],"names":["useQuery","supabase","BudgetsResponseSchema","BudgetWithRelationsSchema","useQueryClient","useMutation","BudgetCreateSchema","BudgetUpdateSchema"],"mappings":";;;;;;;;AAeO,IAAM,UAAA,GAAa;AAAA,EACxB,GAAA,EAAK,CAAC,SAAS,CAAA;AAAA,EAEf,OAAO,MAAM,CAAC,GAAG,UAAA,CAAW,KAAK,MAAM,CAAA;AAAA,EAEvC,IAAA,EAAM,CAAC,MAAA,EAAgB,OAAA,KACrB,CAAC,GAAG,UAAA,CAAW,KAAA,EAAM,EAAG,MAAA,EAAQ,OAAA,IAAW,EAAE,CAAA;AAAA,EAE/C,SAAS,MAAM,CAAC,GAAG,UAAA,CAAW,KAAK,QAAQ,CAAA;AAAA,EAE3C,MAAA,EAAQ,CAAC,MAAA,EAAgB,QAAA,KACvB,CAAC,GAAG,UAAA,CAAW,OAAA,EAAQ,EAAG,MAAA,EAAQ,QAAQ;AAC9C;;;ACFO,SAAS,UAAA,CACd,QACA,OAAA,EACA;AACA,EAAA,MAAM,EAAE,MAAA,EAAQ,OAAA,GAAU,IAAG,GAAI,MAAA;AAEjC,EAAA,OAAOA,mBAAA,CAAS;AAAA,IACd,QAAA,EAAU,UAAA,CAAW,IAAA,CAAK,MAAA,EAAQ,OAAO,CAAA;AAAA,IACzC,SAAS,YAAkC;AACzC,MAAA,IAAI,KAAA,GAAQC,eAAA,CACT,IAAA,CAAK,SAAS,CAAA,CACd,OAAO,uCAAuC,CAAA,CAC9C,EAAA,CAAG,SAAA,EAAW,MAAM,CAAA;AAEvB,MAAA,IAAI,OAAA,CAAQ,UAAU,MAAA,EAAW;AAC/B,QAAA,KAAA,GAAQ,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,OAAA,CAAQ,KAAK,CAAA;AAAA,MACzC;AAEA,MAAA,IAAI,OAAA,CAAQ,SAAS,MAAA,EAAW;AAC9B,QAAA,KAAA,GAAQ,KAAA,CAAM,EAAA,CAAG,MAAA,EAAQ,OAAA,CAAQ,IAAI,CAAA;AAAA,MACvC;AAEA,MAAA,MAAM,EAAE,IAAA,EAAM,KAAA,KAAU,MAAM,KAAA,CAAM,MAAM,MAAM,CAAA;AAEhD,MAAA,IAAI,OAAO,MAAM,KAAA;AAEjB,MAAA,OAAO,EAAE,OAAA,EAASC,4BAAA,CAAsB,KAAA,CAAM,IAAI,CAAA,EAAE;AAAA,IACtD,CAAA;AAAA,IACA,SAAA,EAAW,MAAO,EAAA,GAAK,CAAA;AAAA,IACvB,GAAG;AAAA,GACJ,CAAA;AACH;ACtCO,SAAS,SAAA,CACd,QACA,OAAA,EACA;AACA,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,MAAA;AAE7B,EAAA,OAAOF,mBAAAA,CAAS;AAAA,IACd,QAAA,EAAU,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,QAAQ,CAAA;AAAA,IAC5C,SAAS,YAA0C;AACjD,MAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAMC,gBAC3B,IAAA,CAAK,SAAS,CAAA,CACd,MAAA,CAAO,uCAAuC,CAAA,CAC9C,EAAA,CAAG,IAAA,EAAM,QAAQ,EACjB,MAAA,EAAO;AAEV,MAAA,IAAI,OAAO,MAAM,KAAA;AAEjB,MAAA,OAAOE,gCAAA,CAA0B,MAAM,IAAI,CAAA;AAAA,IAC7C,CAAA;AAAA,IACA,SAAA,EAAW,MAAO,EAAA,GAAK,CAAA;AAAA,IACvB,GAAG;AAAA,GACJ,CAAA;AACH;ACRO,SAAS,gBACd,OAAA,EACA;AACA,EAAA,MAAM,cAAcC,yBAAA,EAAe;AAEnC,EAAA,OAAOC,sBAAA,CAAY;AAAA,IACjB,YAAY,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,YAAW,KAA0B;AACxE,MAAA,MAAM,SAAA,GAAYC,yBAAA,CAAmB,KAAA,CAAM,IAAI,CAAA;AAG/C,MAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,KAAU,MAAML,eAAA,CACpC,IAAA,CAAK,SAAS,EACd,MAAA,CAAO,SAAS,CAAA,CAChB,MAAA,GACA,MAAA,EAAO;AAEV,MAAA,IAAI,OAAO,MAAM,KAAA;AAEjB,MAAA,MAAM,WAAW,OAAA,CAAQ,EAAA;AAGzB,MAAA,IAAI,QAAA,IAAY,QAAA,CAAS,MAAA,GAAS,CAAA,EAAG;AACnC,QAAA,MAAM,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,CAAC,QAAA,MAAc;AAAA,UAC1C,SAAA,EAAW,QAAA;AAAA,UACX;AAAA,SACF,CAAE,CAAA;AACF,QAAA,MAAM,EAAE,KAAA,EAAO,QAAA,EAAS,GAAI,MAAMA,gBAC/B,IAAA,CAAK,aAAa,CAAA,CAClB,MAAA,CAAO,OAAO,CAAA;AACjB,QAAA,IAAI,UAAU,MAAM,QAAA;AAAA,MACtB;AAGA,MAAA,IAAI,UAAA,IAAc,UAAA,CAAW,MAAA,GAAS,CAAA,EAAG;AACvC,QAAA,MAAM,WAAA,GAAc,UAAA,CAAW,GAAA,CAAI,CAAC,UAAA,MAAgB;AAAA,UAClD,SAAA,EAAW,QAAA;AAAA,UACX;AAAA,SACF,CAAE,CAAA;AACF,QAAA,MAAM,EAAE,KAAA,EAAO,YAAA,EAAa,GAAI,MAAMA,gBACnC,IAAA,CAAK,iBAAiB,CAAA,CACtB,MAAA,CAAO,WAAW,CAAA;AACrB,QAAA,IAAI,cAAc,MAAM,YAAA;AAAA,MAC1B;AAGA,MAAA,MAAM,EAAE,IAAA,EAAM,IAAA,EAAM,OAAO,UAAA,EAAW,GAAI,MAAMA,eAAA,CAC7C,IAAA,CAAK,SAAS,CAAA,CACd,OAAO,uCAAuC,CAAA,CAC9C,GAAG,IAAA,EAAM,QAAQ,EACjB,MAAA,EAAO;AAEV,MAAA,IAAI,YAAY,MAAM,UAAA;AAEtB,MAAA,OAAOE,gCAAAA,CAA0B,MAAM,IAAI,CAAA;AAAA,IAC7C,CAAA;AAAA,IACA,SAAA,EAAW,CAAC,CAAA,EAAG,EAAE,QAAO,KAAM;AAC5B,MAAA,WAAA,CAAY,kBAAkB,EAAE,QAAA,EAAU,UAAA,CAAW,KAAA,IAAS,CAAA;AAAA,IAChE,CAAA;AAAA,IACA,GAAG;AAAA,GACJ,CAAA;AACH;AC3DO,SAAS,gBACd,OAAA,EACA;AACA,EAAA,MAAM,cAAcC,yBAAAA,EAAe;AAEnC,EAAA,OAAOC,sBAAAA,CAAY;AAAA,IACjB,YAAY,OAAO,EAAE,UAAU,IAAA,EAAM,QAAA,EAAU,YAAW,KAA0B;AAClF,MAAA,MAAM,SAAA,GAAYE,yBAAA,CAAmB,KAAA,CAAM,IAAI,CAAA;AAG/C,MAAA,MAAM,EAAE,EAAA,EAAI,CAAA,EAAG,GAAG,YAAW,GAAI,SAAA;AACjC,MAAA,IAAI,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,CAAE,SAAS,CAAA,EAAG;AACtC,QAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAMN,eAAA,CACrB,IAAA,CAAK,SAAS,CAAA,CACd,MAAA,CAAO,UAAU,CAAA,CACjB,EAAA,CAAG,MAAM,QAAQ,CAAA;AACpB,QAAA,IAAI,OAAO,MAAM,KAAA;AAAA,MACnB;AAGA,MAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,QAAA,MAAM,EAAE,KAAA,EAAO,cAAA,EAAe,GAAI,MAAMA,eAAA,CACrC,IAAA,CAAK,aAAa,CAAA,CAClB,MAAA,EAAO,CACP,EAAA,CAAG,aAAa,QAAQ,CAAA;AAC3B,QAAA,IAAI,gBAAgB,MAAM,cAAA;AAE1B,QAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,UAAA,MAAM,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,CAAC,QAAA,MAAc;AAAA,YAC1C,SAAA,EAAW,QAAA;AAAA,YACX;AAAA,WACF,CAAE,CAAA;AACF,UAAA,MAAM,EAAE,KAAA,EAAO,cAAA,EAAe,GAAI,MAAMA,gBACrC,IAAA,CAAK,aAAa,CAAA,CAClB,MAAA,CAAO,OAAO,CAAA;AACjB,UAAA,IAAI,gBAAgB,MAAM,cAAA;AAAA,QAC5B;AAAA,MACF;AAGA,MAAA,IAAI,eAAe,MAAA,EAAW;AAC5B,QAAA,MAAM,EAAE,KAAA,EAAO,cAAA,EAAe,GAAI,MAAMA,eAAA,CACrC,IAAA,CAAK,iBAAiB,CAAA,CACtB,MAAA,EAAO,CACP,EAAA,CAAG,aAAa,QAAQ,CAAA;AAC3B,QAAA,IAAI,gBAAgB,MAAM,cAAA;AAE1B,QAAA,IAAI,UAAA,CAAW,SAAS,CAAA,EAAG;AACzB,UAAA,MAAM,WAAA,GAAc,UAAA,CAAW,GAAA,CAAI,CAAC,UAAA,MAAgB;AAAA,YAClD,SAAA,EAAW,QAAA;AAAA,YACX;AAAA,WACF,CAAE,CAAA;AACF,UAAA,MAAM,EAAE,KAAA,EAAO,cAAA,EAAe,GAAI,MAAMA,gBACrC,IAAA,CAAK,iBAAiB,CAAA,CACtB,MAAA,CAAO,WAAW,CAAA;AACrB,UAAA,IAAI,gBAAgB,MAAM,cAAA;AAAA,QAC5B;AAAA,MACF;AAGA,MAAA,MAAM,EAAE,IAAA,EAAM,IAAA,EAAM,OAAO,UAAA,EAAW,GAAI,MAAMA,eAAA,CAC7C,IAAA,CAAK,SAAS,CAAA,CACd,OAAO,uCAAuC,CAAA,CAC9C,GAAG,IAAA,EAAM,QAAQ,EACjB,MAAA,EAAO;AAEV,MAAA,IAAI,YAAY,MAAM,UAAA;AAEtB,MAAA,OAAOE,gCAAAA,CAA0B,MAAM,IAAI,CAAA;AAAA,IAC7C,CAAA;AAAA,IACA,WAAW,CAAC,CAAA,EAAG,EAAE,MAAA,EAAQ,UAAS,KAAM;AACtC,MAAA,WAAA,CAAY,kBAAkB,EAAE,QAAA,EAAU,UAAA,CAAW,KAAA,IAAS,CAAA;AAC9D,MAAA,WAAA,CAAY,iBAAA,CAAkB;AAAA,QAC5B,QAAA,EAAU,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,QAAQ;AAAA,OAC7C,CAAA;AAAA,IACH,CAAA;AAAA,IACA,GAAG;AAAA,GACJ,CAAA;AACH;ACxFO,SAAS,gBACd,OAAA,EAIA;AACA,EAAA,MAAM,cAAcC,yBAAAA,EAAe;AAEnC,EAAA,OAAOC,sBAAAA,CAAY;AAAA,IACjB,UAAA,EAAY,OAAO,EAAE,QAAA,EAAS,KAA0B;AACtD,MAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAMJ,eAAA,CACrB,IAAA,CAAK,SAAS,CAAA,CACd,MAAA,EAAO,CACP,EAAA,CAAG,IAAA,EAAM,QAAQ,CAAA;AAEpB,MAAA,IAAI,OAAO,MAAM,KAAA;AAAA,IACnB,CAAA;AAAA,IACA,WAAW,CAAC,CAAA,EAAG,EAAE,MAAA,EAAQ,UAAS,KAAM;AACtC,MAAA,WAAA,CAAY,iBAAA,CAAkB;AAAA,QAC5B,QAAA,EAAU,UAAA,CAAW,IAAA,CAAK,MAAM;AAAA,OACjC,CAAA;AACD,MAAA,WAAA,CAAY,aAAA,CAAc;AAAA,QACxB,QAAA,EAAU,UAAA,CAAW,MAAA,CAAO,MAAA,EAAQ,QAAQ;AAAA,OAC7C,CAAA;AAAA,IACH,CAAA;AAAA,IACA,GAAG;AAAA,GACJ,CAAA;AACH","file":"index.cjs","sourcesContent":["/**\n * Query key factory for Budget domain\n * Following TanStack Query best practices for hierarchical cache keys\n *\n * Key structure:\n * - ['budgets'] - Root key for all budget queries\n * - ['budgets', 'list', userId, filters] - List of budgets for a user\n * - ['budgets', 'detail', userId, budgetId] - Single budget detail\n */\n\nexport interface BudgetFilters {\n month?: number;\n year?: number;\n}\n\nexport const budgetKeys = {\n all: ['budgets'] as const,\n\n lists: () => [...budgetKeys.all, 'list'] as const,\n\n list: (userId: string, filters?: BudgetFilters) =>\n [...budgetKeys.lists(), userId, filters ?? {}] as const,\n\n details: () => [...budgetKeys.all, 'detail'] as const,\n\n detail: (userId: string, budgetId: string) =>\n [...budgetKeys.details(), userId, budgetId] as const,\n};\n","/**\n * Query hook for fetching budgets list\n * Supabase: budgets table with budget_tags and budget_accounts joins\n */\n\nimport { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { BudgetsResponseSchema, type BudgetWithRelations } from '@pfm-platform/shared';\nimport { budgetKeys, BudgetFilters } from '../keys';\nimport { supabase } from '../client';\n\nexport interface UseBudgetsParams {\n userId: string;\n filters?: BudgetFilters;\n}\n\nexport interface BudgetsData {\n budgets: BudgetWithRelations[];\n}\n\n/**\n * Fetch all budgets for a user from Supabase\n *\n * Returns `{ budgets: BudgetWithRelations[] }` to match the shape expected by\n * feature hooks (useBudgetProgress, useBudgetSummary, useBudgetsByMonth).\n */\nexport function useBudgets(\n params: UseBudgetsParams,\n options?: Omit<UseQueryOptions<BudgetsData>, 'queryKey' | 'queryFn'>\n) {\n const { userId, filters = {} } = params;\n\n return useQuery({\n queryKey: budgetKeys.list(userId, filters),\n queryFn: async (): Promise<BudgetsData> => {\n let query = supabase\n .from('budgets')\n .select('*, budget_tags(*), budget_accounts(*)')\n .eq('user_id', userId);\n\n if (filters.month !== undefined) {\n query = query.eq('month', filters.month);\n }\n\n if (filters.year !== undefined) {\n query = query.eq('year', filters.year);\n }\n\n const { data, error } = await query.order('name');\n\n if (error) throw error;\n\n return { budgets: BudgetsResponseSchema.parse(data) };\n },\n staleTime: 1000 * 60 * 5,\n ...options,\n });\n}\n","/**\n * Query hook for fetching a single budget\n * Supabase: budgets table with budget_tags and budget_accounts joins\n */\n\nimport { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { BudgetWithRelationsSchema, type BudgetWithRelations } from '@pfm-platform/shared';\nimport { budgetKeys } from '../keys';\nimport { supabase } from '../client';\n\nexport interface UseBudgetParams {\n userId: string;\n budgetId: string;\n}\n\n/**\n * Fetch a single budget by ID from Supabase\n */\nexport function useBudget(\n params: UseBudgetParams,\n options?: Omit<UseQueryOptions<BudgetWithRelations>, 'queryKey' | 'queryFn'>\n) {\n const { userId, budgetId } = params;\n\n return useQuery({\n queryKey: budgetKeys.detail(userId, budgetId),\n queryFn: async (): Promise<BudgetWithRelations> => {\n const { data, error } = await supabase\n .from('budgets')\n .select('*, budget_tags(*), budget_accounts(*)')\n .eq('id', budgetId)\n .single();\n\n if (error) throw error;\n\n return BudgetWithRelationsSchema.parse(data);\n },\n staleTime: 1000 * 60 * 5,\n ...options,\n });\n}\n","/**\n * Mutation hook for creating a budget\n * Supabase: INSERT into budgets, then optionally budget_tags and budget_accounts\n */\n\nimport {\n useMutation,\n UseMutationOptions,\n useQueryClient,\n} from '@tanstack/react-query';\nimport {\n BudgetCreateSchema,\n BudgetWithRelationsSchema,\n type BudgetCreate,\n type BudgetWithRelations,\n} from '@pfm-platform/shared';\nimport { budgetKeys } from '../keys';\nimport { supabase } from '../client';\n\nexport interface CreateBudgetParams {\n userId: string;\n data: BudgetCreate;\n tagNames?: string[];\n accountIds?: string[];\n}\n\n/**\n * Create a new budget with optional tag and account associations\n *\n * Multi-step: (1) insert budget, (2) insert budget_tags rows,\n * (3) insert budget_accounts rows, (4) re-fetch with relations.\n */\nexport function useCreateBudget(\n options?: Omit<UseMutationOptions<BudgetWithRelations, Error, CreateBudgetParams>, 'mutationFn'>\n) {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ data, tagNames, accountIds }: CreateBudgetParams) => {\n const validated = BudgetCreateSchema.parse(data);\n\n // Step 1: Insert budget\n const { data: created, error } = await supabase\n .from('budgets')\n .insert(validated)\n .select()\n .single();\n\n if (error) throw error;\n\n const budgetId = created.id;\n\n // Step 2: Insert budget_tags if provided\n if (tagNames && tagNames.length > 0) {\n const tagRows = tagNames.map((tag_name) => ({\n budget_id: budgetId,\n tag_name,\n }));\n const { error: tagError } = await supabase\n .from('budget_tags')\n .insert(tagRows);\n if (tagError) throw tagError;\n }\n\n // Step 3: Insert budget_accounts if provided\n if (accountIds && accountIds.length > 0) {\n const accountRows = accountIds.map((account_id) => ({\n budget_id: budgetId,\n account_id,\n }));\n const { error: accountError } = await supabase\n .from('budget_accounts')\n .insert(accountRows);\n if (accountError) throw accountError;\n }\n\n // Step 4: Re-fetch with relations\n const { data: full, error: fetchError } = await supabase\n .from('budgets')\n .select('*, budget_tags(*), budget_accounts(*)')\n .eq('id', budgetId)\n .single();\n\n if (fetchError) throw fetchError;\n\n return BudgetWithRelationsSchema.parse(full);\n },\n onSuccess: (_, { userId }) => {\n queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });\n },\n ...options,\n });\n}\n","/**\n * Mutation hook for updating a budget\n * Supabase: UPDATE budgets, optionally replace budget_tags and budget_accounts\n */\n\nimport {\n useMutation,\n UseMutationOptions,\n useQueryClient,\n} from '@tanstack/react-query';\nimport {\n BudgetUpdateSchema,\n BudgetWithRelationsSchema,\n type BudgetUpdate,\n type BudgetWithRelations,\n} from '@pfm-platform/shared';\nimport { budgetKeys } from '../keys';\nimport { supabase } from '../client';\n\nexport interface UpdateBudgetParams {\n userId: string;\n budgetId: string;\n data: BudgetUpdate;\n tagNames?: string[];\n accountIds?: string[];\n}\n\n/**\n * Update an existing budget\n *\n * If tagNames or accountIds are provided, replaces all existing\n * junction rows (delete all, then insert new).\n */\nexport function useUpdateBudget(\n options?: Omit<UseMutationOptions<BudgetWithRelations, Error, UpdateBudgetParams>, 'mutationFn'>\n) {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ budgetId, data, tagNames, accountIds }: UpdateBudgetParams) => {\n const validated = BudgetUpdateSchema.parse(data);\n\n // Strip id before sending to Supabase .update()\n const { id: _, ...updateData } = validated;\n if (Object.keys(updateData).length > 0) {\n const { error } = await supabase\n .from('budgets')\n .update(updateData)\n .eq('id', budgetId);\n if (error) throw error;\n }\n\n // Replace budget_tags if provided\n if (tagNames !== undefined) {\n const { error: deleteTagError } = await supabase\n .from('budget_tags')\n .delete()\n .eq('budget_id', budgetId);\n if (deleteTagError) throw deleteTagError;\n\n if (tagNames.length > 0) {\n const tagRows = tagNames.map((tag_name) => ({\n budget_id: budgetId,\n tag_name,\n }));\n const { error: insertTagError } = await supabase\n .from('budget_tags')\n .insert(tagRows);\n if (insertTagError) throw insertTagError;\n }\n }\n\n // Replace budget_accounts if provided\n if (accountIds !== undefined) {\n const { error: deleteAccError } = await supabase\n .from('budget_accounts')\n .delete()\n .eq('budget_id', budgetId);\n if (deleteAccError) throw deleteAccError;\n\n if (accountIds.length > 0) {\n const accountRows = accountIds.map((account_id) => ({\n budget_id: budgetId,\n account_id,\n }));\n const { error: insertAccError } = await supabase\n .from('budget_accounts')\n .insert(accountRows);\n if (insertAccError) throw insertAccError;\n }\n }\n\n // Re-fetch with relations\n const { data: full, error: fetchError } = await supabase\n .from('budgets')\n .select('*, budget_tags(*), budget_accounts(*)')\n .eq('id', budgetId)\n .single();\n\n if (fetchError) throw fetchError;\n\n return BudgetWithRelationsSchema.parse(full);\n },\n onSuccess: (_, { userId, budgetId }) => {\n queryClient.invalidateQueries({ queryKey: budgetKeys.lists() });\n queryClient.invalidateQueries({\n queryKey: budgetKeys.detail(userId, budgetId),\n });\n },\n ...options,\n });\n}\n","/**\n * Mutation hook for deleting a budget\n * Supabase: DELETE FROM budgets WHERE id = ...\n * Junction rows cascade-delete via FK constraints.\n */\n\nimport {\n useMutation,\n UseMutationOptions,\n useQueryClient,\n} from '@tanstack/react-query';\nimport { budgetKeys } from '../keys';\nimport { supabase } from '../client';\n\nexport interface DeleteBudgetParams {\n userId: string;\n budgetId: string;\n}\n\n/**\n * Delete a budget from Supabase (hard delete)\n * Junction rows in budget_tags and budget_accounts cascade-delete.\n */\nexport function useDeleteBudget(\n options?: Omit<\n UseMutationOptions<void, Error, DeleteBudgetParams>,\n 'mutationFn'\n >\n) {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ budgetId }: DeleteBudgetParams) => {\n const { error } = await supabase\n .from('budgets')\n .delete()\n .eq('id', budgetId);\n\n if (error) throw error;\n },\n onSuccess: (_, { userId, budgetId }) => {\n queryClient.invalidateQueries({\n queryKey: budgetKeys.list(userId),\n });\n queryClient.removeQueries({\n queryKey: budgetKeys.detail(userId, budgetId),\n });\n },\n ...options,\n });\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,297 +1,149 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
3
- import { BudgetsResponse, Budget, BudgetCreate, BudgetUpdate } from '@pfm-platform/shared';
4
- import * as axios from 'axios';
3
+ import { BudgetWithRelations, BudgetCreate, BudgetUpdate } from '@pfm-platform/shared';
5
4
 
6
5
  /**
7
6
  * Query key factory for Budget domain
8
- * Hierarchical structure for efficient cache invalidation
7
+ * Following TanStack Query best practices for hierarchical cache keys
8
+ *
9
+ * Key structure:
10
+ * - ['budgets'] - Root key for all budget queries
11
+ * - ['budgets', 'list', userId, filters] - List of budgets for a user
12
+ * - ['budgets', 'detail', userId, budgetId] - Single budget detail
9
13
  */
10
- interface BudgetDateRangeFilters {
11
- start_date?: string;
12
- end_date?: string;
14
+ interface BudgetFilters {
15
+ month?: number;
16
+ year?: number;
13
17
  }
14
18
  declare const budgetKeys: {
15
19
  all: readonly ["budgets"];
16
20
  lists: () => readonly ["budgets", "list"];
17
- list: (userId: string, filters?: BudgetDateRangeFilters) => readonly ["budgets", "list", string, BudgetDateRangeFilters];
21
+ list: (userId: string, filters?: BudgetFilters) => readonly ["budgets", "list", string, BudgetFilters];
18
22
  details: () => readonly ["budgets", "detail"];
19
- detail: (userId: string, budgetId: number) => readonly ["budgets", "detail", string, number];
23
+ detail: (userId: string, budgetId: string) => readonly ["budgets", "detail", string, string];
20
24
  };
21
25
 
22
26
  interface UseBudgetsParams {
23
27
  userId: string;
24
- filters?: BudgetDateRangeFilters;
28
+ filters?: BudgetFilters;
29
+ }
30
+ interface BudgetsData {
31
+ budgets: BudgetWithRelations[];
25
32
  }
26
33
  /**
27
- * Fetch budgets with optional date range filters
28
- * Uses Zod validation to ensure API response matches expected schema
29
- *
30
- * @param params - Query parameters with userId and optional date range filters
31
- * @param options - TanStack Query options
32
- * @returns Query result with validated budgets data
33
- *
34
- * @example
35
- * ```tsx
36
- * // Fetch all budgets
37
- * function BudgetsList() {
38
- * const { data, isLoading } = useBudgets({ userId: 'user123' });
39
- *
40
- * return (
41
- * <ul>
42
- * {data?.budgets.map(budget => (
43
- * <li key={budget.id}>{budget.name}</li>
44
- * ))}
45
- * </ul>
46
- * );
47
- * }
48
- *
49
- * // Fetch budgets for specific date range
50
- * function MonthBudgets() {
51
- * const { data } = useBudgets({
52
- * userId: 'user123',
53
- * filters: {
54
- * start_date: '2025-01-01T00:00:00Z',
55
- * end_date: '2025-01-31T23:59:59Z',
56
- * }
57
- * });
34
+ * Fetch all budgets for a user from Supabase
58
35
  *
59
- * return <BudgetList budgets={data?.budgets} />;
60
- * }
61
- * ```
36
+ * Returns `{ budgets: BudgetWithRelations[] }` to match the shape expected by
37
+ * feature hooks (useBudgetProgress, useBudgetSummary, useBudgetsByMonth).
62
38
  */
63
- declare function useBudgets(params: UseBudgetsParams, options?: Omit<UseQueryOptions<BudgetsResponse>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseQueryResult<{
64
- budgets: {
65
- id: number;
66
- month: number;
67
- year: number;
68
- name: string;
69
- state: "under" | "risk" | "over";
70
- spent: number;
71
- budget_amount: number;
72
- tag_names: string[];
73
- links: {
74
- accounts: number[];
75
- budget_histories: number[];
76
- };
77
- }[];
78
- }, Error>;
39
+ declare function useBudgets(params: UseBudgetsParams, options?: Omit<UseQueryOptions<BudgetsData>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseQueryResult<BudgetsData, Error>;
79
40
 
80
41
  interface UseBudgetParams {
81
42
  userId: string;
82
- budgetId: number;
43
+ budgetId: string;
83
44
  }
84
45
  /**
85
- * Fetch a single budget by ID
86
- * Uses Zod validation to ensure API response matches expected schema
87
- *
88
- * @param params - Query parameters with userId and budgetId
89
- * @param options - TanStack Query options
90
- * @returns Query result with validated budget data
91
- *
92
- * @example
93
- * ```tsx
94
- * function BudgetDetail({ budgetId }: { budgetId: number }) {
95
- * const { data, isLoading, error } = useBudget({
96
- * userId: 'user123',
97
- * budgetId
98
- * });
99
- *
100
- * if (isLoading) return <div>Loading...</div>;
101
- * if (error) return <div>Error: {error.message}</div>;
102
- *
103
- * return (
104
- * <div>
105
- * <h1>{data.name}</h1>
106
- * <p>Amount: ${data.budget_amount}</p>
107
- * <p>Spent: ${data.spent}</p>
108
- * <p>State: {data.state}</p>
109
- * </div>
110
- * );
111
- * }
112
- * ```
46
+ * Fetch a single budget by ID from Supabase
113
47
  */
114
- declare function useBudget(params: UseBudgetParams, options?: Omit<UseQueryOptions<Budget>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseQueryResult<{
115
- id: number;
48
+ declare function useBudget(params: UseBudgetParams, options?: Omit<UseQueryOptions<BudgetWithRelations>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseQueryResult<{
49
+ id: string;
50
+ user_id: string;
51
+ name: string;
116
52
  month: number;
117
53
  year: number;
118
- name: string;
119
- state: "under" | "risk" | "over";
120
- spent: number;
121
54
  budget_amount: number;
122
- tag_names: string[];
123
- links: {
124
- accounts: number[];
125
- budget_histories: number[];
126
- };
55
+ spent: number;
56
+ state: string;
57
+ show_on_dashboard: boolean;
58
+ budget_tags: {
59
+ budget_id: string;
60
+ tag_name: string;
61
+ }[];
62
+ budget_accounts: {
63
+ budget_id: string;
64
+ account_id: string;
65
+ }[];
66
+ created_at?: string | null | undefined;
67
+ updated_at?: string | null | undefined;
127
68
  }, Error>;
128
69
 
129
70
  interface CreateBudgetParams {
130
71
  userId: string;
131
72
  data: BudgetCreate;
73
+ tagNames?: string[];
74
+ accountIds?: string[];
132
75
  }
133
76
  /**
134
- * Create a new budget
135
- * Validates input with Zod schema and invalidates related queries on success
136
- *
137
- * @param options - TanStack Mutation options
138
- * @returns Mutation result
139
- *
140
- * @example
141
- * ```tsx
142
- * function BudgetCreateForm() {
143
- * const createBudget = useCreateBudget();
77
+ * Create a new budget with optional tag and account associations
144
78
  *
145
- * const handleSubmit = (formData) => {
146
- * createBudget.mutate({
147
- * userId: 'user123',
148
- * data: {
149
- * name: formData.name,
150
- * budget_amount: formData.amount,
151
- * tag_names: formData.tags,
152
- * account_list: formData.accounts,
153
- * show_on_dashboard: true,
154
- * other: {}
155
- * }
156
- * }, {
157
- * onSuccess: (newBudget) => {
158
- * toast.success('Budget created!');
159
- * router.push(`/budgets/${newBudget.id}`);
160
- * },
161
- * onError: (error) => {
162
- * toast.error(`Failed: ${error.message}`);
163
- * }
164
- * });
165
- * };
166
- *
167
- * return <form onSubmit={handleSubmit}>...</form>;
168
- * }
169
- * ```
79
+ * Multi-step: (1) insert budget, (2) insert budget_tags rows,
80
+ * (3) insert budget_accounts rows, (4) re-fetch with relations.
170
81
  */
171
- declare function useCreateBudget(options?: Omit<UseMutationOptions<Budget, Error, CreateBudgetParams>, 'mutationFn'>): _tanstack_react_query.UseMutationResult<{
172
- id: number;
82
+ declare function useCreateBudget(options?: Omit<UseMutationOptions<BudgetWithRelations, Error, CreateBudgetParams>, 'mutationFn'>): _tanstack_react_query.UseMutationResult<{
83
+ id: string;
84
+ user_id: string;
85
+ name: string;
173
86
  month: number;
174
87
  year: number;
175
- name: string;
176
- state: "under" | "risk" | "over";
177
- spent: number;
178
88
  budget_amount: number;
179
- tag_names: string[];
180
- links: {
181
- accounts: number[];
182
- budget_histories: number[];
183
- };
89
+ spent: number;
90
+ state: string;
91
+ show_on_dashboard: boolean;
92
+ budget_tags: {
93
+ budget_id: string;
94
+ tag_name: string;
95
+ }[];
96
+ budget_accounts: {
97
+ budget_id: string;
98
+ account_id: string;
99
+ }[];
100
+ created_at?: string | null | undefined;
101
+ updated_at?: string | null | undefined;
184
102
  }, Error, CreateBudgetParams, unknown>;
185
103
 
186
104
  interface UpdateBudgetParams {
187
105
  userId: string;
188
- budgetId: number;
106
+ budgetId: string;
189
107
  data: BudgetUpdate;
108
+ tagNames?: string[];
109
+ accountIds?: string[];
190
110
  }
191
111
  /**
192
112
  * Update an existing budget
193
- * Validates input with Zod schema and invalidates related queries on success
194
- *
195
- * @param options - TanStack Mutation options
196
- * @returns Mutation result
197
113
  *
198
- * @example
199
- * ```tsx
200
- * function BudgetEditForm({ budget }: { budget: Budget }) {
201
- * const updateBudget = useUpdateBudget();
202
- *
203
- * const handleSubmit = (formData) => {
204
- * updateBudget.mutate({
205
- * userId: 'user123',
206
- * budgetId: budget.id,
207
- * data: {
208
- * name: formData.name,
209
- * budget_amount: formData.amount,
210
- * tag_names: formData.tags,
211
- * account_list: formData.accounts,
212
- * show_on_dashboard: formData.showOnDashboard,
213
- * other: formData.metadata
214
- * }
215
- * }, {
216
- * onSuccess: () => {
217
- * toast.success('Budget updated!');
218
- * },
219
- * onError: (error) => {
220
- * toast.error(`Failed: ${error.message}`);
221
- * }
222
- * });
223
- * };
224
- *
225
- * return <form onSubmit={handleSubmit}>...</form>;
226
- * }
227
- * ```
114
+ * If tagNames or accountIds are provided, replaces all existing
115
+ * junction rows (delete all, then insert new).
228
116
  */
229
- declare function useUpdateBudget(options?: Omit<UseMutationOptions<Budget, Error, UpdateBudgetParams>, 'mutationFn'>): _tanstack_react_query.UseMutationResult<{
230
- id: number;
117
+ declare function useUpdateBudget(options?: Omit<UseMutationOptions<BudgetWithRelations, Error, UpdateBudgetParams>, 'mutationFn'>): _tanstack_react_query.UseMutationResult<{
118
+ id: string;
119
+ user_id: string;
120
+ name: string;
231
121
  month: number;
232
122
  year: number;
233
- name: string;
234
- state: "under" | "risk" | "over";
235
- spent: number;
236
123
  budget_amount: number;
237
- tag_names: string[];
238
- links: {
239
- accounts: number[];
240
- budget_histories: number[];
241
- };
124
+ spent: number;
125
+ state: string;
126
+ show_on_dashboard: boolean;
127
+ budget_tags: {
128
+ budget_id: string;
129
+ tag_name: string;
130
+ }[];
131
+ budget_accounts: {
132
+ budget_id: string;
133
+ account_id: string;
134
+ }[];
135
+ created_at?: string | null | undefined;
136
+ updated_at?: string | null | undefined;
242
137
  }, Error, UpdateBudgetParams, unknown>;
243
138
 
244
139
  interface DeleteBudgetParams {
245
140
  userId: string;
246
- budgetId: number;
141
+ budgetId: string;
247
142
  }
248
143
  /**
249
- * Delete a budget
250
- * Removes budget from user's budgets list
251
- * Invalidates related queries on success
252
- *
253
- * @param options - TanStack Mutation options
254
- * @returns Mutation result
255
- *
256
- * @example
257
- * ```tsx
258
- * function BudgetDeleteButton({ budgetId }: { budgetId: number }) {
259
- * const deleteBudget = useDeleteBudget();
260
- *
261
- * const handleDelete = () => {
262
- * if (confirm('Are you sure you want to delete this budget?')) {
263
- * deleteBudget.mutate({
264
- * userId: 'user123',
265
- * budgetId
266
- * }, {
267
- * onSuccess: () => {
268
- * toast.success('Budget deleted');
269
- * router.push('/budgets');
270
- * },
271
- * onError: (error) => {
272
- * toast.error(`Failed: ${error.message}`);
273
- * }
274
- * });
275
- * }
276
- * };
277
- *
278
- * return (
279
- * <button
280
- * onClick={handleDelete}
281
- * disabled={deleteBudget.isPending}
282
- * >
283
- * {deleteBudget.isPending ? 'Deleting...' : 'Delete'}
284
- * </button>
285
- * );
286
- * }
287
- * ```
144
+ * Delete a budget from Supabase (hard delete)
145
+ * Junction rows in budget_tags and budget_accounts cascade-delete.
288
146
  */
289
147
  declare function useDeleteBudget(options?: Omit<UseMutationOptions<void, Error, DeleteBudgetParams>, 'mutationFn'>): _tanstack_react_query.UseMutationResult<void, Error, DeleteBudgetParams, unknown>;
290
148
 
291
- /**
292
- * Axios client for Budget API calls
293
- * Reuses pattern from accounts/transactions data-access
294
- */
295
- declare const api: axios.AxiosInstance;
296
-
297
- export { type BudgetDateRangeFilters, type CreateBudgetParams, type DeleteBudgetParams, type UpdateBudgetParams, type UseBudgetParams, type UseBudgetsParams, api, budgetKeys, useBudget, useBudgets, useCreateBudget, useDeleteBudget, useUpdateBudget };
149
+ export { type BudgetFilters, type BudgetsData, type CreateBudgetParams, type DeleteBudgetParams, type UpdateBudgetParams, type UseBudgetParams, type UseBudgetsParams, budgetKeys, useBudget, useBudgets, useCreateBudget, useDeleteBudget, useUpdateBudget };