@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.
- package/cli/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +60 -44
- package/src/components/advance-invoices/create/locales/de.ts +2 -1
- package/src/components/advance-invoices/create/locales/es.ts +2 -1
- package/src/components/advance-invoices/create/locales/fr.ts +2 -1
- package/src/components/advance-invoices/create/locales/hr.ts +2 -1
- package/src/components/advance-invoices/create/locales/it.ts +2 -1
- package/src/components/advance-invoices/create/locales/nl.ts +2 -1
- package/src/components/advance-invoices/create/locales/pl.ts +2 -1
- package/src/components/advance-invoices/create/locales/pt.ts +2 -1
- package/src/components/advance-invoices/create/locales/sl.ts +2 -1
- package/src/components/credit-notes/create/create-credit-note-form.tsx +52 -42
- package/src/components/credit-notes/create/locales/de.ts +2 -1
- package/src/components/credit-notes/create/locales/es.ts +2 -1
- package/src/components/credit-notes/create/locales/fr.ts +2 -1
- package/src/components/credit-notes/create/locales/hr.ts +2 -1
- package/src/components/credit-notes/create/locales/it.ts +2 -1
- package/src/components/credit-notes/create/locales/nl.ts +2 -1
- package/src/components/credit-notes/create/locales/pl.ts +2 -1
- package/src/components/credit-notes/create/locales/pt.ts +2 -1
- package/src/components/credit-notes/create/locales/sl.ts +2 -1
- package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -92
- package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +48 -82
- package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +22 -31
- package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +33 -48
- package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +56 -76
- package/src/components/dashboard/shared/index.ts +1 -1
- package/src/components/dashboard/shared/use-revenue-data.ts +106 -182
- package/src/components/dashboard/shared/use-stats-counts.ts +18 -68
- package/src/components/dashboard/shared/use-stats-query.ts +35 -5
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +57 -75
- package/src/components/dashboard/top-customers-chart/use-top-customers.ts +38 -49
- package/src/components/delivery-notes/create/create-delivery-note-form.tsx +3 -2
- package/src/components/delivery-notes/create/locales/de.ts +2 -1
- package/src/components/delivery-notes/create/locales/es.ts +2 -1
- package/src/components/delivery-notes/create/locales/fr.ts +2 -1
- package/src/components/delivery-notes/create/locales/hr.ts +2 -1
- package/src/components/delivery-notes/create/locales/it.ts +2 -1
- package/src/components/delivery-notes/create/locales/nl.ts +2 -1
- package/src/components/delivery-notes/create/locales/pl.ts +2 -1
- package/src/components/delivery-notes/create/locales/pt.ts +2 -1
- package/src/components/delivery-notes/create/locales/sl.ts +2 -1
- package/src/components/documents/create/document-details-section.tsx +6 -4
- package/src/components/documents/create/document-recipient-section.tsx +30 -1
- package/src/components/documents/create/live-preview.tsx +15 -28
- package/src/components/documents/create/prepare-document-submission.ts +1 -0
- package/src/components/documents/create/use-document-customer-form.ts +4 -0
- package/src/components/documents/shared/document-preview-skeleton.tsx +63 -0
- package/src/components/documents/shared/index.ts +1 -0
- package/src/components/documents/view/document-actions-bar.tsx +29 -7
- package/src/components/entities/entity-settings-form/locales/de.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/es.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/fr.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/hr.ts +4 -2
- package/src/components/entities/entity-settings-form/locales/it.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/nl.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/pl.ts +6 -2
- package/src/components/entities/entity-settings-form/locales/pt.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/sl.ts +4 -2
- package/src/components/entities/settings/tax-rules-settings-form.tsx +31 -13
- package/src/components/estimates/create/create-estimate-form.tsx +3 -2
- package/src/components/estimates/create/locales/de.ts +2 -1
- package/src/components/estimates/create/locales/es.ts +2 -1
- package/src/components/estimates/create/locales/fr.ts +2 -1
- package/src/components/estimates/create/locales/hr.ts +2 -1
- package/src/components/estimates/create/locales/it.ts +2 -1
- package/src/components/estimates/create/locales/nl.ts +2 -1
- package/src/components/estimates/create/locales/pl.ts +2 -1
- package/src/components/estimates/create/locales/pt.ts +2 -1
- package/src/components/estimates/create/locales/sl.ts +2 -1
- package/src/components/invoices/create/create-invoice-form.tsx +134 -62
- package/src/components/invoices/create/locales/de.ts +8 -1
- package/src/components/invoices/create/locales/es.ts +8 -1
- package/src/components/invoices/create/locales/fr.ts +8 -1
- package/src/components/invoices/create/locales/hr.ts +8 -1
- package/src/components/invoices/create/locales/it.ts +8 -1
- package/src/components/invoices/create/locales/nl.ts +8 -1
- package/src/components/invoices/create/locales/pl.ts +8 -1
- package/src/components/invoices/create/locales/pt.ts +8 -1
- package/src/components/invoices/create/locales/sl.ts +8 -1
- package/src/components/invoices/invoices.hooks.ts +1 -1
- package/src/components/ui/progress.tsx +27 -0
- package/src/generate-schemas.ts +15 -2
- package/src/generated/schemas/advanceinvoice.ts +2 -0
- package/src/generated/schemas/creditnote.ts +2 -0
- package/src/generated/schemas/customer.ts +2 -0
- package/src/generated/schemas/deliverynote.ts +2 -0
- package/src/generated/schemas/entity.ts +10 -0
- package/src/generated/schemas/estimate.ts +2 -0
- package/src/generated/schemas/finasettings.ts +4 -3
- package/src/generated/schemas/invoice.ts +2 -0
- package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +16 -10
- package/src/generated/schemas/rendercreditnotepreview_body.ts +16 -10
- package/src/generated/schemas/renderdeliverynotepreview_body.ts +14 -7
- package/src/generated/schemas/renderestimatepreview_body.ts +14 -7
- package/src/generated/schemas/renderinvoicepreview_body.ts +16 -10
- package/src/generated/schemas/startpdfexport_body.ts +12 -17
- package/src/hooks/use-transaction-type-check.ts +152 -0
- 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 {
|
|
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 =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
220
|
-
thisYear:
|
|
221
|
-
outstanding:
|
|
222
|
-
overdue:
|
|
223
|
-
overdueCount,
|
|
224
|
-
currency,
|
|
225
|
-
}
|
|
226
|
-
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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
*
|
|
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
|
-
|
|
32
|
-
return
|
|
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 {
|
|
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",
|
|
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);
|
|
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 =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
}
|