@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7722.1.901c72b9e9

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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/staff/api/portal/support-tickets/%5Bid%5D/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { StaffTimeTaskComment, StaffTimeTaskStatus } from '../../../../data/entities'\nimport {\n PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,\n PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,\n loadPortalOwnedTicket,\n resolveSupportPortalScope,\n} from '../../../../lib/support/portalTickets'\nimport { portalErrorResponse, readPathId } from '../../../../lib/support/portalRoute'\nimport {\n listCustomerVisibleAttachments,\n loadPortalAttachmentsApi,\n portalSupportAttachmentItemSchema,\n type PortalSupportAttachmentItem,\n} from '../../../../lib/support/portalAttachments'\nimport { derivePortalStatus } from '../../../../lib/support/portalStatus'\n\nconst logger = createLogger('staff').child({ component: 'api/portal/support-tickets/detail' })\n\nexport const metadata: { requireAuth?: boolean } = { requireAuth: false }\n\nconst messageSchema = z.object({\n id: z.string().uuid(),\n authorKind: z.enum(['staff', 'customer']),\n authorName: z.string().nullable(),\n isSelf: z.boolean(),\n body: z.string(),\n createdAt: z.string().nullable(),\n})\n\nconst detailSchema = z.object({\n id: z.string().uuid(),\n supportReference: z.string(),\n subject: z.string(),\n description: z.string(),\n category: z.object({ value: z.string(), label: z.string() }),\n portalStatus: z.enum(['received', 'in_progress', 'resolved']),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n canReply: z.boolean(),\n requester: z.object({ name: z.string().nullable(), isSelf: z.boolean() }),\n messages: z.array(messageSchema),\n attachments: z.array(portalSupportAttachmentItemSchema),\n})\n\nfunction nonEmptyName(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n}\n\nasync function loadVisibleAttachments(\n scope: Parameters<typeof listCustomerVisibleAttachments>[1],\n taskId: string,\n canManage: boolean,\n): Promise<PortalSupportAttachmentItem[]> {\n const api = await loadPortalAttachmentsApi()\n if (!api) return []\n try {\n return await listCustomerVisibleAttachments(api, scope, taskId, canManage)\n } catch (err) {\n logger.warn('staff portal support ticket attachments could not be listed', { err, taskId })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_attachments_failed' })\n return []\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)\n if (scope instanceof Response) return scope\n const taskId = readPathId(req, /\\/support-tickets\\/([^/]+)/)\n const owned = taskId ? await loadPortalOwnedTicket(scope, taskId) : null\n if (!owned) return NextResponse.json({ ok: false, error: 'staff.errors.notFound' }, { status: 404 })\n const { task, ticket } = owned\n const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }\n const status = await scope.em.findOne(StaffTimeTaskStatus, { id: task.taskStatusId, ...decryptionScope }, { fields: ['id', 'isDefault', 'isDone'] })\n const comments = await scope.em.find(\n StaffTimeTaskComment,\n { taskId: task.id, ...decryptionScope, visibility: 'customer', deletedAt: null },\n { orderBy: { createdAt: 'asc', id: 'asc' } },\n )\n const staffUserIds = [...new Set(comments.map((comment) => comment.authorUserId).filter((id): id is string => typeof id === 'string'))]\n const staffNames = new Map<string, string | null>()\n if (staffUserIds.length) {\n try {\n const users = await findWithDecryption(\n scope.em,\n User,\n { id: { $in: staffUserIds }, tenantId: scope.tenantId },\n undefined,\n decryptionScope,\n )\n for (const user of users) staffNames.set(user.id, nonEmptyName(user.name))\n } catch (err) {\n logger.warn('staff portal support ticket could not resolve staff names', { err, taskId: task.id })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_staff_names_failed' })\n staffNames.clear()\n }\n }\n const request = ticket.customerRequestSnapshot\n const canReply = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n const attachments = await loadVisibleAttachments(scope, task.id, canReply)\n return NextResponse.json(\n detailSchema.parse({\n id: task.id,\n supportReference: ticket.supportReference,\n subject: request && typeof request.subject === 'string' ? request.subject : '',\n description: request && typeof request.body === 'string' ? request.body : '',\n category: ticket.categorySnapshot,\n portalStatus: derivePortalStatus(status),\n createdAt: task.createdAt ? task.createdAt.toISOString() : null,\n updatedAt: task.updatedAt ? task.updatedAt.toISOString() : null,\n canReply,\n requester: {\n name: nonEmptyName(ticket.customerSnapshot?.reporterDisplayName),\n isSelf: ticket.reportedByCustomerUserId === scope.auth.sub,\n },\n messages: comments.map((comment) => ({\n id: comment.id,\n authorKind: comment.authorKind === 'customer' ? 'customer' : 'staff',\n authorName:\n comment.authorKind === 'customer'\n ? nonEmptyName(comment.authorDisplaySnapshot?.displayName)\n : comment.authorUserId\n ? staffNames.get(comment.authorUserId) ?? null\n : null,\n isSelf: comment.authorKind === 'customer' && comment.authorCustomerUserId === scope.auth.sub,\n body: comment.body,\n createdAt: comment.createdAt ? comment.createdAt.toISOString() : null,\n })),\n attachments,\n }),\n )\n } catch (err) {\n return portalErrorResponse(err, 'staff portal support ticket GET failed')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff Portal',\n summary: 'Support ticket detail',\n methods: {\n GET: {\n summary: 'Support ticket detail',\n description:\n 'One ticket owned by the signed-in customer: the original request, the customer-facing status, and customer-visible messages only, and the customer-visible files (`attachments`, same items as the attachments list; an empty list when the attachments module is unavailable). Internal notes and internal files are never returned. A ticket of another customer answers 404.',\n responses: [{ status: 200, description: 'Ticket', schema: detailSchema }],\n errors: [\n { status: 401, description: 'No portal session' },\n { status: 403, description: 'Missing portal.support_tickets.view' },\n { status: 404, description: 'Not found or not owned by this customer' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,60 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { loadSupportTicketCategories } from "../../../../lib/support/dictionaries.js";
4
+ import { resolveRoutableCategoryValues } from "../../../../lib/support/routing.js";
5
+ import {
6
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
7
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
8
+ resolveSupportPortalScope
9
+ } from "../../../../lib/support/portalTickets.js";
10
+ import { portalErrorResponse } from "../../../../lib/support/portalRoute.js";
11
+ import { loadPortalAttachmentsApi } from "../../../../lib/support/portalAttachments.js";
12
+ const metadata = { requireAuth: false };
13
+ const optionsResponseSchema = z.object({
14
+ categories: z.array(z.object({ value: z.string(), label: z.string() })),
15
+ canCreate: z.boolean(),
16
+ maxUploadBytes: z.number().int().positive().nullable()
17
+ });
18
+ async function GET(req) {
19
+ try {
20
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE);
21
+ if (scope instanceof Response) return scope;
22
+ const supportScope = { tenantId: scope.tenantId, organizationId: scope.organizationId };
23
+ const categories = await loadSupportTicketCategories(scope.em, supportScope);
24
+ const routes = await resolveRoutableCategoryValues(scope.em, supportScope, categories.map((category) => category.value));
25
+ const routable = categories.filter((category) => routes.has(category.value));
26
+ const canManage = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
27
+ const attachmentsApi = await loadPortalAttachmentsApi();
28
+ const maxUploadBytes = attachmentsApi ? attachmentsApi.resolveAttachmentMaxBytes() : null;
29
+ return NextResponse.json(
30
+ optionsResponseSchema.parse({
31
+ categories: routable,
32
+ canCreate: canManage && routable.length > 0,
33
+ maxUploadBytes: maxUploadBytes && maxUploadBytes > 0 ? maxUploadBytes : null
34
+ })
35
+ );
36
+ } catch (err) {
37
+ return portalErrorResponse(err, "staff portal support options GET failed");
38
+ }
39
+ }
40
+ const openApi = {
41
+ tag: "Staff Portal",
42
+ summary: "Support ticket form options",
43
+ methods: {
44
+ GET: {
45
+ summary: "Support ticket form options",
46
+ description: "Categories the signed-in customer can file a ticket in \u2014 only those that currently route to a usable project \u2014 whether the caller may create tickets (`portal.support_tickets.manage`), and the server upload size limit in bytes (`maxUploadBytes`, `null` when the attachments module is unavailable).",
47
+ responses: [{ status: 200, description: "Options", schema: optionsResponseSchema }],
48
+ errors: [
49
+ { status: 401, description: "No portal session" },
50
+ { status: 403, description: "Portal account not linked to a customer, or missing portal.support_tickets.view" }
51
+ ]
52
+ }
53
+ }
54
+ };
55
+ export {
56
+ GET,
57
+ metadata,
58
+ openApi
59
+ };
60
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/staff/api/portal/support-tickets/options/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { loadSupportTicketCategories } from '../../../../lib/support/dictionaries'\nimport { resolveRoutableCategoryValues } from '../../../../lib/support/routing'\nimport {\n PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,\n PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,\n resolveSupportPortalScope,\n} from '../../../../lib/support/portalTickets'\nimport { portalErrorResponse } from '../../../../lib/support/portalRoute'\nimport { loadPortalAttachmentsApi } from '../../../../lib/support/portalAttachments'\n\nexport const metadata: { requireAuth?: boolean } = { requireAuth: false }\n\nconst optionsResponseSchema = z.object({\n categories: z.array(z.object({ value: z.string(), label: z.string() })),\n canCreate: z.boolean(),\n maxUploadBytes: z.number().int().positive().nullable(),\n})\n\nexport async function GET(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)\n if (scope instanceof Response) return scope\n const supportScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }\n const categories = await loadSupportTicketCategories(scope.em, supportScope)\n const routes = await resolveRoutableCategoryValues(scope.em, supportScope, categories.map((category) => category.value))\n const routable = categories.filter((category) => routes.has(category.value))\n const canManage = await scope.hasFeature(PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n const attachmentsApi = await loadPortalAttachmentsApi()\n const maxUploadBytes = attachmentsApi ? attachmentsApi.resolveAttachmentMaxBytes() : null\n return NextResponse.json(\n optionsResponseSchema.parse({\n categories: routable,\n canCreate: canManage && routable.length > 0,\n maxUploadBytes: maxUploadBytes && maxUploadBytes > 0 ? maxUploadBytes : null,\n }),\n )\n } catch (err) {\n return portalErrorResponse(err, 'staff portal support options GET failed')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff Portal',\n summary: 'Support ticket form options',\n methods: {\n GET: {\n summary: 'Support ticket form options',\n description:\n 'Categories the signed-in customer can file a ticket in \u2014 only those that currently route to a usable project \u2014 whether the caller may create tickets (`portal.support_tickets.manage`), and the server upload size limit in bytes (`maxUploadBytes`, `null` when the attachments module is unavailable).',\n responses: [{ status: 200, description: 'Options', schema: optionsResponseSchema }],\n errors: [\n { status: 401, description: 'No portal session' },\n { status: 403, description: 'Portal account not linked to a customer, or missing portal.support_tickets.view' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,mCAAmC;AAC5C,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AAElC,MAAM,WAAsC,EAAE,aAAa,MAAM;AAExE,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAAA,EACtE,WAAW,EAAE,QAAQ;AAAA,EACrB,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AACvD,CAAC;AAED,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,MAAM,0BAA0B,KAAK,mCAAmC;AACtF,QAAI,iBAAiB,SAAU,QAAO;AACtC,UAAM,eAAe,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AACtF,UAAM,aAAa,MAAM,4BAA4B,MAAM,IAAI,YAAY;AAC3E,UAAM,SAAS,MAAM,8BAA8B,MAAM,IAAI,cAAc,WAAW,IAAI,CAAC,aAAa,SAAS,KAAK,CAAC;AACvH,UAAM,WAAW,WAAW,OAAO,CAAC,aAAa,OAAO,IAAI,SAAS,KAAK,CAAC;AAC3E,UAAM,YAAY,MAAM,MAAM,WAAW,qCAAqC;AAC9E,UAAM,iBAAiB,MAAM,yBAAyB;AACtD,UAAM,iBAAiB,iBAAiB,eAAe,0BAA0B,IAAI;AACrF,WAAO,aAAa;AAAA,MAClB,sBAAsB,MAAM;AAAA,QAC1B,YAAY;AAAA,QACZ,WAAW,aAAa,SAAS,SAAS;AAAA,QAC1C,gBAAgB,kBAAkB,iBAAiB,IAAI,iBAAiB;AAAA,MAC1E,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,oBAAoB,KAAK,yCAAyC;AAAA,EAC3E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,WAAW,QAAQ,sBAAsB,CAAC;AAAA,MAClF,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,QAChD,EAAE,QAAQ,KAAK,aAAa,kFAAkF;AAAA,MAChH;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,220 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
+ import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
5
+ import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
6
+ import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
7
+ import {
8
+ StaffSupportTicket,
9
+ StaffTimeTask,
10
+ StaffTimeTaskStatus
11
+ } from "../../../data/entities.js";
12
+ import {
13
+ PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,
14
+ PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,
15
+ listPortalTicketPage,
16
+ portalOwnedTaskWhere,
17
+ resolveCustomerDisplay,
18
+ resolveSupportPortalScope
19
+ } from "../../../lib/support/portalTickets.js";
20
+ import {
21
+ SUPPORT_TICKET_RESOURCE_KIND,
22
+ buildPortalCommandContext,
23
+ checkPortalRateLimit,
24
+ portalErrorResponse
25
+ } from "../../../lib/support/portalRoute.js";
26
+ import { derivePortalStatus } from "../../../lib/support/portalStatus.js";
27
+ import {
28
+ staffSupportTicketCommandIds
29
+ } from "../../../commands/support-tickets.js";
30
+ const metadata = { requireAuth: false };
31
+ const portalStatusSchema = z.enum(["received", "in_progress", "resolved"]);
32
+ const listQuerySchema = z.object({
33
+ page: z.coerce.number().int().min(1).default(1),
34
+ pageSize: z.coerce.number().int().min(1).max(50).default(20),
35
+ status: portalStatusSchema.optional()
36
+ });
37
+ const ticketListItemSchema = z.object({
38
+ id: z.string().uuid(),
39
+ supportReference: z.string(),
40
+ subject: z.string(),
41
+ category: z.object({ value: z.string(), label: z.string() }),
42
+ portalStatus: portalStatusSchema,
43
+ createdAt: z.string().nullable(),
44
+ updatedAt: z.string().nullable(),
45
+ lastActivityAt: z.string().nullable()
46
+ });
47
+ const listResponseSchema = z.object({
48
+ items: z.array(ticketListItemSchema),
49
+ total: z.number().int(),
50
+ page: z.number().int(),
51
+ pageSize: z.number().int(),
52
+ totalPages: z.number().int()
53
+ });
54
+ const createBodySchema = z.object({
55
+ categoryValue: z.string().trim().min(1).max(150),
56
+ subject: z.string().trim().min(3).max(200),
57
+ description: z.string().trim().min(1).max(1e4)
58
+ });
59
+ const createResponseSchema = z.object({
60
+ ok: z.literal(true),
61
+ id: z.string().uuid(),
62
+ supportReference: z.string()
63
+ });
64
+ function isoOrNull(value) {
65
+ return value ? value.toISOString() : null;
66
+ }
67
+ async function statusIdsFor(scope, status) {
68
+ const where = { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null };
69
+ if (status === "resolved") where.isDone = true;
70
+ else if (status === "received") Object.assign(where, { isDone: false, isDefault: true });
71
+ else Object.assign(where, { isDone: false, isDefault: false });
72
+ const rows = await scope.em.find(StaffTimeTaskStatus, where, { fields: ["id"] });
73
+ return rows.map((row) => row.id);
74
+ }
75
+ async function GET(req) {
76
+ try {
77
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE);
78
+ if (scope instanceof Response) return scope;
79
+ const url = new URL(req.url);
80
+ const query = listQuerySchema.parse(Object.fromEntries(url.searchParams.entries()));
81
+ const statusIds = query.status ? await statusIdsFor(scope, query.status) : null;
82
+ const page = await listPortalTicketPage(scope, {
83
+ statusIds,
84
+ limit: query.pageSize,
85
+ offset: (query.page - 1) * query.pageSize
86
+ });
87
+ const total = page.total;
88
+ const pageIds = page.rows.map((row) => row.id);
89
+ const lastActivityByTask = new Map(page.rows.map((row) => [row.id, row.lastActivityAt]));
90
+ const loadedTasks = pageIds.length ? await scope.em.find(StaffTimeTask, { id: { $in: pageIds }, ...portalOwnedTaskWhere(scope) }) : [];
91
+ const taskById = new Map(loadedTasks.map((task) => [task.id, task]));
92
+ const tasks = pageIds.flatMap((id) => {
93
+ const task = taskById.get(id);
94
+ return task ? [task] : [];
95
+ });
96
+ const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId };
97
+ const taskIds = tasks.map((task) => task.id);
98
+ const tickets = taskIds.length ? await findWithDecryption(
99
+ scope.em,
100
+ StaffSupportTicket,
101
+ { taskId: { $in: taskIds }, ...decryptionScope, deletedAt: null },
102
+ void 0,
103
+ decryptionScope
104
+ ) : [];
105
+ const ticketByTask = new Map(tickets.map((ticket) => [ticket.taskId, ticket]));
106
+ const taskStatusIds = [...new Set(tasks.map((task) => task.taskStatusId))];
107
+ const statuses = taskStatusIds.length ? await scope.em.find(StaffTimeTaskStatus, { id: { $in: taskStatusIds }, ...decryptionScope }, { fields: ["id", "isDefault", "isDone"] }) : [];
108
+ const statusById = new Map(statuses.map((status) => [status.id, status]));
109
+ const items = tasks.flatMap((task) => {
110
+ const ticket = ticketByTask.get(task.id);
111
+ if (!ticket) return [];
112
+ const request = ticket.customerRequestSnapshot;
113
+ const lastActivity = lastActivityByTask.get(task.id) ?? task.createdAt;
114
+ return [
115
+ {
116
+ id: task.id,
117
+ supportReference: ticket.supportReference,
118
+ subject: request && typeof request.subject === "string" ? request.subject : "",
119
+ category: ticket.categorySnapshot,
120
+ portalStatus: derivePortalStatus(statusById.get(task.taskStatusId) ?? null),
121
+ createdAt: isoOrNull(task.createdAt),
122
+ updatedAt: isoOrNull(task.updatedAt),
123
+ lastActivityAt: isoOrNull(lastActivity)
124
+ }
125
+ ];
126
+ });
127
+ return NextResponse.json(
128
+ listResponseSchema.parse({
129
+ items,
130
+ total,
131
+ page: query.page,
132
+ pageSize: query.pageSize,
133
+ totalPages: Math.max(1, Math.ceil(total / query.pageSize))
134
+ })
135
+ );
136
+ } catch (err) {
137
+ return portalErrorResponse(err, "staff portal support tickets GET failed");
138
+ }
139
+ }
140
+ async function POST(req) {
141
+ try {
142
+ const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE);
143
+ if (scope instanceof Response) return scope;
144
+ const limited = await checkPortalRateLimit(scope, "create");
145
+ if (limited) return limited;
146
+ const body = createBodySchema.parse(await readJsonSafe(req, {}));
147
+ const guarded = await runRouteMutationGuards({
148
+ container: scope.container,
149
+ req,
150
+ auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },
151
+ input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: null, operation: "create", mutationPayload: body }
152
+ });
153
+ if (!guarded.ok) return guarded.response;
154
+ const effectiveBody = guarded.modifiedPayload ? createBodySchema.parse({ ...body, ...guarded.modifiedPayload }) : body;
155
+ const customer = await resolveCustomerDisplay(scope);
156
+ const input = {
157
+ tenantId: scope.tenantId,
158
+ organizationId: scope.organizationId,
159
+ customerId: scope.customerId,
160
+ customerUserId: scope.auth.sub,
161
+ customerDisplayName: customer.displayName,
162
+ customerKind: customer.kind,
163
+ reporterDisplayName: scope.auth.displayName ?? null,
164
+ categoryValue: effectiveBody.categoryValue,
165
+ subject: effectiveBody.subject,
166
+ description: effectiveBody.description
167
+ };
168
+ const commandBus = scope.container.resolve("commandBus");
169
+ const { result } = await commandBus.execute(
170
+ staffSupportTicketCommandIds.createFromPortal,
171
+ { input, ctx: buildPortalCommandContext(scope, req) }
172
+ );
173
+ await guarded.runAfterSuccess();
174
+ return NextResponse.json(
175
+ createResponseSchema.parse({ ok: true, id: result.taskId, supportReference: result.supportReference }),
176
+ { status: 201 }
177
+ );
178
+ } catch (err) {
179
+ const rejection = getCommandInterceptorHttpRejection(err);
180
+ if (rejection) return NextResponse.json(rejection.body, { status: rejection.status });
181
+ return portalErrorResponse(err, "staff portal support tickets POST failed");
182
+ }
183
+ }
184
+ const errorSchema = z.object({ ok: z.literal(false).optional(), error: z.string(), code: z.string().optional() });
185
+ const openApi = {
186
+ tag: "Staff Portal",
187
+ summary: "Customer support tickets",
188
+ methods: {
189
+ GET: {
190
+ summary: "List my support tickets",
191
+ description: "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).",
192
+ query: listQuerySchema,
193
+ responses: [{ status: 200, description: "Tickets", schema: listResponseSchema }],
194
+ errors: [
195
+ { status: 401, description: "No portal session", schema: errorSchema },
196
+ { status: 403, description: "Not linked to a customer, or missing portal.support_tickets.view", schema: errorSchema }
197
+ ]
198
+ },
199
+ POST: {
200
+ summary: "File a support ticket",
201
+ description: "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.",
202
+ requestBody: { contentType: "application/json", schema: createBodySchema },
203
+ responses: [{ status: 201, description: "Ticket filed", schema: createResponseSchema }],
204
+ errors: [
205
+ { status: 401, description: "No portal session", schema: errorSchema },
206
+ { status: 403, description: "Missing portal.support_tickets.manage", schema: errorSchema },
207
+ { status: 409, description: "Too many tickets were filed at once; resubmit", schema: errorSchema },
208
+ { status: 422, description: "The category is not available", schema: errorSchema },
209
+ { status: 429, description: "Rate limit exceeded", schema: errorSchema }
210
+ ]
211
+ }
212
+ }
213
+ };
214
+ export {
215
+ GET,
216
+ POST,
217
+ metadata,
218
+ openApi
219
+ };
220
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/staff/api/portal/support-tickets/route.ts"],
4
+ "sourcesContent": ["/**\n * `GET|POST /api/staff/portal/support-tickets` \u2014 customer portal support intake.\n *\n * Every row is loaded with `portalOwnedTaskWhere` (tenant, organization, the\n * signed-in portal user's own `customerEntityId` on the TASK, `source = 'portal'`,\n * not deleted). The project's customer is never consulted, so tickets from many\n * customers can share one project. Subject and description come from the encrypted\n * `customer_request_snapshot`, never from the staff-editable task text, and the\n * response carries no project, task reference, assignee, internal comment or money.\n */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n StaffSupportTicket,\n StaffTimeTask,\n StaffTimeTaskStatus,\n} from '../../../data/entities'\nimport {\n PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE,\n PORTAL_SUPPORT_TICKETS_VIEW_FEATURE,\n listPortalTicketPage,\n portalOwnedTaskWhere,\n resolveCustomerDisplay,\n resolveSupportPortalScope,\n} from '../../../lib/support/portalTickets'\nimport {\n SUPPORT_TICKET_RESOURCE_KIND,\n buildPortalCommandContext,\n checkPortalRateLimit,\n portalErrorResponse,\n} from '../../../lib/support/portalRoute'\nimport { derivePortalStatus } from '../../../lib/support/portalStatus'\nimport {\n staffSupportTicketCommandIds,\n type SupportTicketCreateFromPortalInput,\n type SupportTicketCreateResult,\n} from '../../../commands/support-tickets'\n\nexport const metadata: { requireAuth?: boolean } = { requireAuth: false }\n\nconst portalStatusSchema = z.enum(['received', 'in_progress', 'resolved'])\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(50).default(20),\n status: portalStatusSchema.optional(),\n})\n\nconst ticketListItemSchema = z.object({\n id: z.string().uuid(),\n supportReference: z.string(),\n subject: z.string(),\n category: z.object({ value: z.string(), label: z.string() }),\n portalStatus: portalStatusSchema,\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n lastActivityAt: z.string().nullable(),\n})\n\nconst listResponseSchema = z.object({\n items: z.array(ticketListItemSchema),\n total: z.number().int(),\n page: z.number().int(),\n pageSize: z.number().int(),\n totalPages: z.number().int(),\n})\n\nconst createBodySchema = z.object({\n categoryValue: z.string().trim().min(1).max(150),\n subject: z.string().trim().min(3).max(200),\n description: z.string().trim().min(1).max(10000),\n})\n\nconst createResponseSchema = z.object({\n ok: z.literal(true),\n id: z.string().uuid(),\n supportReference: z.string(),\n})\n\nfunction isoOrNull(value: Date | null | undefined): string | null {\n return value ? value.toISOString() : null\n}\n\nasync function statusIdsFor(\n scope: { em: import('@mikro-orm/postgresql').EntityManager; tenantId: string; organizationId: string },\n status: z.infer<typeof portalStatusSchema>,\n): Promise<string[]> {\n const where: Record<string, unknown> = { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null }\n if (status === 'resolved') where.isDone = true\n else if (status === 'received') Object.assign(where, { isDone: false, isDefault: true })\n else Object.assign(where, { isDone: false, isDefault: false })\n const rows = await scope.em.find(StaffTimeTaskStatus, where, { fields: ['id'] })\n return rows.map((row) => row.id)\n}\n\nexport async function GET(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_VIEW_FEATURE)\n if (scope instanceof Response) return scope\n const url = new URL(req.url)\n const query = listQuerySchema.parse(Object.fromEntries(url.searchParams.entries()))\n const statusIds = query.status ? await statusIdsFor(scope, query.status) : null\n const page = await listPortalTicketPage(scope, {\n statusIds,\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n })\n const total = page.total\n const pageIds = page.rows.map((row) => row.id)\n const lastActivityByTask = new Map(page.rows.map((row) => [row.id, row.lastActivityAt]))\n const loadedTasks = pageIds.length\n ? await scope.em.find(StaffTimeTask, { id: { $in: pageIds }, ...portalOwnedTaskWhere(scope) })\n : []\n const taskById = new Map(loadedTasks.map((task) => [task.id, task]))\n const tasks = pageIds.flatMap((id) => {\n const task = taskById.get(id)\n return task ? [task] : []\n })\n const decryptionScope = { tenantId: scope.tenantId, organizationId: scope.organizationId }\n const taskIds = tasks.map((task) => task.id)\n const tickets = taskIds.length\n ? await findWithDecryption(\n scope.em,\n StaffSupportTicket,\n { taskId: { $in: taskIds }, ...decryptionScope, deletedAt: null },\n undefined,\n decryptionScope,\n )\n : []\n const ticketByTask = new Map(tickets.map((ticket) => [ticket.taskId, ticket]))\n const taskStatusIds = [...new Set(tasks.map((task) => task.taskStatusId))]\n const statuses = taskStatusIds.length\n ? await scope.em.find(StaffTimeTaskStatus, { id: { $in: taskStatusIds }, ...decryptionScope }, { fields: ['id', 'isDefault', 'isDone'] })\n : []\n const statusById = new Map(statuses.map((status) => [status.id, status]))\n const items = tasks.flatMap((task) => {\n const ticket = ticketByTask.get(task.id)\n if (!ticket) return []\n const request = ticket.customerRequestSnapshot\n const lastActivity = lastActivityByTask.get(task.id) ?? task.createdAt\n return [\n {\n id: task.id,\n supportReference: ticket.supportReference,\n subject: request && typeof request.subject === 'string' ? request.subject : '',\n category: ticket.categorySnapshot,\n portalStatus: derivePortalStatus(statusById.get(task.taskStatusId) ?? null),\n createdAt: isoOrNull(task.createdAt),\n updatedAt: isoOrNull(task.updatedAt),\n lastActivityAt: isoOrNull(lastActivity),\n },\n ]\n })\n return NextResponse.json(\n listResponseSchema.parse({\n items,\n total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: Math.max(1, Math.ceil(total / query.pageSize)),\n }),\n )\n } catch (err) {\n return portalErrorResponse(err, 'staff portal support tickets GET failed')\n }\n}\n\nexport async function POST(req: Request) {\n try {\n const scope = await resolveSupportPortalScope(req, PORTAL_SUPPORT_TICKETS_MANAGE_FEATURE)\n if (scope instanceof Response) return scope\n const limited = await checkPortalRateLimit(scope, 'create')\n if (limited) return limited\n const body = createBodySchema.parse(await readJsonSafe<Record<string, unknown>>(req, {}))\n const guarded = await runRouteMutationGuards({\n container: scope.container,\n req,\n auth: { userId: scope.auth.sub, tenantId: scope.tenantId, organizationId: scope.organizationId, userFeatures: [] },\n input: { resourceKind: SUPPORT_TICKET_RESOURCE_KIND, resourceId: null, operation: 'create', mutationPayload: body },\n })\n if (!guarded.ok) return guarded.response\n const effectiveBody = guarded.modifiedPayload\n ? createBodySchema.parse({ ...body, ...guarded.modifiedPayload })\n : body\n const customer = await resolveCustomerDisplay(scope)\n const input: SupportTicketCreateFromPortalInput = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n customerId: scope.customerId,\n customerUserId: scope.auth.sub,\n customerDisplayName: customer.displayName,\n customerKind: customer.kind,\n reporterDisplayName: scope.auth.displayName ?? null,\n categoryValue: effectiveBody.categoryValue,\n subject: effectiveBody.subject,\n description: effectiveBody.description,\n }\n const commandBus = scope.container.resolve('commandBus') as CommandBus\n const { result } = await commandBus.execute<SupportTicketCreateFromPortalInput, SupportTicketCreateResult>(\n staffSupportTicketCommandIds.createFromPortal,\n { input, ctx: buildPortalCommandContext(scope, req) },\n )\n await guarded.runAfterSuccess()\n return NextResponse.json(\n createResponseSchema.parse({ ok: true, id: result.taskId, supportReference: result.supportReference }),\n { status: 201 },\n )\n } catch (err) {\n const rejection = getCommandInterceptorHttpRejection(err)\n if (rejection) return NextResponse.json(rejection.body, { status: rejection.status })\n return portalErrorResponse(err, 'staff portal support tickets POST failed')\n }\n}\n\nconst errorSchema = z.object({ ok: z.literal(false).optional(), error: z.string(), code: z.string().optional() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff Portal',\n summary: 'Customer support tickets',\n methods: {\n GET: {\n summary: 'List my support tickets',\n description:\n \"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).\",\n query: listQuerySchema,\n responses: [{ status: 200, description: 'Tickets', schema: listResponseSchema }],\n errors: [\n { status: 401, description: 'No portal session', schema: errorSchema },\n { status: 403, description: 'Not linked to a customer, or missing portal.support_tickets.view', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'File a support ticket',\n description:\n '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.',\n requestBody: { contentType: 'application/json', schema: createBodySchema },\n responses: [{ status: 201, description: 'Ticket filed', schema: createResponseSchema }],\n errors: [\n { status: 401, description: 'No portal session', schema: errorSchema },\n { status: 403, description: 'Missing portal.support_tickets.manage', schema: errorSchema },\n { status: 409, description: 'Too many tickets were filed at once; resubmit', schema: errorSchema },\n { status: 422, description: 'The category is not available', schema: errorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -16,6 +16,7 @@ import {
16
16
  runTimesheetInterceptors
17
17
  } from "../_shared/withTimesheetInterceptors.js";
18
18
  import {
19
+ preserveOmittedSupportSettings,
19
20
  readTimeTrackingSettings,
20
21
  writeTimeTrackingSettings
21
22
  } from "../../../lib/time-tracking/settings.js";
@@ -115,7 +116,12 @@ async function PUT(req) {
115
116
  if (!interceptors.ok) return interceptors.response;
116
117
  const { session } = interceptors;
117
118
  const settingsSchema = buildTimeTrackingSettingsSchema();
118
- const parsed = settingsSchema.parse(session.body);
119
+ const requestBody = await preserveOmittedSupportSettings(
120
+ resolveConfigService(context),
121
+ { tenantId: context.tenantId },
122
+ session.body
123
+ );
124
+ const parsed = settingsSchema.parse(requestBody);
119
125
  const guardResult = await runRouteMutationGuards({
120
126
  container: context.container,
121
127
  req,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/staff/api/timesheets/settings/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveFeatureAccess } from '../../../lib/time-tracking/featureAccess'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitStaffEvent } from '../../../events'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { buildTimeTrackingSettingsSchema } from '../../../lib/time-tracking/settingKeys'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../guards'\nimport {\n readSearchParamsRecord,\n runTimesheetInterceptors,\n} from '../_shared/withTimesheetInterceptors'\nimport {\n readTimeTrackingSettings,\n writeTimeTrackingSettings,\n type TimeTrackingSettings,\n} from '../../../lib/time-tracking/settings'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/settings' })\n\nconst VIEW_FEATURE = 'staff.timesheets.view'\nconst MANAGE_FEATURE = 'staff.timesheets.settings.manage'\nconst RESOURCE_KIND = STAFF_TIME_TRACKING_RESOURCE_KINDS.settings\nconst RESOURCE_ID = 'time_tracking'\nconst EXPECTED_TENANT_PARAM = 'expectedTenantId'\nconst TENANT_SCOPE_CHANGED_CODE = 'tenant_scope_changed'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [VIEW_FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n}\n\n/**\n * EP-42 \u2014 derived from the setting-key registry, so a contributed key is published in\n * the OpenAPI response shape instead of being invisible to it. Rebuilt per read for the\n * same reason the request schema is: a key may be registered after this module loads.\n */\nfunction settingsResponseSchema(): z.ZodTypeAny {\n return buildTimeTrackingSettingsSchema()\n}\n\ntype SettingsContext = {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string\n organizationId: string | null\n actorId: string\n translate: (key: string, fallback?: string) => string\n}\n\nasync function resolveSettingsContext(req: Request): Promise<SettingsContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })\n }\n const actorId =\n (typeof auth.sub === 'string' && auth.sub.trim().length > 0 && auth.sub) ||\n (typeof auth.userId === 'string' && auth.userId.trim().length > 0 && auth.userId) ||\n (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0 && auth.keyId) ||\n 'system'\n return {\n container,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n actorId,\n translate,\n }\n}\n\nfunction resolveConfigService(context: SettingsContext): ModuleConfigService {\n return context.container.resolve('moduleConfigService') as ModuleConfigService\n}\n\nasync function GET(req: Request) {\n try {\n const context = await resolveSettingsContext(req)\n const interceptors = await runTimesheetInterceptors({\n request: req,\n method: 'GET',\n scope: {\n container: context.container,\n userId: context.actorId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n tenantGlobal: true,\n },\n query: readSearchParamsRecord(req.url),\n })\n if (!interceptors.ok) return interceptors.response\n\n const settings = await readTimeTrackingSettings(resolveConfigService(context), {\n tenantId: context.tenantId,\n })\n return interceptors.session.respond(200, settings)\n } catch (err) {\n return errorResponse(err, 'staff.timesheets.settings.GET failed')\n }\n}\n\nfunction assertExpectedTenant(req: Request, context: SettingsContext): void {\n const expectedTenantId = new URL(req.url).searchParams.get(EXPECTED_TENANT_PARAM)\n if (!expectedTenantId || expectedTenantId === context.tenantId) return\n throw new CrudHttpError(409, {\n error: context.translate(\n 'staff.errors.tenantScopeChanged',\n 'The selected tenant changed before the request was processed.',\n ),\n code: TENANT_SCOPE_CHANGED_CODE,\n })\n}\n\nasync function PUT(req: Request) {\n try {\n const context = await resolveSettingsContext(req)\n assertExpectedTenant(req, context)\n // One lookup through the module's single RBAC authority, answering both\n // questions this route asks: may the caller manage settings, and what grant\n // list do the interceptors and the mutation-guard registry gate on. The\n // decision is checked unconditionally and fails closed; the list is plumbing\n // and is empty \u2014 never absent \u2014 when RBAC could not produce it.\n const access = await resolveFeatureAccess(context.container, context.actorId || null, [MANAGE_FEATURE], {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n if (!access.allowed) {\n throw forbidden(context.translate('staff.errors.forbidden', 'Forbidden'))\n }\n const grantedFeatures = access.grantedFeatures\n\n const interceptors = await runTimesheetInterceptors({\n request: req,\n method: 'PUT',\n scope: {\n container: context.container,\n userId: context.actorId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userFeatures: grantedFeatures,\n tenantGlobal: true,\n },\n body: await readJsonSafe<Record<string, unknown>>(req, {}),\n })\n if (!interceptors.ok) return interceptors.response\n const { session } = interceptors\n\n const settingsSchema = buildTimeTrackingSettingsSchema()\n const parsed = settingsSchema.parse(session.body)\n\n const guardResult = await runRouteMutationGuards({\n container: context.container,\n req,\n auth: {\n userId: context.actorId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userFeatures: grantedFeatures,\n },\n input: {\n resourceKind: RESOURCE_KIND,\n resourceId: RESOURCE_ID,\n operation: 'update',\n mutationPayload: parsed,\n },\n })\n if (!guardResult.ok) return guardResult.response\n\n const effective = guardResult.modifiedPayload\n ? settingsSchema.parse({ ...parsed, ...guardResult.modifiedPayload })\n : parsed\n\n const settings: TimeTrackingSettings = await writeTimeTrackingSettings(\n resolveConfigService(context),\n { tenantId: context.tenantId },\n effective,\n )\n\n await guardResult.runAfterSuccess()\n\n void emitStaffEvent('staff.timesheets.time_tracking.settings_updated', {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n settings,\n }, { persistent: true }).catch((err) => {\n logger.error('staff.timesheets emit time_tracking.settings_updated failed', { err })\n })\n\n return session.respond(200, settings)\n } catch (err) {\n return errorResponse(err, 'staff.timesheets.settings.PUT failed')\n }\n}\n\nasync function errorResponse(err: unknown, message: string) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('staff.errors.invalid_request', 'Invalid request'), details: err.issues },\n { status: 400 },\n )\n }\n logger.error(message, { err })\n return NextResponse.json(\n { error: translate('staff.errors.internal', 'Internal server error') },\n { status: 500 },\n )\n}\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Read time tracking settings',\n tags: ['Staff'],\n get responses() {\n return [\n { status: 200, description: 'Tenant time tracking settings', schema: settingsResponseSchema() },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ]\n },\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Update time tracking settings',\n tags: ['Staff'],\n query: z.object({ [EXPECTED_TENANT_PARAM]: z.string().uuid().optional() }),\n get requestBody() {\n return { schema: buildTimeTrackingSettingsSchema() }\n },\n get responses() {\n return [\n { status: 200, description: 'Updated time tracking settings', schema: settingsResponseSchema() },\n ]\n },\n errors: [\n { status: 400, description: 'Invalid request body' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.settings.manage' },\n { status: 409, description: 'The resolved tenant differs from expectedTenantId' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Time tracking module settings',\n methods: {\n GET: getDoc,\n PUT: putDoc,\n },\n}\n\nexport { GET, PUT }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveFeatureAccess } from '../../../lib/time-tracking/featureAccess'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitStaffEvent } from '../../../events'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { buildTimeTrackingSettingsSchema } from '../../../lib/time-tracking/settingKeys'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../guards'\nimport {\n readSearchParamsRecord,\n runTimesheetInterceptors,\n} from '../_shared/withTimesheetInterceptors'\nimport {\n preserveOmittedSupportSettings,\n readTimeTrackingSettings,\n writeTimeTrackingSettings,\n type TimeTrackingSettings,\n} from '../../../lib/time-tracking/settings'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/settings' })\n\nconst VIEW_FEATURE = 'staff.timesheets.view'\nconst MANAGE_FEATURE = 'staff.timesheets.settings.manage'\nconst RESOURCE_KIND = STAFF_TIME_TRACKING_RESOURCE_KINDS.settings\nconst RESOURCE_ID = 'time_tracking'\nconst EXPECTED_TENANT_PARAM = 'expectedTenantId'\nconst TENANT_SCOPE_CHANGED_CODE = 'tenant_scope_changed'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [VIEW_FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n}\n\n/**\n * EP-42 \u2014 derived from the setting-key registry, so a contributed key is published in\n * the OpenAPI response shape instead of being invisible to it. Rebuilt per read for the\n * same reason the request schema is: a key may be registered after this module loads.\n */\nfunction settingsResponseSchema(): z.ZodTypeAny {\n return buildTimeTrackingSettingsSchema()\n}\n\ntype SettingsContext = {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string\n organizationId: string | null\n actorId: string\n translate: (key: string, fallback?: string) => string\n}\n\nasync function resolveSettingsContext(req: Request): Promise<SettingsContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })\n }\n const actorId =\n (typeof auth.sub === 'string' && auth.sub.trim().length > 0 && auth.sub) ||\n (typeof auth.userId === 'string' && auth.userId.trim().length > 0 && auth.userId) ||\n (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0 && auth.keyId) ||\n 'system'\n return {\n container,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n actorId,\n translate,\n }\n}\n\nfunction resolveConfigService(context: SettingsContext): ModuleConfigService {\n return context.container.resolve('moduleConfigService') as ModuleConfigService\n}\n\nasync function GET(req: Request) {\n try {\n const context = await resolveSettingsContext(req)\n const interceptors = await runTimesheetInterceptors({\n request: req,\n method: 'GET',\n scope: {\n container: context.container,\n userId: context.actorId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n tenantGlobal: true,\n },\n query: readSearchParamsRecord(req.url),\n })\n if (!interceptors.ok) return interceptors.response\n\n const settings = await readTimeTrackingSettings(resolveConfigService(context), {\n tenantId: context.tenantId,\n })\n return interceptors.session.respond(200, settings)\n } catch (err) {\n return errorResponse(err, 'staff.timesheets.settings.GET failed')\n }\n}\n\nfunction assertExpectedTenant(req: Request, context: SettingsContext): void {\n const expectedTenantId = new URL(req.url).searchParams.get(EXPECTED_TENANT_PARAM)\n if (!expectedTenantId || expectedTenantId === context.tenantId) return\n throw new CrudHttpError(409, {\n error: context.translate(\n 'staff.errors.tenantScopeChanged',\n 'The selected tenant changed before the request was processed.',\n ),\n code: TENANT_SCOPE_CHANGED_CODE,\n })\n}\n\nasync function PUT(req: Request) {\n try {\n const context = await resolveSettingsContext(req)\n assertExpectedTenant(req, context)\n // One lookup through the module's single RBAC authority, answering both\n // questions this route asks: may the caller manage settings, and what grant\n // list do the interceptors and the mutation-guard registry gate on. The\n // decision is checked unconditionally and fails closed; the list is plumbing\n // and is empty \u2014 never absent \u2014 when RBAC could not produce it.\n const access = await resolveFeatureAccess(context.container, context.actorId || null, [MANAGE_FEATURE], {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n if (!access.allowed) {\n throw forbidden(context.translate('staff.errors.forbidden', 'Forbidden'))\n }\n const grantedFeatures = access.grantedFeatures\n\n const interceptors = await runTimesheetInterceptors({\n request: req,\n method: 'PUT',\n scope: {\n container: context.container,\n userId: context.actorId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userFeatures: grantedFeatures,\n tenantGlobal: true,\n },\n body: await readJsonSafe<Record<string, unknown>>(req, {}),\n })\n if (!interceptors.ok) return interceptors.response\n const { session } = interceptors\n\n const settingsSchema = buildTimeTrackingSettingsSchema()\n const requestBody = await preserveOmittedSupportSettings(\n resolveConfigService(context),\n { tenantId: context.tenantId },\n session.body,\n )\n const parsed = settingsSchema.parse(requestBody)\n\n const guardResult = await runRouteMutationGuards({\n container: context.container,\n req,\n auth: {\n userId: context.actorId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userFeatures: grantedFeatures,\n },\n input: {\n resourceKind: RESOURCE_KIND,\n resourceId: RESOURCE_ID,\n operation: 'update',\n mutationPayload: parsed,\n },\n })\n if (!guardResult.ok) return guardResult.response\n\n const effective = guardResult.modifiedPayload\n ? settingsSchema.parse({ ...parsed, ...guardResult.modifiedPayload })\n : parsed\n\n const settings: TimeTrackingSettings = await writeTimeTrackingSettings(\n resolveConfigService(context),\n { tenantId: context.tenantId },\n effective,\n )\n\n await guardResult.runAfterSuccess()\n\n void emitStaffEvent('staff.timesheets.time_tracking.settings_updated', {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n settings,\n }, { persistent: true }).catch((err) => {\n logger.error('staff.timesheets emit time_tracking.settings_updated failed', { err })\n })\n\n return session.respond(200, settings)\n } catch (err) {\n return errorResponse(err, 'staff.timesheets.settings.PUT failed')\n }\n}\n\nasync function errorResponse(err: unknown, message: string) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('staff.errors.invalid_request', 'Invalid request'), details: err.issues },\n { status: 400 },\n )\n }\n logger.error(message, { err })\n return NextResponse.json(\n { error: translate('staff.errors.internal', 'Internal server error') },\n { status: 500 },\n )\n}\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Read time tracking settings',\n tags: ['Staff'],\n get responses() {\n return [\n { status: 200, description: 'Tenant time tracking settings', schema: settingsResponseSchema() },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ]\n },\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Update time tracking settings',\n tags: ['Staff'],\n query: z.object({ [EXPECTED_TENANT_PARAM]: z.string().uuid().optional() }),\n get requestBody() {\n return { schema: buildTimeTrackingSettingsSchema() }\n },\n get responses() {\n return [\n { status: 200, description: 'Updated time tracking settings', schema: settingsResponseSchema() },\n ]\n },\n errors: [\n { status: 400, description: 'Invalid request body' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.settings.manage' },\n { status: 409, description: 'The resolved tenant differs from expectedTenantId' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Time tracking module settings',\n methods: {\n GET: getDoc,\n PUT: putDoc,\n },\n}\n\nexport { GET, PUT }\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,eAAe,WAAW,uBAAuB;AAC1D,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAG/B,SAAS,uCAAuC;AAChD,SAAS,oBAAoB;AAC7B,SAAS,0CAA0C;AACnD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,0BAA0B,CAAC;AAEnF,MAAM,eAAe;AACrB,MAAM,iBAAiB;AACvB,MAAM,gBAAgB,mCAAmC;AACzD,MAAM,cAAc;AACpB,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;AAE3B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,YAAY,EAAE;AAAA,EAC1D,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,cAAc,EAAE;AAC9D;AAOA,SAAS,yBAAuC;AAC9C,SAAO,gCAAgC;AACzC;AAUA,eAAe,uBAAuB,KAAwC;AAC5E,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,6BAA6B,cAAc,EAAE,CAAC;AAAA,EAChG;AACA,QAAM,UACH,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,KAAK,EAAE,SAAS,KAAK,KAAK,OACnE,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,KAAK,EAAE,SAAS,KAAK,KAAK,UACzE,OAAO,KAAK,UAAU,YAAY,KAAK,MAAM,KAAK,EAAE,SAAS,KAAK,KAAK,SACxE;AACF,SAAO;AAAA,IACL;AAAA,IACA,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,SAAS;AAAA,IAC9B;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,qBAAqB,SAA+C;AAC3E,SAAO,QAAQ,UAAU,QAAQ,qBAAqB;AACxD;AAEA,eAAe,IAAI,KAAc;AAC/B,MAAI;AACF,UAAM,UAAU,MAAM,uBAAuB,GAAG;AAChD,UAAM,eAAe,MAAM,yBAAyB;AAAA,MAClD,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,OAAO;AAAA,QACL,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,QACxB,cAAc;AAAA,MAChB;AAAA,MACA,OAAO,uBAAuB,IAAI,GAAG;AAAA,IACvC,CAAC;AACD,QAAI,CAAC,aAAa,GAAI,QAAO,aAAa;AAE1C,UAAM,WAAW,MAAM,yBAAyB,qBAAqB,OAAO,GAAG;AAAA,MAC7E,UAAU,QAAQ;AAAA,IACpB,CAAC;AACD,WAAO,aAAa,QAAQ,QAAQ,KAAK,QAAQ;AAAA,EACnD,SAAS,KAAK;AACZ,WAAO,cAAc,KAAK,sCAAsC;AAAA,EAClE;AACF;AAEA,SAAS,qBAAqB,KAAc,SAAgC;AAC1E,QAAM,mBAAmB,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa,IAAI,qBAAqB;AAChF,MAAI,CAAC,oBAAoB,qBAAqB,QAAQ,SAAU;AAChE,QAAM,IAAI,cAAc,KAAK;AAAA,IAC3B,OAAO,QAAQ;AAAA,MACb;AAAA,MACA;AAAA,IACF;AAAA,IACA,MAAM;AAAA,EACR,CAAC;AACH;AAEA,eAAe,IAAI,KAAc;AAC/B,MAAI;AACF,UAAM,UAAU,MAAM,uBAAuB,GAAG;AAChD,yBAAqB,KAAK,OAAO;AAMjC,UAAM,SAAS,MAAM,qBAAqB,QAAQ,WAAW,QAAQ,WAAW,MAAM,CAAC,cAAc,GAAG;AAAA,MACtG,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,IAC1B,CAAC;AACD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,UAAU,QAAQ,UAAU,0BAA0B,WAAW,CAAC;AAAA,IAC1E;AACA,UAAM,kBAAkB,OAAO;AAE/B,UAAM,eAAe,MAAM,yBAAyB;AAAA,MAClD,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,OAAO;AAAA,QACL,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,QACxB,cAAc;AAAA,QACd,cAAc;AAAA,MAChB;AAAA,MACA,MAAM,MAAM,aAAsC,KAAK,CAAC,CAAC;AAAA,IAC3D,CAAC;AACD,QAAI,CAAC,aAAa,GAAI,QAAO,aAAa;AAC1C,UAAM,EAAE,QAAQ,IAAI;AAEpB,UAAM,iBAAiB,gCAAgC;AACvD,UAAM,cAAc,MAAM;AAAA,MACxB,qBAAqB,OAAO;AAAA,MAC5B,EAAE,UAAU,QAAQ,SAAS;AAAA,MAC7B,QAAQ;AAAA,IACV;AACA,UAAM,SAAS,eAAe,MAAM,WAAW;AAE/C,UAAM,cAAc,MAAM,uBAAuB;AAAA,MAC/C,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA,MAAM;AAAA,QACJ,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ;AAAA,QAClB,gBAAgB,QAAQ;AAAA,QACxB,cAAc;AAAA,MAChB;AAAA,MACA,OAAO;AAAA,QACL,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,iBAAiB;AAAA,MACnB;AAAA,IACF,CAAC;AACD,QAAI,CAAC,YAAY,GAAI,QAAO,YAAY;AAExC,UAAM,YAAY,YAAY,kBAC1B,eAAe,MAAM,EAAE,GAAG,QAAQ,GAAG,YAAY,gBAAgB,CAAC,IAClE;AAEJ,UAAM,WAAiC,MAAM;AAAA,MAC3C,qBAAqB,OAAO;AAAA,MAC5B,EAAE,UAAU,QAAQ,SAAS;AAAA,MAC7B;AAAA,IACF;AAEA,UAAM,YAAY,gBAAgB;AAElC,SAAK,eAAe,mDAAmD;AAAA,MACrE,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,MACxB;AAAA,IACF,GAAG,EAAE,YAAY,KAAK,CAAC,EAAE,MAAM,CAAC,QAAQ;AACtC,aAAO,MAAM,+DAA+D,EAAE,IAAI,CAAC;AAAA,IACrF,CAAC;AAED,WAAO,QAAQ,QAAQ,KAAK,QAAQ;AAAA,EACtC,SAAS,KAAK;AACZ,WAAO,cAAc,KAAK,sCAAsC;AAAA,EAClE;AACF;AAEA,eAAe,cAAc,KAAc,SAAiB;AAC1D,MAAI,gBAAgB,GAAG,GAAG;AACxB,WAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,EAC3D;AACA,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI,eAAe,EAAE,UAAU;AAC7B,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,gCAAgC,iBAAiB,GAAG,SAAS,IAAI,OAAO;AAAA,MAC3F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO,MAAM,SAAS,EAAE,IAAI,CAAC;AAC7B,SAAO,aAAa;AAAA,IAClB,EAAE,OAAO,UAAU,yBAAyB,uBAAuB,EAAE;AAAA,IACrE,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;AAEA,MAAM,SAA2B;AAAA,EAC/B,SAAS;AAAA,EACT,MAAM,CAAC,OAAO;AAAA,EACd,IAAI,YAAY;AACd,WAAO;AAAA,MACL,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,uBAAuB,EAAE;AAAA,MAC9F,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,IACtF;AAAA,EACF;AACF;AAEA,MAAM,SAA2B;AAAA,EAC/B,SAAS;AAAA,EACT,MAAM,CAAC,OAAO;AAAA,EACd,OAAO,EAAE,OAAO,EAAE,CAAC,qBAAqB,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAAA,EACzE,IAAI,cAAc;AAChB,WAAO,EAAE,QAAQ,gCAAgC,EAAE;AAAA,EACrD;AAAA,EACA,IAAI,YAAY;AACd,WAAO;AAAA,MACL,EAAE,QAAQ,KAAK,aAAa,kCAAkC,QAAQ,uBAAuB,EAAE;AAAA,IACjG;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,uBAAuB;AAAA,IACnD,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,IAC3C,EAAE,QAAQ,KAAK,aAAa,2CAA2C;AAAA,IACvE,EAAE,QAAQ,KAAK,aAAa,oDAAoD;AAAA,EAClF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,122 @@
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 { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
10
+ import { createLogger } from "@open-mercato/shared/lib/logger";
11
+ import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
12
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
13
+ import { resolveFeatureAccess } from "../../../../lib/time-tracking/featureAccess.js";
14
+ import {
15
+ SUPPORT_ROUTING_RULE_RESOURCE_KIND,
16
+ staffSupportRoutingRuleCommandIds
17
+ } from "../../../../commands/support-routing-rules.js";
18
+ const logger = createLogger("staff").child({ component: "api/timesheets/support-routing-rules/fallback-project" });
19
+ const SETTINGS_FEATURE = "staff.timesheets.settings.manage";
20
+ const metadata = {
21
+ POST: { requireAuth: true, requireFeatures: [SETTINGS_FEATURE] }
22
+ };
23
+ async function POST(req) {
24
+ try {
25
+ const container = await createRequestContainer();
26
+ const auth = await getAuthFromRequest(req);
27
+ const { translate } = await resolveTranslations();
28
+ if (!auth) throw new CrudHttpError(401, { error: translate("staff.errors.unauthorized", "Unauthorized") });
29
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
30
+ const tenantId = scope?.tenantId ?? auth.tenantId ?? null;
31
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null;
32
+ if (!tenantId || !organizationId) {
33
+ throw new CrudHttpError(400, { error: translate("staff.errors.missingScope", "Missing tenant or organization scope.") });
34
+ }
35
+ const actorId = auth.sub ?? "";
36
+ const access = await resolveFeatureAccess(container, actorId, [SETTINGS_FEATURE], { tenantId, organizationId });
37
+ if (!access.allowed) throw forbidden(translate("staff.errors.forbidden", "Forbidden"));
38
+ const input = { tenantId, organizationId };
39
+ const guardResult = await runRouteMutationGuards({
40
+ container,
41
+ req,
42
+ auth: { userId: actorId, tenantId, organizationId, userFeatures: access.grantedFeatures },
43
+ input: {
44
+ resourceKind: SUPPORT_ROUTING_RULE_RESOURCE_KIND,
45
+ resourceId: null,
46
+ operation: "create",
47
+ mutationPayload: input
48
+ }
49
+ });
50
+ if (!guardResult.ok) return guardResult.response;
51
+ const routeCtx = {
52
+ container,
53
+ auth,
54
+ organizationScope: scope,
55
+ selectedOrganizationId: organizationId,
56
+ organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
57
+ request: req
58
+ };
59
+ const commandBus = container.resolve("commandBus");
60
+ const { result, logEntry } = await commandBus.execute(
61
+ staffSupportRoutingRuleCommandIds.createFallbackProject,
62
+ { input, ctx: routeCtx }
63
+ );
64
+ await guardResult.runAfterSuccess();
65
+ const response = NextResponse.json(
66
+ { ruleId: result?.ruleId ?? null, timeProjectId: result?.timeProjectId ?? null, created: result?.created === true },
67
+ { status: result?.created ? 201 : 200 }
68
+ );
69
+ if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
70
+ response.headers.set(
71
+ "x-om-operation",
72
+ serializeOperationMetadata({
73
+ id: logEntry.id,
74
+ undoToken: logEntry.undoToken,
75
+ commandId: logEntry.commandId,
76
+ actionLabel: logEntry.actionLabel ?? null,
77
+ resourceKind: logEntry.resourceKind ?? SUPPORT_ROUTING_RULE_RESOURCE_KIND,
78
+ resourceId: logEntry.resourceId ?? result?.ruleId ?? null,
79
+ executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : void 0
80
+ })
81
+ );
82
+ }
83
+ return response;
84
+ } catch (err) {
85
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
86
+ const interceptorRejection = getCommandInterceptorHttpRejection(err);
87
+ if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
88
+ const { translate } = await resolveTranslations();
89
+ logger.error("staff.support.fallback-project POST failed", { err });
90
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.support_fallback_project_failed" });
91
+ return NextResponse.json({ error: translate("staff.errors.internal", "Internal server error") }, { status: 500 });
92
+ }
93
+ }
94
+ const responseSchema = z.object({
95
+ ruleId: z.string().uuid().nullable(),
96
+ timeProjectId: z.string().uuid().nullable(),
97
+ created: z.boolean()
98
+ });
99
+ const openApi = {
100
+ tag: "Staff",
101
+ summary: "Create the support fallback project",
102
+ methods: {
103
+ POST: {
104
+ summary: "Create the support fallback project and rule",
105
+ description: '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`.',
106
+ responses: [
107
+ { status: 201, description: "Fallback project and rule created", schema: responseSchema },
108
+ { status: 200, description: "A fallback rule already existed", schema: responseSchema }
109
+ ],
110
+ errors: [
111
+ { status: 401, description: "Unauthorized" },
112
+ { status: 403, description: "Missing staff.timesheets.settings.manage" }
113
+ ]
114
+ }
115
+ }
116
+ };
117
+ export {
118
+ POST,
119
+ metadata,
120
+ openApi
121
+ };
122
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CrudHttpError, forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveFeatureAccess } from '../../../../lib/time-tracking/featureAccess'\nimport {\n SUPPORT_ROUTING_RULE_RESOURCE_KIND,\n staffSupportRoutingRuleCommandIds,\n type SupportFallbackProjectResult,\n} from '../../../../commands/support-routing-rules'\nimport type { StaffSupportFallbackProjectCreateInput } from '../../../../data/validators'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/support-routing-rules/fallback-project' })\n\nconst SETTINGS_FEATURE = 'staff.timesheets.settings.manage'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [SETTINGS_FEATURE] },\n}\n\nexport async function POST(req: Request): Promise<Response> {\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!tenantId || !organizationId) {\n throw new CrudHttpError(400, { error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.') })\n }\n\n const actorId = auth.sub ?? ''\n const access = await resolveFeatureAccess(container, actorId, [SETTINGS_FEATURE], { tenantId, organizationId })\n if (!access.allowed) throw forbidden(translate('staff.errors.forbidden', 'Forbidden'))\n\n const input: StaffSupportFallbackProjectCreateInput = { tenantId, organizationId }\n const guardResult = await runRouteMutationGuards({\n container,\n req,\n auth: { userId: actorId, tenantId, organizationId, userFeatures: access.grantedFeatures },\n input: {\n resourceKind: SUPPORT_ROUTING_RULE_RESOURCE_KIND,\n resourceId: null,\n operation: 'create',\n mutationPayload: input,\n },\n })\n if (!guardResult.ok) return guardResult.response\n\n const routeCtx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<StaffSupportFallbackProjectCreateInput, SupportFallbackProjectResult>(\n staffSupportRoutingRuleCommandIds.createFallbackProject,\n { input, ctx: routeCtx },\n )\n await guardResult.runAfterSuccess()\n const response = NextResponse.json(\n { ruleId: result?.ruleId ?? null, timeProjectId: result?.timeProjectId ?? null, created: result?.created === true },\n { status: result?.created ? 201 : 200 },\n )\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? SUPPORT_ROUTING_RULE_RESOURCE_KIND,\n resourceId: logEntry.resourceId ?? result?.ruleId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n }),\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n const { translate } = await resolveTranslations()\n logger.error('staff.support.fallback-project POST failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.support_fallback_project_failed' })\n return NextResponse.json({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })\n }\n}\n\nconst responseSchema = z.object({\n ruleId: z.string().uuid().nullable(),\n timeProjectId: z.string().uuid().nullable(),\n created: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Create the support fallback project',\n methods: {\n POST: {\n summary: 'Create the support fallback project and rule',\n description:\n '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`.',\n responses: [\n { status: 201, description: 'Fallback project and rule created', schema: responseSchema },\n { status: 200, description: 'A fallback rule already existed', schema: responseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.settings.manage' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }