@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
@@ -2,9 +2,10 @@
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 } from "@spaceinvoices/js-sdk";
6
- import { CalendarIcon } from "lucide-react";
5
+ import type { Entity, Estimate, Invoice, ViesCheckResponse } from "@spaceinvoices/js-sdk";
6
+ import { CalendarIcon, Globe, Info, Loader2 } from "lucide-react";
7
7
  import { useRef, useState } from "react";
8
+ import { Badge } from "@/ui/components/ui/badge";
8
9
  import { Button } from "@/ui/components/ui/button";
9
10
  import { Calendar } from "@/ui/components/ui/calendar";
10
11
  import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/ui/components/ui/form";
@@ -612,6 +613,125 @@ export function DocumentNoteField({
612
613
  );
613
614
  }
614
615
 
616
+ /**
617
+ * Tax clause field component with smart code insertion button
618
+ * Similar to DocumentNoteField, auto-populated from entity settings based on transaction type
619
+ */
620
+ type TransactionType = ViesCheckResponse["transaction_type"];
621
+
622
+ const TRANSACTION_TYPE_LABELS: Record<NonNullable<TransactionType>, string> = {
623
+ domestic: "Domestic",
624
+ intra_eu_b2b: "EU B2B",
625
+ intra_eu_b2c: "EU B2C",
626
+ export: "Export",
627
+ };
628
+
629
+ const TRANSACTION_TYPE_VARIANTS: Record<NonNullable<TransactionType>, "secondary" | "default" | "outline"> = {
630
+ domestic: "secondary",
631
+ intra_eu_b2b: "default",
632
+ intra_eu_b2c: "outline",
633
+ export: "outline",
634
+ };
635
+
636
+ export function DocumentTaxClauseField({
637
+ control,
638
+ t,
639
+ entity,
640
+ document,
641
+ transactionType,
642
+ isTransactionTypeFetching,
643
+ isFinaNonDomestic,
644
+ }: {
645
+ control: AnyControl;
646
+ t: (key: string) => string;
647
+ entity?: Entity | null;
648
+ document?: Partial<Invoice | Estimate> | null;
649
+ transactionType?: TransactionType;
650
+ isTransactionTypeFetching?: boolean;
651
+ isFinaNonDomestic?: boolean;
652
+ }) {
653
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
654
+ const [isFocused, setIsFocused] = useState(false);
655
+
656
+ const effectiveTransactionType = transactionType ?? "domestic";
657
+ const showTransactionInfo = true;
658
+
659
+ return (
660
+ <FormField
661
+ control={control}
662
+ name="tax_clause"
663
+ render={({ field }) => {
664
+ const hasContent = field.value;
665
+ const showPreview = !isFocused && hasContent && entity;
666
+ const preview = showPreview ? replaceTemplateVariablesForPreview(field.value || "", entity, document) : null;
667
+
668
+ return (
669
+ <FormItem>
670
+ <div className="flex items-center justify-between">
671
+ <FormLabel>{t("Tax Clause")}</FormLabel>
672
+ <SmartCodeInsertButton
673
+ textareaRef={textareaRef}
674
+ value={field.value || ""}
675
+ onInsert={(newValue) => field.onChange(newValue)}
676
+ t={t}
677
+ />
678
+ </div>
679
+ {showTransactionInfo && (
680
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-md border border-border/50 bg-muted/30 px-2.5 py-1.5 text-sm">
681
+ {isTransactionTypeFetching ? (
682
+ <div className="flex items-center gap-1.5 text-muted-foreground">
683
+ <Loader2 className="size-3.5 animate-spin" />
684
+ <span>{t("Determining transaction type...")}</span>
685
+ </div>
686
+ ) : (
687
+ <>
688
+ <div className="flex items-center gap-1.5">
689
+ <Globe className="size-3.5 text-muted-foreground" />
690
+ <span className="text-muted-foreground">{t("Transaction type")}:</span>
691
+ <Badge variant={TRANSACTION_TYPE_VARIANTS[effectiveTransactionType]}>
692
+ {t(TRANSACTION_TYPE_LABELS[effectiveTransactionType])}
693
+ </Badge>
694
+ </div>
695
+ {isFinaNonDomestic && (
696
+ <div className="flex items-center gap-1.5 text-muted-foreground">
697
+ <Info className="size-3.5" />
698
+ <span>{t("This invoice will not be fiscalized (non-domestic transaction)")}</span>
699
+ </div>
700
+ )}
701
+ </>
702
+ )}
703
+ </div>
704
+ )}
705
+ <FormControl>
706
+ <div className="relative">
707
+ <Textarea
708
+ {...field}
709
+ ref={(e) => {
710
+ field.ref(e);
711
+ (textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
712
+ }}
713
+ value={field.value || ""}
714
+ placeholder={showPreview ? "" : t("Add tax clause...")}
715
+ rows={3}
716
+ className={cn("resize-y", showPreview && "text-transparent caret-transparent")}
717
+ onFocus={() => setIsFocused(true)}
718
+ onBlur={() => setIsFocused(false)}
719
+ />
720
+ {showPreview && (
721
+ <div className="pointer-events-none absolute inset-0 z-10 flex items-start overflow-hidden rounded-md border border-input bg-background px-3 py-2 shadow-xs">
722
+ <div className="w-full whitespace-pre-wrap text-base md:text-sm">{preview}</div>
723
+ </div>
724
+ )}
725
+ </div>
726
+ </FormControl>
727
+ <FormMessage />
728
+ </FormItem>
729
+ );
730
+ }}
731
+ />
732
+ );
733
+ }
734
+
615
735
  /**
616
736
  * Payment terms field component with smart code insertion button
617
737
  * Similar to DocumentNoteField, exported for use in document forms
@@ -2,7 +2,7 @@
2
2
  * Shared document items section for invoices and estimates
3
3
  * Handles: item management (add, remove, reorder)
4
4
  */
5
- import { PlusIcon } from "lucide-react";
5
+ import { PlusIcon, SeparatorHorizontal } from "lucide-react";
6
6
  import type { MutableRefObject } from "react";
7
7
  import type { UseFormGetValues, UseFormSetValue, UseFormWatch } from "react-hook-form";
8
8
  import { Button } from "@/ui/components/ui/button";
@@ -65,6 +65,18 @@ export function DocumentItemsSection({
65
65
  ]);
66
66
  };
67
67
 
68
+ const addSeparator = () => {
69
+ const currentItems = getValues("items") || [];
70
+ setValue("items", [
71
+ ...currentItems,
72
+ {
73
+ type: "separator",
74
+ name: "",
75
+ description: "",
76
+ },
77
+ ]);
78
+ };
79
+
68
80
  const removeItem = (index: number) => {
69
81
  const currentItems = getValues("items") || [];
70
82
  setValue(
@@ -125,9 +137,14 @@ export function DocumentItemsSection({
125
137
  </div>
126
138
  ))}
127
139
 
128
- <Button type="button" variant="outline" onClick={addItem} className="w-full cursor-pointer border-dashed">
129
- <PlusIcon className="mr-2 h-4 w-4" /> {t("Add item")}
130
- </Button>
140
+ <div className="flex gap-2">
141
+ <Button type="button" variant="outline" onClick={addItem} className="flex-1 cursor-pointer border-dashed">
142
+ <PlusIcon className="mr-2 h-4 w-4" /> {t("Add item")}
143
+ </Button>
144
+ <Button type="button" variant="ghost" onClick={addSeparator} className="cursor-pointer text-muted-foreground">
145
+ <SeparatorHorizontal className="mr-2 h-4 w-4" /> {t("Add separator")}
146
+ </Button>
147
+ </div>
131
148
  </div>
132
149
  );
133
150
  }
@@ -30,6 +30,12 @@ type LiveInvoicePreviewProps = {
30
30
  documentTypeLabel?: string;
31
31
  /** Document type to determine which render endpoint to use */
32
32
  documentType?: DocumentTypes;
33
+ /** QR settings overrides for preview (before saving) */
34
+ qrOverrides?: {
35
+ upn_qr_enabled?: boolean;
36
+ upn_qr_display_mode?: "qr_only" | "full_slip";
37
+ epc_qr_enabled?: boolean;
38
+ };
33
39
  };
34
40
 
35
41
  /**
@@ -49,11 +55,12 @@ export function LiveInvoicePreview({
49
55
  currency: _currency = "EUR",
50
56
  template,
51
57
  className,
52
- locale,
58
+ locale: _locale,
53
59
  fixedScale,
54
60
  t: tProp,
55
61
  documentTypeLabel,
56
62
  documentType = "invoice",
63
+ qrOverrides,
57
64
  }: LiveInvoicePreviewProps) {
58
65
  const t = tProp ?? ((key: string) => key);
59
66
  const [previewHtml, setPreviewHtml] = useState<string>("");
@@ -103,7 +110,7 @@ export function LiveInvoicePreview({
103
110
  // Filter out unresolved tax_ids (race condition: form may add
104
111
  // { tax_id: undefined } before the tax dropdown auto-selects a value)
105
112
  items: filterUnresolvedTaxes(invoiceData.items),
106
- issuer: invoiceData.issuer || {
113
+ issuer: {
107
114
  name: activeEntity.name,
108
115
  address: activeEntity.address,
109
116
  address_2: activeEntity.address_2,
@@ -112,11 +119,22 @@ export function LiveInvoicePreview({
112
119
  state: activeEntity.state,
113
120
  country: activeEntity.country,
114
121
  tax_number: activeEntity.tax_number,
122
+ ...invoiceData.issuer,
115
123
  },
116
124
  };
117
125
 
118
126
  // Call the render API using the appropriate SDK method for the document type
119
- const renderParams = { partial: "true" as const, template, locale };
127
+ // Don't send locale let entity locale drive formatting (decimal separators, date format)
128
+ const renderParams: Record<string, any> = { partial: "true" as const, template };
129
+ if (qrOverrides?.upn_qr_enabled !== undefined) {
130
+ renderParams.upn_qr_enabled = qrOverrides.upn_qr_enabled ? "true" : "false";
131
+ if (qrOverrides.upn_qr_display_mode) {
132
+ renderParams.upn_qr_display_mode = qrOverrides.upn_qr_display_mode;
133
+ }
134
+ }
135
+ if (qrOverrides?.epc_qr_enabled !== undefined) {
136
+ renderParams.epc_qr_enabled = qrOverrides.epc_qr_enabled ? "true" : "false";
137
+ }
120
138
  const requestOpts = { entity_id: activeEntity.id };
121
139
  let html: string;
122
140
  switch (documentType) {
@@ -170,9 +188,11 @@ export function LiveInvoicePreview({
170
188
  activeEntity?.city,
171
189
  activeEntity?.name,
172
190
  template,
173
- locale,
174
191
  sdk,
175
192
  documentType,
193
+ qrOverrides?.upn_qr_enabled,
194
+ qrOverrides?.upn_qr_display_mode,
195
+ qrOverrides?.epc_qr_enabled,
176
196
  ],
177
197
  );
178
198
 
@@ -31,6 +31,8 @@ type MarkAsPaidSectionProps = {
31
31
  t: (key: string) => string;
32
32
  /** Always show payment type selector (e.g. for FINA fiscalization) */
33
33
  alwaysShowPaymentType?: boolean;
34
+ /** Force paid state — hides the checkbox and always shows payment selectors */
35
+ forced?: boolean;
34
36
  };
35
37
 
36
38
  export function MarkAsPaidSection({
@@ -40,31 +42,38 @@ export function MarkAsPaidSection({
40
42
  onPaymentTypesChange,
41
43
  t,
42
44
  alwaysShowPaymentType,
45
+ forced,
43
46
  }: MarkAsPaidSectionProps) {
44
- const showPaymentTypes = checked || alwaysShowPaymentType;
47
+ const showPaymentTypes = forced || checked || alwaysShowPaymentType;
45
48
 
46
49
  return (
47
50
  <div className={cn("flex flex-col gap-4 rounded-md border p-4", showPaymentTypes && "gap-3")}>
48
- <div className="flex flex-row items-center space-x-3 space-y-0">
49
- <Checkbox checked={checked} onCheckedChange={(v) => onCheckedChange(v === true)} />
50
- <div className="flex items-center gap-1 leading-none">
51
- <Label>{checked ? t("Paid") : t("Mark as Paid")}</Label>
52
- {!checked && (
53
- <Tooltip>
54
- <TooltipTrigger asChild>
55
- <button
56
- type="button"
57
- className="rounded-full p-1 transition-colors hover:bg-accent"
58
- onClick={(e) => e.preventDefault()}
59
- >
60
- <HelpCircle className="size-4 text-muted-foreground" />
61
- </button>
62
- </TooltipTrigger>
63
- <TooltipContent side="top">{t("Invoice will be marked as fully paid upon creation")}</TooltipContent>
64
- </Tooltip>
65
- )}
51
+ {forced ? (
52
+ <div className="flex flex-row items-center space-x-3 space-y-0">
53
+ <Label>{t("Paid")}</Label>
66
54
  </div>
67
- </div>
55
+ ) : (
56
+ <div className="flex flex-row items-center space-x-3 space-y-0">
57
+ <Checkbox checked={checked} onCheckedChange={(v) => onCheckedChange(v === true)} />
58
+ <div className="flex items-center gap-1 leading-none">
59
+ <Label>{checked ? t("Paid") : t("Mark as Paid")}</Label>
60
+ {!checked && (
61
+ <Tooltip>
62
+ <TooltipTrigger asChild>
63
+ <button
64
+ type="button"
65
+ className="rounded-full p-1 transition-colors hover:bg-accent"
66
+ onClick={(e) => e.preventDefault()}
67
+ >
68
+ <HelpCircle className="size-4 text-muted-foreground" />
69
+ </button>
70
+ </TooltipTrigger>
71
+ <TooltipContent side="top">{t("Invoice will be marked as fully paid upon creation")}</TooltipContent>
72
+ </Tooltip>
73
+ )}
74
+ </div>
75
+ </div>
76
+ )}
68
77
 
69
78
  {showPaymentTypes && (
70
79
  <div className="flex flex-col gap-2">
@@ -127,6 +127,15 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
127
127
  if (values.items) {
128
128
  const priceModes = options.priceModes ?? {};
129
129
  values.items = values.items.map((item: any, index: number) => {
130
+ // Separator items — pass through with only type, name, description
131
+ if (item.type === "separator") {
132
+ return {
133
+ type: "separator",
134
+ name: item.name,
135
+ description: item.description || undefined,
136
+ };
137
+ }
138
+
130
139
  const { price, ...rest } = item;
131
140
 
132
141
  // Transform price based on price mode (from component state, not form)
@@ -136,13 +145,16 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
136
145
  return {
137
146
  ...rest,
138
147
  ...priceFields,
139
- taxes: item.taxes?.map((tax: any) => {
140
- if (tax.tax_id) {
141
- // Only send tax_id, API will resolve the rate
142
- return { tax_id: tax.tax_id };
143
- }
144
- return tax;
145
- }),
148
+ taxes: item.taxes
149
+ ?.map((tax: any) => {
150
+ if (tax.tax_id) {
151
+ // Only send tax_id, API will resolve the rate
152
+ return { tax_id: tax.tax_id };
153
+ }
154
+ return tax;
155
+ })
156
+ // Filter out empty placeholder taxes (UI adds { tax_id: undefined } for the dropdown)
157
+ .filter((tax: any) => tax.tax_id || tax.rate !== undefined || tax.classification),
146
158
  };
147
159
  });
148
160
  }
@@ -80,9 +80,7 @@ export function DocumentPreviewDisplay({
80
80
  try {
81
81
  // Determine document type from shareable ID prefix
82
82
  const docTypePath = getDocTypePathFromShareableId(shareableId);
83
- const response = await fetch(
84
- `${apiBaseUrl}/${docTypePath}/shareable/${shareableId}/html${locale ? `?locale=${locale}` : ""}`,
85
- );
83
+ const response = await fetch(`${apiBaseUrl}/${docTypePath}/shareable/${shareableId}/html`);
86
84
  if (!response.ok) {
87
85
  throw new Error("Failed to load preview");
88
86
  }
@@ -108,7 +106,8 @@ export function DocumentPreviewDisplay({
108
106
  try {
109
107
  // Fetch the rendered HTML by document ID using SDK wrapper
110
108
  // Document type is auto-detected from ID prefix (inv_, est_, cre_, adv_)
111
- const html = await sdk.invoices.renderHtml(document.id, { template, locale }, { entity_id: activeEntity.id });
109
+ // Don't send locale let entity locale drive formatting (decimal separators, date format)
110
+ const html = await sdk.invoices.renderHtml(document.id, { template }, { entity_id: activeEntity.id });
112
111
 
113
112
  setPreviewHtml(html);
114
113
  setError(null);
@@ -44,7 +44,8 @@ const translations = { sl, de, it, fr, es, pt, nl, pl, hr } as const;
44
44
 
45
45
  type Document = Invoice | Estimate | CreditNote | AdvanceInvoice;
46
46
 
47
- const PDF_LOCALE_CODES = [
47
+ /** Language options for PDF label translations (formatting always uses entity locale) */
48
+ const PDF_LANGUAGE_CODES = [
48
49
  { label: "English", code: "en-US" },
49
50
  { label: "German", code: "de-DE" },
50
51
  { label: "Slovenian", code: "sl-SI" },
@@ -100,22 +101,6 @@ interface DocumentActionsBarProps extends ComponentTranslationProps {
100
101
  isVoiding?: boolean;
101
102
  }
102
103
 
103
- function getApiLocale(uiLanguage: string): string {
104
- const localeMap: Record<string, string> = {
105
- en: "en-US",
106
- de: "de-DE",
107
- sl: "sl-SI",
108
- it: "it-IT",
109
- fr: "fr-FR",
110
- es: "es-ES",
111
- pt: "pt-PT",
112
- nl: "nl-NL",
113
- pl: "pl-PL",
114
- hr: "hr-HR",
115
- };
116
- return localeMap[uiLanguage] || "en-US";
117
- }
118
-
119
104
  export function DocumentActionsBar({
120
105
  document,
121
106
  documentType,
@@ -163,7 +148,7 @@ export function DocumentActionsBar({
163
148
  const shareableId = (document as Invoice).shareable_id;
164
149
  const shareUrl = shareableId ? `${window.location.origin}/public/invoices/${shareableId}` : null;
165
150
 
166
- const handleDownloadPdf = (locale: string) => downloadPdf(document, documentType, locale);
151
+ const handleDownloadPdf = (language?: string) => downloadPdf(document, documentType, language);
167
152
  const handleDownloadEslog = () => downloadEslog(document, documentType);
168
153
 
169
154
  const handleCopyShareLink = async () => {
@@ -178,7 +163,6 @@ export function DocumentActionsBar({
178
163
  }
179
164
  };
180
165
 
181
- const apiLocale = getApiLocale(currentLocale);
182
166
  const isDraft = (document as any).is_draft === true;
183
167
 
184
168
  return (
@@ -189,7 +173,7 @@ export function DocumentActionsBar({
189
173
  variant="outline"
190
174
  size="sm"
191
175
  disabled={isDownloadingPdf}
192
- onClick={() => handleDownloadPdf(apiLocale)}
176
+ onClick={() => handleDownloadPdf()}
193
177
  className="cursor-pointer rounded-r-none"
194
178
  >
195
179
  {isDownloadingPdf ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Download className="mr-2 h-4 w-4" />}
@@ -207,13 +191,9 @@ export function DocumentActionsBar({
207
191
  </Button>
208
192
  </DropdownMenuTrigger>
209
193
  <DropdownMenuContent align="end">
210
- {PDF_LOCALE_CODES.map((locale) => (
211
- <DropdownMenuItem
212
- key={locale.code}
213
- onClick={() => handleDownloadPdf(locale.code)}
214
- className="cursor-pointer"
215
- >
216
- {t(locale.label)}
194
+ {PDF_LANGUAGE_CODES.map((lang) => (
195
+ <DropdownMenuItem key={lang.code} onClick={() => handleDownloadPdf(lang.code)} className="cursor-pointer">
196
+ {t(lang.label)}
217
197
  </DropdownMenuItem>
218
198
  ))}
219
199
  </DropdownMenuContent>
@@ -90,6 +90,7 @@ export function DocumentDetailsCard({
90
90
  const t = createTranslation({ translations, locale, ...i18nProps });
91
91
 
92
92
  const currencyCode = document.currency_code;
93
+ const sign = documentType === "credit_note" ? -1 : 1;
93
94
  const fmt = (amount: number) => formatCurrency(amount, currencyCode, locale);
94
95
  const fmtDate = (date: Date | string | null | undefined) => formatDate(date, locale);
95
96
 
@@ -102,9 +103,6 @@ export function DocumentDetailsCard({
102
103
  // Get customer name
103
104
  const customerName = document.customer?.name || "-";
104
105
 
105
- // Calculate tax total
106
- const taxTotal = document.total_with_tax - document.total;
107
-
108
106
  const bodyContent = (
109
107
  <>
110
108
  {/* Document info */}
@@ -152,15 +150,34 @@ export function DocumentDetailsCard({
152
150
  <div className="space-y-2 text-sm">
153
151
  <div className="flex justify-between">
154
152
  <span className="text-muted-foreground">{t("Subtotal")}</span>
155
- <span>{fmt(document.total)}</span>
156
- </div>
157
- <div className="flex justify-between">
158
- <span className="text-muted-foreground">{t("Tax")}</span>
159
- <span>{fmt(taxTotal)}</span>
153
+ <span>{fmt(document.total * sign)}</span>
160
154
  </div>
155
+ {document.taxes && document.taxes.length > 0 ? (
156
+ document.taxes.map((tax: Document["taxes"][number]) => {
157
+ const taxAmount = tax.amount ?? 0;
158
+ return (
159
+ <div key={String(tax.rate)} className="flex justify-between">
160
+ <span className="text-muted-foreground">
161
+ {t("Tax")} {tax.rate ?? 0}%{" "}
162
+ {tax.base != null && (
163
+ <span className="text-xs">
164
+ {t("of")} {fmt(tax.base * sign)}
165
+ </span>
166
+ )}
167
+ </span>
168
+ <span>{fmt(taxAmount * sign)}</span>
169
+ </div>
170
+ );
171
+ })
172
+ ) : (
173
+ <div className="flex justify-between">
174
+ <span className="text-muted-foreground">{t("Tax")}</span>
175
+ <span>{fmt(0)}</span>
176
+ </div>
177
+ )}
161
178
  <div className="flex justify-between font-semibold">
162
179
  <span>{t("Total")}</span>
163
- <span>{fmt(document.total_with_tax)}</span>
180
+ <span>{fmt(document.total_with_tax * sign)}</span>
164
181
  </div>
165
182
 
166
183
  {/* Payment info for invoices/advance invoices */}
@@ -38,6 +38,7 @@ export default {
38
38
  Customer: "Kunde",
39
39
  Subtotal: "Zwischensumme",
40
40
  Tax: "Steuer",
41
+ of: "von",
41
42
  Total: "Gesamt",
42
43
  Paid: "Bezahlt",
43
44
  Due: "Offen",
@@ -108,6 +109,7 @@ export default {
108
109
  Pending: "Ausstehend",
109
110
  Failed: "Fehlgeschlagen",
110
111
  Skipped: "Übersprungen",
112
+ "Skipped by user": "Vom Benutzer übersprungen",
111
113
  "Retry fiscalization": "Fiskalisierung wiederholen",
112
114
 
113
115
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Cliente",
38
38
  Subtotal: "Subtotal",
39
39
  Tax: "Impuesto",
40
+ of: "de",
40
41
  Total: "Total",
41
42
  Paid: "Pagado",
42
43
  Due: "Pendiente",
@@ -106,6 +107,7 @@ export default {
106
107
  Pending: "Pendiente",
107
108
  Failed: "Fallido",
108
109
  Skipped: "Omitido",
110
+ "Skipped by user": "Omitido por el usuario",
109
111
  "Retry fiscalization": "Reintentar fiscalización",
110
112
 
111
113
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Client",
38
38
  Subtotal: "Sous-total",
39
39
  Tax: "Taxe",
40
+ of: "de",
40
41
  Total: "Total",
41
42
  Paid: "Payé",
42
43
  Due: "Dû",
@@ -106,6 +107,7 @@ export default {
106
107
  Pending: "En attente",
107
108
  Failed: "Échoué",
108
109
  Skipped: "Ignoré",
110
+ "Skipped by user": "Ignoré par l'utilisateur",
109
111
  "Retry fiscalization": "Réessayer la fiscalisation",
110
112
 
111
113
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Kupac",
38
38
  Subtotal: "Međuzbroj",
39
39
  Tax: "Porez",
40
+ of: "od",
40
41
  Total: "Ukupno",
41
42
  Paid: "Plaćeno",
42
43
  Due: "Za plaćanje",
@@ -106,6 +107,7 @@ export default {
106
107
  Pending: "Na čekanju",
107
108
  Failed: "Neuspjelo",
108
109
  Skipped: "Preskočeno",
110
+ "Skipped by user": "Korisnik preskočio",
109
111
  "Retry fiscalization": "Ponovi fiskalizaciju",
110
112
 
111
113
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Cliente",
38
38
  Subtotal: "Subtotale",
39
39
  Tax: "Imposta",
40
+ of: "di",
40
41
  Total: "Totale",
41
42
  Paid: "Pagato",
42
43
  Due: "Da pagare",
@@ -106,6 +107,7 @@ export default {
106
107
  Pending: "In attesa",
107
108
  Failed: "Fallito",
108
109
  Skipped: "Saltato",
110
+ "Skipped by user": "Saltato dall'utente",
109
111
  "Retry fiscalization": "Riprova fiscalizzazione",
110
112
 
111
113
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Klant",
38
38
  Subtotal: "Subtotaal",
39
39
  Tax: "Belasting",
40
+ of: "van",
40
41
  Total: "Totaal",
41
42
  Paid: "Betaald",
42
43
  Due: "Openstaand",
@@ -107,6 +108,7 @@ export default {
107
108
  Pending: "In behandeling",
108
109
  Failed: "Mislukt",
109
110
  Skipped: "Overgeslagen",
111
+ "Skipped by user": "Overgeslagen door gebruiker",
110
112
  "Retry fiscalization": "Fiscalisatie opnieuw proberen",
111
113
 
112
114
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Klient",
38
38
  Subtotal: "Suma częściowa",
39
39
  Tax: "Podatek",
40
+ of: "z",
40
41
  Total: "Razem",
41
42
  Paid: "Zapłacono",
42
43
  Due: "Do zapłaty",
@@ -106,6 +107,7 @@ export default {
106
107
  Pending: "Oczekuje",
107
108
  Failed: "Niepowodzenie",
108
109
  Skipped: "Pominięto",
110
+ "Skipped by user": "Pominięto przez użytkownika",
109
111
  "Retry fiscalization": "Ponów fiskalizację",
110
112
 
111
113
  // PDF language names
@@ -37,6 +37,7 @@ export default {
37
37
  Customer: "Cliente",
38
38
  Subtotal: "Subtotal",
39
39
  Tax: "Imposto",
40
+ of: "de",
40
41
  Total: "Total",
41
42
  Paid: "Pago",
42
43
  Due: "Em dívida",
@@ -106,6 +107,7 @@ export default {
106
107
  Pending: "Pendente",
107
108
  Failed: "Falhado",
108
109
  Skipped: "Ignorado",
110
+ "Skipped by user": "Ignorado pelo utilizador",
109
111
  "Retry fiscalization": "Tentar novamente fiscalização",
110
112
 
111
113
  // PDF language names