@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
@@ -0,0 +1,138 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
3
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { notificationTypes } from '../../notifications'
5
+ import { StaffSupportTicket, StaffTeamMember, StaffTimeProject, StaffTimeTask } from '../../data/entities'
6
+ import { resolveAssignmentGraceDaysFromContainer } from '../time-tracking/projectAssignment'
7
+ import {
8
+ loadActiveAssignerUserIds,
9
+ notifyProjectWork,
10
+ resolveProjectWorkRecipients,
11
+ } from '../time-tracking/workRecipients'
12
+
13
+ const logger = createLogger('staff').child({ component: 'lib/support/notifications' })
14
+
15
+ type ContainerLike = { resolve: <T = unknown>(name: string) => T }
16
+
17
+ export type SupportNotificationKind = 'created' | 'customer_replied'
18
+
19
+ const TYPE_BY_KIND: Record<SupportNotificationKind, string> = {
20
+ created: 'staff.support_ticket.created',
21
+ customer_replied: 'staff.support_ticket.customer_replied',
22
+ }
23
+
24
+ async function resolveAssigneeUserId(em: EntityManager, task: StaffTimeTask): Promise<string | null> {
25
+ if (!task.assigneeStaffMemberId) return null
26
+ const member = await findOneWithDecryption(
27
+ em,
28
+ StaffTeamMember,
29
+ { id: task.assigneeStaffMemberId, tenantId: task.tenantId, organizationId: task.organizationId, deletedAt: null },
30
+ undefined,
31
+ { tenantId: task.tenantId, organizationId: task.organizationId },
32
+ )
33
+ return member?.userId ?? null
34
+ }
35
+
36
+ /**
37
+ * Notifies staff about a support ticket through the project escalation chain and
38
+ * records the outcome on the ticket: `unnotified_at` is set (earliest value kept)
39
+ * when the chain reached nobody, and cleared when a notification reaches someone.
40
+ * Notifying is best-effort — the ticket or reply that triggered it is never undone.
41
+ */
42
+ export async function notifySupportTicket(
43
+ container: ContainerLike,
44
+ input: { taskId: string; tenantId: string; organizationId: string; kind: SupportNotificationKind },
45
+ ): Promise<{ notifiedCount: number; step: number } | null> {
46
+ const typeDef = notificationTypes.find((type) => type.type === TYPE_BY_KIND[input.kind])
47
+ if (!typeDef) return null
48
+ const scope = { tenantId: input.tenantId, organizationId: input.organizationId }
49
+ const em = (container.resolve('em') as EntityManager).fork()
50
+ const task = await em.findOne(StaffTimeTask, { id: input.taskId, ...scope, source: 'portal', deletedAt: null })
51
+ if (!task) return null
52
+ const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)
53
+ if (!ticket) return null
54
+ const project = await em.findOne(StaffTimeProject, { id: task.timeProjectId, ...scope }, { fields: ['id', 'name'] })
55
+ const graceDays = await resolveAssignmentGraceDaysFromContainer(container, input.tenantId)
56
+
57
+ const assigneeUserId = await resolveAssigneeUserId(em, task)
58
+ const audience =
59
+ input.kind === 'created'
60
+ ? [assigneeUserId, ...(await loadActiveAssignerUserIds(em, scope, { timeProjectId: task.timeProjectId, graceDays }))]
61
+ : [assigneeUserId]
62
+ const recipients = await resolveProjectWorkRecipients(em, scope, {
63
+ timeProjectId: task.timeProjectId,
64
+ audienceUserIds: audience,
65
+ graceDays,
66
+ })
67
+
68
+ const snapshot = ticket.customerSnapshot ?? {}
69
+ const customerName =
70
+ (typeof snapshot.reporterDisplayName === 'string' && snapshot.reporterDisplayName) ||
71
+ (typeof snapshot.displayName === 'string' && snapshot.displayName) ||
72
+ ''
73
+ const request = ticket.customerRequestSnapshot
74
+ const subject = request && typeof request.subject === 'string' ? request.subject : task.title
75
+ const result = await notifyProjectWork(container, scope, {
76
+ typeDef,
77
+ recipients,
78
+ build: {
79
+ titleVariables: { reference: ticket.supportReference },
80
+ bodyVariables: {
81
+ reference: ticket.supportReference,
82
+ customer: customerName,
83
+ subject,
84
+ category: ticket.categorySnapshot?.label ?? ticket.categoryValue,
85
+ project: project?.name ?? '',
86
+ },
87
+ sourceEntityType: 'staff:staff_time_task',
88
+ sourceEntityId: task.id,
89
+ linkHref: `/backend/staff/time-tracking/support?task=${encodeURIComponent(task.id)}`,
90
+ groupKey: `staff:support-ticket-${input.kind}:${task.id}`,
91
+ },
92
+ })
93
+
94
+ const fresh = await findOneWithDecryption(em, StaffSupportTicket, { id: ticket.id, ...scope }, undefined, scope)
95
+ if (fresh) {
96
+ if (result.notifiedCount === 0) {
97
+ if (!fresh.unnotifiedAt) {
98
+ fresh.unnotifiedAt = new Date()
99
+ fresh.updatedAt = new Date()
100
+ await em.flush()
101
+ }
102
+ logger.warn('staff support ticket notification reached nobody', {
103
+ tenantId: input.tenantId,
104
+ organizationId: input.organizationId,
105
+ timeProjectId: task.timeProjectId,
106
+ ticketId: ticket.id,
107
+ taskId: task.id,
108
+ eventType: typeDef.type,
109
+ emptyStep: result.step,
110
+ })
111
+ } else if (fresh.unnotifiedAt) {
112
+ fresh.unnotifiedAt = null
113
+ fresh.updatedAt = new Date()
114
+ await em.flush()
115
+ }
116
+ }
117
+ return result
118
+ }
119
+
120
+ /** The ticket got an assignee, so somebody now owns it: clear the "nobody notified" flag. */
121
+ export async function clearSupportTicketUnnotified(
122
+ em: EntityManager,
123
+ input: { taskId: string; tenantId: string; organizationId: string },
124
+ ): Promise<boolean> {
125
+ const scope = { tenantId: input.tenantId, organizationId: input.organizationId }
126
+ const ticket = await findOneWithDecryption(
127
+ em,
128
+ StaffSupportTicket,
129
+ { taskId: input.taskId, ...scope, deletedAt: null, unnotifiedAt: { $ne: null } },
130
+ undefined,
131
+ scope,
132
+ )
133
+ if (!ticket) return false
134
+ ticket.unnotifiedAt = null
135
+ ticket.updatedAt = new Date()
136
+ await em.flush()
137
+ return true
138
+ }
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod'
2
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
+ import { createLogger } from '@open-mercato/shared/lib/logger'
4
+ import type { Attachment as AttachmentEntity } from '@open-mercato/core/modules/attachments/data/entities'
5
+ import type { SupportPortalScope } from './portalTickets'
6
+ import {
7
+ CUSTOMER_UPLOAD_TAG_PREFIX,
8
+ CUSTOMER_VISIBLE_ATTACHMENT_TAG,
9
+ SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
10
+ customerUploadTag,
11
+ } from './attachments'
12
+
13
+ const logger = createLogger('staff').child({ component: 'lib/support/portalAttachments' })
14
+
15
+ export type PortalAttachmentsApi = {
16
+ Attachment: typeof import('@open-mercato/core/modules/attachments/data/entities').Attachment
17
+ AttachmentPartition: typeof import('@open-mercato/core/modules/attachments/data/entities').AttachmentPartition
18
+ StorageDriverFactory: typeof import('@open-mercato/core/modules/attachments/lib/drivers').StorageDriverFactory
19
+ checkAttachmentAccess: typeof import('@open-mercato/core/modules/attachments/lib/access').checkAttachmentAccess
20
+ attachmentCrudEvents: typeof import('@open-mercato/core/modules/attachments/lib/crud').attachmentCrudEvents
21
+ attachmentCrudIndexer: typeof import('@open-mercato/core/modules/attachments/lib/crud').attachmentCrudIndexer
22
+ readAttachmentMetadata: typeof import('@open-mercato/core/modules/attachments/lib/metadata').readAttachmentMetadata
23
+ slugifyAttachmentFileName: typeof import('@open-mercato/core/modules/attachments/lib/imageUrls').slugifyAttachmentFileName
24
+ clearAttachmentThumbnailCache: typeof import('@open-mercato/core/modules/attachments/lib/thumbnailCache').clearAttachmentThumbnailCache
25
+ isMultipartRequestWithinUploadLimit: typeof import('@open-mercato/core/modules/attachments/lib/upload-limits').isMultipartRequestWithinUploadLimit
26
+ resolveAttachmentMaxBytes: typeof import('@open-mercato/core/modules/attachments/lib/upload-limits').resolveAttachmentMaxBytes
27
+ isScopedAttachmentUploadError: typeof import('@open-mercato/core/modules/attachments/lib/scoped-upload-service').isScopedAttachmentUploadError
28
+ }
29
+
30
+ export const portalSupportAttachmentItemSchema = z.object({
31
+ id: z.string().uuid(),
32
+ fileName: z.string(),
33
+ fileSize: z.number(),
34
+ mimeType: z.string().nullable(),
35
+ downloadUrl: z.string(),
36
+ uploadedByCustomer: z.boolean(),
37
+ canDelete: z.boolean(),
38
+ createdAt: z.string().nullable(),
39
+ })
40
+
41
+ export type PortalSupportAttachmentItem = z.infer<typeof portalSupportAttachmentItemSchema>
42
+
43
+ /**
44
+ * Staff keeps no static edge to the attachments module: it is loaded on demand and a
45
+ * deployment without it reads as "attachments unavailable" (`null`).
46
+ */
47
+ export async function loadPortalAttachmentsApi(): Promise<PortalAttachmentsApi | null> {
48
+ try {
49
+ const [entities, drivers, access, crud, metadataLib, imageUrls, thumbnails, limits, uploadService] = await Promise.all([
50
+ import('@open-mercato/core/modules/attachments/data/entities'),
51
+ import('@open-mercato/core/modules/attachments/lib/drivers'),
52
+ import('@open-mercato/core/modules/attachments/lib/access'),
53
+ import('@open-mercato/core/modules/attachments/lib/crud'),
54
+ import('@open-mercato/core/modules/attachments/lib/metadata'),
55
+ import('@open-mercato/core/modules/attachments/lib/imageUrls'),
56
+ import('@open-mercato/core/modules/attachments/lib/thumbnailCache'),
57
+ import('@open-mercato/core/modules/attachments/lib/upload-limits'),
58
+ import('@open-mercato/core/modules/attachments/lib/scoped-upload-service'),
59
+ ])
60
+ return {
61
+ Attachment: entities.Attachment,
62
+ AttachmentPartition: entities.AttachmentPartition,
63
+ StorageDriverFactory: drivers.StorageDriverFactory,
64
+ checkAttachmentAccess: access.checkAttachmentAccess,
65
+ attachmentCrudEvents: crud.attachmentCrudEvents,
66
+ attachmentCrudIndexer: crud.attachmentCrudIndexer,
67
+ readAttachmentMetadata: metadataLib.readAttachmentMetadata,
68
+ slugifyAttachmentFileName: imageUrls.slugifyAttachmentFileName,
69
+ clearAttachmentThumbnailCache: thumbnails.clearAttachmentThumbnailCache,
70
+ isMultipartRequestWithinUploadLimit: limits.isMultipartRequestWithinUploadLimit,
71
+ resolveAttachmentMaxBytes: limits.resolveAttachmentMaxBytes,
72
+ isScopedAttachmentUploadError: uploadService.isScopedAttachmentUploadError,
73
+ }
74
+ } catch (err) {
75
+ logger.warn('staff portal support attachments module unavailable', { err })
76
+ return null
77
+ }
78
+ }
79
+
80
+ export function readAttachmentTags(api: PortalAttachmentsApi, attachment: AttachmentEntity): string[] {
81
+ return api.readAttachmentMetadata(attachment.storageMetadata).tags ?? []
82
+ }
83
+
84
+ export function portalAttachmentAuth(scope: SupportPortalScope) {
85
+ return { sub: scope.auth.sub, tenantId: scope.tenantId, orgId: scope.organizationId, email: scope.auth.email }
86
+ }
87
+
88
+ export function serializePortalAttachment(
89
+ api: PortalAttachmentsApi,
90
+ attachment: AttachmentEntity,
91
+ taskId: string,
92
+ customerUserId: string,
93
+ canManage = false,
94
+ ): PortalSupportAttachmentItem {
95
+ const tags = readAttachmentTags(api, attachment)
96
+ return {
97
+ id: attachment.id,
98
+ fileName: attachment.fileName,
99
+ fileSize: attachment.fileSize,
100
+ mimeType: attachment.mimeType ?? null,
101
+ downloadUrl: `/api/staff/portal/support-tickets/${encodeURIComponent(taskId)}/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,
102
+ uploadedByCustomer: tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_TAG_PREFIX)),
103
+ canDelete: canManage && tags.includes(customerUploadTag(customerUserId)),
104
+ createdAt: attachment.createdAt ? attachment.createdAt.toISOString() : null,
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The files of a ticket the customer may see: tagged `customer-visible` and passing
110
+ * the attachments module's own access check. Internal staff files are never returned.
111
+ * `canManage` is whether the caller holds `portal.support_tickets.manage`; without it
112
+ * no item reports `canDelete`, matching the DELETE route's own feature check.
113
+ */
114
+ export async function listCustomerVisibleAttachments(
115
+ api: PortalAttachmentsApi,
116
+ scope: SupportPortalScope,
117
+ taskId: string,
118
+ canManage = false,
119
+ ): Promise<PortalSupportAttachmentItem[]> {
120
+ const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
121
+ const attachments = await findWithDecryption(
122
+ scope.em,
123
+ api.Attachment,
124
+ { entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID, recordId: taskId, ...decryptionScope },
125
+ { orderBy: { createdAt: 'ASC' } },
126
+ decryptionScope,
127
+ )
128
+ const codes = Array.from(new Set(attachments.map((attachment) => attachment.partitionCode)))
129
+ const partitions = codes.length ? await scope.em.find(api.AttachmentPartition, { code: { $in: codes } }) : []
130
+ const partitionByCode = new Map(partitions.map((partition) => [partition.code, partition]))
131
+ const auth = portalAttachmentAuth(scope)
132
+ return attachments
133
+ .filter((attachment) => {
134
+ const partition = partitionByCode.get(attachment.partitionCode)
135
+ if (!partition) return false
136
+ if (!readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG)) return false
137
+ return api.checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: true }).ok
138
+ })
139
+ .map((attachment) => serializePortalAttachment(api, attachment, taskId, scope.auth.sub, canManage))
140
+ }
@@ -0,0 +1,161 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
3
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
5
+ import { StaffSupportTicket, StaffTimeTask, StaffTimeTaskComment, StaffTimeTaskStatus } from '../../data/entities'
6
+ import { emitStaffEvent } from '../../events'
7
+ import { filterPortalRecipientsByFeatureStrict, resolvePortalRecipientUserIdsStrict } from '../time-tracking/portalRecipients'
8
+ import { derivePortalStatus } from './portalStatus'
9
+ import { PORTAL_SUPPORT_TICKETS_VIEW_FEATURE } from './portalTickets'
10
+
11
+ const logger = createLogger('staff').child({ component: 'lib/support/portalBroadcast' })
12
+
13
+ export const PORTAL_FANOUT_BATCH_SIZE = 200
14
+
15
+ type Scope = { tenantId: string; organizationId: string }
16
+
17
+ export type PortalUpdateKind = 'status' | 'reply'
18
+
19
+ /**
20
+ * `version` names the change being announced (the task version for a status change,
21
+ * the comment for a reply). The portal event bridge drops an event whose payload
22
+ * repeats one seen in the last 500 ms, so two different changes must never produce
23
+ * identical payloads, while two publishers of the same change still collapse.
24
+ *
25
+ * Resolves `true` when the update was emitted and `false` when there is genuinely no
26
+ * one to tell (no customer, no portal users, nobody holding the view feature, or the
27
+ * `customer_accounts` module absent). Any other recipient-lookup failure rejects, so a
28
+ * caller never records a publication that did not happen.
29
+ */
30
+ async function emitPortalUpdate(
31
+ em: EntityManager,
32
+ task: StaffTimeTask,
33
+ ticket: StaffSupportTicket,
34
+ portalStatus: string,
35
+ kind: PortalUpdateKind,
36
+ version: string,
37
+ ): Promise<boolean> {
38
+ if (!task.customerId) return false
39
+ const customerUserIds = await resolvePortalRecipientUserIdsStrict(em, {
40
+ tenantId: task.tenantId,
41
+ organizationId: task.organizationId,
42
+ customerId: task.customerId,
43
+ })
44
+ const recipientUserIds = await filterPortalRecipientsByFeatureStrict(
45
+ em,
46
+ customerUserIds,
47
+ { tenantId: task.tenantId, organizationId: task.organizationId },
48
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
49
+ )
50
+ if (recipientUserIds.length === 0) return false
51
+ await emitStaffEvent(
52
+ 'staff.support_ticket.portal_updated',
53
+ {
54
+ id: task.id,
55
+ tenantId: task.tenantId,
56
+ organizationId: task.organizationId,
57
+ ticketId: task.id,
58
+ supportReference: ticket.supportReference,
59
+ portalStatus,
60
+ kind,
61
+ version,
62
+ recipientUserIds,
63
+ },
64
+ { persistent: true },
65
+ )
66
+ return true
67
+ }
68
+
69
+ function emitActivity(task: StaffTimeTask, ticket: StaffSupportTicket, kind: 'portal_status'): void {
70
+ void emitStaffEvent(
71
+ 'staff.support_ticket.activity_recorded',
72
+ {
73
+ id: ticket.id,
74
+ tenantId: task.tenantId,
75
+ organizationId: task.organizationId,
76
+ taskId: task.id,
77
+ projectId: task.timeProjectId,
78
+ kind,
79
+ },
80
+ { persistent: true },
81
+ ).catch((err) => {
82
+ logger.error('staff support activity emit failed', { err, taskId: task.id })
83
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_activity_emit_failed' })
84
+ })
85
+ }
86
+
87
+ /**
88
+ * Compare-and-publish: re-derive the customer-facing status of a portal ticket and
89
+ * broadcast only when it differs from what the portal was last told. Safe to call
90
+ * on any task event; internal tasks and tasks without a ticket row are ignored.
91
+ *
92
+ * `always` skips the comparison for triggers that are a status change by definition.
93
+ * Concurrent publishers (a reopen followed quickly by a resolve) each run on their own
94
+ * fork, so both can read a stale `portalStatusPublished` and skip; the portal refetches
95
+ * the ticket on every event, so publishing after each status change keeps it correct.
96
+ *
97
+ * `portalStatusPublished` advances only after `emitPortalUpdate` settles: an emitted
98
+ * update or a genuinely empty audience. A recipient-lookup or emit failure propagates
99
+ * with the marker untouched, so the persistent fan-out retries and republishes.
100
+ */
101
+ export async function publishTaskPortalStatus(
102
+ em: EntityManager,
103
+ taskId: string,
104
+ scope: Scope,
105
+ options: { always?: boolean } = {},
106
+ ): Promise<boolean> {
107
+ const task = await em.findOne(StaffTimeTask, { id: taskId, ...scope, source: 'portal', deletedAt: null })
108
+ if (!task) return false
109
+ const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)
110
+ if (!ticket) return false
111
+ const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ['id', 'isDefault', 'isDone'] })
112
+ const portalStatus = derivePortalStatus(status)
113
+ const unchanged = ticket.portalStatusPublished === portalStatus
114
+ if (unchanged && !options.always) return false
115
+ await emitPortalUpdate(em, task, ticket, portalStatus, 'status', task.updatedAt ? task.updatedAt.toISOString() : task.id)
116
+ if (!unchanged) {
117
+ ticket.portalStatusPublished = portalStatus
118
+ ticket.updatedAt = new Date()
119
+ await em.flush()
120
+ }
121
+ emitActivity(task, ticket, 'portal_status')
122
+ return true
123
+ }
124
+
125
+ /** A status column's `isDone`/`isDefault` flags changed: re-derive every live portal ticket in it, in batches. */
126
+ export async function fanOutStatusColumn(em: EntityManager, taskStatusId: string, scope: Scope): Promise<number> {
127
+ let published = 0
128
+ let offset = 0
129
+ for (;;) {
130
+ const tasks = await em.find(
131
+ StaffTimeTask,
132
+ { taskStatusId, ...scope, source: 'portal', deletedAt: null },
133
+ { fields: ['id'], limit: PORTAL_FANOUT_BATCH_SIZE, offset, orderBy: { id: 'asc' } },
134
+ )
135
+ if (tasks.length === 0) break
136
+ for (const task of tasks) {
137
+ if (await publishTaskPortalStatus(em.fork(), task.id, scope)) published += 1
138
+ }
139
+ if (tasks.length < PORTAL_FANOUT_BATCH_SIZE) break
140
+ offset += PORTAL_FANOUT_BATCH_SIZE
141
+ }
142
+ return published
143
+ }
144
+
145
+ /** A customer-visible comment was published, retracted or deleted: tell the ticket's own customer users to refetch. */
146
+ export async function publishTicketReply(em: EntityManager, commentId: string, scope: Scope): Promise<boolean> {
147
+ const comment = await em.findOne(StaffTimeTaskComment, { id: commentId, ...scope }, { fields: ['id', 'taskId', 'updatedAt'] })
148
+ if (!comment) return false
149
+ const task = await em.findOne(StaffTimeTask, { id: comment.taskId, ...scope, source: 'portal', deletedAt: null })
150
+ if (!task) return false
151
+ const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)
152
+ if (!ticket) return false
153
+ const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ['id', 'isDefault', 'isDone'] })
154
+ return emitPortalUpdate(em, task, ticket, derivePortalStatus(status), 'reply', `${comment.id}:${comment.updatedAt ? comment.updatedAt.toISOString() : ''}`)
155
+ }
156
+
157
+ export function readEventScope(payload: { tenantId?: unknown; organizationId?: unknown }): Scope | null {
158
+ const tenantId = typeof payload.tenantId === 'string' ? payload.tenantId : null
159
+ const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null
160
+ return tenantId && organizationId ? { tenantId, organizationId } : null
161
+ }
@@ -0,0 +1,108 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
4
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
5
+ import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
6
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import { createLogger } from '@open-mercato/shared/lib/logger'
8
+ import { checkRateLimit, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'
9
+ import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
10
+ import type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'
11
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
12
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
13
+ import type { SupportPortalScope } from './portalTickets'
14
+
15
+ const logger = createLogger('staff').child({ component: 'lib/support/portalRoute' })
16
+
17
+ export const SUPPORT_TICKET_RESOURCE_KIND = 'staff.support_ticket'
18
+
19
+ const RATE_LIMITS = {
20
+ create: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_CREATE', {
21
+ points: 10,
22
+ duration: 3600,
23
+ keyPrefix: 'staff_support_ticket_create',
24
+ }),
25
+ message: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_MESSAGE', {
26
+ points: 60,
27
+ duration: 3600,
28
+ keyPrefix: 'staff_support_ticket_message',
29
+ }),
30
+ upload: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_UPLOAD', {
31
+ points: 30,
32
+ duration: 3600,
33
+ keyPrefix: 'staff_support_ticket_upload',
34
+ }),
35
+ } as const
36
+
37
+ export type PortalRateLimitKind = keyof typeof RATE_LIMITS
38
+
39
+ export function buildPortalCommandContext(scope: SupportPortalScope, req: Request): CommandRuntimeContext {
40
+ const auth: NonNullable<AuthContext> = {
41
+ sub: scope.auth.sub,
42
+ sid: scope.auth.sid,
43
+ tenantId: scope.auth.tenantId,
44
+ orgId: scope.auth.orgId,
45
+ email: scope.auth.email,
46
+ customerEntityId: scope.auth.customerEntityId ?? null,
47
+ personEntityId: scope.auth.personEntityId ?? null,
48
+ }
49
+ return {
50
+ container: scope.container,
51
+ auth,
52
+ organizationScope: null,
53
+ selectedOrganizationId: scope.organizationId,
54
+ organizationIds: [scope.organizationId],
55
+ request: req,
56
+ }
57
+ }
58
+
59
+ /** Per customer user, so one noisy account cannot exhaust its colleagues' budget. */
60
+ export async function checkPortalRateLimit(
61
+ scope: SupportPortalScope,
62
+ kind: PortalRateLimitKind,
63
+ ): Promise<Response | null> {
64
+ let service: RateLimiterService | null = null
65
+ try {
66
+ service = scope.container.resolve('rateLimiterService') as RateLimiterService
67
+ } catch {
68
+ return null
69
+ }
70
+ if (!service) return null
71
+ const { translate } = await resolveTranslations()
72
+ return checkRateLimit(
73
+ service,
74
+ RATE_LIMITS[kind],
75
+ `${scope.tenantId}:${scope.auth.sub}`,
76
+ translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),
77
+ )
78
+ }
79
+
80
+ export async function portalErrorResponse(err: unknown, label: string): Promise<Response> {
81
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
82
+ const rejection = getCommandInterceptorHttpRejection(err)
83
+ if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })
84
+ if (err instanceof z.ZodError) {
85
+ const { translate } = await resolveTranslations()
86
+ return NextResponse.json(
87
+ { ok: false, error: translate('staff.errors.invalid_request', 'Invalid request'), details: err.issues },
88
+ { status: 400 },
89
+ )
90
+ }
91
+ logger.error(label, { err })
92
+ getTelemetryRuntime()?.reportError(err, {
93
+ module: 'staff',
94
+ code: 'staff.portal_support_route_failed',
95
+ attributes: { operation: label },
96
+ })
97
+ return NextResponse.json({ ok: false, error: 'staff.errors.internal' }, { status: 500 })
98
+ }
99
+
100
+ export function readPathId(req: Request, pattern: RegExp): string | null {
101
+ try {
102
+ const match = new URL(req.url).pathname.match(pattern)
103
+ const id = match?.[1] ? decodeURIComponent(match[1]) : null
104
+ return id && z.string().uuid().safeParse(id).success ? id : null
105
+ } catch {
106
+ return null
107
+ }
108
+ }
@@ -0,0 +1,15 @@
1
+ import type { StaffSupportPortalStatus } from '../../data/entities'
2
+
3
+ export type PortalStatusFlags = {
4
+ isDefault: boolean
5
+ isDone: boolean
6
+ }
7
+
8
+ export function derivePortalStatus(status: PortalStatusFlags | null | undefined): StaffSupportPortalStatus {
9
+ if (!status) return 'received'
10
+ if (status.isDone) return 'resolved'
11
+ if (status.isDefault) return 'received'
12
+ return 'in_progress'
13
+ }
14
+
15
+ export const SUPPORT_PORTAL_STATUSES: readonly StaffSupportPortalStatus[] = ['received', 'in_progress', 'resolved']