@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
@@ -1,6 +1,7 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { z } from 'zod'
3
3
  import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
4
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
4
5
  import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
6
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
6
7
  import { Message, MessageObject, MessageRecipient, type MessageActionData } from '../data/entities'
@@ -432,94 +433,95 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
432
433
  }
433
434
  }
434
435
 
435
- if (input.type !== undefined) message.type = input.type
436
- if (input.visibility !== undefined) message.visibility = input.visibility
437
- if (input.sourceEntityType !== undefined) message.sourceEntityType = input.sourceEntityType
438
- if (input.sourceEntityId !== undefined) message.sourceEntityId = input.sourceEntityId
439
- if (input.externalEmail !== undefined) message.externalEmail = input.externalEmail
440
- if (input.externalName !== undefined) message.externalName = input.externalName
441
- if (input.subject !== undefined) message.subject = input.subject
442
- if (input.body !== undefined) message.body = input.body
443
- if (input.bodyFormat !== undefined) message.bodyFormat = input.bodyFormat
444
- if (input.priority !== undefined) message.priority = input.priority
445
- if (input.actionData !== undefined) message.actionData = input.actionData
446
- if (input.sendViaEmail !== undefined) message.sendViaEmail = input.sendViaEmail
447
-
448
- if (input.recipients) {
449
- await em.nativeDelete(MessageRecipient, { messageId: message.id })
450
- for (const recipient of input.recipients) {
451
- em.persist(em.create(MessageRecipient, {
452
- messageId: message.id,
453
- recipientUserId: recipient.userId,
454
- recipientType: recipient.type,
455
- status: 'unread',
456
- }))
436
+ await withAtomicFlush(em, [async () => {
437
+ if (input.type !== undefined) message.type = input.type
438
+ if (input.visibility !== undefined) message.visibility = input.visibility
439
+ if (input.sourceEntityType !== undefined) message.sourceEntityType = input.sourceEntityType
440
+ if (input.sourceEntityId !== undefined) message.sourceEntityId = input.sourceEntityId
441
+ if (input.externalEmail !== undefined) message.externalEmail = input.externalEmail
442
+ if (input.externalName !== undefined) message.externalName = input.externalName
443
+ if (input.subject !== undefined) message.subject = input.subject
444
+ if (input.body !== undefined) message.body = input.body
445
+ if (input.bodyFormat !== undefined) message.bodyFormat = input.bodyFormat
446
+ if (input.priority !== undefined) message.priority = input.priority
447
+ if (input.actionData !== undefined) message.actionData = input.actionData
448
+ if (input.sendViaEmail !== undefined) message.sendViaEmail = input.sendViaEmail
449
+
450
+ if (input.recipients) {
451
+ await em.nativeDelete(MessageRecipient, { messageId: message.id })
452
+ for (const recipient of input.recipients) {
453
+ em.persist(em.create(MessageRecipient, {
454
+ messageId: message.id,
455
+ recipientUserId: recipient.userId,
456
+ recipientType: recipient.type,
457
+ status: 'unread',
458
+ }))
459
+ }
457
460
  }
458
- }
459
461
 
460
- if (input.objects) {
461
- await em.nativeDelete(MessageObject, { messageId: message.id })
462
- for (const object of input.objects) {
463
- em.persist(em.create(MessageObject, {
464
- messageId: message.id,
465
- entityModule: object.entityModule,
466
- entityType: object.entityType,
467
- entityId: object.entityId,
468
- actionRequired: object.actionRequired,
469
- actionType: object.actionType,
470
- actionLabel: object.actionLabel,
471
- }))
462
+ if (input.objects) {
463
+ await em.nativeDelete(MessageObject, { messageId: message.id })
464
+ for (const object of input.objects) {
465
+ em.persist(em.create(MessageObject, {
466
+ messageId: message.id,
467
+ entityModule: object.entityModule,
468
+ entityType: object.entityType,
469
+ entityId: object.entityId,
470
+ actionRequired: object.actionRequired,
471
+ actionType: object.actionType,
472
+ actionLabel: object.actionLabel,
473
+ }))
474
+ }
472
475
  }
473
- }
474
476
 
475
- if (input.attachmentIds) {
476
- const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')
477
- if (input.attachmentIds.length === 0) {
478
- await em.nativeDelete(Attachment, {
479
- entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
480
- recordId: message.id,
481
- tenantId: input.tenantId,
482
- organizationId: input.organizationId,
483
- })
484
- } else {
485
- await em.nativeDelete(Attachment, {
486
- entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
487
- recordId: message.id,
488
- tenantId: input.tenantId,
489
- organizationId: input.organizationId,
490
- id: { $nin: input.attachmentIds },
491
- })
477
+ if (input.attachmentIds) {
478
+ const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')
479
+ if (input.attachmentIds.length === 0) {
480
+ await em.nativeDelete(Attachment, {
481
+ entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
482
+ recordId: message.id,
483
+ tenantId: input.tenantId,
484
+ organizationId: input.organizationId,
485
+ })
486
+ } else {
487
+ await em.nativeDelete(Attachment, {
488
+ entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
489
+ recordId: message.id,
490
+ tenantId: input.tenantId,
491
+ organizationId: input.organizationId,
492
+ id: { $nin: input.attachmentIds },
493
+ })
494
+ }
495
+ await linkAttachmentsToMessage(
496
+ em,
497
+ message.id,
498
+ input.attachmentIds,
499
+ input.organizationId,
500
+ input.tenantId,
501
+ )
492
502
  }
493
- await linkAttachmentsToMessage(
494
- em,
495
- message.id,
496
- input.attachmentIds,
497
- input.organizationId,
498
- input.tenantId,
499
- )
500
- }
501
503
 
502
- if (isSending) {
503
- const finalVisibility = input.visibility ?? message.visibility
504
- const finalSubject = input.subject ?? message.subject
505
- const finalBody = input.body ?? message.body
506
- const finalRecipientCount = input.recipients
507
- ? input.recipients.length
508
- : (preloadedRecipients?.length ?? 0)
509
-
510
- if (finalVisibility !== 'public' && finalRecipientCount === 0) {
511
- throw new Error('at least one recipient is required')
512
- }
513
- if (!finalSubject?.trim()) throw new Error('subject is required')
514
- if (!finalBody?.trim()) throw new Error('body is required')
504
+ if (isSending) {
505
+ const finalVisibility = input.visibility ?? message.visibility
506
+ const finalSubject = input.subject ?? message.subject
507
+ const finalBody = input.body ?? message.body
508
+ const finalRecipientCount = input.recipients
509
+ ? input.recipients.length
510
+ : (preloadedRecipients?.length ?? 0)
515
511
 
516
- message.isDraft = false
517
- message.status = 'sent'
518
- message.sentAt = new Date()
519
- if (!message.threadId) message.threadId = message.id
520
- }
512
+ if (finalVisibility !== 'public' && finalRecipientCount === 0) {
513
+ throw new Error('at least one recipient is required')
514
+ }
515
+ if (!finalSubject?.trim()) throw new Error('subject is required')
516
+ if (!finalBody?.trim()) throw new Error('body is required')
517
+
518
+ message.isDraft = false
519
+ message.status = 'sent'
520
+ message.sentAt = new Date()
521
+ if (!message.threadId) message.threadId = message.id
522
+ }
523
+ }], { transaction: true })
521
524
 
522
- await em.flush()
523
525
  await emitMessageIndexUpsert(ctx.container, {
524
526
  messageId: message.id,
525
527
  tenantId: input.tenantId,
@@ -1,4 +1,5 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
2
3
  import { Message, MessageObject, MessageRecipient, type MessageActionData, type RecipientStatus } from '../data/entities'
3
4
  import { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'
4
5
 
@@ -187,152 +188,156 @@ export async function restoreMessageAggregateSnapshot(
187
188
  em: EntityManager,
188
189
  snapshot: MessageAggregateSnapshot,
189
190
  ): Promise<void> {
190
- const existingMessage = await em.findOne(Message, { id: snapshot.message.id })
191
- if (!existingMessage) {
192
- const created = em.create(Message, {
193
- id: snapshot.message.id,
194
- type: snapshot.message.type,
195
- visibility: snapshot.message.visibility,
196
- sourceEntityType: snapshot.message.sourceEntityType,
197
- sourceEntityId: snapshot.message.sourceEntityId,
198
- externalEmail: snapshot.message.externalEmail,
199
- externalName: snapshot.message.externalName,
200
- threadId: snapshot.message.threadId,
201
- parentMessageId: snapshot.message.parentMessageId,
202
- senderUserId: snapshot.message.senderUserId,
203
- subject: snapshot.message.subject,
204
- body: snapshot.message.body,
205
- bodyFormat: snapshot.message.bodyFormat,
206
- priority: snapshot.message.priority,
207
- status: snapshot.message.status,
208
- isDraft: snapshot.message.isDraft,
209
- sentAt: toDate(snapshot.message.sentAt),
210
- actionData: snapshot.message.actionData as MessageActionData,
211
- actionResult: snapshot.message.actionResult,
212
- actionTaken: snapshot.message.actionTaken,
213
- actionTakenByUserId: snapshot.message.actionTakenByUserId,
214
- actionTakenAt: toDate(snapshot.message.actionTakenAt),
215
- sendViaEmail: snapshot.message.sendViaEmail,
216
- tenantId: snapshot.message.tenantId,
217
- organizationId: snapshot.message.organizationId,
218
- deletedAt: toDate(snapshot.message.deletedAt),
219
- })
220
- em.persist(created)
221
- } else {
222
- existingMessage.type = snapshot.message.type
223
- existingMessage.visibility = snapshot.message.visibility
224
- existingMessage.sourceEntityType = snapshot.message.sourceEntityType
225
- existingMessage.sourceEntityId = snapshot.message.sourceEntityId
226
- existingMessage.externalEmail = snapshot.message.externalEmail
227
- existingMessage.externalName = snapshot.message.externalName
228
- existingMessage.threadId = snapshot.message.threadId
229
- existingMessage.parentMessageId = snapshot.message.parentMessageId
230
- existingMessage.senderUserId = snapshot.message.senderUserId
231
- existingMessage.subject = snapshot.message.subject
232
- existingMessage.body = snapshot.message.body
233
- existingMessage.bodyFormat = snapshot.message.bodyFormat
234
- existingMessage.priority = snapshot.message.priority
235
- existingMessage.status = snapshot.message.status
236
- existingMessage.isDraft = snapshot.message.isDraft
237
- existingMessage.sentAt = toDate(snapshot.message.sentAt)
238
- existingMessage.actionData = snapshot.message.actionData as MessageActionData
239
- existingMessage.actionResult = snapshot.message.actionResult
240
- existingMessage.actionTaken = snapshot.message.actionTaken
241
- existingMessage.actionTakenByUserId = snapshot.message.actionTakenByUserId
242
- existingMessage.actionTakenAt = toDate(snapshot.message.actionTakenAt)
243
- existingMessage.sendViaEmail = snapshot.message.sendViaEmail
244
- existingMessage.tenantId = snapshot.message.tenantId
245
- existingMessage.organizationId = snapshot.message.organizationId
246
- existingMessage.deletedAt = toDate(snapshot.message.deletedAt)
247
- }
191
+ await withAtomicFlush(em, [async () => {
192
+ // Resolve every read before mutating: a query issued between a scalar
193
+ // mutation and the flush can silently reset the Unit of Work (SPEC-018
194
+ // Problem 1). withAtomicFlush flushes once at the end, so ordering is ours.
195
+ const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')
196
+ const existingMessage = await em.findOne(Message, { id: snapshot.message.id })
197
+ const existingRecipients = await em.find(MessageRecipient, { messageId: snapshot.message.id })
198
+ const existingObjects = await em.find(MessageObject, { messageId: snapshot.message.id })
199
+ const attachmentsToRelink = snapshot.attachmentIds.length > 0
200
+ ? await em.find(Attachment, {
201
+ id: { $in: snapshot.attachmentIds },
202
+ tenantId: snapshot.message.tenantId,
203
+ organizationId: snapshot.message.organizationId,
204
+ })
205
+ : []
248
206
 
249
- const existingRecipients = await em.find(MessageRecipient, { messageId: snapshot.message.id })
250
- const recipientById = new Map(existingRecipients.map((item) => [item.id, item]))
251
- const snapshotRecipientIds = new Set(snapshot.recipients.map((item) => item.id))
252
- for (const current of existingRecipients) {
253
- if (!snapshotRecipientIds.has(current.id)) {
254
- em.remove(current)
207
+ if (!existingMessage) {
208
+ const created = em.create(Message, {
209
+ id: snapshot.message.id,
210
+ type: snapshot.message.type,
211
+ visibility: snapshot.message.visibility,
212
+ sourceEntityType: snapshot.message.sourceEntityType,
213
+ sourceEntityId: snapshot.message.sourceEntityId,
214
+ externalEmail: snapshot.message.externalEmail,
215
+ externalName: snapshot.message.externalName,
216
+ threadId: snapshot.message.threadId,
217
+ parentMessageId: snapshot.message.parentMessageId,
218
+ senderUserId: snapshot.message.senderUserId,
219
+ subject: snapshot.message.subject,
220
+ body: snapshot.message.body,
221
+ bodyFormat: snapshot.message.bodyFormat,
222
+ priority: snapshot.message.priority,
223
+ status: snapshot.message.status,
224
+ isDraft: snapshot.message.isDraft,
225
+ sentAt: toDate(snapshot.message.sentAt),
226
+ actionData: snapshot.message.actionData as MessageActionData,
227
+ actionResult: snapshot.message.actionResult,
228
+ actionTaken: snapshot.message.actionTaken,
229
+ actionTakenByUserId: snapshot.message.actionTakenByUserId,
230
+ actionTakenAt: toDate(snapshot.message.actionTakenAt),
231
+ sendViaEmail: snapshot.message.sendViaEmail,
232
+ tenantId: snapshot.message.tenantId,
233
+ organizationId: snapshot.message.organizationId,
234
+ deletedAt: toDate(snapshot.message.deletedAt),
235
+ })
236
+ em.persist(created)
237
+ } else {
238
+ existingMessage.type = snapshot.message.type
239
+ existingMessage.visibility = snapshot.message.visibility
240
+ existingMessage.sourceEntityType = snapshot.message.sourceEntityType
241
+ existingMessage.sourceEntityId = snapshot.message.sourceEntityId
242
+ existingMessage.externalEmail = snapshot.message.externalEmail
243
+ existingMessage.externalName = snapshot.message.externalName
244
+ existingMessage.threadId = snapshot.message.threadId
245
+ existingMessage.parentMessageId = snapshot.message.parentMessageId
246
+ existingMessage.senderUserId = snapshot.message.senderUserId
247
+ existingMessage.subject = snapshot.message.subject
248
+ existingMessage.body = snapshot.message.body
249
+ existingMessage.bodyFormat = snapshot.message.bodyFormat
250
+ existingMessage.priority = snapshot.message.priority
251
+ existingMessage.status = snapshot.message.status
252
+ existingMessage.isDraft = snapshot.message.isDraft
253
+ existingMessage.sentAt = toDate(snapshot.message.sentAt)
254
+ existingMessage.actionData = snapshot.message.actionData as MessageActionData
255
+ existingMessage.actionResult = snapshot.message.actionResult
256
+ existingMessage.actionTaken = snapshot.message.actionTaken
257
+ existingMessage.actionTakenByUserId = snapshot.message.actionTakenByUserId
258
+ existingMessage.actionTakenAt = toDate(snapshot.message.actionTakenAt)
259
+ existingMessage.sendViaEmail = snapshot.message.sendViaEmail
260
+ existingMessage.tenantId = snapshot.message.tenantId
261
+ existingMessage.organizationId = snapshot.message.organizationId
262
+ existingMessage.deletedAt = toDate(snapshot.message.deletedAt)
255
263
  }
256
- }
257
- for (const recipient of snapshot.recipients) {
258
- const existing = recipientById.get(recipient.id)
259
- if (!existing) {
260
- em.persist(em.create(MessageRecipient, {
261
- id: recipient.id,
262
- messageId: recipient.messageId,
263
- recipientUserId: recipient.recipientUserId,
264
- recipientType: recipient.recipientType,
265
- status: recipient.status,
266
- readAt: toDate(recipient.readAt),
267
- archivedAt: toDate(recipient.archivedAt),
268
- deletedAt: toDate(recipient.deletedAt),
269
- }))
270
- continue
264
+
265
+ const recipientById = new Map(existingRecipients.map((item) => [item.id, item]))
266
+ const snapshotRecipientIds = new Set(snapshot.recipients.map((item) => item.id))
267
+ for (const current of existingRecipients) {
268
+ if (!snapshotRecipientIds.has(current.id)) {
269
+ em.remove(current)
270
+ }
271
+ }
272
+ for (const recipient of snapshot.recipients) {
273
+ const existing = recipientById.get(recipient.id)
274
+ if (!existing) {
275
+ em.persist(em.create(MessageRecipient, {
276
+ id: recipient.id,
277
+ messageId: recipient.messageId,
278
+ recipientUserId: recipient.recipientUserId,
279
+ recipientType: recipient.recipientType,
280
+ status: recipient.status,
281
+ readAt: toDate(recipient.readAt),
282
+ archivedAt: toDate(recipient.archivedAt),
283
+ deletedAt: toDate(recipient.deletedAt),
284
+ }))
285
+ continue
286
+ }
287
+ existing.messageId = recipient.messageId
288
+ existing.recipientUserId = recipient.recipientUserId
289
+ existing.recipientType = recipient.recipientType
290
+ existing.status = recipient.status
291
+ existing.readAt = toDate(recipient.readAt)
292
+ existing.archivedAt = toDate(recipient.archivedAt)
293
+ existing.deletedAt = toDate(recipient.deletedAt)
271
294
  }
272
- existing.messageId = recipient.messageId
273
- existing.recipientUserId = recipient.recipientUserId
274
- existing.recipientType = recipient.recipientType
275
- existing.status = recipient.status
276
- existing.readAt = toDate(recipient.readAt)
277
- existing.archivedAt = toDate(recipient.archivedAt)
278
- existing.deletedAt = toDate(recipient.deletedAt)
279
- }
280
295
 
281
- const existingObjects = await em.find(MessageObject, { messageId: snapshot.message.id })
282
- const objectById = new Map(existingObjects.map((item) => [item.id, item]))
283
- const snapshotObjectIds = new Set(snapshot.objects.map((item) => item.id))
284
- for (const current of existingObjects) {
285
- if (!snapshotObjectIds.has(current.id)) {
286
- em.remove(current)
296
+ const objectById = new Map(existingObjects.map((item) => [item.id, item]))
297
+ const snapshotObjectIds = new Set(snapshot.objects.map((item) => item.id))
298
+ for (const current of existingObjects) {
299
+ if (!snapshotObjectIds.has(current.id)) {
300
+ em.remove(current)
301
+ }
287
302
  }
288
- }
289
- for (const object of snapshot.objects) {
290
- const existing = objectById.get(object.id)
291
- if (!existing) {
292
- em.persist(em.create(MessageObject, {
293
- id: object.id,
294
- messageId: object.messageId,
295
- entityModule: object.entityModule,
296
- entityType: object.entityType,
297
- entityId: object.entityId,
298
- actionRequired: object.actionRequired,
299
- actionType: object.actionType,
300
- actionLabel: object.actionLabel,
301
- entitySnapshot: object.entitySnapshot,
302
- }))
303
- continue
303
+ for (const object of snapshot.objects) {
304
+ const existing = objectById.get(object.id)
305
+ if (!existing) {
306
+ em.persist(em.create(MessageObject, {
307
+ id: object.id,
308
+ messageId: object.messageId,
309
+ entityModule: object.entityModule,
310
+ entityType: object.entityType,
311
+ entityId: object.entityId,
312
+ actionRequired: object.actionRequired,
313
+ actionType: object.actionType,
314
+ actionLabel: object.actionLabel,
315
+ entitySnapshot: object.entitySnapshot,
316
+ }))
317
+ continue
318
+ }
319
+ existing.messageId = object.messageId
320
+ existing.entityModule = object.entityModule
321
+ existing.entityType = object.entityType
322
+ existing.entityId = object.entityId
323
+ existing.actionRequired = object.actionRequired
324
+ existing.actionType = object.actionType
325
+ existing.actionLabel = object.actionLabel
326
+ existing.entitySnapshot = object.entitySnapshot
304
327
  }
305
- existing.messageId = object.messageId
306
- existing.entityModule = object.entityModule
307
- existing.entityType = object.entityType
308
- existing.entityId = object.entityId
309
- existing.actionRequired = object.actionRequired
310
- existing.actionType = object.actionType
311
- existing.actionLabel = object.actionLabel
312
- existing.entitySnapshot = object.entitySnapshot
313
- }
314
328
 
315
- const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')
316
- await em.nativeDelete(Attachment, {
317
- entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
318
- recordId: snapshot.message.id,
319
- tenantId: snapshot.message.tenantId,
320
- organizationId: snapshot.message.organizationId,
321
- id: { $nin: snapshot.attachmentIds.length > 0 ? snapshot.attachmentIds : ['00000000-0000-0000-0000-000000000000'] },
322
- })
323
- if (snapshot.attachmentIds.length > 0) {
324
- const attachments = await em.find(Attachment, {
325
- id: { $in: snapshot.attachmentIds },
329
+ await em.nativeDelete(Attachment, {
330
+ entityId: MESSAGE_ATTACHMENT_ENTITY_ID,
331
+ recordId: snapshot.message.id,
326
332
  tenantId: snapshot.message.tenantId,
327
333
  organizationId: snapshot.message.organizationId,
334
+ id: { $nin: snapshot.attachmentIds.length > 0 ? snapshot.attachmentIds : ['00000000-0000-0000-0000-000000000000'] },
328
335
  })
329
- for (const attachment of attachments) {
336
+ for (const attachment of attachmentsToRelink) {
330
337
  attachment.entityId = MESSAGE_ATTACHMENT_ENTITY_ID
331
338
  attachment.recordId = snapshot.message.id
332
339
  }
333
- }
334
-
335
- await em.flush()
340
+ }], { transaction: true })
336
341
  }
337
342
 
338
343
  export function buildCommandLogBase(
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
5
6
  import { perspectiveSaveSchema } from '@open-mercato/core/modules/perspectives/data/validators'
6
7
  import {
7
8
  loadPerspectivesState,
@@ -162,17 +163,12 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
162
163
  }
163
164
 
164
165
  const scope = buildScope(auth)
165
- const saved = await saveUserPerspective(em, cache, {
166
- scope,
167
- tableId,
168
- input: parsed.data,
169
- })
170
166
 
171
167
  const applyToRoles = Array.from(new Set(parsed.data.applyToRoles ?? [])).filter((id) => id.trim().length > 0)
172
168
  const clearRoleIds = Array.from(new Set(parsed.data.clearRoleIds ?? [])).filter((id) => id.trim().length > 0)
173
- let updatedRolePerspectives: Awaited<ReturnType<typeof saveRolePerspectives>> | null = null
169
+ const hasRoleOps = applyToRoles.length > 0 || clearRoleIds.length > 0
174
170
 
175
- if (applyToRoles.length > 0 || clearRoleIds.length > 0) {
171
+ if (hasRoleOps) {
176
172
  const canApplyToRoles = await rbac.userHasAllFeatures?.(
177
173
  auth.sub,
178
174
  ['perspectives.role_defaults'],
@@ -198,30 +194,45 @@ export async function POST(req: Request, ctx: { params: { tableId: string } }) {
198
194
  if (missing.length) {
199
195
  return NextResponse.json({ error: 'Invalid roles', missing }, { status: 400 })
200
196
  }
197
+ }
201
198
 
202
- if (applyToRoles.length) {
203
- updatedRolePerspectives = await saveRolePerspectives(em, cache, {
204
- tableId,
205
- tenantId: auth.tenantId ?? null,
206
- organizationId: auth.orgId ?? null,
207
- input: {
208
- roleIds: applyToRoles,
209
- name: parsed.data.name,
210
- settings: parsed.data.settings,
211
- setDefault: parsed.data.setRoleDefault ?? false,
212
- },
213
- })
214
- }
199
+ let saved: Awaited<ReturnType<typeof saveUserPerspective>> | null = null
200
+ let updatedRolePerspectives: Awaited<ReturnType<typeof saveRolePerspectives>> | null = null
215
201
 
216
- if (clearRoleIds.length) {
217
- await clearRolePerspectives(em, cache, {
202
+ await withAtomicFlush(em, [
203
+ async () => {
204
+ saved = await saveUserPerspective(em, cache, {
205
+ scope,
218
206
  tableId,
219
- tenantId: auth.tenantId ?? null,
220
- organizationId: auth.orgId ?? null,
221
- roleIds: clearRoleIds,
207
+ input: parsed.data,
222
208
  })
223
- }
224
- }
209
+ },
210
+ async () => {
211
+ if (applyToRoles.length) {
212
+ updatedRolePerspectives = await saveRolePerspectives(em, cache, {
213
+ tableId,
214
+ tenantId: auth.tenantId ?? null,
215
+ organizationId: auth.orgId ?? null,
216
+ input: {
217
+ roleIds: applyToRoles,
218
+ name: parsed.data.name,
219
+ settings: parsed.data.settings,
220
+ setDefault: parsed.data.setRoleDefault ?? false,
221
+ },
222
+ })
223
+ }
224
+ },
225
+ async () => {
226
+ if (clearRoleIds.length) {
227
+ await clearRolePerspectives(em, cache, {
228
+ tableId,
229
+ tenantId: auth.tenantId ?? null,
230
+ organizationId: auth.orgId ?? null,
231
+ roleIds: clearRoleIds,
232
+ })
233
+ }
234
+ },
235
+ ], { transaction: true })
225
236
 
226
237
  return NextResponse.json({
227
238
  perspective: saved,