@open-mercato/core 0.6.4-develop.4210.1.d412061cfe → 0.6.4-develop.4236.1.9fa6806b34

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 (370) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_time_entry/index.js +37 -0
  3. package/dist/generated/entities/staff_time_entry/index.js.map +7 -0
  4. package/dist/generated/entities/staff_time_entry_segment/index.js +23 -0
  5. package/dist/generated/entities/staff_time_entry_segment/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +35 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +7 -0
  8. package/dist/generated/entities/staff_time_project_member/index.js +29 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +7 -0
  10. package/dist/generated/entities.ids.generated.js +5 -1
  11. package/dist/generated/entities.ids.generated.js.map +2 -2
  12. package/dist/generated/entity-fields-registry.js +64 -0
  13. package/dist/generated/entity-fields-registry.js.map +2 -2
  14. package/dist/helpers/integration/timesheetFixtures.js +50 -0
  15. package/dist/helpers/integration/timesheetFixtures.js.map +7 -0
  16. package/dist/modules/attachments/api/library/[id]/route.js +20 -16
  17. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/route.js +18 -14
  19. package/dist/modules/attachments/api/route.js.map +2 -2
  20. package/dist/modules/auth/api/roles/acl/route.js +10 -4
  21. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  22. package/dist/modules/auth/api/sidebar/preferences/route.js +27 -20
  23. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  24. package/dist/modules/auth/api/users/acl/route.js +16 -11
  25. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +87 -71
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/services/sidebarPreferencesService.js +39 -30
  29. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  30. package/dist/modules/catalog/commands/categories.js +61 -12
  31. package/dist/modules/catalog/commands/categories.js.map +2 -2
  32. package/dist/modules/catalog/commands/products.js +79 -54
  33. package/dist/modules/catalog/commands/products.js.map +2 -2
  34. package/dist/modules/catalog/commands/variants.js +29 -16
  35. package/dist/modules/catalog/commands/variants.js.map +2 -2
  36. package/dist/modules/currencies/commands/currencies.js +15 -8
  37. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users.js +27 -26
  39. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/password/reset-confirm.js +5 -5
  41. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +11 -10
  43. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  44. package/dist/modules/customers/commands/addresses.js +35 -21
  45. package/dist/modules/customers/commands/addresses.js.map +2 -2
  46. package/dist/modules/customers/commands/companies.js +163 -162
  47. package/dist/modules/customers/commands/companies.js.map +2 -2
  48. package/dist/modules/customers/commands/deals.js +3 -4
  49. package/dist/modules/customers/commands/deals.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +19 -22
  51. package/dist/modules/customers/commands/interactions.js.map +2 -2
  52. package/dist/modules/customers/commands/people.js +18 -15
  53. package/dist/modules/customers/commands/people.js.map +2 -2
  54. package/dist/modules/customers/commands/personCompanyLinks.js +105 -94
  55. package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
  56. package/dist/modules/customers/commands/pipeline-stages.js +30 -23
  57. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  58. package/dist/modules/customers/commands/pipelines.js +27 -20
  59. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  60. package/dist/modules/customers/commands/tags.js +13 -5
  61. package/dist/modules/customers/commands/tags.js.map +2 -2
  62. package/dist/modules/dashboards/api/users/widgets/route.js +0 -1
  63. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  64. package/dist/modules/dashboards/api/widgets/data/route.js +29 -1
  65. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  66. package/dist/modules/data_sync/lib/sync-engine.js +4 -4
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  68. package/dist/modules/data_sync/lib/sync-run-service.js +51 -27
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  70. package/dist/modules/directory/commands/organizations.js +192 -158
  71. package/dist/modules/directory/commands/organizations.js.map +3 -3
  72. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +22 -16
  73. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  74. package/dist/modules/messages/commands/messages.js +77 -75
  75. package/dist/modules/messages/commands/messages.js.map +2 -2
  76. package/dist/modules/messages/commands/shared.js +132 -132
  77. package/dist/modules/messages/commands/shared.js.map +2 -2
  78. package/dist/modules/perspectives/api/[tableId]/route.js +37 -26
  79. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  80. package/dist/modules/resources/commands/resources.js +125 -117
  81. package/dist/modules/resources/commands/resources.js.map +2 -2
  82. package/dist/modules/resources/commands/tags.js +7 -3
  83. package/dist/modules/resources/commands/tags.js.map +2 -2
  84. package/dist/modules/sales/api/quotes/send/route.js +12 -11
  85. package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
  86. package/dist/modules/sales/commands/documents.js +629 -478
  87. package/dist/modules/sales/commands/documents.js.map +2 -2
  88. package/dist/modules/sales/commands/payments.js +146 -146
  89. package/dist/modules/sales/commands/payments.js.map +2 -2
  90. package/dist/modules/sales/commands/returns.js +68 -60
  91. package/dist/modules/sales/commands/returns.js.map +2 -2
  92. package/dist/modules/staff/acl.js +10 -1
  93. package/dist/modules/staff/acl.js.map +2 -2
  94. package/dist/modules/staff/analytics.js +33 -0
  95. package/dist/modules/staff/analytics.js.map +7 -0
  96. package/dist/modules/staff/api/guards.js +31 -0
  97. package/dist/modules/staff/api/guards.js.map +7 -0
  98. package/dist/modules/staff/api/interceptors.js +96 -0
  99. package/dist/modules/staff/api/interceptors.js.map +7 -0
  100. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +170 -0
  101. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +7 -0
  102. package/dist/modules/staff/api/timesheets/my-projects/route.js +103 -0
  103. package/dist/modules/staff/api/timesheets/my-projects/route.js.map +7 -0
  104. package/dist/modules/staff/api/timesheets/projects/kpis/route.js +147 -0
  105. package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +7 -0
  106. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +171 -0
  107. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +7 -0
  108. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +180 -0
  109. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +7 -0
  110. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +155 -0
  111. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +7 -0
  112. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +173 -0
  113. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +7 -0
  114. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +260 -0
  115. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +7 -0
  116. package/dist/modules/staff/api/timesheets/time-entries/route.js +188 -0
  117. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +7 -0
  118. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +159 -0
  119. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +7 -0
  120. package/dist/modules/staff/api/timesheets/time-projects/route.js +230 -0
  121. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +7 -0
  122. package/dist/modules/staff/backend/staff/timesheets/page.js +710 -0
  123. package/dist/modules/staff/backend/staff/timesheets/page.js.map +7 -0
  124. package/dist/modules/staff/backend/staff/timesheets/page.meta.js +22 -0
  125. package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +7 -0
  126. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +125 -0
  127. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +7 -0
  128. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +16 -0
  129. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +7 -0
  130. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +418 -0
  131. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +7 -0
  132. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +16 -0
  133. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +7 -0
  134. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +79 -0
  135. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +7 -0
  136. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +16 -0
  137. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +7 -0
  138. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +602 -0
  139. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +7 -0
  140. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +25 -0
  141. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +7 -0
  142. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +123 -0
  143. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +7 -0
  144. package/dist/modules/staff/cli.js +38 -1
  145. package/dist/modules/staff/cli.js.map +2 -2
  146. package/dist/modules/staff/commands/index.js +2 -0
  147. package/dist/modules/staff/commands/index.js.map +2 -2
  148. package/dist/modules/staff/commands/leave-requests.js +30 -28
  149. package/dist/modules/staff/commands/leave-requests.js.map +3 -3
  150. package/dist/modules/staff/commands/team-members.js +21 -20
  151. package/dist/modules/staff/commands/team-members.js.map +2 -2
  152. package/dist/modules/staff/commands/timesheets-entries.js +409 -0
  153. package/dist/modules/staff/commands/timesheets-entries.js.map +7 -0
  154. package/dist/modules/staff/commands/timesheets-projects.js +618 -0
  155. package/dist/modules/staff/commands/timesheets-projects.js.map +7 -0
  156. package/dist/modules/staff/data/enrichers.js +104 -0
  157. package/dist/modules/staff/data/enrichers.js.map +7 -0
  158. package/dist/modules/staff/data/entities.js +226 -1
  159. package/dist/modules/staff/data/entities.js.map +2 -2
  160. package/dist/modules/staff/data/validators.js +113 -1
  161. package/dist/modules/staff/data/validators.js.map +2 -2
  162. package/dist/modules/staff/events.js +13 -1
  163. package/dist/modules/staff/events.js.map +2 -2
  164. package/dist/modules/staff/lib/crud.js +7 -1
  165. package/dist/modules/staff/lib/crud.js.map +2 -2
  166. package/dist/modules/staff/lib/staffMemberResolver.js +15 -0
  167. package/dist/modules/staff/lib/staffMemberResolver.js.map +7 -0
  168. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js +60 -0
  169. package/dist/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.js.map +7 -0
  170. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js +260 -0
  171. package/dist/modules/staff/lib/timesheets-projects/computeProjectsKpis.js.map +7 -0
  172. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js +41 -0
  173. package/dist/modules/staff/lib/timesheets-projects/dateBuckets.js.map +7 -0
  174. package/dist/modules/staff/lib/timesheets-projects/initials.js +10 -0
  175. package/dist/modules/staff/lib/timesheets-projects/initials.js.map +7 -0
  176. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js +12 -0
  177. package/dist/modules/staff/lib/timesheets-projects/kpiMath.js.map +7 -0
  178. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js +55 -0
  179. package/dist/modules/staff/lib/timesheets-projects/listProjectMembersPreview.js.map +7 -0
  180. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js +66 -0
  181. package/dist/modules/staff/lib/timesheets-projects-ui/HoursSparkline.js.map +7 -0
  182. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +81 -0
  183. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +7 -0
  184. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js +58 -0
  185. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.js.map +7 -0
  186. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js +152 -0
  187. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.js.map +7 -0
  188. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +37 -0
  189. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +7 -0
  190. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js +57 -0
  191. package/dist/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.js.map +7 -0
  192. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js +50 -0
  193. package/dist/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.js.map +7 -0
  194. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js +163 -0
  195. package/dist/modules/staff/lib/timesheets-ui/AddRowDropdown.js.map +7 -0
  196. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js +209 -0
  197. package/dist/modules/staff/lib/timesheets-ui/CalendarPicker.js.map +7 -0
  198. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js +52 -0
  199. package/dist/modules/staff/lib/timesheets-ui/ColorPicker.js.map +7 -0
  200. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +77 -0
  201. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +7 -0
  202. package/dist/modules/staff/lib/timesheets-ui/ListView.js +173 -0
  203. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +7 -0
  204. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js +32 -0
  205. package/dist/modules/staff/lib/timesheets-ui/ProjectColorDot.js.map +7 -0
  206. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +270 -0
  207. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +7 -0
  208. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js +57 -0
  209. package/dist/modules/staff/lib/timesheets-ui/ViewSwitcher.js.map +7 -0
  210. package/dist/modules/staff/lib/timesheets-ui/colors.js +43 -0
  211. package/dist/modules/staff/lib/timesheets-ui/colors.js.map +7 -0
  212. package/dist/modules/staff/migrations/Migration20260326135612.js +24 -0
  213. package/dist/modules/staff/migrations/Migration20260326135612.js.map +7 -0
  214. package/dist/modules/staff/migrations/Migration20260413102715.js +23 -0
  215. package/dist/modules/staff/migrations/Migration20260413102715.js.map +7 -0
  216. package/dist/modules/staff/migrations/Migration20260413111602.js +13 -0
  217. package/dist/modules/staff/migrations/Migration20260413111602.js.map +7 -0
  218. package/dist/modules/staff/migrations/Migration20260511112759.js +19 -0
  219. package/dist/modules/staff/migrations/Migration20260511112759.js.map +7 -0
  220. package/dist/modules/staff/search.js +35 -0
  221. package/dist/modules/staff/search.js.map +2 -2
  222. package/dist/modules/staff/setup.js +15 -1
  223. package/dist/modules/staff/setup.js.map +2 -2
  224. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js +16 -0
  225. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.js.map +7 -0
  226. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js +126 -0
  227. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js.map +7 -0
  228. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js +26 -0
  229. package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.js.map +7 -0
  230. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js +15 -0
  231. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/config.js.map +7 -0
  232. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +238 -0
  233. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +7 -0
  234. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js +26 -0
  235. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +145 -0
  237. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection-table.js +12 -0
  239. package/dist/modules/staff/widgets/injection-table.js.map +7 -0
  240. package/dist/modules/sync_excel/api/import/route.js +19 -17
  241. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  242. package/dist/modules/translations/commands/translations.js +22 -19
  243. package/dist/modules/translations/commands/translations.js.map +2 -2
  244. package/generated/entities/staff_time_entry/index.ts +17 -0
  245. package/generated/entities/staff_time_entry_segment/index.ts +10 -0
  246. package/generated/entities/staff_time_project/index.ts +16 -0
  247. package/generated/entities/staff_time_project_member/index.ts +13 -0
  248. package/generated/entities.ids.generated.ts +5 -1
  249. package/generated/entity-fields-registry.ts +64 -0
  250. package/package.json +7 -7
  251. package/src/helpers/integration/timesheetFixtures.ts +61 -0
  252. package/src/modules/attachments/api/library/[id]/route.ts +24 -17
  253. package/src/modules/attachments/api/route.ts +20 -14
  254. package/src/modules/auth/api/roles/acl/route.ts +11 -5
  255. package/src/modules/auth/api/sidebar/preferences/route.ts +33 -24
  256. package/src/modules/auth/api/users/acl/route.ts +17 -12
  257. package/src/modules/auth/commands/users.ts +96 -80
  258. package/src/modules/auth/services/sidebarPreferencesService.ts +40 -32
  259. package/src/modules/catalog/commands/categories.ts +61 -12
  260. package/src/modules/catalog/commands/products.ts +93 -60
  261. package/src/modules/catalog/commands/variants.ts +29 -16
  262. package/src/modules/currencies/commands/currencies.ts +27 -14
  263. package/src/modules/customer_accounts/api/admin/users.ts +31 -26
  264. package/src/modules/customer_accounts/api/password/reset-confirm.ts +5 -6
  265. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +14 -13
  266. package/src/modules/customers/commands/addresses.ts +35 -23
  267. package/src/modules/customers/commands/companies.ts +166 -165
  268. package/src/modules/customers/commands/deals.ts +2 -4
  269. package/src/modules/customers/commands/interactions.ts +20 -26
  270. package/src/modules/customers/commands/people.ts +18 -15
  271. package/src/modules/customers/commands/personCompanyLinks.ts +109 -100
  272. package/src/modules/customers/commands/pipeline-stages.ts +31 -27
  273. package/src/modules/customers/commands/pipelines.ts +29 -23
  274. package/src/modules/customers/commands/tags.ts +13 -5
  275. package/src/modules/dashboards/api/users/widgets/route.ts +0 -1
  276. package/src/modules/dashboards/api/widgets/data/route.ts +36 -1
  277. package/src/modules/data_sync/lib/sync-engine.ts +4 -5
  278. package/src/modules/data_sync/lib/sync-run-service.ts +57 -28
  279. package/src/modules/directory/commands/organizations.ts +203 -166
  280. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +26 -18
  281. package/src/modules/messages/commands/messages.ts +82 -80
  282. package/src/modules/messages/commands/shared.ts +138 -133
  283. package/src/modules/perspectives/api/[tableId]/route.ts +38 -27
  284. package/src/modules/resources/commands/resources.ts +127 -117
  285. package/src/modules/resources/commands/tags.ts +7 -3
  286. package/src/modules/sales/api/quotes/send/route.ts +17 -12
  287. package/src/modules/sales/commands/documents.ts +673 -481
  288. package/src/modules/sales/commands/payments.ts +158 -152
  289. package/src/modules/sales/commands/returns.ts +74 -63
  290. package/src/modules/staff/acl.ts +11 -0
  291. package/src/modules/staff/analytics.ts +30 -0
  292. package/src/modules/staff/api/guards.ts +59 -0
  293. package/src/modules/staff/api/interceptors.ts +122 -0
  294. package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +191 -0
  295. package/src/modules/staff/api/timesheets/my-projects/route.ts +115 -0
  296. package/src/modules/staff/api/timesheets/projects/kpis/route.ts +159 -0
  297. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +187 -0
  298. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +191 -0
  299. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +168 -0
  300. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +191 -0
  301. package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +292 -0
  302. package/src/modules/staff/api/timesheets/time-entries/route.ts +193 -0
  303. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +167 -0
  304. package/src/modules/staff/api/timesheets/time-projects/route.ts +244 -0
  305. package/src/modules/staff/backend/staff/timesheets/page.meta.ts +20 -0
  306. package/src/modules/staff/backend/staff/timesheets/page.tsx +899 -0
  307. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +12 -0
  308. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +141 -0
  309. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +12 -0
  310. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +579 -0
  311. package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +12 -0
  312. package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +90 -0
  313. package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +23 -0
  314. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +765 -0
  315. package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +138 -0
  316. package/src/modules/staff/cli.ts +40 -1
  317. package/src/modules/staff/commands/index.ts +2 -0
  318. package/src/modules/staff/commands/leave-requests.ts +37 -29
  319. package/src/modules/staff/commands/team-members.ts +25 -20
  320. package/src/modules/staff/commands/timesheets-entries.ts +504 -0
  321. package/src/modules/staff/commands/timesheets-projects.ts +699 -0
  322. package/src/modules/staff/data/enrichers.ts +134 -0
  323. package/src/modules/staff/data/entities.ts +198 -0
  324. package/src/modules/staff/data/validators.ts +129 -0
  325. package/src/modules/staff/events.ts +13 -0
  326. package/src/modules/staff/i18n/de.json +209 -1
  327. package/src/modules/staff/i18n/en.json +209 -1
  328. package/src/modules/staff/i18n/es.json +209 -1
  329. package/src/modules/staff/i18n/pl.json +209 -1
  330. package/src/modules/staff/lib/crud.ts +8 -0
  331. package/src/modules/staff/lib/staffMemberResolver.ts +22 -0
  332. package/src/modules/staff/lib/timesheets-projects/computeProjectHoursTrend.ts +89 -0
  333. package/src/modules/staff/lib/timesheets-projects/computeProjectsKpis.ts +311 -0
  334. package/src/modules/staff/lib/timesheets-projects/dateBuckets.ts +37 -0
  335. package/src/modules/staff/lib/timesheets-projects/initials.ts +6 -0
  336. package/src/modules/staff/lib/timesheets-projects/kpiMath.ts +8 -0
  337. package/src/modules/staff/lib/timesheets-projects/listProjectMembersPreview.ts +83 -0
  338. package/src/modules/staff/lib/timesheets-projects-ui/HoursSparkline.tsx +75 -0
  339. package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +110 -0
  340. package/src/modules/staff/lib/timesheets-projects-ui/ProjectMembersAvatarStack.tsx +73 -0
  341. package/src/modules/staff/lib/timesheets-projects-ui/ProjectsKpiStrip.tsx +185 -0
  342. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +53 -0
  343. package/src/modules/staff/lib/timesheets-projects-ui/ViewModeToggle.tsx +63 -0
  344. package/src/modules/staff/lib/timesheets-projects-ui/useProjectsViewMode.ts +63 -0
  345. package/src/modules/staff/lib/timesheets-ui/AddRowDropdown.tsx +188 -0
  346. package/src/modules/staff/lib/timesheets-ui/CalendarPicker.tsx +229 -0
  347. package/src/modules/staff/lib/timesheets-ui/ColorPicker.tsx +65 -0
  348. package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +99 -0
  349. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +230 -0
  350. package/src/modules/staff/lib/timesheets-ui/ProjectColorDot.tsx +40 -0
  351. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +327 -0
  352. package/src/modules/staff/lib/timesheets-ui/ViewSwitcher.tsx +60 -0
  353. package/src/modules/staff/lib/timesheets-ui/colors.ts +58 -0
  354. package/src/modules/staff/migrations/.snapshot-open-mercato.json +1148 -0
  355. package/src/modules/staff/migrations/Migration20260326135612.ts +26 -0
  356. package/src/modules/staff/migrations/Migration20260413102715.ts +25 -0
  357. package/src/modules/staff/migrations/Migration20260413111602.ts +13 -0
  358. package/src/modules/staff/migrations/Migration20260511112759.ts +21 -0
  359. package/src/modules/staff/search.ts +35 -0
  360. package/src/modules/staff/setup.ts +15 -0
  361. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/config.ts +17 -0
  362. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.tsx +158 -0
  363. package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.ts +25 -0
  364. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/config.ts +15 -0
  365. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +297 -0
  366. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.ts +25 -0
  367. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +161 -0
  368. package/src/modules/staff/widgets/injection-table.ts +10 -0
  369. package/src/modules/sync_excel/api/import/route.ts +23 -18
  370. package/src/modules/translations/commands/translations.ts +49 -41
@@ -26,6 +26,7 @@ import {
26
26
  loadCustomFieldSnapshot,
27
27
  buildCustomFieldResetMap,
28
28
  } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
29
+ import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
29
30
  import { E } from "#generated/entities.ids.generated";
30
31
  import { slugifyTagLabel } from "@open-mercato/shared/lib/utils";
31
32
  import { parseObjectLike } from "@open-mercato/shared/lib/json/parseObjectLike";
@@ -950,15 +951,15 @@ type VariantCleanupSnapshot = {
950
951
  custom: Record<string, unknown> | null;
951
952
  };
952
953
 
953
- async function deleteProductVariantsAndRelatedData(opts: {
954
- em: EntityManager;
955
- product: CatalogProduct;
956
- dataEngine: DataEngine;
957
- ctx: CommandRuntimeContext;
958
- }): Promise<void> {
959
- const { em, product, dataEngine, ctx } = opts;
954
+ async function collectProductVariantCleanup(
955
+ em: EntityManager,
956
+ product: CatalogProduct,
957
+ ): Promise<{
958
+ variants: CatalogProductVariant[];
959
+ cleanupEntries: VariantCleanupSnapshot[];
960
+ }> {
960
961
  const variants = await em.find(CatalogProductVariant, { product });
961
- if (!variants.length) return;
962
+ if (!variants.length) return { variants: [], cleanupEntries: [] };
962
963
  const cleanupEntries: VariantCleanupSnapshot[] = await Promise.all(
963
964
  variants.map(async (variant) => {
964
965
  const custom = await loadCustomFieldSnapshot(em, {
@@ -975,6 +976,14 @@ async function deleteProductVariantsAndRelatedData(opts: {
975
976
  };
976
977
  }),
977
978
  );
979
+ return { variants, cleanupEntries };
980
+ }
981
+
982
+ async function removeProductVariants(
983
+ em: EntityManager,
984
+ variants: CatalogProductVariant[],
985
+ ): Promise<void> {
986
+ if (!variants.length) return;
978
987
  const variantIds = variants.map((variant) => variant.id);
979
988
  if (variantIds.length) {
980
989
  await em.nativeDelete(CatalogProductPrice, {
@@ -984,7 +993,14 @@ async function deleteProductVariantsAndRelatedData(opts: {
984
993
  for (const variant of variants) {
985
994
  em.remove(variant);
986
995
  }
987
- await em.flush();
996
+ }
997
+
998
+ async function emitProductVariantCleanupSideEffects(opts: {
999
+ dataEngine: DataEngine;
1000
+ ctx: CommandRuntimeContext;
1001
+ cleanupEntries: VariantCleanupSnapshot[];
1002
+ }): Promise<void> {
1003
+ const { dataEngine, ctx, cleanupEntries } = opts;
988
1004
  for (const cleanup of cleanupEntries) {
989
1005
  if (!cleanup.custom) continue;
990
1006
  const resetValues = buildCustomFieldResetMap(cleanup.custom, undefined);
@@ -1307,15 +1323,16 @@ const createProductCommand: CommandHandler<
1307
1323
  }
1308
1324
  em.persist(record);
1309
1325
  try {
1310
- await em.flush();
1311
- } catch (error) {
1312
- await rethrowProductUniqueConstraint(error);
1313
- }
1314
- await syncOffers(em, record, parsed.offers);
1315
- await syncCategoryAssignments(em, record, parsed.categoryIds);
1316
- await syncProductTags(em, record, parsed.tags);
1317
- try {
1318
- await em.flush();
1326
+ await withAtomicFlush(
1327
+ em,
1328
+ [
1329
+ () => em.flush(),
1330
+ () => syncOffers(em, record, parsed.offers),
1331
+ () => syncCategoryAssignments(em, record, parsed.categoryIds),
1332
+ () => syncProductTags(em, record, parsed.tags),
1333
+ ],
1334
+ { transaction: true },
1335
+ );
1319
1336
  } catch (error) {
1320
1337
  await rethrowProductUniqueConstraint(error);
1321
1338
  }
@@ -1614,15 +1631,16 @@ const updateProductCommand: CommandHandler<
1614
1631
  record.isConfigurable = parsed.isConfigurable;
1615
1632
  if (parsed.isActive !== undefined) record.isActive = parsed.isActive;
1616
1633
  try {
1617
- await em.flush();
1618
- } catch (error) {
1619
- await rethrowProductUniqueConstraint(error);
1620
- }
1621
- await syncOffers(em, record, parsed.offers);
1622
- await syncCategoryAssignments(em, record, parsed.categoryIds);
1623
- await syncProductTags(em, record, parsed.tags);
1624
- try {
1625
- await em.flush();
1634
+ await withAtomicFlush(
1635
+ em,
1636
+ [
1637
+ () => em.flush(),
1638
+ () => syncOffers(em, record, parsed.offers),
1639
+ () => syncCategoryAssignments(em, record, parsed.categoryIds),
1640
+ () => syncProductTags(em, record, parsed.tags),
1641
+ ],
1642
+ { transaction: true },
1643
+ );
1626
1644
  } catch (error) {
1627
1645
  await rethrowProductUniqueConstraint(error);
1628
1646
  }
@@ -1732,16 +1750,16 @@ const updateProductCommand: CommandHandler<
1732
1750
  ensureTenantScope(ctx, before.tenantId);
1733
1751
  ensureOrganizationScope(ctx, before.organizationId);
1734
1752
  applyProductSnapshot(em, record, before);
1735
- await em.flush();
1736
-
1737
- const relationEm = em.fork();
1738
- const relationRecord = await findOneWithDecryption(relationEm, CatalogProduct, { id: before.id });
1739
- if (relationRecord) {
1740
- await restoreOffersFromSnapshot(relationEm, relationRecord, before.offers);
1741
- await syncCategoryAssignments(relationEm, relationRecord, before.categoryIds);
1742
- await syncProductTags(relationEm, relationRecord, before.tags);
1743
- await relationEm.flush();
1744
- }
1753
+ await withAtomicFlush(
1754
+ em,
1755
+ [
1756
+ () => em.flush(),
1757
+ () => restoreOffersFromSnapshot(em, record, before.offers),
1758
+ () => syncCategoryAssignments(em, record, before.categoryIds),
1759
+ () => syncProductTags(em, record, before.tags),
1760
+ ],
1761
+ { transaction: true },
1762
+ );
1745
1763
  const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
1746
1764
  const resetValues = buildCustomFieldResetMap(
1747
1765
  before.custom ?? undefined,
@@ -1803,23 +1821,38 @@ const deleteProductCommand: CommandHandler<
1803
1821
  ensureTenantScope(ctx, record.tenantId);
1804
1822
  ensureOrganizationScope(ctx, record.organizationId);
1805
1823
  const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
1806
- await deleteProductVariantsAndRelatedData({
1824
+ const { variants, cleanupEntries } = await collectProductVariantCleanup(
1807
1825
  em,
1808
- product: record,
1826
+ record,
1827
+ );
1828
+ await withAtomicFlush(
1829
+ em,
1830
+ [
1831
+ () => removeProductVariants(em, variants),
1832
+ async () => {
1833
+ await em.nativeDelete(CatalogProductPrice, { product: record.id });
1834
+ },
1835
+ async () => {
1836
+ const templateToRemove = await resolveOptionSchemaTemplateForRemoval(
1837
+ em,
1838
+ record,
1839
+ );
1840
+ if (templateToRemove) {
1841
+ record.optionSchemaTemplate = null;
1842
+ em.remove(templateToRemove);
1843
+ }
1844
+ },
1845
+ () => {
1846
+ em.remove(record);
1847
+ },
1848
+ ],
1849
+ { transaction: true },
1850
+ );
1851
+ await emitProductVariantCleanupSideEffects({
1809
1852
  dataEngine,
1810
1853
  ctx,
1854
+ cleanupEntries,
1811
1855
  });
1812
- await em.nativeDelete(CatalogProductPrice, { product: record.id });
1813
- const templateToRemove = await resolveOptionSchemaTemplateForRemoval(
1814
- em,
1815
- record,
1816
- );
1817
- if (templateToRemove) {
1818
- record.optionSchemaTemplate = null;
1819
- em.remove(templateToRemove);
1820
- }
1821
- em.remove(record);
1822
- await em.flush();
1823
1856
  if (snapshot?.custom && Object.keys(snapshot.custom).length) {
1824
1857
  const resetValues = buildCustomFieldResetMap(snapshot.custom, undefined);
1825
1858
  if (Object.keys(resetValues).length) {
@@ -1908,16 +1941,16 @@ const deleteProductCommand: CommandHandler<
1908
1941
  ensureTenantScope(ctx, before.tenantId);
1909
1942
  ensureOrganizationScope(ctx, before.organizationId);
1910
1943
  applyProductSnapshot(em, record, before);
1911
- await em.flush();
1912
-
1913
- const relationEm = em.fork();
1914
- const relationRecord = await findOneWithDecryption(relationEm, CatalogProduct, { id: before.id });
1915
- if (relationRecord) {
1916
- await restoreOffersFromSnapshot(relationEm, relationRecord, before.offers);
1917
- await syncCategoryAssignments(relationEm, relationRecord, before.categoryIds);
1918
- await syncProductTags(relationEm, relationRecord, before.tags);
1919
- await relationEm.flush();
1920
- }
1944
+ await withAtomicFlush(
1945
+ em,
1946
+ [
1947
+ () => em.flush(),
1948
+ () => restoreOffersFromSnapshot(em, record, before.offers),
1949
+ () => syncCategoryAssignments(em, record, before.categoryIds),
1950
+ () => syncProductTags(em, record, before.tags),
1951
+ ],
1952
+ { transaction: true },
1953
+ );
1921
1954
  const dataEngine = ctx.container.resolve("dataEngine") as DataEngine;
1922
1955
  if (before.custom && Object.keys(before.custom).length) {
1923
1956
  await setCustomFieldsIfAny({
@@ -6,6 +6,7 @@ import { UniqueConstraintViolationException } from '@mikro-orm/core'
6
6
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
7
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
8
  import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
9
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
9
10
  import { E } from '#generated/entities.ids.generated'
10
11
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
12
  import {
@@ -588,21 +589,25 @@ const createVariantCommand: CommandHandler<VariantCreateInput, { variantId: stri
588
589
  updatedAt: now,
589
590
  })
590
591
  em.persist(record)
592
+ let previousDefaultVariantId: string | null = null
591
593
  try {
592
- await em.flush()
594
+ await withAtomicFlush(
595
+ em,
596
+ [
597
+ () => em.flush(),
598
+ async () => {
599
+ if (record.isDefault) {
600
+ previousDefaultVariantId = await enforceSingleDefaultVariant(em, record)
601
+ await em.flush()
602
+ }
603
+ },
604
+ () => aggregateVariantMediaToProduct(em, record),
605
+ ],
606
+ { transaction: true }
607
+ )
593
608
  } catch (error) {
594
609
  await rethrowVariantUniqueConstraint(error)
595
610
  }
596
- let previousDefaultVariantId: string | null = null
597
- if (record.isDefault) {
598
- previousDefaultVariantId = await enforceSingleDefaultVariant(em, record)
599
- try {
600
- await em.flush()
601
- } catch (error) {
602
- await rethrowVariantUniqueConstraint(error)
603
- }
604
- }
605
- await aggregateVariantMediaToProduct(em, record)
606
611
  await setCustomFieldsIfAny({
607
612
  dataEngine: ctx.container.resolve('dataEngine'),
608
613
  entityId: E.catalog.catalog_product_variant,
@@ -753,15 +758,23 @@ const updateVariantCommand: CommandHandler<VariantUpdateInput, { variantId: stri
753
758
  }
754
759
 
755
760
  let previousDefaultVariantId: string | null = null
756
- if (parsed.isDefault === true) {
757
- previousDefaultVariantId = await enforceSingleDefaultVariant(em, record)
758
- }
759
761
  try {
760
- await em.flush()
762
+ await withAtomicFlush(
763
+ em,
764
+ [
765
+ async () => {
766
+ if (parsed.isDefault === true) {
767
+ previousDefaultVariantId = await enforceSingleDefaultVariant(em, record)
768
+ }
769
+ },
770
+ () => em.flush(),
771
+ () => aggregateVariantMediaToProduct(em, record),
772
+ ],
773
+ { transaction: true }
774
+ )
761
775
  } catch (error) {
762
776
  await rethrowVariantUniqueConstraint(error)
763
777
  }
764
- await aggregateVariantMediaToProduct(em, record)
765
778
  if (custom && Object.keys(custom).length) {
766
779
  await setCustomFieldsIfAny({
767
780
  dataEngine: ctx.container.resolve('dataEngine'),
@@ -2,6 +2,7 @@ import { registerCommand } from '@open-mercato/shared/lib/commands'
2
2
  import type { CommandHandler } from '@open-mercato/shared/lib/commands'
3
3
  import { buildChanges, requireId, emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
4
4
  import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
5
6
  import type { EntityManager } from '@mikro-orm/postgresql'
6
7
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
8
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
@@ -119,13 +120,19 @@ const createCurrencyCommand: CommandHandler<CurrencyCreateInput, { currencyId: s
119
120
  updatedAt: now,
120
121
  })
121
122
  em.persist(record)
122
-
123
- // Enforce only one base currency before flush to prevent race conditions
124
- if (record.isBase) {
125
- await enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)
126
- }
127
-
128
- await em.flush()
123
+
124
+ // Demote any existing base currency and insert the new record in one
125
+ // transaction; a partial commit would leave zero or two base currencies.
126
+ await withAtomicFlush(
127
+ em,
128
+ [
129
+ () =>
130
+ record.isBase
131
+ ? enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)
132
+ : undefined,
133
+ ],
134
+ { transaction: true },
135
+ )
129
136
 
130
137
  const de = ctx.container.resolve('dataEngine') as DataEngine
131
138
  await emitCrudSideEffects({
@@ -227,13 +234,19 @@ const updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, { currencyId: s
227
234
  ;(record as any)[key] = change.to
228
235
  }
229
236
  record.updatedAt = new Date()
230
-
231
- // Enforce only one base currency before flush to prevent race conditions
232
- if (parsed.isBase === true && record.isBase) {
233
- await enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)
234
- }
235
-
236
- await em.flush()
237
+
238
+ // Demote any existing base currency and persist the scalar changes in one
239
+ // transaction; a partial commit would leave zero or two base currencies.
240
+ await withAtomicFlush(
241
+ em,
242
+ [
243
+ () =>
244
+ parsed.isBase === true && record.isBase
245
+ ? enforceBaseCurrency(em, record.id, record.organizationId, record.tenantId)
246
+ : undefined,
247
+ ],
248
+ { transaction: true },
249
+ )
237
250
 
238
251
  const de = ctx.container.resolve('dataEngine') as DataEngine
239
252
  await emitCrudSideEffects({
@@ -225,35 +225,40 @@ export async function POST(req: Request) {
225
225
  { tenantId: auth.tenantId!, organizationId: auth.orgId! },
226
226
  )
227
227
  user.emailVerifiedAt = new Date()
228
- em.persist(user)
229
- await em.flush()
230
228
 
231
- if (parsed.data.customerEntityId) {
232
- await em.nativeUpdate(CustomerUser, { id: user.id }, { customerEntityId: parsed.data.customerEntityId })
233
- }
229
+ // Persist the user, its company association, and its role links in one
230
+ // transaction so a flush failure on the role loop cannot leave a roleless
231
+ // user committed (privilege gap).
232
+ await em.transactional(async (tx) => {
233
+ tx.persist(user)
234
+ await tx.flush()
234
235
 
235
- if (parsed.data.roleIds && parsed.data.roleIds.length > 0) {
236
- const validRoles = await findWithDecryption(
237
- em,
238
- CustomerRole,
239
- {
240
- id: { $in: parsed.data.roleIds } as any,
241
- tenantId: auth.tenantId,
242
- deletedAt: null,
243
- } as any,
244
- undefined,
245
- { tenantId: auth.tenantId, organizationId: auth.orgId },
246
- )
247
- for (const role of validRoles) {
248
- const userRole = em.create(CustomerUserRole, {
249
- user,
250
- role,
251
- createdAt: new Date(),
252
- } as any)
253
- em.persist(userRole)
236
+ if (parsed.data.customerEntityId) {
237
+ await tx.nativeUpdate(CustomerUser, { id: user.id }, { customerEntityId: parsed.data.customerEntityId })
254
238
  }
255
- await em.flush()
256
- }
239
+
240
+ if (parsed.data.roleIds && parsed.data.roleIds.length > 0) {
241
+ const validRoles = await findWithDecryption(
242
+ tx,
243
+ CustomerRole,
244
+ {
245
+ id: { $in: parsed.data.roleIds } as any,
246
+ tenantId: auth.tenantId,
247
+ deletedAt: null,
248
+ } as any,
249
+ undefined,
250
+ { tenantId: auth.tenantId, organizationId: auth.orgId },
251
+ )
252
+ for (const role of validRoles) {
253
+ const userRole = tx.create(CustomerUserRole, {
254
+ user,
255
+ role,
256
+ createdAt: new Date(),
257
+ } as any)
258
+ tx.persist(userRole)
259
+ }
260
+ }
261
+ })
257
262
 
258
263
  void emitCustomerAccountsEvent('customer_accounts.user.created', {
259
264
  id: user.id,
@@ -44,14 +44,13 @@ export async function POST(req: Request) {
44
44
  await em.transactional(async (trx) => {
45
45
  await customerUserService.updatePassword(user, parsed.data.password, trx)
46
46
  await customerSessionService.revokeAllUserSessions(user.id, trx)
47
+ await trx.nativeUpdate(
48
+ CustomerUser,
49
+ { id: user.id, emailVerifiedAt: null },
50
+ { emailVerifiedAt: new Date() },
51
+ )
47
52
  })
48
53
 
49
- await em.nativeUpdate(
50
- CustomerUser,
51
- { id: user.id, emailVerifiedAt: null },
52
- { emailVerifiedAt: new Date() },
53
- )
54
-
55
54
  void emitCustomerAccountsEvent('customer_accounts.password.changed', {
56
55
  userId: user.id,
57
56
  tenantId: user.tenantId,
@@ -75,19 +75,20 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
75
75
  validatedRoles.push(role)
76
76
  }
77
77
 
78
- // Remove existing roles
79
- await em.nativeDelete(CustomerUserRole, { user: targetUser.id as any })
80
-
81
- // Assign new roles
82
- for (const role of validatedRoles) {
83
- const userRole = em.create(CustomerUserRole, {
84
- user: targetUser,
85
- role,
86
- createdAt: new Date(),
87
- } as any)
88
- em.persist(userRole)
89
- }
90
- await em.flush()
78
+ // Replace the role set atomically: deleting the old roles and inserting the new
79
+ // ones in one transaction prevents a flush failure from leaving the user with
80
+ // zero roles (portal lockout / privilege loss) (#2337).
81
+ await em.transactional(async (tx) => {
82
+ await tx.nativeDelete(CustomerUserRole, { user: targetUser.id as any })
83
+ for (const role of validatedRoles) {
84
+ const userRole = tx.create(CustomerUserRole, {
85
+ user: targetUser,
86
+ role,
87
+ createdAt: new Date(),
88
+ } as any)
89
+ tx.persist(userRole)
90
+ }
91
+ })
91
92
 
92
93
  await customerRbacService.invalidateUserCache(targetUser.id)
93
94
 
@@ -17,6 +17,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
17
17
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
18
18
  import type { CrudIndexerConfig, CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
19
19
  import { E } from '#generated/entities.ids.generated'
20
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
20
21
 
21
22
  const addressCrudIndexer: CrudIndexerConfig<CustomerAddress> = {
22
23
  entityType: E.customers.customer_address,
@@ -130,13 +131,15 @@ const createAddressCommand: CommandHandler<AddressCreateInput, { addressId: stri
130
131
  createdAt: new Date(),
131
132
  updatedAt: new Date(),
132
133
  })
133
- em.persist(address)
134
- await em.flush()
135
-
136
- if (address.isPrimary) {
137
- await enforcePrimaryAddress(em, entity.id, address.id)
138
- await em.flush()
139
- }
134
+ await withAtomicFlush(em, [
135
+ async () => {
136
+ em.persist(address)
137
+ await em.flush()
138
+ if (address.isPrimary) {
139
+ await enforcePrimaryAddress(em, entity.id, address.id)
140
+ }
141
+ },
142
+ ], { transaction: true })
140
143
 
141
144
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
142
145
  await emitCrudSideEffects({
@@ -225,12 +228,13 @@ const updateAddressCommand: CommandHandler<AddressUpdateInput, { addressId: stri
225
228
  if (parsed.longitude !== undefined) address.longitude = parsed.longitude ?? null
226
229
  if (parsed.isPrimary !== undefined) address.isPrimary = parsed.isPrimary
227
230
 
228
- await em.flush()
229
-
230
- if (address.isPrimary) {
231
- await enforcePrimaryAddress(em, typeof address.entity === 'string' ? address.entity : address.entity.id, address.id)
232
- await em.flush()
233
- }
231
+ await withAtomicFlush(em, [
232
+ async () => {
233
+ if (address.isPrimary) {
234
+ await enforcePrimaryAddress(em, typeof address.entity === 'string' ? address.entity : address.entity.id, address.id)
235
+ }
236
+ },
237
+ ], { transaction: true })
234
238
 
235
239
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
236
240
  await emitCrudSideEffects({
@@ -348,11 +352,15 @@ const updateAddressCommand: CommandHandler<AddressUpdateInput, { addressId: stri
348
352
  address.longitude = before.longitude
349
353
  address.isPrimary = before.isPrimary
350
354
  }
351
- await em.flush()
352
- if (before.isPrimary) {
353
- await enforcePrimaryAddress(em, before.entityId, before.id)
354
- await em.flush()
355
- }
355
+ await withAtomicFlush(em, [
356
+ async () => {
357
+ em.persist(address)
358
+ await em.flush()
359
+ if (before.isPrimary) {
360
+ await enforcePrimaryAddress(em, before.entityId, before.id)
361
+ }
362
+ },
363
+ ], { transaction: true })
356
364
 
357
365
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
358
366
  await emitCrudUndoSideEffects({
@@ -471,11 +479,15 @@ const deleteAddressCommand: CommandHandler<{ body?: Record<string, unknown>; que
471
479
  address.longitude = before.longitude
472
480
  address.isPrimary = before.isPrimary
473
481
  }
474
- await em.flush()
475
- if (before.isPrimary) {
476
- await enforcePrimaryAddress(em, before.entityId, before.id)
477
- await em.flush()
478
- }
482
+ await withAtomicFlush(em, [
483
+ async () => {
484
+ em.persist(address)
485
+ await em.flush()
486
+ if (before.isPrimary) {
487
+ await enforcePrimaryAddress(em, before.entityId, before.id)
488
+ }
489
+ },
490
+ ], { transaction: true })
479
491
 
480
492
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
481
493
  await emitCrudUndoSideEffects({