@spaceinvoices/react-ui 0.4.5 → 0.4.7

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 (138) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/components/advance-invoices/advance-invoices.hooks.ts +2 -2
  4. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +146 -74
  5. package/src/components/advance-invoices/create/locales/de.ts +5 -0
  6. package/src/components/advance-invoices/create/locales/es.ts +5 -0
  7. package/src/components/advance-invoices/create/locales/fr.ts +5 -0
  8. package/src/components/advance-invoices/create/locales/hr.ts +5 -0
  9. package/src/components/advance-invoices/create/locales/it.ts +5 -0
  10. package/src/components/advance-invoices/create/locales/nl.ts +5 -0
  11. package/src/components/advance-invoices/create/locales/pl.ts +5 -0
  12. package/src/components/advance-invoices/create/locales/pt.ts +5 -0
  13. package/src/components/advance-invoices/create/locales/sl.ts +5 -0
  14. package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +5 -5
  15. package/src/components/credit-notes/create/create-credit-note-form.tsx +138 -72
  16. package/src/components/credit-notes/create/locales/de.ts +5 -0
  17. package/src/components/credit-notes/create/locales/es.ts +5 -0
  18. package/src/components/credit-notes/create/locales/fr.ts +5 -0
  19. package/src/components/credit-notes/create/locales/hr.ts +5 -0
  20. package/src/components/credit-notes/create/locales/it.ts +5 -0
  21. package/src/components/credit-notes/create/locales/nl.ts +5 -0
  22. package/src/components/credit-notes/create/locales/pl.ts +5 -0
  23. package/src/components/credit-notes/create/locales/pt.ts +5 -0
  24. package/src/components/credit-notes/create/locales/sl.ts +5 -0
  25. package/src/components/credit-notes/credit-notes.hooks.ts +2 -2
  26. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -92
  27. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +48 -82
  28. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +22 -31
  29. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +33 -48
  30. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +56 -76
  31. package/src/components/dashboard/shared/index.ts +1 -1
  32. package/src/components/dashboard/shared/use-revenue-data.ts +106 -182
  33. package/src/components/dashboard/shared/use-stats-counts.ts +18 -68
  34. package/src/components/dashboard/shared/use-stats-query.ts +35 -5
  35. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +57 -75
  36. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +38 -49
  37. package/src/components/delivery-notes/create/create-delivery-note-form.tsx +50 -2
  38. package/src/components/delivery-notes/create/locales/de.ts +5 -0
  39. package/src/components/delivery-notes/create/locales/es.ts +5 -0
  40. package/src/components/delivery-notes/create/locales/fr.ts +5 -0
  41. package/src/components/delivery-notes/create/locales/hr.ts +5 -0
  42. package/src/components/delivery-notes/create/locales/it.ts +5 -0
  43. package/src/components/delivery-notes/create/locales/nl.ts +5 -0
  44. package/src/components/delivery-notes/create/locales/pl.ts +5 -0
  45. package/src/components/delivery-notes/create/locales/pt.ts +5 -0
  46. package/src/components/delivery-notes/create/locales/sl.ts +5 -0
  47. package/src/components/documents/create/document-details-section.tsx +478 -350
  48. package/src/components/documents/create/document-recipient-section.tsx +30 -1
  49. package/src/components/documents/create/live-preview.tsx +15 -28
  50. package/src/components/documents/create/prepare-document-submission.ts +4 -1
  51. package/src/components/documents/create/smart-code-insert-button.tsx +6 -0
  52. package/src/components/documents/create/use-document-customer-form.ts +4 -0
  53. package/src/components/documents/shared/document-preview-skeleton.tsx +63 -0
  54. package/src/components/documents/shared/index.ts +1 -0
  55. package/src/components/documents/view/document-actions-bar.tsx +29 -7
  56. package/src/components/documents/view/document-details-card.tsx +6 -0
  57. package/src/components/documents/view/locales/de.ts +1 -0
  58. package/src/components/documents/view/locales/es.ts +1 -0
  59. package/src/components/documents/view/locales/fr.ts +1 -0
  60. package/src/components/documents/view/locales/hr.ts +1 -0
  61. package/src/components/documents/view/locales/it.ts +1 -0
  62. package/src/components/documents/view/locales/nl.ts +1 -0
  63. package/src/components/documents/view/locales/pl.ts +1 -0
  64. package/src/components/documents/view/locales/pt.ts +1 -0
  65. package/src/components/documents/view/locales/sl.ts +1 -0
  66. package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +6 -0
  67. package/src/components/entities/entity-settings-form/input-with-preview.tsx +2 -145
  68. package/src/components/entities/entity-settings-form/locales/de.ts +4 -0
  69. package/src/components/entities/entity-settings-form/locales/es.ts +4 -0
  70. package/src/components/entities/entity-settings-form/locales/fr.ts +4 -0
  71. package/src/components/entities/entity-settings-form/locales/hr.ts +4 -0
  72. package/src/components/entities/entity-settings-form/locales/it.ts +4 -0
  73. package/src/components/entities/entity-settings-form/locales/nl.ts +4 -0
  74. package/src/components/entities/entity-settings-form/locales/pl.ts +4 -0
  75. package/src/components/entities/entity-settings-form/locales/pt.ts +4 -0
  76. package/src/components/entities/entity-settings-form/locales/sl.ts +4 -0
  77. package/src/components/entities/fina-settings-form/fina-settings-form.tsx +15 -0
  78. package/src/components/entities/fina-settings-form/fina-settings.hooks.ts +5 -1
  79. package/src/components/entities/fina-settings-form/locales/de.ts +3 -0
  80. package/src/components/entities/fina-settings-form/locales/en.ts +3 -0
  81. package/src/components/entities/fina-settings-form/locales/es.ts +3 -0
  82. package/src/components/entities/fina-settings-form/locales/fr.ts +3 -0
  83. package/src/components/entities/fina-settings-form/locales/hr.ts +3 -0
  84. package/src/components/entities/fina-settings-form/locales/it.ts +3 -0
  85. package/src/components/entities/fina-settings-form/locales/nl.ts +3 -0
  86. package/src/components/entities/fina-settings-form/locales/pl.ts +3 -0
  87. package/src/components/entities/fina-settings-form/locales/pt.ts +3 -0
  88. package/src/components/entities/fina-settings-form/locales/sl.ts +3 -0
  89. package/src/components/entities/fina-settings-form/sections/premises-management-section.tsx +4 -4
  90. package/src/components/entities/fina-settings-form/sections/register-premise-dialog.tsx +3 -3
  91. package/src/components/entities/settings/defaults-settings-form.tsx +38 -1
  92. package/src/components/entities/settings/tax-rules-settings-form.tsx +32 -15
  93. package/src/components/estimates/create/create-estimate-form.tsx +46 -4
  94. package/src/components/estimates/create/locales/de.ts +5 -0
  95. package/src/components/estimates/create/locales/es.ts +5 -0
  96. package/src/components/estimates/create/locales/fr.ts +5 -0
  97. package/src/components/estimates/create/locales/hr.ts +5 -0
  98. package/src/components/estimates/create/locales/it.ts +5 -0
  99. package/src/components/estimates/create/locales/nl.ts +5 -0
  100. package/src/components/estimates/create/locales/pl.ts +5 -0
  101. package/src/components/estimates/create/locales/pt.ts +5 -0
  102. package/src/components/estimates/create/locales/sl.ts +5 -0
  103. package/src/components/invoices/create/create-invoice-form.tsx +258 -96
  104. package/src/components/invoices/create/locales/de.ts +19 -0
  105. package/src/components/invoices/create/locales/es.ts +19 -0
  106. package/src/components/invoices/create/locales/fr.ts +19 -0
  107. package/src/components/invoices/create/locales/hr.ts +19 -0
  108. package/src/components/invoices/create/locales/it.ts +19 -0
  109. package/src/components/invoices/create/locales/nl.ts +19 -0
  110. package/src/components/invoices/create/locales/pl.ts +19 -0
  111. package/src/components/invoices/create/locales/pt.ts +19 -0
  112. package/src/components/invoices/create/locales/sl.ts +19 -0
  113. package/src/components/invoices/create/prepare-invoice-submission.ts +5 -5
  114. package/src/components/invoices/invoices.hooks.ts +3 -3
  115. package/src/components/table/table-pagination.tsx +1 -1
  116. package/src/components/ui/progress.tsx +27 -0
  117. package/src/generate-schemas.ts +15 -2
  118. package/src/generated/schemas/advanceinvoice.ts +4 -0
  119. package/src/generated/schemas/creditnote.ts +3 -0
  120. package/src/generated/schemas/customer.ts +2 -0
  121. package/src/generated/schemas/deliverynote.ts +3 -0
  122. package/src/generated/schemas/entity.ts +14 -4
  123. package/src/generated/schemas/entityapikey.ts +19 -0
  124. package/src/generated/schemas/estimate.ts +4 -0
  125. package/src/generated/schemas/finasettings.ts +4 -3
  126. package/src/generated/schemas/index.ts +1 -0
  127. package/src/generated/schemas/invoice.ts +4 -0
  128. package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +17 -11
  129. package/src/generated/schemas/rendercreditnotepreview_body.ts +17 -11
  130. package/src/generated/schemas/renderdeliverynotepreview_body.ts +15 -8
  131. package/src/generated/schemas/renderestimatepreview_body.ts +15 -8
  132. package/src/generated/schemas/renderinvoicepreview_body.ts +17 -11
  133. package/src/generated/schemas/startpdfexport_body.ts +12 -5
  134. package/src/generated/schemas/webhook.ts +4 -0
  135. package/src/hooks/use-transaction-type-check.ts +152 -0
  136. package/src/hooks/use-vies-check.ts +7 -131
  137. package/src/lib/template-variables.tsx +167 -0
  138. package/src/providers/entities-context.tsx +2 -2
@@ -13,7 +13,7 @@ import { Form } from "@/ui/components/ui/form";
13
13
  import { Skeleton } from "@/ui/components/ui/skeleton";
14
14
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui/components/ui/tooltip";
15
15
  import { createInvoiceSchema } from "@/ui/generated/schemas";
16
- import { useViesCheck } from "@/ui/hooks/use-vies-check";
16
+ import { useTransactionTypeCheck } from "@/ui/hooks/use-transaction-type-check";
17
17
  import type { ComponentTranslationProps } from "@/ui/lib/translation";
18
18
  import { createTranslation } from "@/ui/lib/translation";
19
19
  import { cn } from "@/ui/lib/utils";
@@ -22,8 +22,10 @@ import { useFormFooterRegistration } from "@/ui/providers/form-footer-context";
22
22
  import { CUSTOMERS_CACHE_KEY } from "../../customers/customers.hooks";
23
23
  import {
24
24
  DocumentDetailsSection,
25
+ DocumentFooterField,
25
26
  DocumentNoteField,
26
27
  DocumentPaymentTermsField,
28
+ DocumentSignatureField,
27
29
  DocumentTaxClauseField,
28
30
  } from "../../documents/create/document-details-section";
29
31
  import { DocumentItemsSection, type PriceModesMap } from "../../documents/create/document-items-section";
@@ -61,6 +63,8 @@ function calculateDueDate(dateIso: string, days: number): string {
61
63
  return date.toISOString();
62
64
  }
63
65
 
66
+ const DUE_DAYS_PRESETS = [0, 7, 14, 30, 60, 90] as const;
67
+
64
68
  const translations = {
65
69
  sl,
66
70
  de,
@@ -132,6 +136,7 @@ export default function CreateInvoiceForm({
132
136
  // Get default invoice note and payment terms from entity settings
133
137
  const defaultInvoiceNote = (activeEntity?.settings as any)?.default_invoice_note || "";
134
138
  const defaultPaymentTerms = (activeEntity?.settings as any)?.default_invoice_payment_terms || "";
139
+ const defaultFooter = (activeEntity?.settings as any)?.document_footer || "";
135
140
  const defaultInvoiceDueDays = (activeEntity?.settings as any)?.default_invoice_due_days ?? 30;
136
141
 
137
142
  // ============================================================================
@@ -169,8 +174,8 @@ export default function CreateInvoiceForm({
169
174
  const hasFinaPremises = activeFinaPremises.length > 0;
170
175
 
171
176
  // FINA premise/device selection state (no skip - all FINA invoices must be fiscalized)
172
- const [selectedFinaPremiseId, setSelectedFinaPremiseId] = useState<string | undefined>();
173
- const [selectedFinaDeviceId, setSelectedFinaDeviceId] = useState<string | undefined>();
177
+ const [selectedFinaBusinessPremiseName, setSelectedFinaBusinessPremiseName] = useState<string | undefined>();
178
+ const [selectedFinaElectronicDeviceName, setSelectedFinaElectronicDeviceName] = useState<string | undefined>();
174
179
 
175
180
  // UI-only state (not part of API schema)
176
181
  const [markAsPaid, setMarkAsPaid] = useState(false);
@@ -180,6 +185,12 @@ export default function CreateInvoiceForm({
180
185
  // Service date type state (single date or range)
181
186
  const [serviceDateType, setServiceDateType] = useState<"single" | "range">("single");
182
187
 
188
+ // Due days type state for invoice due date selector
189
+ const [dueDaysType, setDueDaysType] = useState<number | "custom">(() => {
190
+ if (mode === "edit" || initialValues?.date_due) return "custom";
191
+ return (DUE_DAYS_PRESETS as readonly number[]).includes(defaultInvoiceDueDays) ? defaultInvoiceDueDays : "custom";
192
+ });
193
+
183
194
  // Price modes per item (gross vs net) - collected from component state at submit
184
195
  // Initialize from initialValues for duplicated documents
185
196
  const initialPriceModes = useMemo(() => {
@@ -269,22 +280,24 @@ export default function CreateInvoiceForm({
269
280
 
270
281
  // Get active FINA devices for selected premise
271
282
  const activeFinaDevices = useMemo(() => {
272
- if (!selectedFinaPremiseId) return [];
273
- const premise = activeFinaPremises.find((p: any) => p.premise_id === selectedFinaPremiseId);
283
+ if (!selectedFinaBusinessPremiseName) return [];
284
+ const premise = activeFinaPremises.find((p: any) => p.business_premise_name === selectedFinaBusinessPremiseName);
274
285
  return premise?.Devices?.filter((d: any) => d.is_active) || [];
275
- }, [activeFinaPremises, selectedFinaPremiseId]);
286
+ }, [activeFinaPremises, selectedFinaBusinessPremiseName]);
276
287
 
277
288
  // Initialize FINA selection from localStorage or first active combo
278
289
  useEffect(() => {
279
- if (!isFinaEnabled || !hasFinaPremises || selectedFinaPremiseId) return;
290
+ if (!isFinaEnabled || !hasFinaPremises || selectedFinaBusinessPremiseName) return;
280
291
 
281
292
  const lastUsed = getLastUsedFinaCombo(entityId);
282
293
  if (lastUsed) {
283
- const premise = activeFinaPremises.find((p: any) => p.premise_id === lastUsed.premise_id);
284
- const device = premise?.Devices?.find((d: any) => d.device_id === lastUsed.device_id && d.is_active);
294
+ const premise = activeFinaPremises.find((p: any) => p.business_premise_name === lastUsed.business_premise_name);
295
+ const device = premise?.Devices?.find(
296
+ (d: any) => d.electronic_device_name === lastUsed.electronic_device_name && d.is_active,
297
+ );
285
298
  if (premise && device) {
286
- setSelectedFinaPremiseId(lastUsed.premise_id);
287
- setSelectedFinaDeviceId(lastUsed.device_id);
299
+ setSelectedFinaBusinessPremiseName(lastUsed.business_premise_name);
300
+ setSelectedFinaElectronicDeviceName(lastUsed.electronic_device_name);
288
301
  return;
289
302
  }
290
303
  }
@@ -292,25 +305,25 @@ export default function CreateInvoiceForm({
292
305
  const firstPremise = activeFinaPremises[0];
293
306
  const firstDevice = firstPremise?.Devices?.find((d: any) => d.is_active);
294
307
  if (firstPremise && firstDevice) {
295
- setSelectedFinaPremiseId(firstPremise.premise_id);
296
- setSelectedFinaDeviceId(firstDevice.device_id);
308
+ setSelectedFinaBusinessPremiseName(firstPremise.business_premise_name);
309
+ setSelectedFinaElectronicDeviceName(firstDevice.electronic_device_name);
297
310
  }
298
- }, [isFinaEnabled, hasFinaPremises, activeFinaPremises, entityId, selectedFinaPremiseId]);
311
+ }, [isFinaEnabled, hasFinaPremises, activeFinaPremises, entityId, selectedFinaBusinessPremiseName]);
299
312
 
300
313
  // When FINA premise changes, select first active device
301
314
  useEffect(() => {
302
- if (!selectedFinaPremiseId) return;
303
- const premise = activeFinaPremises.find((p: any) => p.premise_id === selectedFinaPremiseId);
315
+ if (!selectedFinaBusinessPremiseName) return;
316
+ const premise = activeFinaPremises.find((p: any) => p.business_premise_name === selectedFinaBusinessPremiseName);
304
317
  const firstDevice = premise?.Devices?.find((d: any) => d.is_active);
305
- if (firstDevice && selectedFinaDeviceId !== firstDevice.device_id) {
318
+ if (firstDevice && selectedFinaElectronicDeviceName !== firstDevice.electronic_device_name) {
306
319
  const currentDeviceInPremise = premise?.Devices?.find(
307
- (d: any) => d.device_id === selectedFinaDeviceId && d.is_active,
320
+ (d: any) => d.electronic_device_name === selectedFinaElectronicDeviceName && d.is_active,
308
321
  );
309
322
  if (!currentDeviceInPremise) {
310
- setSelectedFinaDeviceId(firstDevice.device_id);
323
+ setSelectedFinaElectronicDeviceName(firstDevice.electronic_device_name);
311
324
  }
312
325
  }
313
- }, [selectedFinaPremiseId, activeFinaPremises, selectedFinaDeviceId]);
326
+ }, [selectedFinaBusinessPremiseName, activeFinaPremises, selectedFinaElectronicDeviceName]);
314
327
 
315
328
  const form = useForm<CreateInvoiceFormValues>({
316
329
  // Cast resolver to accept extended form type (includes UI-only fields)
@@ -345,9 +358,11 @@ export default function CreateInvoiceForm({
345
358
  },
346
359
  ],
347
360
  currency_code: initialValues?.currency_code || activeEntity?.currency_code || "EUR",
361
+ reference: (initialValues as any)?.reference ?? "",
348
362
  note: initialValues?.note ?? defaultInvoiceNote,
349
363
  tax_clause: (initialValues as any)?.tax_clause ?? "",
350
364
  payment_terms: initialValues?.payment_terms ?? defaultPaymentTerms,
365
+ footer: (initialValues as any)?.footer ?? defaultFooter,
351
366
  date_due:
352
367
  initialValues?.date_due ||
353
368
  calculateDueDate(initialValues?.date || new Date().toISOString(), defaultInvoiceDueDays),
@@ -373,6 +388,20 @@ export default function CreateInvoiceForm({
373
388
  }
374
389
  }, [serviceDateType, form]);
375
390
 
391
+ // Handle due days type change - recalculate due date for presets
392
+ const handleDueDaysTypeChange = useCallback(
393
+ (type: number | "custom") => {
394
+ setDueDaysType(type);
395
+ if (type !== "custom") {
396
+ const currentDate = form.getValues("date");
397
+ if (currentDate) {
398
+ form.setValue("date_due", calculateDueDate(currentDate, type));
399
+ }
400
+ }
401
+ },
402
+ [form],
403
+ );
404
+
376
405
  // Check if FURS selection is ready (needed to prevent number flashing)
377
406
  // Selection is ready when: FURS not enabled, OR no premises, OR we have a valid selection
378
407
  const isFursSelectionReady = !isFursEnabled || !hasFursPremises || (!!selectedPremiseName && !!selectedDeviceName);
@@ -383,17 +412,60 @@ export default function CreateInvoiceForm({
383
412
 
384
413
  // FINA selection ready and active checks
385
414
  const isFinaSelectionReady =
386
- !isFinaEnabled || !hasFinaPremises || (!!selectedFinaPremiseId && !!selectedFinaDeviceId);
387
- const isFinaActive = isFinaEnabled && hasFinaPremises && selectedFinaPremiseId && selectedFinaDeviceId;
415
+ !isFinaEnabled || !hasFinaPremises || (!!selectedFinaBusinessPremiseName && !!selectedFinaElectronicDeviceName);
416
+ const isFinaActive =
417
+ isFinaEnabled && hasFinaPremises && selectedFinaBusinessPremiseName && selectedFinaElectronicDeviceName;
418
+
419
+ // ============================================================================
420
+ // VIES Check - determine transaction type early (needed for number preview)
421
+ // ============================================================================
422
+ const customerCountry = useWatch({ control: form.control, name: "customer.country" });
423
+ const customerCountryCode = useWatch({ control: form.control, name: "customer.country_code" });
424
+ const customerTaxNumber = useWatch({ control: form.control, name: "customer.tax_number" });
425
+ const customerIsEndConsumerWatch =
426
+ useWatch({ control: form.control, name: "customer.is_end_consumer" as any }) === true;
427
+
428
+ const {
429
+ reverseChargeApplies,
430
+ transactionType,
431
+ isFetching: isViesFetching,
432
+ warning: viesWarning,
433
+ } = useTransactionTypeCheck({
434
+ issuerCountryCode: activeEntity?.country_code,
435
+ isTaxSubject: activeEntity?.is_tax_subject ?? true,
436
+ customerCountry,
437
+ customerCountryCode,
438
+ customerTaxNumber,
439
+ customerIsEndConsumer: customerIsEndConsumerWatch,
440
+ enabled: !!activeEntity,
441
+ });
442
+
443
+ // FINA numbering guard: use FINA numbering for domestic transactions (or all if unified numbering is on)
444
+ const finaUnifiedNumbering = finaSettings?.unified_numbering !== false;
445
+ const useFinaNumbering =
446
+ !!isFinaActive && (finaUnifiedNumbering || transactionType == null || transactionType === "domestic");
447
+ const isFinaNonDomestic = !!isFinaActive && !useFinaNumbering;
388
448
 
389
449
  // ============================================================================
390
450
  // Next Invoice Number Preview
391
451
  // ============================================================================
392
452
  // Wait for FURS selection to be ready before querying to prevent number flashing
393
453
  // Skip in edit mode - we use the existing document number
454
+ // Use the same premise/device params for both FURS and FINA (an entity is either one, never both)
455
+ const activePremiseName = isFursActive
456
+ ? selectedPremiseName
457
+ : useFinaNumbering
458
+ ? selectedFinaBusinessPremiseName
459
+ : undefined;
460
+ const activeDeviceNameForNumber = isFursActive
461
+ ? selectedDeviceName
462
+ : useFinaNumbering
463
+ ? selectedFinaElectronicDeviceName
464
+ : undefined;
465
+
394
466
  const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextInvoiceNumber(entityId, {
395
- business_premise_name: isFursActive ? selectedPremiseName : undefined,
396
- electronic_device_name: isFursActive ? selectedDeviceName : undefined,
467
+ business_premise_name: activePremiseName,
468
+ electronic_device_name: activeDeviceNameForNumber,
397
469
  enabled:
398
470
  !!entityId && !isFursLoading && isFursSelectionReady && !isFinaLoading && isFinaSelectionReady && !isEditMode,
399
471
  });
@@ -521,11 +593,6 @@ export default function CreateInvoiceForm({
521
593
  }
522
594
  }, [nextNumberData?.number, form]);
523
595
 
524
- // Watch specific fields for VIES check (stable references)
525
- const customerCountry = useWatch({ control: form.control, name: "customer.country" });
526
- const customerCountryCode = useWatch({ control: form.control, name: "customer.country_code" });
527
- const customerTaxNumber = useWatch({ control: form.control, name: "customer.tax_number" });
528
-
529
596
  // Watch fields needed for document note/payment terms preview
530
597
  const watchedNumber = useWatch({ control: form.control, name: "number" });
531
598
  const watchedDate = useWatch({ control: form.control, name: "date" });
@@ -533,26 +600,45 @@ export default function CreateInvoiceForm({
533
600
  const watchedCurrencyCode = useWatch({ control: form.control, name: "currency_code" });
534
601
  const watchedCustomer = useWatch({ control: form.control, name: "customer" });
535
602
 
536
- // ============================================================================
537
- // VIES Check - determine if reverse charge applies
538
- // ============================================================================
539
- const {
540
- reverseChargeApplies,
541
- transactionType,
542
- customerCountryCode: viesCustomerCountryCode,
543
- isFetching: isViesFetching,
544
- warning: viesWarning,
545
- } = useViesCheck({
546
- issuerCountryCode: activeEntity?.country_code,
547
- isTaxSubject: activeEntity?.is_tax_subject ?? true,
548
- customerCountry,
549
- customerCountryCode,
550
- customerTaxNumber,
551
- enabled: !!activeEntity,
552
- });
603
+ // Croatian invoice validation:
604
+ // - Domestic/3w B2C requires FINA
605
+ // - Domestic B2B (customer has tax number and NOT end consumer) is blocked
606
+ const isCroatianEntity = activeEntity?.country_code === "HR";
607
+ const customerHasTaxNumber = !!customerTaxNumber?.trim();
608
+ const isDomesticTransaction = transactionType === "domestic";
609
+ const requiresFinaFiscalization = isDomesticTransaction || transactionType === "3w_b2c";
610
+ const is3wTransaction = transactionType === "3w_b2b" || transactionType === "3w_b2c";
611
+
612
+ // Auto-toggle is_end_consumer based on tax number for Croatian domestic/3w customers
613
+ // Default: checked (end consumer). When tax number is entered: uncheck (business). User can override.
614
+ const prevAutoSetTaxRef = useRef<string | undefined>(undefined);
615
+ useEffect(() => {
616
+ if (!isCroatianEntity || !(isDomesticTransaction || is3wTransaction)) return;
617
+ const hasTaxNumber = !!customerTaxNumber?.trim();
618
+ const hadTaxNumber = !!prevAutoSetTaxRef.current?.trim();
619
+ prevAutoSetTaxRef.current = customerTaxNumber ?? undefined;
620
+
621
+ // Auto-uncheck when tax number goes from empty to filled (likely a business)
622
+ if (hasTaxNumber && !hadTaxNumber && customerIsEndConsumerWatch) {
623
+ form.setValue("customer.is_end_consumer" as any, false);
624
+ }
625
+ // Auto-check when tax number goes from filled to empty (likely an individual)
626
+ if (!hasTaxNumber && hadTaxNumber && !customerIsEndConsumerWatch) {
627
+ form.setValue("customer.is_end_consumer" as any, true);
628
+ }
629
+ }, [customerTaxNumber, isCroatianEntity, isDomesticTransaction, is3wTransaction, customerIsEndConsumerWatch, form]);
553
630
 
554
- // FINA non-domestic guard: hide FINA selectors for non-domestic transactions
555
- const isFinaNonDomestic = isFinaEnabled && viesCustomerCountryCode != null && viesCustomerCountryCode !== "HR";
631
+ const finaValidationError = (() => {
632
+ if (!isCroatianEntity || !requiresFinaFiscalization) return undefined;
633
+ // Domestic B2B is always blocked (3w B2B never reaches here since requiresFinaFiscalization is false for 3w B2B)
634
+ if (isDomesticTransaction && customerHasTaxNumber && !customerIsEndConsumerWatch) {
635
+ return t("Domestic B2B invoicing in Croatia is not supported");
636
+ }
637
+ if (!isFinaEnabled) {
638
+ return t("FINA fiscalization must be enabled for domestic invoices");
639
+ }
640
+ return undefined;
641
+ })();
556
642
 
557
643
  // Auto-populate tax_clause from entity settings when transaction type changes
558
644
  const effectiveTransactionType = transactionType ?? "domestic";
@@ -590,10 +676,10 @@ export default function CreateInvoiceForm({
590
676
  });
591
677
  }
592
678
  // Save FINA combo to localStorage on successful creation
593
- if (isFinaActive && selectedFinaPremiseId && selectedFinaDeviceId) {
679
+ if (isFinaActive && selectedFinaBusinessPremiseName && selectedFinaElectronicDeviceName) {
594
680
  setLastUsedFinaCombo(entityId, {
595
- premise_id: selectedFinaPremiseId,
596
- device_id: selectedFinaDeviceId,
681
+ business_premise_name: selectedFinaBusinessPremiseName,
682
+ electronic_device_name: selectedFinaElectronicDeviceName,
597
683
  });
598
684
  }
599
685
  // Invalidate customers cache when a customer was created/linked
@@ -625,6 +711,9 @@ export default function CreateInvoiceForm({
625
711
  // Shared submit logic for both regular save and save as draft
626
712
  const submitInvoice = useCallback(
627
713
  (values: CreateInvoiceFormValues, isDraft: boolean) => {
714
+ // Block Croatian domestic B2B and domestic B2C without FINA
715
+ if (finaValidationError) return;
716
+
628
717
  // Skip e-SLOG validation for drafts and edit mode
629
718
  if (!isDraft && !isEditMode && eslogValidationEnabled) {
630
719
  const validationErrors = validateEslogForm(values as any, activeEntity);
@@ -656,8 +745,16 @@ export default function CreateInvoiceForm({
656
745
 
657
746
  // Build FINA options (skip for drafts, edit mode, and non-domestic transactions)
658
747
  const finaOptions =
659
- !isDraft && !isEditMode && isFinaEnabled && !isFinaNonDomestic && selectedFinaPremiseId && selectedFinaDeviceId
660
- ? { premise_id: selectedFinaPremiseId, device_id: selectedFinaDeviceId, payment_type: paymentTypes[0] }
748
+ !isDraft &&
749
+ !isEditMode &&
750
+ useFinaNumbering &&
751
+ selectedFinaBusinessPremiseName &&
752
+ selectedFinaElectronicDeviceName
753
+ ? {
754
+ business_premise_name: selectedFinaBusinessPremiseName,
755
+ electronic_device_name: selectedFinaElectronicDeviceName,
756
+ payment_type: paymentTypes[0],
757
+ }
661
758
  : undefined;
662
759
 
663
760
  // Build e-SLOG options (skip for drafts and edit mode)
@@ -698,20 +795,20 @@ export default function CreateInvoiceForm({
698
795
  updateInvoice,
699
796
  documentId,
700
797
  eslogValidationEnabled,
798
+ finaValidationError,
701
799
  forceLinkedDocuments,
702
800
  form,
703
801
  isEditMode,
704
802
  isEslogAvailable,
705
803
  isFursEnabled,
706
- isFinaEnabled,
707
- isFinaNonDomestic,
804
+ useFinaNumbering,
708
805
  markAsPaid,
709
806
  originalCustomer,
710
807
  paymentTypes,
711
808
  selectedDeviceName,
712
809
  selectedPremiseName,
713
- selectedFinaPremiseId,
714
- selectedFinaDeviceId,
810
+ selectedFinaBusinessPremiseName,
811
+ selectedFinaElectronicDeviceName,
715
812
  showCustomerForm,
716
813
  skipFiscalization,
717
814
  ],
@@ -776,14 +873,23 @@ export default function CreateInvoiceForm({
776
873
  if (entityDefaultPaymentTerms && !form.getValues("payment_terms")) {
777
874
  form.setValue("payment_terms", entityDefaultPaymentTerms);
778
875
  }
876
+ const entityDefaultFooter = (activeEntity.settings as any)?.document_footer;
877
+ if (entityDefaultFooter && !form.getValues("footer")) {
878
+ form.setValue("footer", entityDefaultFooter);
879
+ }
880
+ const entityDefaultSignature = (activeEntity.settings as any)?.default_document_signature;
881
+ if (entityDefaultSignature && !form.getValues("signature")) {
882
+ form.setValue("signature", entityDefaultSignature);
883
+ }
779
884
 
780
- // Auto-populate due date from entity settings when entity loads async
885
+ // Auto-populate due date and due days type from entity settings when entity loads async
781
886
  if (!isEditMode && !initialValues?.date_due) {
782
887
  const dueDays = (activeEntity.settings as any)?.default_invoice_due_days ?? 30;
783
888
  const currentDate = form.getValues("date");
784
889
  if (currentDate) {
785
890
  form.setValue("date_due", calculateDueDate(currentDate, dueDays));
786
891
  }
892
+ setDueDaysType((DUE_DAYS_PRESETS as readonly number[]).includes(dueDays) ? dueDays : "custom");
787
893
  }
788
894
 
789
895
  // Auto-add tax field for tax subject entities
@@ -797,15 +903,16 @@ export default function CreateInvoiceForm({
797
903
  initialSetupDoneRef.current = true;
798
904
  }, [activeEntity, form, isEditMode, initialValues?.date_due]);
799
905
 
800
- // Recalculate due date when document date changes (skip in edit mode)
906
+ // Recalculate due date when document date changes (skip in edit mode and custom due days)
801
907
  const prevDateRef = useRef(form.getValues("date"));
802
908
  useEffect(() => {
803
909
  if (isEditMode) return;
804
910
  if (!watchedDate || watchedDate === prevDateRef.current) return;
805
911
  prevDateRef.current = watchedDate;
806
- const dueDays = (activeEntity?.settings as any)?.default_invoice_due_days ?? 30;
807
- form.setValue("date_due", calculateDueDate(watchedDate, dueDays));
808
- }, [watchedDate, activeEntity, isEditMode, form]);
912
+ if (dueDaysType !== "custom") {
913
+ form.setValue("date_due", calculateDueDate(watchedDate, dueDaysType));
914
+ }
915
+ }, [watchedDate, isEditMode, form, dueDaysType]);
809
916
 
810
917
  // Use form.watch subscription for onChange callback (avoids re-render loops)
811
918
  const prevPayloadRef = useRef<string>("");
@@ -827,8 +934,10 @@ export default function CreateInvoiceForm({
827
934
  customer: formValues.customer,
828
935
  items: transformedItems,
829
936
  currency_code: formValues.currency_code,
937
+ reference: formValues.reference,
830
938
  note: formValues.note,
831
939
  payment_terms: formValues.payment_terms,
940
+ signature: formValues.signature,
832
941
  };
833
942
  };
834
943
 
@@ -862,25 +971,36 @@ export default function CreateInvoiceForm({
862
971
  <div className="flex w-full flex-col md:flex-row md:gap-6">
863
972
  {/* Recipient section skeleton */}
864
973
  <div className="flex-1 space-y-4">
865
- <Skeleton className="h-7 w-24" /> {/* "Recipient" title */}
866
- <Skeleton className="h-10 w-full" /> {/* Customer autocomplete */}
974
+ <Skeleton className="h-7 w-24" />
975
+ <Skeleton className="h-10 w-full" />
867
976
  </div>
868
- {/* Details section skeleton */}
869
- <div className="flex-1 space-y-4">
870
- <Skeleton className="h-7 w-20" /> {/* "Details" title */}
871
- <Skeleton className="h-5 w-16" /> {/* "Number *" label */}
872
- <Skeleton className="h-10 w-full" /> {/* Number field */}
873
- <Skeleton className="h-5 w-12" /> {/* "Date *" label */}
874
- <Skeleton className="h-10 w-full" /> {/* Date picker */}
875
- <Skeleton className="h-5 w-16" /> {/* "Due Date" label */}
876
- <Skeleton className="h-10 w-full" /> {/* Due date picker */}
877
- <Skeleton className="h-5 w-20" /> {/* "Currency *" label */}
878
- <Skeleton className="h-10 w-full" /> {/* Currency select */}
879
- {/* Mark as paid section */}
977
+ {/* Details section skeleton — inline label rows */}
978
+ <div className="flex-1 space-y-3">
979
+ <Skeleton className="h-7 w-20" />
980
+ <div className="flex items-center gap-3">
981
+ <Skeleton className="h-5 w-[6.5rem] shrink-0" />
982
+ <Skeleton className="h-10 flex-1" />
983
+ </div>
984
+ <div className="flex items-center gap-3">
985
+ <Skeleton className="h-5 w-[6.5rem] shrink-0" />
986
+ <Skeleton className="h-10 flex-1" />
987
+ </div>
988
+ <div className="flex items-center gap-3">
989
+ <Skeleton className="h-5 w-[6.5rem] shrink-0" />
990
+ <Skeleton className="h-10 flex-1" />
991
+ </div>
992
+ <div className="flex items-center gap-3">
993
+ <Skeleton className="h-5 w-[6.5rem] shrink-0" />
994
+ <Skeleton className="h-10 flex-1" />
995
+ </div>
996
+ <div className="flex items-center gap-3">
997
+ <Skeleton className="h-5 w-[6.5rem] shrink-0" />
998
+ <Skeleton className="h-10 flex-1" />
999
+ </div>
880
1000
  <div className="space-y-3 rounded-md border p-4">
881
1001
  <div className="flex items-center gap-3">
882
- <Skeleton className="h-4 w-4 rounded" /> {/* Checkbox */}
883
- <Skeleton className="h-5 w-28" /> {/* "Mark as Paid" */}
1002
+ <Skeleton className="h-4 w-4 rounded" />
1003
+ <Skeleton className="h-5 w-28" />
884
1004
  </div>
885
1005
  </div>
886
1006
  </div>
@@ -888,25 +1008,22 @@ export default function CreateInvoiceForm({
888
1008
 
889
1009
  {/* Items section skeleton */}
890
1010
  <div className="space-y-4">
891
- <Skeleton className="h-7 w-16" /> {/* "Items" title */}
1011
+ <Skeleton className="h-7 w-16" />
892
1012
  <div className="space-y-4 rounded-lg border p-4">
893
- <Skeleton className="h-10 w-full" /> {/* Item name */}
1013
+ <Skeleton className="h-10 w-full" />
894
1014
  <div className="flex gap-4">
895
- <Skeleton className="h-10 w-24" /> {/* Quantity */}
896
- <Skeleton className="h-10 flex-1" /> {/* Price */}
1015
+ <Skeleton className="h-10 w-24" />
1016
+ <Skeleton className="h-10 flex-1" />
897
1017
  </div>
898
1018
  </div>
899
- <Skeleton className="h-9 w-24" /> {/* Add item button */}
1019
+ <Skeleton className="h-9 w-24" />
900
1020
  </div>
901
1021
 
902
1022
  {/* Note field skeleton */}
903
1023
  <div className="space-y-2">
904
- <Skeleton className="h-5 w-12" /> {/* "Note" label */}
905
- <Skeleton className="h-24 w-full" /> {/* Textarea */}
1024
+ <Skeleton className="h-5 w-12" />
1025
+ <Skeleton className="h-24 w-full" />
906
1026
  </div>
907
-
908
- {/* Save button skeleton */}
909
- <Skeleton className="h-10 w-24" />
910
1027
  </div>
911
1028
  );
912
1029
  }
@@ -914,6 +1031,14 @@ export default function CreateInvoiceForm({
914
1031
  return (
915
1032
  <Form {...form}>
916
1033
  <form id="create-invoice-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
1034
+ {/* Croatian domestic invoice validation errors */}
1035
+ {finaValidationError && (
1036
+ <Alert variant="destructive">
1037
+ <AlertCircle className="h-4 w-4" />
1038
+ <AlertTitle>{finaValidationError}</AlertTitle>
1039
+ </Alert>
1040
+ )}
1041
+
917
1042
  {/* e-SLOG entity-level validation errors */}
918
1043
  {eslogEntityErrors.length > 0 && (
919
1044
  <Alert variant="destructive">
@@ -942,6 +1067,7 @@ export default function CreateInvoiceForm({
942
1067
  shouldFocusName={shouldFocusName}
943
1068
  selectedCustomerId={selectedCustomerId}
944
1069
  initialCustomerName={initialCustomerName}
1070
+ showEndConsumerToggle={isCroatianEntity && (isDomesticTransaction || is3wTransaction)}
945
1071
  t={t}
946
1072
  />
947
1073
  <DocumentDetailsSection
@@ -963,14 +1089,20 @@ export default function CreateInvoiceForm({
963
1089
  : undefined
964
1090
  }
965
1091
  finaInline={
966
- !isEditMode && isFinaEnabled && hasFinaPremises && !isFinaNonDomestic
1092
+ !isEditMode && useFinaNumbering
967
1093
  ? {
968
- premises: activeFinaPremises.map((p: any) => ({ id: p.id, premise_id: p.premise_id })),
969
- devices: activeFinaDevices.map((d: any) => ({ id: d.id, device_id: d.device_id })),
970
- selectedPremise: selectedFinaPremiseId,
971
- selectedDevice: selectedFinaDeviceId,
972
- onPremiseChange: setSelectedFinaPremiseId,
973
- onDeviceChange: setSelectedFinaDeviceId,
1094
+ premises: activeFinaPremises.map((p: any) => ({
1095
+ id: p.id,
1096
+ business_premise_name: p.business_premise_name,
1097
+ })),
1098
+ devices: activeFinaDevices.map((d: any) => ({
1099
+ id: d.id,
1100
+ electronic_device_name: d.electronic_device_name,
1101
+ })),
1102
+ selectedPremise: selectedFinaBusinessPremiseName,
1103
+ selectedDevice: selectedFinaElectronicDeviceName,
1104
+ onPremiseChange: setSelectedFinaBusinessPremiseName,
1105
+ onDeviceChange: setSelectedFinaElectronicDeviceName,
974
1106
  }
975
1107
  : undefined
976
1108
  }
@@ -978,6 +1110,10 @@ export default function CreateInvoiceForm({
978
1110
  dateType: serviceDateType,
979
1111
  onDateTypeChange: setServiceDateType,
980
1112
  }}
1113
+ dueDays={{
1114
+ dueDaysType,
1115
+ onDueDaysTypeChange: handleDueDaysTypeChange,
1116
+ }}
981
1117
  >
982
1118
  {/* Invoice-specific: Mark as paid section (UI-only state, not in form schema) */}
983
1119
  {/* Hide in edit mode - payments are managed separately */}
@@ -988,7 +1124,7 @@ export default function CreateInvoiceForm({
988
1124
  paymentTypes={paymentTypes}
989
1125
  onPaymentTypesChange={setPaymentTypes}
990
1126
  t={t}
991
- alwaysShowPaymentType={!!isFinaActive}
1127
+ alwaysShowPaymentType={!!isFinaActive && requiresFinaFiscalization}
992
1128
  />
993
1129
  )}
994
1130
  </DocumentDetailsSection>
@@ -1054,6 +1190,32 @@ export default function CreateInvoiceForm({
1054
1190
  }}
1055
1191
  />
1056
1192
 
1193
+ <DocumentSignatureField
1194
+ control={form.control}
1195
+ t={t}
1196
+ entity={activeEntity}
1197
+ document={{
1198
+ number: watchedNumber,
1199
+ date: watchedDate,
1200
+ date_due: watchedDateDue,
1201
+ currency_code: watchedCurrencyCode,
1202
+ customer: watchedCustomer as any,
1203
+ }}
1204
+ />
1205
+
1206
+ <DocumentFooterField
1207
+ control={form.control}
1208
+ t={t}
1209
+ entity={activeEntity}
1210
+ document={{
1211
+ number: watchedNumber,
1212
+ date: watchedDate,
1213
+ date_due: watchedDateDue,
1214
+ currency_code: watchedCurrencyCode,
1215
+ customer: watchedCustomer as any,
1216
+ }}
1217
+ />
1218
+
1057
1219
  {sourceDocuments && sourceDocuments.length > 0 && (
1058
1220
  <LinkedDocumentsInfo documents={sourceDocuments} locale={locale || "en"} t={t} />
1059
1221
  )}
@@ -40,6 +40,9 @@ export default {
40
40
  "Insert variable": "Variable einfügen",
41
41
  "Add payment instructions, terms, or other notes...":
42
42
  "Zahlungsanweisungen, Bedingungen oder andere Notizen hinzufügen...",
43
+ // Signature field
44
+ Signature: "Unterschrift",
45
+ "Add signature text...": "Unterschriftstext hinzufügen...",
43
46
  // Payment terms field
44
47
  "Payment Terms": "Zahlungsbedingungen",
45
48
  "Add payment terms...": "Zahlungsbedingungen hinzufügen...",
@@ -120,6 +123,14 @@ export default {
120
123
  "Advance invoice": "Vorausrechnung",
121
124
  "FINA fiscalized invoices always use the current date":
122
125
  "FINA-fiskalisierte Rechnungen verwenden immer das aktuelle Datum",
126
+ // Due days selector
127
+ "On receipt": "Bei Erhalt",
128
+ "7 days": "7 Tage",
129
+ "14 days": "14 Tage",
130
+ "30 days": "30 Tage",
131
+ "60 days": "60 Tage",
132
+ "90 days": "90 Tage",
133
+ Custom: "Benutzerdefiniert",
123
134
  // Service date
124
135
  "Service Date": "Leistungsdatum",
125
136
  "Single Date": "Datum",
@@ -141,4 +152,12 @@ export default {
141
152
  "Diese Rechnung wird nicht fiskalisiert (nicht-inländische Transaktion)",
142
153
  "Tax Clause": "Steuerklausel",
143
154
  "Add tax clause...": "Steuerklausel hinzufügen...",
155
+ Footer: "Fußzeile",
156
+ "Add document footer...": "Dokumentfußzeile hinzufügen...",
157
+ // Croatian domestic invoice validation
158
+ "End consumer": "Endverbraucher",
159
+ "Domestic B2B invoicing in Croatia is not supported":
160
+ "Inländische B2B-Rechnungsstellung in Kroatien wird nicht unterstützt. Kroatien erfordert die Einhaltung von Fiskalisierung 2.0 für B2B-Rechnungsstellung.",
161
+ "FINA fiscalization must be enabled for domestic invoices":
162
+ "FINA-Fiskalisierung muss für inländische Rechnungen aktiviert sein. Aktivieren Sie sie in den Unternehmenseinstellungen.",
144
163
  } as const;