@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,1326 @@
1
+ import { zodResolver } from "@hookform/resolvers/zod";
2
+ import type { Entity } from "@spaceinvoices/js-sdk";
3
+ import { CreditCard, FileText, Globe, Mail, Palette, Sparkles } from "lucide-react";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { useForm } from "react-hook-form";
6
+ import { z } from "zod";
7
+ import { SmartCodeInsertButton } from "@/ui/components/documents/create/smart-code-insert-button";
8
+ import { Button } from "@/ui/components/ui/button";
9
+ import {
10
+ Form,
11
+ FormControl,
12
+ FormDescription,
13
+ FormField,
14
+ FormItem,
15
+ FormLabel,
16
+ FormMessage,
17
+ } from "@/ui/components/ui/form";
18
+ import { Input } from "@/ui/components/ui/input";
19
+ import { Label } from "@/ui/components/ui/label";
20
+ import { RadioGroup, RadioGroupItem } from "@/ui/components/ui/radio-group";
21
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/components/ui/select";
22
+ import { Switch } from "@/ui/components/ui/switch";
23
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui/components/ui/tabs";
24
+ import { patchEntitySchema } from "@/ui/generated/schemas/entity";
25
+ import { CURRENCY_CODES } from "@/ui/lib/constants";
26
+ import type { ComponentTranslationProps } from "@/ui/lib/translation";
27
+ import { createTranslation } from "@/ui/lib/translation";
28
+ import { useSDK } from "@/ui/providers/sdk-provider";
29
+ import ButtonLoader from "../../button-loader";
30
+ import { useUpdateEntity } from "../entities.hooks";
31
+ import { EmailTemplateVariablesInfo } from "./email-template-variables-info";
32
+ import { ImageUploadWithCrop } from "./image-upload-with-crop";
33
+ import { InputWithPreview } from "./input-with-preview";
34
+ import de from "./locales/de";
35
+ import sl from "./locales/sl";
36
+
37
+ const translations = {
38
+ sl,
39
+ de,
40
+ } as const;
41
+
42
+ // Supported locales (matching backend)
43
+ const SUPPORTED_LOCALES = [
44
+ { value: "en-US", label: "English (US)" },
45
+ { value: "de-DE", label: "Deutsch (DE)" },
46
+ { value: "it-IT", label: "Italiano (IT)" },
47
+ { value: "fr-FR", label: "Français (FR)" },
48
+ { value: "es-ES", label: "Español (ES)" },
49
+ { value: "sl-SI", label: "Slovenščina (SI)" },
50
+ ] as const;
51
+
52
+ // Form schema extends the generated patchEntitySchema but flattens nested settings for better UX
53
+ // Uses .omit() to remove nested fields, then .extend() to add flattened versions
54
+ // This approach keeps the base validation from the API schema while allowing a better form structure
55
+ const entitySettingsFormSchema = patchEntitySchema
56
+ .omit({
57
+ settings: true, // Remove nested settings - we'll flatten them
58
+ metadata: true, // Not used in this form
59
+ environment: true, // Not editable here
60
+ })
61
+ .extend({
62
+ // Flattened settings fields for easier form handling
63
+ // These will be transformed back to nested structure on submission
64
+ primary_color: z
65
+ .union([z.string(), z.null()])
66
+ .refine((val) => !val || /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val), {
67
+ message: "Must be a valid hex color (e.g., #5c6ac4)",
68
+ })
69
+ .optional(),
70
+ has_logo: z.union([z.boolean(), z.null()]).optional(),
71
+ has_signature: z.union([z.boolean(), z.null()]).optional(),
72
+ email: z
73
+ .union([z.string(), z.null()])
74
+ .refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), {
75
+ message: "Must be a valid email address",
76
+ })
77
+ .optional(),
78
+ invoice_email_subject: z.union([z.string(), z.null()]).optional(),
79
+ invoice_email_body: z.union([z.string(), z.null()]).optional(),
80
+ estimate_email_subject: z.union([z.string(), z.null()]).optional(),
81
+ estimate_email_body: z.union([z.string(), z.null()]).optional(),
82
+ default_invoice_note: z.union([z.string(), z.null()]).optional(),
83
+ // Bank account fields (stored in settings.bank_accounts array)
84
+ bank_account_iban: z
85
+ .union([z.string(), z.null()])
86
+ .refine((val) => !val || /^[A-Z]{2}[0-9A-Z]{2,32}$/.test(val.replace(/\s/g, "")), {
87
+ message: "Must be a valid IBAN",
88
+ })
89
+ .optional(),
90
+ bank_account_name: z.union([z.string(), z.null()]).optional(),
91
+ bank_account_bank_name: z.union([z.string(), z.null()]).optional(),
92
+ bank_account_bic: z.union([z.string(), z.null()]).optional(),
93
+ // UPN QR settings (Slovenia only)
94
+ upn_qr_enabled: z.union([z.boolean(), z.null()]).optional(),
95
+ upn_qr_display_mode: z.enum(["qr_only", "full_slip"]).optional(),
96
+ upn_qr_purpose_code: z
97
+ .union([z.string(), z.null()])
98
+ .refine((val) => !val || /^[A-Z]{4}$/.test(val), {
99
+ message: "Must be a 4-letter uppercase code (e.g., OTHR)",
100
+ })
101
+ .optional(),
102
+ });
103
+
104
+ export type EntitySettingsFormSchema = z.infer<typeof entitySettingsFormSchema>;
105
+
106
+ export type EntitySettingsFormProps = {
107
+ entity: Entity;
108
+ cloudinaryCloudName?: string;
109
+ onSuccess?: (data: Entity) => void;
110
+ onError?: (error: unknown) => void;
111
+ onUploadSuccess?: () => void;
112
+ } & ComponentTranslationProps;
113
+
114
+ export function EntitySettingsForm({
115
+ entity,
116
+ cloudinaryCloudName,
117
+ t: translateProp,
118
+ namespace,
119
+ locale,
120
+ onSuccess,
121
+ onError,
122
+ onUploadSuccess,
123
+ }: EntitySettingsFormProps) {
124
+ const translate = createTranslation({
125
+ t: translateProp,
126
+ namespace,
127
+ locale,
128
+ translations,
129
+ });
130
+ const { sdk } = useSDK();
131
+ const [isUploading, setIsUploading] = useState(false);
132
+ const [isUploadingSignature, setIsUploadingSignature] = useState(false);
133
+ const [logoTimestamp, setLogoTimestamp] = useState(Date.now());
134
+ const [signatureTimestamp, setSignatureTimestamp] = useState(Date.now());
135
+ const [uploadedLogoUrl, setUploadedLogoUrl] = useState<string | null>(null);
136
+ const [uploadedSignatureUrl, setUploadedSignatureUrl] = useState<string | null>(null);
137
+ const [fetchedLogoUrl, setFetchedLogoUrl] = useState<string | null>(null);
138
+ const [fetchedSignatureUrl, setFetchedSignatureUrl] = useState<string | null>(null);
139
+
140
+ // Extract current settings from entity (needed early for useEffect)
141
+ const currentSettings = (entity.settings as any) || {};
142
+
143
+ const form = useForm<EntitySettingsFormSchema>({
144
+ resolver: zodResolver(entitySettingsFormSchema),
145
+ defaultValues: {
146
+ name: entity.name || "",
147
+ tax_number: (entity as any).tax_number || null,
148
+ address: (entity as any).address || null,
149
+ address_2: (entity as any).address_2 || null,
150
+ post_code: (entity as any).post_code || null,
151
+ city: (entity as any).city || null,
152
+ state: (entity as any).state || null,
153
+ currency_code: entity.currency_code || undefined,
154
+ locale: entity.locale || "en-US",
155
+ primary_color: currentSettings.primary_color || null,
156
+ has_logo: currentSettings.has_logo || null,
157
+ has_signature: currentSettings.has_signature || null,
158
+ email: currentSettings.email || null,
159
+ invoice_email_subject: currentSettings.email_defaults?.invoice_subject || null,
160
+ invoice_email_body: currentSettings.email_defaults?.invoice_body || null,
161
+ estimate_email_subject: currentSettings.email_defaults?.estimate_subject || null,
162
+ estimate_email_body: currentSettings.email_defaults?.estimate_body || null,
163
+ default_invoice_note: currentSettings.default_invoice_note || null,
164
+ // Bank account and UPN QR settings
165
+ bank_account_iban: currentSettings.bank_accounts?.[0]?.iban || null,
166
+ bank_account_name: currentSettings.bank_accounts?.[0]?.name || null,
167
+ bank_account_bank_name: currentSettings.bank_accounts?.[0]?.bank_name || null,
168
+ bank_account_bic: currentSettings.bank_accounts?.[0]?.bic || null,
169
+ upn_qr_enabled: currentSettings.upn_qr?.enabled || false,
170
+ upn_qr_display_mode: currentSettings.upn_qr?.display_mode || "qr_only",
171
+ upn_qr_purpose_code: currentSettings.upn_qr?.purpose_code || "OTHR",
172
+ },
173
+ });
174
+
175
+ // Refs for textarea cursor position tracking
176
+ const defaultInvoiceNoteRef = useRef<HTMLTextAreaElement>(null);
177
+ const invoiceEmailSubjectRef = useRef<HTMLInputElement>(null);
178
+ const invoiceEmailBodyRef = useRef<HTMLTextAreaElement>(null);
179
+ const estimateEmailSubjectRef = useRef<HTMLInputElement>(null);
180
+ const estimateEmailBodyRef = useRef<HTMLTextAreaElement>(null);
181
+
182
+ // Fetch logo and signature URLs from file metadata on mount
183
+ // Always fetch files to ensure we have the latest uploads, even if entity settings are stale
184
+ useEffect(() => {
185
+ async function fetchFileUrls() {
186
+ try {
187
+ // SDK auto-unwraps response, returns data array directly
188
+ const files = await sdk.files.list({ entity_id: entity.id });
189
+
190
+ const logoFile = files.data.find((f: { category: string }) => f.category === "logo");
191
+ const signatureFile = files.data.find((f: { category: string }) => f.category === "signature");
192
+
193
+ if (logoFile) {
194
+ setFetchedLogoUrl(logoFile.secureUrl);
195
+ // Sync form state if logo exists but form doesn't know about it
196
+ if (!form.getValues("has_logo")) {
197
+ form.setValue("has_logo", true);
198
+ }
199
+ }
200
+ if (signatureFile) {
201
+ setFetchedSignatureUrl(signatureFile.secureUrl);
202
+ // Sync form state if signature exists but form doesn't know about it
203
+ if (!form.getValues("has_signature")) {
204
+ form.setValue("has_signature", true);
205
+ }
206
+ }
207
+ } catch (error) {
208
+ console.error("Failed to fetch file URLs:", error);
209
+ // Fall back to constructed URLs if fetch fails
210
+ }
211
+ }
212
+
213
+ fetchFileUrls();
214
+ }, [entity.id, sdk.files, form]);
215
+
216
+ // Watch the has_logo and has_signature form fields for changes
217
+ const hasLogo = form.watch("has_logo");
218
+ const hasSignature = form.watch("has_signature");
219
+
220
+ // Logo URL priority: freshly uploaded > fetched from API > constructed fallback
221
+ const entityTimestamp = entity.updated_at ? new Date(entity.updated_at).getTime() : logoTimestamp;
222
+ const logoUrl =
223
+ hasLogo && cloudinaryCloudName
224
+ ? uploadedLogoUrl ||
225
+ fetchedLogoUrl ||
226
+ `https://res.cloudinary.com/${cloudinaryCloudName}/image/upload/leka/entities/${entity.id}/logos/logo_${entity.id}.png?v=${entityTimestamp}`
227
+ : undefined;
228
+
229
+ // Signature URL priority: freshly uploaded > fetched from API > constructed fallback
230
+ const signatureEntityTimestamp = entity.updated_at ? new Date(entity.updated_at).getTime() : signatureTimestamp;
231
+ const signatureUrl =
232
+ hasSignature && cloudinaryCloudName
233
+ ? uploadedSignatureUrl ||
234
+ fetchedSignatureUrl ||
235
+ `https://res.cloudinary.com/${cloudinaryCloudName}/image/upload/leka/entities/${entity.id}/signatures/signature_${entity.id}.png?v=${signatureEntityTimestamp}`
236
+ : undefined;
237
+
238
+ const { mutate: updateEntity, isPending } = useUpdateEntity({
239
+ entityId: entity.id,
240
+ onSuccess: (data) => {
241
+ onSuccess?.(data);
242
+ },
243
+ onError: (error) => {
244
+ onError?.(error);
245
+ },
246
+ });
247
+
248
+ const handleImageUpload = async (file: File): Promise<{ secureUrl: string }> => {
249
+ setIsUploading(true);
250
+ try {
251
+ // SDK expects { file: Blob } as first arg, SDKMethodOptions as last
252
+ const result = await sdk.upload.uploadImage({ file }, { entity_id: entity.id });
253
+
254
+ // Note: The upload endpoint automatically sets has_logo=true in entity settings
255
+ // We just need to update the form state
256
+ form.setValue("has_logo", true);
257
+
258
+ // Use the freshly uploaded URL with Cloudinary version (bypasses CDN cache)
259
+ // This ensures immediate preview update without waiting for CDN invalidation
260
+ setUploadedLogoUrl(result.secureUrl);
261
+
262
+ // Update timestamp to bust cache for future loads
263
+ setLogoTimestamp(Date.now());
264
+
265
+ // Trigger entity refetch to get the updated has_logo from database
266
+ onUploadSuccess?.();
267
+
268
+ return result;
269
+ } catch (error) {
270
+ console.error("Upload failed:", error);
271
+ onError?.(error);
272
+ throw error;
273
+ } finally {
274
+ setIsUploading(false);
275
+ }
276
+ };
277
+
278
+ const handleSignatureUpload = async (file: File): Promise<{ secureUrl: string }> => {
279
+ setIsUploadingSignature(true);
280
+ try {
281
+ // SDK expects { file, category } as first arg, SDKMethodOptions as last
282
+ const result = await sdk.files.uploadFile({ file, category: "signature" }, { entity_id: entity.id });
283
+
284
+ // Update form state
285
+ form.setValue("has_signature", true);
286
+
287
+ // Use the freshly uploaded URL
288
+ setUploadedSignatureUrl(result.secureUrl);
289
+
290
+ // Update timestamp to bust cache
291
+ setSignatureTimestamp(Date.now());
292
+
293
+ // Trigger entity refetch
294
+ onUploadSuccess?.();
295
+
296
+ return { secureUrl: result.secureUrl };
297
+ } catch (error) {
298
+ console.error("Signature upload failed:", error);
299
+ onError?.(error);
300
+ throw error;
301
+ } finally {
302
+ setIsUploadingSignature(false);
303
+ }
304
+ };
305
+
306
+ const onSubmit = async (values: EntitySettingsFormSchema) => {
307
+ try {
308
+ // Prepare the update payload
309
+ const updatePayload: any = {
310
+ settings: {
311
+ ...currentSettings,
312
+ primary_color: values.primary_color || undefined,
313
+ has_logo: values.has_logo || undefined,
314
+ has_signature: values.has_signature || undefined,
315
+ email: values.email || undefined,
316
+ email_defaults: {
317
+ invoice_subject: values.invoice_email_subject || undefined,
318
+ invoice_body: values.invoice_email_body || undefined,
319
+ estimate_subject: values.estimate_email_subject || undefined,
320
+ estimate_body: values.estimate_email_body || undefined,
321
+ },
322
+ default_invoice_note: values.default_invoice_note || undefined,
323
+ // UPN QR settings - only include if enabled or was previously enabled
324
+ upn_qr:
325
+ values.upn_qr_enabled || currentSettings.upn_qr
326
+ ? {
327
+ enabled: values.upn_qr_enabled || false,
328
+ display_mode: values.upn_qr_display_mode || "qr_only",
329
+ purpose_code: values.upn_qr_purpose_code || "OTHR",
330
+ }
331
+ : undefined,
332
+ // Bank accounts - store in array format (preserving other accounts if any)
333
+ bank_accounts: values.bank_account_iban
334
+ ? [
335
+ {
336
+ type: "iban" as const,
337
+ iban: values.bank_account_iban,
338
+ name: values.bank_account_name || undefined,
339
+ bank_name: values.bank_account_bank_name || undefined,
340
+ bic: values.bank_account_bic || undefined,
341
+ is_default: true,
342
+ },
343
+ // Preserve any additional bank accounts (beyond the first one)
344
+ ...(currentSettings.bank_accounts?.slice(1) || []),
345
+ ]
346
+ : currentSettings.bank_accounts || undefined,
347
+ },
348
+ };
349
+
350
+ // Add top-level fields if changed
351
+ if (values.name && values.name !== entity.name) {
352
+ updatePayload.name = values.name;
353
+ }
354
+ if (values.tax_number !== undefined && values.tax_number !== (entity as any).tax_number) {
355
+ updatePayload.tax_number = values.tax_number;
356
+ }
357
+ if (values.address !== undefined && values.address !== (entity as any).address) {
358
+ updatePayload.address = values.address;
359
+ }
360
+ if (values.address_2 !== undefined && values.address_2 !== (entity as any).address_2) {
361
+ updatePayload.address_2 = values.address_2;
362
+ }
363
+ if (values.post_code !== undefined && values.post_code !== (entity as any).post_code) {
364
+ updatePayload.post_code = values.post_code;
365
+ }
366
+ if (values.city !== undefined && values.city !== (entity as any).city) {
367
+ updatePayload.city = values.city;
368
+ }
369
+ if (values.state !== undefined && values.state !== (entity as any).state) {
370
+ updatePayload.state = values.state;
371
+ }
372
+ if (values.currency_code && values.currency_code !== entity.currency_code) {
373
+ updatePayload.currency_code = values.currency_code;
374
+ }
375
+ if (values.locale && values.locale !== entity.locale) {
376
+ updatePayload.locale = values.locale;
377
+ }
378
+
379
+ updateEntity({ id: entity.id, data: updatePayload });
380
+ } catch (e) {
381
+ onError?.(e);
382
+ form.setError("root", {
383
+ type: "submit",
384
+ message: "Failed to update entity settings",
385
+ });
386
+ }
387
+ };
388
+
389
+ return (
390
+ <Form {...form}>
391
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-12">
392
+ {/* Entity Details Section */}
393
+ <div className="grid gap-8 lg:grid-cols-2">
394
+ <div className="space-y-4">
395
+ <div className="flex items-center gap-3">
396
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-500/10">
397
+ <Globe className="h-5 w-5 text-green-600 dark:text-green-400" />
398
+ </div>
399
+ <div>
400
+ <h3 className="font-semibold text-lg">{translate("Entity Details")}</h3>
401
+ <p className="text-muted-foreground text-sm">{translate("Basic information about your entity")}</p>
402
+ </div>
403
+ </div>
404
+
405
+ <div className="space-y-6 pl-[52px]">
406
+ <FormField
407
+ control={form.control}
408
+ name="name"
409
+ render={({ field }) => (
410
+ <FormItem>
411
+ <FormLabel className="font-medium text-base">{translate("Entity Name")}</FormLabel>
412
+ <FormControl>
413
+ <Input {...field} placeholder="My Company LLC" className="h-10" />
414
+ </FormControl>
415
+ <FormDescription className="text-xs">
416
+ {translate("Your company or organization name")}
417
+ </FormDescription>
418
+ <FormMessage />
419
+ </FormItem>
420
+ )}
421
+ />
422
+
423
+ <FormField
424
+ control={form.control}
425
+ name="tax_number"
426
+ render={({ field }) => (
427
+ <FormItem className="max-w-xs">
428
+ <FormLabel className="font-medium text-base">{translate("Tax ID")}</FormLabel>
429
+ <FormControl>
430
+ <Input
431
+ {...field}
432
+ value={field.value || ""}
433
+ onChange={(e) => field.onChange(e.target.value || null)}
434
+ placeholder="12-3456789"
435
+ className="h-10"
436
+ />
437
+ </FormControl>
438
+ <FormDescription className="text-xs">
439
+ {translate("Tax identification number (optional)")}
440
+ </FormDescription>
441
+ <FormMessage />
442
+ </FormItem>
443
+ )}
444
+ />
445
+
446
+ <div className="border-t pt-6">
447
+ <FormField
448
+ control={form.control}
449
+ name="address"
450
+ render={({ field }) => (
451
+ <FormItem>
452
+ <FormLabel className="font-medium text-base">{translate("Address")}</FormLabel>
453
+ <FormControl>
454
+ <Input
455
+ {...field}
456
+ value={field.value || ""}
457
+ onChange={(e) => field.onChange(e.target.value || null)}
458
+ placeholder="123 Main Street"
459
+ className="h-10"
460
+ />
461
+ </FormControl>
462
+ <FormDescription className="text-xs">{translate("Street address")}</FormDescription>
463
+ <FormMessage />
464
+ </FormItem>
465
+ )}
466
+ />
467
+ </div>
468
+
469
+ <FormField
470
+ control={form.control}
471
+ name="address_2"
472
+ render={({ field }) => (
473
+ <FormItem>
474
+ <FormLabel className="font-medium text-base">{translate("Address Line 2")}</FormLabel>
475
+ <FormControl>
476
+ <Input
477
+ {...field}
478
+ value={field.value || ""}
479
+ onChange={(e) => field.onChange(e.target.value || null)}
480
+ placeholder="Suite 100"
481
+ className="h-10"
482
+ />
483
+ </FormControl>
484
+ <FormDescription className="text-xs">
485
+ {translate("Apartment, suite, unit, etc. (optional)")}
486
+ </FormDescription>
487
+ <FormMessage />
488
+ </FormItem>
489
+ )}
490
+ />
491
+
492
+ <div className="grid gap-6 md:grid-cols-3">
493
+ <FormField
494
+ control={form.control}
495
+ name="city"
496
+ render={({ field }) => (
497
+ <FormItem>
498
+ <FormLabel className="font-medium text-base">{translate("City")}</FormLabel>
499
+ <FormControl>
500
+ <Input
501
+ {...field}
502
+ value={field.value || ""}
503
+ onChange={(e) => field.onChange(e.target.value || null)}
504
+ placeholder="San Francisco"
505
+ className="h-10"
506
+ />
507
+ </FormControl>
508
+ <FormMessage />
509
+ </FormItem>
510
+ )}
511
+ />
512
+
513
+ <FormField
514
+ control={form.control}
515
+ name="state"
516
+ render={({ field }) => (
517
+ <FormItem>
518
+ <FormLabel className="font-medium text-base">{translate("State/Province")}</FormLabel>
519
+ <FormControl>
520
+ <Input
521
+ {...field}
522
+ value={field.value || ""}
523
+ onChange={(e) => field.onChange(e.target.value || null)}
524
+ placeholder="CA"
525
+ className="h-10"
526
+ />
527
+ </FormControl>
528
+ <FormMessage />
529
+ </FormItem>
530
+ )}
531
+ />
532
+
533
+ <FormField
534
+ control={form.control}
535
+ name="post_code"
536
+ render={({ field }) => (
537
+ <FormItem>
538
+ <FormLabel className="font-medium text-base">{translate("Postal Code")}</FormLabel>
539
+ <FormControl>
540
+ <Input
541
+ {...field}
542
+ value={field.value || ""}
543
+ onChange={(e) => field.onChange(e.target.value || null)}
544
+ placeholder="94102"
545
+ className="h-10"
546
+ />
547
+ </FormControl>
548
+ <FormMessage />
549
+ </FormItem>
550
+ )}
551
+ />
552
+ </div>
553
+
554
+ <FormItem>
555
+ <FormLabel className="font-medium text-base">{translate("Country")}</FormLabel>
556
+ <FormControl>
557
+ <Input value={(entity as any).country || ""} disabled className="h-10" />
558
+ </FormControl>
559
+ <FormDescription className="text-xs">{translate("Country cannot be changed")}</FormDescription>
560
+ </FormItem>
561
+ </div>
562
+ </div>
563
+
564
+ {/* Help Content */}
565
+ <div className="hidden lg:block">
566
+ <div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
567
+ <div className="flex items-start gap-2">
568
+ <Globe className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
569
+ <div className="space-y-2">
570
+ <p className="font-medium text-muted-foreground text-sm">{translate("Entity Information")}</p>
571
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
572
+ {translate(
573
+ "This information appears on your invoices and estimates. Your entity name and address will be displayed prominently on all documents.",
574
+ )}
575
+ </p>
576
+ </div>
577
+ </div>
578
+ </div>
579
+ </div>
580
+ </div>
581
+
582
+ {/* Customization Section */}
583
+ <div className="grid gap-8 lg:grid-cols-2">
584
+ <div className="space-y-4">
585
+ <div className="flex items-center gap-3">
586
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
587
+ <Palette className="h-5 w-5 text-primary" />
588
+ </div>
589
+ <div>
590
+ <h3 className="font-semibold text-lg">{translate("Customization")}</h3>
591
+ <p className="text-muted-foreground text-sm">{translate("Customize your invoice appearance")}</p>
592
+ </div>
593
+ </div>
594
+
595
+ <div className="space-y-6 pl-[52px]">
596
+ <FormField
597
+ control={form.control}
598
+ name="primary_color"
599
+ render={({ field }) => (
600
+ <FormItem className="max-w-xs">
601
+ <FormLabel className="font-medium text-base">{translate("Primary Color")}</FormLabel>
602
+ <FormControl>
603
+ <div className="flex items-center gap-3">
604
+ <div className="relative">
605
+ <Input
606
+ type="color"
607
+ {...field}
608
+ value={field.value || "#5c6ac4"}
609
+ className="h-12 w-16 cursor-pointer rounded-md border-2 p-1.5 shadow-sm transition-all hover:border-primary/50 hover:shadow-md"
610
+ />
611
+ </div>
612
+ <Input
613
+ type="text"
614
+ value={field.value || ""}
615
+ onChange={(e) => field.onChange(e.target.value)}
616
+ placeholder="#5c6ac4"
617
+ className="w-32 font-mono text-sm"
618
+ />
619
+ </div>
620
+ </FormControl>
621
+ <FormDescription className="text-xs">
622
+ {translate("Color used in invoices and documents")}
623
+ </FormDescription>
624
+ <FormMessage />
625
+ </FormItem>
626
+ )}
627
+ />
628
+
629
+ <div className="grid gap-6 border-t pt-6 lg:grid-cols-2">
630
+ <FormField
631
+ control={form.control}
632
+ name="has_logo"
633
+ render={({ field }) => (
634
+ <FormItem>
635
+ <FormLabel className="font-medium text-base">{translate("Logo")}</FormLabel>
636
+ <FormControl>
637
+ <ImageUploadWithCrop
638
+ value={logoUrl || ""}
639
+ onChange={(url) => field.onChange(!!url)}
640
+ onUpload={handleImageUpload}
641
+ translate={translate}
642
+ isUploading={isUploading}
643
+ />
644
+ </FormControl>
645
+ <FormDescription className="text-xs">
646
+ {translate("Upload your company logo for invoices")}
647
+ </FormDescription>
648
+ <FormMessage />
649
+ </FormItem>
650
+ )}
651
+ />
652
+
653
+ <FormField
654
+ control={form.control}
655
+ name="has_signature"
656
+ render={({ field }) => (
657
+ <FormItem>
658
+ <FormLabel className="font-medium text-base">{translate("Signature")}</FormLabel>
659
+ <FormControl>
660
+ <ImageUploadWithCrop
661
+ value={signatureUrl || ""}
662
+ onChange={(url) => field.onChange(!!url)}
663
+ onUpload={handleSignatureUpload}
664
+ translate={translate}
665
+ isUploading={isUploadingSignature}
666
+ imageType="signature"
667
+ />
668
+ </FormControl>
669
+ <FormDescription className="text-xs">
670
+ {translate("Upload a signature image for PDFs (optional)")}
671
+ </FormDescription>
672
+ <FormMessage />
673
+ </FormItem>
674
+ )}
675
+ />
676
+ </div>
677
+ </div>
678
+ </div>
679
+
680
+ {/* Help Content */}
681
+ <div className="hidden lg:block">
682
+ <div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
683
+ <div className="flex items-start gap-2">
684
+ <Palette className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
685
+ <div className="space-y-2">
686
+ <p className="font-medium text-muted-foreground text-sm">{translate("Brand Appearance")}</p>
687
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
688
+ {translate(
689
+ "Customize how your invoices and estimates look. Set your brand color, upload your company logo, and add a signature for a professional touch.",
690
+ )}
691
+ </p>
692
+ </div>
693
+ </div>
694
+ </div>
695
+ </div>
696
+ </div>
697
+
698
+ {/* Entity Settings Section */}
699
+ <div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
700
+ <div className="space-y-4">
701
+ <div className="flex items-center gap-3">
702
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10">
703
+ <Globe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
704
+ </div>
705
+ <div>
706
+ <h3 className="font-semibold text-lg">{translate("Entity Settings")}</h3>
707
+ <p className="text-muted-foreground text-sm">{translate("Configure entity localization")}</p>
708
+ </div>
709
+ </div>
710
+
711
+ <div className="space-y-6 pl-[52px]">
712
+ <div className="grid gap-6 md:grid-cols-2">
713
+ <FormField
714
+ control={form.control}
715
+ name="currency_code"
716
+ render={({ field }) => (
717
+ <FormItem>
718
+ <FormLabel className="font-medium text-sm">{translate("Currency")}</FormLabel>
719
+ <Select onValueChange={field.onChange} value={field.value || ""}>
720
+ <FormControl>
721
+ <SelectTrigger className="h-10">
722
+ <SelectValue placeholder={translate("Select currency")} />
723
+ </SelectTrigger>
724
+ </FormControl>
725
+ <SelectContent>
726
+ {CURRENCY_CODES.map((currency) => (
727
+ <SelectItem key={currency.value} value={currency.value}>
728
+ {currency.label}
729
+ </SelectItem>
730
+ ))}
731
+ </SelectContent>
732
+ </Select>
733
+ <FormMessage />
734
+ </FormItem>
735
+ )}
736
+ />
737
+
738
+ <FormField
739
+ control={form.control}
740
+ name="locale"
741
+ render={({ field }) => (
742
+ <FormItem>
743
+ <FormLabel className="font-medium text-sm">{translate("Locale")}</FormLabel>
744
+ <Select onValueChange={field.onChange} value={field.value || ""}>
745
+ <FormControl>
746
+ <SelectTrigger className="h-10">
747
+ <SelectValue placeholder={translate("Select locale")} />
748
+ </SelectTrigger>
749
+ </FormControl>
750
+ <SelectContent>
751
+ {SUPPORTED_LOCALES.map((locale) => (
752
+ <SelectItem key={locale.value} value={locale.value}>
753
+ {locale.label}
754
+ </SelectItem>
755
+ ))}
756
+ </SelectContent>
757
+ </Select>
758
+ <FormMessage />
759
+ </FormItem>
760
+ )}
761
+ />
762
+ </div>
763
+ </div>
764
+ </div>
765
+
766
+ {/* Help Content */}
767
+ <div className="hidden lg:block">
768
+ <div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
769
+ <div className="flex items-start gap-2">
770
+ <Globe className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
771
+ <div className="space-y-2">
772
+ <p className="font-medium text-muted-foreground text-sm">{translate("Localization")}</p>
773
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
774
+ {translate(
775
+ "Set your default currency and locale for invoices and estimates. These settings affect how numbers, dates, and currencies are displayed.",
776
+ )}
777
+ </p>
778
+ </div>
779
+ </div>
780
+ </div>
781
+ </div>
782
+ </div>
783
+
784
+ {/* UPN QR Payment Section - Only for Slovenian entities */}
785
+ {(entity as any).country_code === "SI" && (
786
+ <div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
787
+ <div className="space-y-4">
788
+ <div className="flex items-center gap-3">
789
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10">
790
+ <CreditCard className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
791
+ </div>
792
+ <div>
793
+ <h3 className="font-semibold text-lg">{translate("UPN QR Payment")}</h3>
794
+ <p className="text-muted-foreground text-sm">
795
+ {translate("Configure UPN QR payment slip for invoices")}
796
+ </p>
797
+ </div>
798
+ </div>
799
+
800
+ <div className="space-y-6 pl-[52px]">
801
+ <div className="space-y-4 rounded-lg border p-4">
802
+ <p className="font-medium text-sm">{translate("Bank Account")}</p>
803
+
804
+ <FormField
805
+ control={form.control}
806
+ name="bank_account_name"
807
+ render={({ field }) => (
808
+ <FormItem>
809
+ <FormLabel>{translate("Account Name")}</FormLabel>
810
+ <FormControl>
811
+ <Input
812
+ {...field}
813
+ value={field.value || ""}
814
+ onChange={(e) => field.onChange(e.target.value || null)}
815
+ placeholder={translate("Main Business Account")}
816
+ className="h-9"
817
+ />
818
+ </FormControl>
819
+ <FormMessage />
820
+ </FormItem>
821
+ )}
822
+ />
823
+
824
+ <FormField
825
+ control={form.control}
826
+ name="bank_account_iban"
827
+ render={({ field }) => (
828
+ <FormItem>
829
+ <FormLabel>{translate("IBAN")}</FormLabel>
830
+ <FormControl>
831
+ <Input
832
+ {...field}
833
+ value={field.value || ""}
834
+ onChange={(e) => field.onChange(e.target.value.toUpperCase().replace(/\s/g, "") || null)}
835
+ placeholder="SI56 0123 4567 8901 234"
836
+ className="h-9 font-mono"
837
+ />
838
+ </FormControl>
839
+ <FormMessage />
840
+ </FormItem>
841
+ )}
842
+ />
843
+
844
+ <div className="grid gap-4 md:grid-cols-2">
845
+ <FormField
846
+ control={form.control}
847
+ name="bank_account_bank_name"
848
+ render={({ field }) => (
849
+ <FormItem>
850
+ <FormLabel>{translate("Bank Name")}</FormLabel>
851
+ <FormControl>
852
+ <Input
853
+ {...field}
854
+ value={field.value || ""}
855
+ onChange={(e) => field.onChange(e.target.value || null)}
856
+ placeholder="NLB d.d."
857
+ className="h-9"
858
+ />
859
+ </FormControl>
860
+ <FormMessage />
861
+ </FormItem>
862
+ )}
863
+ />
864
+
865
+ <FormField
866
+ control={form.control}
867
+ name="bank_account_bic"
868
+ render={({ field }) => (
869
+ <FormItem>
870
+ <FormLabel>{translate("BIC/SWIFT")}</FormLabel>
871
+ <FormControl>
872
+ <Input
873
+ {...field}
874
+ value={field.value || ""}
875
+ onChange={(e) => field.onChange(e.target.value.toUpperCase() || null)}
876
+ placeholder="LJBASI2X"
877
+ className="h-9 font-mono"
878
+ />
879
+ </FormControl>
880
+ <FormMessage />
881
+ </FormItem>
882
+ )}
883
+ />
884
+ </div>
885
+ </div>
886
+
887
+ <div className="border-t pt-6">
888
+ <FormField
889
+ control={form.control}
890
+ name="upn_qr_enabled"
891
+ render={({ field }) => (
892
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
893
+ <div className="space-y-0.5">
894
+ <FormLabel className="font-medium text-base">
895
+ {translate("Enable UPN QR on invoices")}
896
+ </FormLabel>
897
+ <FormDescription className="text-xs">
898
+ {translate("Show payment QR code on PDF invoices for easy mobile banking payments")}
899
+ </FormDescription>
900
+ </div>
901
+ <FormControl>
902
+ <Switch
903
+ checked={field.value || false}
904
+ onCheckedChange={field.onChange}
905
+ disabled={!form.watch("bank_account_iban")}
906
+ />
907
+ </FormControl>
908
+ </FormItem>
909
+ )}
910
+ />
911
+
912
+ {form.watch("upn_qr_enabled") && (
913
+ <div className="mt-4 space-y-4">
914
+ <FormField
915
+ control={form.control}
916
+ name="upn_qr_display_mode"
917
+ render={({ field }) => (
918
+ <FormItem className="space-y-3">
919
+ <FormLabel className="font-medium text-sm">{translate("Display Mode")}</FormLabel>
920
+ <FormControl>
921
+ <RadioGroup
922
+ onValueChange={field.onChange}
923
+ value={field.value}
924
+ className="flex flex-col space-y-2"
925
+ >
926
+ <div className="flex items-center space-x-3">
927
+ <RadioGroupItem value="qr_only" id="qr_only" />
928
+ <Label htmlFor="qr_only" className="cursor-pointer font-normal">
929
+ <span className="font-medium">{translate("QR code only")}</span>
930
+ <span className="block text-muted-foreground text-xs">
931
+ {translate("Shows compact QR code inline with invoice content")}
932
+ </span>
933
+ </Label>
934
+ </div>
935
+ <div className="flex items-center space-x-3">
936
+ <RadioGroupItem value="full_slip" id="full_slip" />
937
+ <Label htmlFor="full_slip" className="cursor-pointer font-normal">
938
+ <span className="font-medium">{translate("Full UPN payment slip")}</span>
939
+ <span className="block text-muted-foreground text-xs">
940
+ {translate("Shows complete payment slip at bottom of page")}
941
+ </span>
942
+ </Label>
943
+ </div>
944
+ </RadioGroup>
945
+ </FormControl>
946
+ <FormMessage />
947
+ </FormItem>
948
+ )}
949
+ />
950
+
951
+ <FormField
952
+ control={form.control}
953
+ name="upn_qr_purpose_code"
954
+ render={({ field }) => (
955
+ <FormItem className="max-w-xs">
956
+ <FormLabel className="font-medium text-sm">{translate("Purpose Code")}</FormLabel>
957
+ <Select onValueChange={field.onChange} value={field.value || "OTHR"}>
958
+ <FormControl>
959
+ <SelectTrigger className="h-10">
960
+ <SelectValue />
961
+ </SelectTrigger>
962
+ </FormControl>
963
+ <SelectContent>
964
+ <SelectItem value="OTHR">{translate("OTHR - Other")}</SelectItem>
965
+ <SelectItem value="GDSV">{translate("GDSV - Goods and Services")}</SelectItem>
966
+ <SelectItem value="SUPP">{translate("SUPP - Supplier Payment")}</SelectItem>
967
+ </SelectContent>
968
+ </Select>
969
+ <FormDescription className="text-xs">
970
+ {translate("Payment purpose code (ISO 20022)")}
971
+ </FormDescription>
972
+ <FormMessage />
973
+ </FormItem>
974
+ )}
975
+ />
976
+ </div>
977
+ )}
978
+ </div>
979
+ </div>
980
+ </div>
981
+
982
+ {/* Help Content */}
983
+ <div className="hidden lg:block">
984
+ <div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
985
+ <div className="flex items-start gap-2">
986
+ <CreditCard className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
987
+ <div className="space-y-2">
988
+ <p className="font-medium text-muted-foreground text-sm">{translate("UPN QR Payments")}</p>
989
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
990
+ {translate(
991
+ "UPN QR is a Slovenian standard for payment slips. When enabled, your invoices will include a QR code that customers can scan with their mobile banking app to pay instantly.",
992
+ )}
993
+ </p>
994
+ </div>
995
+ </div>
996
+ </div>
997
+ </div>
998
+ </div>
999
+ )}
1000
+
1001
+ {/* Document Defaults Section */}
1002
+ <div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
1003
+ <div className="space-y-4">
1004
+ <div className="flex items-center gap-3">
1005
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/10">
1006
+ <FileText className="h-5 w-5 text-amber-600 dark:text-amber-400" />
1007
+ </div>
1008
+ <div>
1009
+ <h3 className="font-semibold text-lg">{translate("Document Defaults")}</h3>
1010
+ <p className="text-muted-foreground text-sm">{translate("Default values for new documents")}</p>
1011
+ </div>
1012
+ </div>
1013
+
1014
+ <div className="space-y-6 pl-[52px]">
1015
+ <FormField
1016
+ control={form.control}
1017
+ name="default_invoice_note"
1018
+ render={({ field }) => (
1019
+ <FormItem>
1020
+ <div className="flex items-center justify-between">
1021
+ <FormLabel className="font-medium text-sm">{translate("Default Invoice Note")}</FormLabel>
1022
+ <SmartCodeInsertButton
1023
+ textareaRef={defaultInvoiceNoteRef}
1024
+ value={field.value || ""}
1025
+ onInsert={(newValue) => field.onChange(newValue)}
1026
+ t={translate}
1027
+ />
1028
+ </div>
1029
+ <FormControl>
1030
+ <InputWithPreview
1031
+ ref={defaultInvoiceNoteRef}
1032
+ value={field.value || ""}
1033
+ onChange={field.onChange}
1034
+ placeholder={translate(
1035
+ "Payment due by {document_due_date}. Please reference invoice {document_number}.",
1036
+ )}
1037
+ entity={entity}
1038
+ multiline
1039
+ rows={3}
1040
+ className="resize-y"
1041
+ />
1042
+ </FormControl>
1043
+ <FormDescription className="text-xs">
1044
+ {translate("This note will be pre-filled when creating new invoices")}
1045
+ </FormDescription>
1046
+ <FormMessage />
1047
+ </FormItem>
1048
+ )}
1049
+ />
1050
+ </div>
1051
+ </div>
1052
+
1053
+ {/* Help Content */}
1054
+ <div className="hidden lg:block">
1055
+ <div className="sticky top-6 space-y-6 border-muted border-l-2 pl-4">
1056
+ <div className="flex items-start gap-2">
1057
+ <FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
1058
+ <div className="space-y-2">
1059
+ <p className="font-medium text-muted-foreground text-sm">{translate("Invoice Notes")}</p>
1060
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
1061
+ {translate(
1062
+ "Set a default note that will appear on all new invoices. Use template variables to personalize the note automatically.",
1063
+ )}
1064
+ </p>
1065
+ </div>
1066
+ </div>
1067
+ <div className="space-y-3">
1068
+ <div className="flex items-start gap-2">
1069
+ <Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
1070
+ <div className="space-y-2">
1071
+ <p className="font-medium text-muted-foreground text-sm">{translate("Smart Template Variables")}</p>
1072
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
1073
+ {translate("Use variables to personalize your notes automatically")}
1074
+ </p>
1075
+ </div>
1076
+ </div>
1077
+ <EmailTemplateVariablesInfo translate={translate} />
1078
+ </div>
1079
+ </div>
1080
+ </div>
1081
+ </div>
1082
+
1083
+ {/* Email Settings Section */}
1084
+ <div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
1085
+ <div className="space-y-4">
1086
+ <div className="flex items-center gap-3">
1087
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10">
1088
+ <Mail className="h-5 w-5 text-purple-600 dark:text-purple-400" />
1089
+ </div>
1090
+ <div>
1091
+ <h3 className="font-semibold text-lg">{translate("Email Settings")}</h3>
1092
+ <p className="text-muted-foreground text-sm">{translate("Configure email settings for invoices")}</p>
1093
+ </div>
1094
+ </div>
1095
+
1096
+ <div className="space-y-6 pl-[52px]">
1097
+ <FormField
1098
+ control={form.control}
1099
+ name="email"
1100
+ render={({ field }) => (
1101
+ <FormItem>
1102
+ <FormLabel className="font-medium text-sm">{translate("Email Address")}</FormLabel>
1103
+ <FormControl>
1104
+ <div className="relative">
1105
+ <Mail className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
1106
+ <Input
1107
+ type="email"
1108
+ {...field}
1109
+ value={field.value || ""}
1110
+ onChange={(e) => field.onChange(e.target.value)}
1111
+ placeholder="invoices@example.com"
1112
+ className="h-10 pl-10"
1113
+ />
1114
+ </div>
1115
+ </FormControl>
1116
+ <FormDescription className="text-xs">
1117
+ {translate("Email address to send invoices to")}
1118
+ </FormDescription>
1119
+ <FormMessage />
1120
+ </FormItem>
1121
+ )}
1122
+ />
1123
+
1124
+ <div className="border-t pt-6">
1125
+ <div className="mb-4 flex items-center gap-2">
1126
+ <Sparkles className="h-4 w-4 text-muted-foreground" />
1127
+ <p className="font-medium text-muted-foreground text-xs">Default Email Templates</p>
1128
+ </div>
1129
+
1130
+ <Tabs defaultValue="invoice" className="w-full">
1131
+ <TabsList className="w-full">
1132
+ <TabsTrigger value="invoice" className="cursor-pointer">
1133
+ {translate("Invoice")}
1134
+ </TabsTrigger>
1135
+ <TabsTrigger value="estimate" className="cursor-pointer">
1136
+ {translate("Estimate")}
1137
+ </TabsTrigger>
1138
+ </TabsList>
1139
+
1140
+ <TabsContent value="invoice" className="mt-4 space-y-4">
1141
+ <FormField
1142
+ control={form.control}
1143
+ name="invoice_email_subject"
1144
+ render={({ field }) => (
1145
+ <FormItem>
1146
+ <div className="flex items-center justify-between">
1147
+ <FormLabel className="font-medium text-sm">{translate("Email Subject")}</FormLabel>
1148
+ <SmartCodeInsertButton
1149
+ textareaRef={invoiceEmailSubjectRef as React.RefObject<HTMLTextAreaElement | null>}
1150
+ value={field.value || ""}
1151
+ onInsert={(newValue) => field.onChange(newValue)}
1152
+ t={translate}
1153
+ />
1154
+ </div>
1155
+ <FormControl>
1156
+ <InputWithPreview
1157
+ ref={invoiceEmailSubjectRef}
1158
+ value={field.value || ""}
1159
+ onChange={field.onChange}
1160
+ placeholder="Invoice {document_number} from {entity_name}"
1161
+ entity={entity}
1162
+ className="h-10"
1163
+ />
1164
+ </FormControl>
1165
+ <FormDescription className="text-xs">
1166
+ {translate("Subject line for invoice emails")}
1167
+ </FormDescription>
1168
+ <FormMessage />
1169
+ </FormItem>
1170
+ )}
1171
+ />
1172
+
1173
+ <FormField
1174
+ control={form.control}
1175
+ name="invoice_email_body"
1176
+ render={({ field }) => (
1177
+ <FormItem>
1178
+ <div className="flex items-center justify-between">
1179
+ <FormLabel className="font-medium text-sm">{translate("Email Body")}</FormLabel>
1180
+ <SmartCodeInsertButton
1181
+ textareaRef={invoiceEmailBodyRef}
1182
+ value={field.value || ""}
1183
+ onInsert={(newValue) => field.onChange(newValue)}
1184
+ t={translate}
1185
+ />
1186
+ </div>
1187
+ <FormControl>
1188
+ <InputWithPreview
1189
+ ref={invoiceEmailBodyRef}
1190
+ value={field.value || ""}
1191
+ onChange={field.onChange}
1192
+ placeholder="Please find your invoice attached."
1193
+ entity={entity}
1194
+ multiline
1195
+ className="min-h-[200px] resize-none"
1196
+ rows={8}
1197
+ />
1198
+ </FormControl>
1199
+ <FormDescription className="text-xs">
1200
+ {translate("Body content for invoice emails")}
1201
+ </FormDescription>
1202
+ <FormMessage />
1203
+ </FormItem>
1204
+ )}
1205
+ />
1206
+ </TabsContent>
1207
+
1208
+ <TabsContent value="estimate" className="mt-4 space-y-4">
1209
+ <FormField
1210
+ control={form.control}
1211
+ name="estimate_email_subject"
1212
+ render={({ field }) => (
1213
+ <FormItem>
1214
+ <div className="flex items-center justify-between">
1215
+ <FormLabel className="font-medium text-sm">{translate("Email Subject")}</FormLabel>
1216
+ <SmartCodeInsertButton
1217
+ textareaRef={estimateEmailSubjectRef as React.RefObject<HTMLTextAreaElement | null>}
1218
+ value={field.value || ""}
1219
+ onInsert={(newValue) => field.onChange(newValue)}
1220
+ t={translate}
1221
+ />
1222
+ </div>
1223
+ <FormControl>
1224
+ <InputWithPreview
1225
+ ref={estimateEmailSubjectRef}
1226
+ value={field.value || ""}
1227
+ onChange={field.onChange}
1228
+ placeholder="Estimate {document_number} from {entity_name}"
1229
+ entity={entity}
1230
+ className="h-10"
1231
+ />
1232
+ </FormControl>
1233
+ <FormDescription className="text-xs">
1234
+ {translate("Subject line for estimate emails")}
1235
+ </FormDescription>
1236
+ <FormMessage />
1237
+ </FormItem>
1238
+ )}
1239
+ />
1240
+
1241
+ <FormField
1242
+ control={form.control}
1243
+ name="estimate_email_body"
1244
+ render={({ field }) => (
1245
+ <FormItem>
1246
+ <div className="flex items-center justify-between">
1247
+ <FormLabel className="font-medium text-sm">{translate("Email Body")}</FormLabel>
1248
+ <SmartCodeInsertButton
1249
+ textareaRef={estimateEmailBodyRef}
1250
+ value={field.value || ""}
1251
+ onInsert={(newValue) => field.onChange(newValue)}
1252
+ t={translate}
1253
+ />
1254
+ </div>
1255
+ <FormControl>
1256
+ <InputWithPreview
1257
+ ref={estimateEmailBodyRef}
1258
+ value={field.value || ""}
1259
+ onChange={field.onChange}
1260
+ placeholder="Please find your estimate attached."
1261
+ entity={entity}
1262
+ multiline
1263
+ className="min-h-[200px] resize-none"
1264
+ rows={8}
1265
+ />
1266
+ </FormControl>
1267
+ <FormDescription className="text-xs">
1268
+ {translate("Body content for estimate emails")}
1269
+ </FormDescription>
1270
+ <FormMessage />
1271
+ </FormItem>
1272
+ )}
1273
+ />
1274
+ </TabsContent>
1275
+ </Tabs>
1276
+ </div>
1277
+ </div>
1278
+ </div>
1279
+
1280
+ {/* Help Content */}
1281
+ <div className="hidden lg:block">
1282
+ <div className="sticky top-6 space-y-6 border-muted border-l-2 pl-4">
1283
+ <div className="space-y-3">
1284
+ <div className="flex items-start gap-2">
1285
+ <Mail className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
1286
+ <div className="space-y-2">
1287
+ <p className="font-medium text-muted-foreground text-sm">{translate("Email Defaults")}</p>
1288
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
1289
+ {translate(
1290
+ "Configure default email templates for sending invoices and estimates to your customers. Customize the subject line and body for each document type.",
1291
+ )}
1292
+ </p>
1293
+ </div>
1294
+ </div>
1295
+ </div>
1296
+ <div className="space-y-3">
1297
+ <div className="flex items-start gap-2">
1298
+ <Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
1299
+ <div className="space-y-2">
1300
+ <p className="font-medium text-muted-foreground text-sm">{translate("Smart Template Variables")}</p>
1301
+ <p className="text-muted-foreground/80 text-xs leading-relaxed">
1302
+ {translate("Use variables to personalize your emails automatically")}
1303
+ </p>
1304
+ </div>
1305
+ </div>
1306
+ <EmailTemplateVariablesInfo translate={translate} />
1307
+ </div>
1308
+ </div>
1309
+ </div>
1310
+ </div>
1311
+
1312
+ <div className="flex items-center justify-end gap-3 pt-4">
1313
+ <Button type="submit" className="cursor-pointer px-8" disabled={isPending} aria-busy={isPending} size="lg">
1314
+ {isPending ? <ButtonLoader /> : translate("Save Settings")}
1315
+ </Button>
1316
+ </div>
1317
+
1318
+ {form.formState.errors.root && (
1319
+ <div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3">
1320
+ <p className="font-medium text-destructive text-sm">{form.formState.errors.root.message}</p>
1321
+ </div>
1322
+ )}
1323
+ </form>
1324
+ </Form>
1325
+ );
1326
+ }