@open-mercato/core 0.4.8-develop-28cee031d6 → 0.4.8-develop-84f3678a58

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 (330) hide show
  1. package/agentic/standalone-guide.md +235 -0
  2. package/dist/generated/entities/customer_role/index.js +27 -0
  3. package/dist/generated/entities/customer_role/index.js.map +7 -0
  4. package/dist/generated/entities/customer_role_acl/index.js +19 -0
  5. package/dist/generated/entities/customer_role_acl/index.js.map +7 -0
  6. package/dist/generated/entities/customer_user/index.js +37 -0
  7. package/dist/generated/entities/customer_user/index.js.map +7 -0
  8. package/dist/generated/entities/customer_user_acl/index.js +19 -0
  9. package/dist/generated/entities/customer_user_acl/index.js.map +7 -0
  10. package/dist/generated/entities/customer_user_email_verification/index.js +17 -0
  11. package/dist/generated/entities/customer_user_email_verification/index.js.map +7 -0
  12. package/dist/generated/entities/customer_user_invitation/index.js +33 -0
  13. package/dist/generated/entities/customer_user_invitation/index.js.map +7 -0
  14. package/dist/generated/entities/customer_user_password_reset/index.js +15 -0
  15. package/dist/generated/entities/customer_user_password_reset/index.js.map +7 -0
  16. package/dist/generated/entities/customer_user_role/index.js +13 -0
  17. package/dist/generated/entities/customer_user_role/index.js.map +7 -0
  18. package/dist/generated/entities/customer_user_session/index.js +21 -0
  19. package/dist/generated/entities/customer_user_session/index.js.map +7 -0
  20. package/dist/generated/entities/organization/index.js +2 -0
  21. package/dist/generated/entities/organization/index.js.map +2 -2
  22. package/dist/generated/entities.ids.generated.js +14 -1
  23. package/dist/generated/entities.ids.generated.js.map +2 -2
  24. package/dist/generated/entity-fields-registry.js +18 -0
  25. package/dist/generated/entity-fields-registry.js.map +2 -2
  26. package/dist/modules/auth/services/rbacService.js +3 -9
  27. package/dist/modules/auth/services/rbacService.js.map +2 -2
  28. package/dist/modules/customer_accounts/acl.js +12 -0
  29. package/dist/modules/customer_accounts/acl.js.map +7 -0
  30. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +87 -0
  31. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +7 -0
  32. package/dist/modules/customer_accounts/api/admin/roles/[id].js +216 -0
  33. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +7 -0
  34. package/dist/modules/customer_accounts/api/admin/roles.js +189 -0
  35. package/dist/modules/customer_accounts/api/admin/roles.js.map +7 -0
  36. package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js +69 -0
  37. package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js.map +7 -0
  38. package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js +64 -0
  39. package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js.map +7 -0
  40. package/dist/modules/customer_accounts/api/admin/users/[id].js +253 -0
  41. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +7 -0
  42. package/dist/modules/customer_accounts/api/admin/users-invite.js +78 -0
  43. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +7 -0
  44. package/dist/modules/customer_accounts/api/admin/users.js +251 -0
  45. package/dist/modules/customer_accounts/api/admin/users.js.map +7 -0
  46. package/dist/modules/customer_accounts/api/email/verify.js +59 -0
  47. package/dist/modules/customer_accounts/api/email/verify.js.map +7 -0
  48. package/dist/modules/customer_accounts/api/interceptors.js +5 -0
  49. package/dist/modules/customer_accounts/api/interceptors.js.map +7 -0
  50. package/dist/modules/customer_accounts/api/invitations/accept.js +114 -0
  51. package/dist/modules/customer_accounts/api/invitations/accept.js.map +7 -0
  52. package/dist/modules/customer_accounts/api/login.js +143 -0
  53. package/dist/modules/customer_accounts/api/login.js.map +7 -0
  54. package/dist/modules/customer_accounts/api/magic-link/request.js +78 -0
  55. package/dist/modules/customer_accounts/api/magic-link/request.js.map +7 -0
  56. package/dist/modules/customer_accounts/api/magic-link/verify.js +114 -0
  57. package/dist/modules/customer_accounts/api/magic-link/verify.js.map +7 -0
  58. package/dist/modules/customer_accounts/api/password/reset-confirm.js +59 -0
  59. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +7 -0
  60. package/dist/modules/customer_accounts/api/password/reset-request.js +77 -0
  61. package/dist/modules/customer_accounts/api/password/reset-request.js.map +7 -0
  62. package/dist/modules/customer_accounts/api/portal/events/stream.js +163 -0
  63. package/dist/modules/customer_accounts/api/portal/events/stream.js.map +7 -0
  64. package/dist/modules/customer_accounts/api/portal/feature-check.js +57 -0
  65. package/dist/modules/customer_accounts/api/portal/feature-check.js.map +7 -0
  66. package/dist/modules/customer_accounts/api/portal/logout.js +64 -0
  67. package/dist/modules/customer_accounts/api/portal/logout.js.map +7 -0
  68. package/dist/modules/customer_accounts/api/portal/notifications/[id]/dismiss.js +49 -0
  69. package/dist/modules/customer_accounts/api/portal/notifications/[id]/dismiss.js.map +7 -0
  70. package/dist/modules/customer_accounts/api/portal/notifications/[id]/read.js +49 -0
  71. package/dist/modules/customer_accounts/api/portal/notifications/[id]/read.js.map +7 -0
  72. package/dist/modules/customer_accounts/api/portal/notifications/mark-all-read.js +46 -0
  73. package/dist/modules/customer_accounts/api/portal/notifications/mark-all-read.js.map +7 -0
  74. package/dist/modules/customer_accounts/api/portal/notifications/unread-count.js +42 -0
  75. package/dist/modules/customer_accounts/api/portal/notifications/unread-count.js.map +7 -0
  76. package/dist/modules/customer_accounts/api/portal/notifications.js +105 -0
  77. package/dist/modules/customer_accounts/api/portal/notifications.js.map +7 -0
  78. package/dist/modules/customer_accounts/api/portal/password-change.js +57 -0
  79. package/dist/modules/customer_accounts/api/portal/password-change.js.map +7 -0
  80. package/dist/modules/customer_accounts/api/portal/profile.js +135 -0
  81. package/dist/modules/customer_accounts/api/portal/profile.js.map +7 -0
  82. package/dist/modules/customer_accounts/api/portal/sessions/[id].js +62 -0
  83. package/dist/modules/customer_accounts/api/portal/sessions/[id].js.map +7 -0
  84. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +75 -0
  85. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +7 -0
  86. package/dist/modules/customer_accounts/api/portal/sessions.js +77 -0
  87. package/dist/modules/customer_accounts/api/portal/sessions.js.map +7 -0
  88. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +90 -0
  89. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +7 -0
  90. package/dist/modules/customer_accounts/api/portal/users/[id].js +71 -0
  91. package/dist/modules/customer_accounts/api/portal/users/[id].js.map +7 -0
  92. package/dist/modules/customer_accounts/api/portal/users-invite.js +92 -0
  93. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +7 -0
  94. package/dist/modules/customer_accounts/api/portal/users.js +79 -0
  95. package/dist/modules/customer_accounts/api/portal/users.js.map +7 -0
  96. package/dist/modules/customer_accounts/api/signup.js +121 -0
  97. package/dist/modules/customer_accounts/api/signup.js.map +7 -0
  98. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.js +491 -0
  99. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.js.map +7 -0
  100. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.js +15 -0
  101. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.js.map +7 -0
  102. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +343 -0
  103. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +7 -0
  104. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.js +16 -0
  105. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.js.map +7 -0
  106. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.js +180 -0
  107. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.js.map +7 -0
  108. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.js +16 -0
  109. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.js.map +7 -0
  110. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +176 -0
  111. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +7 -0
  112. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.meta.js +33 -0
  113. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.meta.js.map +7 -0
  114. package/dist/modules/customer_accounts/backend/page.js +466 -0
  115. package/dist/modules/customer_accounts/backend/page.js.map +7 -0
  116. package/dist/modules/customer_accounts/backend/page.meta.js +35 -0
  117. package/dist/modules/customer_accounts/backend/page.meta.js.map +7 -0
  118. package/dist/modules/customer_accounts/ce.js +26 -0
  119. package/dist/modules/customer_accounts/ce.js.map +7 -0
  120. package/dist/modules/customer_accounts/data/enrichers.js +85 -0
  121. package/dist/modules/customer_accounts/data/enrichers.js.map +7 -0
  122. package/dist/modules/customer_accounts/data/entities.js +377 -0
  123. package/dist/modules/customer_accounts/data/entities.js.map +7 -0
  124. package/dist/modules/customer_accounts/data/extensions.js +8 -0
  125. package/dist/modules/customer_accounts/data/extensions.js.map +7 -0
  126. package/dist/modules/customer_accounts/data/validators.js +111 -0
  127. package/dist/modules/customer_accounts/data/validators.js.map +7 -0
  128. package/dist/modules/customer_accounts/di.js +17 -0
  129. package/dist/modules/customer_accounts/di.js.map +7 -0
  130. package/dist/modules/customer_accounts/events.js +28 -0
  131. package/dist/modules/customer_accounts/events.js.map +7 -0
  132. package/dist/modules/customer_accounts/index.js +15 -0
  133. package/dist/modules/customer_accounts/index.js.map +7 -0
  134. package/dist/modules/customer_accounts/lib/customerAuth.js +71 -0
  135. package/dist/modules/customer_accounts/lib/customerAuth.js.map +7 -0
  136. package/dist/modules/customer_accounts/lib/customerAuthServer.js +29 -0
  137. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +7 -0
  138. package/dist/modules/customer_accounts/lib/rateLimiter.js +63 -0
  139. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +7 -0
  140. package/dist/modules/customer_accounts/lib/tokenGenerator.js +12 -0
  141. package/dist/modules/customer_accounts/lib/tokenGenerator.js.map +7 -0
  142. package/dist/modules/customer_accounts/migrations/Migration20260313222043.js +49 -0
  143. package/dist/modules/customer_accounts/migrations/Migration20260313222043.js.map +7 -0
  144. package/dist/modules/customer_accounts/notifications.client.js +47 -0
  145. package/dist/modules/customer_accounts/notifications.client.js.map +7 -0
  146. package/dist/modules/customer_accounts/notifications.js +46 -0
  147. package/dist/modules/customer_accounts/notifications.js.map +7 -0
  148. package/dist/modules/customer_accounts/search.js +120 -0
  149. package/dist/modules/customer_accounts/search.js.map +7 -0
  150. package/dist/modules/customer_accounts/services/customerInvitationService.js +87 -0
  151. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +7 -0
  152. package/dist/modules/customer_accounts/services/customerRbacService.js +109 -0
  153. package/dist/modules/customer_accounts/services/customerRbacService.js.map +7 -0
  154. package/dist/modules/customer_accounts/services/customerSessionService.js +75 -0
  155. package/dist/modules/customer_accounts/services/customerSessionService.js.map +7 -0
  156. package/dist/modules/customer_accounts/services/customerTokenService.js +91 -0
  157. package/dist/modules/customer_accounts/services/customerTokenService.js.map +7 -0
  158. package/dist/modules/customer_accounts/services/customerUserService.js +92 -0
  159. package/dist/modules/customer_accounts/services/customerUserService.js.map +7 -0
  160. package/dist/modules/customer_accounts/setup.js +179 -0
  161. package/dist/modules/customer_accounts/setup.js.map +7 -0
  162. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +54 -0
  163. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +7 -0
  164. package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js +68 -0
  165. package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js.map +7 -0
  166. package/dist/modules/customer_accounts/subscribers/notifyStaffOnSignup.js +29 -0
  167. package/dist/modules/customer_accounts/subscribers/notifyStaffOnSignup.js.map +7 -0
  168. package/dist/modules/customer_accounts/translations.js +9 -0
  169. package/dist/modules/customer_accounts/translations.js.map +7 -0
  170. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +63 -0
  171. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +7 -0
  172. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.js +17 -0
  173. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.js.map +7 -0
  174. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +55 -0
  175. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +7 -0
  176. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.js +17 -0
  177. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.js.map +7 -0
  178. package/dist/modules/customer_accounts/widgets/injection-table.js +26 -0
  179. package/dist/modules/customer_accounts/widgets/injection-table.js.map +7 -0
  180. package/dist/modules/customer_accounts/workers/cleanupExpiredSessions.js +23 -0
  181. package/dist/modules/customer_accounts/workers/cleanupExpiredSessions.js.map +7 -0
  182. package/dist/modules/customer_accounts/workers/cleanupExpiredTokens.js +38 -0
  183. package/dist/modules/customer_accounts/workers/cleanupExpiredTokens.js.map +7 -0
  184. package/dist/modules/directory/api/get/organizations/lookup.js +83 -0
  185. package/dist/modules/directory/api/get/organizations/lookup.js.map +7 -0
  186. package/dist/modules/directory/commands/organizations.js +32 -1
  187. package/dist/modules/directory/commands/organizations.js.map +2 -2
  188. package/dist/modules/directory/data/entities.js +6 -2
  189. package/dist/modules/directory/data/entities.js.map +2 -2
  190. package/dist/modules/directory/data/validators.js +3 -0
  191. package/dist/modules/directory/data/validators.js.map +2 -2
  192. package/dist/modules/directory/migrations/Migration20260314143323.js +15 -0
  193. package/dist/modules/directory/migrations/Migration20260314143323.js.map +7 -0
  194. package/dist/modules/directory/setup.js +36 -0
  195. package/dist/modules/directory/setup.js.map +2 -2
  196. package/dist/modules/payment_gateways/migrations/Migration20260313222043.js +15 -0
  197. package/dist/modules/payment_gateways/migrations/Migration20260313222043.js.map +7 -0
  198. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +131 -0
  199. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +7 -0
  200. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +96 -0
  201. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +7 -0
  202. package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +94 -0
  203. package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +7 -0
  204. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +89 -0
  205. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +7 -0
  206. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +104 -0
  207. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +7 -0
  208. package/dist/modules/portal/index.js +11 -0
  209. package/dist/modules/portal/index.js.map +7 -0
  210. package/dist/modules/portal/setup.js +23 -0
  211. package/dist/modules/portal/setup.js.map +7 -0
  212. package/generated/entities/customer_role/index.ts +12 -0
  213. package/generated/entities/customer_role_acl/index.ts +8 -0
  214. package/generated/entities/customer_user/index.ts +17 -0
  215. package/generated/entities/customer_user_acl/index.ts +8 -0
  216. package/generated/entities/customer_user_email_verification/index.ts +7 -0
  217. package/generated/entities/customer_user_invitation/index.ts +15 -0
  218. package/generated/entities/customer_user_password_reset/index.ts +6 -0
  219. package/generated/entities/customer_user_role/index.ts +5 -0
  220. package/generated/entities/customer_user_session/index.ts +9 -0
  221. package/generated/entities/organization/index.ts +1 -0
  222. package/generated/entities.ids.generated.ts +14 -1
  223. package/generated/entity-fields-registry.ts +18 -0
  224. package/package.json +3 -3
  225. package/src/modules/auth/services/rbacService.ts +3 -9
  226. package/src/modules/customer_accounts/AGENTS.md +377 -0
  227. package/src/modules/customer_accounts/acl.ts +8 -0
  228. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +98 -0
  229. package/src/modules/customer_accounts/api/admin/roles/[id].ts +246 -0
  230. package/src/modules/customer_accounts/api/admin/roles.ts +212 -0
  231. package/src/modules/customer_accounts/api/admin/users/[id]/reset-password.ts +78 -0
  232. package/src/modules/customer_accounts/api/admin/users/[id]/verify-email.ts +72 -0
  233. package/src/modules/customer_accounts/api/admin/users/[id].ts +289 -0
  234. package/src/modules/customer_accounts/api/admin/users-invite.ts +86 -0
  235. package/src/modules/customer_accounts/api/admin/users.ts +280 -0
  236. package/src/modules/customer_accounts/api/email/verify.ts +66 -0
  237. package/src/modules/customer_accounts/api/interceptors.ts +3 -0
  238. package/src/modules/customer_accounts/api/invitations/accept.ts +128 -0
  239. package/src/modules/customer_accounts/api/login.ts +163 -0
  240. package/src/modules/customer_accounts/api/magic-link/request.ts +87 -0
  241. package/src/modules/customer_accounts/api/magic-link/verify.ts +132 -0
  242. package/src/modules/customer_accounts/api/password/reset-confirm.ts +69 -0
  243. package/src/modules/customer_accounts/api/password/reset-request.ts +87 -0
  244. package/src/modules/customer_accounts/api/portal/events/stream.ts +209 -0
  245. package/src/modules/customer_accounts/api/portal/feature-check.ts +60 -0
  246. package/src/modules/customer_accounts/api/portal/logout.ts +71 -0
  247. package/src/modules/customer_accounts/api/portal/notifications/[id]/dismiss.ts +54 -0
  248. package/src/modules/customer_accounts/api/portal/notifications/[id]/read.ts +54 -0
  249. package/src/modules/customer_accounts/api/portal/notifications/mark-all-read.ts +49 -0
  250. package/src/modules/customer_accounts/api/portal/notifications/unread-count.ts +45 -0
  251. package/src/modules/customer_accounts/api/portal/notifications.ts +115 -0
  252. package/src/modules/customer_accounts/api/portal/password-change.ts +65 -0
  253. package/src/modules/customer_accounts/api/portal/profile.ts +151 -0
  254. package/src/modules/customer_accounts/api/portal/sessions/[id].ts +70 -0
  255. package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +87 -0
  256. package/src/modules/customer_accounts/api/portal/sessions.ts +84 -0
  257. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +106 -0
  258. package/src/modules/customer_accounts/api/portal/users/[id].ts +81 -0
  259. package/src/modules/customer_accounts/api/portal/users-invite.ts +103 -0
  260. package/src/modules/customer_accounts/api/portal/users.ts +86 -0
  261. package/src/modules/customer_accounts/api/signup.ts +136 -0
  262. package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.ts +11 -0
  263. package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.tsx +607 -0
  264. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.ts +12 -0
  265. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +385 -0
  266. package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.ts +12 -0
  267. package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.tsx +203 -0
  268. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.meta.ts +31 -0
  269. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +217 -0
  270. package/src/modules/customer_accounts/backend/page.meta.ts +33 -0
  271. package/src/modules/customer_accounts/backend/page.tsx +535 -0
  272. package/src/modules/customer_accounts/ce.ts +22 -0
  273. package/src/modules/customer_accounts/data/enrichers.ts +117 -0
  274. package/src/modules/customer_accounts/data/entities.ts +302 -0
  275. package/src/modules/customer_accounts/data/extensions.ts +4 -0
  276. package/src/modules/customer_accounts/data/validators.ts +128 -0
  277. package/src/modules/customer_accounts/di.ts +15 -0
  278. package/src/modules/customer_accounts/events.ts +28 -0
  279. package/src/modules/customer_accounts/i18n/de.json +176 -0
  280. package/src/modules/customer_accounts/i18n/en.json +176 -0
  281. package/src/modules/customer_accounts/i18n/es.json +176 -0
  282. package/src/modules/customer_accounts/i18n/pl.json +176 -0
  283. package/src/modules/customer_accounts/index.ts +13 -0
  284. package/src/modules/customer_accounts/lib/customerAuth.ts +85 -0
  285. package/src/modules/customer_accounts/lib/customerAuthServer.ts +54 -0
  286. package/src/modules/customer_accounts/lib/rateLimiter.ts +36 -0
  287. package/src/modules/customer_accounts/lib/tokenGenerator.ts +9 -0
  288. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +1255 -0
  289. package/src/modules/customer_accounts/migrations/Migration20260313222043.ts +62 -0
  290. package/src/modules/customer_accounts/notifications.client.ts +46 -0
  291. package/src/modules/customer_accounts/notifications.ts +44 -0
  292. package/src/modules/customer_accounts/search.ts +134 -0
  293. package/src/modules/customer_accounts/services/customerInvitationService.ts +109 -0
  294. package/src/modules/customer_accounts/services/customerRbacService.ts +144 -0
  295. package/src/modules/customer_accounts/services/customerSessionService.ts +90 -0
  296. package/src/modules/customer_accounts/services/customerTokenService.ts +98 -0
  297. package/src/modules/customer_accounts/services/customerUserService.ts +105 -0
  298. package/src/modules/customer_accounts/setup.ts +212 -0
  299. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +65 -0
  300. package/src/modules/customer_accounts/subscribers/autoLinkCrmReverse.ts +78 -0
  301. package/src/modules/customer_accounts/subscribers/notifyStaffOnSignup.ts +32 -0
  302. package/src/modules/customer_accounts/translations.ts +5 -0
  303. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +89 -0
  304. package/src/modules/customer_accounts/widgets/injection/account-status/widget.ts +16 -0
  305. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +78 -0
  306. package/src/modules/customer_accounts/widgets/injection/company-users/widget.ts +16 -0
  307. package/src/modules/customer_accounts/widgets/injection-table.ts +24 -0
  308. package/src/modules/customer_accounts/workers/cleanupExpiredSessions.ts +33 -0
  309. package/src/modules/customer_accounts/workers/cleanupExpiredTokens.ts +51 -0
  310. package/src/modules/directory/api/get/organizations/lookup.ts +92 -0
  311. package/src/modules/directory/commands/organizations.ts +34 -1
  312. package/src/modules/directory/data/entities.ts +5 -1
  313. package/src/modules/directory/data/validators.ts +4 -0
  314. package/src/modules/directory/migrations/.snapshot-open-mercato.json +20 -1
  315. package/src/modules/directory/migrations/Migration20260314143323.ts +15 -0
  316. package/src/modules/directory/setup.ts +41 -0
  317. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +4 -1
  318. package/src/modules/payment_gateways/migrations/Migration20260313222043.ts +17 -0
  319. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +158 -0
  320. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +120 -0
  321. package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +118 -0
  322. package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +112 -0
  323. package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +138 -0
  324. package/src/modules/portal/i18n/de.json +93 -0
  325. package/src/modules/portal/i18n/en.json +93 -0
  326. package/src/modules/portal/i18n/es.json +93 -0
  327. package/src/modules/portal/i18n/pl.json +93 -0
  328. package/src/modules/portal/index.ts +9 -0
  329. package/src/modules/portal/setup.ts +23 -0
  330. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +226 -0
@@ -0,0 +1,535 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
8
+ import type { ColumnDef } from '@tanstack/react-table'
9
+ import { RowActions } from '@open-mercato/ui/backend/RowActions'
10
+ import { Button } from '@open-mercato/ui/primitives/button'
11
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
12
+ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
13
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
18
+
19
+ type UserRow = {
20
+ id: string
21
+ displayName: string
22
+ email: string
23
+ emailVerified: boolean
24
+ isActive: boolean
25
+ lastLoginAt: string | null
26
+ roles: Array<{ id: string; name: string; slug: string }>
27
+ createdAt: string
28
+ personEntityId: string | null
29
+ customerEntityId: string | null
30
+ }
31
+
32
+ type UsersResponse = {
33
+ items?: UserRow[]
34
+ total?: number
35
+ totalPages?: number
36
+ }
37
+
38
+ function formatDate(value: string | null | undefined, fallback: string): string {
39
+ if (!value) return fallback
40
+ const date = new Date(value)
41
+ if (Number.isNaN(date.getTime())) return fallback
42
+ return date.toLocaleDateString()
43
+ }
44
+
45
+ async function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {
46
+ try {
47
+ const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(
48
+ '/api/customer_accounts/admin/roles?pageSize=100',
49
+ )
50
+ if (!call.ok) return []
51
+ const items = Array.isArray(call.result?.items) ? call.result!.items : []
52
+ return items
53
+ .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')
54
+ .map((item) => ({ value: item.id, label: item.name, id: item.id }))
55
+ } catch {
56
+ return []
57
+ }
58
+ }
59
+
60
+ function CreateUserDialog({
61
+ open,
62
+ onOpenChange,
63
+ roleOptions,
64
+ onCreated,
65
+ onRunMutation,
66
+ }: {
67
+ open: boolean
68
+ onOpenChange: (next: boolean) => void
69
+ roleOptions: Array<{ id: string; label: string }>
70
+ onCreated: () => void
71
+ onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>
72
+ }) {
73
+ const t = useT()
74
+ const [email, setEmail] = React.useState('')
75
+ const [displayName, setDisplayName] = React.useState('')
76
+ const [password, setPassword] = React.useState('')
77
+ const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])
78
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
79
+
80
+ const resetForm = React.useCallback(() => {
81
+ setEmail('')
82
+ setDisplayName('')
83
+ setPassword('')
84
+ setSelectedRoleIds([])
85
+ }, [])
86
+
87
+ const handleSubmit = React.useCallback(async (event: React.FormEvent) => {
88
+ event.preventDefault()
89
+ if (!email.trim() || !displayName.trim() || !password.trim()) {
90
+ flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')
91
+ return
92
+ }
93
+ setIsSubmitting(true)
94
+ try {
95
+ await onRunMutation(async () => {
96
+ const call = await apiCall<{ ok: boolean; error?: string }>(
97
+ '/api/customer_accounts/admin/users',
98
+ {
99
+ method: 'POST',
100
+ headers: { 'content-type': 'application/json' },
101
+ body: JSON.stringify({
102
+ email: email.trim(),
103
+ displayName: displayName.trim(),
104
+ password,
105
+ roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,
106
+ }),
107
+ },
108
+ )
109
+ if (!call.ok) {
110
+ flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')
111
+ return
112
+ }
113
+ flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')
114
+ resetForm()
115
+ onOpenChange(false)
116
+ onCreated()
117
+ })
118
+ } catch (err) {
119
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')
120
+ flash(message, 'error')
121
+ } finally {
122
+ setIsSubmitting(false)
123
+ }
124
+ }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])
125
+
126
+ const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
127
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
128
+ event.preventDefault()
129
+ const form = (event.target as HTMLElement).closest('form')
130
+ if (form) form.requestSubmit()
131
+ }
132
+ }, [])
133
+
134
+ return (
135
+ <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>
136
+ <DialogContent className="sm:max-w-lg">
137
+ <DialogHeader>
138
+ <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>
139
+ </DialogHeader>
140
+ <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className="space-y-4">
141
+ <div className="space-y-2">
142
+ <label className="text-sm font-medium" htmlFor="create-email">
143
+ {t('customer_accounts.admin.createUser.fields.email', 'Email')}
144
+ </label>
145
+ <input
146
+ id="create-email"
147
+ type="email"
148
+ required
149
+ value={email}
150
+ onChange={(event) => setEmail(event.target.value)}
151
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
152
+ placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}
153
+ />
154
+ </div>
155
+ <div className="space-y-2">
156
+ <label className="text-sm font-medium" htmlFor="create-name">
157
+ {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}
158
+ </label>
159
+ <input
160
+ id="create-name"
161
+ type="text"
162
+ required
163
+ value={displayName}
164
+ onChange={(event) => setDisplayName(event.target.value)}
165
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
166
+ placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}
167
+ />
168
+ </div>
169
+ <div className="space-y-2">
170
+ <label className="text-sm font-medium" htmlFor="create-password">
171
+ {t('customer_accounts.admin.createUser.fields.password', 'Password')}
172
+ </label>
173
+ <input
174
+ id="create-password"
175
+ type="password"
176
+ required
177
+ minLength={8}
178
+ value={password}
179
+ onChange={(event) => setPassword(event.target.value)}
180
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
181
+ placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}
182
+ />
183
+ </div>
184
+ {roleOptions.length > 0 && (
185
+ <div className="space-y-2">
186
+ <p className="text-sm font-medium">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>
187
+ <div className="flex flex-wrap gap-2">
188
+ {roleOptions.map((role) => {
189
+ const isSelected = selectedRoleIds.includes(role.id)
190
+ return (
191
+ <button
192
+ key={role.id}
193
+ type="button"
194
+ onClick={() => setSelectedRoleIds((prev) =>
195
+ prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],
196
+ )}
197
+ className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${
198
+ isSelected
199
+ ? 'border-primary bg-primary/10 text-primary'
200
+ : 'border-border bg-background text-muted-foreground hover:bg-muted'
201
+ }`}
202
+ >
203
+ {role.label}
204
+ </button>
205
+ )
206
+ })}
207
+ </div>
208
+ </div>
209
+ )}
210
+ <div className="flex justify-end gap-2 pt-2">
211
+ <Button type="button" variant="outline" onClick={() => { resetForm(); onOpenChange(false) }}>
212
+ {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}
213
+ </Button>
214
+ <Button type="submit" disabled={isSubmitting}>
215
+ {isSubmitting
216
+ ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')
217
+ : t('customer_accounts.admin.createUser.actions.create', 'Create User')}
218
+ </Button>
219
+ </div>
220
+ </form>
221
+ </DialogContent>
222
+ </Dialog>
223
+ )
224
+ }
225
+
226
+ export default function CustomerAccountsPage() {
227
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
228
+ const t = useT()
229
+ const router = useRouter()
230
+ const [rows, setRows] = React.useState<UserRow[]>([])
231
+ const [page, setPage] = React.useState(1)
232
+ const [pageSize] = React.useState(50)
233
+ const [total, setTotal] = React.useState(0)
234
+ const [totalPages, setTotalPages] = React.useState(1)
235
+ const [search, setSearch] = React.useState('')
236
+ const [filterValues, setFilterValues] = React.useState<FilterValues>({})
237
+ const [isLoading, setIsLoading] = React.useState(true)
238
+ const [reloadToken, setReloadToken] = React.useState(0)
239
+ const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])
240
+ const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
241
+
242
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
243
+ entityType: string
244
+ entityId?: string
245
+ }>({
246
+ contextId: 'customer_accounts:users-list',
247
+ })
248
+
249
+ const runMutationWithContext = React.useCallback(
250
+ async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
251
+ return runMutation({
252
+ operation,
253
+ mutationPayload,
254
+ context: { entityType: 'customer_accounts:user' },
255
+ })
256
+ },
257
+ [runMutation],
258
+ )
259
+
260
+ React.useEffect(() => {
261
+ let cancelled = false
262
+ fetchRoleFilterOptions().then((opts) => {
263
+ if (!cancelled) setRoleOptions(opts)
264
+ })
265
+ return () => { cancelled = true }
266
+ }, [])
267
+
268
+ const filters = React.useMemo<FilterDef[]>(() => [
269
+ {
270
+ id: 'status',
271
+ label: t('customer_accounts.admin.filters.status', 'Status'),
272
+ type: 'select',
273
+ options: [
274
+ { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },
275
+ { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },
276
+ ],
277
+ },
278
+ {
279
+ id: 'roleId',
280
+ label: t('customer_accounts.admin.filters.role', 'Role'),
281
+ type: 'select',
282
+ options: roleOptions,
283
+ },
284
+ ], [roleOptions, t])
285
+
286
+ const queryParams = React.useMemo(() => {
287
+ const params = new URLSearchParams()
288
+ params.set('page', String(page))
289
+ params.set('pageSize', String(pageSize))
290
+ if (search.trim()) params.set('search', search.trim())
291
+ const status = filterValues.status
292
+ if (typeof status === 'string' && status.trim()) params.set('status', status)
293
+ const roleId = filterValues.roleId
294
+ if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)
295
+ return params.toString()
296
+ }, [filterValues, page, pageSize, search])
297
+
298
+ React.useEffect(() => {
299
+ let cancelled = false
300
+ async function load() {
301
+ setIsLoading(true)
302
+ try {
303
+ const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }
304
+ const payload = await readApiResultOrThrow<UsersResponse>(
305
+ `/api/customer_accounts/admin/users?${queryParams}`,
306
+ undefined,
307
+ { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },
308
+ )
309
+ if (cancelled) return
310
+ const items = Array.isArray(payload?.items) ? payload.items : []
311
+ setRows(items)
312
+ setTotal(typeof payload?.total === 'number' ? payload.total : items.length)
313
+ setTotalPages(typeof payload?.totalPages === 'number' ? payload.totalPages : 1)
314
+ } catch (err) {
315
+ if (!cancelled) {
316
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')
317
+ flash(message, 'error')
318
+ }
319
+ } finally {
320
+ if (!cancelled) setIsLoading(false)
321
+ }
322
+ }
323
+ load()
324
+ return () => { cancelled = true }
325
+ }, [queryParams, reloadToken, t])
326
+
327
+ const handleToggleActive = React.useCallback(async (user: UserRow) => {
328
+ const nextActive = !user.isActive
329
+ const actionLabel = nextActive
330
+ ? t('customer_accounts.admin.actions.activate', 'Activate')
331
+ : t('customer_accounts.admin.actions.deactivate', 'Deactivate')
332
+ const confirmed = await confirm({
333
+ title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user "{{name}}"?', {
334
+ action: actionLabel,
335
+ name: user.displayName || user.email,
336
+ }),
337
+ variant: nextActive ? 'default' : 'destructive',
338
+ })
339
+ if (!confirmed) return
340
+ try {
341
+ await runMutationWithContext(async () => {
342
+ const call = await apiCall(
343
+ `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,
344
+ {
345
+ method: 'PUT',
346
+ headers: { 'content-type': 'application/json' },
347
+ body: JSON.stringify({ isActive: nextActive }),
348
+ },
349
+ )
350
+ if (!call.ok) {
351
+ flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')
352
+ return
353
+ }
354
+ flash(
355
+ nextActive
356
+ ? t('customer_accounts.admin.flash.activated', 'User activated')
357
+ : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),
358
+ 'success',
359
+ )
360
+ setReloadToken((token) => token + 1)
361
+ }, { userId: user.id, isActive: nextActive })
362
+ } catch (err) {
363
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')
364
+ flash(message, 'error')
365
+ }
366
+ }, [confirm, runMutationWithContext, t])
367
+
368
+ const handleDelete = React.useCallback(async (user: UserRow) => {
369
+ const confirmed = await confirm({
370
+ title: t('customer_accounts.admin.confirm.delete', 'Delete user "{{name}}"?', {
371
+ name: user.displayName || user.email,
372
+ }),
373
+ variant: 'destructive',
374
+ })
375
+ if (!confirmed) return
376
+ try {
377
+ await runMutationWithContext(async () => {
378
+ const call = await apiCall(
379
+ `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,
380
+ { method: 'DELETE' },
381
+ )
382
+ if (!call.ok) {
383
+ flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')
384
+ return
385
+ }
386
+ flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')
387
+ setReloadToken((token) => token + 1)
388
+ }, { id: user.id })
389
+ } catch (err) {
390
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')
391
+ flash(message, 'error')
392
+ }
393
+ }, [confirm, runMutationWithContext, t])
394
+
395
+ const handleFiltersApply = React.useCallback((values: FilterValues) => {
396
+ setFilterValues(values)
397
+ setPage(1)
398
+ }, [])
399
+
400
+ const handleFiltersClear = React.useCallback(() => {
401
+ setFilterValues({})
402
+ setPage(1)
403
+ }, [])
404
+
405
+ const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {
406
+ const noValue = <span className="text-muted-foreground text-sm">-</span>
407
+ return [
408
+ {
409
+ accessorKey: 'displayName',
410
+ header: t('customer_accounts.admin.columns.displayName', 'Name'),
411
+ cell: ({ row }) => (
412
+ <Link
413
+ href={`/backend/customer_accounts/${row.original.id}`}
414
+ className="font-medium hover:underline"
415
+ >
416
+ {row.original.displayName || row.original.email}
417
+ </Link>
418
+ ),
419
+ },
420
+ {
421
+ accessorKey: 'email',
422
+ header: t('customer_accounts.admin.columns.email', 'Email'),
423
+ },
424
+ {
425
+ accessorKey: 'emailVerified',
426
+ header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),
427
+ cell: ({ row }) => (
428
+ <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
429
+ row.original.emailVerified
430
+ ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
431
+ : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
432
+ }`}>
433
+ {row.original.emailVerified
434
+ ? t('customer_accounts.admin.verified', 'Yes')
435
+ : t('customer_accounts.admin.unverified', 'No')}
436
+ </span>
437
+ ),
438
+ },
439
+ {
440
+ accessorKey: 'isActive',
441
+ header: t('customer_accounts.admin.columns.status', 'Status'),
442
+ cell: ({ row }) => (
443
+ <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
444
+ row.original.isActive
445
+ ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
446
+ : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
447
+ }`}>
448
+ {row.original.isActive
449
+ ? t('customer_accounts.admin.active', 'Active')
450
+ : t('customer_accounts.admin.inactive', 'Inactive')}
451
+ </span>
452
+ ),
453
+ },
454
+ {
455
+ accessorKey: 'lastLoginAt',
456
+ header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),
457
+ cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,
458
+ },
459
+ {
460
+ accessorKey: 'roles',
461
+ header: t('customer_accounts.admin.columns.roles', 'Roles'),
462
+ cell: ({ row }) => {
463
+ const roles = row.original.roles
464
+ if (!roles || !roles.length) return noValue
465
+ return <span className="text-sm">{roles.map((r) => r.name).join(', ')}</span>
466
+ },
467
+ },
468
+ {
469
+ accessorKey: 'createdAt',
470
+ header: t('customer_accounts.admin.columns.createdAt', 'Created'),
471
+ cell: ({ row }) => formatDate(row.original.createdAt, '-'),
472
+ },
473
+ ]
474
+ }, [t])
475
+
476
+ return (
477
+ <Page>
478
+ <PageBody>
479
+ <DataTable<UserRow>
480
+ title={t('customer_accounts.admin.title', 'Customer Accounts')}
481
+ actions={(
482
+ <Button onClick={() => setCreateDialogOpen(true)}>
483
+ {t('customer_accounts.admin.actions.createUser', 'Create User')}
484
+ </Button>
485
+ )}
486
+ columns={columns}
487
+ data={rows}
488
+ searchValue={search}
489
+ onSearchChange={(value) => { setSearch(value); setPage(1) }}
490
+ searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}
491
+ filters={filters}
492
+ filterValues={filterValues}
493
+ onFiltersApply={handleFiltersApply}
494
+ onFiltersClear={handleFiltersClear}
495
+ perspective={{ tableId: 'customer_accounts.admin.users' }}
496
+ onRowClick={(row) => router.push(`/backend/customer_accounts/${row.id}`)}
497
+ rowActions={(row) => (
498
+ <RowActions
499
+ items={[
500
+ {
501
+ id: 'view',
502
+ label: t('customer_accounts.admin.actions.view', 'View'),
503
+ onSelect: () => { router.push(`/backend/customer_accounts/${row.id}`) },
504
+ },
505
+ {
506
+ id: 'toggle-active',
507
+ label: row.isActive
508
+ ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')
509
+ : t('customer_accounts.admin.actions.activate', 'Activate'),
510
+ onSelect: () => { void handleToggleActive(row) },
511
+ },
512
+ {
513
+ id: 'delete',
514
+ label: t('customer_accounts.admin.actions.delete', 'Delete'),
515
+ destructive: true,
516
+ onSelect: () => { void handleDelete(row) },
517
+ },
518
+ ]}
519
+ />
520
+ )}
521
+ pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}
522
+ isLoading={isLoading}
523
+ />
524
+ <CreateUserDialog
525
+ open={createDialogOpen}
526
+ onOpenChange={setCreateDialogOpen}
527
+ roleOptions={roleOptions}
528
+ onCreated={() => setReloadToken((token) => token + 1)}
529
+ onRunMutation={runMutationWithContext}
530
+ />
531
+ </PageBody>
532
+ {ConfirmDialogElement}
533
+ </Page>
534
+ )
535
+ }
@@ -0,0 +1,22 @@
1
+ export const entities = [
2
+ {
3
+ id: 'customer_accounts:customer_user',
4
+ label: 'Customer User',
5
+ description: 'A customer-facing user account linked to CRM person/company entities.',
6
+ labelField: 'displayName',
7
+ showInSidebar: false,
8
+ defaultEditor: false,
9
+ fields: [],
10
+ },
11
+ {
12
+ id: 'customer_accounts:customer_role',
13
+ label: 'Customer Role',
14
+ description: 'A role definition for customer portal access control.',
15
+ labelField: 'name',
16
+ showInSidebar: false,
17
+ defaultEditor: false,
18
+ fields: [],
19
+ },
20
+ ]
21
+
22
+ export default entities
@@ -0,0 +1,117 @@
1
+ import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { CustomerUser } from './entities'
4
+
5
+ type EntityRecord = Record<string, unknown> & { id: string }
6
+ type EnricherScope = EnricherContext & { em: EntityManager }
7
+
8
+ type PersonAccountEnrichment = {
9
+ _customer_accounts: {
10
+ hasAccount: boolean
11
+ userId?: string
12
+ isActive?: boolean
13
+ lastLoginAt?: string | null
14
+ }
15
+ }
16
+
17
+ type CompanyUserCountEnrichment = {
18
+ _customer_accounts: {
19
+ userCount: number
20
+ }
21
+ }
22
+
23
+ const PERSON_FALLBACK = { _customer_accounts: { hasAccount: false } }
24
+ const COMPANY_FALLBACK = { _customer_accounts: { userCount: 0 } }
25
+
26
+ const personAccountStatusEnricher: ResponseEnricher<EntityRecord, PersonAccountEnrichment> = {
27
+ id: 'customer_accounts.person-account-status',
28
+ targetEntity: 'customers.person',
29
+ features: ['customer_accounts.view'],
30
+ priority: 10,
31
+ timeout: 2000,
32
+ critical: false,
33
+ fallback: PERSON_FALLBACK,
34
+
35
+ async enrichOne(record, context: EnricherScope) {
36
+ return (await this.enrichMany!([record], context))[0]
37
+ },
38
+
39
+ async enrichMany(records, context: EnricherScope) {
40
+ if (records.length === 0) return records as (EntityRecord & PersonAccountEnrichment)[]
41
+
42
+ const em = context.em.fork()
43
+ const recordIds = records.map((r) => r.id)
44
+
45
+ const users = await em.find(CustomerUser, {
46
+ personEntityId: { $in: recordIds },
47
+ tenantId: context.tenantId,
48
+ deletedAt: null,
49
+ })
50
+
51
+ const userByPersonId = new Map<string, CustomerUser>()
52
+ for (const user of users) {
53
+ if (user.personEntityId) {
54
+ userByPersonId.set(user.personEntityId, user)
55
+ }
56
+ }
57
+
58
+ return records.map((record) => {
59
+ const user = userByPersonId.get(record.id)
60
+ if (!user) {
61
+ return { ...record, _customer_accounts: { hasAccount: false } }
62
+ }
63
+ return {
64
+ ...record,
65
+ _customer_accounts: {
66
+ hasAccount: true,
67
+ userId: user.id,
68
+ isActive: user.isActive,
69
+ lastLoginAt: user.lastLoginAt?.toISOString() ?? null,
70
+ },
71
+ }
72
+ })
73
+ },
74
+ }
75
+
76
+ const companyUserCountEnricher: ResponseEnricher<EntityRecord, CompanyUserCountEnrichment> = {
77
+ id: 'customer_accounts.company-user-count',
78
+ targetEntity: 'customers.company',
79
+ features: ['customer_accounts.view'],
80
+ priority: 10,
81
+ timeout: 2000,
82
+ critical: false,
83
+ fallback: COMPANY_FALLBACK,
84
+
85
+ async enrichOne(record, context: EnricherScope) {
86
+ return (await this.enrichMany!([record], context))[0]
87
+ },
88
+
89
+ async enrichMany(records, context: EnricherScope) {
90
+ if (records.length === 0) return records as (EntityRecord & CompanyUserCountEnrichment)[]
91
+
92
+ const em = context.em.fork()
93
+ const recordIds = records.map((r) => r.id)
94
+
95
+ const users = await em.find(CustomerUser, {
96
+ customerEntityId: { $in: recordIds },
97
+ tenantId: context.tenantId,
98
+ deletedAt: null,
99
+ })
100
+
101
+ const countByCompanyId = new Map<string, number>()
102
+ for (const user of users) {
103
+ if (user.customerEntityId) {
104
+ countByCompanyId.set(user.customerEntityId, (countByCompanyId.get(user.customerEntityId) ?? 0) + 1)
105
+ }
106
+ }
107
+
108
+ return records.map((record) => ({
109
+ ...record,
110
+ _customer_accounts: {
111
+ userCount: countByCompanyId.get(record.id) ?? 0,
112
+ },
113
+ }))
114
+ },
115
+ }
116
+
117
+ export const enrichers: ResponseEnricher[] = [personAccountStatusEnricher, companyUserCountEnricher]