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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * `GET|POST /api/staff/portal/support-tickets` — customer portal support intake.
3
+ *
4
+ * Every row is loaded with `portalOwnedTaskWhere` (tenant, organization, the
5
+ * signed-in portal user's own `customerEntityId` on the TASK, `source = 'portal'`,
6
+ * not deleted). The project's customer is never consulted, so tickets from many
7
+ * customers can share one project. Subject and description come from the encrypted
8
+ * `customer_request_snapshot`, never from the staff-editable task text, and the
9
+ * response carries no project, task reference, assignee, internal comment or money.
10
+ */
11
+ import { NextResponse } from 'next/server'
12
+ import { z } from 'zod'
13
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
14
+ import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
15
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
16
+ import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
17
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
18
+ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
19
+ import {
20
+ StaffSupportTicket,
21
+ StaffTimeTask,
22
+ StaffTimeTaskStatus,
23
+ } from '../../../data/entities'
24
+ import {
25
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
26
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
27
+ listPortalTicketPage,
28
+ portalOwnedTaskWhere,
29
+ resolveCustomerDisplay,
30
+ resolveSupportPortalScope,
31
+ } from '../../../lib/support/portalTickets'
32
+ import {
33
+ SUPPORT_TICKET_RESOURCE_KIND,
34
+ buildPortalCommandContext,
35
+ checkPortalRateLimit,
36
+ portalErrorResponse,
37
+ } from '../../../lib/support/portalRoute'
38
+ import { derivePortalStatus } from '../../../lib/support/portalStatus'
39
+ import {
40
+ staffSupportTicketCommandIds,
41
+ type SupportTicketCreateFromPortalInput,
42
+ type SupportTicketCreateResult,
43
+ } from '../../../commands/support-tickets'
44
+
45
+ export const metadata: { requireAuth?: boolean } = { requireAuth: false }
46
+
47
+ const portalStatusSchema = z.enum(['received', 'in_progress', 'resolved'])
48
+
49
+ const listQuerySchema = z.object({
50
+ page: z.coerce.number().int().min(1).default(1),
51
+ pageSize: z.coerce.number().int().min(1).max(50).default(20),
52
+ status: portalStatusSchema.optional(),
53
+ })
54
+
55
+ const ticketListItemSchema = z.object({
56
+ id: z.string().uuid(),
57
+ supportReference: z.string(),
58
+ subject: z.string(),
59
+ category: z.object({ value: z.string(), label: z.string() }),
60
+ portalStatus: portalStatusSchema,
61
+ createdAt: z.string().nullable(),
62
+ updatedAt: z.string().nullable(),
63
+ lastActivityAt: z.string().nullable(),
64
+ })
65
+
66
+ const listResponseSchema = z.object({
67
+ items: z.array(ticketListItemSchema),
68
+ total: z.number().int(),
69
+ page: z.number().int(),
70
+ pageSize: z.number().int(),
71
+ totalPages: z.number().int(),
72
+ })
73
+
74
+ const createBodySchema = z.object({
75
+ categoryValue: z.string().trim().min(1).max(150),
76
+ subject: z.string().trim().min(3).max(200),
77
+ description: z.string().trim().min(1).max(10000),
78
+ })
79
+
80
+ const createResponseSchema = z.object({
81
+ ok: z.literal(true),
82
+ id: z.string().uuid(),
83
+ supportReference: z.string(),
84
+ })
85
+
86
+ function isoOrNull(value: Date | null | undefined): string | null {
87
+ return value ? value.toISOString() : null
88
+ }
89
+
90
+ async function statusIdsFor(
91
+ scope: { em: import('@mikro-orm/postgresql').EntityManager; tenantId: string; organizationId: string },
92
+ status: z.infer<typeof portalStatusSchema>,
93
+ ): Promise<string[]> {
94
+ const where: Record<string, unknown> = { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null }
95
+ if (status === 'resolved') where.isDone = true
96
+ else if (status === 'received') Object.assign(where, { isDone: false, isDefault: true })
97
+ else Object.assign(where, { isDone: false, isDefault: false })
98
+ const rows = await scope.em.find(StaffTimeTaskStatus, where, { fields: ['id'] })
99
+ return rows.map((row) => row.id)
100
+ }
101
+
102
+ export async function GET(req: Request) {
103
+ try {
104
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)
105
+ if (scope instanceof Response) return scope
106
+ const url = new URL(req.url)
107
+ const query = listQuerySchema.parse(Object.fromEntries(url.searchParams.entries()))
108
+ const statusIds = query.status ? await statusIdsFor(scope, query.status) : null
109
+ const page = await listPortalTicketPage(scope, {
110
+ statusIds,
111
+ limit: query.pageSize,
112
+ offset: (query.page - 1) * query.pageSize,
113
+ })
114
+ const total = page.total
115
+ const pageIds = page.rows.map((row) => row.id)
116
+ const lastActivityByTask = new Map(page.rows.map((row) => [row.id, row.lastActivityAt]))
117
+ const loadedTasks = pageIds.length
118
+ ? await scope.em.find(StaffTimeTask, { id: { $in: pageIds }, ...portalOwnedTaskWhere(scope) })
119
+ : []
120
+ const taskById = new Map(loadedTasks.map((task) => [task.id, task]))
121
+ const tasks = pageIds.flatMap((id) => {
122
+ const task = taskById.get(id)
123
+ return task ? [task] : []
124
+ })
125
+ const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }
126
+ const taskIds = tasks.map((task) => task.id)
127
+ const tickets = taskIds.length
128
+ ? await findWithDecryption(
129
+ scope.em,
130
+ StaffSupportTicket,
131
+ { taskId: { $in: taskIds }, ...decryptionScope, deletedAt: null },
132
+ undefined,
133
+ decryptionScope,
134
+ )
135
+ : []
136
+ const ticketByTask = new Map(tickets.map((ticket) => [ticket.taskId, ticket]))
137
+ const taskStatusIds = [...new Set(tasks.map((task) => task.taskStatusId))]
138
+ const statuses = taskStatusIds.length
139
+ ? await scope.em.find(StaffTimeTaskStatus, { id: { $in: taskStatusIds }, ...decryptionScope }, { fields: ['id', 'isDefault', 'isDone'] })
140
+ : []
141
+ const statusById = new Map(statuses.map((status) => [status.id, status]))
142
+ const items = tasks.flatMap((task) => {
143
+ const ticket = ticketByTask.get(task.id)
144
+ if (!ticket) return []
145
+ const request = ticket.customerRequestSnapshot
146
+ const lastActivity = lastActivityByTask.get(task.id) ?? task.createdAt
147
+ return [
148
+ {
149
+ id: task.id,
150
+ supportReference: ticket.supportReference,
151
+ subject: request && typeof request.subject === 'string' ? request.subject : '',
152
+ category: ticket.categorySnapshot,
153
+ portalStatus: derivePortalStatus(statusById.get(task.taskStatusId) ?? null),
154
+ createdAt: isoOrNull(task.createdAt),
155
+ updatedAt: isoOrNull(task.updatedAt),
156
+ lastActivityAt: isoOrNull(lastActivity),
157
+ },
158
+ ]
159
+ })
160
+ return NextResponse.json(
161
+ listResponseSchema.parse({
162
+ items,
163
+ total,
164
+ page: query.page,
165
+ pageSize: query.pageSize,
166
+ totalPages: Math.max(1, Math.ceil(total / query.pageSize)),
167
+ }),
168
+ )
169
+ } catch (err) {
170
+ return portalErrorResponse(err, 'staff portal support tickets GET failed')
171
+ }
172
+ }
173
+
174
+ export async function POST(req: Request) {
175
+ try {
176
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)
177
+ if (scope instanceof Response) return scope
178
+ const limited = await checkPortalRateLimit(scope, 'create')
179
+ if (limited) return limited
180
+ const body = createBodySchema.parse(await readJsonSafe<Record<string, unknown>>(req, {}))
181
+ const guarded = await runRouteMutationGuards({
182
+ container: scope.container,
183
+ req,
184
+ auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
185
+ input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: null, operation: 'create', mutationPayload: body },
186
+ })
187
+ if (!guarded.ok) return guarded.response
188
+ const effectiveBody = guarded.modifiedPayload
189
+ ? createBodySchema.parse({ ...body, ...guarded.modifiedPayload })
190
+ : body
191
+ const customer = await resolveCustomerDisplay(scope)
192
+ const input: SupportTicketCreateFromPortalInput = {
193
+ tenantId: scope.tenantId,
194
+ organizationId: scope.organizationId,
195
+ customerId: scope.customerId,
196
+ customerUserId: scope.auth.sub,
197
+ customerDisplayName: customer.displayName,
198
+ customerKind: customer.kind,
199
+ reporterDisplayName: scope.auth.displayName ?? null,
200
+ categoryValue: effectiveBody.categoryValue,
201
+ subject: effectiveBody.subject,
202
+ description: effectiveBody.description,
203
+ }
204
+ const commandBus = scope.container.resolve('commandBus') as CommandBus
205
+ const { result } = await commandBus.execute<SupportTicketCreateFromPortalInput, SupportTicketCreateResult>(
206
+ staffSupportTicketCommandIds.createFromPortal,
207
+ { input, ctx: buildPortalCommandContext(scope, req) },
208
+ )
209
+ await guarded.runAfterSuccess()
210
+ return NextResponse.json(
211
+ createResponseSchema.parse({ ok: true, id: result.taskId, supportReference: result.supportReference }),
212
+ { status: 201 },
213
+ )
214
+ } catch (err) {
215
+ const rejection = getCommandInterceptorHttpRejection(err)
216
+ if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })
217
+ return portalErrorResponse(err, 'staff portal support tickets POST failed')
218
+ }
219
+ }
220
+
221
+ const errorSchema = z.object({ ok: z.literal(false).optional(), error: z.string(), code: z.string().optional() })
222
+
223
+ export const openApi: OpenApiRouteDoc = {
224
+ tag: 'Staff Portal',
225
+ summary: 'Customer support tickets',
226
+ methods: {
227
+ GET: {
228
+ summary: 'List my support tickets',
229
+ description:
230
+ "Support tickets filed by the signed-in portal user's customer, newest activity first: `lastActivityAt` is the later of the filing time and the newest customer-visible message (from either side), and the list is ordered by it. Internal edits do not reorder the list. Status is the customer-facing status derived from the board column (received, in_progress, resolved).",
231
+ query: listQuerySchema,
232
+ responses: [{ status: 200, description: 'Tickets', schema: listResponseSchema }],
233
+ errors: [
234
+ { status: 401, description: 'No portal session', schema: errorSchema },
235
+ { status: 403, description: 'Not linked to a customer, or missing portal.support_tickets.view', schema: errorSchema },
236
+ ],
237
+ },
238
+ POST: {
239
+ summary: 'File a support ticket',
240
+ description:
241
+ 'Creates a ticket in the category the customer chose. The category routes to a project (its own rule, else the organization fallback rule); the ticket lands in that project default column. Rate limited per customer user.',
242
+ requestBody: { contentType: 'application/json', schema: createBodySchema },
243
+ responses: [{ status: 201, description: 'Ticket filed', schema: createResponseSchema }],
244
+ errors: [
245
+ { status: 401, description: 'No portal session', schema: errorSchema },
246
+ { status: 403, description: 'Missing portal.support_tickets.manage', schema: errorSchema },
247
+ { status: 409, description: 'Too many tickets were filed at once; resubmit', schema: errorSchema },
248
+ { status: 422, description: 'The category is not available', schema: errorSchema },
249
+ { status: 429, description: 'Rate limit exceeded', schema: errorSchema },
250
+ ],
251
+ },
252
+ },
253
+ }
@@ -18,6 +18,7 @@ import {
18
18
  runTimesheetInterceptors,
19
19
  } from '../_shared/withTimesheetInterceptors'
20
20
  import {
21
+ preserveOmittedSupportSettings,
21
22
  readTimeTrackingSettings,
22
23
  writeTimeTrackingSettings,
23
24
  type TimeTrackingSettings,
@@ -152,7 +153,12 @@ async function PUT(req: Request) {
152
153
  const { session } = interceptors
153
154
 
154
155
  const settingsSchema = buildTimeTrackingSettingsSchema()
155
- const parsed = settingsSchema.parse(session.body)
156
+ const requestBody = await preserveOmittedSupportSettings(
157
+ resolveConfigService(context),
158
+ { tenantId: context.tenantId },
159
+ session.body,
160
+ )
161
+ const parsed = settingsSchema.parse(requestBody)
156
162
 
157
163
  const guardResult = await runRouteMutationGuards({
158
164
  container: context.container,
@@ -0,0 +1,131 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
7
+ import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
8
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
+ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
10
+ import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
11
+ import { createLogger } from '@open-mercato/shared/lib/logger'
12
+ import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
13
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
14
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
15
+ import { resolveFeatureAccess } from '../../../../lib/time-tracking/featureAccess'
16
+ import {
17
+ SUPPORT_ROUTING_RULE_RESOURCE_KIND,
18
+ staffSupportRoutingRuleCommandIds,
19
+ type SupportFallbackProjectResult,
20
+ } from '../../../../commands/support-routing-rules'
21
+ import type { StaffSupportFallbackProjectCreateInput } from '../../../../data/validators'
22
+
23
+ const logger = createLogger('staff').child({ component: 'api/timesheets/support-routing-rules/fallback-project' })
24
+
25
+ const SETTINGS_FEATURE = 'staff.timesheets.settings.manage'
26
+
27
+ export const metadata = {
28
+ POST: { requireAuth: true, requireFeatures: [SETTINGS_FEATURE] },
29
+ }
30
+
31
+ export async function POST(req: Request): Promise<Response> {
32
+ try {
33
+ const container = await createRequestContainer()
34
+ const auth = await getAuthFromRequest(req)
35
+ const { translate } = await resolveTranslations()
36
+ if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
37
+
38
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
39
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null
40
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
41
+ if (!tenantId || !organizationId) {
42
+ throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
43
+ }
44
+
45
+ const actorId = auth.sub ?? ''
46
+ const access = await resolveFeatureAccess(container, actorId, [SETTINGS_FEATURE], { tenantId, organizationId })
47
+ if (!access.allowed) throw forbidden(translate('staff.errors.forbidden', 'Forbidden'))
48
+
49
+ const input: StaffSupportFallbackProjectCreateInput = { tenantId, organizationId }
50
+ const guardResult = await runRouteMutationGuards({
51
+ container,
52
+ req,
53
+ auth: { userId: actorId, tenantId, organizationId, userFeatures: access.grantedFeatures },
54
+ input: {
55
+ resourceKind: SUPPORT_ROUTING_RULE_RESOURCE_KIND,
56
+ resourceId: null,
57
+ operation: 'create',
58
+ mutationPayload: input,
59
+ },
60
+ })
61
+ if (!guardResult.ok) return guardResult.response
62
+
63
+ const routeCtx: CommandRuntimeContext = {
64
+ container,
65
+ auth,
66
+ organizationScope: scope,
67
+ selectedOrganizationId: organizationId,
68
+ organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
69
+ request: req,
70
+ }
71
+ const commandBus = container.resolve('commandBus') as CommandBus
72
+ const { result, logEntry } = await commandBus.execute<StaffSupportFallbackProjectCreateInput, SupportFallbackProjectResult>(
73
+ staffSupportRoutingRuleCommandIds.createFallbackProject,
74
+ { input, ctx: routeCtx },
75
+ )
76
+ await guardResult.runAfterSuccess()
77
+ const response = NextResponse.json(
78
+ { ruleId: result?.ruleId ?? null, timeProjectId: result?.timeProjectId ?? null, created: result?.created === true },
79
+ { status: result?.created ? 201 : 200 },
80
+ )
81
+ if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
82
+ response.headers.set(
83
+ 'x-om-operation',
84
+ serializeOperationMetadata({
85
+ id: logEntry.id,
86
+ undoToken: logEntry.undoToken,
87
+ commandId: logEntry.commandId,
88
+ actionLabel: logEntry.actionLabel ?? null,
89
+ resourceKind: logEntry.resourceKind ?? SUPPORT_ROUTING_RULE_RESOURCE_KIND,
90
+ resourceId: logEntry.resourceId ?? result?.ruleId ?? null,
91
+ executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
92
+ }),
93
+ )
94
+ }
95
+ return response
96
+ } catch (err) {
97
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
98
+ const interceptorRejection = getCommandInterceptorHttpRejection(err)
99
+ if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
100
+ const { translate } = await resolveTranslations()
101
+ logger.error('staff.support.fallback-project POST failed', { err })
102
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_fallback_project_failed' })
103
+ return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })
104
+ }
105
+ }
106
+
107
+ const responseSchema = z.object({
108
+ ruleId: z.string().uuid().nullable(),
109
+ timeProjectId: z.string().uuid().nullable(),
110
+ created: z.boolean(),
111
+ })
112
+
113
+ export const openApi: OpenApiRouteDoc = {
114
+ tag: 'Staff',
115
+ summary: 'Create the support fallback project',
116
+ methods: {
117
+ POST: {
118
+ summary: 'Create the support fallback project and rule',
119
+ description:
120
+ 'Creates the "Customer support" project (no customer, standard task statuses) and the organization fallback routing rule pointing at it. Idempotent: when a fallback rule already exists nothing is created and the existing rule is returned with `created: false`.',
121
+ responses: [
122
+ { status: 201, description: 'Fallback project and rule created', schema: responseSchema },
123
+ { status: 200, description: 'A fallback rule already existed', schema: responseSchema },
124
+ ],
125
+ errors: [
126
+ { status: 401, description: 'Unauthorized' },
127
+ { status: 403, description: 'Missing staff.timesheets.settings.manage' },
128
+ ],
129
+ },
130
+ },
131
+ }
@@ -0,0 +1,248 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
7
+ import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
+ import { createLogger } from '@open-mercato/shared/lib/logger'
10
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
11
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
+ import { StaffTimeProject } from '../../../../data/entities'
13
+ import { resolveAssignmentGraceDaysFromContainer } from '../../../../lib/time-tracking/projectAssignment'
14
+ import { projectReachesRecipientBeforeManagers } from '../../../../lib/time-tracking/workRecipients'
15
+ import { loadSupportTicketCategories } from '../../../../lib/support/dictionaries'
16
+ import {
17
+ loadSupportRoutingRules,
18
+ loadUsableSupportProjectIds,
19
+ resolveSupportRouteFromRules,
20
+ type SupportRoutingRuleView,
21
+ } from '../../../../lib/support/routing'
22
+
23
+ const logger = createLogger('staff').child({ component: 'api/timesheets/support-routing-rules/preview' })
24
+
25
+ export const metadata = {
26
+ GET: { requireAuth: true, requireFeatures: ['staff.timesheets.settings.manage'] },
27
+ }
28
+
29
+ type RuleProblem = 'paused' | 'project_unusable' | 'category_missing' | null
30
+
31
+ type UnnotifiedCountDb = {
32
+ staff_support_tickets: {
33
+ task_id: string
34
+ tenant_id: string
35
+ organization_id: string
36
+ unnotified_at: Date | null
37
+ deleted_at: Date | null
38
+ }
39
+ staff_time_tasks: {
40
+ id: string
41
+ tenant_id: string
42
+ organization_id: string
43
+ time_project_id: string
44
+ source: string
45
+ deleted_at: Date | null
46
+ }
47
+ }
48
+
49
+ async function countUnnotifiedTicketsByProject(
50
+ em: EntityManager,
51
+ scope: { tenantId: string; organizationId: string },
52
+ ): Promise<Map<string, number>> {
53
+ const rows = await em
54
+ .getKysely<UnnotifiedCountDb>()
55
+ .selectFrom('staff_support_tickets')
56
+ .innerJoin('staff_time_tasks', 'staff_time_tasks.id', 'staff_support_tickets.task_id')
57
+ .select(['staff_time_tasks.time_project_id as time_project_id'])
58
+ .select((builder) => builder.fn.countAll<string | number>().as('ticket_count'))
59
+ .where('staff_support_tickets.tenant_id', '=', scope.tenantId)
60
+ .where('staff_support_tickets.organization_id', '=', scope.organizationId)
61
+ .where('staff_support_tickets.deleted_at', 'is', null)
62
+ .where('staff_support_tickets.unnotified_at', 'is not', null)
63
+ .where('staff_time_tasks.tenant_id', '=', scope.tenantId)
64
+ .where('staff_time_tasks.organization_id', '=', scope.organizationId)
65
+ .where('staff_time_tasks.source', '=', 'portal')
66
+ .where('staff_time_tasks.deleted_at', 'is', null)
67
+ .groupBy('staff_time_tasks.time_project_id')
68
+ .execute()
69
+ const counts = new Map<string, number>()
70
+ for (const row of rows) {
71
+ const count = Number(row.ticket_count)
72
+ if (Number.isFinite(count) && count > 0) counts.set(row.time_project_id, count)
73
+ }
74
+ return counts
75
+ }
76
+
77
+ export async function GET(req: Request): Promise<Response> {
78
+ try {
79
+ const container = await createRequestContainer()
80
+ const auth = await getAuthFromRequest(req)
81
+ const { translate } = await resolveTranslations()
82
+ if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
83
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
84
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null
85
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
86
+ if (!tenantId || !organizationId) {
87
+ throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })
88
+ }
89
+ const supportScope = { tenantId, organizationId }
90
+ const em = (container.resolve('em') as EntityManager).fork()
91
+ const [categories, rules] = await Promise.all([
92
+ loadSupportTicketCategories(em, supportScope),
93
+ loadSupportRoutingRules(em, supportScope),
94
+ ])
95
+ const projectIds = rules.map((rule) => rule.timeProjectId)
96
+ const usable = await loadUsableSupportProjectIds(em, supportScope, projectIds)
97
+ const projects = projectIds.length
98
+ ? await em.find(
99
+ StaffTimeProject,
100
+ { id: { $in: Array.from(new Set(projectIds)) }, tenantId, organizationId },
101
+ { fields: ['id', 'name', 'code', 'status', 'deletedAt', 'ownerUserId', 'defaultAssigneeStaffMemberId'] },
102
+ )
103
+ : []
104
+ const projectById = new Map(projects.map((project) => [project.id, project]))
105
+ const graceDays = await resolveAssignmentGraceDaysFromContainer(container, tenantId)
106
+ const [unnotifiedByProject, reachability] = await Promise.all([
107
+ countUnnotifiedTicketsByProject(em.fork(), supportScope),
108
+ Promise.all(
109
+ projects.map(async (project) => {
110
+ if (project.deletedAt) return [project.id, true] as const
111
+ const reaches = await projectReachesRecipientBeforeManagers(em.fork(), supportScope, project, { graceDays })
112
+ return [project.id, reaches] as const
113
+ }),
114
+ ),
115
+ ])
116
+ const reachesById = new Map<string, boolean>(reachability)
117
+ let unnotifiedTicketCount = 0
118
+ for (const count of unnotifiedByProject.values()) unnotifiedTicketCount += count
119
+ const categoryValues = new Set(categories.map((category) => category.value))
120
+ const fallbackRule = rules.find((rule) => rule.categoryValue === null) ?? null
121
+
122
+ const describeProject = (id: string) => {
123
+ const project = projectById.get(id)
124
+ return {
125
+ id,
126
+ name: project?.name ?? null,
127
+ code: project?.code ?? null,
128
+ status: project && !project.deletedAt ? project.status : 'deleted',
129
+ usable: usable.has(id),
130
+ nobodyNotified: usable.has(id) && reachesById.get(id) === false,
131
+ unnotifiedTicketCount: unnotifiedByProject.get(id) ?? 0,
132
+ }
133
+ }
134
+
135
+ const ruleVersion = (rule: SupportRoutingRuleView): string | null =>
136
+ rule.updatedAt instanceof Date ? rule.updatedAt.toISOString() : null
137
+
138
+ const ruleViews = rules.map((rule) => {
139
+ let problem: RuleProblem = null
140
+ if (rule.categoryValue !== null) {
141
+ if (!categoryValues.has(rule.categoryValue)) problem = 'category_missing'
142
+ else if (!rule.isActive) problem = 'paused'
143
+ else if (!usable.has(rule.timeProjectId)) problem = 'project_unusable'
144
+ } else if (!usable.has(rule.timeProjectId)) {
145
+ problem = 'project_unusable'
146
+ }
147
+ return {
148
+ id: rule.id,
149
+ categoryValue: rule.categoryValue,
150
+ isActive: rule.isActive,
151
+ isFallback: rule.categoryValue === null,
152
+ project: describeProject(rule.timeProjectId),
153
+ problem,
154
+ updatedAt: ruleVersion(rule),
155
+ }
156
+ })
157
+
158
+ const categoryViews = categories.map((category) => {
159
+ const route = resolveSupportRouteFromRules(rules, category.value, usable)
160
+ return {
161
+ value: category.value,
162
+ label: category.label,
163
+ route: route ? { via: route.via, project: describeProject(route.timeProjectId) } : null,
164
+ }
165
+ })
166
+
167
+ return NextResponse.json({
168
+ fallback: fallbackRule
169
+ ? { ruleId: fallbackRule.id, project: describeProject(fallbackRule.timeProjectId), updatedAt: ruleVersion(fallbackRule) }
170
+ : null,
171
+ rules: ruleViews,
172
+ categories: categoryViews,
173
+ unnotifiedTicketCount,
174
+ })
175
+ } catch (err) {
176
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
177
+ const { translate } = await resolveTranslations()
178
+ logger.error('staff.support.routing preview failed', { err })
179
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_routing_preview_failed' })
180
+ return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })
181
+ }
182
+ }
183
+
184
+ const projectSchema = z.object({
185
+ id: z.string().uuid(),
186
+ name: z.string().nullable(),
187
+ code: z.string().nullable(),
188
+ status: z.string(),
189
+ usable: z.boolean(),
190
+ nobodyNotified: z
191
+ .boolean()
192
+ .optional()
193
+ .describe('True for a usable project with no active assigner, no usable default assignee and no owner: new-ticket notifications fall through to the organization project managers'),
194
+ unnotifiedTicketCount: z
195
+ .number()
196
+ .int()
197
+ .optional()
198
+ .describe('Live portal tickets in this project whose support notification reached nobody (`unnotified_at` set)'),
199
+ })
200
+
201
+ const ruleVersionSchema = z
202
+ .string()
203
+ .nullable()
204
+ .describe('The rule `updated_at` read in the same snapshot as its values; send it as the optimistic-lock expected version when editing or deleting the rule')
205
+
206
+ const previewSchema = z.object({
207
+ fallback: z.object({ ruleId: z.string().uuid(), project: projectSchema, updatedAt: ruleVersionSchema }).nullable(),
208
+ rules: z.array(
209
+ z.object({
210
+ id: z.string().uuid(),
211
+ categoryValue: z.string().nullable(),
212
+ isActive: z.boolean(),
213
+ isFallback: z.boolean(),
214
+ project: projectSchema,
215
+ problem: z.enum(['paused', 'project_unusable', 'category_missing']).nullable(),
216
+ updatedAt: ruleVersionSchema,
217
+ }),
218
+ ),
219
+ categories: z.array(
220
+ z.object({
221
+ value: z.string(),
222
+ label: z.string(),
223
+ route: z.object({ via: z.enum(['category', 'fallback']), project: projectSchema }).nullable(),
224
+ }),
225
+ ),
226
+ unnotifiedTicketCount: z
227
+ .number()
228
+ .int()
229
+ .optional()
230
+ .describe('Live portal tickets in the organization whose support notification reached nobody (`unnotified_at` set)'),
231
+ })
232
+
233
+ export const openApi: OpenApiRouteDoc = {
234
+ tag: 'Staff',
235
+ summary: 'Preview support ticket routing',
236
+ methods: {
237
+ GET: {
238
+ summary: 'Preview support ticket routing',
239
+ description:
240
+ 'Returns the fallback rule, every routing rule with the problem that makes it fall back (paused, unusable project, deleted category), the project each support category routes to right now, which routing targets would notify nobody before the organization project managers (`project.nobodyNotified`), how many live tickets were not notified to anyone (`project.unnotifiedTicketCount`, `unnotifiedTicketCount`), and each rule `updatedAt` version token read together with its values.',
241
+ responses: [{ status: 200, description: 'Routing preview', schema: previewSchema }],
242
+ errors: [
243
+ { status: 401, description: 'Unauthorized' },
244
+ { status: 403, description: 'Missing staff.timesheets.settings.manage' },
245
+ ],
246
+ },
247
+ },
248
+ }