@spaceinvoices/react-ui 0.4.6 → 0.4.8

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.
Files changed (100) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +60 -44
  4. package/src/components/advance-invoices/create/locales/de.ts +2 -1
  5. package/src/components/advance-invoices/create/locales/es.ts +2 -1
  6. package/src/components/advance-invoices/create/locales/fr.ts +2 -1
  7. package/src/components/advance-invoices/create/locales/hr.ts +2 -1
  8. package/src/components/advance-invoices/create/locales/it.ts +2 -1
  9. package/src/components/advance-invoices/create/locales/nl.ts +2 -1
  10. package/src/components/advance-invoices/create/locales/pl.ts +2 -1
  11. package/src/components/advance-invoices/create/locales/pt.ts +2 -1
  12. package/src/components/advance-invoices/create/locales/sl.ts +2 -1
  13. package/src/components/credit-notes/create/create-credit-note-form.tsx +52 -42
  14. package/src/components/credit-notes/create/locales/de.ts +2 -1
  15. package/src/components/credit-notes/create/locales/es.ts +2 -1
  16. package/src/components/credit-notes/create/locales/fr.ts +2 -1
  17. package/src/components/credit-notes/create/locales/hr.ts +2 -1
  18. package/src/components/credit-notes/create/locales/it.ts +2 -1
  19. package/src/components/credit-notes/create/locales/nl.ts +2 -1
  20. package/src/components/credit-notes/create/locales/pl.ts +2 -1
  21. package/src/components/credit-notes/create/locales/pt.ts +2 -1
  22. package/src/components/credit-notes/create/locales/sl.ts +2 -1
  23. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -92
  24. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +48 -82
  25. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +22 -31
  26. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +33 -48
  27. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +56 -76
  28. package/src/components/dashboard/shared/index.ts +1 -1
  29. package/src/components/dashboard/shared/use-revenue-data.ts +106 -182
  30. package/src/components/dashboard/shared/use-stats-counts.ts +18 -68
  31. package/src/components/dashboard/shared/use-stats-query.ts +35 -5
  32. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +57 -75
  33. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +38 -49
  34. package/src/components/delivery-notes/create/create-delivery-note-form.tsx +3 -2
  35. package/src/components/delivery-notes/create/locales/de.ts +2 -1
  36. package/src/components/delivery-notes/create/locales/es.ts +2 -1
  37. package/src/components/delivery-notes/create/locales/fr.ts +2 -1
  38. package/src/components/delivery-notes/create/locales/hr.ts +2 -1
  39. package/src/components/delivery-notes/create/locales/it.ts +2 -1
  40. package/src/components/delivery-notes/create/locales/nl.ts +2 -1
  41. package/src/components/delivery-notes/create/locales/pl.ts +2 -1
  42. package/src/components/delivery-notes/create/locales/pt.ts +2 -1
  43. package/src/components/delivery-notes/create/locales/sl.ts +2 -1
  44. package/src/components/documents/create/document-details-section.tsx +6 -4
  45. package/src/components/documents/create/document-recipient-section.tsx +30 -1
  46. package/src/components/documents/create/live-preview.tsx +15 -28
  47. package/src/components/documents/create/prepare-document-submission.ts +1 -0
  48. package/src/components/documents/create/use-document-customer-form.ts +4 -0
  49. package/src/components/documents/shared/document-preview-skeleton.tsx +63 -0
  50. package/src/components/documents/shared/index.ts +1 -0
  51. package/src/components/documents/view/document-actions-bar.tsx +29 -7
  52. package/src/components/entities/entity-settings-form/locales/de.ts +6 -3
  53. package/src/components/entities/entity-settings-form/locales/es.ts +6 -3
  54. package/src/components/entities/entity-settings-form/locales/fr.ts +6 -3
  55. package/src/components/entities/entity-settings-form/locales/hr.ts +4 -2
  56. package/src/components/entities/entity-settings-form/locales/it.ts +6 -3
  57. package/src/components/entities/entity-settings-form/locales/nl.ts +6 -3
  58. package/src/components/entities/entity-settings-form/locales/pl.ts +6 -2
  59. package/src/components/entities/entity-settings-form/locales/pt.ts +6 -3
  60. package/src/components/entities/entity-settings-form/locales/sl.ts +4 -2
  61. package/src/components/entities/settings/tax-rules-settings-form.tsx +31 -13
  62. package/src/components/estimates/create/create-estimate-form.tsx +3 -2
  63. package/src/components/estimates/create/locales/de.ts +2 -1
  64. package/src/components/estimates/create/locales/es.ts +2 -1
  65. package/src/components/estimates/create/locales/fr.ts +2 -1
  66. package/src/components/estimates/create/locales/hr.ts +2 -1
  67. package/src/components/estimates/create/locales/it.ts +2 -1
  68. package/src/components/estimates/create/locales/nl.ts +2 -1
  69. package/src/components/estimates/create/locales/pl.ts +2 -1
  70. package/src/components/estimates/create/locales/pt.ts +2 -1
  71. package/src/components/estimates/create/locales/sl.ts +2 -1
  72. package/src/components/invoices/create/create-invoice-form.tsx +134 -62
  73. package/src/components/invoices/create/locales/de.ts +8 -1
  74. package/src/components/invoices/create/locales/es.ts +8 -1
  75. package/src/components/invoices/create/locales/fr.ts +8 -1
  76. package/src/components/invoices/create/locales/hr.ts +8 -1
  77. package/src/components/invoices/create/locales/it.ts +8 -1
  78. package/src/components/invoices/create/locales/nl.ts +8 -1
  79. package/src/components/invoices/create/locales/pl.ts +8 -1
  80. package/src/components/invoices/create/locales/pt.ts +8 -1
  81. package/src/components/invoices/create/locales/sl.ts +8 -1
  82. package/src/components/invoices/invoices.hooks.ts +1 -1
  83. package/src/components/ui/progress.tsx +27 -0
  84. package/src/generate-schemas.ts +15 -2
  85. package/src/generated/schemas/advanceinvoice.ts +2 -0
  86. package/src/generated/schemas/creditnote.ts +2 -0
  87. package/src/generated/schemas/customer.ts +2 -0
  88. package/src/generated/schemas/deliverynote.ts +2 -0
  89. package/src/generated/schemas/entity.ts +10 -0
  90. package/src/generated/schemas/estimate.ts +2 -0
  91. package/src/generated/schemas/finasettings.ts +4 -3
  92. package/src/generated/schemas/invoice.ts +2 -0
  93. package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +16 -10
  94. package/src/generated/schemas/rendercreditnotepreview_body.ts +16 -10
  95. package/src/generated/schemas/renderdeliverynotepreview_body.ts +14 -7
  96. package/src/generated/schemas/renderestimatepreview_body.ts +14 -7
  97. package/src/generated/schemas/renderinvoicepreview_body.ts +16 -10
  98. package/src/generated/schemas/startpdfexport_body.ts +12 -17
  99. package/src/hooks/use-transaction-type-check.ts +152 -0
  100. package/src/hooks/use-vies-check.ts +7 -131
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Revenue data hook using the entity stats API.
3
3
  * Server-side aggregation for accurate calculations.
4
+ * Sends 7 queries in a single batch request.
4
5
  */
5
- import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
6
- import { useQueries } from "@tanstack/react-query";
7
- import { useSDK } from "@/ui/providers/sdk-provider";
8
- import { STATS_QUERY_CACHE_KEY } from "./use-stats-query";
6
+ import type { StatsQueryDataItem, StatsQueryRequest } from "@spaceinvoices/js-sdk";
7
+ import { useStatsBatchQuery } from "./use-stats-query";
9
8
 
10
9
  export const REVENUE_DATA_CACHE_KEY = "dashboard-revenue-data";
11
10
 
@@ -37,192 +36,117 @@ export type RevenueData = {
37
36
  };
38
37
 
39
38
  export function useRevenueData(entityId: string | undefined) {
40
- const { sdk } = useSDK();
41
39
  const monthRange = getMonthDateRange();
42
40
  const yearRange = getYearDateRange();
43
41
 
44
- const queries = useQueries({
45
- queries: [
46
- // This month revenue (using converted amounts for multi-currency support)
47
- {
48
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-this-month", monthRange.from],
49
- queryFn: async () => {
50
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
51
- return sdk.entityStats.queryEntityStats(
52
- {
53
- metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
54
- table: "invoices",
55
- date_from: monthRange.from,
56
- date_to: monthRange.to,
57
- filters: { is_draft: false },
58
- group_by: ["quote_currency"], // Get the currency for display
59
- },
60
- { entity_id: entityId },
61
- );
62
- },
63
- enabled: !!entityId && !!sdk,
64
- staleTime: 120_000,
65
- },
66
- // This year revenue (using converted amounts for multi-currency support)
67
- {
68
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-this-year", yearRange.from],
69
- queryFn: async () => {
70
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
71
- return sdk.entityStats.queryEntityStats(
72
- {
73
- metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
74
- table: "invoices",
75
- date_from: yearRange.from,
76
- date_to: yearRange.to,
77
- filters: { is_draft: false },
78
- },
79
- { entity_id: entityId },
80
- );
81
- },
82
- enabled: !!entityId && !!sdk,
83
- staleTime: 120_000,
84
- },
85
- // Outstanding (unpaid, not voided)
86
- {
87
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "outstanding"],
88
- queryFn: async () => {
89
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
90
- return sdk.entityStats.queryEntityStats(
91
- {
92
- metrics: [{ type: "sum", field: "total_due", alias: "outstanding" }],
93
- table: "invoices",
94
- filters: { is_draft: false, paid_in_full: false },
95
- },
96
- { entity_id: entityId },
97
- );
98
- },
99
- enabled: !!entityId && !!sdk,
100
- staleTime: 120_000,
101
- },
102
- // Overdue (past due date, unpaid)
103
- {
104
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "overdue"],
105
- queryFn: async () => {
106
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
107
- return sdk.entityStats.queryEntityStats(
108
- {
109
- metrics: [
110
- { type: "sum", field: "total_due", alias: "overdue" },
111
- { type: "count", alias: "count" },
112
- ],
113
- table: "invoices",
114
- filters: { is_draft: false, paid_in_full: false },
115
- group_by: ["overdue_bucket"],
116
- },
117
- { entity_id: entityId },
118
- );
119
- },
120
- enabled: !!entityId && !!sdk,
121
- staleTime: 120_000,
122
- },
123
- // Credit notes: this month
124
- {
125
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-this-month", monthRange.from],
126
- queryFn: async () => {
127
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
128
- return sdk.entityStats.queryEntityStats(
129
- {
130
- metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
131
- table: "credit_notes",
132
- date_from: monthRange.from,
133
- date_to: monthRange.to,
134
- filters: { is_draft: false },
135
- },
136
- { entity_id: entityId },
137
- );
138
- },
139
- enabled: !!entityId && !!sdk,
140
- staleTime: 120_000,
141
- },
142
- // Credit notes: this year
143
- {
144
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-this-year", yearRange.from],
145
- queryFn: async () => {
146
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
147
- return sdk.entityStats.queryEntityStats(
148
- {
149
- metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
150
- table: "credit_notes",
151
- date_from: yearRange.from,
152
- date_to: yearRange.to,
153
- filters: { is_draft: false },
154
- },
155
- { entity_id: entityId },
156
- );
157
- },
158
- enabled: !!entityId && !!sdk,
159
- staleTime: 120_000,
160
- },
161
- // Credit notes: outstanding
162
- {
163
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-outstanding"],
164
- queryFn: async () => {
165
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
166
- return sdk.entityStats.queryEntityStats(
167
- {
168
- metrics: [{ type: "sum", field: "total_due", alias: "outstanding" }],
169
- table: "credit_notes",
170
- filters: { is_draft: false, paid_in_full: false },
171
- },
172
- { entity_id: entityId },
173
- );
174
- },
175
- enabled: !!entityId && !!sdk,
176
- staleTime: 120_000,
177
- },
178
- ],
179
- });
42
+ const queries: StatsQueryRequest[] = [
43
+ // [0] This month revenue (using converted amounts for multi-currency support)
44
+ {
45
+ metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
46
+ table: "invoices",
47
+ date_from: monthRange.from,
48
+ date_to: monthRange.to,
49
+ filters: { is_draft: false },
50
+ group_by: ["quote_currency"],
51
+ },
52
+ // [1] This year revenue
53
+ {
54
+ metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
55
+ table: "invoices",
56
+ date_from: yearRange.from,
57
+ date_to: yearRange.to,
58
+ filters: { is_draft: false },
59
+ },
60
+ // [2] Outstanding (unpaid, not voided)
61
+ {
62
+ metrics: [{ type: "sum", field: "total_due", alias: "outstanding" }],
63
+ table: "invoices",
64
+ filters: { is_draft: false, paid_in_full: false },
65
+ },
66
+ // [3] Overdue (past due date, unpaid)
67
+ {
68
+ metrics: [
69
+ { type: "sum", field: "total_due", alias: "overdue" },
70
+ { type: "count", alias: "count" },
71
+ ],
72
+ table: "invoices",
73
+ filters: { is_draft: false, paid_in_full: false },
74
+ group_by: ["overdue_bucket"],
75
+ },
76
+ // [4] Credit notes: this month
77
+ {
78
+ metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
79
+ table: "credit_notes",
80
+ date_from: monthRange.from,
81
+ date_to: monthRange.to,
82
+ filters: { is_draft: false },
83
+ },
84
+ // [5] Credit notes: this year
85
+ {
86
+ metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
87
+ table: "credit_notes",
88
+ date_from: yearRange.from,
89
+ date_to: yearRange.to,
90
+ filters: { is_draft: false },
91
+ },
92
+ // [6] Credit notes: outstanding
93
+ {
94
+ metrics: [{ type: "sum", field: "total_due", alias: "outstanding" }],
95
+ table: "credit_notes",
96
+ filters: { is_draft: false, paid_in_full: false },
97
+ },
98
+ ];
99
+
100
+ const { data: results, isLoading } = useStatsBatchQuery(entityId, "revenue-data", queries, {
101
+ select: (batch) => {
102
+ const [thisMonthRes, thisYearRes, outstandingRes, overdueRes, cnThisMonthRes, cnThisYearRes, cnOutstandingRes] =
103
+ batch;
180
104
 
181
- const [
182
- thisMonthQuery,
183
- thisYearQuery,
184
- outstandingQuery,
185
- overdueQuery,
186
- cnThisMonthQuery,
187
- cnThisYearQuery,
188
- cnOutstandingQuery,
189
- ] = queries;
105
+ // Extract this month revenue and currency (may have multiple rows if grouped by quote_currency)
106
+ const thisMonthData = thisMonthRes.data || [];
107
+ const thisMonthRevenue = thisMonthData.reduce(
108
+ (sum: number, row: StatsQueryDataItem) => sum + (Number(row.revenue) || 0),
109
+ 0,
110
+ );
111
+ const currency = (thisMonthData[0]?.quote_currency as string) || "EUR";
190
112
 
191
- // Extract this month revenue and currency (may have multiple rows if grouped by quote_currency)
192
- const thisMonthData = thisMonthQuery.data?.data || [];
193
- const thisMonthRevenue = thisMonthData.reduce(
194
- (sum: number, row: StatsQueryDataItem) => sum + (Number(row.revenue) || 0),
195
- 0,
196
- );
197
- // Get currency from first row with data
198
- const currency = (thisMonthData[0]?.quote_currency as string) || "EUR";
113
+ // Credit note totals
114
+ const cnThisMonth = Number(cnThisMonthRes.data?.[0]?.revenue) || 0;
115
+ const cnThisYear = Number(cnThisYearRes.data?.[0]?.revenue) || 0;
116
+ const cnOutstanding = Number(cnOutstandingRes.data?.[0]?.outstanding) || 0;
199
117
 
200
- // Credit note totals
201
- const cnThisMonth = Number(cnThisMonthQuery.data?.data?.[0]?.revenue) || 0;
202
- const cnThisYear = Number(cnThisYearQuery.data?.data?.[0]?.revenue) || 0;
203
- const cnOutstanding = Number(cnOutstandingQuery.data?.data?.[0]?.outstanding) || 0;
118
+ // Extract overdue data (buckets other than "current") — stays invoice-only
119
+ const overdueData = overdueRes.data || [];
120
+ const overdueBuckets = overdueData.filter((row: StatsQueryDataItem) => row.overdue_bucket !== "current");
121
+ const totalOverdue = overdueBuckets.reduce(
122
+ (sum: number, row: StatsQueryDataItem) => sum + (Number(row.overdue) || 0),
123
+ 0,
124
+ );
125
+ const overdueCount = overdueBuckets.reduce(
126
+ (sum: number, row: StatsQueryDataItem) => sum + (Number(row.count) || 0),
127
+ 0,
128
+ );
204
129
 
205
- // Extract overdue data (buckets other than "current") — stays invoice-only
206
- const overdueData = overdueQuery.data?.data || [];
207
- const overdueBuckets = overdueData.filter((row: StatsQueryDataItem) => row.overdue_bucket !== "current");
208
- const totalOverdue = overdueBuckets.reduce(
209
- (sum: number, row: StatsQueryDataItem) => sum + (Number(row.overdue) || 0),
210
- 0,
211
- );
212
- const overdueCount = overdueBuckets.reduce(
213
- (sum: number, row: StatsQueryDataItem) => sum + (Number(row.count) || 0),
214
- 0,
215
- );
130
+ return {
131
+ thisMonth: thisMonthRevenue - cnThisMonth,
132
+ thisYear: (Number(thisYearRes.data?.[0]?.revenue) || 0) - cnThisYear,
133
+ outstanding: (Number(outstandingRes.data?.[0]?.outstanding) || 0) - cnOutstanding,
134
+ overdue: totalOverdue,
135
+ overdueCount,
136
+ currency,
137
+ } as RevenueData;
138
+ },
139
+ });
216
140
 
217
141
  return {
218
- data: {
219
- thisMonth: thisMonthRevenue - cnThisMonth,
220
- thisYear: (Number(thisYearQuery.data?.data?.[0]?.revenue) || 0) - cnThisYear,
221
- outstanding: (Number(outstandingQuery.data?.data?.[0]?.outstanding) || 0) - cnOutstanding,
222
- overdue: totalOverdue,
223
- overdueCount,
224
- currency, // Currency from document data
225
- } as RevenueData,
226
- isLoading: queries.some((q) => q.isLoading),
142
+ data: results ?? {
143
+ thisMonth: 0,
144
+ thisYear: 0,
145
+ outstanding: 0,
146
+ overdue: 0,
147
+ overdueCount: 0,
148
+ currency: "EUR",
149
+ },
150
+ isLoading,
227
151
  };
228
152
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Stats counts hook using the entity stats API.
3
3
  * Server-side counting for accurate totals.
4
+ * Sends 4 queries in a single batch request.
4
5
  */
5
- import { useQueries } from "@tanstack/react-query";
6
- import { useSDK } from "@/ui/providers/sdk-provider";
7
- import { STATS_QUERY_CACHE_KEY } from "./use-stats-query";
6
+ import type { StatsQueryRequest } from "@spaceinvoices/js-sdk";
7
+ import { useStatsBatchQuery } from "./use-stats-query";
8
8
 
9
9
  export const STATS_COUNTS_CACHE_KEY = "dashboard-stats-counts";
10
10
 
@@ -16,74 +16,24 @@ export type StatsCountsData = {
16
16
  };
17
17
 
18
18
  export function useStatsCountsData(entityId: string | undefined) {
19
- const { sdk } = useSDK();
19
+ const queries: StatsQueryRequest[] = [
20
+ { metrics: [{ type: "count", alias: "total" }], table: "invoices" },
21
+ { metrics: [{ type: "count", alias: "total" }], table: "estimates" },
22
+ { metrics: [{ type: "count", alias: "total" }], table: "customers" },
23
+ { metrics: [{ type: "count", alias: "total" }], table: "items" },
24
+ ];
20
25
 
21
- const queries = useQueries({
22
- queries: [
23
- // Invoices count
24
- {
25
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-invoices"],
26
- queryFn: async () => {
27
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
28
- return sdk.entityStats.queryEntityStats(
29
- { metrics: [{ type: "count", alias: "total" }], table: "invoices" },
30
- { entity_id: entityId },
31
- );
32
- },
33
- enabled: !!entityId && !!sdk,
34
- staleTime: 120_000,
35
- },
36
- // Estimates count
37
- {
38
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-estimates"],
39
- queryFn: async () => {
40
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
41
- return sdk.entityStats.queryEntityStats(
42
- { metrics: [{ type: "count", alias: "total" }], table: "estimates" },
43
- { entity_id: entityId },
44
- );
45
- },
46
- enabled: !!entityId && !!sdk,
47
- staleTime: 120_000,
48
- },
49
- // Customers count
50
- {
51
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-customers"],
52
- queryFn: async () => {
53
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
54
- return sdk.entityStats.queryEntityStats(
55
- { metrics: [{ type: "count", alias: "total" }], table: "customers" },
56
- { entity_id: entityId },
57
- );
58
- },
59
- enabled: !!entityId && !!sdk,
60
- staleTime: 120_000,
61
- },
62
- // Items count
63
- {
64
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-items"],
65
- queryFn: async () => {
66
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
67
- return sdk.entityStats.queryEntityStats(
68
- { metrics: [{ type: "count", alias: "total" }], table: "items" },
69
- { entity_id: entityId },
70
- );
71
- },
72
- enabled: !!entityId && !!sdk,
73
- staleTime: 120_000,
74
- },
75
- ],
26
+ const { data: results, isLoading } = useStatsBatchQuery(entityId, "stats-counts", queries, {
27
+ select: (batch) => ({
28
+ invoices: Number(batch[0].data?.[0]?.total) || 0,
29
+ estimates: Number(batch[1].data?.[0]?.total) || 0,
30
+ customers: Number(batch[2].data?.[0]?.total) || 0,
31
+ items: Number(batch[3].data?.[0]?.total) || 0,
32
+ }),
76
33
  });
77
34
 
78
- const [invoicesQuery, estimatesQuery, customersQuery, itemsQuery] = queries;
79
-
80
35
  return {
81
- data: {
82
- invoices: Number(invoicesQuery.data?.data?.[0]?.total) || 0,
83
- estimates: Number(estimatesQuery.data?.data?.[0]?.total) || 0,
84
- customers: Number(customersQuery.data?.data?.[0]?.total) || 0,
85
- items: Number(itemsQuery.data?.data?.[0]?.total) || 0,
86
- } as StatsCountsData,
87
- isLoading: queries.some((q) => q.isLoading),
36
+ data: (results ?? { invoices: 0, estimates: 0, customers: 0, items: 0 }) as StatsCountsData,
37
+ isLoading,
88
38
  };
89
39
  }
@@ -2,7 +2,7 @@
2
2
  * Base hook for stats queries using the entity stats API.
3
3
  * Provides server-side aggregation instead of client-side calculation.
4
4
  */
5
- import type { StatsQueryRequest, StatsQueryResponse } from "@spaceinvoices/js-sdk";
5
+ import type { StatsQueryBatchResponse, StatsQueryRequest, StatsQueryResponse } from "@spaceinvoices/js-sdk";
6
6
  import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
7
7
  import { useSDK } from "@/ui/providers/sdk-provider";
8
8
 
@@ -14,8 +14,9 @@ export type StatsQueryOptions<TData = StatsQueryResponse> = Omit<
14
14
  >;
15
15
 
16
16
  /**
17
- * Generic hook for executing stats queries.
18
- * Use this as a base for specific stats hooks.
17
+ * Generic hook for executing a single stats query.
18
+ * Wraps the query in a batch array and unwraps the first result.
19
+ * Use this as a base for simple stats hooks.
19
20
  */
20
21
  export function useStatsQuery<TData = StatsQueryResponse>(
21
22
  entityId: string | undefined,
@@ -28,11 +29,40 @@ export function useStatsQuery<TData = StatsQueryResponse>(
28
29
  queryKey: [STATS_QUERY_CACHE_KEY, entityId, query],
29
30
  queryFn: async () => {
30
31
  if (!entityId || !sdk) throw new Error("Missing entity or SDK");
31
- // SDK's wrapMethod already unwraps success response and throws on error
32
- return await sdk.entityStats.queryEntityStats(query, { entity_id: entityId });
32
+ const results = await sdk.entityStats.queryEntityStats([query], { entity_id: entityId });
33
+ return results[0];
33
34
  },
34
35
  enabled: !!entityId && !!sdk,
35
36
  staleTime: 120_000, // 2 minutes
36
37
  ...options,
37
38
  });
38
39
  }
40
+
41
+ export type StatsBatchQueryOptions<TData = StatsQueryBatchResponse> = Omit<
42
+ UseQueryOptions<StatsQueryBatchResponse, Error, TData>,
43
+ "queryKey" | "queryFn"
44
+ >;
45
+
46
+ /**
47
+ * Hook for executing a batch of stats queries in a single request.
48
+ * Returns all results in the same order as the input queries.
49
+ */
50
+ export function useStatsBatchQuery<TData = StatsQueryBatchResponse>(
51
+ entityId: string | undefined,
52
+ queryKey: string,
53
+ queries: StatsQueryRequest[],
54
+ options?: StatsBatchQueryOptions<TData>,
55
+ ) {
56
+ const { sdk } = useSDK();
57
+
58
+ return useQuery({
59
+ queryKey: [STATS_QUERY_CACHE_KEY, entityId, queryKey, queries],
60
+ queryFn: async () => {
61
+ if (!entityId || !sdk) throw new Error("Missing entity or SDK");
62
+ return sdk.entityStats.queryEntityStats(queries, { entity_id: entityId });
63
+ },
64
+ enabled: !!entityId && !!sdk,
65
+ staleTime: 120_000,
66
+ ...options,
67
+ });
68
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Tax collected hook - uses stats API to aggregate tax amounts by rate.
3
3
  * Shows tax breakdown for previous month and current year.
4
+ * Sends 2 queries in a single batch request.
4
5
  */
5
- import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
6
- import { useQueries } from "@tanstack/react-query";
7
- import { useSDK } from "@/ui/providers/sdk-provider";
6
+ import type { StatsQueryDataItem, StatsQueryRequest } from "@spaceinvoices/js-sdk";
7
+ import { useStatsBatchQuery } from "../shared/use-stats-query";
8
8
 
9
9
  export const TAX_COLLECTED_CACHE_KEY = "dashboard-tax-collected";
10
10
 
@@ -55,91 +55,73 @@ function transformTaxData(data: StatsQueryDataItem[]): TaxByRate[] {
55
55
  return data
56
56
  .filter((row) => row.rate != null && row.tax_total != null)
57
57
  .map((row) => ({
58
- name: "Tax", // Could be enhanced to include tax name if available
58
+ name: "Tax",
59
59
  rate: Number(row.rate),
60
60
  amount: Number(row.tax_total),
61
61
  }))
62
- .sort((a, b) => b.rate - a.rate); // Sort by rate descending
62
+ .sort((a, b) => b.rate - a.rate);
63
63
  }
64
64
 
65
65
  export function useTaxCollectedData(entityId: string | undefined) {
66
- const { sdk } = useSDK();
67
66
  const prevMonthRange = getPreviousMonthDateRange();
68
67
  const yearRange = getYearDateRange();
69
68
 
70
- const queries = useQueries({
71
- queries: [
72
- // Previous month taxes - aggregated by rate using stats API
73
- {
74
- queryKey: [TAX_COLLECTED_CACHE_KEY, entityId, "prev-month", prevMonthRange.from],
75
- queryFn: async () => {
76
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
77
- // Use invoice_taxes virtual table to aggregate taxes by rate
78
- return sdk.entityStats.queryEntityStats(
79
- {
80
- table: "invoice_taxes",
81
- metrics: [{ type: "sum", field: "tax", alias: "tax_total" }],
82
- group_by: ["rate", "quote_currency"],
83
- date_from: prevMonthRange.from,
84
- date_to: prevMonthRange.to,
85
- filters: { is_draft: false, voided_at: null },
86
- },
87
- { entity_id: entityId },
88
- );
89
- },
90
- enabled: !!entityId && !!sdk,
91
- staleTime: 120_000, // 1 minute
92
- },
93
- // Current year taxes - aggregated by rate using stats API
94
- {
95
- queryKey: [TAX_COLLECTED_CACHE_KEY, entityId, "year", yearRange.from],
96
- queryFn: async () => {
97
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
98
- return sdk.entityStats.queryEntityStats(
99
- {
100
- table: "invoice_taxes",
101
- metrics: [{ type: "sum", field: "tax", alias: "tax_total" }],
102
- group_by: ["rate", "quote_currency"],
103
- date_from: yearRange.from,
104
- date_to: yearRange.to,
105
- filters: { is_draft: false, voided_at: null },
106
- },
107
- { entity_id: entityId },
108
- );
109
- },
110
- enabled: !!entityId && !!sdk,
111
- staleTime: 120_000,
112
- },
113
- ],
114
- });
69
+ const queries: StatsQueryRequest[] = [
70
+ // [0] Previous month taxes
71
+ {
72
+ table: "invoice_taxes",
73
+ metrics: [{ type: "sum", field: "tax", alias: "tax_total" }],
74
+ group_by: ["rate", "quote_currency"],
75
+ date_from: prevMonthRange.from,
76
+ date_to: prevMonthRange.to,
77
+ filters: { is_draft: false, voided_at: null },
78
+ },
79
+ // [1] Current year taxes
80
+ {
81
+ table: "invoice_taxes",
82
+ metrics: [{ type: "sum", field: "tax", alias: "tax_total" }],
83
+ group_by: ["rate", "quote_currency"],
84
+ date_from: yearRange.from,
85
+ date_to: yearRange.to,
86
+ filters: { is_draft: false, voided_at: null },
87
+ },
88
+ ];
115
89
 
116
- const [prevMonthQuery, yearQuery] = queries;
90
+ const {
91
+ data: results,
92
+ isLoading,
93
+ error,
94
+ } = useStatsBatchQuery(entityId, "tax-collected", queries, {
95
+ select: (batch) => {
96
+ const prevMonthTaxes = transformTaxData(batch[0].data || []);
97
+ const yearTaxes = transformTaxData(batch[1].data || []);
117
98
 
118
- // Transform stats data to TaxByRate arrays
119
- const prevMonthTaxes = transformTaxData(prevMonthQuery.data?.data || []);
120
- const yearTaxes = transformTaxData(yearQuery.data?.data || []);
99
+ const currency =
100
+ (batch[0].data?.[0]?.quote_currency as string) || (batch[1].data?.[0]?.quote_currency as string) || "EUR";
121
101
 
122
- // Get currency from first result or default
123
- const currency =
124
- (prevMonthQuery.data?.data?.[0]?.quote_currency as string) ||
125
- (yearQuery.data?.data?.[0]?.quote_currency as string) ||
126
- "EUR";
102
+ return {
103
+ previousMonth: {
104
+ label: prevMonthRange.label,
105
+ taxes: prevMonthTaxes,
106
+ total: prevMonthTaxes.reduce((sum, t) => sum + t.amount, 0),
107
+ },
108
+ currentYear: {
109
+ label: yearRange.label,
110
+ taxes: yearTaxes,
111
+ total: yearTaxes.reduce((sum, t) => sum + t.amount, 0),
112
+ },
113
+ currency,
114
+ } as TaxCollectedData;
115
+ },
116
+ });
127
117
 
128
118
  return {
129
- data: {
130
- previousMonth: {
131
- label: prevMonthRange.label,
132
- taxes: prevMonthTaxes,
133
- total: prevMonthTaxes.reduce((sum, t) => sum + t.amount, 0),
134
- },
135
- currentYear: {
136
- label: yearRange.label,
137
- taxes: yearTaxes,
138
- total: yearTaxes.reduce((sum, t) => sum + t.amount, 0),
139
- },
140
- currency,
141
- } as TaxCollectedData,
142
- isLoading: queries.some((q) => q.isLoading),
143
- error: queries.find((q) => q.error)?.error,
119
+ data: results ?? {
120
+ previousMonth: { label: prevMonthRange.label, taxes: [], total: 0 },
121
+ currentYear: { label: yearRange.label, taxes: [], total: 0 },
122
+ currency: "EUR",
123
+ },
124
+ isLoading,
125
+ error,
144
126
  };
145
127
  }