shutro-erp 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (412) hide show
  1. package/dist/account_portal/.kosh/bindings.js +1 -1
  2. package/dist/account_portal/.kosh/schema.d.ts +181 -3
  3. package/dist/account_portal/module.d.ts +359 -6
  4. package/dist/accounting/.kosh/bindings.js +1 -1
  5. package/dist/accounting/.kosh/schema.d.ts +181 -3
  6. package/dist/accounting/index.js +214 -2
  7. package/dist/accounting/module.d.ts +217 -5
  8. package/dist/accounting/module.js +23 -2
  9. package/dist/calendar/.kosh/bindings.d.ts +22 -0
  10. package/dist/calendar/.kosh/bindings.js +15 -0
  11. package/dist/calendar/.kosh/schema.d.ts +795 -0
  12. package/dist/calendar/demo.d.ts +6 -0
  13. package/dist/calendar/demo.js +24 -0
  14. package/dist/calendar/index.d.ts +3 -0
  15. package/dist/calendar/index.js +291 -0
  16. package/dist/calendar/module.d.ts +1705 -0
  17. package/dist/calendar/module.js +180 -0
  18. package/dist/cli/init.js +48 -0
  19. package/dist/cli/templates.d.ts +1 -1
  20. package/dist/cli/templates.js +137 -0
  21. package/dist/contacts/.kosh/bindings.js +1 -1
  22. package/dist/contacts/.kosh/schema.d.ts +205 -3
  23. package/dist/contacts/demo.js +30 -1
  24. package/dist/contacts/index.d.ts +1 -1
  25. package/dist/contacts/index.js +155 -9
  26. package/dist/contacts/module.d.ts +338 -2
  27. package/dist/contacts/module.js +82 -5
  28. package/dist/crm/.kosh/bindings.d.ts +22 -0
  29. package/dist/crm/.kosh/bindings.js +15 -0
  30. package/dist/crm/.kosh/schema.d.ts +799 -0
  31. package/dist/crm/demo.d.ts +6 -0
  32. package/dist/crm/demo.js +45 -0
  33. package/dist/crm/index.d.ts +3 -0
  34. package/dist/crm/index.js +361 -0
  35. package/dist/crm/module.d.ts +1843 -0
  36. package/dist/crm/module.js +207 -0
  37. package/dist/event/.kosh/bindings.d.ts +22 -0
  38. package/dist/event/.kosh/bindings.js +15 -0
  39. package/dist/event/.kosh/schema.d.ts +843 -0
  40. package/dist/event/demo.d.ts +6 -0
  41. package/dist/event/demo.js +65 -0
  42. package/dist/event/index.d.ts +14 -0
  43. package/dist/event/index.js +333 -0
  44. package/dist/event/mails.d.ts +15 -0
  45. package/dist/event/mails.js +112 -0
  46. package/dist/event/module.d.ts +2800 -0
  47. package/dist/event/module.js +323 -0
  48. package/dist/event/web/CheckInBox.d.ts +9 -0
  49. package/dist/event/web/CheckInBox.js +87 -0
  50. package/dist/event/web/index.d.ts +11 -0
  51. package/dist/event/web/index.js +16 -0
  52. package/dist/event/web/messages.d.ts +2 -0
  53. package/dist/event/web/messages.js +23 -0
  54. package/dist/event_account/.kosh/bindings.d.ts +22 -0
  55. package/dist/event_account/.kosh/bindings.js +15 -0
  56. package/dist/event_account/.kosh/schema.d.ts +1639 -0
  57. package/dist/event_account/index.d.ts +3 -0
  58. package/dist/event_account/index.js +28 -0
  59. package/dist/event_account/module.d.ts +6968 -0
  60. package/dist/event_account/module.js +90 -0
  61. package/dist/hr/.kosh/bindings.d.ts +22 -0
  62. package/dist/hr/.kosh/bindings.js +15 -0
  63. package/dist/hr/.kosh/schema.d.ts +878 -0
  64. package/dist/hr/demo.d.ts +6 -0
  65. package/dist/hr/demo.js +60 -0
  66. package/dist/hr/index.d.ts +3 -0
  67. package/dist/hr/index.js +483 -0
  68. package/dist/hr/module.d.ts +2611 -0
  69. package/dist/hr/module.js +350 -0
  70. package/dist/integrations/index.js +12 -0
  71. package/dist/integrations/web/OAuthAppsPage.d.ts +9 -0
  72. package/dist/integrations/web/OAuthAppsPage.js +79 -0
  73. package/dist/integrations/web/index.d.ts +1 -0
  74. package/dist/integrations/web/index.js +3 -0
  75. package/dist/integrations/web/messages.js +58 -0
  76. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  77. package/dist/l10n_bd/.kosh/schema.d.ts +181 -3
  78. package/dist/l10n_bd/module.d.ts +359 -6
  79. package/dist/l10n_in/.kosh/bindings.js +1 -1
  80. package/dist/l10n_in/.kosh/schema.d.ts +181 -3
  81. package/dist/l10n_in/module.d.ts +380 -7
  82. package/dist/maintenance/.kosh/bindings.d.ts +22 -0
  83. package/dist/maintenance/.kosh/bindings.js +15 -0
  84. package/dist/maintenance/.kosh/schema.d.ts +911 -0
  85. package/dist/maintenance/demo.d.ts +6 -0
  86. package/dist/maintenance/demo.js +60 -0
  87. package/dist/maintenance/index.d.ts +3 -0
  88. package/dist/maintenance/index.js +422 -0
  89. package/dist/maintenance/module.d.ts +3545 -0
  90. package/dist/maintenance/module.js +446 -0
  91. package/dist/maintenance_stock/.kosh/bindings.d.ts +22 -0
  92. package/dist/maintenance_stock/.kosh/bindings.js +15 -0
  93. package/dist/maintenance_stock/.kosh/schema.d.ts +1371 -0
  94. package/dist/maintenance_stock/index.d.ts +9 -0
  95. package/dist/maintenance_stock/index.js +52 -0
  96. package/dist/maintenance_stock/module.d.ts +6953 -0
  97. package/dist/maintenance_stock/module.js +192 -0
  98. package/dist/messaging/web/index.js +1 -1
  99. package/dist/modules-sdk/index.d.ts +1 -1
  100. package/dist/modules-sdk/manifest.d.ts +70 -0
  101. package/dist/modules-sdk/manifest.js +35 -1
  102. package/dist/mrp/.kosh/bindings.d.ts +22 -0
  103. package/dist/mrp/.kosh/bindings.js +15 -0
  104. package/dist/mrp/.kosh/schema.d.ts +1378 -0
  105. package/dist/mrp/demo.d.ts +7 -0
  106. package/dist/mrp/demo.js +42 -0
  107. package/dist/mrp/index.d.ts +3 -0
  108. package/dist/mrp/index.js +392 -0
  109. package/dist/mrp/module.d.ts +8608 -0
  110. package/dist/mrp/module.js +695 -0
  111. package/dist/mrp_account/.kosh/bindings.d.ts +22 -0
  112. package/dist/mrp_account/.kosh/bindings.js +15 -0
  113. package/dist/mrp_account/.kosh/schema.d.ts +2106 -0
  114. package/dist/mrp_account/index.d.ts +3 -0
  115. package/dist/mrp_account/index.js +22 -0
  116. package/dist/mrp_account/module.d.ts +20840 -0
  117. package/dist/mrp_account/module.js +123 -0
  118. package/dist/notify/web/index.js +1 -1
  119. package/dist/payment/.kosh/bindings.js +1 -1
  120. package/dist/payment/.kosh/schema.d.ts +181 -3
  121. package/dist/payment/module.d.ts +359 -6
  122. package/dist/portal/.kosh/bindings.js +1 -1
  123. package/dist/portal/.kosh/schema.d.ts +205 -3
  124. package/dist/portal/module.d.ts +166 -2
  125. package/dist/pos/.kosh/bindings.d.ts +22 -0
  126. package/dist/pos/.kosh/bindings.js +15 -0
  127. package/dist/pos/.kosh/schema.d.ts +1690 -0
  128. package/dist/pos/accounting.d.ts +10 -0
  129. package/dist/pos/accounting.js +149 -0
  130. package/dist/pos/demo.d.ts +6 -0
  131. package/dist/pos/demo.js +90 -0
  132. package/dist/pos/index.d.ts +9 -0
  133. package/dist/pos/index.js +349 -0
  134. package/dist/pos/module.d.ts +9636 -0
  135. package/dist/pos/module.js +423 -0
  136. package/dist/pos/routes.d.ts +39 -0
  137. package/dist/pos/routes.js +368 -0
  138. package/dist/pos/web/Register.d.ts +9 -0
  139. package/dist/pos/web/Register.js +252 -0
  140. package/dist/pos/web/index.d.ts +11 -0
  141. package/dist/pos/web/index.js +16 -0
  142. package/dist/pos/web/messages.d.ts +2 -0
  143. package/dist/pos/web/messages.js +87 -0
  144. package/dist/pos_stock/.kosh/bindings.d.ts +22 -0
  145. package/dist/pos_stock/.kosh/bindings.js +15 -0
  146. package/dist/pos_stock/.kosh/schema.d.ts +2015 -0
  147. package/dist/pos_stock/index.d.ts +9 -0
  148. package/dist/pos_stock/index.js +35 -0
  149. package/dist/pos_stock/module.d.ts +11935 -0
  150. package/dist/pos_stock/module.js +115 -0
  151. package/dist/product/.kosh/bindings.js +1 -1
  152. package/dist/product/.kosh/schema.d.ts +178 -1
  153. package/dist/product/index.js +81 -4
  154. package/dist/product/module.d.ts +43 -1
  155. package/dist/product/module.js +9 -2
  156. package/dist/project/.kosh/bindings.js +1 -1
  157. package/dist/project/.kosh/schema.d.ts +181 -3
  158. package/dist/project/index.js +147 -7
  159. package/dist/project/module.d.ts +210 -2
  160. package/dist/project/module.js +52 -5
  161. package/dist/project_portal/.kosh/bindings.js +1 -1
  162. package/dist/project_portal/.kosh/schema.d.ts +181 -3
  163. package/dist/project_portal/module.d.ts +353 -4
  164. package/dist/purchase/.kosh/bindings.js +1 -1
  165. package/dist/purchase/.kosh/schema.d.ts +181 -3
  166. package/dist/purchase/index.js +163 -3
  167. package/dist/purchase/module.d.ts +215 -3
  168. package/dist/purchase/module.js +21 -3
  169. package/dist/purchase_account/.kosh/bindings.js +1 -1
  170. package/dist/purchase_account/.kosh/schema.d.ts +182 -3
  171. package/dist/purchase_account/index.js +26 -1
  172. package/dist/purchase_account/module.d.ts +393 -7
  173. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  174. package/dist/purchase_portal/.kosh/schema.d.ts +181 -3
  175. package/dist/purchase_portal/module.d.ts +366 -5
  176. package/dist/purchase_stock/.kosh/bindings.d.ts +22 -0
  177. package/dist/purchase_stock/.kosh/bindings.js +15 -0
  178. package/dist/purchase_stock/.kosh/schema.d.ts +1310 -0
  179. package/dist/purchase_stock/index.d.ts +7 -0
  180. package/dist/purchase_stock/index.js +91 -0
  181. package/dist/purchase_stock/module.d.ts +6763 -0
  182. package/dist/purchase_stock/module.js +144 -0
  183. package/dist/purchase_stock_account/.kosh/bindings.d.ts +22 -0
  184. package/dist/purchase_stock_account/.kosh/bindings.js +15 -0
  185. package/dist/purchase_stock_account/.kosh/schema.d.ts +1959 -0
  186. package/dist/purchase_stock_account/index.d.ts +3 -0
  187. package/dist/purchase_stock_account/index.js +24 -0
  188. package/dist/purchase_stock_account/module.d.ts +35366 -0
  189. package/dist/purchase_stock_account/module.js +162 -0
  190. package/dist/recruitment/.kosh/bindings.d.ts +22 -0
  191. package/dist/recruitment/.kosh/bindings.js +15 -0
  192. package/dist/recruitment/.kosh/schema.d.ts +990 -0
  193. package/dist/recruitment/demo.d.ts +6 -0
  194. package/dist/recruitment/demo.js +45 -0
  195. package/dist/recruitment/index.d.ts +3 -0
  196. package/dist/recruitment/index.js +385 -0
  197. package/dist/recruitment/module.d.ts +3247 -0
  198. package/dist/recruitment/module.js +193 -0
  199. package/dist/recruitment/web/HireButton.d.ts +9 -0
  200. package/dist/recruitment/web/HireButton.js +32 -0
  201. package/dist/recruitment/web/index.d.ts +11 -0
  202. package/dist/recruitment/web/index.js +16 -0
  203. package/dist/recruitment/web/messages.d.ts +2 -0
  204. package/dist/recruitment/web/messages.js +19 -0
  205. package/dist/reporting/documents.d.ts +5 -0
  206. package/dist/reporting/documents.js +28 -1
  207. package/dist/reporting/index.d.ts +1 -1
  208. package/dist/reporting/index.js +4 -2
  209. package/dist/reporting/models.js +9 -0
  210. package/dist/reporting/overviews.d.ts +16 -0
  211. package/dist/reporting/overviews.js +56 -0
  212. package/dist/reporting/routes-documents.js +1 -1
  213. package/dist/reporting/routes.js +138 -80
  214. package/dist/reporting/web/DashboardView.d.ts +11 -0
  215. package/dist/reporting/web/DashboardView.js +2 -2
  216. package/dist/reporting/web/DesignerView.js +5 -0
  217. package/dist/reporting/web/Overview.d.ts +9 -0
  218. package/dist/reporting/web/Overview.js +53 -0
  219. package/dist/reporting/web/index.d.ts +1 -0
  220. package/dist/reporting/web/index.js +4 -1
  221. package/dist/reporting/web/messages.js +9 -0
  222. package/dist/sale_account/.kosh/bindings.js +1 -1
  223. package/dist/sale_account/.kosh/schema.d.ts +186 -3
  224. package/dist/sale_account/index.js +22 -1
  225. package/dist/sale_account/module.d.ts +461 -7
  226. package/dist/sale_portal/.kosh/bindings.js +1 -1
  227. package/dist/sale_portal/.kosh/schema.d.ts +185 -3
  228. package/dist/sale_portal/module.d.ts +434 -5
  229. package/dist/sale_project/.kosh/bindings.js +1 -1
  230. package/dist/sale_project/.kosh/schema.d.ts +187 -3
  231. package/dist/sale_project/module.d.ts +476 -6
  232. package/dist/sale_stock/.kosh/bindings.d.ts +22 -0
  233. package/dist/sale_stock/.kosh/bindings.js +15 -0
  234. package/dist/sale_stock/.kosh/schema.d.ts +1228 -0
  235. package/dist/sale_stock/index.d.ts +7 -0
  236. package/dist/sale_stock/index.js +84 -0
  237. package/dist/sale_stock/module.d.ts +6791 -0
  238. package/dist/sale_stock/module.js +146 -0
  239. package/dist/sales/.kosh/bindings.js +1 -1
  240. package/dist/sales/.kosh/schema.d.ts +185 -3
  241. package/dist/sales/index.js +191 -4
  242. package/dist/sales/module.d.ts +355 -3
  243. package/dist/sales/module.js +78 -3
  244. package/dist/server/api/metadata.d.ts +5 -0
  245. package/dist/server/api/metadata.js +5 -0
  246. package/dist/server/api/rest.js +64 -1
  247. package/dist/server/api/views.js +1 -0
  248. package/dist/server/app-settings.d.ts +7 -0
  249. package/dist/server/app-settings.js +10 -0
  250. package/dist/server/auth/base.js +85 -0
  251. package/dist/server/auth/oauth.d.ts +21 -0
  252. package/dist/server/auth/oauth.js +579 -0
  253. package/dist/server/auth/service.d.ts +4 -0
  254. package/dist/server/auth/service.js +17 -0
  255. package/dist/server/identity.d.ts +2 -0
  256. package/dist/server/images.d.ts +12 -0
  257. package/dist/server/images.js +139 -0
  258. package/dist/server/index.d.ts +2 -0
  259. package/dist/server/index.js +2 -0
  260. package/dist/server/server.d.ts +4 -0
  261. package/dist/server/server.js +9 -1
  262. package/dist/settings/app-settings.d.ts +2 -0
  263. package/dist/settings/app-settings.js +518 -0
  264. package/dist/settings/company.js +3 -1
  265. package/dist/settings/index.d.ts +1 -0
  266. package/dist/settings/index.js +15 -0
  267. package/dist/settings/people.js +32 -6
  268. package/dist/settings/web/AppSettingsPage.d.ts +11 -0
  269. package/dist/settings/web/AppSettingsPage.js +210 -0
  270. package/dist/settings/web/CompaniesPage.js +2 -2
  271. package/dist/settings/web/CompanyPage.js +13 -2
  272. package/dist/settings/web/ConnectedAppsPage.d.ts +7 -0
  273. package/dist/settings/web/ConnectedAppsPage.js +33 -0
  274. package/dist/settings/web/OverviewPage.js +28 -4
  275. package/dist/settings/web/PeoplePage.js +23 -18
  276. package/dist/settings/web/ProfilePage.js +9 -7
  277. package/dist/settings/web/index.d.ts +2 -0
  278. package/dist/settings/web/index.js +8 -0
  279. package/dist/settings/web/messages.js +146 -2
  280. package/dist/stock/.kosh/bindings.d.ts +22 -0
  281. package/dist/stock/.kosh/bindings.js +15 -0
  282. package/dist/stock/.kosh/schema.d.ts +1147 -0
  283. package/dist/stock/demo.d.ts +7 -0
  284. package/dist/stock/demo.js +55 -0
  285. package/dist/stock/index.d.ts +3 -0
  286. package/dist/stock/index.js +674 -0
  287. package/dist/stock/module.d.ts +6330 -0
  288. package/dist/stock/module.js +1325 -0
  289. package/dist/stock/web/ScanBox.d.ts +8 -0
  290. package/dist/stock/web/ScanBox.js +90 -0
  291. package/dist/stock/web/index.d.ts +11 -0
  292. package/dist/stock/web/index.js +16 -0
  293. package/dist/stock/web/messages.d.ts +2 -0
  294. package/dist/stock/web/messages.js +25 -0
  295. package/dist/stock_account/.kosh/bindings.d.ts +22 -0
  296. package/dist/stock_account/.kosh/bindings.js +15 -0
  297. package/dist/stock_account/.kosh/schema.d.ts +1875 -0
  298. package/dist/stock_account/index.d.ts +3 -0
  299. package/dist/stock_account/index.js +130 -0
  300. package/dist/stock_account/module.d.ts +10655 -0
  301. package/dist/stock_account/module.js +339 -0
  302. package/dist/storage/attachments.d.ts +5 -0
  303. package/dist/storage/attachments.js +12 -1
  304. package/dist/storage/models.js +3 -0
  305. package/dist/storage/routes.js +13 -2
  306. package/dist/todo/.kosh/bindings.d.ts +22 -0
  307. package/dist/todo/.kosh/bindings.js +15 -0
  308. package/dist/todo/.kosh/schema.d.ts +732 -0
  309. package/dist/todo/demo.d.ts +6 -0
  310. package/dist/todo/demo.js +35 -0
  311. package/dist/todo/index.d.ts +3 -0
  312. package/dist/todo/index.js +187 -0
  313. package/dist/todo/module.d.ts +1180 -0
  314. package/dist/todo/module.js +130 -0
  315. package/dist/trade_finance/.kosh/bindings.d.ts +22 -0
  316. package/dist/trade_finance/.kosh/bindings.js +15 -0
  317. package/dist/trade_finance/.kosh/schema.d.ts +884 -0
  318. package/dist/trade_finance/demo.d.ts +6 -0
  319. package/dist/trade_finance/demo.js +63 -0
  320. package/dist/trade_finance/index.d.ts +3 -0
  321. package/dist/trade_finance/index.js +413 -0
  322. package/dist/trade_finance/module.d.ts +3779 -0
  323. package/dist/trade_finance/module.js +505 -0
  324. package/dist/trade_finance_account/.kosh/bindings.d.ts +22 -0
  325. package/dist/trade_finance_account/.kosh/bindings.js +15 -0
  326. package/dist/trade_finance_account/.kosh/schema.d.ts +1711 -0
  327. package/dist/trade_finance_account/index.d.ts +3 -0
  328. package/dist/trade_finance_account/index.js +92 -0
  329. package/dist/trade_finance_account/module.d.ts +7928 -0
  330. package/dist/trade_finance_account/module.js +244 -0
  331. package/dist/trade_finance_purchase/.kosh/bindings.d.ts +22 -0
  332. package/dist/trade_finance_purchase/.kosh/bindings.js +15 -0
  333. package/dist/trade_finance_purchase/.kosh/schema.d.ts +1153 -0
  334. package/dist/trade_finance_purchase/index.d.ts +8 -0
  335. package/dist/trade_finance_purchase/index.js +50 -0
  336. package/dist/trade_finance_purchase/module.d.ts +4252 -0
  337. package/dist/trade_finance_purchase/module.js +44 -0
  338. package/dist/trade_finance_sale/.kosh/bindings.d.ts +22 -0
  339. package/dist/trade_finance_sale/.kosh/bindings.js +15 -0
  340. package/dist/trade_finance_sale/.kosh/schema.d.ts +1071 -0
  341. package/dist/trade_finance_sale/index.d.ts +8 -0
  342. package/dist/trade_finance_sale/index.js +50 -0
  343. package/dist/trade_finance_sale/module.d.ts +4280 -0
  344. package/dist/trade_finance_sale/module.js +44 -0
  345. package/dist/ui/api.d.ts +6 -1
  346. package/dist/ui/api.js +2 -1
  347. package/dist/ui/app.js +51 -17
  348. package/dist/ui/components/AppIcon.js +33 -1
  349. package/dist/ui/components/Avatar.d.ts +21 -0
  350. package/dist/ui/components/Avatar.js +71 -0
  351. package/dist/ui/components/primitives.d.ts +2 -0
  352. package/dist/ui/components/primitives.js +7 -1
  353. package/dist/ui/i18n/messages.d.ts +53 -0
  354. package/dist/ui/i18n/messages.js +98 -0
  355. package/dist/ui/index.d.ts +2 -0
  356. package/dist/ui/index.js +2 -0
  357. package/dist/ui/offline/queue.d.ts +5 -1
  358. package/dist/ui/offline/queue.js +2 -0
  359. package/dist/ui/registry.d.ts +12 -1
  360. package/dist/ui/registry.js +10 -1
  361. package/dist/ui/search/ViewTabs.d.ts +14 -0
  362. package/dist/ui/search/ViewTabs.js +42 -0
  363. package/dist/ui/shell/Consent.d.ts +5 -0
  364. package/dist/ui/shell/Consent.js +35 -0
  365. package/dist/ui/shell/Home.js +136 -16
  366. package/dist/ui/shell/Sidebar.js +15 -22
  367. package/dist/ui/views/FormView.js +98 -17
  368. package/dist/ui/views/KanbanView.d.ts +2 -0
  369. package/dist/ui/views/KanbanView.js +32 -3
  370. package/dist/ui/views/ListView.js +155 -31
  371. package/dist/ui/views/StatsRow.d.ts +14 -0
  372. package/dist/ui/views/StatsRow.js +88 -0
  373. package/dist/ui/views/widgets.d.ts +2 -1
  374. package/dist/ui/views/widgets.js +194 -1
  375. package/dist/views/builders.d.ts +19 -2
  376. package/dist/views/builders.js +21 -1
  377. package/dist/views/condition.d.ts +6 -0
  378. package/dist/views/condition.js +20 -0
  379. package/dist/views/documents.d.ts +8 -0
  380. package/dist/views/extend.d.ts +2 -2
  381. package/dist/views/extend.js +11 -1
  382. package/dist/views/index.d.ts +2 -2
  383. package/dist/views/index.js +2 -2
  384. package/dist/views/types.d.ts +52 -5
  385. package/dist/views/validate.js +39 -1
  386. package/dist/website/.kosh/bindings.d.ts +22 -0
  387. package/dist/website/.kosh/bindings.js +15 -0
  388. package/dist/website/.kosh/schema.d.ts +881 -0
  389. package/dist/website/demo.d.ts +6 -0
  390. package/dist/website/demo.js +115 -0
  391. package/dist/website/index.d.ts +9 -0
  392. package/dist/website/index.js +250 -0
  393. package/dist/website/module.d.ts +2987 -0
  394. package/dist/website/module.js +281 -0
  395. package/dist/website/render.d.ts +45 -0
  396. package/dist/website/render.js +102 -0
  397. package/dist/website/routes.d.ts +12 -0
  398. package/dist/website/routes.js +246 -0
  399. package/dist/website/web/PagePreview.d.ts +7 -0
  400. package/dist/website/web/PagePreview.js +33 -0
  401. package/dist/website/web/index.d.ts +11 -0
  402. package/dist/website/web/index.js +16 -0
  403. package/dist/website/web/messages.d.ts +2 -0
  404. package/dist/website/web/messages.js +15 -0
  405. package/dist/website_crm/.kosh/bindings.d.ts +22 -0
  406. package/dist/website_crm/.kosh/bindings.js +15 -0
  407. package/dist/website_crm/.kosh/schema.d.ts +966 -0
  408. package/dist/website_crm/index.d.ts +3 -0
  409. package/dist/website_crm/index.js +39 -0
  410. package/dist/website_crm/module.d.ts +2955 -0
  411. package/dist/website_crm/module.js +44 -0
  412. package/package.json +118 -1
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { createHash, randomBytes } from 'node:crypto';
19
19
  import { layout } from '../mail/index.js';
20
- import { audit, HttpProblem, isAdministratorUser } from '../server/index.js';
20
+ import { audit, HttpProblem, imageVersions, isAdministratorUser, } from '../server/index.js';
21
21
  /** How long an invitation works. */
22
22
  export const INVITATION_DAYS = 7;
23
23
  const ref = (env, name) => env.model(name);
@@ -164,6 +164,7 @@ export function registerPeopleRoutes(context, options) {
164
164
  const invitations = await rows(ref(su, 'settings.invitation').search({ where: { user: { in: ids }, usedAt: null } }), ['user', 'expiresAt']);
165
165
  const known = await knownGroups();
166
166
  const admin = adminGroups(su);
167
+ const photos = await imageVersions(su, 'user', ids);
167
168
  // "Purchase · Manager": a group named by the app level that gives it, else by its own label.
168
169
  const levelLabel = new Map();
169
170
  for (const m of modules)
@@ -184,6 +185,7 @@ export function registerPeopleRoutes(context, options) {
184
185
  name: String(u['name'] ?? ''),
185
186
  login: String(u['login'] ?? ''),
186
187
  email: typeof u['email'] === 'string' ? u['email'] : null,
188
+ avatar: photos[String(id)] ?? null,
187
189
  active: u['active'] !== false,
188
190
  state: u['active'] === false
189
191
  ? 'deactivated'
@@ -278,16 +280,29 @@ export function registerPeopleRoutes(context, options) {
278
280
  };
279
281
  });
280
282
  });
283
+ // An invitation (the person chooses their password), or — with `password` — a person added directly: the
284
+ // manager sets the password and passes it on; the login may then be a plain name (`rahim`) and the email optional.
281
285
  app.post('/api/v1/settings/people', async (request, reply) => {
282
286
  const body = (request.body ?? {});
283
287
  const name = String(body['name'] ?? '').trim();
284
- const login = String(body['email'] ?? '')
288
+ const address = String(body['email'] ?? '')
285
289
  .trim()
286
290
  .toLowerCase();
291
+ const password = typeof body['password'] === 'string' ? body['password'] : undefined;
292
+ const direct = password !== undefined;
293
+ const login = direct
294
+ ? String(body['login'] ?? address)
295
+ .trim()
296
+ .toLowerCase()
297
+ : address;
287
298
  if (name === '' || name.length > 200)
288
299
  throw bad('The name is 1–200 characters.');
289
- if (!EMAIL.test(login))
300
+ if (!direct && !EMAIL.test(login))
290
301
  throw bad('The email is their login and where the invitation goes.');
302
+ if (direct && !/^[a-z0-9._@+-]{3,100}$/.test(login))
303
+ throw bad('The login is 3–100 letters, digits, or . _ @ + -.');
304
+ if (direct && address !== '' && !EMAIL.test(address))
305
+ throw bad('The email is not valid.');
291
306
  const made = await guarded(request, async (env, su, manager) => {
292
307
  const assignments = await checkAssignments(su, body['assignments'] ?? [], manager);
293
308
  const taken = await ref(withArchived(su), 'res.users').search({ where: { login }, limit: 1 });
@@ -301,16 +316,27 @@ export function registerPeopleRoutes(context, options) {
301
316
  {
302
317
  login,
303
318
  name,
304
- email: login,
319
+ email: address === '' ? null : address,
305
320
  company: home,
306
321
  companies: { set: companies.length === 0 ? [env.companyId] : companies },
307
322
  },
308
323
  ]);
309
324
  const user = created.ids[0];
310
- // No usable password until the invitation is used.
311
- await auth.setPassword(su, user, randomBytes(32).toString('base64url'));
325
+ // Added directly: the manager's password (checked for length); invited: none usable until the link is used.
326
+ await auth.setPassword(su, user, password ?? randomBytes(32).toString('base64url'));
312
327
  if (assignments.length > 0)
313
328
  await ref(su, 'auth.user_group').create(assignments.map((a) => ({ user, group: a.group, company: a.company })));
329
+ if (direct) {
330
+ await audit(env, {
331
+ kind: 'people.created',
332
+ summary: `${name} (${login}) added with a password and ${assignments.length} role(s).`,
333
+ targetModel: 'res.users',
334
+ targetId: user,
335
+ details: { assignments },
336
+ ip: request.ip,
337
+ });
338
+ return { id: user, login, activationUrl: null, expiresAt: null, emailed: false };
339
+ }
314
340
  const link = await invite(su, user, env.uid);
315
341
  await audit(env, {
316
342
  kind: 'people.invited',
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Settings › App settings (RFC 0020): the apps' own settings, declared in their manifests. The apps you may see on
3
+ * the left; an app's sections as cards — each setting with its help, a mark when it differs from the default with
4
+ * *Reset*, and *Save* per section, sending only what changed. Company settings name the company they apply to. The
5
+ * server checks every value and who may change it, and audits each change. Each setting opens its history (who, when,
6
+ * old → new); administrators copy a company's settings to another, export them as a file and import one, every one
7
+ * of those previewed before anything is written.
8
+ */
9
+ import { type PageProps } from '../../ui/index.js';
10
+ import { type ReactElement } from 'react';
11
+ export declare function AppSettingsPage({ api }: PageProps): ReactElement;
@@ -0,0 +1,210 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Settings › App settings (RFC 0020): the apps' own settings, declared in their manifests. The apps you may see on
4
+ * the left; an app's sections as cards — each setting with its help, a mark when it differs from the default with
5
+ * *Reset*, and *Save* per section, sending only what changed. Company settings name the company they apply to. The
6
+ * server checks every value and who may change it, and audits each change. Each setting opens its history (who, when,
7
+ * old → new); administrators copy a company's settings to another, export them as a file and import one, every one
8
+ * of those previewed before anything is written.
9
+ */
10
+ import { Alert, ApiError, AppIcon, Badge, Button, Checkbox, DateField, Dialog, Input, Label, Select, Textarea, useKosh, } from '../../ui/index.js';
11
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
12
+ import { Copy, Download, History, RotateCcw, Upload } from 'lucide-react';
13
+ import { useEffect, useRef, useState } from 'react';
14
+ import { useTranslation } from 'react-i18next';
15
+ const shown = (value) => value === null || value === ''
16
+ ? '—'
17
+ : value === true
18
+ ? '✓'
19
+ : value === false
20
+ ? '✗'
21
+ : String(value);
22
+ const BASE = '/api/v1/settings/app-settings';
23
+ const message = (error) => (error instanceof ApiError ? error.message : String(error));
24
+ /** The app shown: `?app=` in the address, else the first. */
25
+ function useChosenApp(apps) {
26
+ const [chosen, setChosen] = useState(() => new URLSearchParams(window.location.search).get('app') ?? undefined);
27
+ const set = (app) => {
28
+ setChosen(app);
29
+ const url = new URL(window.location.href);
30
+ url.searchParams.set('app', app);
31
+ window.history.replaceState(window.history.state, '', url);
32
+ };
33
+ return [apps.some((a) => a.app === chosen) ? chosen : apps[0]?.app, set];
34
+ }
35
+ function Control({ setting, value, disabled, onChange, }) {
36
+ const id = `setting-${setting.key}`;
37
+ const { t } = useTranslation('settings');
38
+ switch (setting.type) {
39
+ case 'boolean':
40
+ return (_jsx(Checkbox, { id: id, checked: value === true, disabled: disabled, onCheckedChange: (on) => onChange(on === true) }));
41
+ case 'selection':
42
+ return (_jsx(Select, { id: id, label: setting.label, value: typeof value === 'string' ? value : 'none', disabled: disabled, options: [{ value: 'none', label: t('appSettings.notSet') }, ...(setting.options ?? [])], onChange: (v) => onChange(v === 'none' ? null : v) }));
43
+ case 'text':
44
+ return (_jsx(Textarea, { id: id, rows: 4, disabled: disabled, value: typeof value === 'string' ? value : '', onChange: (e) => onChange(e.target.value) }));
45
+ case 'date':
46
+ return disabled ? (_jsx(Input, { id: id, disabled: true, className: "w-48", value: typeof value === 'string' ? value : '', readOnly: true })) : (_jsx(DateField, { id: id, value: typeof value === 'string' ? value : null, onChange: onChange }));
47
+ default:
48
+ return (_jsx(Input, { id: id, className: setting.type === 'char' ? undefined : 'w-48', disabled: disabled, type: setting.type === 'integer' ? 'number' : 'text', inputMode: setting.type === 'decimal' ? 'decimal' : undefined, min: setting.min ?? undefined, max: setting.max ?? undefined, value: value === null ? '' : String(value), onChange: (e) => {
49
+ const raw = e.target.value;
50
+ onChange(setting.type === 'integer'
51
+ ? raw === ''
52
+ ? null
53
+ : Number.parseInt(raw, 10)
54
+ : raw === ''
55
+ ? null
56
+ : raw);
57
+ } }));
58
+ }
59
+ }
60
+ /** Who changed a setting, when, from what to what — the audit log of this section, newest first. */
61
+ function HistoryDialog({ api, app, section, setting, onClose, }) {
62
+ const { t } = useTranslation('settings');
63
+ const { format } = useKosh();
64
+ const history = useQuery({
65
+ queryKey: ['settings', 'app-settings', app, section.key, 'history', setting?.key],
66
+ queryFn: () => api.call('GET', `${BASE}/${app}/${section.key}/history?key=${encodeURIComponent(setting?.key ?? '')}`),
67
+ enabled: setting !== null,
68
+ });
69
+ return (_jsx(Dialog, { open: setting !== null, onOpenChange: (open) => {
70
+ if (!open)
71
+ onClose();
72
+ }, title: t('appSettings.historyOf', { setting: setting?.label ?? '' }), children: history.error !== null ? (_jsx(Alert, { tone: "danger", children: message(history.error) })) : history.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : history.data.events.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('appSettings.neverChanged') })) : (_jsx("ol", { className: "grid gap-2 text-sm", children: history.data.events.map((e) => (_jsxs("li", { className: "grid gap-0.5 rounded-md border p-2", children: [_jsxs("span", { className: "text-xs text-muted-foreground", children: [format.datetime(e.at), " \u00B7 ", e.actor?.name ?? t('appSettings.system'), " \u00B7", ' ', t(`appSettings.how.${e.how}`)] }), e.changes.map((c) => (_jsxs("span", { className: "tabular-nums", children: [_jsx("s", { className: "text-muted-foreground", children: shown(c.from) }), " \u2192 ", _jsx("b", { children: shown(c.to) })] }, c.key)))] }, e.at))) })) }));
73
+ }
74
+ /** The changes a copy or an import would make, section by section. */
75
+ function ChangesList({ preview }) {
76
+ const { t } = useTranslation('settings');
77
+ return (_jsxs("div", { className: "grid gap-3 text-sm", children: [preview.sections.length === 0 ? (_jsx("p", { className: "text-muted-foreground", children: t('appSettings.nothingToChange') })) : (preview.sections.map((s) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "font-medium", children: [s.appLabel, " \u203A ", s.label] }), _jsx("ul", { className: "grid gap-0.5 ps-4", children: s.changes.map((c) => (_jsxs("li", { className: "tabular-nums", children: [c.label, ": ", _jsx("s", { className: "text-muted-foreground", children: shown(c.from) }), " \u2192", ' ', _jsx("b", { children: shown(c.to) })] }, c.key))) })] }, `${s.app}.${s.section}`)))), (preview.skipped ?? []).length === 0 ? null : (_jsx("p", { className: "text-xs text-muted-foreground", children: t('appSettings.skipped', { what: (preview.skipped ?? []).join(', ') }) }))] }));
78
+ }
79
+ /** Administrators: copy this company's settings to another, export them, import a file — each previewed first. */
80
+ function Tools({ api, onDone }) {
81
+ const { t } = useTranslation('settings');
82
+ const { companyId } = useKosh();
83
+ const [copying, setCopying] = useState(false);
84
+ const [to, setTo] = useState('');
85
+ const [importing, setImporting] = useState(null);
86
+ const [error, setError] = useState(null);
87
+ const [done, setDone] = useState(null);
88
+ const file = useRef(null);
89
+ const companies = useQuery({
90
+ queryKey: ['settings', 'companies'],
91
+ queryFn: () => api.call('GET', '/api/v1/settings/companies'),
92
+ enabled: copying,
93
+ });
94
+ const targets = (companies.data?.companies ?? []).filter((c) => c.id !== companyId && c.member);
95
+ const copyPreview = useQuery({
96
+ queryKey: ['settings', 'app-settings', 'copy', companyId, to],
97
+ queryFn: () => api.call('POST', `${BASE}/copy`, { from: companyId, to: Number(to), preview: true }),
98
+ enabled: copying && to !== '',
99
+ });
100
+ const copy = useMutation({
101
+ mutationFn: () => api.call('POST', `${BASE}/copy`, { from: companyId, to: Number(to) }),
102
+ onSuccess: (r) => {
103
+ setCopying(false);
104
+ setDone(t('appSettings.copied', { count: r.sections.length }));
105
+ onDone();
106
+ },
107
+ onError: (e) => setError(message(e)),
108
+ });
109
+ const importPreview = useQuery({
110
+ queryKey: ['settings', 'app-settings', 'import', importing],
111
+ queryFn: () => api.call('POST', `${BASE}/import`, { file: importing, preview: true }),
112
+ enabled: importing !== null,
113
+ retry: false,
114
+ });
115
+ const doImport = useMutation({
116
+ mutationFn: () => api.call('POST', `${BASE}/import`, { file: importing }),
117
+ onSuccess: (r) => {
118
+ setImporting(null);
119
+ setDone(t('appSettings.imported', { count: r.sections.length }));
120
+ onDone();
121
+ },
122
+ onError: (e) => setError(message(e)),
123
+ });
124
+ const exportFile = async () => {
125
+ try {
126
+ const data = await api.call('GET', `${BASE}/export`);
127
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
128
+ const url = URL.createObjectURL(blob);
129
+ const a = document.createElement('a');
130
+ a.href = url;
131
+ a.download = `app-settings-${data.company.name.replaceAll(/[^\w-]+/g, '_')}.json`;
132
+ a.click();
133
+ URL.revokeObjectURL(url);
134
+ }
135
+ catch (e) {
136
+ setError(message(e));
137
+ }
138
+ };
139
+ const read = (f) => {
140
+ if (f === undefined)
141
+ return;
142
+ f.text().then((text) => {
143
+ try {
144
+ setImporting(JSON.parse(text));
145
+ setError(null);
146
+ }
147
+ catch {
148
+ setError(t('appSettings.notJson'));
149
+ }
150
+ }, (e) => setError(message(e)));
151
+ };
152
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { size: "sm", variant: "secondary", onClick: () => setCopying(true), children: [_jsx(Copy, { "aria-hidden": true }), t('appSettings.copyTo')] }), _jsxs(Button, { size: "sm", variant: "secondary", onClick: () => void exportFile(), children: [_jsx(Download, { "aria-hidden": true }), t('appSettings.export')] }), _jsxs(Button, { size: "sm", variant: "secondary", onClick: () => file.current?.click(), children: [_jsx(Upload, { "aria-hidden": true }), t('appSettings.import')] }), _jsx("input", { ref: file, type: "file", accept: "application/json,.json", className: "sr-only", "aria-label": t('appSettings.importFile'), onChange: (e) => {
153
+ read(e.target.files?.[0]);
154
+ e.target.value = '';
155
+ } }), done === null ? null : _jsx("span", { className: "text-sm text-muted-foreground", children: done }), error === null ? null : _jsx(Alert, { tone: "danger", children: error }), _jsx(Dialog, { open: copying, onOpenChange: setCopying, title: t('appSettings.copyTitle'), description: t('appSettings.copyIntro'), footer: _jsx(Button, { variant: "primary", disabled: to === '' || (copyPreview.data?.sections.length ?? 0) === 0, busy: copy.isPending, onClick: () => copy.mutate(), children: t('appSettings.copyNow') }), children: _jsxs("div", { className: "grid gap-3", children: [_jsx(Select, { id: "copy-to", label: t('appSettings.toCompany'), value: to === '' ? 'none' : to, options: [
156
+ { value: 'none', label: t('appSettings.chooseCompany') },
157
+ ...targets.map((c) => ({ value: String(c.id), label: c.name })),
158
+ ], onChange: (v) => setTo(v === 'none' ? '' : v) }), copyPreview.error !== null ? (_jsx(Alert, { tone: "danger", children: message(copyPreview.error) })) : copyPreview.data === undefined ? null : (_jsx(ChangesList, { preview: copyPreview.data }))] }) }), _jsx(Dialog, { open: importing !== null, onOpenChange: (open) => {
159
+ if (!open)
160
+ setImporting(null);
161
+ }, title: t('appSettings.importTitle'), description: t('appSettings.importIntro'), footer: _jsx(Button, { variant: "primary", disabled: (importPreview.data?.sections.length ?? 0) === 0, busy: doImport.isPending, onClick: () => doImport.mutate(), children: t('appSettings.importNow') }), children: importPreview.error !== null ? (_jsx(Alert, { tone: "danger", children: message(importPreview.error) })) : importPreview.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx(ChangesList, { preview: importPreview.data })) })] }));
162
+ }
163
+ function SectionCard({ api, app, section, }) {
164
+ const { t } = useTranslation('settings');
165
+ const queryClient = useQueryClient();
166
+ const [draft, setDraft] = useState({});
167
+ const [history, setHistory] = useState(null);
168
+ // biome-ignore lint/correctness/useExhaustiveDependencies: a reloaded section starts a clean draft
169
+ useEffect(() => setDraft({}), [section]);
170
+ const refresh = () => void queryClient.invalidateQueries({ queryKey: ['settings', 'app-settings', app] });
171
+ const save = useMutation({
172
+ mutationFn: () => api.call('PATCH', `${BASE}/${app}/${section.key}`, { values: draft }),
173
+ onSuccess: refresh,
174
+ });
175
+ const reset = useMutation({
176
+ mutationFn: (key) => api.call('POST', `${BASE}/${app}/${section.key}/reset`, { keys: [key] }),
177
+ onSuccess: refresh,
178
+ });
179
+ const currentOf = (s) => (s.key in draft ? draft[s.key] : s.value);
180
+ const dirty = Object.keys(draft).length > 0;
181
+ return (_jsxs("section", { id: `section-${section.key}`, "aria-labelledby": `section-${section.key}-title`, className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("h2", { id: `section-${section.key}-title`, className: "font-semibold", children: section.label }), section.description === null ? null : (_jsx("p", { className: "text-sm text-muted-foreground", children: section.description }))] }), _jsx(Badge, { tone: "neutral", children: section.scope === 'company' ? t('appSettings.perCompany') : t('appSettings.everywhere') })] }), _jsx("div", { className: "grid gap-5", children: section.settings
182
+ .filter((s) => {
183
+ if (s.visibleWhen === null)
184
+ return true;
185
+ const other = section.settings.find((x) => x.key === s.visibleWhen?.key);
186
+ return other !== undefined && currentOf(other) === s.visibleWhen.equals;
187
+ })
188
+ .map((s) => (_jsxs("div", { className: "grid gap-1.5 sm:grid-cols-[16rem_1fr] sm:gap-6", children: [_jsxs("div", { className: "grid content-start gap-1", children: [_jsxs(Label, { htmlFor: `setting-${s.key}`, className: "flex flex-wrap items-center gap-2", children: [s.label, s.changed && !(s.key in draft) ? (_jsx(Badge, { tone: "info", children: t('appSettings.changed') })) : null] }), s.help === null ? null : _jsx("p", { className: "text-xs text-muted-foreground", children: s.help })] }), _jsxs("div", { className: "flex flex-wrap items-start gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(Control, { setting: s, value: currentOf(s), disabled: !section.editable, onChange: (v) => setDraft((d) => ({ ...d, [s.key]: v })) }) }), _jsx(Button, { size: "icon", variant: "ghost", "aria-label": t('appSettings.historyOf', { setting: s.label }), title: t('appSettings.history'), onClick: () => setHistory(s), children: _jsx(History, { "aria-hidden": true }) }), s.changed && section.editable && !(s.key in draft) ? (_jsxs(Button, { size: "sm", variant: "ghost", busy: reset.isPending && reset.variables === s.key, "aria-label": t('appSettings.resetOne', { setting: s.label }), title: t('appSettings.defaultIs', {
189
+ value: s.default === null || s.default === '' ? '—' : String(s.default),
190
+ }), onClick: () => reset.mutate(s.key), children: [_jsx(RotateCcw, { "aria-hidden": true }), t('appSettings.reset')] })) : null] })] }, s.key))) }), save.error === null && reset.error === null ? null : (_jsx(Alert, { tone: "danger", children: message(save.error ?? reset.error) })), _jsx(HistoryDialog, { api: api, app: app, section: section, setting: history, onClose: () => setHistory(null) }), section.editable ? (_jsxs("div", { className: "flex items-center justify-end gap-2", children: [save.isSuccess && !dirty ? (_jsx("span", { className: "text-sm text-muted-foreground", children: t('appSettings.saved') })) : null, dirty ? (_jsx(Button, { variant: "ghost", onClick: () => setDraft({}), children: t('appSettings.discard') })) : null, _jsx(Button, { variant: "primary", disabled: !dirty, busy: save.isPending, onClick: () => save.mutate(), children: t('appSettings.save') })] })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: t('appSettings.readOnly') }))] }));
191
+ }
192
+ export function AppSettingsPage({ api }) {
193
+ const { t } = useTranslation('settings');
194
+ const { companyId } = useKosh();
195
+ const queryClient = useQueryClient();
196
+ const list = useQuery({
197
+ queryKey: ['settings', 'app-settings'],
198
+ queryFn: () => api.call('GET', BASE),
199
+ });
200
+ const apps = list.data?.apps ?? [];
201
+ const [app, choose] = useChosenApp(apps);
202
+ const settings = useQuery({
203
+ queryKey: ['settings', 'app-settings', app, companyId],
204
+ queryFn: () => api.call('GET', `${BASE}/${app}`),
205
+ enabled: app !== undefined,
206
+ });
207
+ if (list.error !== null)
208
+ return _jsx(Alert, { tone: "danger", children: message(list.error) });
209
+ return (_jsxs("div", { className: "mx-auto grid max-w-5xl gap-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('appSettings.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('appSettings.intro') })] }), list.data?.administrator === true ? (_jsx(Tools, { api: api, onDone: () => void queryClient.invalidateQueries({ queryKey: ['settings', 'app-settings'] }) })) : null] }), list.data !== undefined && apps.length === 0 ? (_jsx("p", { className: "rounded-lg border bg-surface p-6 text-center text-sm text-muted-foreground", children: t('appSettings.none') })) : (_jsxs("div", { className: "grid gap-6 md:grid-cols-[13rem_1fr]", children: [_jsx("nav", { "aria-label": t('appSettings.apps'), className: "md:sticky md:top-16 md:self-start", children: _jsx("ul", { className: "flex gap-1 overflow-x-auto md:grid", children: apps.map((a) => (_jsx("li", { children: _jsxs("button", { type: "button", "aria-current": a.app === app ? 'page' : undefined, onClick: () => choose(a.app), className: `flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm hover:bg-muted ${a.app === app ? 'bg-accent font-medium text-accent-foreground' : ''}`, children: [_jsx(AppIcon, { name: a.icon ?? a.app, className: "size-6 rounded-md [&_svg]:size-3.5" }), a.label] }) }, a.app))) }) }), _jsx("div", { className: "grid gap-4", children: settings.error !== null ? (_jsx(Alert, { tone: "danger", children: message(settings.error) })) : settings.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (settings.data.sections.map((s) => (_jsx(SectionCard, { api: api, app: settings.data.app, section: s }, s.key)))) })] }))] }));
210
+ }
@@ -4,7 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  * in it; switching to one; and, for administrators, a new company — its apps' defaults (journals, sequences, taxes…)
5
5
  * are installed with it, and the administrators can switch to it at once.
6
6
  */
7
- import { Alert, ApiError, Badge, Button, Dialog, Input, Label, Select, useKosh, } from '../../ui/index.js';
7
+ import { Alert, ApiError, Avatar, Badge, Button, Dialog, Input, imageUrl, Label, Select, useKosh, } from '../../ui/index.js';
8
8
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
9
9
  import { Building2, Plus } from 'lucide-react';
10
10
  import { useEffect, useId, useState } from 'react';
@@ -48,5 +48,5 @@ export function CompaniesPage({ api }) {
48
48
  if (list.error !== null)
49
49
  return _jsx(Alert, { tone: "danger", children: message(list.error) });
50
50
  const data = list.data;
51
- return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('companies.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('companies.intro') })] }), data?.canCreate === true ? (_jsxs(Button, { variant: "primary", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4", "aria-hidden": true }), t('companies.new')] })) : null] }), data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx("ul", { className: "divide-y rounded-lg border bg-surface", children: data.companies.map((c) => (_jsxs("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Building2, { className: "size-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "flex items-center gap-2 font-medium", children: [_jsx("span", { className: "truncate", children: c.name }), c.current ? _jsx(Badge, { tone: "info", children: t('companies.current') }) : null] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [c.currency, " \u00B7 ", t('companies.people', { count: c.people })] })] }), c.current || !c.member || switchCompany === undefined ? null : (_jsx(Button, { size: "sm", "aria-label": t('companies.switchTo', { name: c.name }), onClick: () => switchCompany(c.id), children: t('companies.switch') }))] }, c.id))) })), data === undefined ? null : (_jsx(NewCompanyDialog, { api: api, currencies: data.currencies, open: creating, onOpenChange: setCreating }))] }));
51
+ return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('companies.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('companies.intro') })] }), data?.canCreate === true ? (_jsxs(Button, { variant: "primary", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4", "aria-hidden": true }), t('companies.new')] })) : null] }), data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : (_jsx("ul", { className: "divide-y rounded-lg border bg-surface", children: data.companies.map((c) => (_jsxs("li", { className: "flex flex-wrap items-center gap-3 px-4 py-3", children: [c.logo === null ? (_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Building2, { className: "size-4" }) })) : (_jsx(Avatar, { name: c.name, src: imageUrl('company', c.id, c.logo), shape: "square", className: "size-9 border" })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("p", { className: "flex items-center gap-2 font-medium", children: [_jsx("span", { className: "truncate", children: c.name }), c.current ? _jsx(Badge, { tone: "info", children: t('companies.current') }) : null] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [c.currency, " \u00B7 ", t('companies.people', { count: c.people })] })] }), c.current || !c.member || switchCompany === undefined ? null : (_jsx(Button, { size: "sm", "aria-label": t('companies.switchTo', { name: c.name }), onClick: () => switchCompany(c.id), children: t('companies.switch') }))] }, c.id))) })), data === undefined ? null : (_jsx(NewCompanyDialog, { api: api, currencies: data.currencies, open: creating, onOpenChange: setCreating }))] }));
52
52
  }
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * apps add (tax ids, fiscal year…), as the server offers them. The preview shows the colour as buttons, links and
6
6
  * selections take it, in light and dark; the themes keep text readable whatever colour is chosen.
7
7
  */
8
- import { Alert, ApiError, accentVariables, Badge, Button, Checkbox, Input, Label, Select, Textarea, } from '../../ui/index.js';
8
+ import { Alert, ApiError, accentVariables, Badge, Button, Checkbox, ImagePicker, Input, imageUrl, Label, Select, Textarea, } from '../../ui/index.js';
9
9
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
10
10
  import { Check } from 'lucide-react';
11
11
  import { useId, useState } from 'react';
@@ -58,6 +58,17 @@ function CompanyForm({ api, company }) {
58
58
  const [grouping, setGrouping] = useState(company.numberGrouping);
59
59
  const [color, setColor] = useState(company.accentColor);
60
60
  const [typed, setTyped] = useState(company.accentColor ?? '');
61
+ const me = useQuery({
62
+ queryKey: ['me'],
63
+ queryFn: () => api.call('GET', '/api/v1/auth/me'),
64
+ });
65
+ const logo = useMutation({
66
+ mutationFn: (image) => image === null
67
+ ? api.call('DELETE', `/api/v1/images/company/${company.id}`)
68
+ : api.call('PUT', `/api/v1/images/company/${company.id}`, { image }),
69
+ // The sidebar and the switcher show the logo from the user's details.
70
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['me'] }),
71
+ });
61
72
  // Only the details changed are sent.
62
73
  const [details, setDetails] = useState({});
63
74
  const save = useMutation({
@@ -81,7 +92,7 @@ function CompanyForm({ api, company }) {
81
92
  return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('company.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('company.intro') })] }), _jsxs("form", { className: "grid gap-6", onSubmit: (e) => {
82
93
  e.preventDefault();
83
94
  save.mutate();
84
- }, children: [_jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsx("h2", { className: "font-semibold", children: t('company.identity') }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-name`, children: t('company.name') }), _jsx(Input, { id: `${id}-name`, value: name, required: true, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-grouping`, children: t('company.grouping') }), _jsx(Select, { id: `${id}-grouping`, label: t('company.grouping'), value: grouping, options: [
95
+ }, children: [_jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsx("h2", { className: "font-semibold", children: t('company.identity') }), _jsx(ImagePicker, { name: name, src: imageUrl('company', company.id, me.data?.logos?.[String(company.id)]), fit: "logo", busy: logo.isPending, onChoose: (image) => logo.mutate(image), onRemove: () => logo.mutate(null) }), logo.error === null ? null : (_jsx(Alert, { tone: "danger", children: logo.error instanceof ApiError ? logo.error.message : String(logo.error) })), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-name`, children: t('company.name') }), _jsx(Input, { id: `${id}-name`, value: name, required: true, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-grouping`, children: t('company.grouping') }), _jsx(Select, { id: `${id}-grouping`, label: t('company.grouping'), value: grouping, options: [
85
96
  { value: 'international', label: t('company.international') },
86
97
  { value: 'lakh', label: t('company.lakh') },
87
98
  ], onChange: (v) => setGrouping(v === 'lakh' ? 'lakh' : 'international') })] })] }), company.details.length === 0 ? null : (_jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold", children: t('company.details') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('company.detailsHint', { currency: company.currency }) })] }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2", children: company.details.map((d) => (_jsx(DetailField, { detail: d, value: d.key in details ? details[d.key] : d.value, onChange: (v) => setDetails((all) => ({ ...all, [d.key]: v })) }, d.key))) })] })), _jsxs("section", { className: "grid gap-4 rounded-lg border bg-surface p-5", children: [_jsxs("div", { children: [_jsx("h2", { className: "font-semibold", children: t('company.brand') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('company.brandHint') })] }), _jsxs("fieldset", { className: "grid gap-3", children: [_jsx("legend", { className: "sr-only", children: t('company.brand') }), _jsx("div", { className: "flex flex-wrap gap-2", children: SWATCHES.map((swatch) => (_jsx("button", { type: "button", "aria-label": swatch, "aria-pressed": color === swatch, onClick: () => pick(swatch), className: "grid size-8 place-items-center rounded-full border-2 border-surface ring-1 ring-border", style: { background: swatch }, children: color === swatch ? _jsx(Check, { className: "size-4 text-white", "aria-hidden": true }) : null }, swatch))) }), _jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-hex`, children: t('company.hex') }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "color", "aria-label": t('company.picker'), value: color ?? '#4f46e5', onChange: (e) => pick(e.target.value), className: "size-9 cursor-pointer rounded-md border bg-surface p-1" }), _jsx(Input, { id: `${id}-hex`, className: "w-32 font-mono", value: typed, placeholder: "#4f46e5", "aria-invalid": typed !== '' && !HEX.test(typed) ? true : undefined, onChange: (e) => {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Connected apps (RFC 0017 §5.2), from the account menu: the apps you allowed to act for you through OAuth, what each
3
+ * may do in words, when it last did, and *Remove access* — which ends every token the app holds for you at once.
4
+ */
5
+ import { type PageProps } from '../../ui/index.js';
6
+ import type { ReactElement } from 'react';
7
+ export declare function ConnectedAppsPage({ api }: PageProps): ReactElement;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Connected apps (RFC 0017 §5.2), from the account menu: the apps you allowed to act for you through OAuth, what each
4
+ * may do in words, when it last did, and *Remove access* — which ends every token the app holds for you at once.
5
+ */
6
+ import { Alert, ApiError, Button, useKosh } from '../../ui/index.js';
7
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
8
+ import { ExternalLink, Plug } from 'lucide-react';
9
+ import { useTranslation } from 'react-i18next';
10
+ export function ConnectedAppsPage({ api }) {
11
+ const { t } = useTranslation('settings');
12
+ const { format } = useKosh();
13
+ const queryClient = useQueryClient();
14
+ const grants = useQuery({
15
+ queryKey: ['oauth', 'grants'],
16
+ queryFn: () => api.call('GET', '/api/v1/auth/oauth/grants'),
17
+ });
18
+ const revoke = useMutation({
19
+ mutationFn: (id) => api.call('POST', `/api/v1/auth/oauth/grants/${id}/revoke`),
20
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['oauth', 'grants'] }),
21
+ });
22
+ const error = grants.error ?? revoke.error;
23
+ return (_jsxs("div", { className: "mx-auto grid max-w-3xl gap-6", children: [_jsxs("header", { children: [_jsx("h1", { className: "text-xl font-semibold", children: t('connected.title') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('connected.intro') })] }), error === null ? null : (_jsx(Alert, { tone: "danger", children: error instanceof ApiError ? error.message : String(error) })), grants.data === undefined ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u2026" })) : grants.data.grants.length === 0 ? (_jsx("p", { className: "rounded-lg border bg-surface p-6 text-center text-sm text-muted-foreground", children: t('connected.none') })) : (_jsx("ul", { className: "grid gap-3", children: grants.data.grants.map((g) => (_jsxs("li", { className: "flex flex-wrap items-start gap-3 rounded-lg border bg-surface p-4", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Plug, { className: "size-4" }) }), _jsxs("div", { className: "grid min-w-0 flex-1 gap-1", children: [_jsxs("p", { className: "flex items-center gap-2 font-medium", children: [g.app, g.homepage === null ? null : (_jsx("a", { href: g.homepage, target: "_blank", rel: "noreferrer noopener", "aria-label": t('connected.homepage', { app: g.app }), className: "text-muted-foreground hover:text-foreground", children: _jsx(ExternalLink, { className: "size-3.5", "aria-hidden": true }) }))] }), _jsx("ul", { className: "flex flex-wrap gap-x-3 text-sm text-muted-foreground", children: g.scopes.map((s) => (_jsx("li", { children: s.label }, s.scope))) }), _jsx("p", { className: "text-xs text-muted-foreground", children: [
24
+ g.createdAt === null
25
+ ? null
26
+ : t('connected.since', { when: format.date(g.createdAt.slice(0, 10)) }),
27
+ g.lastUsedAt === null
28
+ ? t('connected.neverUsed')
29
+ : t('connected.lastUsed', { when: format.datetime(g.lastUsedAt) }),
30
+ ]
31
+ .filter((x) => x !== null)
32
+ .join(' · ') })] }), _jsx(Button, { size: "sm", variant: "danger", busy: revoke.isPending && revoke.variables === g.id, onClick: () => revoke.mutate(g.id), "aria-label": t('connected.revokeApp', { app: g.app }), children: t('connected.revoke') })] }, g.id))) }))] }));
33
+ }
@@ -12,15 +12,39 @@ import { Search, UserRound } from 'lucide-react';
12
12
  import { useId, useState } from 'react';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  const normalise = (s) => s.toLocaleLowerCase().normalize('NFKD');
15
+ /** Each section's icon (AppIcon names); a module's own section shows the Settings gear. */
16
+ const SECTION_ICONS = {
17
+ 'settings.action_company': 'company',
18
+ 'settings.action_companies': 'companies',
19
+ 'settings.action_people': 'users',
20
+ 'settings.action_roles': 'shield',
21
+ 'settings.action_security': 'security',
22
+ 'settings.action_audit': 'audit',
23
+ 'settings.action_apps': 'apps',
24
+ 'settings.action_app_settings': 'settings',
25
+ 'assistant.action_settings': 'assistant',
26
+ };
15
27
  export function OverviewPage({ api }) {
16
28
  const { t } = useTranslation('settings');
17
29
  const id = useId();
18
30
  const [query, setQuery] = useState('');
19
31
  const menus = useQuery({ queryKey: ['menus', 'settings'], queryFn: () => api.menus('settings') });
20
- const about = (action) => ({
21
- description: t(`overview.about.${action}.description`, { defaultValue: '' }),
22
- keywords: t(`overview.about.${action}.keywords`, { defaultValue: '' }),
32
+ // The apps' own settings (RFC 0020): their sections' words make App settings findable by them.
33
+ const appSettings = useQuery({
34
+ queryKey: ['settings', 'app-settings'],
35
+ queryFn: () => api.call('GET', '/api/v1/settings/app-settings'),
23
36
  });
37
+ const about = (action) => {
38
+ const words = action === 'settings.action_app_settings'
39
+ ? (appSettings.data?.apps ?? [])
40
+ .flatMap((a) => [a.label, ...a.sections.flatMap((s) => [s.label, s.keywords])])
41
+ .join(' ')
42
+ : '';
43
+ return {
44
+ description: t(`overview.about.${action}.description`, { defaultValue: '' }),
45
+ keywords: `${t(`overview.about.${action}.keywords`, { defaultValue: '' })} ${words}`,
46
+ };
47
+ };
24
48
  const entry = (n) => n.action === undefined || n.action === 'settings.action_overview'
25
49
  ? null
26
50
  : { label: n.label, action: n.action, ...about(n.action) };
@@ -62,5 +86,5 @@ export function OverviewPage({ api }) {
62
86
  const showProfile = matches(profile);
63
87
  return (_jsxs("div", { className: "mx-auto grid max-w-4xl gap-6", children: [_jsxs("header", { className: "grid gap-3", children: [_jsx("h1", { className: "text-2xl font-semibold", children: t('overview.title') }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }), _jsx("label", { htmlFor: `${id}-q`, className: "sr-only", children: t('overview.search') }), _jsx("input", { id: `${id}-q`, type: "search",
64
88
  // biome-ignore lint/a11y/noAutofocus: the page is a search first; it opens on purpose
65
- autoFocus: true, value: query, placeholder: t('overview.search'), onChange: (e) => setQuery(e.target.value), className: "h-11 w-full rounded-lg border border-input bg-surface ps-10 pe-3 text-base shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" })] })] }), shown.length === 0 && !showProfile ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('overview.nothing', { query }) })) : null, shown.map((g) => (_jsxs("section", { "aria-labelledby": `${id}-${g.id}`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-${g.id}`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: g.label }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: g.entries.map((e) => card(e, _jsxs(Link, { to: "/$app/a/$action", params: { app: 'settings', action: e.action }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx(AppIcon, { name: "settings", className: "size-9" }), _jsxs("span", { className: "grid gap-0.5", children: [_jsx("span", { className: "font-medium", children: e.label }), e.description === '' ? null : (_jsx("span", { className: "text-sm text-muted-foreground", children: e.description }))] })] }))) })] }, g.id))), showProfile ? (_jsxs("section", { "aria-labelledby": `${id}-you`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-you`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: t('overview.you') }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: card(profile, _jsxs(Link, { to: "/me/$page", params: { page: 'settings.Profile' }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-lg bg-accent text-accent-foreground", children: _jsx(UserRound, { className: "size-4" }) }), _jsxs("span", { className: "grid gap-0.5", children: [_jsx("span", { className: "font-medium", children: profile.label }), _jsx("span", { className: "text-sm text-muted-foreground", children: profile.description })] })] })) })] })) : null] }));
89
+ autoFocus: true, value: query, placeholder: t('overview.search'), onChange: (e) => setQuery(e.target.value), className: "h-11 w-full rounded-lg border border-input bg-surface ps-10 pe-3 text-base shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" })] })] }), shown.length === 0 && !showProfile ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('overview.nothing', { query }) })) : null, shown.map((g) => (_jsxs("section", { "aria-labelledby": `${id}-${g.id}`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-${g.id}`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: g.label }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: g.entries.map((e) => card(e, _jsxs(Link, { to: "/$app/a/$action", params: { app: 'settings', action: e.action }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx(AppIcon, { name: SECTION_ICONS[e.action] ?? 'settings', className: "size-9" }), _jsxs("span", { className: "grid gap-0.5", children: [_jsx("span", { className: "font-medium", children: e.label }), e.description === '' ? null : (_jsx("span", { className: "text-sm text-muted-foreground", children: e.description }))] })] }))) })] }, g.id))), showProfile ? (_jsxs("section", { "aria-labelledby": `${id}-you`, className: "grid gap-2", children: [_jsx("h2", { id: `${id}-you`, className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: t('overview.you') }), _jsx("ul", { className: "grid gap-3 sm:grid-cols-2", children: card(profile, _jsxs(Link, { to: "/me/$page", params: { page: 'settings.Profile' }, className: "flex h-full items-start gap-3 rounded-lg border bg-surface p-4 hover:border-primary/40 hover:bg-accent/40", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-lg bg-accent text-accent-foreground", children: _jsx(UserRound, { className: "size-4" }) }), _jsxs("span", { className: "grid gap-0.5", children: [_jsx("span", { className: "font-medium", children: profile.label }), _jsx("span", { className: "text-sm text-muted-foreground", children: profile.description })] })] })) })] })) : null] }));
66
90
  }