@spaceinvoices/react-ui 0.4.4 → 0.4.5

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 (147) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +61 -11
  4. package/src/components/advance-invoices/create/locales/de.ts +15 -0
  5. package/src/components/advance-invoices/create/locales/es.ts +15 -0
  6. package/src/components/advance-invoices/create/locales/fr.ts +15 -0
  7. package/src/components/advance-invoices/create/locales/hr.ts +15 -0
  8. package/src/components/advance-invoices/create/locales/it.ts +15 -0
  9. package/src/components/advance-invoices/create/locales/nl.ts +15 -0
  10. package/src/components/advance-invoices/create/locales/pl.ts +15 -0
  11. package/src/components/advance-invoices/create/locales/pt.ts +15 -0
  12. package/src/components/advance-invoices/create/locales/sl.ts +15 -0
  13. package/src/components/advance-invoices/list/list-row-actions.tsx +48 -1
  14. package/src/components/advance-invoices/list/list-table.tsx +21 -1
  15. package/src/components/advance-invoices/list/locales/de.ts +4 -0
  16. package/src/components/advance-invoices/list/locales/en.ts +3 -0
  17. package/src/components/advance-invoices/list/locales/es.ts +4 -0
  18. package/src/components/advance-invoices/list/locales/fr.ts +4 -0
  19. package/src/components/advance-invoices/list/locales/hr.ts +3 -0
  20. package/src/components/advance-invoices/list/locales/it.ts +4 -0
  21. package/src/components/advance-invoices/list/locales/nl.ts +4 -0
  22. package/src/components/advance-invoices/list/locales/pl.ts +3 -0
  23. package/src/components/advance-invoices/list/locales/pt.ts +4 -0
  24. package/src/components/advance-invoices/list/locales/sl.ts +3 -0
  25. package/src/components/credit-notes/create/create-credit-note-form.tsx +71 -8
  26. package/src/components/credit-notes/create/locales/de.ts +16 -0
  27. package/src/components/credit-notes/create/locales/es.ts +16 -0
  28. package/src/components/credit-notes/create/locales/fr.ts +16 -0
  29. package/src/components/credit-notes/create/locales/hr.ts +16 -0
  30. package/src/components/credit-notes/create/locales/it.ts +16 -0
  31. package/src/components/credit-notes/create/locales/nl.ts +16 -0
  32. package/src/components/credit-notes/create/locales/pl.ts +16 -0
  33. package/src/components/credit-notes/create/locales/pt.ts +16 -0
  34. package/src/components/credit-notes/create/locales/sl.ts +17 -1
  35. package/src/components/credit-notes/list/list-row-actions.tsx +44 -1
  36. package/src/components/credit-notes/list/list-table.tsx +16 -2
  37. package/src/components/credit-notes/list/locales/de.ts +2 -0
  38. package/src/components/credit-notes/list/locales/en.ts +2 -0
  39. package/src/components/credit-notes/list/locales/es.ts +2 -0
  40. package/src/components/credit-notes/list/locales/fr.ts +2 -0
  41. package/src/components/credit-notes/list/locales/hr.ts +2 -0
  42. package/src/components/credit-notes/list/locales/it.ts +2 -0
  43. package/src/components/credit-notes/list/locales/nl.ts +2 -0
  44. package/src/components/credit-notes/list/locales/pl.ts +2 -0
  45. package/src/components/credit-notes/list/locales/pt.ts +2 -0
  46. package/src/components/credit-notes/list/locales/sl.ts +2 -0
  47. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -9
  48. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +77 -48
  49. package/src/components/dashboard/shared/use-revenue-data.ts +77 -9
  50. package/src/components/delivery-notes/create/create-delivery-note-form.tsx +67 -7
  51. package/src/components/delivery-notes/create/locales/de.ts +16 -0
  52. package/src/components/delivery-notes/create/locales/es.ts +16 -0
  53. package/src/components/delivery-notes/create/locales/fr.ts +16 -0
  54. package/src/components/delivery-notes/create/locales/hr.ts +16 -0
  55. package/src/components/delivery-notes/create/locales/it.ts +16 -0
  56. package/src/components/delivery-notes/create/locales/nl.ts +16 -0
  57. package/src/components/delivery-notes/create/locales/pl.ts +16 -0
  58. package/src/components/delivery-notes/create/locales/pt.ts +16 -0
  59. package/src/components/delivery-notes/create/locales/sl.ts +17 -1
  60. package/src/components/delivery-notes/list/list-table.tsx +17 -8
  61. package/src/components/delivery-notes/list/locales/de.ts +32 -1
  62. package/src/components/delivery-notes/list/locales/en.ts +31 -0
  63. package/src/components/delivery-notes/list/locales/es.ts +32 -1
  64. package/src/components/delivery-notes/list/locales/fr.ts +32 -1
  65. package/src/components/delivery-notes/list/locales/hr.ts +32 -1
  66. package/src/components/delivery-notes/list/locales/it.ts +32 -1
  67. package/src/components/delivery-notes/list/locales/nl.ts +32 -1
  68. package/src/components/delivery-notes/list/locales/pl.ts +32 -1
  69. package/src/components/delivery-notes/list/locales/pt.ts +32 -1
  70. package/src/components/delivery-notes/list/locales/sl.ts +32 -1
  71. package/src/components/documents/create/document-add-item-form.tsx +70 -0
  72. package/src/components/documents/create/document-details-section.tsx +122 -2
  73. package/src/components/documents/create/document-items-section.tsx +21 -4
  74. package/src/components/documents/create/live-preview.tsx +24 -4
  75. package/src/components/documents/create/mark-as-paid-section.tsx +29 -20
  76. package/src/components/documents/create/prepare-document-submission.ts +19 -7
  77. package/src/components/documents/shared/document-preview-display.tsx +3 -4
  78. package/src/components/documents/view/document-actions-bar.tsx +7 -27
  79. package/src/components/documents/view/document-details-card.tsx +26 -9
  80. package/src/components/documents/view/locales/de.ts +2 -0
  81. package/src/components/documents/view/locales/es.ts +2 -0
  82. package/src/components/documents/view/locales/fr.ts +2 -0
  83. package/src/components/documents/view/locales/hr.ts +2 -0
  84. package/src/components/documents/view/locales/it.ts +2 -0
  85. package/src/components/documents/view/locales/nl.ts +2 -0
  86. package/src/components/documents/view/locales/pl.ts +2 -0
  87. package/src/components/documents/view/locales/pt.ts +2 -0
  88. package/src/components/documents/view/locales/sl.ts +3 -1
  89. package/src/components/documents/view/use-document-download.ts +6 -3
  90. package/src/components/entities/create-entity-form.tsx +2 -1
  91. package/src/components/entities/entity-settings-form/input-with-preview.tsx +30 -2
  92. package/src/components/entities/entity-settings-form/locales/de.ts +1 -0
  93. package/src/components/entities/entity-settings-form/locales/es.ts +1 -0
  94. package/src/components/entities/entity-settings-form/locales/fr.ts +1 -0
  95. package/src/components/entities/entity-settings-form/locales/hr.ts +1 -0
  96. package/src/components/entities/entity-settings-form/locales/it.ts +1 -0
  97. package/src/components/entities/entity-settings-form/locales/nl.ts +1 -0
  98. package/src/components/entities/entity-settings-form/locales/pl.ts +1 -0
  99. package/src/components/entities/entity-settings-form/locales/pt.ts +1 -0
  100. package/src/components/entities/entity-settings-form/locales/sl.ts +1 -0
  101. package/src/components/entities/settings/company-settings-form.tsx +173 -20
  102. package/src/components/estimates/create/create-estimate-form.tsx +64 -6
  103. package/src/components/estimates/create/locales/de.ts +16 -0
  104. package/src/components/estimates/create/locales/es.ts +16 -0
  105. package/src/components/estimates/create/locales/fr.ts +16 -0
  106. package/src/components/estimates/create/locales/hr.ts +16 -0
  107. package/src/components/estimates/create/locales/it.ts +16 -0
  108. package/src/components/estimates/create/locales/nl.ts +16 -0
  109. package/src/components/estimates/create/locales/pl.ts +16 -0
  110. package/src/components/estimates/create/locales/pt.ts +16 -0
  111. package/src/components/estimates/create/locales/sl.ts +17 -1
  112. package/src/components/estimates/list/list-table.tsx +11 -2
  113. package/src/components/estimates/list/locales/de.ts +1 -0
  114. package/src/components/estimates/list/locales/en.ts +1 -0
  115. package/src/components/estimates/list/locales/es.ts +1 -0
  116. package/src/components/estimates/list/locales/fr.ts +1 -0
  117. package/src/components/estimates/list/locales/hr.ts +1 -0
  118. package/src/components/estimates/list/locales/it.ts +1 -0
  119. package/src/components/estimates/list/locales/nl.ts +1 -0
  120. package/src/components/estimates/list/locales/pl.ts +1 -0
  121. package/src/components/estimates/list/locales/pt.ts +1 -0
  122. package/src/components/estimates/list/locales/sl.ts +1 -0
  123. package/src/components/export/document-export-form.tsx +46 -12
  124. package/src/components/invoices/create/create-invoice-form.tsx +58 -10
  125. package/src/components/invoices/create/locales/de.ts +15 -0
  126. package/src/components/invoices/create/locales/es.ts +15 -0
  127. package/src/components/invoices/create/locales/fr.ts +15 -0
  128. package/src/components/invoices/create/locales/hr.ts +15 -0
  129. package/src/components/invoices/create/locales/it.ts +15 -0
  130. package/src/components/invoices/create/locales/nl.ts +15 -0
  131. package/src/components/invoices/create/locales/pl.ts +15 -0
  132. package/src/components/invoices/create/locales/pt.ts +15 -0
  133. package/src/components/invoices/create/locales/sl.ts +16 -1
  134. package/src/components/invoices/view/fiscalization-status-card.tsx +10 -8
  135. package/src/components/table/search-input.tsx +17 -0
  136. package/src/generated/schemas/advanceinvoice.ts +4 -2
  137. package/src/generated/schemas/creditnote.ts +2 -1
  138. package/src/generated/schemas/deliverynote.ts +2 -1
  139. package/src/generated/schemas/estimate.ts +4 -2
  140. package/src/generated/schemas/invoice.ts +2 -1
  141. package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +17 -23
  142. package/src/generated/schemas/rendercreditnotepreview_body.ts +17 -23
  143. package/src/generated/schemas/renderdeliverynotepreview_body.ts +17 -23
  144. package/src/generated/schemas/renderestimatepreview_body.ts +17 -23
  145. package/src/generated/schemas/renderinvoicepreview_body.ts +19 -23
  146. package/src/hooks/use-duplicate-document.ts +16 -9
  147. package/src/hooks/use-vies-check.ts +3 -0
@@ -20,7 +20,7 @@ export function useCollectionRateData(entityId: string | undefined) {
20
20
 
21
21
  const queries = useQueries({
22
22
  queries: [
23
- // Total invoiced (non-voided)
23
+ // Total invoiced (including voided — counter credit notes cancel them out)
24
24
  {
25
25
  queryKey: [STATS_QUERY_CACHE_KEY, entityId, "total-invoiced"],
26
26
  queryFn: async () => {
@@ -29,7 +29,7 @@ export function useCollectionRateData(entityId: string | undefined) {
29
29
  {
30
30
  metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
31
31
  table: "invoices",
32
- filters: { is_draft: false, voided_at: null },
32
+ filters: { is_draft: false },
33
33
  },
34
34
  { entity_id: entityId },
35
35
  );
@@ -37,15 +37,50 @@ export function useCollectionRateData(entityId: string | undefined) {
37
37
  enabled: !!entityId && !!sdk,
38
38
  staleTime: 120_000,
39
39
  },
40
- // Total collected (payments)
40
+ // Invoice payments (credit_note_id IS NULL)
41
41
  {
42
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "total-collected"],
42
+ queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-payments"],
43
43
  queryFn: async () => {
44
44
  if (!entityId || !sdk) throw new Error("Missing entity or SDK");
45
45
  return sdk.entityStats.queryEntityStats(
46
46
  {
47
47
  metrics: [{ type: "sum", field: "amount", alias: "total" }],
48
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 },
49
84
  },
50
85
  { entity_id: entityId },
51
86
  );
@@ -56,17 +91,21 @@ export function useCollectionRateData(entityId: string | undefined) {
56
91
  ],
57
92
  });
58
93
 
59
- const [invoicedQuery, collectedQuery] = queries;
94
+ const [invoicedQuery, invoicePaymentsQuery, cnPaymentsQuery, cnQuery] = queries;
60
95
 
61
96
  const totalInvoiced = Number(invoicedQuery.data?.data?.[0]?.total) || 0;
62
- const totalCollected = Number(collectedQuery.data?.data?.[0]?.total) || 0;
63
- const collectionRate = totalInvoiced > 0 ? (totalCollected / totalInvoiced) * 100 : 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;
64
103
 
65
104
  return {
66
105
  data: {
67
106
  collectionRate,
68
- totalCollected,
69
- totalInvoiced,
107
+ totalCollected: netCollected,
108
+ totalInvoiced: netInvoiced,
70
109
  currency: "EUR", // TODO: Get from entity settings
71
110
  } as CollectionRateData,
72
111
  isLoading: queries.some((q) => q.isLoading),
@@ -3,7 +3,7 @@
3
3
  * Server-side aggregation by month for accurate trend data.
4
4
  */
5
5
  import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
6
- import { useQuery } from "@tanstack/react-query";
6
+ import { useQueries } from "@tanstack/react-query";
7
7
  import { useSDK } from "@/ui/providers/sdk-provider";
8
8
  import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
9
9
 
@@ -37,57 +37,86 @@ export function useRevenueTrendData(entityId: string | undefined) {
37
37
 
38
38
  const { months, startDate, endDate } = getLastMonths(6);
39
39
 
40
- const query = useQuery({
41
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-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: "total_with_tax_converted", alias: "revenue" }],
47
- table: "invoices",
48
- date_from: startDate,
49
- date_to: endDate,
50
- filters: { is_draft: false, voided_at: null },
51
- group_by: ["month", "quote_currency"], // Include currency for display
52
- order_by: [{ field: "month", direction: "asc" }],
40
+ const sharedQueryParams = {
41
+ date_from: startDate,
42
+ date_to: endDate,
43
+ filters: { is_draft: false },
44
+ group_by: ["month", "quote_currency"],
45
+ order_by: [{ field: "month", direction: "asc" as const }],
46
+ };
47
+
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
+ );
53
78
  },
54
- { entity_id: entityId },
55
- );
56
- },
57
- enabled: !!entityId && !!sdk,
58
- staleTime: 120_000,
59
- select: (response) => {
60
- // Build a map of all months with 0 revenue
61
- const monthMap: Record<string, number> = {};
62
- for (const month of months) {
63
- monthMap[month] = 0;
64
- }
79
+ enabled: !!entityId && !!sdk,
80
+ staleTime: 120_000,
81
+ },
82
+ ],
83
+ });
65
84
 
66
- // Fill in the actual revenue from the API response
67
- // Sum up revenues per month (in case of multiple rows due to quote_currency grouping)
68
- const data = response.data || [];
69
- let currency = "EUR";
70
- for (const row of data as StatsQueryDataItem[]) {
71
- const month = String(row.month);
72
- if (month in monthMap) {
73
- monthMap[month] += Number(row.revenue) || 0;
74
- }
75
- // Get currency from first row with data
76
- if (row.quote_currency && currency === "EUR") {
77
- currency = String(row.quote_currency);
78
- }
79
- }
85
+ const [invoiceQuery, cnQuery] = queries;
80
86
 
81
- return {
82
- data: months.map((month) => ({ month, revenue: monthMap[month] })),
83
- currency, // Currency from document data
84
- };
85
- },
86
- });
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
+ }
94
+
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
+ }
107
+
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
+ }
87
116
 
88
117
  return {
89
- data: query.data?.data || [],
90
- currency: query.data?.currency || "EUR",
91
- isLoading: query.isLoading,
118
+ data: months.map((month) => ({ month, revenue: monthMap[month] - cnMonthMap[month] })),
119
+ currency,
120
+ isLoading: queries.some((q) => q.isLoading),
92
121
  };
93
122
  }
@@ -54,7 +54,7 @@ export function useRevenueData(entityId: string | undefined) {
54
54
  table: "invoices",
55
55
  date_from: monthRange.from,
56
56
  date_to: monthRange.to,
57
- filters: { is_draft: false, voided_at: null },
57
+ filters: { is_draft: false },
58
58
  group_by: ["quote_currency"], // Get the currency for display
59
59
  },
60
60
  { entity_id: entityId },
@@ -74,7 +74,7 @@ export function useRevenueData(entityId: string | undefined) {
74
74
  table: "invoices",
75
75
  date_from: yearRange.from,
76
76
  date_to: yearRange.to,
77
- filters: { is_draft: false, voided_at: null },
77
+ filters: { is_draft: false },
78
78
  },
79
79
  { entity_id: entityId },
80
80
  );
@@ -91,7 +91,7 @@ export function useRevenueData(entityId: string | undefined) {
91
91
  {
92
92
  metrics: [{ type: "sum", field: "total_due", alias: "outstanding" }],
93
93
  table: "invoices",
94
- filters: { is_draft: false, voided_at: null, paid_in_full: false },
94
+ filters: { is_draft: false, paid_in_full: false },
95
95
  },
96
96
  { entity_id: entityId },
97
97
  );
@@ -111,7 +111,7 @@ export function useRevenueData(entityId: string | undefined) {
111
111
  { type: "count", alias: "count" },
112
112
  ],
113
113
  table: "invoices",
114
- filters: { is_draft: false, voided_at: null, paid_in_full: false },
114
+ filters: { is_draft: false, paid_in_full: false },
115
115
  group_by: ["overdue_bucket"],
116
116
  },
117
117
  { entity_id: entityId },
@@ -120,10 +120,73 @@ export function useRevenueData(entityId: string | undefined) {
120
120
  enabled: !!entityId && !!sdk,
121
121
  staleTime: 120_000,
122
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
+ },
123
178
  ],
124
179
  });
125
180
 
126
- const [thisMonthQuery, thisYearQuery, outstandingQuery, overdueQuery] = queries;
181
+ const [
182
+ thisMonthQuery,
183
+ thisYearQuery,
184
+ outstandingQuery,
185
+ overdueQuery,
186
+ cnThisMonthQuery,
187
+ cnThisYearQuery,
188
+ cnOutstandingQuery,
189
+ ] = queries;
127
190
 
128
191
  // Extract this month revenue and currency (may have multiple rows if grouped by quote_currency)
129
192
  const thisMonthData = thisMonthQuery.data?.data || [];
@@ -134,7 +197,12 @@ export function useRevenueData(entityId: string | undefined) {
134
197
  // Get currency from first row with data
135
198
  const currency = (thisMonthData[0]?.quote_currency as string) || "EUR";
136
199
 
137
- // Extract overdue data (buckets other than "current")
200
+ // Credit note totals
201
+ const cnThisMonth = Number(cnThisMonthQuery.data?.data?.[0]?.revenue) || 0;
202
+ const cnThisYear = Number(cnThisYearQuery.data?.data?.[0]?.revenue) || 0;
203
+ const cnOutstanding = Number(cnOutstandingQuery.data?.data?.[0]?.outstanding) || 0;
204
+
205
+ // Extract overdue data (buckets other than "current") — stays invoice-only
138
206
  const overdueData = overdueQuery.data?.data || [];
139
207
  const overdueBuckets = overdueData.filter((row: StatsQueryDataItem) => row.overdue_bucket !== "current");
140
208
  const totalOverdue = overdueBuckets.reduce(
@@ -148,9 +216,9 @@ export function useRevenueData(entityId: string | undefined) {
148
216
 
149
217
  return {
150
218
  data: {
151
- thisMonth: thisMonthRevenue,
152
- thisYear: Number(thisYearQuery.data?.data?.[0]?.revenue) || 0,
153
- outstanding: Number(outstandingQuery.data?.data?.[0]?.outstanding) || 0,
219
+ thisMonth: thisMonthRevenue - cnThisMonth,
220
+ thisYear: (Number(thisYearQuery.data?.data?.[0]?.revenue) || 0) - cnThisYear,
221
+ outstanding: (Number(outstandingQuery.data?.data?.[0]?.outstanding) || 0) - cnOutstanding,
154
222
  overdue: totalOverdue,
155
223
  overdueCount,
156
224
  currency, // Currency from document data
@@ -10,12 +10,17 @@ import { Form } from "@/ui/components/ui/form";
10
10
  import { Label } from "@/ui/components/ui/label";
11
11
  import { createDeliveryNoteSchema } from "@/ui/generated/schemas";
12
12
  import { useNextDocumentNumber } from "@/ui/hooks/use-next-document-number";
13
+ import { useViesCheck } from "@/ui/hooks/use-vies-check";
13
14
  import type { ComponentTranslationProps } from "@/ui/lib/translation";
14
15
  import { createTranslation } from "@/ui/lib/translation";
15
16
  import { useEntities } from "@/ui/providers/entities-context";
16
17
  import { useFormFooterRegistration } from "@/ui/providers/form-footer-context";
17
18
  import { CUSTOMERS_CACHE_KEY } from "../../customers/customers.hooks";
18
- import { DocumentDetailsSection, DocumentNoteField } from "../../documents/create/document-details-section";
19
+ import {
20
+ DocumentDetailsSection,
21
+ DocumentNoteField,
22
+ DocumentTaxClauseField,
23
+ } from "../../documents/create/document-details-section";
19
24
  import { DocumentItemsSection, type PriceModesMap } from "../../documents/create/document-items-section";
20
25
  import { DocumentRecipientSection } from "../../documents/create/document-recipient-section";
21
26
  import type { DocumentTypes } from "../../documents/types";
@@ -106,13 +111,18 @@ export default function CreateDeliveryNoteForm({
106
111
  // Cast customer to form schema type (API type may have additional fields)
107
112
  customer: (initialValues?.customer as CreateDeliveryNoteFormValues["customer"]) ?? undefined,
108
113
  items: initialValues?.items?.length
109
- ? initialValues.items.map((item) => ({
114
+ ? initialValues.items.map((item: any) => ({
115
+ type: item.type,
110
116
  name: item.name || "",
111
117
  description: item.description || "",
112
- quantity: item.quantity ?? 1,
113
- // Use gross_price if set, otherwise use price
114
- price: item.gross_price ?? item.price,
115
- taxes: item.taxes || [],
118
+ ...(item.type !== "separator"
119
+ ? {
120
+ quantity: item.quantity ?? 1,
121
+ // Use gross_price if set, otherwise use price
122
+ price: item.gross_price ?? item.price,
123
+ taxes: item.taxes || [],
124
+ }
125
+ : {}),
116
126
  }))
117
127
  : [
118
128
  {
@@ -125,6 +135,7 @@ export default function CreateDeliveryNoteForm({
125
135
  ],
126
136
  currency_code: initialValues?.currency_code || activeEntity?.currency_code || "EUR",
127
137
  note: initialValues?.note ?? "",
138
+ tax_clause: (initialValues as any)?.tax_clause ?? "",
128
139
  },
129
140
  });
130
141
 
@@ -162,6 +173,37 @@ export default function CreateDeliveryNoteForm({
162
173
  control: form.control,
163
174
  });
164
175
 
176
+ // ============================================================================
177
+ // VIES Check - determine if reverse charge applies
178
+ // ============================================================================
179
+ const {
180
+ reverseChargeApplies,
181
+ transactionType,
182
+ isFetching: isViesFetching,
183
+ warning: viesWarning,
184
+ } = useViesCheck({
185
+ issuerCountryCode: activeEntity?.country_code,
186
+ isTaxSubject: activeEntity?.is_tax_subject ?? true,
187
+ customerCountry: formValues.customer?.country,
188
+ customerCountryCode: formValues.customer?.country_code,
189
+ customerTaxNumber: formValues.customer?.tax_number,
190
+ enabled: !!activeEntity,
191
+ });
192
+
193
+ // Auto-populate tax_clause from entity settings when transaction type changes
194
+ const effectiveTransactionType = transactionType ?? "domestic";
195
+ const prevTransactionTypeRef = useRef<string | undefined>(undefined);
196
+ useEffect(() => {
197
+ if (effectiveTransactionType === prevTransactionTypeRef.current) return;
198
+ prevTransactionTypeRef.current = effectiveTransactionType;
199
+
200
+ const taxClauseDefaults = (activeEntity?.settings as any)?.tax_clause_defaults;
201
+ if (!taxClauseDefaults) return;
202
+
203
+ const clause = taxClauseDefaults[effectiveTransactionType] ?? "";
204
+ form.setValue("tax_clause", clause);
205
+ }, [effectiveTransactionType, activeEntity, form]);
206
+
165
207
  // Extract customer management logic into a custom hook
166
208
  const {
167
209
  originalCustomer,
@@ -232,7 +274,7 @@ export default function CreateDeliveryNoteForm({
232
274
  useFormFooterRegistration({
233
275
  formId: "create-delivery-note-form",
234
276
  isPending,
235
- isDirty: form.formState.isDirty,
277
+ isDirty: form.formState.isDirty || !!initialValues,
236
278
  label: t("Create Delivery Note"),
237
279
  secondaryAction,
238
280
  });
@@ -313,6 +355,10 @@ export default function CreateDeliveryNoteForm({
313
355
  maxTaxesPerItem={activeEntity?.country_rules?.max_taxes_per_item}
314
356
  priceModesRef={priceModesRef}
315
357
  initialPriceModes={initialPriceModes}
358
+ taxesDisabled={reverseChargeApplies}
359
+ taxesDisabledMessage={
360
+ reverseChargeApplies ? t("Reverse charge - tax exempt EU B2B sale") : viesWarning ? viesWarning : undefined
361
+ }
316
362
  />
317
363
 
318
364
  <DocumentNoteField
@@ -326,6 +372,20 @@ export default function CreateDeliveryNoteForm({
326
372
  customer: formValues.customer as any,
327
373
  }}
328
374
  />
375
+
376
+ <DocumentTaxClauseField
377
+ control={form.control}
378
+ t={t}
379
+ entity={activeEntity}
380
+ document={{
381
+ number: formValues.number,
382
+ date: formValues.date,
383
+ currency_code: formValues.currency_code,
384
+ customer: formValues.customer as any,
385
+ }}
386
+ transactionType={transactionType}
387
+ isTransactionTypeFetching={isViesFetching}
388
+ />
329
389
  </form>
330
390
  </Form>
331
391
  );
@@ -47,4 +47,20 @@ export default {
47
47
  "Gross price (tax included)": "Bruttopreis (inkl. MwSt.)",
48
48
  "Net price (before tax)": "Nettopreis (exkl. MwSt.)",
49
49
  "Hide prices": "Preise ausblenden",
50
+ // Separator items
51
+ "Add separator": "Trennzeile hinzufügen",
52
+ "Section header": "Abschnittsüberschrift",
53
+ "Section title...": "Abschnittstitel...",
54
+ // Transaction type
55
+ "Transaction type": "Transaktionstyp",
56
+ Domestic: "Inland",
57
+ "EU B2B": "EU B2B",
58
+ "EU B2C": "EU B2C",
59
+ Export: "Export",
60
+ "Determining transaction type...": "Transaktionstyp wird ermittelt...",
61
+ "This invoice will not be fiscalized (non-domestic transaction)":
62
+ "Diese Rechnung wird nicht fiskalisiert (nicht-inländische Transaktion)",
63
+ "Tax Clause": "Steuerklausel",
64
+ "Add tax clause...": "Steuerklausel hinzufügen...",
65
+ "Reverse charge - tax exempt EU B2B sale": "Umkehrung der Steuerschuldnerschaft - steuerbefreiter EU B2B Verkauf",
50
66
  } as const;
@@ -46,4 +46,20 @@ export default {
46
46
  "Gross price (tax included)": "Precio bruto (impuestos incluidos)",
47
47
  "Net price (before tax)": "Precio neto (antes de impuestos)",
48
48
  "Hide prices": "Ocultar precios",
49
+ // Separator items
50
+ "Add separator": "Añadir separador",
51
+ "Section header": "Encabezado de sección",
52
+ "Section title...": "Título de sección...",
53
+ // Transaction type
54
+ "Transaction type": "Tipo de transacción",
55
+ Domestic: "Nacional",
56
+ "EU B2B": "EU B2B",
57
+ "EU B2C": "EU B2C",
58
+ Export: "Exportación",
59
+ "Determining transaction type...": "Determinando tipo de transacción...",
60
+ "This invoice will not be fiscalized (non-domestic transaction)":
61
+ "Esta factura no será fiscalizada (transacción no nacional)",
62
+ "Tax Clause": "Cláusula fiscal",
63
+ "Add tax clause...": "Agregar cláusula fiscal...",
64
+ "Reverse charge - tax exempt EU B2B sale": "Inversión del sujeto pasivo - venta EU B2B exenta de impuestos",
49
65
  } as const;
@@ -47,4 +47,20 @@ export default {
47
47
  "Gross price (tax included)": "Prix brut (taxes incluses)",
48
48
  "Net price (before tax)": "Prix net (avant taxes)",
49
49
  "Hide prices": "Masquer les prix",
50
+ // Separator items
51
+ "Add separator": "Ajouter un séparateur",
52
+ "Section header": "En-tête de section",
53
+ "Section title...": "Titre de section...",
54
+ // Transaction type
55
+ "Transaction type": "Type de transaction",
56
+ Domestic: "Nationale",
57
+ "EU B2B": "EU B2B",
58
+ "EU B2C": "EU B2C",
59
+ Export: "Exportation",
60
+ "Determining transaction type...": "Détermination du type de transaction...",
61
+ "This invoice will not be fiscalized (non-domestic transaction)":
62
+ "Cette facture ne sera pas fiscalisée (transaction non nationale)",
63
+ "Tax Clause": "Clause fiscale",
64
+ "Add tax clause...": "Ajouter une clause fiscale...",
65
+ "Reverse charge - tax exempt EU B2B sale": "Autoliquidation - vente B2B UE exonérée de taxe",
50
66
  } as const;
@@ -46,4 +46,20 @@ export default {
46
46
  "Gross price (tax included)": "Bruto cijena (s porezom)",
47
47
  "Net price (before tax)": "Neto cijena (prije poreza)",
48
48
  "Hide prices": "Sakrij cijene",
49
+ // Separator items
50
+ "Add separator": "Dodaj separator",
51
+ "Section header": "Naslov odjeljka",
52
+ "Section title...": "Naslov odjeljka...",
53
+ // Transaction type
54
+ "Transaction type": "Vrsta transakcije",
55
+ Domestic: "Domaća",
56
+ "EU B2B": "EU B2B",
57
+ "EU B2C": "EU B2C",
58
+ Export: "Izvoz",
59
+ "Determining transaction type...": "Određivanje vrste transakcije...",
60
+ "This invoice will not be fiscalized (non-domestic transaction)":
61
+ "Ovaj račun neće biti fiskaliziran (nedomaća transakcija)",
62
+ "Tax Clause": "Porezna klauzula",
63
+ "Add tax clause...": "Dodajte poreznu klauzulu...",
64
+ "Reverse charge - tax exempt EU B2B sale": "Prijenos porezne obveze - porezno oslobođena EU B2B prodaja",
49
65
  } as const;
@@ -46,4 +46,20 @@ export default {
46
46
  "Gross price (tax included)": "Prezzo lordo (imposte incluse)",
47
47
  "Net price (before tax)": "Prezzo netto (prima delle imposte)",
48
48
  "Hide prices": "Nascondi prezzi",
49
+ // Separator items
50
+ "Add separator": "Aggiungi separatore",
51
+ "Section header": "Intestazione sezione",
52
+ "Section title...": "Titolo sezione...",
53
+ // Transaction type
54
+ "Transaction type": "Tipo di transazione",
55
+ Domestic: "Nazionale",
56
+ "EU B2B": "EU B2B",
57
+ "EU B2C": "EU B2C",
58
+ Export: "Esportazione",
59
+ "Determining transaction type...": "Determinazione tipo di transazione...",
60
+ "This invoice will not be fiscalized (non-domestic transaction)":
61
+ "Questa fattura non sarà fiscalizzata (transazione non nazionale)",
62
+ "Tax Clause": "Clausola fiscale",
63
+ "Add tax clause...": "Aggiungi clausola fiscale...",
64
+ "Reverse charge - tax exempt EU B2B sale": "Inversione contabile - vendita B2B UE esente da imposta",
49
65
  } as const;
@@ -47,4 +47,20 @@ export default {
47
47
  "Gross price (tax included)": "Brutoprijs (inclusief belasting)",
48
48
  "Net price (before tax)": "Nettoprijs (exclusief belasting)",
49
49
  "Hide prices": "Prijzen verbergen",
50
+ // Separator items
51
+ "Add separator": "Scheidingslijn toevoegen",
52
+ "Section header": "Sectiekop",
53
+ "Section title...": "Sectietitel...",
54
+ // Transaction type
55
+ "Transaction type": "Transactietype",
56
+ Domestic: "Binnenland",
57
+ "EU B2B": "EU B2B",
58
+ "EU B2C": "EU B2C",
59
+ Export: "Export",
60
+ "Determining transaction type...": "Transactietype bepalen...",
61
+ "This invoice will not be fiscalized (non-domestic transaction)":
62
+ "Deze factuur wordt niet gefiscaliseerd (niet-binnenlandse transactie)",
63
+ "Tax Clause": "Belastingclausule",
64
+ "Add tax clause...": "Belastingclausule toevoegen...",
65
+ "Reverse charge - tax exempt EU B2B sale": "Verlegging - belastingvrijgestelde EU B2B verkoop",
50
66
  } as const;
@@ -46,4 +46,20 @@ export default {
46
46
  "Gross price (tax included)": "Cena brutto (z podatkiem)",
47
47
  "Net price (before tax)": "Cena netto (przed podatkiem)",
48
48
  "Hide prices": "Ukryj ceny",
49
+ // Separator items
50
+ "Add separator": "Dodaj separator",
51
+ "Section header": "Nagłówek sekcji",
52
+ "Section title...": "Tytuł sekcji...",
53
+ // Transaction type
54
+ "Transaction type": "Typ transakcji",
55
+ Domestic: "Krajowa",
56
+ "EU B2B": "EU B2B",
57
+ "EU B2C": "EU B2C",
58
+ Export: "Eksport",
59
+ "Determining transaction type...": "Określanie typu transakcji...",
60
+ "This invoice will not be fiscalized (non-domestic transaction)":
61
+ "Ta faktura nie będzie fiskalizowana (transakcja niekrajowa)",
62
+ "Tax Clause": "Klauzula podatkowa",
63
+ "Add tax clause...": "Dodaj klauzulę podatkową...",
64
+ "Reverse charge - tax exempt EU B2B sale": "Odwrotne obciążenie - zwolniona z podatku sprzedaż EU B2B",
49
65
  } as const;