@open-mercato/core 0.10.1-develop.7716.1.cefe7975df → 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
@@ -0,0 +1,172 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
+ import { StaffTeamMember, StaffTimeProjectMember } from '../../data/entities'
4
+ import { DEFAULT_ASSIGNMENT_GRACE_DAYS, MAX_ASSIGNMENT_GRACE_DAYS } from './settingKeys'
5
+
6
+ const MS_PER_DAY = 86_400_000
7
+ const ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/
8
+
9
+ export function normalizeAssignmentGraceDays(value: unknown): number {
10
+ if (typeof value !== 'number' || !Number.isInteger(value)) return DEFAULT_ASSIGNMENT_GRACE_DAYS
11
+ if (value < 0 || value > MAX_ASSIGNMENT_GRACE_DAYS) return DEFAULT_ASSIGNMENT_GRACE_DAYS
12
+ return value
13
+ }
14
+
15
+ /**
16
+ * `assigned_start_date` / `assigned_end_date` are `date` columns, so both bounds
17
+ * are compared as whole days rather than timestamps. Date instances are read
18
+ * through their local calendar fields because the postgres driver materialises a
19
+ * `date` as local midnight, and "today" likewise means the server's local day.
20
+ */
21
+ export function toDayIndex(value: unknown): number | null {
22
+ if (value == null) return null
23
+ if (value instanceof Date) {
24
+ if (Number.isNaN(value.getTime())) return null
25
+ return Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()) / MS_PER_DAY
26
+ }
27
+ if (typeof value === 'string') {
28
+ const match = ISO_DATE_PREFIX.exec(value.trim())
29
+ if (!match) return null
30
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) / MS_PER_DAY
31
+ }
32
+ return null
33
+ }
34
+
35
+ /**
36
+ * An entry date is a calendar day the client sent as `YYYY-MM-DD`, which
37
+ * `z.coerce.date()` turns into UTC midnight — so a Date is read through its UTC
38
+ * fields here, the same way the grid addresses a cell (D-8).
39
+ */
40
+ export function toEntryDayIndex(value: unknown): number | null {
41
+ if (value instanceof Date) {
42
+ if (Number.isNaN(value.getTime())) return null
43
+ return Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY
44
+ }
45
+ return toDayIndex(value)
46
+ }
47
+
48
+ /**
49
+ * A membership only grants access inside its assignment window: from
50
+ * `assigned_start_date` until `assigned_end_date` plus the tenant's grace days.
51
+ * A null end date is open-ended and never expires.
52
+ */
53
+ export function isWithinAssignmentWindow(
54
+ membership: Pick<StaffTimeProjectMember, 'assignedStartDate' | 'assignedEndDate'>,
55
+ todayIndex: number,
56
+ graceDays: number,
57
+ ): boolean {
58
+ if (membership.assignedStartDate != null) {
59
+ const startIndex = toDayIndex(membership.assignedStartDate)
60
+ if (startIndex === null || todayIndex < startIndex) return false
61
+ }
62
+ if (membership.assignedEndDate == null) return true
63
+ const endIndex = toDayIndex(membership.assignedEndDate)
64
+ if (endIndex === null) return false
65
+ return todayIndex <= endIndex + graceDays
66
+ }
67
+
68
+
69
+ export type ProjectAssignmentScope = {
70
+ tenantId: string
71
+ organizationId: string
72
+ }
73
+
74
+ export type ActiveProjectMember = {
75
+ membershipId: string
76
+ staffMemberId: string
77
+ userId: string | null
78
+ isAssigner: boolean
79
+ }
80
+
81
+ type MembershipLike = Pick<
82
+ StaffTimeProjectMember,
83
+ 'status' | 'deletedAt' | 'assignedStartDate' | 'assignedEndDate'
84
+ >
85
+
86
+ export function isActiveAssignment(membership: MembershipLike, now: Date, graceDays: number): boolean {
87
+ if (membership.status !== 'active' || membership.deletedAt) return false
88
+ const todayIndex = toDayIndex(now)
89
+ if (todayIndex === null) return false
90
+ return isWithinAssignmentWindow(membership, todayIndex, normalizeAssignmentGraceDays(graceDays))
91
+ }
92
+
93
+ export async function loadActiveProjectMembers(
94
+ em: EntityManager,
95
+ scope: ProjectAssignmentScope,
96
+ timeProjectId: string,
97
+ options: { graceDays?: number | null; now?: Date; assignersOnly?: boolean } = {},
98
+ ): Promise<ActiveProjectMember[]> {
99
+ const where: Record<string, unknown> = {
100
+ timeProjectId,
101
+ tenantId: scope.tenantId,
102
+ organizationId: scope.organizationId,
103
+ status: 'active',
104
+ deletedAt: null,
105
+ }
106
+ if (options.assignersOnly) where.isAssigner = true
107
+ const memberships = await em.find(StaffTimeProjectMember, where)
108
+ const now = options.now ?? new Date()
109
+ const graceDays = normalizeAssignmentGraceDays(options.graceDays)
110
+ const active = memberships.filter((membership) => isActiveAssignment(membership, now, graceDays))
111
+ if (active.length === 0) return []
112
+ const staffMembers = await findWithDecryption(
113
+ em,
114
+ StaffTeamMember,
115
+ {
116
+ id: { $in: Array.from(new Set(active.map((membership) => membership.staffMemberId))) },
117
+ tenantId: scope.tenantId,
118
+ organizationId: scope.organizationId,
119
+ deletedAt: null,
120
+ },
121
+ {},
122
+ scope,
123
+ )
124
+ const userByMember = new Map(staffMembers.map((member) => [member.id, member.userId ?? null]))
125
+ return active
126
+ .filter((membership) => userByMember.has(membership.staffMemberId))
127
+ .map((membership) => ({
128
+ membershipId: membership.id,
129
+ staffMemberId: membership.staffMemberId,
130
+ userId: userByMember.get(membership.staffMemberId) ?? null,
131
+ isAssigner: membership.isAssigner === true,
132
+ }))
133
+ }
134
+
135
+ export async function resolveUsableDefaultAssignee(
136
+ em: EntityManager,
137
+ scope: ProjectAssignmentScope,
138
+ project: { id: string; defaultAssigneeStaffMemberId?: string | null },
139
+ options: { graceDays?: number | null; now?: Date } = {},
140
+ ): Promise<string | null> {
141
+ const candidate = project.defaultAssigneeStaffMemberId ?? null
142
+ if (!candidate) return null
143
+ const members = await loadActiveProjectMembers(em, scope, project.id, options)
144
+ return members.some((member) => member.staffMemberId === candidate) ? candidate : null
145
+ }
146
+
147
+ export async function isActiveProjectAssigner(
148
+ em: EntityManager,
149
+ scope: ProjectAssignmentScope,
150
+ timeProjectId: string,
151
+ staffMemberId: string,
152
+ options: { graceDays?: number | null; now?: Date } = {},
153
+ ): Promise<boolean> {
154
+ const assigners = await loadActiveProjectMembers(em, scope, timeProjectId, { ...options, assignersOnly: true })
155
+ return assigners.some((member) => member.staffMemberId === staffMemberId)
156
+ }
157
+
158
+ type ConfigContainer = { resolve: (name: string) => unknown }
159
+
160
+ export async function resolveAssignmentGraceDaysFromContainer(
161
+ container: ConfigContainer,
162
+ tenantId: string,
163
+ ): Promise<number | null> {
164
+ try {
165
+ const { readTimeTrackingSettings } = await import('./settings')
166
+ const configService = container.resolve('moduleConfigService') as Parameters<typeof readTimeTrackingSettings>[0]
167
+ const settings = await readTimeTrackingSettings(configService, { tenantId })
168
+ return settings.access.assignmentGraceDays
169
+ } catch {
170
+ return null
171
+ }
172
+ }
@@ -26,6 +26,9 @@ export const TIME_TRACKING_DEFAULTS_GROUP = 'defaults'
26
26
  export const TIME_TRACKING_TARGETS_GROUP = 'targets'
27
27
  export const TIME_TRACKING_WARNINGS_GROUP = 'warnings'
28
28
  export const TIME_TRACKING_ACCESS_GROUP = 'access'
29
+ export const TIME_TRACKING_SUPPORT_GROUP = 'support'
30
+ export const DEFAULT_SUPPORT_REFERENCE_PREFIX = 'SUP'
31
+ export const supportReferencePrefixSchema = z.string().regex(/^[A-Z][A-Z0-9]{1,9}$/)
29
32
 
30
33
  /**
31
34
  * Days past `assigned_end_date` that project access survives (spec D-12). The
@@ -198,6 +201,14 @@ const BUILT_IN_SETTING_KEYS: readonly TimeTrackingSettingKeyInput[] = [
198
201
  labelKey: 'staff.time_tracking.settings.access.graceDays',
199
202
  priority: BUILT_IN_STRATEGY_PRIORITY,
200
203
  },
204
+ {
205
+ group: TIME_TRACKING_SUPPORT_GROUP,
206
+ key: 'referencePrefix',
207
+ schema: supportReferencePrefixSchema,
208
+ default: DEFAULT_SUPPORT_REFERENCE_PREFIX,
209
+ labelKey: 'staff.support.settings.referencePrefix',
210
+ priority: BUILT_IN_STRATEGY_PRIORITY,
211
+ },
201
212
  ]
202
213
 
203
214
  for (const input of BUILT_IN_SETTING_KEYS) {
@@ -8,10 +8,12 @@ import {
8
8
  import {
9
9
  DEFAULT_ASSIGNMENT_GRACE_DAYS,
10
10
  MAX_ASSIGNMENT_GRACE_DAYS,
11
+ TIME_TRACKING_SUPPORT_GROUP,
11
12
  buildDefaultTimeTrackingSettings,
12
13
  listTimeTrackingSettingKeys,
13
14
  normalizeTimeTrackingSettingsRecord,
14
15
  registerTimeTrackingSettingKey,
16
+ supportReferencePrefixSchema,
15
17
  timeTrackingSettingKeyIds,
16
18
  type TimeTrackingSettingKeyDefinition,
17
19
  type TimeTrackingSettingKeyInput,
@@ -32,9 +34,10 @@ export const TIME_TRACKING_TARGETS_DAILY_HOURS_KEY = 'targets.dailyHours'
32
34
  export const TIME_TRACKING_WARNINGS_OVERLAP_KEY = 'warnings.overlap'
33
35
  export const TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY = 'warnings.runningTimer'
34
36
  export const TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY = 'access.assignmentGraceDays'
37
+ export const TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY = 'support.referencePrefix'
35
38
 
36
39
  /**
37
- * The eight keys the module shipped. FROZEN — a stored config row is named after one
40
+ * The built-in keys the module ships. FROZEN — a stored config row is named after one
38
41
  * of these, so none may be renamed or removed. The list is no longer the whole truth:
39
42
  * `timeTrackingSettingKeyIds()` returns these plus every contributed key (EP-42).
40
43
  */
@@ -47,6 +50,7 @@ export const TIME_TRACKING_SETTING_KEYS = [
47
50
  TIME_TRACKING_WARNINGS_OVERLAP_KEY,
48
51
  TIME_TRACKING_WARNINGS_RUNNING_TIMER_KEY,
49
52
  TIME_TRACKING_ACCESS_ASSIGNMENT_GRACE_DAYS_KEY,
53
+ TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,
50
54
  ] as const
51
55
 
52
56
  export type TimeTrackingSettingKey = (typeof TIME_TRACKING_SETTING_KEYS)[number]
@@ -69,12 +73,21 @@ export type TimeTrackingAccess = {
69
73
  assignmentGraceDays: number
70
74
  }
71
75
 
76
+ export type TimeTrackingSupport = {
77
+ referencePrefix: string
78
+ }
79
+
72
80
  export type TimeTrackingBuiltInSettings = {
73
81
  rounding: RoundingSettings
74
82
  defaults: TimeTrackingEntryDefaults
75
83
  targets: TimeTrackingTargets
76
84
  warnings: TimeTrackingWarnings
77
85
  access: TimeTrackingAccess
86
+ /**
87
+ * Optional in the type so code that builds a settings object without the support
88
+ * group keeps compiling; a normalized read always carries it.
89
+ */
90
+ support?: TimeTrackingSupport
78
91
  }
79
92
 
80
93
  /**
@@ -166,3 +179,35 @@ export async function writeTimeTrackingSettings(
166
179
 
167
180
  return settings
168
181
  }
182
+
183
+ function readSettingsGroup(input: Record<string, unknown>, group: string): Record<string, unknown> | null {
184
+ const value = input[group]
185
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
186
+ return value as Record<string, unknown>
187
+ }
188
+
189
+ /**
190
+ * A settings payload from a client that predates the support group omits it. That
191
+ * omission keeps the tenant's stored reference prefix instead of resetting it to the
192
+ * default, so an older settings screen cannot silently rename future ticket references.
193
+ */
194
+ export async function preserveOmittedSupportSettings(
195
+ configService: ModuleConfigService,
196
+ scope: TimeTrackingSettingsScope,
197
+ input: Record<string, unknown>,
198
+ ): Promise<Record<string, unknown>> {
199
+ const supportGroup = readSettingsGroup(input, TIME_TRACKING_SUPPORT_GROUP)
200
+ if (supportGroup && supportGroup.referencePrefix !== undefined) return input
201
+ const record = await configService.getRecord(
202
+ STAFF_TIME_TRACKING_MODULE_ID,
203
+ TIME_TRACKING_SUPPORT_REFERENCE_PREFIX_KEY,
204
+ { tenantId: scope.tenantId },
205
+ )
206
+ if (!record) return input
207
+ const storedPrefix = supportReferencePrefixSchema.safeParse(record.value)
208
+ if (!storedPrefix.success) return input
209
+ return {
210
+ ...input,
211
+ [TIME_TRACKING_SUPPORT_GROUP]: { ...(supportGroup ?? {}), referencePrefix: storedPrefix.data },
212
+ }
213
+ }
@@ -0,0 +1,165 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
7
+ import { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
+ import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
9
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
+ import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
11
+ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
12
+ import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
13
+ import { createLogger } from '@open-mercato/shared/lib/logger'
14
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
15
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
16
+ import { StaffTimeTask } from '../../data/entities'
17
+ import { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../api/guards'
18
+ import {
19
+ staffTimeTaskAssignmentCommandIds,
20
+ type StaffTimeTaskAssignmentInput,
21
+ type StaffTimeTaskAssignmentResult,
22
+ } from '../../commands/timesheets-task-assignment'
23
+ import { resolveFeatureAccess } from './featureAccess'
24
+ import { assertProjectAccess } from './access'
25
+ import { resolveListProjectAccess } from './listProjectAccess'
26
+
27
+ export type TaskAssignmentAction = keyof typeof staffTimeTaskAssignmentCommandIds
28
+
29
+ export const TASK_ASSIGNMENT_VIEW_FEATURE = 'staff.timesheets.tasks.view'
30
+
31
+ const FAILURE_CODES: Record<TaskAssignmentAction, string> = {
32
+ claim: 'staff.task_claim_failed',
33
+ release: 'staff.task_release_failed',
34
+ }
35
+
36
+ export type TaskAssignmentRouteContext = {
37
+ params?: Promise<Record<string, string | string[]>> | Record<string, string | string[]>
38
+ }
39
+
40
+ export const taskAssignmentResponseSchema = z.object({
41
+ id: z.string().uuid(),
42
+ timeProjectId: z.string().uuid(),
43
+ assigneeStaffMemberId: z.string().uuid().nullable(),
44
+ updatedAt: z.string().nullable(),
45
+ })
46
+
47
+ async function resolveTaskId(req: Request, action: TaskAssignmentAction, context?: TaskAssignmentRouteContext): Promise<string | null> {
48
+ const params = context?.params ? await context.params : null
49
+ const fromParams = params?.id
50
+ if (typeof fromParams === 'string' && fromParams.length > 0) return fromParams
51
+ try {
52
+ const match = new URL(req.url).pathname.match(new RegExp(`/tasks/([^/]+)/${action}`))
53
+ return match?.[1] ?? null
54
+ } catch {
55
+ return null
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The `POST /api/staff/timesheets/tasks/{id}/claim|release` handler. Both actions
61
+ * share every step — scope, project access (the tasks list rule), optimistic lock,
62
+ * mutation guards, the command and the undo header — and differ only in the command.
63
+ */
64
+ export function createTaskAssignmentPostHandler(action: TaskAssignmentAction) {
65
+ const logger = createLogger('staff').child({ component: `api/timesheets/tasks/${action}` })
66
+ return async function POST(req: Request, context?: TaskAssignmentRouteContext): Promise<Response> {
67
+ try {
68
+ const container = await createRequestContainer()
69
+ const auth = await getAuthFromRequest(req)
70
+ const { translate } = await resolveTranslations()
71
+ if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
72
+
73
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
74
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null
75
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
76
+ if (!tenantId || !organizationId) {
77
+ throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
78
+ }
79
+ const taskId = await resolveTaskId(req, action, context)
80
+ if (!taskId || !z.string().uuid().safeParse(taskId).success) {
81
+ throw new CrudHttpError(400, { error: translate('staff.time_tracking.tasks.errors.idRequired', 'Task id is required.') })
82
+ }
83
+
84
+ const actorId = auth.sub ?? ''
85
+ const viewAccess = await resolveFeatureAccess(container, actorId, [TASK_ASSIGNMENT_VIEW_FEATURE], { tenantId, organizationId })
86
+ if (!viewAccess.allowed) throw forbidden(translate('staff.errors.forbidden', 'Forbidden'))
87
+
88
+ const routeCtx: CommandRuntimeContext = {
89
+ container,
90
+ auth,
91
+ organizationScope: scope,
92
+ selectedOrganizationId: organizationId,
93
+ organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
94
+ request: req,
95
+ }
96
+
97
+ const em = (container.resolve('em') as EntityManager).fork()
98
+ const task = await em.findOne(StaffTimeTask, { id: taskId, tenantId, organizationId, deletedAt: null })
99
+ const access = task ? await resolveListProjectAccess(routeCtx) : null
100
+ if (!task || !access || !assertProjectAccess(access, task.timeProjectId)) {
101
+ throw new CrudHttpError(404, {
102
+ error: translate('staff.time_tracking.tasks.errors.notFound', 'Task not found or not accessible.'),
103
+ })
104
+ }
105
+
106
+ await enforceCommandOptimisticLockWithGuards(container, {
107
+ resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
108
+ resourceId: task.id,
109
+ current: task.updatedAt,
110
+ request: req,
111
+ })
112
+
113
+ const input: StaffTimeTaskAssignmentInput = { id: taskId }
114
+ const guardResult = await runRouteMutationGuards({
115
+ container,
116
+ req,
117
+ auth: { userId: actorId, tenantId, organizationId, userFeatures: viewAccess.grantedFeatures },
118
+ input: {
119
+ resourceKind: STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
120
+ resourceId: taskId,
121
+ operation: 'update',
122
+ mutationPayload: input,
123
+ },
124
+ })
125
+ if (!guardResult.ok) return guardResult.response
126
+
127
+ const commandBus = container.resolve('commandBus') as CommandBus
128
+ const { result, logEntry } = await commandBus.execute<StaffTimeTaskAssignmentInput, StaffTimeTaskAssignmentResult>(
129
+ staffTimeTaskAssignmentCommandIds[action],
130
+ { input, ctx: routeCtx },
131
+ )
132
+ await guardResult.runAfterSuccess()
133
+
134
+ const response = NextResponse.json({
135
+ id: result?.taskId ?? taskId,
136
+ timeProjectId: result?.timeProjectId ?? task.timeProjectId,
137
+ assigneeStaffMemberId: result?.assigneeStaffMemberId ?? null,
138
+ updatedAt: result?.updatedAt ?? null,
139
+ })
140
+ if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
141
+ response.headers.set(
142
+ 'x-om-operation',
143
+ serializeOperationMetadata({
144
+ id: logEntry.id,
145
+ undoToken: logEntry.undoToken,
146
+ commandId: logEntry.commandId,
147
+ actionLabel: logEntry.actionLabel ?? null,
148
+ resourceKind: logEntry.resourceKind ?? STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTask,
149
+ resourceId: logEntry.resourceId ?? taskId,
150
+ executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
151
+ }),
152
+ )
153
+ }
154
+ return response
155
+ } catch (err) {
156
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
157
+ const interceptorRejection = getCommandInterceptorHttpRejection(err)
158
+ if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
159
+ const { translate } = await resolveTranslations()
160
+ logger.error(`staff.timesheets.tasks.${action} POST failed`, { err })
161
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: FAILURE_CODES[action] })
162
+ return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,183 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
3
+ import { createLogger } from '@open-mercato/shared/lib/logger'
4
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
5
+ import { resolveNotificationService } from '../../../notifications/lib/notificationService'
6
+ import {
7
+ buildBatchNotificationFromType,
8
+ buildFeatureNotificationFromType,
9
+ } from '../../../notifications/lib/notificationBuilder'
10
+ import { StaffTimeProject } from '../../data/entities'
11
+ import { loadActiveProjectMembers, type ProjectAssignmentScope } from './projectAssignment'
12
+
13
+ const logger = createLogger('staff').child({ component: 'lib/time-tracking/workRecipients' })
14
+
15
+ export const PROJECT_MANAGERS_FEATURE = 'staff.timesheets.projects.manage'
16
+
17
+ export type WorkRecipientStep = 1 | 2 | 3 | 4
18
+
19
+ export type ResolvedWorkRecipients =
20
+ | { step: 1 | 2 | 3; userIds: string[] }
21
+ | { step: 4; userIds: [] }
22
+
23
+ export type WorkRecipientsInput = {
24
+ timeProjectId: string
25
+ /** Step 1: the type's own audience. Empty or omitted starts the chain at step 2. */
26
+ audienceUserIds?: readonly (string | null | undefined)[]
27
+ /** Excluded from steps 1–3. Step 4 hands resolution to `createForFeature`, which cannot exclude a user. */
28
+ actorUserId?: string | null
29
+ graceDays?: number | null
30
+ now?: Date
31
+ }
32
+
33
+ function uniqueExcluding(ids: readonly (string | null | undefined)[], actorUserId: string | null): string[] {
34
+ const seen = new Set<string>()
35
+ for (const id of ids) {
36
+ if (typeof id !== 'string' || id.length === 0) continue
37
+ if (actorUserId && id === actorUserId) continue
38
+ seen.add(id)
39
+ }
40
+ return [...seen]
41
+ }
42
+
43
+ /**
44
+ * The escalation chain shared by every project-work notification: the type's own
45
+ * audience, then the project's active assigners, then the project owner, then the
46
+ * organization's project managers. Each step is used only when the one before it
47
+ * is empty. Steps 1–3 are resolved here; step 4 is returned as a marker for
48
+ * `notifyProjectWork`, which sends it through `createForFeature`.
49
+ */
50
+ export async function resolveProjectWorkRecipients(
51
+ em: EntityManager,
52
+ scope: ProjectAssignmentScope,
53
+ input: WorkRecipientsInput,
54
+ ): Promise<ResolvedWorkRecipients> {
55
+ const actorUserId = input.actorUserId ?? null
56
+ const audience = uniqueExcluding(input.audienceUserIds ?? [], actorUserId)
57
+ if (audience.length > 0) return { step: 1, userIds: audience }
58
+
59
+ const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {
60
+ graceDays: input.graceDays,
61
+ now: input.now,
62
+ assignersOnly: true,
63
+ })
64
+ const assignerUserIds = uniqueExcluding(assigners.map((member) => member.userId), actorUserId)
65
+ if (assignerUserIds.length > 0) return { step: 2, userIds: assignerUserIds }
66
+
67
+ const project = await em.findOne(
68
+ StaffTimeProject,
69
+ { id: input.timeProjectId, tenantId: scope.tenantId, organizationId: scope.organizationId },
70
+ { fields: ['id', 'ownerUserId'] },
71
+ )
72
+ const owner = uniqueExcluding([project?.ownerUserId], actorUserId)
73
+ if (owner.length > 0) return { step: 3, userIds: owner }
74
+
75
+ return { step: 4, userIds: [] }
76
+ }
77
+
78
+ export type ActiveAssignerUserIdsInput = {
79
+ timeProjectId: string
80
+ graceDays?: number | null
81
+ now?: Date
82
+ }
83
+
84
+ export async function loadActiveAssignerUserIds(
85
+ em: EntityManager,
86
+ scope: ProjectAssignmentScope,
87
+ input: ActiveAssignerUserIdsInput,
88
+ ): Promise<string[]> {
89
+ const assigners = await loadActiveProjectMembers(em, scope, input.timeProjectId, {
90
+ graceDays: input.graceDays,
91
+ now: input.now,
92
+ assignersOnly: true,
93
+ })
94
+ return uniqueExcluding(assigners.map((member) => member.userId), null)
95
+ }
96
+
97
+ export type ProjectRecipientProbe = {
98
+ id: string
99
+ ownerUserId?: string | null
100
+ defaultAssigneeStaffMemberId?: string | null
101
+ }
102
+
103
+ /**
104
+ * Whether new project work reaches somebody before the chain falls through to the
105
+ * organization's project managers (step 4): an active assigner with a user account,
106
+ * a usable default assignee with a user account (the ticket's step-1 audience), or
107
+ * the project owner. `false` is the state the support routing settings warn about.
108
+ */
109
+ export async function projectReachesRecipientBeforeManagers(
110
+ em: EntityManager,
111
+ scope: ProjectAssignmentScope,
112
+ project: ProjectRecipientProbe,
113
+ options: { graceDays?: number | null; now?: Date } = {},
114
+ ): Promise<boolean> {
115
+ if (typeof project.ownerUserId === 'string' && project.ownerUserId.length > 0) return true
116
+ const members = await loadActiveProjectMembers(em, scope, project.id, options)
117
+ const hasUser = (member: { userId: string | null }) => typeof member.userId === 'string' && member.userId.length > 0
118
+ if (members.some((member) => member.isAssigner && hasUser(member))) return true
119
+ const defaultAssigneeId = project.defaultAssigneeStaffMemberId ?? null
120
+ if (!defaultAssigneeId) return false
121
+ return members.some((member) => member.staffMemberId === defaultAssigneeId && hasUser(member))
122
+ }
123
+
124
+ type ContainerLike = { resolve: <T = unknown>(name: string) => T }
125
+
126
+ export type NotifyProjectWorkOptions = {
127
+ typeDef: NotificationTypeDefinition
128
+ recipients: ResolvedWorkRecipients
129
+ build: {
130
+ titleVariables?: Record<string, string>
131
+ bodyVariables?: Record<string, string>
132
+ sourceEntityType?: string
133
+ sourceEntityId?: string
134
+ linkHref?: string
135
+ groupKey?: string
136
+ }
137
+ }
138
+
139
+ export type NotifyProjectWorkResult = {
140
+ step: WorkRecipientStep
141
+ notifiedCount: number
142
+ }
143
+
144
+ /**
145
+ * Sends one notification to the resolved recipients. Step 4 always passes
146
+ * `restrictRecipientsToOrganization: true`: without it `createForFeature` notifies
147
+ * every holder of the feature in the whole tenant. With the flag on, the service
148
+ * returns no recipients (and logs) when the organization is missing, more than 200
149
+ * tenant-wide candidates hold the feature, the container is unavailable, the RBAC
150
+ * check throws, or nobody in the organization holds it; `notifiedCount` is then 0
151
+ * and the caller decides what "nobody notified" means for its record.
152
+ */
153
+ export async function notifyProjectWork(
154
+ container: ContainerLike,
155
+ scope: ProjectAssignmentScope,
156
+ options: NotifyProjectWorkOptions,
157
+ ): Promise<NotifyProjectWorkResult> {
158
+ const notificationService = resolveNotificationService(container)
159
+ const context = { tenantId: scope.tenantId, organizationId: scope.organizationId }
160
+ const { typeDef, recipients, build } = options
161
+ if (recipients.step === 4) {
162
+ const created = await notificationService.createForFeature(
163
+ {
164
+ ...buildFeatureNotificationFromType(typeDef, { ...build, requiredFeature: PROJECT_MANAGERS_FEATURE }),
165
+ restrictRecipientsToOrganization: true,
166
+ },
167
+ context,
168
+ )
169
+ return { step: 4, notifiedCount: created.length }
170
+ }
171
+ if (recipients.userIds.length === 0) return { step: recipients.step, notifiedCount: 0 }
172
+ try {
173
+ const created = await notificationService.createBatch(
174
+ buildBatchNotificationFromType(typeDef, { ...build, recipientUserIds: recipients.userIds }),
175
+ context,
176
+ )
177
+ return { step: recipients.step, notifiedCount: created.length }
178
+ } catch (err) {
179
+ logger.error('staff project-work notification failed', { err, type: typeDef.type, step: recipients.step })
180
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.project_work_notification_failed' })
181
+ return { step: recipients.step, notifiedCount: 0 }
182
+ }
183
+ }