@open-mercato/core 0.10.1-develop.7716.1.cefe7975df → 0.10.1-develop.7721.1.b5c4a00a68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -0,0 +1,233 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { sql } from 'kysely'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import { createLogger } from '@open-mercato/shared/lib/logger'
7
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
8
+ import { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'
9
+ import { StaffSupportTicket, StaffTimeTask } from '../../data/entities'
10
+ import { PORTAL_SUPPORT_NOT_LINKED_CODE } from '../support-ui/portalSupport'
11
+
12
+ const logger = createLogger('staff').child({ component: 'lib/support/portalTickets' })
13
+
14
+ export const PORTAL_SUPPORT_TICKETS_VIEW_FEATURE = 'portal.support_tickets.view'
15
+ export const PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE = 'portal.support_tickets.manage'
16
+
17
+ type AppContainer = Awaited<ReturnType<typeof createRequestContainer>>
18
+
19
+ export type SupportPortalScope = {
20
+ auth: CustomerAuthContext
21
+ customerId: string
22
+ tenantId: string
23
+ organizationId: string
24
+ container: AppContainer
25
+ em: EntityManager
26
+ hasFeature: (feature: string) => Promise<boolean>
27
+ }
28
+
29
+ type CustomerFeatureGuard = (auth: CustomerAuthContext, features: string[], rbac: unknown) => Promise<void>
30
+
31
+ /**
32
+ * The portal identity is resolved through `customer_accounts` with a dynamic import,
33
+ * exactly like the EP-50 time-reports routes, so `staff` keeps no static edge to the
34
+ * portal module: a deployment without it answers 401 instead of failing to load.
35
+ * A session with no `customerEntityId` is refused (403); a missing feature guard
36
+ * fails closed (403) so a revoked grant keeps working as a revocation.
37
+ */
38
+ export async function resolveSupportPortalScope(
39
+ req: Request,
40
+ requiredFeature: string,
41
+ ): Promise<SupportPortalScope | Response> {
42
+ let auth: CustomerAuthContext | null = null
43
+ let requireFeature: CustomerFeatureGuard | null = null
44
+ try {
45
+ const customerAuth = await import('@open-mercato/core/modules/customer_accounts/lib/customerAuth')
46
+ auth = await customerAuth.getCustomerAuthFromRequest(req)
47
+ const guard: unknown = (customerAuth as { requireCustomerFeature?: unknown }).requireCustomerFeature
48
+ requireFeature = typeof guard === 'function' ? (guard as CustomerFeatureGuard) : null
49
+ } catch (err) {
50
+ logger.warn('staff portal support could not resolve customer auth', { err })
51
+ return NextResponse.json({ ok: false, error: 'staff.errors.unauthorized' }, { status: 401 })
52
+ }
53
+ if (!auth) return NextResponse.json({ ok: false, error: 'staff.errors.unauthorized' }, { status: 401 })
54
+ const customerId =
55
+ typeof auth.customerEntityId === 'string' && auth.customerEntityId.trim().length > 0
56
+ ? auth.customerEntityId.trim()
57
+ : null
58
+ if (!customerId) {
59
+ return NextResponse.json(
60
+ { ok: false, error: 'staff.errors.customerAccountNotLinked', code: PORTAL_SUPPORT_NOT_LINKED_CODE },
61
+ { status: 403 },
62
+ )
63
+ }
64
+ if (!requireFeature) {
65
+ logger.error('staff portal support could not resolve requireCustomerFeature; refusing the request', {
66
+ feature: requiredFeature,
67
+ })
68
+ return NextResponse.json({ ok: false, error: 'staff.errors.forbidden' }, { status: 403 })
69
+ }
70
+ const guard = requireFeature
71
+ const container = await createRequestContainer()
72
+ const resolvedAuth = auth
73
+ const hasFeature = async (feature: string): Promise<boolean> => {
74
+ try {
75
+ await guard(resolvedAuth, [feature], container.resolve('customerRbacService'))
76
+ return true
77
+ } catch {
78
+ return false
79
+ }
80
+ }
81
+ try {
82
+ await guard(auth, [requiredFeature], container.resolve('customerRbacService'))
83
+ } catch (response) {
84
+ if (response instanceof Response) return response
85
+ logger.warn('staff portal support feature check failed', { err: response })
86
+ return NextResponse.json({ ok: false, error: 'staff.errors.forbidden' }, { status: 403 })
87
+ }
88
+ return {
89
+ auth,
90
+ customerId,
91
+ tenantId: auth.tenantId,
92
+ organizationId: auth.orgId,
93
+ container,
94
+ em: (container.resolve('em') as EntityManager).fork(),
95
+ hasFeature,
96
+ }
97
+ }
98
+
99
+ /**
100
+ * THE ownership clause for every portal support read and write. Ownership comes from
101
+ * the task's own `customer_id` — never the project's — so tickets from many customers
102
+ * can share one project. Callers load WITH this clause rather than loading and
103
+ * checking afterwards, so a foreign id is never read even momentarily.
104
+ */
105
+ export function portalOwnedTaskWhere(scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId'>) {
106
+ return {
107
+ tenantId: scope.tenantId,
108
+ organizationId: scope.organizationId,
109
+ customerId: scope.customerId,
110
+ source: 'portal' as const,
111
+ deletedAt: null,
112
+ }
113
+ }
114
+
115
+ export async function loadPortalOwnedTicket(
116
+ scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,
117
+ taskId: string,
118
+ ): Promise<{ task: StaffTimeTask; ticket: StaffSupportTicket } | null> {
119
+ const task = await scope.em.findOne(StaffTimeTask, { id: taskId, ...portalOwnedTaskWhere(scope) })
120
+ if (!task) return null
121
+ const ticket = await findOneWithDecryption(
122
+ scope.em,
123
+ StaffSupportTicket,
124
+ { taskId: task.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
125
+ undefined,
126
+ { tenantId: scope.tenantId, organizationId: scope.organizationId },
127
+ )
128
+ if (!ticket) return null
129
+ return { task, ticket }
130
+ }
131
+
132
+ export async function resolveCustomerDisplay(
133
+ scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,
134
+ ): Promise<{ displayName: string | null; kind: string | null }> {
135
+ try {
136
+ const customer = await findOneWithDecryption(
137
+ scope.em,
138
+ CustomerEntity,
139
+ { id: scope.customerId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
140
+ undefined,
141
+ { tenantId: scope.tenantId, organizationId: scope.organizationId },
142
+ )
143
+ const displayName = typeof customer?.displayName === 'string' ? customer.displayName : null
144
+ const kind = typeof customer?.kind === 'string' ? customer.kind : null
145
+ return { displayName, kind }
146
+ } catch {
147
+ return { displayName: null, kind: null }
148
+ }
149
+ }
150
+
151
+ type PortalTicketActivityDb = {
152
+ staff_time_tasks: {
153
+ id: string
154
+ tenant_id: string
155
+ organization_id: string
156
+ customer_id: string | null
157
+ source: string
158
+ task_status_id: string
159
+ created_at: Date
160
+ deleted_at: Date | null
161
+ }
162
+ staff_time_task_comments: {
163
+ task_id: string
164
+ tenant_id: string
165
+ organization_id: string
166
+ visibility: string
167
+ created_at: Date
168
+ deleted_at: Date | null
169
+ }
170
+ }
171
+
172
+ export type PortalTicketPageRow = { id: string; lastActivityAt: Date }
173
+
174
+ function toDate(value: unknown, fallback: Date): Date {
175
+ if (value instanceof Date) return value
176
+ if (typeof value === 'string' || typeof value === 'number') {
177
+ const parsed = new Date(value)
178
+ if (!Number.isNaN(parsed.getTime())) return parsed
179
+ }
180
+ return fallback
181
+ }
182
+
183
+ /**
184
+ * One page of the caller's tickets, newest customer-visible activity first. Activity
185
+ * is the latest of the ticket's filing time and its newest customer-visible comment
186
+ * (either side), so internal staff edits — which bump `updated_at` — never reorder the
187
+ * customer's list while a reply the customer can read always does. The ownership
188
+ * clause is the same as `portalOwnedTaskWhere`; the activity is computed per row by a
189
+ * correlated `max()` served by the comments `(organization_id, task_id, created_at)` index.
190
+ */
191
+ export async function listPortalTicketPage(
192
+ scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,
193
+ options: { statusIds: readonly string[] | null; limit: number; offset: number },
194
+ ): Promise<{ rows: PortalTicketPageRow[]; total: number }> {
195
+ if (options.statusIds && options.statusIds.length === 0) return { rows: [], total: 0 }
196
+ const db = scope.em.getKysely<PortalTicketActivityDb>()
197
+ let owned = db
198
+ .selectFrom('staff_time_tasks as task')
199
+ .where('task.tenant_id', '=', scope.tenantId)
200
+ .where('task.organization_id', '=', scope.organizationId)
201
+ .where('task.customer_id', '=', scope.customerId)
202
+ .where('task.source', '=', 'portal')
203
+ .where('task.deleted_at', 'is', null)
204
+ if (options.statusIds) owned = owned.where('task.task_status_id', 'in', [...options.statusIds])
205
+ const lastActivity = sql<Date>`greatest(task.created_at, coalesce((
206
+ select max(visible_comment.created_at)
207
+ from staff_time_task_comments as visible_comment
208
+ where visible_comment.organization_id = task.organization_id
209
+ and visible_comment.task_id = task.id
210
+ and visible_comment.tenant_id = task.tenant_id
211
+ and visible_comment.visibility = 'customer'
212
+ and visible_comment.deleted_at is null
213
+ ), task.created_at))`
214
+ const [countRow, pageRows] = await Promise.all([
215
+ owned.select((builder) => builder.fn.countAll<string | number>().as('total')).executeTakeFirst(),
216
+ owned
217
+ .select(['task.id as id', 'task.created_at as created_at'])
218
+ .select(lastActivity.as('last_activity_at'))
219
+ .orderBy('last_activity_at', 'desc')
220
+ .orderBy('task.id', 'desc')
221
+ .limit(options.limit)
222
+ .offset(options.offset)
223
+ .execute(),
224
+ ])
225
+ const total = Number(countRow?.total ?? 0)
226
+ return {
227
+ total: Number.isFinite(total) ? total : 0,
228
+ rows: pageRows.map((row) => ({
229
+ id: row.id,
230
+ lastActivityAt: toDate(row.last_activity_at, toDate(row.created_at, new Date(0))),
231
+ })),
232
+ }
233
+ }
@@ -0,0 +1,117 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import {
3
+ StaffSupportRoutingRule,
4
+ StaffTimeProject,
5
+ StaffTimeTaskStatus,
6
+ type StaffSupportRoutedVia,
7
+ } from '../../data/entities'
8
+
9
+ export type SupportScope = {
10
+ tenantId: string
11
+ organizationId: string
12
+ }
13
+
14
+ export type SupportRoutingRuleView = {
15
+ id: string
16
+ categoryValue: string | null
17
+ timeProjectId: string
18
+ isActive: boolean
19
+ updatedAt?: Date | null
20
+ }
21
+
22
+ export type SupportRoute = {
23
+ timeProjectId: string
24
+ via: StaffSupportRoutedVia
25
+ }
26
+
27
+ export function resolveSupportRouteFromRules(
28
+ rules: SupportRoutingRuleView[],
29
+ categoryValue: string,
30
+ usableProjectIds: ReadonlySet<string>,
31
+ ): SupportRoute | null {
32
+ const categoryRule = rules.find((rule) => rule.categoryValue === categoryValue)
33
+ if (categoryRule && categoryRule.isActive && usableProjectIds.has(categoryRule.timeProjectId)) {
34
+ return { timeProjectId: categoryRule.timeProjectId, via: 'category' }
35
+ }
36
+ const fallbackRule = rules.find((rule) => rule.categoryValue === null)
37
+ if (fallbackRule && usableProjectIds.has(fallbackRule.timeProjectId)) {
38
+ return { timeProjectId: fallbackRule.timeProjectId, via: 'fallback' }
39
+ }
40
+ return null
41
+ }
42
+
43
+ export async function loadSupportRoutingRules(
44
+ em: EntityManager,
45
+ scope: SupportScope,
46
+ ): Promise<SupportRoutingRuleView[]> {
47
+ const rows = await em.find(StaffSupportRoutingRule, {
48
+ tenantId: scope.tenantId,
49
+ organizationId: scope.organizationId,
50
+ deletedAt: null,
51
+ })
52
+ return rows.map((row) => ({
53
+ id: row.id,
54
+ categoryValue: row.categoryValue ?? null,
55
+ timeProjectId: row.timeProjectId,
56
+ isActive: row.isActive,
57
+ updatedAt: row.updatedAt ?? null,
58
+ }))
59
+ }
60
+
61
+ export async function loadUsableSupportProjectIds(
62
+ em: EntityManager,
63
+ scope: SupportScope,
64
+ projectIds: string[],
65
+ ): Promise<Set<string>> {
66
+ const unique = Array.from(new Set(projectIds))
67
+ if (unique.length === 0) return new Set()
68
+ const projects = await em.find(
69
+ StaffTimeProject,
70
+ {
71
+ id: { $in: unique },
72
+ tenantId: scope.tenantId,
73
+ organizationId: scope.organizationId,
74
+ deletedAt: null,
75
+ status: { $in: ['active', 'on_hold'] },
76
+ },
77
+ { fields: ['id'] },
78
+ )
79
+ if (projects.length === 0) return new Set()
80
+ const defaults = await em.find(
81
+ StaffTimeTaskStatus,
82
+ {
83
+ timeProjectId: { $in: projects.map((project) => project.id) },
84
+ tenantId: scope.tenantId,
85
+ organizationId: scope.organizationId,
86
+ isDefault: true,
87
+ deletedAt: null,
88
+ },
89
+ { fields: ['timeProjectId'] },
90
+ )
91
+ return new Set(defaults.map((status) => status.timeProjectId))
92
+ }
93
+
94
+ export async function resolveSupportRoute(
95
+ em: EntityManager,
96
+ scope: SupportScope,
97
+ categoryValue: string,
98
+ ): Promise<SupportRoute | null> {
99
+ const rules = await loadSupportRoutingRules(em, scope)
100
+ const usable = await loadUsableSupportProjectIds(em, scope, rules.map((rule) => rule.timeProjectId))
101
+ return resolveSupportRouteFromRules(rules, categoryValue, usable)
102
+ }
103
+
104
+ export async function resolveRoutableCategoryValues(
105
+ em: EntityManager,
106
+ scope: SupportScope,
107
+ categoryValues: string[],
108
+ ): Promise<Map<string, SupportRoute>> {
109
+ const rules = await loadSupportRoutingRules(em, scope)
110
+ const usable = await loadUsableSupportProjectIds(em, scope, rules.map((rule) => rule.timeProjectId))
111
+ const routes = new Map<string, SupportRoute>()
112
+ for (const value of categoryValues) {
113
+ const route = resolveSupportRouteFromRules(rules, value, usable)
114
+ if (route) routes.set(value, route)
115
+ }
116
+ return routes
117
+ }
@@ -0,0 +1,45 @@
1
+ export type RoutingRuleSnapshot = {
2
+ id: string
3
+ categoryValue: string | null
4
+ project: { id: string }
5
+ updatedAt?: string | null
6
+ }
7
+
8
+ export type RoutingRuleSnapshotSet = {
9
+ rules: RoutingRuleSnapshot[]
10
+ }
11
+
12
+ export type RoutingRuleDialogState =
13
+ | { mode: 'create'; categoryValue: string; timeProjectId: string }
14
+ | {
15
+ mode: 'edit'
16
+ ruleId: string
17
+ categoryValue: string
18
+ timeProjectId: string
19
+ expectedUpdatedAt: string | null
20
+ }
21
+
22
+ export function routingRuleVersion(rule: { updatedAt?: string | null }): string | null {
23
+ return typeof rule.updatedAt === 'string' && rule.updatedAt.length > 0 ? rule.updatedAt : null
24
+ }
25
+
26
+ export function buildRoutingRuleEditDraft(rule: RoutingRuleSnapshot): RoutingRuleDialogState {
27
+ return {
28
+ mode: 'edit',
29
+ ruleId: rule.id,
30
+ categoryValue: rule.categoryValue ?? '',
31
+ timeProjectId: rule.project.id,
32
+ expectedUpdatedAt: routingRuleVersion(rule),
33
+ }
34
+ }
35
+
36
+ export function rebaseRoutingRuleDialog(
37
+ draft: RoutingRuleDialogState | null,
38
+ snapshot: RoutingRuleSnapshotSet,
39
+ ): RoutingRuleDialogState | null {
40
+ if (!draft || draft.mode === 'create') return draft
41
+ const current = snapshot.rules.find((rule) => rule.id === draft.ruleId)
42
+ if (!current) return null
43
+ if (routingRuleVersion(current) === draft.expectedUpdatedAt) return draft
44
+ return buildRoutingRuleEditDraft(current)
45
+ }
@@ -0,0 +1,151 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
3
+ import { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
4
+ import { StaffSupportRoutingRule, StaffTimeProject, StaffTimeTaskStatus } from '../../data/entities'
5
+ import { seedProjectTaskStatuses } from '../timesheets-tasks/seedProjectStatuses'
6
+ import type { TaskStatusLabelTranslate } from '../timesheets-tasks/defaultStatusTemplate'
7
+ import { deriveProjectCode } from '../time-tracking/projectCode'
8
+ import { seedSupportTicketCategories } from './dictionaries'
9
+
10
+ export const SUPPORT_FALLBACK_PROJECT_NAME = 'Customer support'
11
+
12
+ type SupportScope = {
13
+ tenantId: string
14
+ organizationId: string
15
+ }
16
+
17
+ export type ProjectCodeGenerator = (name: string, taken: Set<string>) => string
18
+
19
+ export type EnsureFallbackResult = {
20
+ ruleId: string
21
+ timeProjectId: string
22
+ created: boolean
23
+ /** Board columns seeded for a freshly created project; empty when nothing was created. */
24
+ taskStatusIds: string[]
25
+ }
26
+
27
+ type ProjectCodeResolverLike = { generate(name: string, taken: Set<string>): string }
28
+
29
+ /**
30
+ * The `timeProjectCodeResolver` DI override (EP-39), so a seeded project gets the
31
+ * same code a project created through the UI would. `undefined` (built-in
32
+ * generator) when the container does not carry one.
33
+ */
34
+ export function resolveContainerProjectCodeGenerator(
35
+ container: { resolve: (name: string) => unknown } | null | undefined,
36
+ ): ProjectCodeGenerator | undefined {
37
+ if (!container) return undefined
38
+ try {
39
+ const resolver = container.resolve('timeProjectCodeResolver') as ProjectCodeResolverLike | undefined
40
+ if (!resolver || typeof resolver.generate !== 'function') return undefined
41
+ return (name, taken) => resolver.generate(name, taken)
42
+ } catch {
43
+ return undefined
44
+ }
45
+ }
46
+
47
+ export async function seedSupportDefaults(
48
+ em: EntityManager,
49
+ scope: SupportScope,
50
+ options: { translate: TaskStatusLabelTranslate; generateCode?: ProjectCodeGenerator },
51
+ ): Promise<void> {
52
+ await seedSupportTicketCategories(em, scope)
53
+ await ensureFallbackProjectAndRule(em, scope, {
54
+ translate: options.translate,
55
+ generateCode: options.generateCode,
56
+ projectName: options.translate('staff.support.fallbackProject.name', SUPPORT_FALLBACK_PROJECT_NAME),
57
+ })
58
+ }
59
+
60
+ async function findFallbackRule(em: EntityManager, scope: SupportScope): Promise<StaffSupportRoutingRule | null> {
61
+ return em.findOne(StaffSupportRoutingRule, {
62
+ tenantId: scope.tenantId,
63
+ organizationId: scope.organizationId,
64
+ categoryValue: null,
65
+ deletedAt: null,
66
+ })
67
+ }
68
+
69
+ /**
70
+ * Idempotent: an existing fallback rule is returned untouched, and a call that
71
+ * loses a race to a concurrent one (unique index on the fallback rule or on the
72
+ * project code) re-reads the winner's rule and reports `created: false`.
73
+ */
74
+ export async function ensureFallbackProjectAndRule(
75
+ em: EntityManager,
76
+ scope: SupportScope,
77
+ options: { translate: TaskStatusLabelTranslate; generateCode?: ProjectCodeGenerator; projectName?: string },
78
+ ): Promise<EnsureFallbackResult> {
79
+ const existingRule = await findFallbackRule(em, scope)
80
+ if (existingRule) {
81
+ return { ruleId: existingRule.id, timeProjectId: existingRule.timeProjectId, created: false, taskStatusIds: [] }
82
+ }
83
+ const takenRows = await em.find(
84
+ StaffTimeProject,
85
+ { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
86
+ { fields: ['code'] },
87
+ )
88
+ const taken = new Set(takenRows.map((row) => row.code))
89
+ const name = options.projectName ?? SUPPORT_FALLBACK_PROJECT_NAME
90
+ const generate = options.generateCode ?? ((projectName: string, codes: Set<string>) => deriveProjectCode(projectName, codes))
91
+ const now = new Date()
92
+ const project = em.create(StaffTimeProject, {
93
+ tenantId: scope.tenantId,
94
+ organizationId: scope.organizationId,
95
+ name,
96
+ code: generate(name, taken),
97
+ customerId: null,
98
+ customerSnapshot: null,
99
+ projectType: 'internal',
100
+ status: 'active',
101
+ billableByDefault: false,
102
+ budgetKind: 'none',
103
+ createdAt: now,
104
+ updatedAt: now,
105
+ deletedAt: null,
106
+ })
107
+ let rule: StaffSupportRoutingRule | null = null
108
+ let statuses: StaffTimeTaskStatus[] = []
109
+ try {
110
+ await withAtomicFlush(
111
+ em,
112
+ [
113
+ () => {
114
+ em.persist(project)
115
+ },
116
+ () => {
117
+ statuses = seedProjectTaskStatuses(
118
+ em,
119
+ { tenantId: scope.tenantId, organizationId: scope.organizationId, timeProjectId: project.id },
120
+ options.translate,
121
+ )
122
+ rule = em.create(StaffSupportRoutingRule, {
123
+ tenantId: scope.tenantId,
124
+ organizationId: scope.organizationId,
125
+ categoryValue: null,
126
+ timeProjectId: project.id,
127
+ isActive: true,
128
+ createdAt: now,
129
+ updatedAt: now,
130
+ deletedAt: null,
131
+ })
132
+ em.persist(rule)
133
+ },
134
+ ],
135
+ { transaction: true, label: 'staff.support.ensure_fallback_project' },
136
+ )
137
+ } catch (err) {
138
+ if (!isUniqueViolation(err)) throw err
139
+ const winner = await findFallbackRule(em.fork(), scope)
140
+ if (!winner) throw err
141
+ return { ruleId: winner.id, timeProjectId: winner.timeProjectId, created: false, taskStatusIds: [] }
142
+ }
143
+ const created = rule as StaffSupportRoutingRule | null
144
+ if (!created) throw new Error('[internal] fallback routing rule was not created')
145
+ return {
146
+ ruleId: created.id,
147
+ timeProjectId: project.id,
148
+ created: true,
149
+ taskStatusIds: statuses.map((status) => status.id),
150
+ }
151
+ }
@@ -0,0 +1,39 @@
1
+ import { createLogger } from '@open-mercato/shared/lib/logger'
2
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
3
+ import type { StaffSupportTicket, StaffTimeTask } from '../../data/entities'
4
+ import { emitStaffEvent } from '../../events'
5
+
6
+ const logger = createLogger('staff').child({ component: 'lib/support/supportActivity' })
7
+
8
+ export type SupportAttachmentActivityKind = 'attachment_added' | 'attachment_removed'
9
+
10
+ /**
11
+ * Tells open backoffice screens that a customer changed the files on a ticket.
12
+ * Ids only, like every `activity_recorded` broadcast. The attachment id keeps
13
+ * two quick changes of the same kind distinct for the browser bridge's 500 ms
14
+ * duplicate filter. A failed publish is reported and never fails the upload or
15
+ * deletion that already happened.
16
+ */
17
+ export function emitSupportAttachmentActivity(
18
+ task: Pick<StaffTimeTask, 'id' | 'tenantId' | 'organizationId' | 'timeProjectId'>,
19
+ ticket: Pick<StaffSupportTicket, 'id'>,
20
+ kind: SupportAttachmentActivityKind,
21
+ attachmentId: string,
22
+ ): void {
23
+ void emitStaffEvent(
24
+ 'staff.support_ticket.activity_recorded',
25
+ {
26
+ id: ticket.id,
27
+ tenantId: task.tenantId,
28
+ organizationId: task.organizationId,
29
+ taskId: task.id,
30
+ projectId: task.timeProjectId,
31
+ kind,
32
+ attachmentId,
33
+ },
34
+ { persistent: true },
35
+ ).catch((err) => {
36
+ logger.error('staff support attachment activity emit failed', { err, taskId: task.id })
37
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_activity_emit_failed' })
38
+ })
39
+ }
@@ -0,0 +1,20 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
6
+
7
+ export function PortalSupportNotLinked({ size = 'lg' }: { size?: 'sm' | 'lg' }) {
8
+ const t = useT()
9
+ return (
10
+ <EmptyState
11
+ variant="subtle"
12
+ size={size}
13
+ title={t('staff.portal.support.notLinked.title', 'Support tickets are not available for your account')}
14
+ description={t(
15
+ 'staff.portal.support.notLinked.description',
16
+ 'Your portal account is not linked to a company, so there are no tickets to show. Ask your account administrator to link it.',
17
+ )}
18
+ />
19
+ )
20
+ }