@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7721.1.b5c4a00a68

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 (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -24,9 +24,6 @@ import {
24
24
  type StaffTimeProjectMemberStatus,
25
25
  } from '../data/entities'
26
26
 
27
- const timeProjectCrudIndexer: CrudIndexerConfig<StaffTimeProject> = {
28
- entityType: 'staff:staff_time_project',
29
- }
30
27
  const timeProjectMemberCrudIndexer: CrudIndexerConfig<StaffTimeProjectMember> = {
31
28
  entityType: 'staff:staff_time_project_member',
32
29
  }
@@ -41,7 +38,7 @@ import {
41
38
  type StaffTimeProjectMemberAssignInput,
42
39
  type StaffTimeProjectMemberUpdateInput,
43
40
  } from '../data/validators'
44
- import { staffTimeProjectCrudEvents, staffTimeProjectMemberCrudEvents } from '../lib/crud'
41
+ import { staffTimeProjectCrudEvents, staffTimeProjectCrudIndexer, staffTimeProjectMemberCrudEvents } from '../lib/crud'
45
42
  import { DEFAULT_BUDGET_WARN_AT_PERCENT } from '../lib/timesheets-projects/budgetBurn'
46
43
  import { seedProjectTaskStatuses } from '../lib/timesheets-tasks/seedProjectStatuses'
47
44
  import {
@@ -142,6 +139,7 @@ type TimeProjectSnapshot = {
142
139
  budgetKind: string
143
140
  budgetValue: string | null
144
141
  budgetWarnAtPercent: number
142
+ defaultAssigneeStaffMemberId: string | null
145
143
  deletedAt: string | null
146
144
  }
147
145
 
@@ -159,6 +157,7 @@ type TimeProjectMemberSnapshot = {
159
157
  role: string | null
160
158
  status: string
161
159
  showInGrid: boolean
160
+ isAssigner: boolean
162
161
  assignedStartDate: string
163
162
  assignedEndDate: string | null
164
163
  deletedAt: string | null
@@ -199,6 +198,7 @@ async function loadTimeProjectSnapshot(em: EntityManager, id: string, scope?: St
199
198
  budgetKind: project.budgetKind ?? 'none',
200
199
  budgetValue: project.budgetValue ?? null,
201
200
  budgetWarnAtPercent: project.budgetWarnAtPercent ?? DEFAULT_BUDGET_WARN_AT_PERCENT,
201
+ defaultAssigneeStaffMemberId: project.defaultAssigneeStaffMemberId ?? null,
202
202
  deletedAt: project.deletedAt ? project.deletedAt.toISOString() : null,
203
203
  }
204
204
  }
@@ -221,6 +221,7 @@ async function loadTimeProjectMemberSnapshot(em: EntityManager, id: string, scop
221
221
  role: member.role ?? null,
222
222
  status: member.status,
223
223
  showInGrid: member.showInGrid ?? false,
224
+ isAssigner: member.isAssigner === true,
224
225
  assignedStartDate: member.assignedStartDate instanceof Date ? member.assignedStartDate.toISOString().split('T')[0] : String(member.assignedStartDate),
225
226
  assignedEndDate: member.assignedEndDate instanceof Date ? member.assignedEndDate.toISOString().split('T')[0] : (member.assignedEndDate ?? null),
226
227
  deletedAt: member.deletedAt ? member.deletedAt.toISOString() : null,
@@ -249,6 +250,7 @@ function timeProjectSeedFromSnapshot(snapshot: TimeProjectSnapshot): Record<stri
249
250
  budgetKind: (snapshot.budgetKind ?? 'none') as StaffTimeProjectBudgetKind,
250
251
  budgetValue: snapshot.budgetValue ?? null,
251
252
  budgetWarnAtPercent: snapshot.budgetWarnAtPercent ?? DEFAULT_BUDGET_WARN_AT_PERCENT,
253
+ defaultAssigneeStaffMemberId: snapshot.defaultAssigneeStaffMemberId ?? null,
252
254
  createdAt: new Date(),
253
255
  updatedAt: new Date(),
254
256
  deletedAt: null,
@@ -265,6 +267,7 @@ function timeProjectMemberSeedFromSnapshot(snapshot: TimeProjectMemberSnapshot):
265
267
  role: snapshot.role ?? null,
266
268
  status: (snapshot.status ?? 'active') as StaffTimeProjectMemberStatus,
267
269
  showInGrid: snapshot.showInGrid ?? false,
270
+ isAssigner: snapshot.isAssigner === true,
268
271
  assignedStartDate: new Date(snapshot.assignedStartDate),
269
272
  assignedEndDate: snapshot.assignedEndDate ? new Date(snapshot.assignedEndDate) : null,
270
273
  createdAt: new Date(),
@@ -368,7 +371,7 @@ const createTimeProjectCommand: CommandHandler<StaffTimeProjectCreateInput, { ti
368
371
  tenantId: project.tenantId,
369
372
  },
370
373
  events: staffTimeProjectCrudEvents,
371
- indexer: timeProjectCrudIndexer,
374
+ indexer: staffTimeProjectCrudIndexer,
372
375
  })
373
376
 
374
377
  return { timeProjectId: project.id }
@@ -418,7 +421,7 @@ const createTimeProjectCommand: CommandHandler<StaffTimeProjectCreateInput, { ti
418
421
  tenantId: project.tenantId,
419
422
  },
420
423
  events: staffTimeProjectCrudEvents,
421
- indexer: timeProjectCrudIndexer,
424
+ indexer: staffTimeProjectCrudIndexer,
422
425
  })
423
426
  }
424
427
  },
@@ -428,10 +431,60 @@ const createTimeProjectCommand: CommandHandler<StaffTimeProjectCreateInput, { ti
428
431
  seedFromSnapshot: timeProjectSeedFromSnapshot,
429
432
  buildResult: (entity) => ({ timeProjectId: entity.id }),
430
433
  events: staffTimeProjectCrudEvents,
431
- indexer: timeProjectCrudIndexer,
434
+ indexer: staffTimeProjectCrudIndexer,
432
435
  }),
433
436
  }
434
437
 
438
+ async function requireAssignerUserAccount(
439
+ em: EntityManager,
440
+ member: { staffMemberId: string; tenantId: string; organizationId: string },
441
+ ): Promise<void> {
442
+ const teamMember = await findOneWithDecryption(
443
+ em,
444
+ StaffTeamMember,
445
+ { id: member.staffMemberId, tenantId: member.tenantId, organizationId: member.organizationId, deletedAt: null },
446
+ undefined,
447
+ { tenantId: member.tenantId, organizationId: member.organizationId },
448
+ )
449
+ if (teamMember?.userId) return
450
+ const { translate } = await resolveTranslations()
451
+ const message = translate(
452
+ 'staff.timesheets.errors.assignerNeedsUser',
453
+ 'Only team members with a user account can be assigners, because they must log in and receive notifications.',
454
+ )
455
+ throw new CrudHttpError(422, { code: 'assigner_needs_user', error: message, fieldErrors: { isAssigner: message } })
456
+ }
457
+
458
+ async function requireDefaultAssigneeMember(
459
+ em: EntityManager,
460
+ project: { id: string; tenantId: string; organizationId: string },
461
+ staffMemberId: string,
462
+ ): Promise<void> {
463
+ const membership = await em.findOne(
464
+ StaffTimeProjectMember,
465
+ {
466
+ timeProjectId: project.id,
467
+ staffMemberId,
468
+ tenantId: project.tenantId,
469
+ organizationId: project.organizationId,
470
+ status: 'active',
471
+ deletedAt: null,
472
+ },
473
+ { fields: ['id'] },
474
+ )
475
+ if (membership) return
476
+ const { translate } = await resolveTranslations()
477
+ const message = translate(
478
+ 'staff.timesheets.errors.defaultAssigneeNotMember',
479
+ 'The default assignee must be an active member of this project.',
480
+ )
481
+ throw new CrudHttpError(422, {
482
+ code: 'default_assignee_not_member',
483
+ error: message,
484
+ fieldErrors: { defaultAssigneeStaffMemberId: message },
485
+ })
486
+ }
487
+
435
488
  const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { timeProjectId: string }> = {
436
489
  id: 'staff.timesheets.time_projects.update',
437
490
  async prepare(rawInput, ctx) {
@@ -480,6 +533,10 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
480
533
  ?? (customerUnchanged && nextCustomerId ? project.customerSnapshot ?? null : null)
481
534
  }
482
535
 
536
+ if (parsed.defaultAssigneeStaffMemberId) {
537
+ await requireDefaultAssigneeMember(em, project, parsed.defaultAssigneeStaffMemberId)
538
+ }
539
+
483
540
  if (parsed.name !== undefined) project.name = parsed.name
484
541
  if (parsed.customerId !== undefined) project.customerId = parsed.customerId ?? null
485
542
  if (nextCustomerSnapshot !== undefined) project.customerSnapshot = nextCustomerSnapshot
@@ -496,6 +553,7 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
496
553
  if (parsed.budgetKind !== undefined) project.budgetKind = parsed.budgetKind
497
554
  if (parsed.budgetValue !== undefined) project.budgetValue = parsed.budgetValue ?? null
498
555
  if (parsed.budgetWarnAtPercent !== undefined) project.budgetWarnAtPercent = parsed.budgetWarnAtPercent
556
+ if (parsed.defaultAssigneeStaffMemberId !== undefined) project.defaultAssigneeStaffMemberId = parsed.defaultAssigneeStaffMemberId ?? null
499
557
  // `currency_code` is intentionally absent here and from the update schema:
500
558
  // D-3 gives that column to `staff.timesheets.time_projects.change_currency`,
501
559
  // which owns the acknowledgement and the locked-entry refusal. A caller that
@@ -536,7 +594,7 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
536
594
  tenantId: project.tenantId,
537
595
  },
538
596
  events: staffTimeProjectCrudEvents,
539
- indexer: timeProjectCrudIndexer,
597
+ indexer: staffTimeProjectCrudIndexer,
540
598
  })
541
599
 
542
600
  return { timeProjectId: project.id }
@@ -563,6 +621,7 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
563
621
  'budgetKind',
564
622
  'budgetValue',
565
623
  'budgetWarnAtPercent',
624
+ 'defaultAssigneeStaffMemberId',
566
625
  'deletedAt',
567
626
  ])
568
627
  // `buildChanges` compares by identity, and the two snapshots are loaded through
@@ -612,6 +671,7 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
612
671
  project.budgetKind = (before.budgetKind ?? 'none') as StaffTimeProjectBudgetKind
613
672
  project.budgetValue = before.budgetValue ?? null
614
673
  project.budgetWarnAtPercent = before.budgetWarnAtPercent ?? DEFAULT_BUDGET_WARN_AT_PERCENT
674
+ project.defaultAssigneeStaffMemberId = before.defaultAssigneeStaffMemberId ?? null
615
675
  // Undo of a plain update restores what a plain update could change; the currency
616
676
  // stays where change_currency left it (D-3).
617
677
  project.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null
@@ -628,7 +688,7 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
628
688
  tenantId: project.tenantId,
629
689
  },
630
690
  events: staffTimeProjectCrudEvents,
631
- indexer: timeProjectCrudIndexer,
691
+ indexer: staffTimeProjectCrudIndexer,
632
692
  })
633
693
  },
634
694
  }
@@ -673,7 +733,7 @@ const deleteTimeProjectCommand: CommandHandler<{ id?: string }, { timeProjectId:
673
733
  tenantId: project.tenantId,
674
734
  },
675
735
  events: staffTimeProjectCrudEvents,
676
- indexer: timeProjectCrudIndexer,
736
+ indexer: staffTimeProjectCrudIndexer,
677
737
  })
678
738
  return { timeProjectId: project.id }
679
739
  },
@@ -759,7 +819,7 @@ const deleteTimeProjectCommand: CommandHandler<{ id?: string }, { timeProjectId:
759
819
  tenantId: project.tenantId,
760
820
  },
761
821
  events: staffTimeProjectCrudEvents,
762
- indexer: timeProjectCrudIndexer,
822
+ indexer: staffTimeProjectCrudIndexer,
763
823
  })
764
824
  },
765
825
  }
@@ -932,7 +992,7 @@ const changeTimeProjectCurrencyCommand: CommandHandler<
932
992
  tenantId: project.tenantId,
933
993
  },
934
994
  events: timeProjectCurrencyChangeEvents(previousCurrencyCode, parsed.currencyCode),
935
- indexer: timeProjectCrudIndexer,
995
+ indexer: staffTimeProjectCrudIndexer,
936
996
  })
937
997
 
938
998
  return {
@@ -1002,7 +1062,7 @@ const changeTimeProjectCurrencyCommand: CommandHandler<
1002
1062
  tenantId: project.tenantId,
1003
1063
  },
1004
1064
  events: staffTimeProjectCrudEvents,
1005
- indexer: timeProjectCrudIndexer,
1065
+ indexer: staffTimeProjectCrudIndexer,
1006
1066
  })
1007
1067
  },
1008
1068
  }
@@ -1048,6 +1108,10 @@ const assignTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberAssig
1048
1108
  })
1049
1109
  }
1050
1110
 
1111
+ if (parsed.isAssigner === true) {
1112
+ await requireAssignerUserAccount(em, { staffMemberId: parsed.staffMemberId, tenantId: parsed.tenantId, organizationId: parsed.organizationId })
1113
+ }
1114
+
1051
1115
  const now = new Date()
1052
1116
  const member = em.create(StaffTimeProjectMember, {
1053
1117
  tenantId: parsed.tenantId,
@@ -1057,6 +1121,7 @@ const assignTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberAssig
1057
1121
  role: parsed.role ?? null,
1058
1122
  status: parsed.status ?? 'active',
1059
1123
  showInGrid: false,
1124
+ isAssigner: parsed.isAssigner === true,
1060
1125
  assignedStartDate: parsed.assignedStartDate,
1061
1126
  assignedEndDate: parsed.assignedEndDate ?? null,
1062
1127
  createdAt: now,
@@ -1187,9 +1252,13 @@ const updateTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberUpdat
1187
1252
  ensureTenantScope(ctx, member.tenantId)
1188
1253
  ensureOrganizationScope(ctx, member.organizationId)
1189
1254
 
1255
+ if (parsed.isAssigner === true && member.isAssigner !== true) {
1256
+ await requireAssignerUserAccount(em, member)
1257
+ }
1190
1258
  if (parsed.role !== undefined) member.role = parsed.role ?? null
1191
1259
  if (parsed.status !== undefined) member.status = parsed.status
1192
1260
  if (parsed.assignedEndDate !== undefined) member.assignedEndDate = parsed.assignedEndDate ?? null
1261
+ if (parsed.isAssigner !== undefined) member.isAssigner = parsed.isAssigner
1193
1262
  member.updatedAt = new Date()
1194
1263
  await em.flush()
1195
1264
 
@@ -1214,6 +1283,7 @@ const updateTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberUpdat
1214
1283
  'role',
1215
1284
  'status',
1216
1285
  'assignedEndDate',
1286
+ 'isAssigner',
1217
1287
  ])
1218
1288
  const { translate } = await resolveTranslations()
1219
1289
  return {
@@ -1245,6 +1315,7 @@ const updateTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberUpdat
1245
1315
  member.role = before.role ?? null
1246
1316
  member.status = (before.status ?? 'active') as StaffTimeProjectMemberStatus
1247
1317
  member.assignedEndDate = before.assignedEndDate ? new Date(before.assignedEndDate) : null
1318
+ member.isAssigner = before.isAssigner === true
1248
1319
  member.updatedAt = new Date()
1249
1320
  await em.flush()
1250
1321
 
@@ -0,0 +1,319 @@
1
+ import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
4
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
5
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
+ import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
8
+ import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
9
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
10
+ import { StaffTeamMember, StaffTimeTask, StaffTimeTaskStatus } from '../data/entities'
11
+ import {
12
+ isActiveProjectAssigner,
13
+ resolveAssignmentGraceDaysFromContainer,
14
+ } from '../lib/time-tracking/projectAssignment'
15
+ import {
16
+ TASK_LIST_CACHE_RESOURCE,
17
+ emitTaskAssigneeChanged,
18
+ taskCrudEvents,
19
+ taskCrudIndexer,
20
+ } from './timesheets-tasks'
21
+ import {
22
+ applyScopeToWhere,
23
+ commandActorScope,
24
+ ensureOrganizationScope,
25
+ ensureTenantScope,
26
+ extractUndoPayload,
27
+ } from './shared'
28
+
29
+ export const staffTimeTaskAssignmentCommandIds = {
30
+ claim: 'staff.timesheets.tasks.claim',
31
+ release: 'staff.timesheets.tasks.release',
32
+ } as const
33
+
34
+ export const TASK_ALREADY_CLAIMED_CODE = 'task_already_claimed'
35
+ export const TASK_NOT_CLAIMABLE_CODE = 'task_not_claimable'
36
+ export const TASK_NOT_ASSIGNER_CODE = 'task_not_assigner'
37
+ export const TASK_NOT_ASSIGNEE_CODE = 'task_not_assignee'
38
+ export const TASK_ASSIGNMENT_CHANGED_CODE = 'task_assignment_changed'
39
+
40
+ const assignmentInputSchema = z.object({ id: z.string().uuid() })
41
+
42
+ export type StaffTimeTaskAssignmentInput = z.infer<typeof assignmentInputSchema>
43
+
44
+ export type StaffTimeTaskAssignmentResult = {
45
+ taskId: string
46
+ tenantId: string
47
+ organizationId: string
48
+ timeProjectId: string
49
+ assigneeStaffMemberId: string | null
50
+ updatedAt: string | null
51
+ }
52
+
53
+ type AssignmentUndoPayload = {
54
+ taskId: string
55
+ tenantId: string
56
+ organizationId: string
57
+ previousAssigneeStaffMemberId: string | null
58
+ nextAssigneeStaffMemberId: string | null
59
+ }
60
+
61
+ type Translate = (key: string, fallback: string) => string
62
+
63
+ function notFound(translate: Translate): CrudHttpError {
64
+ return new CrudHttpError(404, {
65
+ error: translate('staff.time_tracking.tasks.errors.notFound', 'Task not found or not accessible.'),
66
+ })
67
+ }
68
+
69
+ function notClaimable(translate: Translate): CrudHttpError {
70
+ return new CrudHttpError(422, {
71
+ code: TASK_NOT_CLAIMABLE_CODE,
72
+ error: translate('staff.timesheets.errors.taskNotClaimable', 'A finished task cannot be claimed.'),
73
+ })
74
+ }
75
+
76
+ function alreadyClaimed(translate: Translate): CrudHttpError {
77
+ return new CrudHttpError(409, {
78
+ code: TASK_ALREADY_CLAIMED_CODE,
79
+ error: translate('staff.timesheets.errors.taskAlreadyClaimed', 'Someone else already took this task.'),
80
+ })
81
+ }
82
+
83
+ async function loadDoneStatusIds(em: EntityManager, task: StaffTimeTask): Promise<string[]> {
84
+ const statuses = await em.find(
85
+ StaffTimeTaskStatus,
86
+ { timeProjectId: task.timeProjectId, tenantId: task.tenantId, organizationId: task.organizationId, isDone: true },
87
+ { fields: ['id'] },
88
+ )
89
+ return statuses.map((status) => status.id)
90
+ }
91
+
92
+ async function resolveActorStaffMemberId(em: EntityManager, ctx: CommandRuntimeContext, task: StaffTimeTask): Promise<string | null> {
93
+ const userId = ctx.auth?.sub ?? null
94
+ if (!userId) return null
95
+ const member = await findOneWithDecryption(
96
+ em,
97
+ StaffTeamMember,
98
+ { userId, tenantId: task.tenantId, organizationId: task.organizationId, deletedAt: null },
99
+ undefined,
100
+ { tenantId: task.tenantId, organizationId: task.organizationId },
101
+ )
102
+ return member?.id ?? null
103
+ }
104
+
105
+ async function loadScopedTask(em: EntityManager, ctx: CommandRuntimeContext, id: string, translate: Translate): Promise<StaffTimeTask> {
106
+ const task = await em.findOne(
107
+ StaffTimeTask,
108
+ applyScopeToWhere<StaffTimeTask>({ id, deletedAt: null }, commandActorScope(ctx)),
109
+ )
110
+ if (!task) throw notFound(translate)
111
+ ensureTenantScope(ctx, task.tenantId)
112
+ ensureOrganizationScope(ctx, task.organizationId)
113
+ return task
114
+ }
115
+
116
+ async function emitTaskSideEffects(ctx: CommandRuntimeContext, task: StaffTimeTask, undo = false): Promise<void> {
117
+ const params = {
118
+ dataEngine: ctx.container.resolve('dataEngine') as DataEngine,
119
+ action: 'updated' as const,
120
+ entity: task,
121
+ identifiers: { id: task.id, organizationId: task.organizationId, tenantId: task.tenantId },
122
+ events: taskCrudEvents,
123
+ indexer: taskCrudIndexer,
124
+ }
125
+ if (undo) await emitCrudUndoSideEffects(params)
126
+ else await emitCrudSideEffects(params)
127
+ }
128
+
129
+ /**
130
+ * The conditional UPDATE is the only moment this writer owns the row, so the outcome is
131
+ * built from what it wrote rather than re-read: a later writer may already have replaced
132
+ * the assignee, and reporting or undoing their value would be wrong.
133
+ */
134
+ function captureWrittenState(
135
+ em: EntityManager,
136
+ task: StaffTimeTask,
137
+ assigneeStaffMemberId: string | null,
138
+ updatedAt: Date,
139
+ ): StaffTimeTask {
140
+ em.clear()
141
+ task.assigneeStaffMemberId = assigneeStaffMemberId
142
+ task.updatedAt = updatedAt
143
+ return task
144
+ }
145
+
146
+ function toResult(task: StaffTimeTask): StaffTimeTaskAssignmentResult {
147
+ return {
148
+ taskId: task.id,
149
+ tenantId: task.tenantId,
150
+ organizationId: task.organizationId,
151
+ timeProjectId: task.timeProjectId,
152
+ assigneeStaffMemberId: task.assigneeStaffMemberId ?? null,
153
+ updatedAt: task.updatedAt ? task.updatedAt.toISOString() : null,
154
+ }
155
+ }
156
+
157
+ async function restoreAssignee(
158
+ ctx: CommandRuntimeContext,
159
+ payload: AssignmentUndoPayload,
160
+ ): Promise<void> {
161
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
162
+ const where = {
163
+ id: payload.taskId,
164
+ tenantId: payload.tenantId,
165
+ organizationId: payload.organizationId,
166
+ assigneeStaffMemberId: payload.nextAssigneeStaffMemberId,
167
+ deletedAt: null,
168
+ }
169
+ const current = await em.findOne(StaffTimeTask, where)
170
+ if (!current) return
171
+ const now = new Date()
172
+ const affected = await em.nativeUpdate(
173
+ StaffTimeTask,
174
+ where,
175
+ { assigneeStaffMemberId: payload.previousAssigneeStaffMemberId, updatedAt: now },
176
+ )
177
+ if (affected === 0) return
178
+ const task = captureWrittenState(em, current, payload.previousAssigneeStaffMemberId, now)
179
+ await emitTaskSideEffects(ctx, task, true)
180
+ emitTaskAssigneeChanged(task, payload.nextAssigneeStaffMemberId, 'manual', ctx.auth?.sub ?? null)
181
+ }
182
+
183
+ const claimTaskCommand: CommandHandler<StaffTimeTaskAssignmentInput, StaffTimeTaskAssignmentResult> = {
184
+ id: staffTimeTaskAssignmentCommandIds.claim,
185
+ async execute(rawInput, ctx) {
186
+ const parsed = assignmentInputSchema.parse(rawInput)
187
+ const { translate } = await resolveTranslations()
188
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
189
+ const task = await loadScopedTask(em, ctx, parsed.id, translate)
190
+ const scope = { tenantId: task.tenantId, organizationId: task.organizationId }
191
+
192
+ const staffMemberId = await resolveActorStaffMemberId(em, ctx, task)
193
+ const graceDays = await resolveAssignmentGraceDaysFromContainer(ctx.container, task.tenantId)
194
+ const isAssigner = staffMemberId
195
+ ? await isActiveProjectAssigner(em, scope, task.timeProjectId, staffMemberId, { graceDays })
196
+ : false
197
+ if (!staffMemberId || !isAssigner) {
198
+ throw new CrudHttpError(403, {
199
+ code: TASK_NOT_ASSIGNER_CODE,
200
+ error: translate('staff.timesheets.errors.notAssigner', 'Only active assigners of this project can claim tasks.'),
201
+ })
202
+ }
203
+
204
+ const doneStatusIds = await loadDoneStatusIds(em, task)
205
+ if (doneStatusIds.includes(task.taskStatusId)) throw notClaimable(translate)
206
+ if (task.assigneeStaffMemberId) throw alreadyClaimed(translate)
207
+
208
+ const now = new Date()
209
+ const affected = await em.nativeUpdate(
210
+ StaffTimeTask,
211
+ {
212
+ id: task.id,
213
+ tenantId: task.tenantId,
214
+ organizationId: task.organizationId,
215
+ assigneeStaffMemberId: null,
216
+ deletedAt: null,
217
+ ...(doneStatusIds.length > 0 ? { taskStatusId: { $nin: doneStatusIds } } : {}),
218
+ },
219
+ { assigneeStaffMemberId: staffMemberId, updatedAt: now },
220
+ )
221
+ if (affected === 0) {
222
+ em.clear()
223
+ const current = await em.findOne(StaffTimeTask, { id: task.id, tenantId: task.tenantId, organizationId: task.organizationId, deletedAt: null })
224
+ if (!current) throw notFound(translate)
225
+ if (!current.assigneeStaffMemberId && doneStatusIds.includes(current.taskStatusId)) throw notClaimable(translate)
226
+ throw alreadyClaimed(translate)
227
+ }
228
+ const claimed = captureWrittenState(em, task, staffMemberId, now)
229
+ await emitTaskSideEffects(ctx, claimed)
230
+ emitTaskAssigneeChanged(claimed, null, 'claim', ctx.auth?.sub ?? null)
231
+ return toResult(claimed)
232
+ },
233
+ buildLog: async ({ result }) => {
234
+ const { translate } = await resolveTranslations()
235
+ return {
236
+ actionLabel: translate('staff.audit.timesheets.tasks.claim', 'Claim task'),
237
+ resourceKind: 'staff.timesheets.task',
238
+ resourceId: result.taskId,
239
+ tenantId: result.tenantId,
240
+ organizationId: result.organizationId,
241
+ context: { cacheAliases: [TASK_LIST_CACHE_RESOURCE] },
242
+ payload: {
243
+ undo: {
244
+ taskId: result.taskId,
245
+ tenantId: result.tenantId,
246
+ organizationId: result.organizationId,
247
+ previousAssigneeStaffMemberId: null,
248
+ nextAssigneeStaffMemberId: result.assigneeStaffMemberId,
249
+ } satisfies AssignmentUndoPayload,
250
+ },
251
+ }
252
+ },
253
+ undo: async ({ logEntry, ctx }) => {
254
+ const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)
255
+ if (!payload?.taskId) return
256
+ await restoreAssignee(ctx, payload)
257
+ },
258
+ }
259
+
260
+ const releaseTaskCommand: CommandHandler<StaffTimeTaskAssignmentInput, StaffTimeTaskAssignmentResult & { previousAssigneeStaffMemberId: string | null }> = {
261
+ id: staffTimeTaskAssignmentCommandIds.release,
262
+ async execute(rawInput, ctx) {
263
+ const parsed = assignmentInputSchema.parse(rawInput)
264
+ const { translate } = await resolveTranslations()
265
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
266
+ const task = await loadScopedTask(em, ctx, parsed.id, translate)
267
+ const staffMemberId = await resolveActorStaffMemberId(em, ctx, task)
268
+ if (!staffMemberId || task.assigneeStaffMemberId !== staffMemberId) {
269
+ throw new CrudHttpError(403, {
270
+ code: TASK_NOT_ASSIGNEE_CODE,
271
+ error: translate('staff.timesheets.errors.notAssignee', 'Only the current assignee can release this task.'),
272
+ })
273
+ }
274
+ const now = new Date()
275
+ const affected = await em.nativeUpdate(
276
+ StaffTimeTask,
277
+ { id: task.id, tenantId: task.tenantId, organizationId: task.organizationId, assigneeStaffMemberId: staffMemberId, deletedAt: null },
278
+ { assigneeStaffMemberId: null, updatedAt: now },
279
+ )
280
+ if (affected === 0) {
281
+ throw new CrudHttpError(409, {
282
+ code: TASK_ASSIGNMENT_CHANGED_CODE,
283
+ error: translate('staff.timesheets.errors.taskChanged', 'The task changed while you were releasing it. Refresh and try again.'),
284
+ })
285
+ }
286
+ const released = captureWrittenState(em, task, null, now)
287
+ await emitTaskSideEffects(ctx, released)
288
+ emitTaskAssigneeChanged(released, staffMemberId, 'release', ctx.auth?.sub ?? null)
289
+ return { ...toResult(released), previousAssigneeStaffMemberId: staffMemberId }
290
+ },
291
+ buildLog: async ({ result }) => {
292
+ const { translate } = await resolveTranslations()
293
+ return {
294
+ actionLabel: translate('staff.audit.timesheets.tasks.release', 'Release task'),
295
+ resourceKind: 'staff.timesheets.task',
296
+ resourceId: result.taskId,
297
+ tenantId: result.tenantId,
298
+ organizationId: result.organizationId,
299
+ context: { cacheAliases: [TASK_LIST_CACHE_RESOURCE] },
300
+ payload: {
301
+ undo: {
302
+ taskId: result.taskId,
303
+ tenantId: result.tenantId,
304
+ organizationId: result.organizationId,
305
+ previousAssigneeStaffMemberId: result.previousAssigneeStaffMemberId,
306
+ nextAssigneeStaffMemberId: null,
307
+ } satisfies AssignmentUndoPayload,
308
+ },
309
+ }
310
+ },
311
+ undo: async ({ logEntry, ctx }) => {
312
+ const payload = extractUndoPayload<AssignmentUndoPayload>(logEntry)
313
+ if (!payload?.taskId) return
314
+ await restoreAssignee(ctx, payload)
315
+ },
316
+ }
317
+
318
+ registerCommand(claimTaskCommand)
319
+ registerCommand(releaseTaskCommand)