@spaceinvoices/react-ui 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +60 -44
  4. package/src/components/advance-invoices/create/locales/de.ts +2 -1
  5. package/src/components/advance-invoices/create/locales/es.ts +2 -1
  6. package/src/components/advance-invoices/create/locales/fr.ts +2 -1
  7. package/src/components/advance-invoices/create/locales/hr.ts +2 -1
  8. package/src/components/advance-invoices/create/locales/it.ts +2 -1
  9. package/src/components/advance-invoices/create/locales/nl.ts +2 -1
  10. package/src/components/advance-invoices/create/locales/pl.ts +2 -1
  11. package/src/components/advance-invoices/create/locales/pt.ts +2 -1
  12. package/src/components/advance-invoices/create/locales/sl.ts +2 -1
  13. package/src/components/credit-notes/create/create-credit-note-form.tsx +52 -42
  14. package/src/components/credit-notes/create/locales/de.ts +2 -1
  15. package/src/components/credit-notes/create/locales/es.ts +2 -1
  16. package/src/components/credit-notes/create/locales/fr.ts +2 -1
  17. package/src/components/credit-notes/create/locales/hr.ts +2 -1
  18. package/src/components/credit-notes/create/locales/it.ts +2 -1
  19. package/src/components/credit-notes/create/locales/nl.ts +2 -1
  20. package/src/components/credit-notes/create/locales/pl.ts +2 -1
  21. package/src/components/credit-notes/create/locales/pt.ts +2 -1
  22. package/src/components/credit-notes/create/locales/sl.ts +2 -1
  23. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -92
  24. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +48 -82
  25. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +22 -31
  26. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +33 -48
  27. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +56 -76
  28. package/src/components/dashboard/shared/index.ts +1 -1
  29. package/src/components/dashboard/shared/use-revenue-data.ts +106 -182
  30. package/src/components/dashboard/shared/use-stats-counts.ts +18 -68
  31. package/src/components/dashboard/shared/use-stats-query.ts +35 -5
  32. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +57 -75
  33. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +38 -49
  34. package/src/components/delivery-notes/create/create-delivery-note-form.tsx +3 -2
  35. package/src/components/delivery-notes/create/locales/de.ts +2 -1
  36. package/src/components/delivery-notes/create/locales/es.ts +2 -1
  37. package/src/components/delivery-notes/create/locales/fr.ts +2 -1
  38. package/src/components/delivery-notes/create/locales/hr.ts +2 -1
  39. package/src/components/delivery-notes/create/locales/it.ts +2 -1
  40. package/src/components/delivery-notes/create/locales/nl.ts +2 -1
  41. package/src/components/delivery-notes/create/locales/pl.ts +2 -1
  42. package/src/components/delivery-notes/create/locales/pt.ts +2 -1
  43. package/src/components/delivery-notes/create/locales/sl.ts +2 -1
  44. package/src/components/documents/create/document-details-section.tsx +6 -4
  45. package/src/components/documents/create/document-recipient-section.tsx +30 -1
  46. package/src/components/documents/create/live-preview.tsx +15 -28
  47. package/src/components/documents/create/prepare-document-submission.ts +1 -0
  48. package/src/components/documents/create/use-document-customer-form.ts +4 -0
  49. package/src/components/documents/shared/document-preview-skeleton.tsx +63 -0
  50. package/src/components/documents/shared/index.ts +1 -0
  51. package/src/components/documents/view/document-actions-bar.tsx +29 -7
  52. package/src/components/entities/entity-settings-form/locales/de.ts +6 -3
  53. package/src/components/entities/entity-settings-form/locales/es.ts +6 -3
  54. package/src/components/entities/entity-settings-form/locales/fr.ts +6 -3
  55. package/src/components/entities/entity-settings-form/locales/hr.ts +4 -2
  56. package/src/components/entities/entity-settings-form/locales/it.ts +6 -3
  57. package/src/components/entities/entity-settings-form/locales/nl.ts +6 -3
  58. package/src/components/entities/entity-settings-form/locales/pl.ts +6 -2
  59. package/src/components/entities/entity-settings-form/locales/pt.ts +6 -3
  60. package/src/components/entities/entity-settings-form/locales/sl.ts +4 -2
  61. package/src/components/entities/settings/tax-rules-settings-form.tsx +31 -13
  62. package/src/components/estimates/create/create-estimate-form.tsx +3 -2
  63. package/src/components/estimates/create/locales/de.ts +2 -1
  64. package/src/components/estimates/create/locales/es.ts +2 -1
  65. package/src/components/estimates/create/locales/fr.ts +2 -1
  66. package/src/components/estimates/create/locales/hr.ts +2 -1
  67. package/src/components/estimates/create/locales/it.ts +2 -1
  68. package/src/components/estimates/create/locales/nl.ts +2 -1
  69. package/src/components/estimates/create/locales/pl.ts +2 -1
  70. package/src/components/estimates/create/locales/pt.ts +2 -1
  71. package/src/components/estimates/create/locales/sl.ts +2 -1
  72. package/src/components/invoices/create/create-invoice-form.tsx +134 -62
  73. package/src/components/invoices/create/locales/de.ts +8 -1
  74. package/src/components/invoices/create/locales/es.ts +8 -1
  75. package/src/components/invoices/create/locales/fr.ts +8 -1
  76. package/src/components/invoices/create/locales/hr.ts +8 -1
  77. package/src/components/invoices/create/locales/it.ts +8 -1
  78. package/src/components/invoices/create/locales/nl.ts +8 -1
  79. package/src/components/invoices/create/locales/pl.ts +8 -1
  80. package/src/components/invoices/create/locales/pt.ts +8 -1
  81. package/src/components/invoices/create/locales/sl.ts +8 -1
  82. package/src/components/invoices/invoices.hooks.ts +1 -1
  83. package/src/components/ui/progress.tsx +27 -0
  84. package/src/generate-schemas.ts +15 -2
  85. package/src/generated/schemas/advanceinvoice.ts +2 -0
  86. package/src/generated/schemas/creditnote.ts +2 -0
  87. package/src/generated/schemas/customer.ts +2 -0
  88. package/src/generated/schemas/deliverynote.ts +2 -0
  89. package/src/generated/schemas/entity.ts +10 -0
  90. package/src/generated/schemas/estimate.ts +2 -0
  91. package/src/generated/schemas/finasettings.ts +4 -3
  92. package/src/generated/schemas/invoice.ts +2 -0
  93. package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +16 -10
  94. package/src/generated/schemas/rendercreditnotepreview_body.ts +16 -10
  95. package/src/generated/schemas/renderdeliverynotepreview_body.ts +14 -7
  96. package/src/generated/schemas/renderestimatepreview_body.ts +14 -7
  97. package/src/generated/schemas/renderinvoicepreview_body.ts +16 -10
  98. package/src/generated/schemas/startpdfexport_body.ts +12 -17
  99. package/src/hooks/use-transaction-type-check.ts +152 -0
  100. package/src/hooks/use-vies-check.ts +7 -131
@@ -1,68 +1,57 @@
1
1
  /**
2
2
  * Top customers hook using the entity stats API.
3
3
  * Server-side aggregation by customer for accurate rankings.
4
+ * Sends 1 query in a batch request.
4
5
  */
5
6
  import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
6
- import { useQuery } from "@tanstack/react-query";
7
- import { useSDK } from "@/ui/providers/sdk-provider";
8
- import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
7
+ import { useStatsQuery } from "../shared/use-stats-query";
9
8
 
10
9
  export const TOP_CUSTOMERS_CACHE_KEY = "dashboard-top-customers";
11
10
 
12
11
  export type TopCustomersData = { name: string; revenue: number }[];
13
12
 
14
13
  export function useTopCustomersData(entityId: string | undefined, limit = 5) {
15
- const { sdk } = useSDK();
16
-
17
- const query = useQuery({
18
- queryKey: [STATS_QUERY_CACHE_KEY, entityId, "top-customers", limit],
19
- queryFn: async () => {
20
- if (!entityId || !sdk) throw new Error("Missing entity or SDK");
21
- return sdk.entityStats.queryEntityStats(
22
- {
23
- metrics: [
24
- { type: "sum", field: "total_with_tax_converted", alias: "revenue" },
25
- { type: "count", alias: "invoice_count" },
26
- ],
27
- table: "invoices",
28
- filters: { is_draft: false, voided_at: null },
29
- group_by: ["customer_name", "quote_currency"], // Include currency for display
30
- order_by: [{ field: "revenue", direction: "desc" }],
31
- limit: limit * 2, // Get more results to aggregate across currencies
32
- },
33
- { entity_id: entityId },
34
- );
14
+ const query = useStatsQuery(
15
+ entityId,
16
+ {
17
+ metrics: [
18
+ { type: "sum", field: "total_with_tax_converted", alias: "revenue" },
19
+ { type: "count", alias: "invoice_count" },
20
+ ],
21
+ table: "invoices",
22
+ filters: { is_draft: false, voided_at: null },
23
+ group_by: ["customer_name", "quote_currency"],
24
+ order_by: [{ field: "revenue", direction: "desc" }],
25
+ limit: limit * 2,
35
26
  },
36
- enabled: !!entityId && !!sdk,
37
- staleTime: 120_000,
38
- select: (response) => {
39
- const data = response.data || [];
40
-
41
- // Aggregate by customer name (in case of multiple quote_currency rows)
42
- const customerMap: Record<string, number> = {};
43
- let currency = "EUR";
44
-
45
- for (const row of data as StatsQueryDataItem[]) {
46
- const name = String(row.customer_name || "Unknown");
47
- customerMap[name] = (customerMap[name] || 0) + (Number(row.revenue) || 0);
48
- // Get currency from first row with data
49
- if (row.quote_currency && currency === "EUR") {
50
- currency = String(row.quote_currency);
27
+ {
28
+ select: (response) => {
29
+ const data = response.data || [];
30
+
31
+ // Aggregate by customer name (in case of multiple quote_currency rows)
32
+ const customerMap: Record<string, number> = {};
33
+ let currency = "EUR";
34
+
35
+ for (const row of data as StatsQueryDataItem[]) {
36
+ const name = String(row.customer_name || "Unknown");
37
+ customerMap[name] = (customerMap[name] || 0) + (Number(row.revenue) || 0);
38
+ if (row.quote_currency && currency === "EUR") {
39
+ currency = String(row.quote_currency);
40
+ }
51
41
  }
52
- }
53
42
 
54
- // Convert to array, sort by revenue, and take top 'limit'
55
- const customers = Object.entries(customerMap)
56
- .map(([name, revenue]) => ({ name, revenue }))
57
- .sort((a, b) => b.revenue - a.revenue)
58
- .slice(0, limit);
43
+ const customers = Object.entries(customerMap)
44
+ .map(([name, revenue]) => ({ name, revenue }))
45
+ .sort((a, b) => b.revenue - a.revenue)
46
+ .slice(0, limit);
59
47
 
60
- return {
61
- data: customers,
62
- currency, // Currency from document data
63
- };
48
+ return {
49
+ data: customers,
50
+ currency,
51
+ };
52
+ },
64
53
  },
65
- });
54
+ );
66
55
 
67
56
  return {
68
57
  data: query.data?.data || [],
@@ -10,7 +10,7 @@ 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
+ import { useTransactionTypeCheck } from "@/ui/hooks/use-transaction-type-check";
14
14
  import type { ComponentTranslationProps } from "@/ui/lib/translation";
15
15
  import { createTranslation } from "@/ui/lib/translation";
16
16
  import { useEntities } from "@/ui/providers/entities-context";
@@ -202,12 +202,13 @@ export default function CreateDeliveryNoteForm({
202
202
  transactionType,
203
203
  isFetching: isViesFetching,
204
204
  warning: viesWarning,
205
- } = useViesCheck({
205
+ } = useTransactionTypeCheck({
206
206
  issuerCountryCode: activeEntity?.country_code,
207
207
  isTaxSubject: activeEntity?.is_tax_subject ?? true,
208
208
  customerCountry: formValues.customer?.country,
209
209
  customerCountryCode: formValues.customer?.country_code,
210
210
  customerTaxNumber: formValues.customer?.tax_number,
211
+ customerIsEndConsumer: (formValues.customer as any)?.is_end_consumer,
211
212
  enabled: !!activeEntity,
212
213
  });
213
214
 
@@ -59,7 +59,8 @@ export default {
59
59
  Domestic: "Inland",
60
60
  "EU B2B": "EU B2B",
61
61
  "EU B2C": "EU B2C",
62
- Export: "Export",
62
+ "3W B2B": "3W B2B",
63
+ "3W B2C": "3W B2C",
63
64
  "Determining transaction type...": "Transaktionstyp wird ermittelt...",
64
65
  "This invoice will not be fiscalized (non-domestic transaction)":
65
66
  "Diese Rechnung wird nicht fiskalisiert (nicht-inländische Transaktion)",
@@ -58,7 +58,8 @@ export default {
58
58
  Domestic: "Nacional",
59
59
  "EU B2B": "EU B2B",
60
60
  "EU B2C": "EU B2C",
61
- Export: "Exportación",
61
+ "3W B2B": "3W B2B",
62
+ "3W B2C": "3W B2C",
62
63
  "Determining transaction type...": "Determinando tipo de transacción...",
63
64
  "This invoice will not be fiscalized (non-domestic transaction)":
64
65
  "Esta factura no será fiscalizada (transacción no nacional)",
@@ -59,7 +59,8 @@ export default {
59
59
  Domestic: "Nationale",
60
60
  "EU B2B": "EU B2B",
61
61
  "EU B2C": "EU B2C",
62
- Export: "Exportation",
62
+ "3W B2B": "3W B2B",
63
+ "3W B2C": "3W B2C",
63
64
  "Determining transaction type...": "Détermination du type de transaction...",
64
65
  "This invoice will not be fiscalized (non-domestic transaction)":
65
66
  "Cette facture ne sera pas fiscalisée (transaction non nationale)",
@@ -58,7 +58,8 @@ export default {
58
58
  Domestic: "Domaća",
59
59
  "EU B2B": "EU B2B",
60
60
  "EU B2C": "EU B2C",
61
- Export: "Izvoz",
61
+ "3W B2B": "3W B2B",
62
+ "3W B2C": "3W B2C",
62
63
  "Determining transaction type...": "Određivanje vrste transakcije...",
63
64
  "This invoice will not be fiscalized (non-domestic transaction)":
64
65
  "Ovaj račun neće biti fiskaliziran (nedomaća transakcija)",
@@ -58,7 +58,8 @@ export default {
58
58
  Domestic: "Nazionale",
59
59
  "EU B2B": "EU B2B",
60
60
  "EU B2C": "EU B2C",
61
- Export: "Esportazione",
61
+ "3W B2B": "3W B2B",
62
+ "3W B2C": "3W B2C",
62
63
  "Determining transaction type...": "Determinazione tipo di transazione...",
63
64
  "This invoice will not be fiscalized (non-domestic transaction)":
64
65
  "Questa fattura non sarà fiscalizzata (transazione non nazionale)",
@@ -59,7 +59,8 @@ export default {
59
59
  Domestic: "Binnenland",
60
60
  "EU B2B": "EU B2B",
61
61
  "EU B2C": "EU B2C",
62
- Export: "Export",
62
+ "3W B2B": "3W B2B",
63
+ "3W B2C": "3W B2C",
63
64
  "Determining transaction type...": "Transactietype bepalen...",
64
65
  "This invoice will not be fiscalized (non-domestic transaction)":
65
66
  "Deze factuur wordt niet gefiscaliseerd (niet-binnenlandse transactie)",
@@ -58,7 +58,8 @@ export default {
58
58
  Domestic: "Krajowa",
59
59
  "EU B2B": "EU B2B",
60
60
  "EU B2C": "EU B2C",
61
- Export: "Eksport",
61
+ "3W B2B": "3W B2B",
62
+ "3W B2C": "3W B2C",
62
63
  "Determining transaction type...": "Określanie typu transakcji...",
63
64
  "This invoice will not be fiscalized (non-domestic transaction)":
64
65
  "Ta faktura nie będzie fiskalizowana (transakcja niekrajowa)",
@@ -59,7 +59,8 @@ export default {
59
59
  Domestic: "Nacional",
60
60
  "EU B2B": "EU B2B",
61
61
  "EU B2C": "EU B2C",
62
- Export: "Exportação",
62
+ "3W B2B": "3W B2B",
63
+ "3W B2C": "3W B2C",
63
64
  "Determining transaction type...": "Determinando tipo de transação...",
64
65
  "This invoice will not be fiscalized (non-domestic transaction)":
65
66
  "Esta fatura não será fiscalizada (transação não nacional)",
@@ -58,7 +58,8 @@ export default {
58
58
  Domestic: "Domača",
59
59
  "EU B2B": "EU B2B",
60
60
  "EU B2C": "EU B2C",
61
- Export: "Izvoz",
61
+ "3W B2B": "3W B2B",
62
+ "3W B2C": "3W B2C",
62
63
  "Determining transaction type...": "Določanje vrste transakcije...",
63
64
  "This invoice will not be fiscalized (non-domestic transaction)":
64
65
  "Ta račun ne bo fiskaliziran (nedomača transakcija)",
@@ -2,7 +2,7 @@
2
2
  * Shared document details section for invoices and estimates
3
3
  * Handles: number, date, and document-type-specific date field (date_due or date_valid_till)
4
4
  */
5
- import type { Entity, Estimate, Invoice, ViesCheckResponse } from "@spaceinvoices/js-sdk";
5
+ import type { Entity, Estimate, Invoice, TransactionTypeCheckResponse } from "@spaceinvoices/js-sdk";
6
6
  import { CalendarIcon, ChevronDown, Globe, Info, Loader2 } from "lucide-react";
7
7
  import { useRef, useState } from "react";
8
8
  import { Badge } from "@/ui/components/ui/badge";
@@ -596,20 +596,22 @@ export function DocumentNoteField({
596
596
  * Tax clause field component with smart code insertion button
597
597
  * Similar to DocumentNoteField, auto-populated from entity settings based on transaction type
598
598
  */
599
- type TransactionType = ViesCheckResponse["transaction_type"];
599
+ type TransactionType = TransactionTypeCheckResponse["transaction_type"];
600
600
 
601
601
  const TRANSACTION_TYPE_LABELS: Record<NonNullable<TransactionType>, string> = {
602
602
  domestic: "Domestic",
603
603
  intra_eu_b2b: "EU B2B",
604
604
  intra_eu_b2c: "EU B2C",
605
- export: "Export",
605
+ "3w_b2b": "3W B2B",
606
+ "3w_b2c": "3W B2C",
606
607
  };
607
608
 
608
609
  const TRANSACTION_TYPE_VARIANTS: Record<NonNullable<TransactionType>, "secondary" | "default" | "outline"> = {
609
610
  domestic: "secondary",
610
611
  intra_eu_b2b: "default",
611
612
  intra_eu_b2c: "outline",
612
- export: "outline",
613
+ "3w_b2b": "outline",
614
+ "3w_b2c": "outline",
613
615
  };
614
616
 
615
617
  export function DocumentTaxClauseField({
@@ -4,8 +4,10 @@
4
4
  */
5
5
  import { X } from "lucide-react";
6
6
  import { useEffect, useRef } from "react";
7
+ import { useController } from "react-hook-form";
7
8
  import { FormInput } from "@/ui/components/form";
8
9
  import { Button } from "@/ui/components/ui/button";
10
+ import { Checkbox } from "@/ui/components/ui/checkbox";
9
11
  import { Label } from "@/ui/components/ui/label";
10
12
  import { cn } from "@/ui/lib/utils";
11
13
  import { CustomerAutocomplete } from "../../customers/customer-autocomplete";
@@ -22,6 +24,8 @@ type DocumentRecipientSectionProps = {
22
24
  selectedCustomerId?: string;
23
25
  /** Initial customer name for display (used when duplicating documents) */
24
26
  initialCustomerName?: string;
27
+ /** Show end consumer (B2C) toggle next to tax number (Croatian entity + domestic transaction) */
28
+ showEndConsumerToggle?: boolean;
25
29
  t: (key: string) => string;
26
30
  };
27
31
 
@@ -34,10 +38,16 @@ export function DocumentRecipientSection({
34
38
  shouldFocusName,
35
39
  selectedCustomerId,
36
40
  initialCustomerName,
41
+ showEndConsumerToggle,
37
42
  t,
38
43
  }: DocumentRecipientSectionProps) {
39
44
  const nameInputRef = useRef<HTMLInputElement>(null);
40
45
 
46
+ const endConsumerController = useController({
47
+ control: control as any,
48
+ name: "customer.is_end_consumer" as any,
49
+ });
50
+
41
51
  useEffect(() => {
42
52
  if (showCustomerForm && shouldFocusName) {
43
53
  // Small delay to ensure the input is rendered
@@ -93,7 +103,26 @@ export function DocumentRecipientSection({
93
103
  <FormInput control={control} name="customer.country" placeholder={t("Country")} label="" />
94
104
  </div>
95
105
 
96
- <FormInput control={control} name="customer.tax_number" placeholder={t("Tax Number")} label="" />
106
+ <div className="flex items-center gap-3">
107
+ <div className="flex-1">
108
+ <FormInput control={control} name="customer.tax_number" placeholder={t("Tax Number")} label="" />
109
+ </div>
110
+ {showEndConsumerToggle && (
111
+ <div className="flex items-center gap-1.5 pt-0.5">
112
+ <Checkbox
113
+ id="is_end_consumer"
114
+ checked={endConsumerController.field.value === true}
115
+ onCheckedChange={(checked) => endConsumerController.field.onChange(checked === true)}
116
+ />
117
+ <Label
118
+ htmlFor="is_end_consumer"
119
+ className="cursor-pointer whitespace-nowrap font-normal text-muted-foreground text-sm"
120
+ >
121
+ {t("End consumer")}
122
+ </Label>
123
+ </div>
124
+ )}
125
+ </div>
97
126
  </>
98
127
  )}
99
128
  </div>
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import type { CreateInvoiceRequest } from "@spaceinvoices/js-sdk";
4
- import { Loader2 } from "lucide-react";
5
4
  import { useCallback, useEffect, useRef, useState } from "react";
6
5
  import { cn } from "@/ui/lib/utils";
7
6
  import { useEntities } from "@/ui/providers/entities-context";
8
7
  import { useSDK } from "@/ui/providers/sdk-provider";
8
+ import { DocumentPreviewSkeleton } from "../shared/document-preview-skeleton";
9
9
  import { ScaledDocumentPreview } from "../shared/scaled-document-preview";
10
10
  import { useA4Scaling } from "../shared/use-a4-scaling";
11
11
  import type { DocumentTypes } from "../types";
@@ -58,7 +58,7 @@ export function LiveInvoicePreview({
58
58
  locale: _locale,
59
59
  fixedScale,
60
60
  t: tProp,
61
- documentTypeLabel,
61
+ documentTypeLabel: _documentTypeLabel,
62
62
  documentType = "invoice",
63
63
  qrOverrides,
64
64
  }: LiveInvoicePreviewProps) {
@@ -235,18 +235,10 @@ export function LiveInvoicePreview({
235
235
  };
236
236
  }, []);
237
237
 
238
+ const showSkeleton = (!previewHtml && !error) || (isLoading && !previewHtml);
239
+
238
240
  return (
239
241
  <div ref={containerRef} className={cn("relative", className)}>
240
- {/* Loading overlay */}
241
- {isLoading && (
242
- <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm">
243
- <div className="flex flex-col items-center gap-2">
244
- <Loader2 className="h-8 w-8 animate-spin text-primary" />
245
- <p className="text-muted-foreground text-sm">{t("Generating preview...")}</p>
246
- </div>
247
- </div>
248
- )}
249
-
250
242
  {/* Error state */}
251
243
  {error && !isLoading && (
252
244
  <div className="flex min-h-[200px] items-center justify-center rounded-lg border border-destructive/50 bg-destructive/10 p-8">
@@ -257,25 +249,20 @@ export function LiveInvoicePreview({
257
249
  </div>
258
250
  )}
259
251
 
260
- {/* Empty state */}
261
- {!previewHtml && !isLoading && !error && (
262
- <div className="flex min-h-[200px] items-center justify-center rounded-lg border border-dashed p-8">
263
- <div className="text-center">
264
- <p className="font-semibold text-muted-foreground">{documentTypeLabel || t("Document Preview")}</p>
265
- <p className="text-muted-foreground text-sm">{t("Start filling the form to see a live preview")}</p>
266
- </div>
267
- </div>
268
- )}
252
+ {/* Skeleton: shown on initial load and when loading without existing preview */}
253
+ {showSkeleton && <DocumentPreviewSkeleton />}
269
254
 
270
255
  {/* Preview - Scoped HTML injection with A4 scaling */}
271
256
  {previewHtml && !error && (
272
- <ScaledDocumentPreview
273
- htmlContent={previewHtml}
274
- scale={scale}
275
- contentHeight={contentHeight}
276
- A4_WIDTH_PX={A4_WIDTH_PX}
277
- contentRef={contentRef}
278
- />
257
+ <div className={cn(isLoading && "opacity-50 transition-opacity duration-200")}>
258
+ <ScaledDocumentPreview
259
+ htmlContent={previewHtml}
260
+ scale={scale}
261
+ contentHeight={contentHeight}
262
+ A4_WIDTH_PX={A4_WIDTH_PX}
263
+ contentRef={contentRef}
264
+ />
265
+ </div>
279
266
  )}
280
267
  </div>
281
268
  );
@@ -7,6 +7,7 @@ type CustomerData = {
7
7
  state?: string | null;
8
8
  country?: string | null;
9
9
  tax_number?: string | null;
10
+ is_end_consumer?: boolean | null;
10
11
  save_customer?: boolean;
11
12
  };
12
13
 
@@ -14,6 +14,7 @@ export type CustomerData = {
14
14
  state?: string | null;
15
15
  country?: string | null;
16
16
  tax_number?: string | null;
17
+ is_end_consumer?: boolean | null;
17
18
  };
18
19
 
19
20
  /**
@@ -97,6 +98,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
97
98
  state: toFormValue(customer.state),
98
99
  country: toFormValue(customer.country),
99
100
  tax_number: toFormValue(customer.tax_number),
101
+ is_end_consumer: customer.is_end_consumer ?? undefined,
100
102
  } as PathValue<TForm, Path<TForm>>,
101
103
  );
102
104
  setOriginalCustomer(null);
@@ -115,6 +117,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
115
117
  state: toFormValue(customer.state),
116
118
  country: toFormValue(customer.country),
117
119
  tax_number: toFormValue(customer.tax_number),
120
+ is_end_consumer: customer.is_end_consumer ?? undefined,
118
121
  };
119
122
 
120
123
  setValue("customer" as Path<TForm>, customerData as PathValue<TForm, Path<TForm>>);
@@ -140,6 +143,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
140
143
  state: undefined,
141
144
  country: undefined,
142
145
  tax_number: undefined,
146
+ is_end_consumer: true,
143
147
  } as PathValue<TForm, Path<TForm>>,
144
148
  );
145
149
  setOriginalCustomer(null);
@@ -0,0 +1,63 @@
1
+ import { Skeleton } from "@/ui/components/ui/skeleton";
2
+
3
+ /** A4-shaped skeleton that mimics a document layout */
4
+ export function DocumentPreviewSkeleton() {
5
+ return (
6
+ <div className="rounded-lg border bg-neutral-100 p-4">
7
+ <div className="mx-auto bg-white p-8" style={{ maxWidth: 794, aspectRatio: "210 / 297" }}>
8
+ {/* Header: logo + company info */}
9
+ <div className="flex justify-between">
10
+ <Skeleton className="h-10 w-32 bg-neutral-200" />
11
+ <div className="flex flex-col items-end gap-1.5">
12
+ <Skeleton className="h-3 w-40 bg-neutral-200" />
13
+ <Skeleton className="h-3 w-32 bg-neutral-200" />
14
+ <Skeleton className="h-3 w-28 bg-neutral-200" />
15
+ </div>
16
+ </div>
17
+
18
+ {/* Title */}
19
+ <Skeleton className="mt-8 h-5 w-28 bg-neutral-200" />
20
+
21
+ {/* Recipient + details row */}
22
+ <div className="mt-6 flex justify-between">
23
+ <div className="flex flex-col gap-1.5">
24
+ <Skeleton className="h-3 w-36 bg-neutral-200" />
25
+ <Skeleton className="h-3 w-28 bg-neutral-200" />
26
+ <Skeleton className="h-3 w-24 bg-neutral-200" />
27
+ </div>
28
+ <div className="flex flex-col items-end gap-1.5">
29
+ <Skeleton className="h-3 w-24 bg-neutral-200" />
30
+ <Skeleton className="h-3 w-20 bg-neutral-200" />
31
+ <Skeleton className="h-3 w-24 bg-neutral-200" />
32
+ </div>
33
+ </div>
34
+
35
+ {/* Table header */}
36
+ <div className="mt-8 flex gap-4 border-b pb-2">
37
+ <Skeleton className="h-3 w-8 bg-neutral-200" />
38
+ <Skeleton className="h-3 flex-1 bg-neutral-200" />
39
+ <Skeleton className="h-3 w-12 bg-neutral-200" />
40
+ <Skeleton className="h-3 w-16 bg-neutral-200" />
41
+ <Skeleton className="h-3 w-16 bg-neutral-200" />
42
+ </div>
43
+ {/* Table rows */}
44
+ {[1, 2, 3].map((i) => (
45
+ <div key={i} className="flex gap-4 border-b py-2.5">
46
+ <Skeleton className="h-3 w-8 bg-neutral-200" />
47
+ <Skeleton className="h-3 flex-1 bg-neutral-200" />
48
+ <Skeleton className="h-3 w-12 bg-neutral-200" />
49
+ <Skeleton className="h-3 w-16 bg-neutral-200" />
50
+ <Skeleton className="h-3 w-16 bg-neutral-200" />
51
+ </div>
52
+ ))}
53
+
54
+ {/* Totals */}
55
+ <div className="mt-4 flex flex-col items-end gap-1.5">
56
+ <Skeleton className="h-3 w-32 bg-neutral-200" />
57
+ <Skeleton className="h-3 w-28 bg-neutral-200" />
58
+ <Skeleton className="h-4 w-36 bg-neutral-200" />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -1,3 +1,4 @@
1
1
  export { DocumentPreviewDisplay, InvoicePreviewDisplay } from "./document-preview-display";
2
+ export { DocumentPreviewSkeleton } from "./document-preview-skeleton";
2
3
  export { ScaledDocumentPreview } from "./scaled-document-preview";
3
4
  export { useA4Scaling } from "./use-a4-scaling";
@@ -81,8 +81,10 @@ interface DocumentActionsBarProps extends ComponentTranslationProps {
81
81
  onDuplicate?: (targetType: DocumentType) => void;
82
82
  /** Called when user wants to edit the document */
83
83
  onEdit?: () => void;
84
- /** Whether the document is editable (not voided, not FURS fiscalized) */
84
+ /** Whether the document is editable (not voided, not fiscalized) */
85
85
  isEditable?: boolean;
86
+ /** Reason why editing is disabled (shown as tooltip) */
87
+ editDisabledReason?: string;
86
88
  /** Called when user wants to finalize a draft document */
87
89
  onFinalize?: () => void;
88
90
  /** Whether finalization is in progress */
@@ -119,6 +121,7 @@ export function DocumentActionsBar({
119
121
  onDuplicate,
120
122
  onEdit,
121
123
  isEditable,
124
+ editDisabledReason,
122
125
  onFinalize,
123
126
  isFinalizing,
124
127
  onDeleteDraft,
@@ -233,12 +236,31 @@ export function DocumentActionsBar({
233
236
  )}
234
237
 
235
238
  {/* Edit */}
236
- {onEdit && isEditable && (
237
- <Button variant="outline" size="sm" onClick={onEdit} className="cursor-pointer">
238
- <Pencil className="mr-2 h-4 w-4" />
239
- {t("Edit")}
240
- </Button>
241
- )}
239
+ {onEdit &&
240
+ (isEditable ? (
241
+ <Button variant="outline" size="sm" onClick={onEdit} className="cursor-pointer">
242
+ <Pencil className="mr-2 h-4 w-4" />
243
+ {t("Edit")}
244
+ </Button>
245
+ ) : (
246
+ <Tooltip>
247
+ <TooltipTrigger asChild>
248
+ <Button
249
+ variant="outline"
250
+ size="sm"
251
+ aria-disabled="true"
252
+ className="pointer-events-auto opacity-50"
253
+ onClick={(e) => e.preventDefault()}
254
+ >
255
+ <Pencil className="mr-2 h-4 w-4" />
256
+ {t("Edit")}
257
+ </Button>
258
+ </TooltipTrigger>
259
+ <TooltipContent>
260
+ <p>{editDisabledReason}</p>
261
+ </TooltipContent>
262
+ </Tooltip>
263
+ ))}
242
264
 
243
265
  {/* Share Link */}
244
266
  {shareUrl ? (
@@ -184,9 +184,12 @@ export default {
184
184
  "tax-clauses.intra_eu_b2b.description":
185
185
  "Für Verkäufe an EU-Unternehmen mit gültiger USt-IdNr. Enthält typischerweise eine Reverse-Charge-Erklärung.",
186
186
  "Enter reverse charge clause...": "Reverse-Charge-Klausel eingeben...",
187
- "tax-clauses.export.label": "Export (Nicht-EU)",
188
- "tax-clauses.export.description":
189
- "Für Verkäufe an Kunden außerhalb der EU. Enthält typischerweise die Steuerbefreiung für Exporte.",
187
+ "tax-clauses.3w_b2b.label": "Nicht-EU B2B (Export)",
188
+ "tax-clauses.3w_b2b.description":
189
+ "Für Verkäufe an Unternehmen außerhalb der EU. Enthält typischerweise die Steuerbefreiung für Exporte.",
190
+ "tax-clauses.3w_b2c.label": "Nicht-EU B2C (Export)",
191
+ "tax-clauses.3w_b2c.description":
192
+ "Für Verkäufe an Verbraucher außerhalb der EU. Enthält typischerweise die Steuerbefreiung für Exporte.",
190
193
  "Enter export exemption clause...": "Exportbefreiungsklausel eingeben...",
191
194
  "tax-clauses.domestic.label": "Standard / Inländisch",
192
195
  "tax-clauses.domestic.description":
@@ -189,9 +189,12 @@ export default {
189
189
  "tax-clauses.intra_eu_b2b.description":
190
190
  "Para ventas a empresas de la UE con números de IVA válidos. Normalmente incluye una declaración de inversión del sujeto pasivo.",
191
191
  "Enter reverse charge clause...": "Introduzca la cláusula de inversión del sujeto pasivo...",
192
- "tax-clauses.export.label": "Exportación (fuera de la UE)",
193
- "tax-clauses.export.description":
194
- "Para ventas a clientes fuera de la UE. Normalmente indica la exención de IVA por exportación.",
192
+ "tax-clauses.3w_b2b.label": "Fuera de UE B2B (exportación)",
193
+ "tax-clauses.3w_b2b.description":
194
+ "Para ventas a empresas fuera de la UE. Normalmente indica la exención de IVA por exportación.",
195
+ "tax-clauses.3w_b2c.label": "Fuera de UE B2C (exportación)",
196
+ "tax-clauses.3w_b2c.description":
197
+ "Para ventas a consumidores fuera de la UE. Normalmente indica la exención de IVA por exportación.",
195
198
  "Enter export exemption clause...": "Introduzca la cláusula de exención de exportación...",
196
199
  "tax-clauses.domestic.label": "Predeterminado / Nacional",
197
200
  "tax-clauses.domestic.description":
@@ -188,9 +188,12 @@ export default {
188
188
  "tax-clauses.intra_eu_b2b.description":
189
189
  "Pour les ventes à des entreprises UE avec des numéros de TVA valides. Inclut généralement une déclaration d'autoliquidation.",
190
190
  "Enter reverse charge clause...": "Entrez la clause d'autoliquidation...",
191
- "tax-clauses.export.label": "Exportation (hors UE)",
192
- "tax-clauses.export.description":
193
- "Pour les ventes à des clients hors UE. Indique généralement l'exonération de TVA à l'exportation.",
191
+ "tax-clauses.3w_b2b.label": "Hors UE B2B (exportation)",
192
+ "tax-clauses.3w_b2b.description":
193
+ "Pour les ventes à des entreprises hors UE. Indique généralement l'exonération de TVA à l'exportation.",
194
+ "tax-clauses.3w_b2c.label": "Hors UE B2C (exportation)",
195
+ "tax-clauses.3w_b2c.description":
196
+ "Pour les ventes à des consommateurs hors UE. Indique généralement l'exonération de TVA à l'exportation.",
194
197
  "Enter export exemption clause...": "Entrez la clause d'exonération à l'exportation...",
195
198
  "tax-clauses.domestic.label": "Par défaut / National",
196
199
  "tax-clauses.domestic.description":