@spaceinvoices/react-ui 0.4.5 → 0.4.7

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 (138) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/components/advance-invoices/advance-invoices.hooks.ts +2 -2
  4. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +146 -74
  5. package/src/components/advance-invoices/create/locales/de.ts +5 -0
  6. package/src/components/advance-invoices/create/locales/es.ts +5 -0
  7. package/src/components/advance-invoices/create/locales/fr.ts +5 -0
  8. package/src/components/advance-invoices/create/locales/hr.ts +5 -0
  9. package/src/components/advance-invoices/create/locales/it.ts +5 -0
  10. package/src/components/advance-invoices/create/locales/nl.ts +5 -0
  11. package/src/components/advance-invoices/create/locales/pl.ts +5 -0
  12. package/src/components/advance-invoices/create/locales/pt.ts +5 -0
  13. package/src/components/advance-invoices/create/locales/sl.ts +5 -0
  14. package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +5 -5
  15. package/src/components/credit-notes/create/create-credit-note-form.tsx +138 -72
  16. package/src/components/credit-notes/create/locales/de.ts +5 -0
  17. package/src/components/credit-notes/create/locales/es.ts +5 -0
  18. package/src/components/credit-notes/create/locales/fr.ts +5 -0
  19. package/src/components/credit-notes/create/locales/hr.ts +5 -0
  20. package/src/components/credit-notes/create/locales/it.ts +5 -0
  21. package/src/components/credit-notes/create/locales/nl.ts +5 -0
  22. package/src/components/credit-notes/create/locales/pl.ts +5 -0
  23. package/src/components/credit-notes/create/locales/pt.ts +5 -0
  24. package/src/components/credit-notes/create/locales/sl.ts +5 -0
  25. package/src/components/credit-notes/credit-notes.hooks.ts +2 -2
  26. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -92
  27. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +48 -82
  28. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +22 -31
  29. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +33 -48
  30. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +56 -76
  31. package/src/components/dashboard/shared/index.ts +1 -1
  32. package/src/components/dashboard/shared/use-revenue-data.ts +106 -182
  33. package/src/components/dashboard/shared/use-stats-counts.ts +18 -68
  34. package/src/components/dashboard/shared/use-stats-query.ts +35 -5
  35. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +57 -75
  36. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +38 -49
  37. package/src/components/delivery-notes/create/create-delivery-note-form.tsx +50 -2
  38. package/src/components/delivery-notes/create/locales/de.ts +5 -0
  39. package/src/components/delivery-notes/create/locales/es.ts +5 -0
  40. package/src/components/delivery-notes/create/locales/fr.ts +5 -0
  41. package/src/components/delivery-notes/create/locales/hr.ts +5 -0
  42. package/src/components/delivery-notes/create/locales/it.ts +5 -0
  43. package/src/components/delivery-notes/create/locales/nl.ts +5 -0
  44. package/src/components/delivery-notes/create/locales/pl.ts +5 -0
  45. package/src/components/delivery-notes/create/locales/pt.ts +5 -0
  46. package/src/components/delivery-notes/create/locales/sl.ts +5 -0
  47. package/src/components/documents/create/document-details-section.tsx +478 -350
  48. package/src/components/documents/create/document-recipient-section.tsx +30 -1
  49. package/src/components/documents/create/live-preview.tsx +15 -28
  50. package/src/components/documents/create/prepare-document-submission.ts +4 -1
  51. package/src/components/documents/create/smart-code-insert-button.tsx +6 -0
  52. package/src/components/documents/create/use-document-customer-form.ts +4 -0
  53. package/src/components/documents/shared/document-preview-skeleton.tsx +63 -0
  54. package/src/components/documents/shared/index.ts +1 -0
  55. package/src/components/documents/view/document-actions-bar.tsx +29 -7
  56. package/src/components/documents/view/document-details-card.tsx +6 -0
  57. package/src/components/documents/view/locales/de.ts +1 -0
  58. package/src/components/documents/view/locales/es.ts +1 -0
  59. package/src/components/documents/view/locales/fr.ts +1 -0
  60. package/src/components/documents/view/locales/hr.ts +1 -0
  61. package/src/components/documents/view/locales/it.ts +1 -0
  62. package/src/components/documents/view/locales/nl.ts +1 -0
  63. package/src/components/documents/view/locales/pl.ts +1 -0
  64. package/src/components/documents/view/locales/pt.ts +1 -0
  65. package/src/components/documents/view/locales/sl.ts +1 -0
  66. package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +6 -0
  67. package/src/components/entities/entity-settings-form/input-with-preview.tsx +2 -145
  68. package/src/components/entities/entity-settings-form/locales/de.ts +4 -0
  69. package/src/components/entities/entity-settings-form/locales/es.ts +4 -0
  70. package/src/components/entities/entity-settings-form/locales/fr.ts +4 -0
  71. package/src/components/entities/entity-settings-form/locales/hr.ts +4 -0
  72. package/src/components/entities/entity-settings-form/locales/it.ts +4 -0
  73. package/src/components/entities/entity-settings-form/locales/nl.ts +4 -0
  74. package/src/components/entities/entity-settings-form/locales/pl.ts +4 -0
  75. package/src/components/entities/entity-settings-form/locales/pt.ts +4 -0
  76. package/src/components/entities/entity-settings-form/locales/sl.ts +4 -0
  77. package/src/components/entities/fina-settings-form/fina-settings-form.tsx +15 -0
  78. package/src/components/entities/fina-settings-form/fina-settings.hooks.ts +5 -1
  79. package/src/components/entities/fina-settings-form/locales/de.ts +3 -0
  80. package/src/components/entities/fina-settings-form/locales/en.ts +3 -0
  81. package/src/components/entities/fina-settings-form/locales/es.ts +3 -0
  82. package/src/components/entities/fina-settings-form/locales/fr.ts +3 -0
  83. package/src/components/entities/fina-settings-form/locales/hr.ts +3 -0
  84. package/src/components/entities/fina-settings-form/locales/it.ts +3 -0
  85. package/src/components/entities/fina-settings-form/locales/nl.ts +3 -0
  86. package/src/components/entities/fina-settings-form/locales/pl.ts +3 -0
  87. package/src/components/entities/fina-settings-form/locales/pt.ts +3 -0
  88. package/src/components/entities/fina-settings-form/locales/sl.ts +3 -0
  89. package/src/components/entities/fina-settings-form/sections/premises-management-section.tsx +4 -4
  90. package/src/components/entities/fina-settings-form/sections/register-premise-dialog.tsx +3 -3
  91. package/src/components/entities/settings/defaults-settings-form.tsx +38 -1
  92. package/src/components/entities/settings/tax-rules-settings-form.tsx +32 -15
  93. package/src/components/estimates/create/create-estimate-form.tsx +46 -4
  94. package/src/components/estimates/create/locales/de.ts +5 -0
  95. package/src/components/estimates/create/locales/es.ts +5 -0
  96. package/src/components/estimates/create/locales/fr.ts +5 -0
  97. package/src/components/estimates/create/locales/hr.ts +5 -0
  98. package/src/components/estimates/create/locales/it.ts +5 -0
  99. package/src/components/estimates/create/locales/nl.ts +5 -0
  100. package/src/components/estimates/create/locales/pl.ts +5 -0
  101. package/src/components/estimates/create/locales/pt.ts +5 -0
  102. package/src/components/estimates/create/locales/sl.ts +5 -0
  103. package/src/components/invoices/create/create-invoice-form.tsx +258 -96
  104. package/src/components/invoices/create/locales/de.ts +19 -0
  105. package/src/components/invoices/create/locales/es.ts +19 -0
  106. package/src/components/invoices/create/locales/fr.ts +19 -0
  107. package/src/components/invoices/create/locales/hr.ts +19 -0
  108. package/src/components/invoices/create/locales/it.ts +19 -0
  109. package/src/components/invoices/create/locales/nl.ts +19 -0
  110. package/src/components/invoices/create/locales/pl.ts +19 -0
  111. package/src/components/invoices/create/locales/pt.ts +19 -0
  112. package/src/components/invoices/create/locales/sl.ts +19 -0
  113. package/src/components/invoices/create/prepare-invoice-submission.ts +5 -5
  114. package/src/components/invoices/invoices.hooks.ts +3 -3
  115. package/src/components/table/table-pagination.tsx +1 -1
  116. package/src/components/ui/progress.tsx +27 -0
  117. package/src/generate-schemas.ts +15 -2
  118. package/src/generated/schemas/advanceinvoice.ts +4 -0
  119. package/src/generated/schemas/creditnote.ts +3 -0
  120. package/src/generated/schemas/customer.ts +2 -0
  121. package/src/generated/schemas/deliverynote.ts +3 -0
  122. package/src/generated/schemas/entity.ts +14 -4
  123. package/src/generated/schemas/entityapikey.ts +19 -0
  124. package/src/generated/schemas/estimate.ts +4 -0
  125. package/src/generated/schemas/finasettings.ts +4 -3
  126. package/src/generated/schemas/index.ts +1 -0
  127. package/src/generated/schemas/invoice.ts +4 -0
  128. package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +17 -11
  129. package/src/generated/schemas/rendercreditnotepreview_body.ts +17 -11
  130. package/src/generated/schemas/renderdeliverynotepreview_body.ts +15 -8
  131. package/src/generated/schemas/renderestimatepreview_body.ts +15 -8
  132. package/src/generated/schemas/renderinvoicepreview_body.ts +17 -11
  133. package/src/generated/schemas/startpdfexport_body.ts +12 -5
  134. package/src/generated/schemas/webhook.ts +4 -0
  135. package/src/hooks/use-transaction-type-check.ts +152 -0
  136. package/src/hooks/use-vies-check.ts +7 -131
  137. package/src/lib/template-variables.tsx +167 -0
  138. package/src/providers/entities-context.tsx +2 -2
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Collection rate hook using the entity stats API.
3
3
  * Server-side aggregation 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 "../shared/use-stats-query";
6
+ import type { StatsQueryRequest } from "@spaceinvoices/js-sdk";
7
+ import { useStatsBatchQuery } from "../shared/use-stats-query";
8
8
 
9
9
  export const COLLECTION_RATE_CACHE_KEY = "dashboard-collection-rate";
10
10
 
@@ -16,98 +16,54 @@ export type CollectionRateData = {
16
16
  };
17
17
 
18
18
  export function useCollectionRateData(entityId: string | undefined) {
19
- const { sdk } = useSDK();
19
+ const queries: StatsQueryRequest[] = [
20
+ // [0] Total invoiced (including voided — counter credit notes cancel them out)
21
+ {
22
+ metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
23
+ table: "invoices",
24
+ filters: { is_draft: false },
25
+ },
26
+ // [1] Invoice payments (credit_note_id IS NULL)
27
+ {
28
+ metrics: [{ type: "sum", field: "amount", alias: "total" }],
29
+ table: "payments",
30
+ filters: { credit_note_id: null },
31
+ },
32
+ // [2] Credit note payments / refunds (credit_note_id IS NOT NULL)
33
+ {
34
+ metrics: [{ type: "sum", field: "amount", alias: "total" }],
35
+ table: "payments",
36
+ filters: { credit_note_id: { not: null } },
37
+ },
38
+ // [3] Credit notes total (subtracted from invoiced to get net revenue)
39
+ {
40
+ metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
41
+ table: "credit_notes",
42
+ filters: { is_draft: false },
43
+ },
44
+ ];
20
45
 
21
- const queries = useQueries({
22
- queries: [
23
- // Total invoiced (including voided — counter credit notes cancel them out)
24
- {
25
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "total-invoiced"],
26
- queryFn: async () => {
27
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
28
- return sdk.entityStats.queryEntityStats(
29
- {
30
- metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
31
- table: "invoices",
32
- filters: { is_draft: false },
33
- },
34
- { entity_id: entityId },
35
- );
36
- },
37
- enabled: !!entityId && !!sdk,
38
- staleTime: 120_000,
39
- },
40
- // Invoice payments (credit_note_id IS NULL)
41
- {
42
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-payments"],
43
- queryFn: async () => {
44
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
45
- return sdk.entityStats.queryEntityStats(
46
- {
47
- metrics: [{ type: "sum", field: "amount", alias: "total" }],
48
- table: "payments",
49
- filters: { credit_note_id: null },
50
- },
51
- { entity_id: entityId },
52
- );
53
- },
54
- enabled: !!entityId && !!sdk,
55
- staleTime: 120_000,
56
- },
57
- // Credit note payments / refunds (credit_note_id IS NOT NULL)
58
- {
59
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-payments"],
60
- queryFn: async () => {
61
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
62
- return sdk.entityStats.queryEntityStats(
63
- {
64
- metrics: [{ type: "sum", field: "amount", alias: "total" }],
65
- table: "payments",
66
- filters: { credit_note_id: { not: null } },
67
- },
68
- { entity_id: entityId },
69
- );
70
- },
71
- enabled: !!entityId && !!sdk,
72
- staleTime: 120_000,
73
- },
74
- // Credit notes total (subtracted from invoiced to get net revenue)
75
- {
76
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-total"],
77
- queryFn: async () => {
78
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
79
- return sdk.entityStats.queryEntityStats(
80
- {
81
- metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
82
- table: "credit_notes",
83
- filters: { is_draft: false },
84
- },
85
- { entity_id: entityId },
86
- );
87
- },
88
- enabled: !!entityId && !!sdk,
89
- staleTime: 120_000,
90
- },
91
- ],
92
- });
93
-
94
- const [invoicedQuery, invoicePaymentsQuery, cnPaymentsQuery, cnQuery] = queries;
46
+ const { data: results, isLoading } = useStatsBatchQuery(entityId, "collection-rate", queries, {
47
+ select: (batch) => {
48
+ const totalInvoiced = Number(batch[0].data?.[0]?.total) || 0;
49
+ const invoicePayments = Number(batch[1].data?.[0]?.total) || 0;
50
+ const cnPayments = Number(batch[2].data?.[0]?.total) || 0;
51
+ const cnTotal = Number(batch[3].data?.[0]?.total) || 0;
52
+ const netInvoiced = totalInvoiced - cnTotal;
53
+ const netCollected = invoicePayments - cnPayments;
54
+ const collectionRate = netInvoiced > 0 ? (netCollected / netInvoiced) * 100 : 0;
95
55
 
96
- const totalInvoiced = Number(invoicedQuery.data?.data?.[0]?.total) || 0;
97
- const invoicePayments = Number(invoicePaymentsQuery.data?.data?.[0]?.total) || 0;
98
- const cnPayments = Number(cnPaymentsQuery.data?.data?.[0]?.total) || 0;
99
- const cnTotal = Number(cnQuery.data?.data?.[0]?.total) || 0;
100
- const netInvoiced = totalInvoiced - cnTotal;
101
- const netCollected = invoicePayments - cnPayments;
102
- const collectionRate = netInvoiced > 0 ? (netCollected / netInvoiced) * 100 : 0;
56
+ return {
57
+ collectionRate,
58
+ totalCollected: netCollected,
59
+ totalInvoiced: netInvoiced,
60
+ currency: "EUR", // TODO: Get from entity settings
61
+ } as CollectionRateData;
62
+ },
63
+ });
103
64
 
104
65
  return {
105
- data: {
106
- collectionRate,
107
- totalCollected: netCollected,
108
- totalInvoiced: netInvoiced,
109
- currency: "EUR", // TODO: Get from entity settings
110
- } as CollectionRateData,
111
- isLoading: queries.some((q) => q.isLoading),
66
+ data: results ?? { collectionRate: 0, totalCollected: 0, totalInvoiced: 0, currency: "EUR" },
67
+ isLoading,
112
68
  };
113
69
  }
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Invoice status hook using the entity stats API.
3
3
  * Server-side counting by invoice status.
4
+ * Sends 3 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 "../shared/use-stats-query";
6
+ import type { StatsQueryDataItem, StatsQueryRequest } from "@spaceinvoices/js-sdk";
7
+ import { useStatsBatchQuery } from "../shared/use-stats-query";
9
8
 
10
9
  export const INVOICE_STATUS_CACHE_KEY = "dashboard-invoice-status";
11
10
 
@@ -17,89 +16,56 @@ export type InvoiceStatusData = {
17
16
  };
18
17
 
19
18
  export function useInvoiceStatusData(entityId: string | undefined) {
20
- const { sdk } = useSDK();
19
+ const queries: StatsQueryRequest[] = [
20
+ // [0] Paid invoices
21
+ {
22
+ metrics: [{ type: "count", alias: "count" }],
23
+ table: "invoices",
24
+ filters: { is_draft: false, voided_at: null, paid_in_full: true },
25
+ },
26
+ // [1] Unpaid invoices grouped by overdue bucket (current = pending, others = overdue)
27
+ {
28
+ metrics: [{ type: "count", alias: "count" }],
29
+ table: "invoices",
30
+ filters: { is_draft: false, voided_at: null, paid_in_full: false },
31
+ group_by: ["overdue_bucket"],
32
+ },
33
+ // [2] Total count to derive voided
34
+ {
35
+ metrics: [{ type: "count", alias: "count" }],
36
+ table: "invoices",
37
+ filters: { is_draft: false },
38
+ },
39
+ ];
21
40
 
22
- const queries = useQueries({
23
- queries: [
24
- // Paid invoices
25
- {
26
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-status-paid"],
27
- queryFn: async () => {
28
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
29
- return sdk.entityStats.queryEntityStats(
30
- {
31
- metrics: [{ type: "count", alias: "count" }],
32
- table: "invoices",
33
- filters: { is_draft: false, voided_at: null, paid_in_full: true },
34
- },
35
- { entity_id: entityId },
36
- );
37
- },
38
- enabled: !!entityId && !!sdk,
39
- staleTime: 120_000,
40
- },
41
- // Unpaid invoices grouped by overdue bucket (current = pending, others = overdue)
42
- {
43
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-status-unpaid"],
44
- queryFn: async () => {
45
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
46
- return sdk.entityStats.queryEntityStats(
47
- {
48
- metrics: [{ type: "count", alias: "count" }],
49
- table: "invoices",
50
- filters: { is_draft: false, voided_at: null, paid_in_full: false },
51
- group_by: ["overdue_bucket"],
52
- },
53
- { entity_id: entityId },
54
- );
55
- },
56
- enabled: !!entityId && !!sdk,
57
- staleTime: 120_000,
58
- },
59
- // Total count to derive voided
60
- {
61
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-status-total"],
62
- queryFn: async () => {
63
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
64
- return sdk.entityStats.queryEntityStats(
65
- {
66
- metrics: [{ type: "count", alias: "count" }],
67
- table: "invoices",
68
- filters: { is_draft: false },
69
- },
70
- { entity_id: entityId },
71
- );
72
- },
73
- enabled: !!entityId && !!sdk,
74
- staleTime: 120_000,
75
- },
76
- ],
77
- });
78
-
79
- const [paidQuery, unpaidQuery, totalQuery] = queries;
41
+ const { data: results, isLoading } = useStatsBatchQuery(entityId, "invoice-status", queries, {
42
+ select: (batch) => {
43
+ const paid = Number(batch[0].data?.[0]?.count) || 0;
80
44
 
81
- const paid = Number(paidQuery.data?.data?.[0]?.count) || 0;
45
+ // Parse unpaid buckets
46
+ const unpaidData = batch[1].data || [];
47
+ let pending = 0;
48
+ let overdue = 0;
49
+ for (const row of unpaidData as StatsQueryDataItem[]) {
50
+ const bucket = String(row.overdue_bucket);
51
+ const count = Number(row.count) || 0;
52
+ if (bucket === "current") {
53
+ pending = count;
54
+ } else {
55
+ overdue += count;
56
+ }
57
+ }
82
58
 
83
- // Parse unpaid buckets
84
- const unpaidData = unpaidQuery.data?.data || [];
85
- let pending = 0;
86
- let overdue = 0;
87
- for (const row of unpaidData as StatsQueryDataItem[]) {
88
- const bucket = String(row.overdue_bucket);
89
- const count = Number(row.count) || 0;
90
- if (bucket === "current") {
91
- pending = count;
92
- } else {
93
- overdue += count;
94
- }
95
- }
59
+ // Calculate voided as: total - paid - pending - overdue
60
+ const total = Number(batch[2].data?.[0]?.count) || 0;
61
+ const voided = Math.max(0, total - paid - pending - overdue);
96
62
 
97
- // Calculate voided as: total - paid - pending - overdue
98
- const total = Number(totalQuery.data?.data?.[0]?.count) || 0;
99
- const voided = Math.max(0, total - paid - pending - overdue);
63
+ return { paid, pending, overdue, voided } as InvoiceStatusData;
64
+ },
65
+ });
100
66
 
101
67
  return {
102
- data: { paid, pending, overdue, voided } as InvoiceStatusData,
103
- isLoading: queries.some((q) => q.isLoading),
68
+ data: results ?? { paid: 0, pending: 0, overdue: 0, voided: 0 },
69
+ isLoading,
104
70
  };
105
71
  }
@@ -1,47 +1,38 @@
1
1
  /**
2
2
  * Payment methods hook using the entity stats API.
3
3
  * Server-side aggregation by payment type.
4
+ * Sends 1 query in a batch request.
4
5
  */
5
6
  import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
6
- import { useQuery } from "@tanstack/react-query";
7
- import { useSDK } from "@/ui/providers/sdk-provider";
8
- import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
7
+ import { useStatsQuery } from "../shared/use-stats-query";
9
8
 
10
9
  export const PAYMENT_METHODS_CACHE_KEY = "dashboard-payment-methods";
11
10
 
12
11
  export type PaymentMethodsData = { type: string; count: number; amount: number }[];
13
12
 
14
13
  export function usePaymentMethodsData(entityId: string | undefined) {
15
- const { sdk } = useSDK();
16
-
17
- const query = useQuery({
18
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "payment-methods"],
19
- queryFn: async () => {
20
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
21
- return sdk.entityStats.queryEntityStats(
22
- {
23
- metrics: [
24
- { type: "count", alias: "count" },
25
- { type: "sum", field: "amount", alias: "amount" },
26
- ],
27
- table: "payments",
28
- group_by: ["type"],
29
- order_by: [{ field: "amount", direction: "desc" }],
30
- },
31
- { entity_id: entityId },
32
- );
14
+ const query = useStatsQuery(
15
+ entityId,
16
+ {
17
+ metrics: [
18
+ { type: "count", alias: "count" },
19
+ { type: "sum", field: "amount", alias: "amount" },
20
+ ],
21
+ table: "payments",
22
+ group_by: ["type"],
23
+ order_by: [{ field: "amount", direction: "desc" }],
33
24
  },
34
- enabled: !!entityId && !!sdk,
35
- staleTime: 120_000,
36
- select: (response) => {
37
- const data = response.data || [];
38
- return (data as StatsQueryDataItem[]).map((row) => ({
39
- type: String(row.type || "other"),
40
- count: Number(row.count) || 0,
41
- amount: Number(row.amount) || 0,
42
- }));
25
+ {
26
+ select: (response) => {
27
+ const data = response.data || [];
28
+ return (data as StatsQueryDataItem[]).map((row) => ({
29
+ type: String(row.type || "other"),
30
+ count: Number(row.count) || 0,
31
+ amount: Number(row.amount) || 0,
32
+ }));
33
+ },
43
34
  },
44
- });
35
+ );
45
36
 
46
37
  return {
47
38
  data: query.data || [],
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Payment trend hook using the entity stats API.
3
3
  * Server-side aggregation by month for accurate trend data.
4
+ * Sends 1 query in a batch request.
4
5
  */
5
6
  import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
6
- import { useQuery } from "@tanstack/react-query";
7
- import { useSDK } from "@/ui/providers/sdk-provider";
8
- import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
7
+ import { useStatsQuery } from "../shared/use-stats-query";
9
8
 
10
9
  export const PAYMENT_TREND_CACHE_KEY = "dashboard-payment-trend";
11
10
 
@@ -13,9 +12,7 @@ function getLastMonths(count: number): { months: string[]; startDate: string; en
13
12
  const months: string[] = [];
14
13
  const now = new Date();
15
14
 
16
- // Start of the month 'count-1' months ago
17
15
  const startDate = new Date(now.getFullYear(), now.getMonth() - (count - 1), 1);
18
- // End of current month
19
16
  const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
20
17
 
21
18
  for (let i = count - 1; i >= 0; i--) {
@@ -33,56 +30,44 @@ function getLastMonths(count: number): { months: string[]; startDate: string; en
33
30
  export type PaymentTrendData = { month: string; amount: number }[];
34
31
 
35
32
  export function usePaymentTrendData(entityId: string | undefined) {
36
- const { sdk } = useSDK();
37
-
38
33
  const { months, startDate, endDate } = getLastMonths(6);
39
34
 
40
- const query = useQuery({
41
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "payment-trend", startDate, endDate],
42
- queryFn: async () => {
43
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
44
- return sdk.entityStats.queryEntityStats(
45
- {
46
- metrics: [{ type: "sum", field: "amount_converted", alias: "amount" }],
47
- table: "payments",
48
- date_from: startDate,
49
- date_to: endDate,
50
- group_by: ["month", "currency_code"], // Include currency for display
51
- order_by: [{ field: "month", direction: "asc" }],
52
- },
53
- { entity_id: entityId },
54
- );
35
+ const query = useStatsQuery(
36
+ entityId,
37
+ {
38
+ metrics: [{ type: "sum", field: "amount_converted", alias: "amount" }],
39
+ table: "payments",
40
+ date_from: startDate,
41
+ date_to: endDate,
42
+ group_by: ["month", "currency_code"],
43
+ order_by: [{ field: "month", direction: "asc" }],
55
44
  },
56
- enabled: !!entityId && !!sdk,
57
- staleTime: 120_000,
58
- select: (response) => {
59
- // Build a map of all months with 0 amount
60
- const monthMap: Record<string, number> = {};
61
- for (const month of months) {
62
- monthMap[month] = 0;
63
- }
64
-
65
- // Fill in the actual amounts from the API response
66
- // Sum up amounts per month (in case of multiple rows due to currency_code grouping)
67
- const data = response.data || [];
68
- let currency = "EUR";
69
- for (const row of data as StatsQueryDataItem[]) {
70
- const month = String(row.month);
71
- if (month in monthMap) {
72
- monthMap[month] += Number(row.amount) || 0;
45
+ {
46
+ select: (response) => {
47
+ const monthMap: Record<string, number> = {};
48
+ for (const month of months) {
49
+ monthMap[month] = 0;
73
50
  }
74
- // Get currency from first row with data
75
- if (row.currency_code && currency === "EUR") {
76
- currency = String(row.currency_code);
51
+
52
+ const data = response.data || [];
53
+ let currency = "EUR";
54
+ for (const row of data as StatsQueryDataItem[]) {
55
+ const month = String(row.month);
56
+ if (month in monthMap) {
57
+ monthMap[month] += Number(row.amount) || 0;
58
+ }
59
+ if (row.currency_code && currency === "EUR") {
60
+ currency = String(row.currency_code);
61
+ }
77
62
  }
78
- }
79
63
 
80
- return {
81
- data: months.map((month) => ({ month, amount: monthMap[month] })),
82
- currency, // Currency from payment data
83
- };
64
+ return {
65
+ data: months.map((month) => ({ month, amount: monthMap[month] })),
66
+ currency,
67
+ };
68
+ },
84
69
  },
85
- });
70
+ );
86
71
 
87
72
  return {
88
73
  data: query.data?.data || [],
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Revenue trend hook using the entity stats API.
3
3
  * Server-side aggregation by month for accurate trend data.
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";
8
- import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
6
+ import type { StatsQueryDataItem, StatsQueryRequest } from "@spaceinvoices/js-sdk";
7
+ import { useStatsBatchQuery } from "../shared/use-stats-query";
9
8
 
10
9
  export const REVENUE_TREND_CACHE_KEY = "dashboard-revenue-trend";
11
10
 
@@ -13,9 +12,7 @@ function getLastMonths(count: number): { months: string[]; startDate: string; en
13
12
  const months: string[] = [];
14
13
  const now = new Date();
15
14
 
16
- // Start of the month 'count-1' months ago
17
15
  const startDate = new Date(now.getFullYear(), now.getMonth() - (count - 1), 1);
18
- // End of current month
19
16
  const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
20
17
 
21
18
  for (let i = count - 1; i >= 0; i--) {
@@ -33,11 +30,9 @@ function getLastMonths(count: number): { months: string[]; startDate: string; en
33
30
  export type RevenueTrendData = { month: string; revenue: number }[];
34
31
 
35
32
  export function useRevenueTrendData(entityId: string | undefined) {
36
- const { sdk } = useSDK();
37
-
38
33
  const { months, startDate, endDate } = getLastMonths(6);
39
34
 
40
- const sharedQueryParams = {
35
+ const sharedParams = {
41
36
  date_from: startDate,
42
37
  date_to: endDate,
43
38
  filters: { is_draft: false },
@@ -45,78 +40,63 @@ export function useRevenueTrendData(entityId: string | undefined) {
45
40
  order_by: [{ field: "month", direction: "asc" as const }],
46
41
  };
47
42
 
48
- const queries = useQueries({
49
- queries: [
50
- {
51
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-trend", startDate, endDate],
52
- queryFn: async () => {
53
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
54
- return sdk.entityStats.queryEntityStats(
55
- {
56
- metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
57
- table: "invoices",
58
- ...sharedQueryParams,
59
- },
60
- { entity_id: entityId },
61
- );
62
- },
63
- enabled: !!entityId && !!sdk,
64
- staleTime: 120_000,
65
- },
66
- {
67
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-revenue-trend", startDate, endDate],
68
- queryFn: async () => {
69
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
70
- return sdk.entityStats.queryEntityStats(
71
- {
72
- metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
73
- table: "credit_notes",
74
- ...sharedQueryParams,
75
- },
76
- { entity_id: entityId },
77
- );
78
- },
79
- enabled: !!entityId && !!sdk,
80
- staleTime: 120_000,
81
- },
82
- ],
83
- });
43
+ const queries: StatsQueryRequest[] = [
44
+ // [0] Invoice revenue by month
45
+ {
46
+ metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
47
+ table: "invoices",
48
+ ...sharedParams,
49
+ },
50
+ // [1] Credit note revenue by month
51
+ {
52
+ metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
53
+ table: "credit_notes",
54
+ ...sharedParams,
55
+ },
56
+ ];
84
57
 
85
- const [invoiceQuery, cnQuery] = queries;
58
+ const { data: results, isLoading } = useStatsBatchQuery(entityId, "revenue-trend", queries, {
59
+ select: (batch) => {
60
+ // Build month maps
61
+ const monthMap: Record<string, number> = {};
62
+ const cnMonthMap: Record<string, number> = {};
63
+ for (const month of months) {
64
+ monthMap[month] = 0;
65
+ cnMonthMap[month] = 0;
66
+ }
86
67
 
87
- // Build month maps
88
- const monthMap: Record<string, number> = {};
89
- const cnMonthMap: Record<string, number> = {};
90
- for (const month of months) {
91
- monthMap[month] = 0;
92
- cnMonthMap[month] = 0;
93
- }
68
+ // Fill invoice revenue per month
69
+ const invoiceData = (batch[0].data || []) as StatsQueryDataItem[];
70
+ let currency = "EUR";
71
+ for (const row of invoiceData) {
72
+ const month = String(row.month);
73
+ if (month in monthMap) {
74
+ monthMap[month] += Number(row.revenue) || 0;
75
+ }
76
+ if (row.quote_currency && currency === "EUR") {
77
+ currency = String(row.quote_currency);
78
+ }
79
+ }
94
80
 
95
- // Fill invoice revenue per month
96
- const invoiceData = (invoiceQuery.data?.data || []) as StatsQueryDataItem[];
97
- let currency = "EUR";
98
- for (const row of invoiceData) {
99
- const month = String(row.month);
100
- if (month in monthMap) {
101
- monthMap[month] += Number(row.revenue) || 0;
102
- }
103
- if (row.quote_currency && currency === "EUR") {
104
- currency = String(row.quote_currency);
105
- }
106
- }
81
+ // Fill credit note revenue per month
82
+ const cnData = (batch[1].data || []) as StatsQueryDataItem[];
83
+ for (const row of cnData) {
84
+ const month = String(row.month);
85
+ if (month in cnMonthMap) {
86
+ cnMonthMap[month] += Number(row.revenue) || 0;
87
+ }
88
+ }
107
89
 
108
- // Fill credit note revenue per month
109
- const cnData = (cnQuery.data?.data || []) as StatsQueryDataItem[];
110
- for (const row of cnData) {
111
- const month = String(row.month);
112
- if (month in cnMonthMap) {
113
- cnMonthMap[month] += Number(row.revenue) || 0;
114
- }
115
- }
90
+ return {
91
+ data: months.map((month) => ({ month, revenue: monthMap[month] - cnMonthMap[month] })),
92
+ currency,
93
+ };
94
+ },
95
+ });
116
96
 
117
97
  return {
118
- data: months.map((month) => ({ month, revenue: monthMap[month] - cnMonthMap[month] })),
119
- currency,
120
- isLoading: queries.some((q) => q.isLoading),
98
+ data: results?.data || [],
99
+ currency: results?.currency || "EUR",
100
+ isLoading,
121
101
  };
122
102
  }
@@ -2,4 +2,4 @@ export type { RevenueData } from "./use-revenue-data";
2
2
  export { REVENUE_DATA_CACHE_KEY, useRevenueData } from "./use-revenue-data";
3
3
  export type { StatsCountsData } from "./use-stats-counts";
4
4
  export { STATS_COUNTS_CACHE_KEY, useStatsCountsData } from "./use-stats-counts";
5
- export { STATS_QUERY_CACHE_KEY, useStatsQuery } from "./use-stats-query";
5
+ export { STATS_QUERY_CACHE_KEY, useStatsBatchQuery, useStatsQuery } from "./use-stats-query";