@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,100 @@
1
+ import { z } from "zod";
2
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { createLogger } from "@open-mercato/shared/lib/logger";
4
+ import {
5
+ CUSTOMER_UPLOAD_TAG_PREFIX,
6
+ CUSTOMER_VISIBLE_ATTACHMENT_TAG,
7
+ SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,
8
+ customerUploadTag
9
+ } from "./attachments.js";
10
+ const logger = createLogger("staff").child({ component: "lib/support/portalAttachments" });
11
+ const portalSupportAttachmentItemSchema = z.object({
12
+ id: z.string().uuid(),
13
+ fileName: z.string(),
14
+ fileSize: z.number(),
15
+ mimeType: z.string().nullable(),
16
+ downloadUrl: z.string(),
17
+ uploadedByCustomer: z.boolean(),
18
+ canDelete: z.boolean(),
19
+ createdAt: z.string().nullable()
20
+ });
21
+ async function loadPortalAttachmentsApi() {
22
+ try {
23
+ const [entities, drivers, access, crud, metadataLib, imageUrls, thumbnails, limits, uploadService] = await Promise.all([
24
+ import("@open-mercato/core/modules/attachments/data/entities"),
25
+ import("@open-mercato/core/modules/attachments/lib/drivers"),
26
+ import("@open-mercato/core/modules/attachments/lib/access"),
27
+ import("@open-mercato/core/modules/attachments/lib/crud"),
28
+ import("@open-mercato/core/modules/attachments/lib/metadata"),
29
+ import("@open-mercato/core/modules/attachments/lib/imageUrls"),
30
+ import("@open-mercato/core/modules/attachments/lib/thumbnailCache"),
31
+ import("@open-mercato/core/modules/attachments/lib/upload-limits"),
32
+ import("@open-mercato/core/modules/attachments/lib/scoped-upload-service")
33
+ ]);
34
+ return {
35
+ Attachment: entities.Attachment,
36
+ AttachmentPartition: entities.AttachmentPartition,
37
+ StorageDriverFactory: drivers.StorageDriverFactory,
38
+ checkAttachmentAccess: access.checkAttachmentAccess,
39
+ attachmentCrudEvents: crud.attachmentCrudEvents,
40
+ attachmentCrudIndexer: crud.attachmentCrudIndexer,
41
+ readAttachmentMetadata: metadataLib.readAttachmentMetadata,
42
+ slugifyAttachmentFileName: imageUrls.slugifyAttachmentFileName,
43
+ clearAttachmentThumbnailCache: thumbnails.clearAttachmentThumbnailCache,
44
+ isMultipartRequestWithinUploadLimit: limits.isMultipartRequestWithinUploadLimit,
45
+ resolveAttachmentMaxBytes: limits.resolveAttachmentMaxBytes,
46
+ isScopedAttachmentUploadError: uploadService.isScopedAttachmentUploadError
47
+ };
48
+ } catch (err) {
49
+ logger.warn("staff portal support attachments module unavailable", { err });
50
+ return null;
51
+ }
52
+ }
53
+ function readAttachmentTags(api, attachment) {
54
+ return api.readAttachmentMetadata(attachment.storageMetadata).tags ?? [];
55
+ }
56
+ function portalAttachmentAuth(scope) {
57
+ return { sub: scope.auth.sub, tenantId: scope.tenantId, orgId: scope.organizationId, email: scope.auth.email };
58
+ }
59
+ function serializePortalAttachment(api, attachment, taskId, customerUserId, canManage = false) {
60
+ const tags = readAttachmentTags(api, attachment);
61
+ return {
62
+ id: attachment.id,
63
+ fileName: attachment.fileName,
64
+ fileSize: attachment.fileSize,
65
+ mimeType: attachment.mimeType ?? null,
66
+ downloadUrl: `/api/staff/portal/support-tickets/${encodeURIComponent(taskId)}/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,
67
+ uploadedByCustomer: tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_TAG_PREFIX)),
68
+ canDelete: canManage && tags.includes(customerUploadTag(customerUserId)),
69
+ createdAt: attachment.createdAt ? attachment.createdAt.toISOString() : null
70
+ };
71
+ }
72
+ async function listCustomerVisibleAttachments(api, scope, taskId, canManage = false) {
73
+ const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId };
74
+ const attachments = await findWithDecryption(
75
+ scope.em,
76
+ api.Attachment,
77
+ { entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID, recordId: taskId, ...decryptionScope },
78
+ { orderBy: { createdAt: "ASC" } },
79
+ decryptionScope
80
+ );
81
+ const codes = Array.from(new Set(attachments.map((attachment) => attachment.partitionCode)));
82
+ const partitions = codes.length ? await scope.em.find(api.AttachmentPartition, { code: { $in: codes } }) : [];
83
+ const partitionByCode = new Map(partitions.map((partition) => [partition.code, partition]));
84
+ const auth = portalAttachmentAuth(scope);
85
+ return attachments.filter((attachment) => {
86
+ const partition = partitionByCode.get(attachment.partitionCode);
87
+ if (!partition) return false;
88
+ if (!readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG)) return false;
89
+ return api.checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: true }).ok;
90
+ }).map((attachment) => serializePortalAttachment(api, attachment, taskId, scope.auth.sub, canManage));
91
+ }
92
+ export {
93
+ listCustomerVisibleAttachments,
94
+ loadPortalAttachmentsApi,
95
+ portalAttachmentAuth,
96
+ portalSupportAttachmentItemSchema,
97
+ readAttachmentTags,
98
+ serializePortalAttachment
99
+ };
100
+ //# sourceMappingURL=portalAttachments.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/support/portalAttachments.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { Attachment as AttachmentEntity } from '@open-mercato/core/modules/attachments/data/entities'\nimport type { SupportPortalScope } from './portalTickets'\nimport {\n CUSTOMER_UPLOAD_TAG_PREFIX,\n CUSTOMER_VISIBLE_ATTACHMENT_TAG,\n SUPPORT_TICKET_ATTACHMENT_ENTITY_ID,\n customerUploadTag,\n} from './attachments'\n\nconst logger = createLogger('staff').child({ component: 'lib/support/portalAttachments' })\n\nexport type PortalAttachmentsApi = {\n Attachment: typeof import('@open-mercato/core/modules/attachments/data/entities').Attachment\n AttachmentPartition: typeof import('@open-mercato/core/modules/attachments/data/entities').AttachmentPartition\n StorageDriverFactory: typeof import('@open-mercato/core/modules/attachments/lib/drivers').StorageDriverFactory\n checkAttachmentAccess: typeof import('@open-mercato/core/modules/attachments/lib/access').checkAttachmentAccess\n attachmentCrudEvents: typeof import('@open-mercato/core/modules/attachments/lib/crud').attachmentCrudEvents\n attachmentCrudIndexer: typeof import('@open-mercato/core/modules/attachments/lib/crud').attachmentCrudIndexer\n readAttachmentMetadata: typeof import('@open-mercato/core/modules/attachments/lib/metadata').readAttachmentMetadata\n slugifyAttachmentFileName: typeof import('@open-mercato/core/modules/attachments/lib/imageUrls').slugifyAttachmentFileName\n clearAttachmentThumbnailCache: typeof import('@open-mercato/core/modules/attachments/lib/thumbnailCache').clearAttachmentThumbnailCache\n isMultipartRequestWithinUploadLimit: typeof import('@open-mercato/core/modules/attachments/lib/upload-limits').isMultipartRequestWithinUploadLimit\n resolveAttachmentMaxBytes: typeof import('@open-mercato/core/modules/attachments/lib/upload-limits').resolveAttachmentMaxBytes\n isScopedAttachmentUploadError: typeof import('@open-mercato/core/modules/attachments/lib/scoped-upload-service').isScopedAttachmentUploadError\n}\n\nexport const portalSupportAttachmentItemSchema = z.object({\n id: z.string().uuid(),\n fileName: z.string(),\n fileSize: z.number(),\n mimeType: z.string().nullable(),\n downloadUrl: z.string(),\n uploadedByCustomer: z.boolean(),\n canDelete: z.boolean(),\n createdAt: z.string().nullable(),\n})\n\nexport type PortalSupportAttachmentItem = z.infer<typeof portalSupportAttachmentItemSchema>\n\n/**\n * Staff keeps no static edge to the attachments module: it is loaded on demand and a\n * deployment without it reads as \"attachments unavailable\" (`null`).\n */\nexport async function loadPortalAttachmentsApi(): Promise<PortalAttachmentsApi | null> {\n try {\n const [entities, drivers, access, crud, metadataLib, imageUrls, thumbnails, limits, uploadService] = await Promise.all([\n import('@open-mercato/core/modules/attachments/data/entities'),\n import('@open-mercato/core/modules/attachments/lib/drivers'),\n import('@open-mercato/core/modules/attachments/lib/access'),\n import('@open-mercato/core/modules/attachments/lib/crud'),\n import('@open-mercato/core/modules/attachments/lib/metadata'),\n import('@open-mercato/core/modules/attachments/lib/imageUrls'),\n import('@open-mercato/core/modules/attachments/lib/thumbnailCache'),\n import('@open-mercato/core/modules/attachments/lib/upload-limits'),\n import('@open-mercato/core/modules/attachments/lib/scoped-upload-service'),\n ])\n return {\n Attachment: entities.Attachment,\n AttachmentPartition: entities.AttachmentPartition,\n StorageDriverFactory: drivers.StorageDriverFactory,\n checkAttachmentAccess: access.checkAttachmentAccess,\n attachmentCrudEvents: crud.attachmentCrudEvents,\n attachmentCrudIndexer: crud.attachmentCrudIndexer,\n readAttachmentMetadata: metadataLib.readAttachmentMetadata,\n slugifyAttachmentFileName: imageUrls.slugifyAttachmentFileName,\n clearAttachmentThumbnailCache: thumbnails.clearAttachmentThumbnailCache,\n isMultipartRequestWithinUploadLimit: limits.isMultipartRequestWithinUploadLimit,\n resolveAttachmentMaxBytes: limits.resolveAttachmentMaxBytes,\n isScopedAttachmentUploadError: uploadService.isScopedAttachmentUploadError,\n }\n } catch (err) {\n logger.warn('staff portal support attachments module unavailable', { err })\n return null\n }\n}\n\nexport function readAttachmentTags(api: PortalAttachmentsApi, attachment: AttachmentEntity): string[] {\n return api.readAttachmentMetadata(attachment.storageMetadata).tags ?? []\n}\n\nexport function portalAttachmentAuth(scope: SupportPortalScope) {\n return { sub: scope.auth.sub, tenantId: scope.tenantId, orgId: scope.organizationId, email: scope.auth.email }\n}\n\nexport function serializePortalAttachment(\n api: PortalAttachmentsApi,\n attachment: AttachmentEntity,\n taskId: string,\n customerUserId: string,\n canManage = false,\n): PortalSupportAttachmentItem {\n const tags = readAttachmentTags(api, attachment)\n return {\n id: attachment.id,\n fileName: attachment.fileName,\n fileSize: attachment.fileSize,\n mimeType: attachment.mimeType ?? null,\n downloadUrl: `/api/staff/portal/support-tickets/${encodeURIComponent(taskId)}/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,\n uploadedByCustomer: tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_TAG_PREFIX)),\n canDelete: canManage && tags.includes(customerUploadTag(customerUserId)),\n createdAt: attachment.createdAt ? attachment.createdAt.toISOString() : null,\n }\n}\n\n/**\n * The files of a ticket the customer may see: tagged `customer-visible` and passing\n * the attachments module's own access check. Internal staff files are never returned.\n * `canManage` is whether the caller holds `portal.support_tickets.manage`; without it\n * no item reports `canDelete`, matching the DELETE route's own feature check.\n */\nexport async function listCustomerVisibleAttachments(\n api: PortalAttachmentsApi,\n scope: SupportPortalScope,\n taskId: string,\n canManage = false,\n): Promise<PortalSupportAttachmentItem[]> {\n const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }\n const attachments = await findWithDecryption(\n scope.em,\n api.Attachment,\n { entityId: SUPPORT_TICKET_ATTACHMENT_ENTITY_ID, recordId: taskId, ...decryptionScope },\n { orderBy: { createdAt: 'ASC' } },\n decryptionScope,\n )\n const codes = Array.from(new Set(attachments.map((attachment) => attachment.partitionCode)))\n const partitions = codes.length ? await scope.em.find(api.AttachmentPartition, { code: { $in: codes } }) : []\n const partitionByCode = new Map(partitions.map((partition) => [partition.code, partition]))\n const auth = portalAttachmentAuth(scope)\n return attachments\n .filter((attachment) => {\n const partition = partitionByCode.get(attachment.partitionCode)\n if (!partition) return false\n if (!readAttachmentTags(api, attachment).includes(CUSTOMER_VISIBLE_ATTACHMENT_TAG)) return false\n return api.checkAttachmentAccess(auth, attachment, partition, { requireAuthForPublic: true }).ok\n })\n .map((attachment) => serializePortalAttachment(api, attachment, taskId, scope.auth.sub, canManage))\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,117 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
2
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
4
+ import { StaffSupportTicket, StaffTimeTask, StaffTimeTaskComment, StaffTimeTaskStatus } from "../../data/entities.js";
5
+ import { emitStaffEvent } from "../../events.js";
6
+ import { filterPortalRecipientsByFeatureStrict, resolvePortalRecipientUserIdsStrict } from "../time-tracking/portalRecipients.js";
7
+ import { derivePortalStatus } from "./portalStatus.js";
8
+ import { PORTAL_SUPPORT_TICKETS_VIEW_FEATURE } from "./portalTickets.js";
9
+ const logger = createLogger("staff").child({ component: "lib/support/portalBroadcast" });
10
+ const PORTAL_FANOUT_BATCH_SIZE = 200;
11
+ async function emitPortalUpdate(em, task, ticket, portalStatus, kind, version) {
12
+ if (!task.customerId) return false;
13
+ const customerUserIds = await resolvePortalRecipientUserIdsStrict(em, {
14
+ tenantId: task.tenantId,
15
+ organizationId: task.organizationId,
16
+ customerId: task.customerId
17
+ });
18
+ const recipientUserIds = await filterPortalRecipientsByFeatureStrict(
19
+ em,
20
+ customerUserIds,
21
+ { tenantId: task.tenantId, organizationId: task.organizationId },
22
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE
23
+ );
24
+ if (recipientUserIds.length === 0) return false;
25
+ await emitStaffEvent(
26
+ "staff.support_ticket.portal_updated",
27
+ {
28
+ id: task.id,
29
+ tenantId: task.tenantId,
30
+ organizationId: task.organizationId,
31
+ ticketId: task.id,
32
+ supportReference: ticket.supportReference,
33
+ portalStatus,
34
+ kind,
35
+ version,
36
+ recipientUserIds
37
+ },
38
+ { persistent: true }
39
+ );
40
+ return true;
41
+ }
42
+ function emitActivity(task, ticket, kind) {
43
+ void emitStaffEvent(
44
+ "staff.support_ticket.activity_recorded",
45
+ {
46
+ id: ticket.id,
47
+ tenantId: task.tenantId,
48
+ organizationId: task.organizationId,
49
+ taskId: task.id,
50
+ projectId: task.timeProjectId,
51
+ kind
52
+ },
53
+ { persistent: true }
54
+ ).catch((err) => {
55
+ logger.error("staff support activity emit failed", { err, taskId: task.id });
56
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.support_activity_emit_failed" });
57
+ });
58
+ }
59
+ async function publishTaskPortalStatus(em, taskId, scope, options = {}) {
60
+ const task = await em.findOne(StaffTimeTask, { id: taskId, ...scope, source: "portal", deletedAt: null });
61
+ if (!task) return false;
62
+ const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, void 0, scope);
63
+ if (!ticket) return false;
64
+ const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ["id", "isDefault", "isDone"] });
65
+ const portalStatus = derivePortalStatus(status);
66
+ const unchanged = ticket.portalStatusPublished === portalStatus;
67
+ if (unchanged && !options.always) return false;
68
+ await emitPortalUpdate(em, task, ticket, portalStatus, "status", task.updatedAt ? task.updatedAt.toISOString() : task.id);
69
+ if (!unchanged) {
70
+ ticket.portalStatusPublished = portalStatus;
71
+ ticket.updatedAt = /* @__PURE__ */ new Date();
72
+ await em.flush();
73
+ }
74
+ emitActivity(task, ticket, "portal_status");
75
+ return true;
76
+ }
77
+ async function fanOutStatusColumn(em, taskStatusId, scope) {
78
+ let published = 0;
79
+ let offset = 0;
80
+ for (; ; ) {
81
+ const tasks = await em.find(
82
+ StaffTimeTask,
83
+ { taskStatusId, ...scope, source: "portal", deletedAt: null },
84
+ { fields: ["id"], limit: PORTAL_FANOUT_BATCH_SIZE, offset, orderBy: { id: "asc" } }
85
+ );
86
+ if (tasks.length === 0) break;
87
+ for (const task of tasks) {
88
+ if (await publishTaskPortalStatus(em.fork(), task.id, scope)) published += 1;
89
+ }
90
+ if (tasks.length < PORTAL_FANOUT_BATCH_SIZE) break;
91
+ offset += PORTAL_FANOUT_BATCH_SIZE;
92
+ }
93
+ return published;
94
+ }
95
+ async function publishTicketReply(em, commentId, scope) {
96
+ const comment = await em.findOne(StaffTimeTaskComment, { id: commentId, ...scope }, { fields: ["id", "taskId", "updatedAt"] });
97
+ if (!comment) return false;
98
+ const task = await em.findOne(StaffTimeTask, { id: comment.taskId, ...scope, source: "portal", deletedAt: null });
99
+ if (!task) return false;
100
+ const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, void 0, scope);
101
+ if (!ticket) return false;
102
+ const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ["id", "isDefault", "isDone"] });
103
+ return emitPortalUpdate(em, task, ticket, derivePortalStatus(status), "reply", `${comment.id}:${comment.updatedAt ? comment.updatedAt.toISOString() : ""}`);
104
+ }
105
+ function readEventScope(payload) {
106
+ const tenantId = typeof payload.tenantId === "string" ? payload.tenantId : null;
107
+ const organizationId = typeof payload.organizationId === "string" ? payload.organizationId : null;
108
+ return tenantId && organizationId ? { tenantId, organizationId } : null;
109
+ }
110
+ export {
111
+ PORTAL_FANOUT_BATCH_SIZE,
112
+ fanOutStatusColumn,
113
+ publishTaskPortalStatus,
114
+ publishTicketReply,
115
+ readEventScope
116
+ };
117
+ //# sourceMappingURL=portalBroadcast.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/support/portalBroadcast.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { StaffSupportTicket, StaffTimeTask, StaffTimeTaskComment, StaffTimeTaskStatus } from '../../data/entities'\nimport { emitStaffEvent } from '../../events'\nimport { filterPortalRecipientsByFeatureStrict, resolvePortalRecipientUserIdsStrict } from '../time-tracking/portalRecipients'\nimport { derivePortalStatus } from './portalStatus'\nimport { PORTAL_SUPPORT_TICKETS_VIEW_FEATURE } from './portalTickets'\n\nconst logger = createLogger('staff').child({ component: 'lib/support/portalBroadcast' })\n\nexport const PORTAL_FANOUT_BATCH_SIZE = 200\n\ntype Scope = { tenantId: string; organizationId: string }\n\nexport type PortalUpdateKind = 'status' | 'reply'\n\n/**\n * `version` names the change being announced (the task version for a status change,\n * the comment for a reply). The portal event bridge drops an event whose payload\n * repeats one seen in the last 500 ms, so two different changes must never produce\n * identical payloads, while two publishers of the same change still collapse.\n *\n * Resolves `true` when the update was emitted and `false` when there is genuinely no\n * one to tell (no customer, no portal users, nobody holding the view feature, or the\n * `customer_accounts` module absent). Any other recipient-lookup failure rejects, so a\n * caller never records a publication that did not happen.\n */\nasync function emitPortalUpdate(\n em: EntityManager,\n task: StaffTimeTask,\n ticket: StaffSupportTicket,\n portalStatus: string,\n kind: PortalUpdateKind,\n version: string,\n): Promise<boolean> {\n if (!task.customerId) return false\n const customerUserIds = await resolvePortalRecipientUserIdsStrict(em, {\n tenantId: task.tenantId,\n organizationId: task.organizationId,\n customerId: task.customerId,\n })\n const recipientUserIds = await filterPortalRecipientsByFeatureStrict(\n em,\n customerUserIds,\n { tenantId: task.tenantId, organizationId: task.organizationId },\n PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,\n )\n if (recipientUserIds.length === 0) return false\n await emitStaffEvent(\n 'staff.support_ticket.portal_updated',\n {\n id: task.id,\n tenantId: task.tenantId,\n organizationId: task.organizationId,\n ticketId: task.id,\n supportReference: ticket.supportReference,\n portalStatus,\n kind,\n version,\n recipientUserIds,\n },\n { persistent: true },\n )\n return true\n}\n\nfunction emitActivity(task: StaffTimeTask, ticket: StaffSupportTicket, kind: 'portal_status'): void {\n void emitStaffEvent(\n 'staff.support_ticket.activity_recorded',\n {\n id: ticket.id,\n tenantId: task.tenantId,\n organizationId: task.organizationId,\n taskId: task.id,\n projectId: task.timeProjectId,\n kind,\n },\n { persistent: true },\n ).catch((err) => {\n logger.error('staff support activity emit failed', { err, taskId: task.id })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_activity_emit_failed' })\n })\n}\n\n/**\n * Compare-and-publish: re-derive the customer-facing status of a portal ticket and\n * broadcast only when it differs from what the portal was last told. Safe to call\n * on any task event; internal tasks and tasks without a ticket row are ignored.\n *\n * `always` skips the comparison for triggers that are a status change by definition.\n * Concurrent publishers (a reopen followed quickly by a resolve) each run on their own\n * fork, so both can read a stale `portalStatusPublished` and skip; the portal refetches\n * the ticket on every event, so publishing after each status change keeps it correct.\n *\n * `portalStatusPublished` advances only after `emitPortalUpdate` settles: an emitted\n * update or a genuinely empty audience. A recipient-lookup or emit failure propagates\n * with the marker untouched, so the persistent fan-out retries and republishes.\n */\nexport async function publishTaskPortalStatus(\n em: EntityManager,\n taskId: string,\n scope: Scope,\n options: { always?: boolean } = {},\n): Promise<boolean> {\n const task = await em.findOne(StaffTimeTask, { id: taskId, ...scope, source: 'portal', deletedAt: null })\n if (!task) return false\n const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)\n if (!ticket) return false\n const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ['id', 'isDefault', 'isDone'] })\n const portalStatus = derivePortalStatus(status)\n const unchanged = ticket.portalStatusPublished === portalStatus\n if (unchanged && !options.always) return false\n await emitPortalUpdate(em, task, ticket, portalStatus, 'status', task.updatedAt ? task.updatedAt.toISOString() : task.id)\n if (!unchanged) {\n ticket.portalStatusPublished = portalStatus\n ticket.updatedAt = new Date()\n await em.flush()\n }\n emitActivity(task, ticket, 'portal_status')\n return true\n}\n\n/** A status column's `isDone`/`isDefault` flags changed: re-derive every live portal ticket in it, in batches. */\nexport async function fanOutStatusColumn(em: EntityManager, taskStatusId: string, scope: Scope): Promise<number> {\n let published = 0\n let offset = 0\n for (;;) {\n const tasks = await em.find(\n StaffTimeTask,\n { taskStatusId, ...scope, source: 'portal', deletedAt: null },\n { fields: ['id'], limit: PORTAL_FANOUT_BATCH_SIZE, offset, orderBy: { id: 'asc' } },\n )\n if (tasks.length === 0) break\n for (const task of tasks) {\n if (await publishTaskPortalStatus(em.fork(), task.id, scope)) published += 1\n }\n if (tasks.length < PORTAL_FANOUT_BATCH_SIZE) break\n offset += PORTAL_FANOUT_BATCH_SIZE\n }\n return published\n}\n\n/** A customer-visible comment was published, retracted or deleted: tell the ticket's own customer users to refetch. */\nexport async function publishTicketReply(em: EntityManager, commentId: string, scope: Scope): Promise<boolean> {\n const comment = await em.findOne(StaffTimeTaskComment, { id: commentId, ...scope }, { fields: ['id', 'taskId', 'updatedAt'] })\n if (!comment) return false\n const task = await em.findOne(StaffTimeTask, { id: comment.taskId, ...scope, source: 'portal', deletedAt: null })\n if (!task) return false\n const ticket = await findOneWithDecryption(em, StaffSupportTicket, { taskId: task.id, ...scope, deletedAt: null }, undefined, scope)\n if (!ticket) return false\n const status = await em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...scope }, { fields: ['id', 'isDefault', 'isDone'] })\n return emitPortalUpdate(em, task, ticket, derivePortalStatus(status), 'reply', `${comment.id}:${comment.updatedAt ? comment.updatedAt.toISOString() : ''}`)\n}\n\nexport function readEventScope(payload: { tenantId?: unknown; organizationId?: unknown }): Scope | null {\n const tenantId = typeof payload.tenantId === 'string' ? payload.tenantId : null\n const organizationId = typeof payload.organizationId === 'string' ? payload.organizationId : null\n return tenantId && organizationId ? { tenantId, organizationId } : null\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB,eAAe,sBAAsB,2BAA2B;AAC7F,SAAS,sBAAsB;AAC/B,SAAS,uCAAuC,2CAA2C;AAC3F,SAAS,0BAA0B;AACnC,SAAS,2CAA2C;AAEpD,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,8BAA8B,CAAC;AAEhF,MAAM,2BAA2B;AAiBxC,eAAe,iBACb,IACA,MACA,QACA,cACA,MACA,SACkB;AAClB,MAAI,CAAC,KAAK,WAAY,QAAO;AAC7B,QAAM,kBAAkB,MAAM,oCAAoC,IAAI;AAAA,IACpE,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,IACrB,YAAY,KAAK;AAAA,EACnB,CAAC;AACD,QAAM,mBAAmB,MAAM;AAAA,IAC7B;AAAA,IACA;AAAA,IACA,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,eAAe;AAAA,IAC/D;AAAA,EACF;AACA,MAAI,iBAAiB,WAAW,EAAG,QAAO;AAC1C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,IAAI,KAAK;AAAA,MACT,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,kBAAkB,OAAO;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,EAAE,YAAY,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAAqB,QAA4B,MAA6B;AAClG,OAAK;AAAA,IACH;AAAA,IACA;AAAA,MACE,IAAI,OAAO;AAAA,MACX,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK;AAAA,MAChB;AAAA,IACF;AAAA,IACA,EAAE,YAAY,KAAK;AAAA,EACrB,EAAE,MAAM,CAAC,QAAQ;AACf,WAAO,MAAM,sCAAsC,EAAE,KAAK,QAAQ,KAAK,GAAG,CAAC;AAC3E,wBAAoB,GAAG,YAAY,KAAK,EAAE,QAAQ,SAAS,MAAM,qCAAqC,CAAC;AAAA,EACzG,CAAC;AACH;AAgBA,eAAsB,wBACpB,IACA,QACA,OACA,UAAgC,CAAC,GACf;AAClB,QAAM,OAAO,MAAM,GAAG,QAAQ,eAAe,EAAE,IAAI,QAAQ,GAAG,OAAO,QAAQ,UAAU,WAAW,KAAK,CAAC;AACxG,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,MAAM,sBAAsB,IAAI,oBAAoB,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,WAAW,KAAK,GAAG,QAAW,KAAK;AACnI,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,MAAM,GAAG,QAAQ,qBAAqB,EAAE,IAAI,KAAK,cAAc,GAAG,MAAM,GAAG,EAAE,QAAQ,CAAC,MAAM,aAAa,QAAQ,EAAE,CAAC;AACnI,QAAM,eAAe,mBAAmB,MAAM;AAC9C,QAAM,YAAY,OAAO,0BAA0B;AACnD,MAAI,aAAa,CAAC,QAAQ,OAAQ,QAAO;AACzC,QAAM,iBAAiB,IAAI,MAAM,QAAQ,cAAc,UAAU,KAAK,YAAY,KAAK,UAAU,YAAY,IAAI,KAAK,EAAE;AACxH,MAAI,CAAC,WAAW;AACd,WAAO,wBAAwB;AAC/B,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,GAAG,MAAM;AAAA,EACjB;AACA,eAAa,MAAM,QAAQ,eAAe;AAC1C,SAAO;AACT;AAGA,eAAsB,mBAAmB,IAAmB,cAAsB,OAA+B;AAC/G,MAAI,YAAY;AAChB,MAAI,SAAS;AACb,aAAS;AACP,UAAM,QAAQ,MAAM,GAAG;AAAA,MACrB;AAAA,MACA,EAAE,cAAc,GAAG,OAAO,QAAQ,UAAU,WAAW,KAAK;AAAA,MAC5D,EAAE,QAAQ,CAAC,IAAI,GAAG,OAAO,0BAA0B,QAAQ,SAAS,EAAE,IAAI,MAAM,EAAE;AAAA,IACpF;AACA,QAAI,MAAM,WAAW,EAAG;AACxB,eAAW,QAAQ,OAAO;AACxB,UAAI,MAAM,wBAAwB,GAAG,KAAK,GAAG,KAAK,IAAI,KAAK,EAAG,cAAa;AAAA,IAC7E;AACA,QAAI,MAAM,SAAS,yBAA0B;AAC7C,cAAU;AAAA,EACZ;AACA,SAAO;AACT;AAGA,eAAsB,mBAAmB,IAAmB,WAAmB,OAAgC;AAC7G,QAAM,UAAU,MAAM,GAAG,QAAQ,sBAAsB,EAAE,IAAI,WAAW,GAAG,MAAM,GAAG,EAAE,QAAQ,CAAC,MAAM,UAAU,WAAW,EAAE,CAAC;AAC7H,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,MAAM,GAAG,QAAQ,eAAe,EAAE,IAAI,QAAQ,QAAQ,GAAG,OAAO,QAAQ,UAAU,WAAW,KAAK,CAAC;AAChH,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,MAAM,sBAAsB,IAAI,oBAAoB,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,WAAW,KAAK,GAAG,QAAW,KAAK;AACnI,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,MAAM,GAAG,QAAQ,qBAAqB,EAAE,IAAI,KAAK,cAAc,GAAG,MAAM,GAAG,EAAE,QAAQ,CAAC,MAAM,aAAa,QAAQ,EAAE,CAAC;AACnI,SAAO,iBAAiB,IAAI,MAAM,QAAQ,mBAAmB,MAAM,GAAG,SAAS,GAAG,QAAQ,EAAE,IAAI,QAAQ,YAAY,QAAQ,UAAU,YAAY,IAAI,EAAE,EAAE;AAC5J;AAEO,SAAS,eAAe,SAAyE;AACtG,QAAM,WAAW,OAAO,QAAQ,aAAa,WAAW,QAAQ,WAAW;AAC3E,QAAM,iBAAiB,OAAO,QAAQ,mBAAmB,WAAW,QAAQ,iBAAiB;AAC7F,SAAO,YAAY,iBAAiB,EAAE,UAAU,eAAe,IAAI;AACrE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,99 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
4
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
5
+ import { createLogger } from "@open-mercato/shared/lib/logger";
6
+ import { checkRateLimit, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from "@open-mercato/shared/lib/ratelimit/helpers";
7
+ import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
8
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
9
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
10
+ const logger = createLogger("staff").child({ component: "lib/support/portalRoute" });
11
+ const SUPPORT_TICKET_RESOURCE_KIND = "staff.support_ticket";
12
+ const RATE_LIMITS = {
13
+ create: readEndpointRateLimitConfig("STAFF_SUPPORT_TICKET_CREATE", {
14
+ points: 10,
15
+ duration: 3600,
16
+ keyPrefix: "staff_support_ticket_create"
17
+ }),
18
+ message: readEndpointRateLimitConfig("STAFF_SUPPORT_TICKET_MESSAGE", {
19
+ points: 60,
20
+ duration: 3600,
21
+ keyPrefix: "staff_support_ticket_message"
22
+ }),
23
+ upload: readEndpointRateLimitConfig("STAFF_SUPPORT_TICKET_UPLOAD", {
24
+ points: 30,
25
+ duration: 3600,
26
+ keyPrefix: "staff_support_ticket_upload"
27
+ })
28
+ };
29
+ function buildPortalCommandContext(scope, req) {
30
+ const auth = {
31
+ sub: scope.auth.sub,
32
+ sid: scope.auth.sid,
33
+ tenantId: scope.auth.tenantId,
34
+ orgId: scope.auth.orgId,
35
+ email: scope.auth.email,
36
+ customerEntityId: scope.auth.customerEntityId ?? null,
37
+ personEntityId: scope.auth.personEntityId ?? null
38
+ };
39
+ return {
40
+ container: scope.container,
41
+ auth,
42
+ organizationScope: null,
43
+ selectedOrganizationId: scope.organizationId,
44
+ organizationIds: [scope.organizationId],
45
+ request: req
46
+ };
47
+ }
48
+ async function checkPortalRateLimit(scope, kind) {
49
+ let service = null;
50
+ try {
51
+ service = scope.container.resolve("rateLimiterService");
52
+ } catch {
53
+ return null;
54
+ }
55
+ if (!service) return null;
56
+ const { translate } = await resolveTranslations();
57
+ return checkRateLimit(
58
+ service,
59
+ RATE_LIMITS[kind],
60
+ `${scope.tenantId}:${scope.auth.sub}`,
61
+ translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK)
62
+ );
63
+ }
64
+ async function portalErrorResponse(err, label) {
65
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
66
+ const rejection = getCommandInterceptorHttpRejection(err);
67
+ if (rejection) return NextResponse.json(rejection.body, { status: rejection.status });
68
+ if (err instanceof z.ZodError) {
69
+ const { translate } = await resolveTranslations();
70
+ return NextResponse.json(
71
+ { ok: false, error: translate("staff.errors.invalid_request", "Invalid request"), details: err.issues },
72
+ { status: 400 }
73
+ );
74
+ }
75
+ logger.error(label, { err });
76
+ getTelemetryRuntime()?.reportError(err, {
77
+ module: "staff",
78
+ code: "staff.portal_support_route_failed",
79
+ attributes: { operation: label }
80
+ });
81
+ return NextResponse.json({ ok: false, error: "staff.errors.internal" }, { status: 500 });
82
+ }
83
+ function readPathId(req, pattern) {
84
+ try {
85
+ const match = new URL(req.url).pathname.match(pattern);
86
+ const id = match?.[1] ? decodeURIComponent(match[1]) : null;
87
+ return id && z.string().uuid().safeParse(id).success ? id : null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ export {
93
+ SUPPORT_TICKET_RESOURCE_KIND,
94
+ buildPortalCommandContext,
95
+ checkPortalRateLimit,
96
+ portalErrorResponse,
97
+ readPathId
98
+ };
99
+ //# sourceMappingURL=portalRoute.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/support/portalRoute.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { checkRateLimit, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { SupportPortalScope } from './portalTickets'\n\nconst logger = createLogger('staff').child({ component: 'lib/support/portalRoute' })\n\nexport const SUPPORT_TICKET_RESOURCE_KIND = 'staff.support_ticket'\n\nconst RATE_LIMITS = {\n create: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_CREATE', {\n points: 10,\n duration: 3600,\n keyPrefix: 'staff_support_ticket_create',\n }),\n message: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_MESSAGE', {\n points: 60,\n duration: 3600,\n keyPrefix: 'staff_support_ticket_message',\n }),\n upload: readEndpointRateLimitConfig('STAFF_SUPPORT_TICKET_UPLOAD', {\n points: 30,\n duration: 3600,\n keyPrefix: 'staff_support_ticket_upload',\n }),\n} as const\n\nexport type PortalRateLimitKind = keyof typeof RATE_LIMITS\n\nexport function buildPortalCommandContext(scope: SupportPortalScope, req: Request): CommandRuntimeContext {\n const auth: NonNullable<AuthContext> = {\n sub: scope.auth.sub,\n sid: scope.auth.sid,\n tenantId: scope.auth.tenantId,\n orgId: scope.auth.orgId,\n email: scope.auth.email,\n customerEntityId: scope.auth.customerEntityId ?? null,\n personEntityId: scope.auth.personEntityId ?? null,\n }\n return {\n container: scope.container,\n auth,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n request: req,\n }\n}\n\n/** Per customer user, so one noisy account cannot exhaust its colleagues' budget. */\nexport async function checkPortalRateLimit(\n scope: SupportPortalScope,\n kind: PortalRateLimitKind,\n): Promise<Response | null> {\n let service: RateLimiterService | null = null\n try {\n service = scope.container.resolve('rateLimiterService') as RateLimiterService\n } catch {\n return null\n }\n if (!service) return null\n const { translate } = await resolveTranslations()\n return checkRateLimit(\n service,\n RATE_LIMITS[kind],\n `${scope.tenantId}:${scope.auth.sub}`,\n translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),\n )\n}\n\nexport async function portalErrorResponse(err: unknown, label: string): Promise<Response> {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const rejection = getCommandInterceptorHttpRejection(err)\n if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })\n if (err instanceof z.ZodError) {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n { ok: false, error: translate('staff.errors.invalid_request', 'Invalid request'), details: err.issues },\n { status: 400 },\n )\n }\n logger.error(label, { err })\n getTelemetryRuntime()?.reportError(err, {\n module: 'staff',\n code: 'staff.portal_support_route_failed',\n attributes: { operation: label },\n })\n return NextResponse.json({ ok: false, error: 'staff.errors.internal' }, { status: 500 })\n}\n\nexport function readPathId(req: Request, pattern: RegExp): string | null {\n try {\n const match = new URL(req.url).pathname.match(pattern)\n const id = match?.[1] ? decodeURIComponent(match[1]) : null\n return id && z.string().uuid().safeParse(id).success ? id : null\n } catch {\n return null\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAGlB,SAAS,0CAA0C;AACnD,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,2BAA2B,4BAA4B;AAChF,SAAS,mCAAmC;AAE5C,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAGpC,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,0BAA0B,CAAC;AAE5E,MAAM,+BAA+B;AAE5C,MAAM,cAAc;AAAA,EAClB,QAAQ,4BAA4B,+BAA+B;AAAA,IACjE,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,EACb,CAAC;AAAA,EACD,SAAS,4BAA4B,gCAAgC;AAAA,IACnE,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,EACb,CAAC;AAAA,EACD,QAAQ,4BAA4B,+BAA+B;AAAA,IACjE,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,WAAW;AAAA,EACb,CAAC;AACH;AAIO,SAAS,0BAA0B,OAA2B,KAAqC;AACxG,QAAM,OAAiC;AAAA,IACrC,KAAK,MAAM,KAAK;AAAA,IAChB,KAAK,MAAM,KAAK;AAAA,IAChB,UAAU,MAAM,KAAK;AAAA,IACrB,OAAO,MAAM,KAAK;AAAA,IAClB,OAAO,MAAM,KAAK;AAAA,IAClB,kBAAkB,MAAM,KAAK,oBAAoB;AAAA,IACjD,gBAAgB,MAAM,KAAK,kBAAkB;AAAA,EAC/C;AACA,SAAO;AAAA,IACL,WAAW,MAAM;AAAA,IACjB;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB,MAAM;AAAA,IAC9B,iBAAiB,CAAC,MAAM,cAAc;AAAA,IACtC,SAAS;AAAA,EACX;AACF;AAGA,eAAsB,qBACpB,OACA,MAC0B;AAC1B,MAAI,UAAqC;AACzC,MAAI;AACF,cAAU,MAAM,UAAU,QAAQ,oBAAoB;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,SAAO;AAAA,IACL;AAAA,IACA,YAAY,IAAI;AAAA,IAChB,GAAG,MAAM,QAAQ,IAAI,MAAM,KAAK,GAAG;AAAA,IACnC,UAAU,sBAAsB,yBAAyB;AAAA,EAC3D;AACF;AAEA,eAAsB,oBAAoB,KAAc,OAAkC;AACxF,MAAI,gBAAgB,GAAG,EAAG,QAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AACnF,QAAM,YAAY,mCAAmC,GAAG;AACxD,MAAI,UAAW,QAAO,aAAa,KAAK,UAAU,MAAM,EAAE,QAAQ,UAAU,OAAO,CAAC;AACpF,MAAI,eAAe,EAAE,UAAU;AAC7B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO,aAAa;AAAA,MAClB,EAAE,IAAI,OAAO,OAAO,UAAU,gCAAgC,iBAAiB,GAAG,SAAS,IAAI,OAAO;AAAA,MACtG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO,MAAM,OAAO,EAAE,IAAI,CAAC;AAC3B,sBAAoB,GAAG,YAAY,KAAK;AAAA,IACtC,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,YAAY,EAAE,WAAW,MAAM;AAAA,EACjC,CAAC;AACD,SAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AACzF;AAEO,SAAS,WAAW,KAAc,SAAgC;AACvE,MAAI;AACF,UAAM,QAAQ,IAAI,IAAI,IAAI,GAAG,EAAE,SAAS,MAAM,OAAO;AACrD,UAAM,KAAK,QAAQ,CAAC,IAAI,mBAAmB,MAAM,CAAC,CAAC,IAAI;AACvD,WAAO,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,UAAU,KAAK;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ function derivePortalStatus(status) {
2
+ if (!status) return "received";
3
+ if (status.isDone) return "resolved";
4
+ if (status.isDefault) return "received";
5
+ return "in_progress";
6
+ }
7
+ const SUPPORT_PORTAL_STATUSES = ["received", "in_progress", "resolved"];
8
+ export {
9
+ SUPPORT_PORTAL_STATUSES,
10
+ derivePortalStatus
11
+ };
12
+ //# sourceMappingURL=portalStatus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/support/portalStatus.ts"],
4
+ "sourcesContent": ["import type { StaffSupportPortalStatus } from '../../data/entities'\n\nexport type PortalStatusFlags = {\n isDefault: boolean\n isDone: boolean\n}\n\nexport function derivePortalStatus(status: PortalStatusFlags | null | undefined): StaffSupportPortalStatus {\n if (!status) return 'received'\n if (status.isDone) return 'resolved'\n if (status.isDefault) return 'received'\n return 'in_progress'\n}\n\nexport const SUPPORT_PORTAL_STATUSES: readonly StaffSupportPortalStatus[] = ['received', 'in_progress', 'resolved']\n"],
5
+ "mappings": "AAOO,SAAS,mBAAmB,QAAwE;AACzG,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,OAAQ,QAAO;AAC1B,MAAI,OAAO,UAAW,QAAO;AAC7B,SAAO;AACT;AAEO,MAAM,0BAA+D,CAAC,YAAY,eAAe,UAAU;",
6
+ "names": []
7
+ }
@@ -0,0 +1,148 @@
1
+ import { NextResponse } from "next/server";
2
+ import { sql } from "kysely";
3
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
+ import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { CustomerEntity } from "@open-mercato/core/modules/customers/data/entities";
7
+ import { StaffSupportTicket, StaffTimeTask } from "../../data/entities.js";
8
+ import { PORTAL_SUPPORT_NOT_LINKED_CODE } from "../support-ui/portalSupport.js";
9
+ const logger = createLogger("staff").child({ component: "lib/support/portalTickets" });
10
+ const PORTAL_SUPPORT_TICKETS_VIEW_FEATURE = "portal.support_tickets.view";
11
+ const PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE = "portal.support_tickets.manage";
12
+ async function resolveSupportPortalScope(req, requiredFeature) {
13
+ let auth = null;
14
+ let requireFeature = null;
15
+ try {
16
+ const customerAuth = await import("@open-mercato/core/modules/customer_accounts/lib/customerAuth");
17
+ auth = await customerAuth.getCustomerAuthFromRequest(req);
18
+ const guard2 = customerAuth.requireCustomerFeature;
19
+ requireFeature = typeof guard2 === "function" ? guard2 : null;
20
+ } catch (err) {
21
+ logger.warn("staff portal support could not resolve customer auth", { err });
22
+ return NextResponse.json({ ok: false, error: "staff.errors.unauthorized" }, { status: 401 });
23
+ }
24
+ if (!auth) return NextResponse.json({ ok: false, error: "staff.errors.unauthorized" }, { status: 401 });
25
+ const customerId = typeof auth.customerEntityId === "string" && auth.customerEntityId.trim().length > 0 ? auth.customerEntityId.trim() : null;
26
+ if (!customerId) {
27
+ return NextResponse.json(
28
+ { ok: false, error: "staff.errors.customerAccountNotLinked", code: PORTAL_SUPPORT_NOT_LINKED_CODE },
29
+ { status: 403 }
30
+ );
31
+ }
32
+ if (!requireFeature) {
33
+ logger.error("staff portal support could not resolve requireCustomerFeature; refusing the request", {
34
+ feature: requiredFeature
35
+ });
36
+ return NextResponse.json({ ok: false, error: "staff.errors.forbidden" }, { status: 403 });
37
+ }
38
+ const guard = requireFeature;
39
+ const container = await createRequestContainer();
40
+ const resolvedAuth = auth;
41
+ const hasFeature = async (feature) => {
42
+ try {
43
+ await guard(resolvedAuth, [feature], container.resolve("customerRbacService"));
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ };
49
+ try {
50
+ await guard(auth, [requiredFeature], container.resolve("customerRbacService"));
51
+ } catch (response) {
52
+ if (response instanceof Response) return response;
53
+ logger.warn("staff portal support feature check failed", { err: response });
54
+ return NextResponse.json({ ok: false, error: "staff.errors.forbidden" }, { status: 403 });
55
+ }
56
+ return {
57
+ auth,
58
+ customerId,
59
+ tenantId: auth.tenantId,
60
+ organizationId: auth.orgId,
61
+ container,
62
+ em: container.resolve("em").fork(),
63
+ hasFeature
64
+ };
65
+ }
66
+ function portalOwnedTaskWhere(scope) {
67
+ return {
68
+ tenantId: scope.tenantId,
69
+ organizationId: scope.organizationId,
70
+ customerId: scope.customerId,
71
+ source: "portal",
72
+ deletedAt: null
73
+ };
74
+ }
75
+ async function loadPortalOwnedTicket(scope, taskId) {
76
+ const task = await scope.em.findOne(StaffTimeTask, { id: taskId, ...portalOwnedTaskWhere(scope) });
77
+ if (!task) return null;
78
+ const ticket = await findOneWithDecryption(
79
+ scope.em,
80
+ StaffSupportTicket,
81
+ { taskId: task.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
82
+ void 0,
83
+ { tenantId: scope.tenantId, organizationId: scope.organizationId }
84
+ );
85
+ if (!ticket) return null;
86
+ return { task, ticket };
87
+ }
88
+ async function resolveCustomerDisplay(scope) {
89
+ try {
90
+ const customer = await findOneWithDecryption(
91
+ scope.em,
92
+ CustomerEntity,
93
+ { id: scope.customerId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
94
+ void 0,
95
+ { tenantId: scope.tenantId, organizationId: scope.organizationId }
96
+ );
97
+ const displayName = typeof customer?.displayName === "string" ? customer.displayName : null;
98
+ const kind = typeof customer?.kind === "string" ? customer.kind : null;
99
+ return { displayName, kind };
100
+ } catch {
101
+ return { displayName: null, kind: null };
102
+ }
103
+ }
104
+ function toDate(value, fallback) {
105
+ if (value instanceof Date) return value;
106
+ if (typeof value === "string" || typeof value === "number") {
107
+ const parsed = new Date(value);
108
+ if (!Number.isNaN(parsed.getTime())) return parsed;
109
+ }
110
+ return fallback;
111
+ }
112
+ async function listPortalTicketPage(scope, options) {
113
+ if (options.statusIds && options.statusIds.length === 0) return { rows: [], total: 0 };
114
+ const db = scope.em.getKysely();
115
+ let owned = db.selectFrom("staff_time_tasks as task").where("task.tenant_id", "=", scope.tenantId).where("task.organization_id", "=", scope.organizationId).where("task.customer_id", "=", scope.customerId).where("task.source", "=", "portal").where("task.deleted_at", "is", null);
116
+ if (options.statusIds) owned = owned.where("task.task_status_id", "in", [...options.statusIds]);
117
+ const lastActivity = sql`greatest(task.created_at, coalesce((
118
+ select max(visible_comment.created_at)
119
+ from staff_time_task_comments as visible_comment
120
+ where visible_comment.organization_id = task.organization_id
121
+ and visible_comment.task_id = task.id
122
+ and visible_comment.tenant_id = task.tenant_id
123
+ and visible_comment.visibility = 'customer'
124
+ and visible_comment.deleted_at is null
125
+ ), task.created_at))`;
126
+ const [countRow, pageRows] = await Promise.all([
127
+ owned.select((builder) => builder.fn.countAll().as("total")).executeTakeFirst(),
128
+ owned.select(["task.id as id", "task.created_at as created_at"]).select(lastActivity.as("last_activity_at")).orderBy("last_activity_at", "desc").orderBy("task.id", "desc").limit(options.limit).offset(options.offset).execute()
129
+ ]);
130
+ const total = Number(countRow?.total ?? 0);
131
+ return {
132
+ total: Number.isFinite(total) ? total : 0,
133
+ rows: pageRows.map((row) => ({
134
+ id: row.id,
135
+ lastActivityAt: toDate(row.last_activity_at, toDate(row.created_at, /* @__PURE__ */ new Date(0)))
136
+ }))
137
+ };
138
+ }
139
+ export {
140
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
141
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
142
+ listPortalTicketPage,
143
+ loadPortalOwnedTicket,
144
+ portalOwnedTaskWhere,
145
+ resolveCustomerDisplay,
146
+ resolveSupportPortalScope
147
+ };
148
+ //# sourceMappingURL=portalTickets.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/support/portalTickets.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { sql } from 'kysely'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CustomerAuthContext } from '@open-mercato/shared/modules/customer-auth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'\nimport { StaffSupportTicket, StaffTimeTask } from '../../data/entities'\nimport { PORTAL_SUPPORT_NOT_LINKED_CODE } from '../support-ui/portalSupport'\n\nconst logger = createLogger('staff').child({ component: 'lib/support/portalTickets' })\n\nexport const PORTAL_SUPPORT_TICKETS_VIEW_FEATURE = 'portal.support_tickets.view'\nexport const PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE = 'portal.support_tickets.manage'\n\ntype AppContainer = Awaited<ReturnType<typeof createRequestContainer>>\n\nexport type SupportPortalScope = {\n auth: CustomerAuthContext\n customerId: string\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n hasFeature: (feature: string) => Promise<boolean>\n}\n\ntype CustomerFeatureGuard = (auth: CustomerAuthContext, features: string[], rbac: unknown) => Promise<void>\n\n/**\n * The portal identity is resolved through `customer_accounts` with a dynamic import,\n * exactly like the EP-50 time-reports routes, so `staff` keeps no static edge to the\n * portal module: a deployment without it answers 401 instead of failing to load.\n * A session with no `customerEntityId` is refused (403); a missing feature guard\n * fails closed (403) so a revoked grant keeps working as a revocation.\n */\nexport async function resolveSupportPortalScope(\n req: Request,\n requiredFeature: string,\n): Promise<SupportPortalScope | Response> {\n let auth: CustomerAuthContext | null = null\n let requireFeature: CustomerFeatureGuard | null = null\n try {\n const customerAuth = await import('@open-mercato/core/modules/customer_accounts/lib/customerAuth')\n auth = await customerAuth.getCustomerAuthFromRequest(req)\n const guard: unknown = (customerAuth as { requireCustomerFeature?: unknown }).requireCustomerFeature\n requireFeature = typeof guard === 'function' ? (guard as CustomerFeatureGuard) : null\n } catch (err) {\n logger.warn('staff portal support could not resolve customer auth', { err })\n return NextResponse.json({ ok: false, error: 'staff.errors.unauthorized' }, { status: 401 })\n }\n if (!auth) return NextResponse.json({ ok: false, error: 'staff.errors.unauthorized' }, { status: 401 })\n const customerId =\n typeof auth.customerEntityId === 'string' && auth.customerEntityId.trim().length > 0\n ? auth.customerEntityId.trim()\n : null\n if (!customerId) {\n return NextResponse.json(\n { ok: false, error: 'staff.errors.customerAccountNotLinked', code: PORTAL_SUPPORT_NOT_LINKED_CODE },\n { status: 403 },\n )\n }\n if (!requireFeature) {\n logger.error('staff portal support could not resolve requireCustomerFeature; refusing the request', {\n feature: requiredFeature,\n })\n return NextResponse.json({ ok: false, error: 'staff.errors.forbidden' }, { status: 403 })\n }\n const guard = requireFeature\n const container = await createRequestContainer()\n const resolvedAuth = auth\n const hasFeature = async (feature: string): Promise<boolean> => {\n try {\n await guard(resolvedAuth, [feature], container.resolve('customerRbacService'))\n return true\n } catch {\n return false\n }\n }\n try {\n await guard(auth, [requiredFeature], container.resolve('customerRbacService'))\n } catch (response) {\n if (response instanceof Response) return response\n logger.warn('staff portal support feature check failed', { err: response })\n return NextResponse.json({ ok: false, error: 'staff.errors.forbidden' }, { status: 403 })\n }\n return {\n auth,\n customerId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n container,\n em: (container.resolve('em') as EntityManager).fork(),\n hasFeature,\n }\n}\n\n/**\n * THE ownership clause for every portal support read and write. Ownership comes from\n * the task's own `customer_id` \u2014 never the project's \u2014 so tickets from many customers\n * can share one project. Callers load WITH this clause rather than loading and\n * checking afterwards, so a foreign id is never read even momentarily.\n */\nexport function portalOwnedTaskWhere(scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId'>) {\n return {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n customerId: scope.customerId,\n source: 'portal' as const,\n deletedAt: null,\n }\n}\n\nexport async function loadPortalOwnedTicket(\n scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,\n taskId: string,\n): Promise<{ task: StaffTimeTask; ticket: StaffSupportTicket } | null> {\n const task = await scope.em.findOne(StaffTimeTask, { id: taskId, ...portalOwnedTaskWhere(scope) })\n if (!task) return null\n const ticket = await findOneWithDecryption(\n scope.em,\n StaffSupportTicket,\n { taskId: task.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!ticket) return null\n return { task, ticket }\n}\n\nexport async function resolveCustomerDisplay(\n scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,\n): Promise<{ displayName: string | null; kind: string | null }> {\n try {\n const customer = await findOneWithDecryption(\n scope.em,\n CustomerEntity,\n { id: scope.customerId, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n const displayName = typeof customer?.displayName === 'string' ? customer.displayName : null\n const kind = typeof customer?.kind === 'string' ? customer.kind : null\n return { displayName, kind }\n } catch {\n return { displayName: null, kind: null }\n }\n}\n\ntype PortalTicketActivityDb = {\n staff_time_tasks: {\n id: string\n tenant_id: string\n organization_id: string\n customer_id: string | null\n source: string\n task_status_id: string\n created_at: Date\n deleted_at: Date | null\n }\n staff_time_task_comments: {\n task_id: string\n tenant_id: string\n organization_id: string\n visibility: string\n created_at: Date\n deleted_at: Date | null\n }\n}\n\nexport type PortalTicketPageRow = { id: string; lastActivityAt: Date }\n\nfunction toDate(value: unknown, fallback: Date): Date {\n if (value instanceof Date) return value\n if (typeof value === 'string' || typeof value === 'number') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed\n }\n return fallback\n}\n\n/**\n * One page of the caller's tickets, newest customer-visible activity first. Activity\n * is the latest of the ticket's filing time and its newest customer-visible comment\n * (either side), so internal staff edits \u2014 which bump `updated_at` \u2014 never reorder the\n * customer's list while a reply the customer can read always does. The ownership\n * clause is the same as `portalOwnedTaskWhere`; the activity is computed per row by a\n * correlated `max()` served by the comments `(organization_id, task_id, created_at)` index.\n */\nexport async function listPortalTicketPage(\n scope: Pick<SupportPortalScope, 'tenantId' | 'organizationId' | 'customerId' | 'em'>,\n options: { statusIds: readonly string[] | null; limit: number; offset: number },\n): Promise<{ rows: PortalTicketPageRow[]; total: number }> {\n if (options.statusIds && options.statusIds.length === 0) return { rows: [], total: 0 }\n const db = scope.em.getKysely<PortalTicketActivityDb>()\n let owned = db\n .selectFrom('staff_time_tasks as task')\n .where('task.tenant_id', '=', scope.tenantId)\n .where('task.organization_id', '=', scope.organizationId)\n .where('task.customer_id', '=', scope.customerId)\n .where('task.source', '=', 'portal')\n .where('task.deleted_at', 'is', null)\n if (options.statusIds) owned = owned.where('task.task_status_id', 'in', [...options.statusIds])\n const lastActivity = sql<Date>`greatest(task.created_at, coalesce((\n select max(visible_comment.created_at)\n from staff_time_task_comments as visible_comment\n where visible_comment.organization_id = task.organization_id\n and visible_comment.task_id = task.id\n and visible_comment.tenant_id = task.tenant_id\n and visible_comment.visibility = 'customer'\n and visible_comment.deleted_at is null\n ), task.created_at))`\n const [countRow, pageRows] = await Promise.all([\n owned.select((builder) => builder.fn.countAll<string | number>().as('total')).executeTakeFirst(),\n owned\n .select(['task.id as id', 'task.created_at as created_at'])\n .select(lastActivity.as('last_activity_at'))\n .orderBy('last_activity_at', 'desc')\n .orderBy('task.id', 'desc')\n .limit(options.limit)\n .offset(options.offset)\n .execute(),\n ])\n const total = Number(countRow?.total ?? 0)\n return {\n total: Number.isFinite(total) ? total : 0,\n rows: pageRows.map((row) => ({\n id: row.id,\n lastActivityAt: toDate(row.last_activity_at, toDate(row.created_at, new Date(0))),\n })),\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["guard"]
7
+ }