@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
@@ -265,8 +265,9 @@ export async function seedInboundMessage(
265
265
  }
266
266
 
267
267
  /**
268
- * Best-effort hard delete of the rows one {@link seedInboundMessage} call created
269
- * (thread mapping, channel link, conversation and message). Run it before
268
+ * Best-effort hard delete of the rows one seeded or test-ingested inbound message
269
+ * created (thread mapping, channel link, external message, conversation and
270
+ * platform message). Run it before
270
271
  * {@link deleteChannelIfExists}: the purge is authorized against the live channel,
271
272
  * and disconnecting a channel deliberately retains its conversations and messages.
272
273
  * Safe to call with null ids in `finally`.
@@ -341,6 +342,63 @@ export async function ingestInboundChatMessage(
341
342
  };
342
343
  }
343
344
 
345
+ /**
346
+ * Ingest an inbound email through the real `ingest_inbound_message` command on a
347
+ * channel connected with the default `email` flavor. The stub adapter turns the
348
+ * frame into an email-shaped inbound message whose sender is `senderAddress`, so
349
+ * the hub derives `externalEmail` from it and composes the platform message via
350
+ * `messages.messages.compose` exactly as a real IMAP/Gmail/Graph adapter would.
351
+ *
352
+ * Use this — not {@link seedInboundMessage}, which bypasses compose — for any
353
+ * assertion about what the hub does with an inbound email after ingest (for
354
+ * example that it is not echoed back to its sender, #6089). Prefix the supplied
355
+ * external conversation id with `inbound-seed:` when the test will use
356
+ * {@link purgeSeededInboundMessage} for teardown.
357
+ */
358
+ export async function ingestInboundEmailMessage(
359
+ request: APIRequestContext,
360
+ token: string,
361
+ input: {
362
+ channelId: string;
363
+ senderAddress: string;
364
+ senderDisplayName?: string;
365
+ subject?: string;
366
+ body?: string;
367
+ externalMessageId: string;
368
+ externalConversationId: string;
369
+ },
370
+ ): Promise<{
371
+ status: string;
372
+ messageId: string | null;
373
+ conversationId: string | null;
374
+ channelLinkId: string | null;
375
+ channelType: string | null;
376
+ }> {
377
+ const { senderAddress, ...rest } = input;
378
+ const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {
379
+ token,
380
+ data: { action: 'ingest-inbound', senderIdentifier: senderAddress, ...rest },
381
+ });
382
+ expect(
383
+ response.status(),
384
+ 'POST /api/communication_channels/test-seed (ingest-inbound, email) should return 201',
385
+ ).toBe(201);
386
+ const body = await readJsonSafe<{
387
+ status?: string;
388
+ messageId?: string | null;
389
+ conversationId?: string | null;
390
+ channelLinkId?: string | null;
391
+ channelType?: string | null;
392
+ }>(response);
393
+ return {
394
+ status: body?.status ?? 'unknown',
395
+ messageId: body?.messageId ?? null,
396
+ conversationId: body?.conversationId ?? null,
397
+ channelLinkId: body?.channelLinkId ?? null,
398
+ channelType: body?.channelType ?? null,
399
+ };
400
+ }
401
+
344
402
  export type InspectedMessageLinks = {
345
403
  links: Array<{
346
404
  id: string;
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
5
6
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
6
7
  import type { ProgressService } from '../../../progress/lib/progressService'
7
8
  import {
@@ -52,6 +53,15 @@ export async function POST(req: Request) {
52
53
  const ids = Array.from(new Set(parsed.data.ids))
53
54
  const container = await createRequestContainer()
54
55
  const progressService = container.resolve('progressService') as ProgressService
56
+ const orgScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
57
+ if (orgScope?.selectionRejected) {
58
+ return NextResponse.json(responseSchema.parse({
59
+ ok: false,
60
+ progressJobId: null,
61
+ message: 'Your selected organization is no longer available. Please re-select an organization and try again.',
62
+ }), { status: 422 })
63
+ }
64
+ const organizationId = orgScope?.selectedId ?? auth.orgId
55
65
 
56
66
  const progressJob = await progressService.createJob(
57
67
  {
@@ -69,7 +79,7 @@ export async function POST(req: Request) {
69
79
  },
70
80
  {
71
81
  tenantId: auth.tenantId,
72
- organizationId: auth.orgId,
82
+ organizationId,
73
83
  userId: auth.sub,
74
84
  },
75
85
  )
@@ -79,7 +89,7 @@ export async function POST(req: Request) {
79
89
  progressJobId: progressJob.id,
80
90
  ids,
81
91
  scope: {
82
- organizationId: auth.orgId,
92
+ organizationId,
83
93
  tenantId: auth.tenantId,
84
94
  userId: auth.sub,
85
95
  },
@@ -13,6 +13,7 @@ import {
13
13
  ChannelThreadMapping,
14
14
  CommunicationChannel,
15
15
  ExternalConversation,
16
+ ExternalMessage,
16
17
  MessageChannelLink,
17
18
  } from '../../../data/entities'
18
19
  import { ChannelAccessDeniedError, assertCanManageChannel } from '../../../lib/access-control'
@@ -64,8 +65,8 @@ import {
64
65
  * the inbound auto-link tests (TC-CRM-EMAIL-002..005). NOTE: this action
65
66
  * deliberately bypasses `messages.messages.compose` — it can never prove that
66
67
  * the hub accepts a message, only that downstream subscribers fire.
67
- * - `purge-inbound`: hard-delete the rows one `emit-inbound` call seeded, for
68
- * integration teardown.
68
+ * - `purge-inbound`: hard-delete the rows one test-seeded or test-ingested
69
+ * inbound message created, for integration teardown.
69
70
  */
70
71
  type RbacServiceLike = {
71
72
  loadAcl: (
@@ -137,9 +138,15 @@ const connectChannelSchema = z.object({
137
138
  const ingestInboundSchema = z.object({
138
139
  action: z.literal('ingest-inbound'),
139
140
  channelId: z.string().uuid(),
140
- /** Opaque sender handle — a Discord snowflake, a Slack member id, etc. */
141
+ /**
142
+ * Opaque sender handle — a Discord snowflake, a Slack member id, etc. For a
143
+ * channel connected with the default `email` flavor this is the sender's
144
+ * address, and the email stub normalizes it into an email-shaped frame.
145
+ */
141
146
  senderIdentifier: z.string().min(1).max(255),
142
147
  senderDisplayName: z.string().max(255).optional(),
148
+ /** Honored by the email stub only; chat frames carry no subject. */
149
+ subject: z.string().max(500).optional(),
143
150
  body: z.string().max(50_000).optional(),
144
151
  externalMessageId: z.string().min(1).max(255),
145
152
  externalConversationId: z.string().min(1).max(255),
@@ -197,11 +204,11 @@ const emitInboundSchema = z.object({
197
204
  })
198
205
 
199
206
  /**
200
- * Teardown for `emit-inbound`: hard-delete the rows one seed created (thread
201
- * mapping, channel link, synthetic conversation and the `messages` row), scoped
202
- * to the caller's channel. Disconnecting a channel deliberately retains its
203
- * conversations and messages, so without this every seed leaks into the shared
204
- * integration database.
207
+ * Teardown for test-seeded or test-ingested inbound records: hard-delete the
208
+ * thread mapping, channel link, external message, synthetic conversation and
209
+ * platform message, scoped to the caller's channel. Disconnecting a channel
210
+ * deliberately retains these records, so without this every test leaks into the
211
+ * shared integration database.
205
212
  */
206
213
  const purgeInboundSchema = z.object({
207
214
  action: z.literal('purge-inbound'),
@@ -529,8 +536,8 @@ export async function POST(req: Request): Promise<Response> {
529
536
 
530
537
  if (body.action === 'purge-inbound') {
531
538
  const rowScope = { tenantId, organizationId }
532
- // Only rows hanging off a conversation `emit-inbound` minted are purgeable,
533
- // so a caller cannot use this action to hard-delete real channel traffic.
539
+ // Only rows hanging off a test-prefixed conversation are purgeable, so a
540
+ // caller cannot use this action to hard-delete real channel traffic.
534
541
  const seededConversation = await findOneWithDecryption(
535
542
  em,
536
543
  ExternalConversation,
@@ -556,6 +563,11 @@ export async function POST(req: Request): Promise<Response> {
556
563
  externalConversationId: body.conversationId,
557
564
  ...rowScope,
558
565
  })
566
+ await em.nativeDelete(ExternalMessage, {
567
+ conversationId: body.conversationId,
568
+ channelId: body.channelId,
569
+ ...rowScope,
570
+ })
559
571
  await em.nativeDelete(ExternalConversation, {
560
572
  id: body.conversationId,
561
573
  channelId: body.channelId,
@@ -564,6 +576,7 @@ export async function POST(req: Request): Promise<Response> {
564
576
  await deleteTestSeedPlatformMessage(em, {
565
577
  messageId: body.messageId,
566
578
  channelId: body.channelId,
579
+ conversationId: body.conversationId,
567
580
  ...rowScope,
568
581
  })
569
582
  return NextResponse.json({ ok: true })
@@ -579,11 +592,14 @@ export async function POST(req: Request): Promise<Response> {
579
592
  // check that `providerKey` matches the channel it names, so passing a
580
593
  // different one would silently stamp the wrong provider onto the link.
581
594
  const channelProviderKey = ownedChannel.providerKey
582
- if (channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY) {
595
+ if (
596
+ channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY
597
+ && channelProviderKey !== TEST_SEED_PROVIDER_KEY
598
+ ) {
583
599
  return NextResponse.json(
584
600
  {
585
601
  error:
586
- 'ingest-inbound requires a channel connected with providerFlavor: "chat"; ' +
602
+ 'ingest-inbound requires a channel connected through the test-seed stub; ' +
587
603
  `channel ${body.channelId} is '${channelProviderKey}'`,
588
604
  },
589
605
  { status: 422 },
@@ -610,6 +626,7 @@ export async function POST(req: Request): Promise<Response> {
610
626
  externalConversationId: body.externalConversationId,
611
627
  senderIdentifier: body.senderIdentifier,
612
628
  senderDisplayName: body.senderDisplayName,
629
+ ...(body.subject !== undefined ? { subject: body.subject } : {}),
613
630
  body: body.body ?? '',
614
631
  },
615
632
  eventType: 'message',
@@ -143,15 +143,16 @@ export async function createTestSeedPlatformMessage(
143
143
  }
144
144
 
145
145
  /**
146
- * Hard-delete a `messages` row seeded by {@link createTestSeedPlatformMessage}.
147
- * Matches only rows stamped as test-seed inbound for the given channel and
148
- * scope, so integration teardown can never remove a real message.
146
+ * Hard-delete a platform message created by the test-seed harness. Matches only
147
+ * the direct seed stamp or a test-prefixed conversation already verified by the
148
+ * test-seed route, so integration teardown cannot remove real traffic.
149
149
  */
150
150
  export async function deleteTestSeedPlatformMessage(
151
151
  em: EntityManager,
152
152
  input: {
153
153
  messageId: string
154
154
  channelId: string
155
+ conversationId: string
155
156
  tenantId: string
156
157
  organizationId: string | null
157
158
  },
@@ -159,11 +160,20 @@ export async function deleteTestSeedPlatformMessage(
159
160
  await em.getConnection().execute(
160
161
  `DELETE FROM messages
161
162
  WHERE id = ?
162
- AND source_entity_type = 'communication_channels.test_seed_inbound'
163
- AND source_entity_id = ?
163
+ AND (
164
+ (source_entity_type = 'communication_channels.test_seed_inbound' AND source_entity_id = ?)
165
+ OR
166
+ (source_entity_type = 'communication_channels.external_conversation' AND source_entity_id = ?)
167
+ )
164
168
  AND tenant_id = ?
165
169
  AND organization_id IS NOT DISTINCT FROM ?`,
166
- [input.messageId, input.channelId, input.tenantId, input.organizationId],
170
+ [
171
+ input.messageId,
172
+ input.channelId,
173
+ input.conversationId,
174
+ input.tenantId,
175
+ input.organizationId,
176
+ ],
167
177
  )
168
178
  }
169
179
 
@@ -341,10 +351,39 @@ class TestSeedChannelAdapter implements ChannelAdapter {
341
351
  }
342
352
  }
343
353
 
344
- async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {
345
- // The test-seed inbound path seeds MessageChannelLink rows directly and emits
346
- // the hub event, so this adapter never normalizes a raw inbound payload.
347
- throw new Error('[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness')
354
+ async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {
355
+ // Email-shaped frame for the test-seed `ingest-inbound` action: the sender is
356
+ // an address, so the message travels the REAL ingest path for an email
357
+ // channel — `ingest_inbound_message` derives `externalEmail` from it and
358
+ // composes through `messages.messages.compose` with `sendViaEmail: false`.
359
+ // That is the only way a test can prove the hub does not echo inbound mail
360
+ // back to its sender (#6089); `emit-inbound` bypasses compose entirely.
361
+ const frame = (raw.raw ?? {}) as Record<string, unknown>
362
+ const senderIdentifier = String(frame.senderIdentifier ?? '').trim()
363
+ if (!senderIdentifier.includes('@')) {
364
+ throw new Error('[internal] TestSeedChannelAdapter requires an email-shaped senderIdentifier')
365
+ }
366
+ const subject = typeof frame.subject === 'string' ? frame.subject : undefined
367
+ const body = typeof frame.body === 'string' ? frame.body : ''
368
+ const senderDisplayName =
369
+ typeof frame.senderDisplayName === 'string' ? frame.senderDisplayName : undefined
370
+ return {
371
+ externalMessageId: String(frame.externalMessageId ?? ''),
372
+ externalConversationId: String(frame.externalConversationId ?? ''),
373
+ senderIdentifier,
374
+ senderDisplayName,
375
+ subject,
376
+ body,
377
+ bodyFormat: 'text',
378
+ timestamp: new Date(),
379
+ channelPayload: {
380
+ from: { address: senderIdentifier, ...(senderDisplayName ? { name: senderDisplayName } : {}) },
381
+ ...(subject !== undefined ? { subject } : {}),
382
+ text: body,
383
+ },
384
+ channelContentType: 'text/plain',
385
+ channelMetadata: {},
386
+ }
348
387
  }
349
388
 
350
389
  async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {
@@ -322,14 +322,13 @@ const composeMessageCommand: CommandHandler<unknown, { id: string; threadId: str
322
322
  )?.threadId ?? input.parentMessageId
323
323
  : undefined
324
324
 
325
- const isPublicVisibility = input.visibility === 'public'
326
- // #6093: a channel-ingested message was RECEIVED, not sent. Ingest passes
327
- // `sendViaEmail: false` on purpose; forcing it for public visibility would
328
- // mail the assignee a copy and echo the message to its external sender
329
- // (#6089) the moment the recipients waiver lets it compose. #6090 removes
330
- // the forcing for every caller; until then it is skipped here for the
331
- // ingest path only.
332
- const sendViaEmail = isPublicVisibility && !input.inboundFromChannel ? true : input.sendViaEmail
325
+ // Honor the caller's explicit `sendViaEmail` (#6089, #6090). The messages API
326
+ // route forces email delivery for public messages a user composes; internal
327
+ // callers (communication_channels inbound ingest, send-as-user) compose
328
+ // `visibility: 'public'` with `sendViaEmail: false` on purpose. Forcing it
329
+ // here echoed every inbound email back to its sender through the tenant
330
+ // system email channel and made send-as-user deliver twice.
331
+ const sendViaEmail = input.sendViaEmail
333
332
  const message = trx.create(Message, {
334
333
  type: input.type,
335
334
  visibility: input.visibility ?? null,
@@ -435,8 +434,7 @@ const composeMessageCommand: CommandHandler<unknown, { id: string; threadId: str
435
434
  messageId,
436
435
  senderUserId: input.userId,
437
436
  recipientUserIds: input.recipients.map((recipient) => recipient.userId),
438
- sendViaEmail:
439
- input.visibility === 'public' && !input.inboundFromChannel ? true : input.sendViaEmail,
437
+ sendViaEmail: input.sendViaEmail,
440
438
  inboundFromChannel: input.inboundFromChannel === true,
441
439
  externalEmail: responseExternalEmail,
442
440
  tenantId: input.tenantId,
@@ -121,7 +121,7 @@ export default function PortalDashboardPage({ params }: Props) {
121
121
  const isHidden = hiddenWidgets.has(widget.metadata.id)
122
122
  return (
123
123
  <Button key={widget.metadata.id} type="button" variant={isHidden ? 'outline' : 'default'} size="sm" className="rounded-lg text-sm" onClick={() => toggleWidget(widget.metadata.id)}>
124
- {widget.metadata.title || widget.metadata.id}
124
+ {widget.metadata.title ? t(widget.metadata.title, widget.metadata.title) : widget.metadata.id}
125
125
  </Button>
126
126
  )
127
127
  })}
@@ -136,7 +136,7 @@ export default function PortalDashboardPage({ params }: Props) {
136
136
  if (!WidgetComponent) return null
137
137
  return (
138
138
  <PortalCard key={widget.metadata.id}>
139
- <PortalCardHeader title={widget.metadata.title || widget.metadata.id} />
139
+ <PortalCardHeader title={widget.metadata.title ? t(widget.metadata.title, widget.metadata.title) : widget.metadata.id} />
140
140
  <WidgetComponent context={{ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }} />
141
141
  </PortalCard>
142
142
  )
@@ -407,7 +407,8 @@ Pinned by `lib/time-tracking-ui/__tests__/TimeEntryDialog.test.tsx` →
407
407
  | `staff.time_task.board:column-header` | `lib/time-tracking-ui/KanbanColumn.tsx` | `{ statusId, statusName, isDone, total, loggedMinutes }` |
408
408
  | `staff.time_task.board:{card-badges,card-footer}` | `lib/time-tracking-ui/KanbanCard.tsx` | `{ entityId, recordId, taskId, timeProjectId, taskStatusId, timerRunning }` |
409
409
  | `staff.my_work:{before-sections,after-sections}` | `backend/staff/time-tracking/page.tsx` | `{ staffMemberId, today, projectIds, retryLastMutation }` |
410
- | `staff.time_tracking.settings:sections` | `backend/staff/time-tracking/settings/page.tsx` | `{ moduleId, canManage }` |
410
+ | `staff.time_tracking.settings:sections` | `backend/staff/time-tracking/settings/page.tsx` | `{ moduleId, canManage, keys, values, setValue }` |
411
+ | `staff.time_tracking.settings:tabs` | `backend/staff/time-tracking/settings/page.tsx` | `{ moduleId, canManage, settings, onSettingsSaved }` — `placement: { kind: 'tab' }`; a tab owns its saves (writes built from `context.settings`, reported via `onSettingsSaved`) |
411
412
  | `staff.timesheets.timer-bar:actions` | `lib/timesheets-ui/TimerBar.tsx` | `{ staffMemberId, retryLastMutation }` |
412
413
 
413
414
  No context carries a rate, cost or amount; contributions that need money must ask for it
@@ -646,9 +647,10 @@ columns time tracking already carried:
646
647
  the query engine only ever looks a link up by `base`. **These are strings, never
647
648
  imports.** Staff is slated for extraction into a standalone package, so
648
649
  `data/extensions.ts` may not import a `customers` or `sales` symbol and
649
- `requires: ['planner', 'resources']` in [`index.ts`](./index.ts) must not grow;
650
- `__tests__/entityExtensions.test.ts` fails if either happens, and pins every table and
651
- column against the migration snapshots.
650
+ `requires` in [`index.ts`](./index.ts) (`['planner', 'resources', 'dictionaries']`;
651
+ `dictionaries` backs the support-ticket categories) must never gain `customers`, `sales` or
652
+ `customer_accounts`; `__tests__/entityExtensions.test.ts` fails if either happens, and pins
653
+ every table and column against the migration snapshots.
652
654
 
653
655
  **Declaration-only, exactly as in `apps/mercato/src/modules/example/data/extensions.ts`.**
654
656
  Nothing in the platform passes `includeExtensions` to a query, the basic engine's
@@ -820,6 +822,20 @@ The CLI enqueues onto the same queue with the same `ProgressJob` the settings
820
822
  screen uses; it does not do the work in process. An unknown `--hook` id refuses to
821
823
  start rather than silently doing nothing.
822
824
 
825
+ ## Customer support tickets
826
+
827
+ Design of record: [`.ai/specs/2026-10-06-staff-helpdesk-support-tickets.md`](../../../../../.ai/specs/2026-10-06-staff-helpdesk-support-tickets.md).
828
+
829
+ - **A ticket is a task.** `staff_time_tasks.source = 'portal'` plus `customer_id` (the ticket owner — never the project's customer), and a 1:1 `staff_support_tickets` row (same-module FK `task_id`) holding every helpdesk-only field. The customer's original request lives in the encrypted `customer_request_snapshot`; the portal reads it and NEVER the staff-editable task title/description.
830
+ - **Routing** (`lib/support/routing.ts`): per-category `staff_support_routing_rules` override the organization fallback rule (`category_value IS NULL`, exactly one, cannot be deleted or paused). Paused, unusable (completed/deleted/no `isDefault` status) or orphaned overrides fall back. Categories are the per-org dictionary `staff.support_ticket_category` (exact org, no ancestors). `seedDefaults` seeds categories, the "Customer support" project and the fallback rule; existing tenants run `mercato staff seed-support-defaults`.
831
+ - **Settings UI**: categories, routing and the reference prefix are the "Support" tab (`widgets/injection/time-tracking-settings-support`) of `/backend/staff/time-tracking/settings`, mounted on the `staff.time_tracking.settings:tabs` spot. A tab saves on its own; when it writes the settings record it must build the body from `context.settings` and call `context.onSettingsSaved(response)` so the page's draft is rebased, not overwritten.
832
+ - **Backoffice reads for agents**: `GET /api/staff/timesheets/support-tickets/options` (`staff.timesheets.tasks.view`) serves the Support Inbox filters (categories, project-scoped assignees); tasks carry `_staffAssignment: { canClaim, canRelease }`, computed with the claim command's predicate. Never derive claimability client-side.
833
+ - **Portal routes** `/api/staff/portal/support-tickets/*` (options, list, create, detail, messages, attachments) resolve identity via dynamic import (no static `customer_accounts` edge) and load every row WITH `portalOwnedTaskWhere` (`lib/support/portalTickets.ts`). Features `portal.support_tickets.view|manage` are customer features granted via `defaultCustomerRoleFeatures`, not `acl.ts`. Attachments load the attachments module dynamically; customer uploads carry `customer-visible` + `customer-upload:<customerUserId>` tags and only the uploader may delete.
834
+ - **Conversation**: comment `visibility` defaults to `internal`; `customer` is refused (422) on non-portal tasks; `author_kind = 'customer'` comments are immutable for staff, `manage_all` included. Comment CRUD events carry `portalVisible`.
835
+ - **Events**: `staff.support_ticket.created|customer_replied` are server-side only (they name the customer); `activity_recorded` is the id-only client broadcast (also emitted with `kind: attachment_added|attachment_removed` plus the changed `attachmentId` — which keeps rapid uploads distinct for the browser bridge's 500 ms duplicate filter — by the portal attachments route via `lib/support/supportActivity.ts`, so an open TaskDrawer refetches "Ticket files", whose list is a drawer query invalidated with task/comments); `portal_updated` is emitted only by `lib/support/portalBroadcast.ts` with pinned `recipientUserIds` and never with an empty list. `staff.timesheets.time_task.assignee_changed` (client broadcast, ids only, `changeKind`) is emitted by claim/release/manual edits and the portal default assignee.
836
+ - **Assigners and default assignee**: `staff_time_project_members.is_assigner` (requires a linked user) grants claim (`POST /api/staff/timesheets/tasks/{id}/claim`, conditional update, 409 on a lost race) and release. `staff_time_projects.default_assignee_staff_member_id` applies to portal tickets ONLY — staff task creation keeps US-C1 (omitted or `null` → creator).
837
+ - **Notification chain** (`lib/time-tracking/workRecipients.ts`): type audience → active assigners → owner → `createForFeature('staff.timesheets.projects.manage', restrictRecipientsToOrganization: true)`. The actor is excluded in steps 1–3 only. A support notification that reaches nobody sets `staff_support_tickets.unnotified_at` (cleared on assignment).
838
+
823
839
  ## Internal-Only Surfaces (NOT public contract)
824
840
 
825
841
  These are subject to change without deprecation; do not import them from non-staff code:
@@ -836,9 +852,9 @@ If you need data from staff in another core module, the correct path is:
836
852
 
837
853
  ## Dependencies
838
854
 
839
- Staff currently declares `requires: ['planner', 'resources']` in [`index.ts`](./index.ts). The dependency direction is intentional and asymmetric:
855
+ Staff currently declares `requires: ['planner', 'resources', 'dictionaries']` in [`index.ts`](./index.ts). The dependency direction is intentional and asymmetric:
840
856
 
841
- - Staff depends on planner + resources (hard requirement at load time).
857
+ - Staff depends on planner + resources (hard requirement at load time), and on dictionaries for the support-ticket categories.
842
858
  - Planner soft-resolves `availabilityAccessResolver` via DI with `allowUnregistered: true` (graceful degradation when staff is absent).
843
859
 
844
860
  This asymmetry will be reconciled in the Phase 2/3 follow-up when staff becomes its own npm package; for now, planner is the only consumer that must work without staff registered.