@spaceinvoices/react-ui 0.1.1

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 (352) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +340 -0
  3. package/cli/dist/index.js +922 -0
  4. package/package.json +87 -0
  5. package/registry.json +600 -0
  6. package/spaceinvoices.schema.json +47 -0
  7. package/src/app.tsx +25 -0
  8. package/src/common/autocomplete.tsx +135 -0
  9. package/src/components/activities/activity-timeline.tsx +160 -0
  10. package/src/components/activities/index.ts +1 -0
  11. package/src/components/activities/locales/de.ts +30 -0
  12. package/src/components/activities/locales/sl.ts +30 -0
  13. package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
  14. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
  15. package/src/components/advance-invoices/create/locales/de.ts +29 -0
  16. package/src/components/advance-invoices/create/locales/sl.ts +25 -0
  17. package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
  18. package/src/components/advance-invoices/index.ts +5 -0
  19. package/src/components/advance-invoices/list/index.ts +3 -0
  20. package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
  21. package/src/components/advance-invoices/list/list-table.tsx +178 -0
  22. package/src/components/advance-invoices/list/locales/de.ts +32 -0
  23. package/src/components/advance-invoices/list/locales/sl.ts +32 -0
  24. package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
  25. package/src/components/button-loader.tsx +11 -0
  26. package/src/components/combobox.tsx +96 -0
  27. package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
  28. package/src/components/company-registry/company-registry.hooks.ts +67 -0
  29. package/src/components/company-registry/index.ts +7 -0
  30. package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
  31. package/src/components/credit-notes/create/index.ts +1 -0
  32. package/src/components/credit-notes/create/locales/de.ts +69 -0
  33. package/src/components/credit-notes/create/locales/sl.ts +67 -0
  34. package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
  35. package/src/components/credit-notes/index.ts +10 -0
  36. package/src/components/credit-notes/list/index.ts +3 -0
  37. package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
  38. package/src/components/credit-notes/list/list-table.tsx +183 -0
  39. package/src/components/credit-notes/list/locales/de.ts +33 -0
  40. package/src/components/credit-notes/list/locales/sl.ts +33 -0
  41. package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
  42. package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
  43. package/src/components/customers/create-customer-form/locales/de.ts +20 -0
  44. package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
  45. package/src/components/customers/customer-autocomplete.tsx +173 -0
  46. package/src/components/customers/customer-combobox.tsx +130 -0
  47. package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
  48. package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
  49. package/src/components/customers/customer-list-table/index.ts +2 -0
  50. package/src/components/customers/customer-list-table/locales/de.ts +16 -0
  51. package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
  52. package/src/components/customers/customers.hooks.test.ts +348 -0
  53. package/src/components/customers/customers.hooks.ts +57 -0
  54. package/src/components/customers/index.ts +5 -0
  55. package/src/components/dashboard/chart-empty-state.tsx +29 -0
  56. package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
  57. package/src/components/dashboard/collection-rate-card/index.ts +4 -0
  58. package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
  59. package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
  60. package/src/components/dashboard/index.ts +54 -0
  61. package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
  62. package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
  63. package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
  64. package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
  65. package/src/components/dashboard/loading-card.tsx +19 -0
  66. package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
  67. package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
  68. package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
  69. package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
  70. package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
  71. package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
  72. package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
  73. package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
  74. package/src/components/dashboard/revenue-card.tsx +49 -0
  75. package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
  76. package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
  77. package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
  78. package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
  79. package/src/components/dashboard/shared/index.ts +5 -0
  80. package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
  81. package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
  82. package/src/components/dashboard/shared/use-stats-query.ts +38 -0
  83. package/src/components/dashboard/stat-card.tsx +41 -0
  84. package/src/components/dashboard/tax-collected-card/index.ts +2 -0
  85. package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
  86. package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
  87. package/src/components/dashboard/top-customers-chart/index.ts +4 -0
  88. package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
  89. package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
  90. package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
  91. package/src/components/documents/create/document-add-item-form.tsx +379 -0
  92. package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
  93. package/src/components/documents/create/document-details-section.tsx +597 -0
  94. package/src/components/documents/create/document-items-section.tsx +133 -0
  95. package/src/components/documents/create/document-recipient-section.tsx +101 -0
  96. package/src/components/documents/create/form-types.ts +36 -0
  97. package/src/components/documents/create/index.ts +9 -0
  98. package/src/components/documents/create/live-preview.tsx +235 -0
  99. package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
  100. package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
  101. package/src/components/documents/create/prepare-document-submission.ts +187 -0
  102. package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
  103. package/src/components/documents/create/prepare-preview-data.ts +16 -0
  104. package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
  105. package/src/components/documents/create/use-document-customer-form.ts +161 -0
  106. package/src/components/documents/document-preview.tsx +13 -0
  107. package/src/components/documents/documents.hooks.ts +146 -0
  108. package/src/components/documents/index.ts +23 -0
  109. package/src/components/documents/shared/document-preview-display.tsx +172 -0
  110. package/src/components/documents/shared/index.ts +3 -0
  111. package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
  112. package/src/components/documents/shared/use-a4-scaling.ts +62 -0
  113. package/src/components/documents/types.ts +61 -0
  114. package/src/components/documents/view/document-actions-bar.tsx +328 -0
  115. package/src/components/documents/view/document-details-card.tsx +179 -0
  116. package/src/components/documents/view/document-payments-list.tsx +256 -0
  117. package/src/components/documents/view/index.ts +4 -0
  118. package/src/components/documents/view/locales/de.ts +85 -0
  119. package/src/components/documents/view/locales/sl.ts +84 -0
  120. package/src/components/documents/view/use-document-download.ts +125 -0
  121. package/src/components/entities/create-entity-form.tsx +105 -0
  122. package/src/components/entities/entities.hooks.ts +50 -0
  123. package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
  124. package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
  125. package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
  126. package/src/components/entities/entity-settings-form/index.ts +2 -0
  127. package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
  128. package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
  129. package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
  130. package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
  131. package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
  132. package/src/components/entities/furs-settings-form/index.ts +3 -0
  133. package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
  134. package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
  135. package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
  136. package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
  137. package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
  138. package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
  139. package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
  140. package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
  141. package/src/components/entities/keys.ts +2 -0
  142. package/src/components/entities/settings/branding-settings-form.tsx +274 -0
  143. package/src/components/entities/settings/company-settings-form.tsx +256 -0
  144. package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
  145. package/src/components/entities/settings/email-settings-form.tsx +288 -0
  146. package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
  147. package/src/components/entities/settings/index.ts +8 -0
  148. package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
  149. package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
  150. package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
  151. package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
  152. package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
  153. package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
  154. package/src/components/entities/settings/settings-footer.tsx +16 -0
  155. package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
  156. package/src/components/estimates/create/create-estimate-form.tsx +384 -0
  157. package/src/components/estimates/create/locales/de.ts +64 -0
  158. package/src/components/estimates/create/locales/sl.ts +63 -0
  159. package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
  160. package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
  161. package/src/components/estimates/estimates.hooks.ts +15 -0
  162. package/src/components/estimates/index.ts +6 -0
  163. package/src/components/estimates/list/index.ts +3 -0
  164. package/src/components/estimates/list/list-row-actions.tsx +103 -0
  165. package/src/components/estimates/list/list-table.tsx +171 -0
  166. package/src/components/estimates/list/locales/de.ts +26 -0
  167. package/src/components/estimates/list/locales/sl.ts +26 -0
  168. package/src/components/estimates/list/use-estimate-download.ts +63 -0
  169. package/src/components/export/document-export-form.tsx +288 -0
  170. package/src/components/export/index.ts +2 -0
  171. package/src/components/form/form-input.tsx +89 -0
  172. package/src/components/form/index.ts +1 -0
  173. package/src/components/invoices/create/create-invoice-form.tsx +852 -0
  174. package/src/components/invoices/create/eslog-validation.test.ts +242 -0
  175. package/src/components/invoices/create/eslog-validation.ts +208 -0
  176. package/src/components/invoices/create/locales/de.ts +118 -0
  177. package/src/components/invoices/create/locales/sl.ts +114 -0
  178. package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
  179. package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
  180. package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
  181. package/src/components/invoices/index.ts +9 -0
  182. package/src/components/invoices/invoices-furs.hooks.ts +28 -0
  183. package/src/components/invoices/invoices.hooks.ts +110 -0
  184. package/src/components/invoices/list/index.ts +3 -0
  185. package/src/components/invoices/list/list-row-actions.tsx +132 -0
  186. package/src/components/invoices/list/list-table.tsx +165 -0
  187. package/src/components/invoices/list/locales/de.ts +33 -0
  188. package/src/components/invoices/list/locales/sl.ts +33 -0
  189. package/src/components/invoices/list/use-invoice-download.ts +62 -0
  190. package/src/components/invoices/send-email-dialog/index.ts +1 -0
  191. package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
  192. package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
  193. package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
  194. package/src/components/invoices/send-email-dialog.tsx +2 -0
  195. package/src/components/invoices/shared/index.ts +2 -0
  196. package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
  197. package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
  198. package/src/components/invoices/view/eslog-info-display.tsx +160 -0
  199. package/src/components/invoices/view/furs-info-display.tsx +213 -0
  200. package/src/components/items/create-item-form/create-item-form.tsx +155 -0
  201. package/src/components/items/create-item-form/locales/de.ts +14 -0
  202. package/src/components/items/create-item-form/locales/en.ts +9 -0
  203. package/src/components/items/create-item-form/locales/sl.ts +14 -0
  204. package/src/components/items/item-combobox.tsx +147 -0
  205. package/src/components/items/item-list-table/item-list-header.tsx +33 -0
  206. package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
  207. package/src/components/items/item-list-table/item-list-row.tsx +32 -0
  208. package/src/components/items/item-list-table/item-list-table.tsx +76 -0
  209. package/src/components/items/item-list-table/locales/de.ts +10 -0
  210. package/src/components/items/item-list-table/locales/en.ts +10 -0
  211. package/src/components/items/item-list-table/locales/sl.ts +10 -0
  212. package/src/components/items/items.hooks.ts +63 -0
  213. package/src/components/loading-spinner.tsx +24 -0
  214. package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
  215. package/src/components/payments/create-payment-form/locales/de.ts +20 -0
  216. package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
  217. package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
  218. package/src/components/payments/edit-payment-form/index.ts +1 -0
  219. package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
  220. package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
  221. package/src/components/payments/index.ts +4 -0
  222. package/src/components/payments/list/index.ts +2 -0
  223. package/src/components/payments/list/list-row-actions.tsx +98 -0
  224. package/src/components/payments/list/list-table.tsx +186 -0
  225. package/src/components/payments/list/locales/de.ts +19 -0
  226. package/src/components/payments/list/locales/sl.ts +19 -0
  227. package/src/components/payments/payments.hooks.ts +15 -0
  228. package/src/components/request-logs/index.ts +3 -0
  229. package/src/components/request-logs/request-log-detail.tsx +242 -0
  230. package/src/components/request-logs/request-log-list-table.tsx +266 -0
  231. package/src/components/request-logs/request-logs-page.tsx +10 -0
  232. package/src/components/table/README.md +410 -0
  233. package/src/components/table/data-table.tsx +251 -0
  234. package/src/components/table/date-cell.tsx +35 -0
  235. package/src/components/table/filter-bar.tsx +114 -0
  236. package/src/components/table/filter-panel.tsx +407 -0
  237. package/src/components/table/hooks/use-table-fetch.ts +17 -0
  238. package/src/components/table/hooks/use-table-query.ts +36 -0
  239. package/src/components/table/hooks/use-table-state.ts +293 -0
  240. package/src/components/table/index.ts +35 -0
  241. package/src/components/table/search-input.tsx +85 -0
  242. package/src/components/table/sortable-header.tsx +56 -0
  243. package/src/components/table/table-empty-state.tsx +40 -0
  244. package/src/components/table/table-no-results.tsx +41 -0
  245. package/src/components/table/table-pagination.tsx +42 -0
  246. package/src/components/table/table-skeleton.tsx +54 -0
  247. package/src/components/table/types.ts +136 -0
  248. package/src/components/tax-reports/index.ts +1 -0
  249. package/src/components/tax-reports/kir-export-form.tsx +172 -0
  250. package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
  251. package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
  252. package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
  253. package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
  254. package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
  255. package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
  256. package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
  257. package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
  258. package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
  259. package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
  260. package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
  261. package/src/components/taxes/taxes.hooks.ts +35 -0
  262. package/src/components/ui/alert-dialog.tsx +61 -0
  263. package/src/components/ui/alert.tsx +72 -0
  264. package/src/components/ui/badge.tsx +48 -0
  265. package/src/components/ui/breadcrumb.tsx +132 -0
  266. package/src/components/ui/button.tsx +61 -0
  267. package/src/components/ui/calendar.tsx +213 -0
  268. package/src/components/ui/card.tsx +94 -0
  269. package/src/components/ui/chart.tsx +380 -0
  270. package/src/components/ui/checkbox.tsx +27 -0
  271. package/src/components/ui/collapsible.tsx +56 -0
  272. package/src/components/ui/command.tsx +187 -0
  273. package/src/components/ui/dialog.tsx +187 -0
  274. package/src/components/ui/drawer.tsx +123 -0
  275. package/src/components/ui/dropdown-menu.tsx +291 -0
  276. package/src/components/ui/form.tsx +166 -0
  277. package/src/components/ui/input-group.tsx +149 -0
  278. package/src/components/ui/input.tsx +20 -0
  279. package/src/components/ui/label.tsx +18 -0
  280. package/src/components/ui/loading-spinner.tsx +16 -0
  281. package/src/components/ui/popover.tsx +108 -0
  282. package/src/components/ui/radio-group.tsx +37 -0
  283. package/src/components/ui/select.tsx +200 -0
  284. package/src/components/ui/separator.tsx +23 -0
  285. package/src/components/ui/sheet.tsx +145 -0
  286. package/src/components/ui/sidebar.tsx +771 -0
  287. package/src/components/ui/skeleton.tsx +13 -0
  288. package/src/components/ui/sonner.tsx +60 -0
  289. package/src/components/ui/spinner.tsx +10 -0
  290. package/src/components/ui/sticky-form-footer.tsx +55 -0
  291. package/src/components/ui/switch.tsx +30 -0
  292. package/src/components/ui/table.tsx +101 -0
  293. package/src/components/ui/tabs.tsx +80 -0
  294. package/src/components/ui/textarea.tsx +18 -0
  295. package/src/components/ui/tooltip.tsx +89 -0
  296. package/src/components/wl-subscription/index.ts +2 -0
  297. package/src/components/wl-subscription/locked-feature.tsx +173 -0
  298. package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
  299. package/src/frontend.tsx +28 -0
  300. package/src/generate-schemas.ts +265 -0
  301. package/src/generated/schemas/advanceinvoice.ts +177 -0
  302. package/src/generated/schemas/creditnote.ts +187 -0
  303. package/src/generated/schemas/customer.ts +29 -0
  304. package/src/generated/schemas/entity.ts +252 -0
  305. package/src/generated/schemas/estimate.ts +159 -0
  306. package/src/generated/schemas/furssettings.ts +25 -0
  307. package/src/generated/schemas/index.ts +24 -0
  308. package/src/generated/schemas/invoice.ts +167 -0
  309. package/src/generated/schemas/item.ts +38 -0
  310. package/src/generated/schemas/payment.ts +44 -0
  311. package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
  312. package/src/generated/schemas/previewestimate_body.ts +309 -0
  313. package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
  314. package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
  315. package/src/generated/schemas/renderdocument_body.ts +594 -0
  316. package/src/generated/schemas/sendemail_body.ts +26 -0
  317. package/src/generated/schemas/startpdfexport_body.ts +20 -0
  318. package/src/generated/schemas/tax.ts +48 -0
  319. package/src/generated/schemas/uploadfile_body.ts +23 -0
  320. package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
  321. package/src/generated/schemas/userfurssettings.ts +19 -0
  322. package/src/hooks/create-resource-hooks.test.ts +483 -0
  323. package/src/hooks/create-resource-hooks.ts +300 -0
  324. package/src/hooks/use-debounce.ts +12 -0
  325. package/src/hooks/use-duplicate-document.ts +185 -0
  326. package/src/hooks/use-media-query.tsx +19 -0
  327. package/src/hooks/use-mobile.ts +39 -0
  328. package/src/hooks/use-next-document-number.ts +57 -0
  329. package/src/hooks/use-resource-mutation.ts +118 -0
  330. package/src/hooks/use-vies-check.ts +130 -0
  331. package/src/index.css +11 -0
  332. package/src/index.html +13 -0
  333. package/src/index.tsx +12 -0
  334. package/src/lib/auth.ts +4 -0
  335. package/src/lib/browser-cookies.ts +70 -0
  336. package/src/lib/constants.ts +287 -0
  337. package/src/lib/cookies.ts +36 -0
  338. package/src/lib/schemas/advance-invoice.ts +43 -0
  339. package/src/lib/schemas/credit-note.ts +32 -0
  340. package/src/lib/schemas/estimate.ts +31 -0
  341. package/src/lib/schemas/index.ts +18 -0
  342. package/src/lib/schemas/invoice.ts +43 -0
  343. package/src/lib/schemas/shared.ts +79 -0
  344. package/src/lib/translation.ts +38 -0
  345. package/src/lib/utils.ts +6 -0
  346. package/src/providers/entities-context.tsx +41 -0
  347. package/src/providers/entities-provider.tsx +201 -0
  348. package/src/providers/form-footer-context.tsx +72 -0
  349. package/src/providers/sdk-provider.tsx +164 -0
  350. package/src/providers/white-label-provider.tsx +91 -0
  351. package/src/providers/wl-subscription-provider.tsx +277 -0
  352. package/src/utils/string-helpers.ts +111 -0
@@ -0,0 +1,852 @@
1
+ import { zodResolver } from "@hookform/resolvers/zod";
2
+ import type { CreateInvoiceRequest, Invoice } from "@spaceinvoices/js-sdk";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { AlertCircle, Check, FileCode2, X } from "lucide-react";
5
+ import type { ReactNode } from "react";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import type { Resolver } from "react-hook-form";
8
+ import { useForm, useWatch } from "react-hook-form";
9
+ import type { z } from "zod";
10
+ import { Alert, AlertDescription, AlertTitle } from "@/ui/components/ui/alert";
11
+ import { Button } from "@/ui/components/ui/button";
12
+ import { Form } from "@/ui/components/ui/form";
13
+ import { Skeleton } from "@/ui/components/ui/skeleton";
14
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui/components/ui/tooltip";
15
+ import { createInvoiceSchema } from "@/ui/generated/schemas";
16
+ import { useViesCheck } from "@/ui/hooks/use-vies-check";
17
+ import type { ComponentTranslationProps } from "@/ui/lib/translation";
18
+ import { createTranslation } from "@/ui/lib/translation";
19
+ import { cn } from "@/ui/lib/utils";
20
+ import { useEntities } from "@/ui/providers/entities-context";
21
+ import { useFormFooterRegistration } from "@/ui/providers/form-footer-context";
22
+ import { CUSTOMERS_CACHE_KEY } from "../../customers/customers.hooks";
23
+ import {
24
+ DocumentDetailsSection,
25
+ DocumentNoteField,
26
+ DocumentPaymentTermsField,
27
+ } from "../../documents/create/document-details-section";
28
+ import { DocumentItemsSection, type PriceModesMap } from "../../documents/create/document-items-section";
29
+ import { DocumentRecipientSection } from "../../documents/create/document-recipient-section";
30
+ import { MarkAsPaidSection } from "../../documents/create/mark-as-paid-section";
31
+ import type { DocumentTypes } from "../../documents/types";
32
+ import { useFursPremises, useFursSettings } from "../../entities/furs-settings-form/furs-settings.hooks";
33
+ import {
34
+ getLastUsedFursCombo,
35
+ setLastUsedFursCombo,
36
+ useCreateInvoice,
37
+ useNextInvoiceNumber,
38
+ useUpdateInvoice,
39
+ } from "../invoices.hooks";
40
+ import { getEntityErrors, getFormFieldErrors, validateEslogForm } from "./eslog-validation";
41
+ import de from "./locales/de";
42
+ import sl from "./locales/sl";
43
+ import { prepareInvoiceSubmission } from "./prepare-invoice-submission";
44
+ import { useInvoiceCustomerForm } from "./use-invoice-customer-form";
45
+
46
+ const translations = {
47
+ sl,
48
+ de,
49
+ } as const;
50
+
51
+ // Form values: extend schema with local-only fields (number is for display, not sent to API)
52
+ type CreateInvoiceFormValues = z.infer<typeof createInvoiceSchema> & {
53
+ number?: string;
54
+ };
55
+
56
+ /** Preview payload extends request with display-only fields */
57
+ type InvoicePreviewPayload = Partial<CreateInvoiceRequest> & { number?: string };
58
+
59
+ type DocumentAddFormProps = {
60
+ type: DocumentTypes;
61
+ entityId: string;
62
+ onSuccess?: (data: Invoice) => void;
63
+ onError?: (error: unknown) => void;
64
+ onChange?: (data: InvoicePreviewPayload) => void;
65
+ onAddNewTax?: () => void;
66
+ onHeaderActionChange?: (action: ReactNode) => void;
67
+ /** Initial values for form fields (used for document duplication or editing) */
68
+ initialValues?: Partial<CreateInvoiceRequest>;
69
+ /** Mode: create (default) or edit */
70
+ mode?: "create" | "edit";
71
+ /** Document ID for edit mode */
72
+ documentId?: string;
73
+ } & ComponentTranslationProps;
74
+
75
+ export default function CreateInvoiceForm({
76
+ type: _type,
77
+ entityId,
78
+ onSuccess,
79
+ onError,
80
+ onChange,
81
+ onAddNewTax,
82
+ onHeaderActionChange,
83
+ initialValues,
84
+ mode = "create",
85
+ documentId,
86
+ t: translateProp,
87
+ namespace,
88
+ locale,
89
+ }: DocumentAddFormProps) {
90
+ const t = createTranslation({
91
+ t: translateProp,
92
+ namespace,
93
+ locale,
94
+ translations,
95
+ });
96
+
97
+ const isEditMode = mode === "edit";
98
+ const { activeEntity } = useEntities();
99
+ const queryClient = useQueryClient();
100
+
101
+ // Get default invoice note and payment terms from entity settings
102
+ const defaultInvoiceNote = (activeEntity?.settings as any)?.default_invoice_note || "";
103
+ const defaultPaymentTerms = (activeEntity?.settings as any)?.default_invoice_payment_terms || "";
104
+
105
+ // ============================================================================
106
+ // FURS Settings & Premises
107
+ // ============================================================================
108
+ const { data: fursSettings, isLoading: isFursSettingsLoading } = useFursSettings(entityId);
109
+ const { data: fursPremises, isLoading: isFursPremisesLoading } = useFursPremises(entityId, {
110
+ enabled: fursSettings?.enabled === true,
111
+ });
112
+
113
+ // Loading state for FURS - don't render form until we know if FURS is active
114
+ const isFursLoading = isFursSettingsLoading || (fursSettings?.enabled && isFursPremisesLoading);
115
+
116
+ // Check if FURS is enabled and has active premises
117
+ const isFursEnabled = fursSettings?.enabled === true;
118
+ const activePremises = useMemo(() => fursPremises?.filter((p) => p.is_active) || [], [fursPremises]);
119
+ const hasFursPremises = activePremises.length > 0;
120
+
121
+ // FURS premise/device selection state
122
+ const [selectedPremiseName, setSelectedPremiseName] = useState<string | undefined>();
123
+ const [selectedDeviceName, setSelectedDeviceName] = useState<string | undefined>();
124
+ const [skipFiscalization, setSkipFiscalization] = useState(false);
125
+
126
+ // UI-only state (not part of API schema)
127
+ const [markAsPaid, setMarkAsPaid] = useState(false);
128
+ const [paymentType, setPaymentType] = useState("bank_transfer");
129
+ const [isDraftPending, setIsDraftPending] = useState(false);
130
+
131
+ // Service date type state (single date or range)
132
+ const [serviceDateType, setServiceDateType] = useState<"single" | "range">("single");
133
+
134
+ // Price modes per item (gross vs net) - collected from component state at submit
135
+ // Initialize from initialValues for duplicated documents
136
+ const initialPriceModes = useMemo(() => {
137
+ if (!initialValues?.items) return {};
138
+ return initialValues.items.reduce((acc, item, index) => {
139
+ acc[index] = item.gross_price != null;
140
+ return acc;
141
+ }, {} as PriceModesMap);
142
+ }, [initialValues?.items]);
143
+ const priceModesRef = useRef<PriceModesMap>(initialPriceModes);
144
+
145
+ // ============================================================================
146
+ // e-SLOG Settings (Slovenian e-Invoice)
147
+ // ============================================================================
148
+ const isSloenianEntity = activeEntity?.country_code === "SI";
149
+ const entityEslogEnabled = !!(activeEntity?.settings as any)?.eslog_validation_enabled;
150
+ const isEslogAvailable = isSloenianEntity && entityEslogEnabled;
151
+
152
+ // e-SLOG validation state - defaults to entity setting
153
+ const [eslogValidationEnabled, setEslogValidationEnabled] = useState<boolean | undefined>(undefined);
154
+ // e-SLOG entity-level errors (require settings update, can't be fixed in form)
155
+ const [eslogEntityErrors, setEslogEntityErrors] = useState<Array<{ field: string; message: string }>>([]);
156
+
157
+ // Initialize e-SLOG state from entity settings
158
+ useEffect(() => {
159
+ if (isEslogAvailable && eslogValidationEnabled === undefined) {
160
+ setEslogValidationEnabled(true);
161
+ }
162
+ }, [isEslogAvailable, eslogValidationEnabled]);
163
+
164
+ // Clear entity errors when eslog validation is disabled
165
+ useEffect(() => {
166
+ if (!eslogValidationEnabled) {
167
+ setEslogEntityErrors([]);
168
+ }
169
+ }, [eslogValidationEnabled]);
170
+
171
+ // Get active devices for selected premise
172
+ const activeDevices = useMemo(() => {
173
+ if (!selectedPremiseName) return [];
174
+ const premise = activePremises.find((p) => p.business_premise_name === selectedPremiseName);
175
+ return premise?.Devices?.filter((d) => d.is_active) || [];
176
+ }, [activePremises, selectedPremiseName]);
177
+
178
+ // Initialize FURS selection from localStorage or first active combo
179
+ useEffect(() => {
180
+ if (!isFursEnabled || !hasFursPremises || selectedPremiseName) return;
181
+
182
+ const lastUsed = getLastUsedFursCombo(entityId);
183
+ if (lastUsed) {
184
+ // Verify the last-used combo is still valid (premise/device still exist and active)
185
+ const premise = activePremises.find((p) => p.business_premise_name === lastUsed.business_premise_name);
186
+ const device = premise?.Devices?.find(
187
+ (d) => d.electronic_device_name === lastUsed.electronic_device_name && d.is_active,
188
+ );
189
+ if (premise && device) {
190
+ setSelectedPremiseName(lastUsed.business_premise_name);
191
+ setSelectedDeviceName(lastUsed.electronic_device_name);
192
+ return;
193
+ }
194
+ }
195
+
196
+ // Fall back to first active premise/device
197
+ const firstPremise = activePremises[0];
198
+ const firstDevice = firstPremise?.Devices?.find((d) => d.is_active);
199
+ if (firstPremise && firstDevice) {
200
+ setSelectedPremiseName(firstPremise.business_premise_name);
201
+ setSelectedDeviceName(firstDevice.electronic_device_name);
202
+ }
203
+ }, [isFursEnabled, hasFursPremises, activePremises, entityId, selectedPremiseName]);
204
+
205
+ // When premise changes, select first active device
206
+ useEffect(() => {
207
+ if (!selectedPremiseName) return;
208
+ const premise = activePremises.find((p) => p.business_premise_name === selectedPremiseName);
209
+ const firstDevice = premise?.Devices?.find((d) => d.is_active);
210
+ if (firstDevice && selectedDeviceName !== firstDevice.electronic_device_name) {
211
+ // Only update if the current device is not in this premise
212
+ const currentDeviceInPremise = premise?.Devices?.find(
213
+ (d) => d.electronic_device_name === selectedDeviceName && d.is_active,
214
+ );
215
+ if (!currentDeviceInPremise) {
216
+ setSelectedDeviceName(firstDevice.electronic_device_name);
217
+ }
218
+ }
219
+ }, [selectedPremiseName, activePremises, selectedDeviceName]);
220
+
221
+ const form = useForm<CreateInvoiceFormValues>({
222
+ // Cast resolver to accept extended form type (includes UI-only fields)
223
+ resolver: zodResolver(createInvoiceSchema) as Resolver<CreateInvoiceFormValues>,
224
+ defaultValues: {
225
+ number: initialValues?.number || "", // Edit mode uses initialValues, create mode uses useNextInvoiceNumber
226
+ date: initialValues?.date || new Date().toISOString(),
227
+ customer_id: initialValues?.customer_id ?? undefined,
228
+ // Cast customer to form schema type (API type may have additional fields)
229
+ customer: (initialValues?.customer as CreateInvoiceFormValues["customer"]) ?? undefined,
230
+ items: initialValues?.items?.length
231
+ ? initialValues.items.map((item) => ({
232
+ name: item.name || "",
233
+ description: item.description || "",
234
+ quantity: item.quantity ?? 1,
235
+ // Use gross_price if set, otherwise use price
236
+ price: item.gross_price ?? item.price,
237
+ taxes: item.taxes || [],
238
+ }))
239
+ : [
240
+ {
241
+ name: "",
242
+ description: "",
243
+ quantity: 1,
244
+ price: undefined,
245
+ taxes: [],
246
+ },
247
+ ],
248
+ currency_code: initialValues?.currency_code || activeEntity?.currency_code || "EUR",
249
+ note: initialValues?.note ?? defaultInvoiceNote,
250
+ payment_terms: initialValues?.payment_terms ?? defaultPaymentTerms,
251
+ date_service: new Date().toISOString(),
252
+ },
253
+ });
254
+
255
+ // Skip fiscalization is only allowed for bank transfers or unpaid invoices
256
+ const canSkipFiscalization = !markAsPaid || paymentType === "bank_transfer";
257
+
258
+ // Auto-disable skip when it becomes invalid (e.g., user changes payment type to cash)
259
+ useEffect(() => {
260
+ if (!canSkipFiscalization && skipFiscalization) {
261
+ setSkipFiscalization(false);
262
+ }
263
+ }, [canSkipFiscalization, skipFiscalization]);
264
+
265
+ // Clear date_service_to when switching from range to single
266
+ useEffect(() => {
267
+ if (serviceDateType === "single") {
268
+ form.setValue("date_service_to", undefined);
269
+ }
270
+ }, [serviceDateType, form]);
271
+
272
+ // Check if FURS selection is ready (needed to prevent number flashing)
273
+ // Selection is ready when: FURS not enabled, OR no premises, OR we have a valid selection
274
+ const isFursSelectionReady = !isFursEnabled || !hasFursPremises || (!!selectedPremiseName && !!selectedDeviceName);
275
+
276
+ // FURS is "active" for this invoice if enabled and we have a valid selection (and not skipped)
277
+ const isFursActive =
278
+ isFursEnabled && hasFursPremises && selectedPremiseName && selectedDeviceName && !skipFiscalization;
279
+
280
+ // ============================================================================
281
+ // Next Invoice Number Preview
282
+ // ============================================================================
283
+ // Wait for FURS selection to be ready before querying to prevent number flashing
284
+ // Skip in edit mode - we use the existing document number
285
+ const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextInvoiceNumber(entityId, {
286
+ business_premise_name: isFursActive ? selectedPremiseName : undefined,
287
+ electronic_device_name: isFursActive ? selectedDeviceName : undefined,
288
+ enabled: !!entityId && !isFursLoading && isFursSelectionReady && !isEditMode,
289
+ });
290
+
291
+ // Overall loading state - wait until we have FURS data, selection ready, and next number (only in create mode)
292
+ const isFormDataLoading = isEditMode
293
+ ? false // In edit mode, don't wait for next number
294
+ : isFursLoading || !isFursSelectionReady || isNextNumberLoading;
295
+
296
+ // Update header action with FURS and e-SLOG toggle buttons
297
+ useEffect(() => {
298
+ if (!onHeaderActionChange) return;
299
+
300
+ // Don't set header action while loading or in edit mode (FURS/e-SLOG not editable)
301
+ if (isFursLoading || isEditMode) {
302
+ onHeaderActionChange(null);
303
+ return;
304
+ }
305
+
306
+ const showFursToggle = isFursEnabled && hasFursPremises;
307
+ const showEslogToggle = isEslogAvailable;
308
+
309
+ if (showFursToggle || showEslogToggle) {
310
+ const isFursChecked = !skipFiscalization;
311
+ const isEslogChecked = eslogValidationEnabled === true;
312
+
313
+ onHeaderActionChange(
314
+ <div className="flex items-center gap-2">
315
+ {/* e-SLOG toggle */}
316
+ {showEslogToggle && (
317
+ <TooltipProvider>
318
+ <Tooltip>
319
+ <TooltipTrigger asChild>
320
+ <Button
321
+ type="button"
322
+ variant={isEslogChecked ? "outline" : "ghost"}
323
+ size="sm"
324
+ className={cn("h-8 cursor-pointer gap-2", !isEslogChecked && "text-muted-foreground")}
325
+ onClick={() => setEslogValidationEnabled(!eslogValidationEnabled)}
326
+ >
327
+ <div
328
+ className={cn(
329
+ "flex size-4 items-center justify-center rounded border",
330
+ isEslogChecked
331
+ ? "border-primary bg-primary text-primary-foreground"
332
+ : "border-muted-foreground bg-background text-muted-foreground",
333
+ )}
334
+ >
335
+ {isEslogChecked ? <Check className="size-3" /> : <FileCode2 className="size-3" />}
336
+ </div>
337
+ <span>{t("e-SLOG")}</span>
338
+ </Button>
339
+ </TooltipTrigger>
340
+ <TooltipContent side="bottom" className="max-w-xs">
341
+ {isEslogChecked
342
+ ? t("Click to skip e-SLOG validation for this invoice")
343
+ : t("Click to enable e-SLOG validation")}
344
+ </TooltipContent>
345
+ </Tooltip>
346
+ </TooltipProvider>
347
+ )}
348
+
349
+ {/* FURS toggle */}
350
+ {showFursToggle && (
351
+ <TooltipProvider>
352
+ <Tooltip>
353
+ <TooltipTrigger asChild>
354
+ <Button
355
+ type="button"
356
+ variant={isFursChecked ? "outline" : "ghost"}
357
+ size="sm"
358
+ className={cn(
359
+ "h-8 cursor-pointer gap-2",
360
+ !canSkipFiscalization && "cursor-not-allowed opacity-50",
361
+ !isFursChecked && "text-destructive hover:text-destructive",
362
+ )}
363
+ onClick={() => canSkipFiscalization && setSkipFiscalization(!skipFiscalization)}
364
+ >
365
+ <div
366
+ className={cn(
367
+ "flex size-4 items-center justify-center rounded border",
368
+ isFursChecked
369
+ ? "border-primary bg-primary text-primary-foreground"
370
+ : "border-destructive bg-destructive text-destructive-foreground",
371
+ )}
372
+ >
373
+ {isFursChecked ? <Check className="size-3" /> : <X className="size-3" />}
374
+ </div>
375
+ <span>{t("Fiscally verify")}</span>
376
+ </Button>
377
+ </TooltipTrigger>
378
+ <TooltipContent side="bottom" className="max-w-xs">
379
+ {canSkipFiscalization
380
+ ? isFursChecked
381
+ ? t("Click to skip fiscalization for this invoice")
382
+ : t("Click to enable fiscalization")
383
+ : t("Cannot skip fiscalization for cash payments")}
384
+ </TooltipContent>
385
+ </Tooltip>
386
+ </TooltipProvider>
387
+ )}
388
+ </div>,
389
+ );
390
+ } else {
391
+ onHeaderActionChange(null);
392
+ }
393
+ }, [
394
+ isFursLoading,
395
+ isFursEnabled,
396
+ hasFursPremises,
397
+ skipFiscalization,
398
+ canSkipFiscalization,
399
+ isEslogAvailable,
400
+ eslogValidationEnabled,
401
+ isEditMode,
402
+ onHeaderActionChange,
403
+ t,
404
+ ]);
405
+
406
+ // Pre-fill invoice number from preview
407
+ useEffect(() => {
408
+ if (nextNumberData?.number) {
409
+ form.setValue("number", nextNumberData.number);
410
+ }
411
+ }, [nextNumberData?.number, form]);
412
+
413
+ // Watch specific fields for VIES check (stable references)
414
+ const customerCountry = useWatch({ control: form.control, name: "customer.country" });
415
+ const customerCountryCode = useWatch({ control: form.control, name: "customer.country_code" });
416
+ const customerTaxNumber = useWatch({ control: form.control, name: "customer.tax_number" });
417
+
418
+ // Watch fields needed for document note/payment terms preview
419
+ const watchedNumber = useWatch({ control: form.control, name: "number" });
420
+ const watchedDate = useWatch({ control: form.control, name: "date" });
421
+ const watchedDateDue = useWatch({ control: form.control, name: "date_due" });
422
+ const watchedCurrencyCode = useWatch({ control: form.control, name: "currency_code" });
423
+ const watchedCustomer = useWatch({ control: form.control, name: "customer" });
424
+
425
+ // ============================================================================
426
+ // VIES Check - determine if reverse charge applies
427
+ // ============================================================================
428
+ const { reverseChargeApplies, warning: viesWarning } = useViesCheck({
429
+ issuerCountryCode: activeEntity?.country_code,
430
+ isTaxSubject: activeEntity?.is_tax_subject ?? true,
431
+ customerCountry,
432
+ customerCountryCode,
433
+ customerTaxNumber,
434
+ enabled: !!activeEntity,
435
+ });
436
+
437
+ // Extract customer management logic into a custom hook
438
+ const {
439
+ originalCustomer,
440
+ showCustomerForm,
441
+ shouldFocusName,
442
+ selectedCustomerId,
443
+ initialCustomerName,
444
+ handleCustomerSelect,
445
+ handleCustomerClear,
446
+ } = useInvoiceCustomerForm(form as any);
447
+
448
+ const { mutate: createInvoice, isPending: isCreatePending } = useCreateInvoice({
449
+ entityId,
450
+ onSuccess: (data) => {
451
+ // Save FURS combo to localStorage on successful creation
452
+ if (isFursActive && selectedPremiseName && selectedDeviceName) {
453
+ setLastUsedFursCombo(entityId, {
454
+ business_premise_name: selectedPremiseName,
455
+ electronic_device_name: selectedDeviceName,
456
+ });
457
+ }
458
+ // Invalidate customers cache when a customer was created/linked
459
+ // This ensures the new customer appears in autocomplete for future documents
460
+ if (data.customer_id) {
461
+ queryClient.invalidateQueries({ queryKey: [CUSTOMERS_CACHE_KEY] });
462
+ }
463
+ onSuccess?.(data);
464
+ },
465
+ onError,
466
+ });
467
+
468
+ const { mutate: updateInvoice, isPending: isUpdatePending } = useUpdateInvoice({
469
+ entityId,
470
+ onSuccess: (data) => {
471
+ // Invalidate customers cache when a customer was created/linked
472
+ if (data.customer_id) {
473
+ queryClient.invalidateQueries({ queryKey: [CUSTOMERS_CACHE_KEY] });
474
+ }
475
+ // Invalidate document queries to refresh the view
476
+ queryClient.invalidateQueries({ queryKey: ["documents", "invoice", documentId] });
477
+ onSuccess?.(data);
478
+ },
479
+ onError,
480
+ });
481
+
482
+ const isPending = isCreatePending || isUpdatePending;
483
+
484
+ // Shared submit logic for both regular save and save as draft
485
+ const submitInvoice = useCallback(
486
+ (values: CreateInvoiceFormValues, isDraft: boolean) => {
487
+ // Skip e-SLOG validation for drafts and edit mode
488
+ if (!isDraft && !isEditMode && eslogValidationEnabled) {
489
+ const validationErrors = validateEslogForm(values, activeEntity);
490
+
491
+ if (validationErrors.length > 0) {
492
+ const entityErrors = getEntityErrors(validationErrors);
493
+ const formErrors = getFormFieldErrors(validationErrors);
494
+ setEslogEntityErrors(entityErrors);
495
+ for (const error of formErrors) {
496
+ form.setError(error.field as any, {
497
+ type: "eslog",
498
+ message: error.message,
499
+ });
500
+ }
501
+ return;
502
+ }
503
+ setEslogEntityErrors([]);
504
+ }
505
+
506
+ // Build FURS options (skip for drafts and edit mode)
507
+ const fursOptions =
508
+ !isDraft && !isEditMode && isFursEnabled
509
+ ? skipFiscalization
510
+ ? { skip: true }
511
+ : selectedPremiseName && selectedDeviceName
512
+ ? { business_premise_name: selectedPremiseName, electronic_device_name: selectedDeviceName }
513
+ : undefined
514
+ : undefined;
515
+
516
+ // Build e-SLOG options (skip for drafts and edit mode)
517
+ const eslogOptions =
518
+ !isDraft && !isEditMode && isEslogAvailable
519
+ ? { validation_enabled: eslogValidationEnabled === true }
520
+ : undefined;
521
+
522
+ const payload = prepareInvoiceSubmission(values, {
523
+ originalCustomer,
524
+ wasCustomerFormShown: showCustomerForm,
525
+ markAsPaid: isDraft || isEditMode ? false : markAsPaid,
526
+ paymentType,
527
+ furs: fursOptions,
528
+ eslog: eslogOptions,
529
+ priceModes: priceModesRef.current,
530
+ isDraft,
531
+ });
532
+
533
+ if (isEditMode && documentId) {
534
+ // In edit mode, use updateInvoice
535
+ // Remove number from payload as it's not editable
536
+ const { number, ...updatePayload } = payload;
537
+ updateInvoice({ id: documentId, data: updatePayload });
538
+ } else {
539
+ createInvoice(payload);
540
+ }
541
+ },
542
+ [
543
+ activeEntity,
544
+ createInvoice,
545
+ updateInvoice,
546
+ documentId,
547
+ eslogValidationEnabled,
548
+ form,
549
+ isEditMode,
550
+ isEslogAvailable,
551
+ isFursEnabled,
552
+ markAsPaid,
553
+ originalCustomer,
554
+ paymentType,
555
+ selectedDeviceName,
556
+ selectedPremiseName,
557
+ showCustomerForm,
558
+ skipFiscalization,
559
+ ],
560
+ );
561
+
562
+ // Handle save as draft - triggers form validation then submits with isDraft=true
563
+ const handleSaveAsDraft = useCallback(async () => {
564
+ setIsDraftPending(true);
565
+ try {
566
+ const isValid = await form.trigger();
567
+ if (isValid) {
568
+ const values = form.getValues();
569
+ submitInvoice(values, true);
570
+ }
571
+ } finally {
572
+ setIsDraftPending(false);
573
+ }
574
+ }, [form, submitInvoice]);
575
+
576
+ // Memoize secondary action to prevent infinite loops in useFormFooterRegistration
577
+ // Don't show "Save as Draft" in edit mode
578
+ const draftLabel = t("Save as Draft");
579
+ const saveLabel = isEditMode ? t("Update") : t("Save");
580
+ const secondaryAction = useMemo(
581
+ () =>
582
+ isEditMode
583
+ ? undefined
584
+ : {
585
+ label: draftLabel,
586
+ onClick: handleSaveAsDraft,
587
+ isPending: isDraftPending,
588
+ },
589
+ [draftLabel, handleSaveAsDraft, isDraftPending, isEditMode],
590
+ );
591
+
592
+ // Watch isDirty to get stable reference
593
+ const isDirty = form.formState.isDirty;
594
+
595
+ useFormFooterRegistration({
596
+ formId: "create-invoice-form",
597
+ isPending,
598
+ isDirty,
599
+ label: saveLabel,
600
+ secondaryAction,
601
+ });
602
+
603
+ // Track if initial setup has been done
604
+ const initialSetupDoneRef = useRef(false);
605
+
606
+ // Set default note and payment terms from entity settings when entity data is available
607
+ // This handles the case where activeEntity loads asynchronously
608
+ useEffect(() => {
609
+ if (initialSetupDoneRef.current) return;
610
+ if (!activeEntity) return;
611
+
612
+ const entityDefaultNote = (activeEntity.settings as any)?.default_invoice_note;
613
+ if (entityDefaultNote && !form.getValues("note")) {
614
+ form.setValue("note", entityDefaultNote);
615
+ }
616
+ const entityDefaultPaymentTerms = (activeEntity.settings as any)?.default_invoice_payment_terms;
617
+ if (entityDefaultPaymentTerms && !form.getValues("payment_terms")) {
618
+ form.setValue("payment_terms", entityDefaultPaymentTerms);
619
+ }
620
+
621
+ // Auto-add tax field for tax subject entities
622
+ if (activeEntity.is_tax_subject) {
623
+ const items = form.getValues("items") || [];
624
+ if (items.length > 0 && (!items[0].taxes || items[0].taxes.length === 0)) {
625
+ form.setValue("items.0.taxes", [{ tax_id: undefined }]);
626
+ }
627
+ }
628
+
629
+ initialSetupDoneRef.current = true;
630
+ }, [activeEntity, form]);
631
+
632
+ // Use form.watch subscription for onChange callback (avoids re-render loops)
633
+ const prevPayloadRef = useRef<string>("");
634
+
635
+ useEffect(() => {
636
+ if (!onChange) return;
637
+
638
+ const buildPayload = (formValues: any): InvoicePreviewPayload => {
639
+ const currentItems = formValues.items || [];
640
+ const transformedItems = currentItems.map((item: any, index: number) => {
641
+ const { price, ...rest } = item;
642
+ const isGross = priceModesRef.current[index] ?? false;
643
+ return isGross ? { ...rest, gross_price: price } : { ...rest, price };
644
+ });
645
+ return {
646
+ number: formValues.number,
647
+ date: formValues.date,
648
+ customer_id: formValues.customer_id,
649
+ customer: formValues.customer,
650
+ items: transformedItems,
651
+ currency_code: formValues.currency_code,
652
+ note: formValues.note,
653
+ payment_terms: formValues.payment_terms,
654
+ };
655
+ };
656
+
657
+ // Initial call
658
+ const initialPayload = buildPayload(form.getValues());
659
+ prevPayloadRef.current = JSON.stringify(initialPayload);
660
+ onChange(initialPayload);
661
+
662
+ // Subscribe to changes
663
+ const subscription = form.watch((formValues) => {
664
+ const payload = buildPayload(formValues);
665
+ const payloadStr = JSON.stringify(payload);
666
+ if (payloadStr !== prevPayloadRef.current) {
667
+ prevPayloadRef.current = payloadStr;
668
+ onChange(payload);
669
+ }
670
+ });
671
+
672
+ return () => subscription.unsubscribe();
673
+ }, [onChange, form]);
674
+
675
+ const onSubmit = (values: CreateInvoiceFormValues) => {
676
+ submitInvoice(values, false);
677
+ };
678
+
679
+ // Show skeleton while loading FURS data and next number
680
+ if (isFormDataLoading) {
681
+ return (
682
+ <div className="space-y-8">
683
+ {/* Recipient + Details columns */}
684
+ <div className="flex w-full flex-col md:flex-row md:gap-6">
685
+ {/* Recipient section skeleton */}
686
+ <div className="flex-1 space-y-4">
687
+ <Skeleton className="h-7 w-24" /> {/* "Recipient" title */}
688
+ <Skeleton className="h-10 w-full" /> {/* Customer autocomplete */}
689
+ </div>
690
+ {/* Details section skeleton */}
691
+ <div className="flex-1 space-y-4">
692
+ <Skeleton className="h-7 w-20" /> {/* "Details" title */}
693
+ <Skeleton className="h-5 w-16" /> {/* "Number *" label */}
694
+ <Skeleton className="h-10 w-full" /> {/* Number field */}
695
+ <Skeleton className="h-5 w-12" /> {/* "Date *" label */}
696
+ <Skeleton className="h-10 w-full" /> {/* Date picker */}
697
+ <Skeleton className="h-5 w-16" /> {/* "Due Date" label */}
698
+ <Skeleton className="h-10 w-full" /> {/* Due date picker */}
699
+ <Skeleton className="h-5 w-20" /> {/* "Currency *" label */}
700
+ <Skeleton className="h-10 w-full" /> {/* Currency select */}
701
+ {/* Mark as paid section */}
702
+ <div className="space-y-3 rounded-md border p-4">
703
+ <div className="flex items-center gap-3">
704
+ <Skeleton className="h-4 w-4 rounded" /> {/* Checkbox */}
705
+ <Skeleton className="h-5 w-28" /> {/* "Mark as Paid" */}
706
+ </div>
707
+ </div>
708
+ </div>
709
+ </div>
710
+
711
+ {/* Items section skeleton */}
712
+ <div className="space-y-4">
713
+ <Skeleton className="h-7 w-16" /> {/* "Items" title */}
714
+ <div className="space-y-4 rounded-lg border p-4">
715
+ <Skeleton className="h-10 w-full" /> {/* Item name */}
716
+ <div className="flex gap-4">
717
+ <Skeleton className="h-10 w-24" /> {/* Quantity */}
718
+ <Skeleton className="h-10 flex-1" /> {/* Price */}
719
+ </div>
720
+ </div>
721
+ <Skeleton className="h-9 w-24" /> {/* Add item button */}
722
+ </div>
723
+
724
+ {/* Note field skeleton */}
725
+ <div className="space-y-2">
726
+ <Skeleton className="h-5 w-12" /> {/* "Note" label */}
727
+ <Skeleton className="h-24 w-full" /> {/* Textarea */}
728
+ </div>
729
+
730
+ {/* Save button skeleton */}
731
+ <Skeleton className="h-10 w-24" />
732
+ </div>
733
+ );
734
+ }
735
+
736
+ return (
737
+ <Form {...form}>
738
+ <form id="create-invoice-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
739
+ {/* e-SLOG entity-level validation errors */}
740
+ {eslogEntityErrors.length > 0 && (
741
+ <Alert variant="destructive">
742
+ <AlertCircle className="h-4 w-4" />
743
+ <AlertTitle>{t("e-SLOG Validation Failed")}</AlertTitle>
744
+ <AlertDescription>
745
+ <p className="mb-2">{t("The following entity settings need to be updated:")}</p>
746
+ <ul className="list-disc space-y-1 pl-4">
747
+ {eslogEntityErrors.map((error) => (
748
+ <li key={error.field} className="text-sm">
749
+ {error.message}
750
+ </li>
751
+ ))}
752
+ </ul>
753
+ </AlertDescription>
754
+ </Alert>
755
+ )}
756
+
757
+ <div className="flex w-full flex-col md:flex-row md:gap-6">
758
+ <DocumentRecipientSection
759
+ control={form.control}
760
+ entityId={entityId}
761
+ onCustomerSelect={handleCustomerSelect}
762
+ onCustomerClear={handleCustomerClear}
763
+ showCustomerForm={showCustomerForm}
764
+ shouldFocusName={shouldFocusName}
765
+ selectedCustomerId={selectedCustomerId}
766
+ initialCustomerName={initialCustomerName}
767
+ t={t}
768
+ />
769
+ <DocumentDetailsSection
770
+ control={form.control}
771
+ documentType={_type}
772
+ t={t}
773
+ fursInline={
774
+ // Hide FURS selector in edit mode - fiscalization is set at creation only
775
+ !isEditMode && isFursEnabled && hasFursPremises
776
+ ? {
777
+ premises: activePremises.map((p) => ({ id: p.id, business_premise_name: p.business_premise_name })),
778
+ devices: activeDevices.map((d) => ({ id: d.id, electronic_device_name: d.electronic_device_name })),
779
+ selectedPremise: selectedPremiseName,
780
+ selectedDevice: selectedDeviceName,
781
+ onPremiseChange: setSelectedPremiseName,
782
+ onDeviceChange: setSelectedDeviceName,
783
+ isSkipped: skipFiscalization,
784
+ }
785
+ : undefined
786
+ }
787
+ serviceDate={{
788
+ dateType: serviceDateType,
789
+ onDateTypeChange: setServiceDateType,
790
+ }}
791
+ >
792
+ {/* Invoice-specific: Mark as paid section (UI-only state, not in form schema) */}
793
+ {/* Hide in edit mode - payments are managed separately */}
794
+ {!isEditMode && (
795
+ <MarkAsPaidSection
796
+ checked={markAsPaid}
797
+ onCheckedChange={setMarkAsPaid}
798
+ paymentType={paymentType}
799
+ onPaymentTypeChange={setPaymentType}
800
+ t={t}
801
+ />
802
+ )}
803
+ </DocumentDetailsSection>
804
+ </div>
805
+
806
+ <DocumentItemsSection
807
+ control={form.control}
808
+ watch={form.watch}
809
+ setValue={form.setValue}
810
+ getValues={form.getValues}
811
+ entityId={entityId}
812
+ currencyCode={activeEntity?.currency_code ?? undefined}
813
+ onAddNewTax={onAddNewTax}
814
+ t={t}
815
+ taxesDisabled={reverseChargeApplies}
816
+ taxesDisabledMessage={
817
+ reverseChargeApplies ? t("Reverse charge - tax exempt EU B2B sale") : viesWarning ? viesWarning : undefined
818
+ }
819
+ maxTaxesPerItem={activeEntity?.country_rules?.max_taxes_per_item}
820
+ priceModesRef={priceModesRef}
821
+ initialPriceModes={initialPriceModes}
822
+ />
823
+
824
+ <DocumentNoteField
825
+ control={form.control}
826
+ t={t}
827
+ entity={activeEntity}
828
+ document={{
829
+ number: watchedNumber,
830
+ date: watchedDate,
831
+ date_due: watchedDateDue,
832
+ currency_code: watchedCurrencyCode,
833
+ customer: watchedCustomer as any,
834
+ }}
835
+ />
836
+
837
+ <DocumentPaymentTermsField
838
+ control={form.control}
839
+ t={t}
840
+ entity={activeEntity}
841
+ document={{
842
+ number: watchedNumber,
843
+ date: watchedDate,
844
+ date_due: watchedDateDue,
845
+ currency_code: watchedCurrencyCode,
846
+ customer: watchedCustomer as any,
847
+ }}
848
+ />
849
+ </form>
850
+ </Form>
851
+ );
852
+ }