@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,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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
63
|
+
return { paid, pending, overdue, voided } as InvoiceStatusData;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
100
66
|
|
|
101
67
|
return {
|
|
102
|
-
data: { paid, pending, overdue, voided }
|
|
103
|
-
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 {
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
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 =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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:
|
|
119
|
-
currency,
|
|
120
|
-
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";
|