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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -17,7 +17,7 @@ import { User } from "@open-mercato/core/modules/auth/data/entities";
17
17
  import { StaffTimeTaskComment } from "../../../../../data/entities.js";
18
18
  import {
19
19
  staffTimeTaskCommentCreateSchema,
20
- staffTimeTaskCommentUpdateSchema
20
+ staffTimeTaskCommentUpdateRequestSchema
21
21
  } from "../../../../../data/validators.js";
22
22
  import { staffTimeTaskCommentCommandIds } from "../../../../../commands/timesheets-task-comments.js";
23
23
  import { STAFF_TIME_TRACKING_RESOURCE_KINDS } from "../../../../guards.js";
@@ -46,6 +46,8 @@ const commentItemSchema = z.object({
46
46
  authorUserId: z.string().uuid().nullable(),
47
47
  authorName: z.string().nullable(),
48
48
  authorEmail: z.string().nullable(),
49
+ visibility: z.enum(["internal", "customer"]),
50
+ authorKind: z.enum(["staff", "customer"]),
49
51
  createdAt: z.string().nullable(),
50
52
  updatedAt: z.string().nullable()
51
53
  });
@@ -62,8 +64,16 @@ const createResponseSchema = z.object({
62
64
  authorUserId: z.string().uuid().nullable()
63
65
  });
64
66
  const okResponseSchema = z.object({ ok: z.boolean() });
65
- const createBodySchema = z.object({ body: z.string().trim().min(1).max(5e3) });
66
- const updateBodySchema = z.object({ id: z.string().uuid(), body: z.string().trim().min(1).max(5e3) });
67
+ const visibilitySchema = z.enum(["internal", "customer"]);
68
+ const createBodySchema = z.object({
69
+ body: z.string().trim().min(1).max(5e3),
70
+ visibility: visibilitySchema.optional()
71
+ });
72
+ const updateBodySchema = z.object({
73
+ id: z.string().uuid(),
74
+ body: z.string().trim().min(1).max(5e3).optional(),
75
+ visibility: visibilitySchema.optional()
76
+ });
67
77
  function extractTaskId(req) {
68
78
  try {
69
79
  const match = new URL(req.url).pathname.match(/\/tasks\/([^/]+)\/comments/);
@@ -150,13 +160,17 @@ async function resolveAuthorMeta(route, authorUserIds) {
150
160
  function serializeComment(comment, authors) {
151
161
  const authorUserId = comment.authorUserId ?? null;
152
162
  const meta = authorUserId ? authors.get(authorUserId) ?? null : null;
163
+ const authorKind = comment.authorKind === "customer" ? "customer" : "staff";
164
+ const customerName = comment.authorDisplaySnapshot?.displayName ?? null;
153
165
  return {
154
166
  id: comment.id,
155
167
  taskId: comment.taskId,
156
168
  body: comment.body,
157
169
  authorUserId,
158
- authorName: meta?.name ?? null,
159
- authorEmail: meta?.email ?? null,
170
+ authorName: authorKind === "customer" ? customerName : meta?.name ?? null,
171
+ authorEmail: authorKind === "customer" ? null : meta?.email ?? null,
172
+ visibility: comment.visibility === "customer" ? "customer" : "internal",
173
+ authorKind,
160
174
  createdAt: comment.createdAt ? comment.createdAt.toISOString() : null,
161
175
  updatedAt: comment.updatedAt ? comment.updatedAt.toISOString() : null
162
176
  };
@@ -256,7 +270,7 @@ async function POST(req) {
256
270
  const parsedBody = createBodySchema.parse(raw ?? {});
257
271
  const input = parseScopedCommandInput(
258
272
  staffTimeTaskCommentCreateSchema,
259
- { taskId: route.taskId, body: parsedBody.body },
273
+ { taskId: route.taskId, body: parsedBody.body, visibility: parsedBody.visibility ?? "internal" },
260
274
  route.ctx,
261
275
  route.translate
262
276
  );
@@ -289,7 +303,11 @@ async function PUT(req) {
289
303
  const raw = await readJsonSafe(req, {});
290
304
  const parsedBody = updateBodySchema.parse(raw ?? {});
291
305
  await requireCommentOnTask(route, parsedBody.id);
292
- const input = staffTimeTaskCommentUpdateSchema.parse({ id: parsedBody.id, body: parsedBody.body });
306
+ const input = staffTimeTaskCommentUpdateRequestSchema.parse({
307
+ id: parsedBody.id,
308
+ body: parsedBody.body,
309
+ visibility: parsedBody.visibility
310
+ });
293
311
  const guardResult = await runGuards(route, "update", input.id, input);
294
312
  if (!guardResult.ok) return guardResult.response;
295
313
  const commandBus = route.ctx.container.resolve("commandBus");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/staff/api/timesheets/tasks/%5Bid%5D/comments/route.ts"],
4
- "sourcesContent": ["/**\n * Task comments (T3.4b) \u2014 the \"Komentarze (2)\" thread in the task drawer of\n * screen 7: avatar, author name, timestamp, body, and a composer that posts on\n * \u2318\u21B5.\n *\n * Hand-written rather than `makeCrudRoute`, for one reason that decides\n * everything else: **a caller who cannot see the task's project must not be able\n * to tell that the task exists.** A CRUD list would answer such a request with\n * an empty page \u2014 which is a different response from \"no such task\" and\n * therefore an existence oracle for every task id in the tenant. Here every\n * method funnels through the same `requireAccessibleTask` call before it touches\n * a comment, so \"not yours\" and \"does not exist\" produce one and the same 404\n * body, byte for byte.\n *\n * Because the route is hand-written it wires `runRouteMutationGuards` itself\n * (`packages/core/AGENTS.md` \u00A7 API Routes) \u2014 the same guard set every\n * `makeCrudRoute` write runs \u2014 and the writes go through the task-comment\n * commands so audit, undo, events and indexing stay consistent.\n *\n * Two more things the thread depends on:\n *\n * - **Oldest first.** Screen 7 reads as a conversation, so the list sorts by\n * `created_at ASC` with an id tiebreak, not the newest-first default a grid\n * would want.\n * - **The author's display name is resolved for rendering**, reusing the same\n * `User` lookup `api/comments.ts` uses for the team-member timeline rather\n * than inventing a cross-module join. A name that cannot be resolved degrades\n * to `null` and the thread still renders \u2014 the comment is the record, the\n * name is decoration.\n */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { CrudHttpError, 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 { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { StaffTimeTaskComment } from '../../../../../data/entities'\nimport {\n staffTimeTaskCommentCreateSchema,\n staffTimeTaskCommentUpdateSchema,\n} from '../../../../../data/validators'\nimport { staffTimeTaskCommentCommandIds } from '../../../../../commands/timesheets-task-comments'\nimport { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../../../guards'\nimport {\n loadTagProjectAccess as loadTaskProjectAccess,\n requireAccessibleTask,\n type TagAssignmentScope as TaskAccessScope,\n} from '../../../tags/access'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/tasks/comments' })\n\nconst RESOURCE_KIND = STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTaskComment\n\nconst VIEW_FEATURE = 'staff.timesheets.tasks.view'\nconst MANAGE_FEATURE = 'staff.timesheets.tasks.manage'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [VIEW_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n}\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n})\n\nconst commentItemSchema = z.object({\n id: z.string().uuid(),\n taskId: z.string().uuid(),\n body: z.string(),\n authorUserId: z.string().uuid().nullable(),\n authorName: z.string().nullable(),\n authorEmail: z.string().nullable(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n})\n\nconst listResponseSchema = z.object({\n items: z.array(commentItemSchema),\n total: z.number().int(),\n page: z.number().int(),\n pageSize: z.number().int(),\n totalPages: z.number().int(),\n})\n\nconst createResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n taskId: z.string().uuid(),\n authorUserId: z.string().uuid().nullable(),\n})\n\nconst okResponseSchema = z.object({ ok: z.boolean() })\n\nconst createBodySchema = z.object({ body: z.string().trim().min(1).max(5000) })\nconst updateBodySchema = z.object({ id: z.string().uuid(), body: z.string().trim().min(1).max(5000) })\n\ntype RouteContext = {\n ctx: CommandRuntimeContext\n scope: TaskAccessScope\n translate: (key: string, fallback?: string) => string\n taskId: string\n}\n\n/**\n * The task id comes from the path segment. Read from the URL rather than the\n * handler's `params` argument so the route behaves identically under the app's\n * catch-all dispatcher and under a direct call in tests.\n */\nfunction extractTaskId(req: Request): string | null {\n try {\n const match = new URL(req.url).pathname.match(/\\/tasks\\/([^/]+)\\/comments/)\n const id = match?.[1] ? decodeURIComponent(match[1]) : null\n return id && id.length > 0 ? id : null\n } catch {\n return null\n }\n}\n\nasync function buildRouteContext(req: Request): Promise<RouteContext> {\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 organizationScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope,\n selectedOrganizationId: resolveSingleOrganizationIdOrDeny(organizationScope, auth) ?? null,\n organizationIds: organizationScope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const tenantId = auth.tenantId ?? null\n const organizationId = ctx.selectedOrganizationId ?? auth.orgId ?? null\n const userId = auth.sub ?? null\n if (!tenantId || !organizationId || !userId) {\n throw new CrudHttpError(400, {\n error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.'),\n })\n }\n\n const taskId = extractTaskId(req)\n if (!taskId) {\n throw new CrudHttpError(400, {\n error: translate('staff.time_tracking.taskComments.errors.taskIdRequired', 'Task id is required.'),\n })\n }\n\n return { ctx, scope: { tenantId, organizationId, userId }, translate, taskId }\n}\n\n/**\n * The single gate every method passes through. Resolving project access before\n * the task is loaded \u2014 and answering both misses with one body \u2014 is what keeps\n * the endpoint from confirming that a task the caller may not see exists.\n */\nasync function requireTaskAccess(route: RouteContext): Promise<void> {\n const access = await loadTaskProjectAccess(route.ctx.container, route.scope)\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n await requireAccessibleTask(em, route.taskId, route.scope, access, route.translate)\n}\n\nfunction commentNotFoundError(translate: (key: string, fallback?: string) => string): CrudHttpError {\n return new CrudHttpError(404, {\n error: translate('staff.time_tracking.taskComments.errors.notFound', 'Comment not found or not accessible.'),\n })\n}\n\n/**\n * A comment addressed through another task's URL is refused exactly like a\n * comment that does not exist, so the path segment cannot be swapped to reach a\n * thread the caller has no route to.\n */\nasync function requireCommentOnTask(route: RouteContext, commentId: string): Promise<StaffTimeTaskComment> {\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n const comment = await em.findOne(StaffTimeTaskComment, {\n id: commentId,\n taskId: route.taskId,\n tenantId: route.scope.tenantId,\n organizationId: route.scope.organizationId,\n deletedAt: null,\n })\n if (!comment) throw commentNotFoundError(route.translate)\n return comment\n}\n\ntype AuthorMeta = { name: string | null; email: string | null }\n\nasync function resolveAuthorMeta(\n route: RouteContext,\n authorUserIds: string[],\n): Promise<Map<string, AuthorMeta>> {\n const map = new Map<string, AuthorMeta>()\n const unique = Array.from(new Set(authorUserIds))\n if (unique.length === 0) return map\n try {\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n const users = await findWithDecryption(\n em,\n User,\n { id: { $in: unique } },\n undefined,\n { tenantId: route.scope.tenantId, organizationId: route.scope.organizationId },\n )\n for (const user of users) {\n const name = typeof user.name === 'string' && user.name.trim().length > 0 ? user.name.trim() : null\n map.set(user.id, { name, email: user.email ?? null })\n }\n } catch (err) {\n // A thread that renders without display names is still a usable thread.\n logger.warn('staff.timesheets.task comments failed to resolve author metadata', { err })\n }\n return map\n}\n\nfunction serializeComment(comment: StaffTimeTaskComment, authors: Map<string, AuthorMeta>) {\n const authorUserId = comment.authorUserId ?? null\n const meta = authorUserId ? authors.get(authorUserId) ?? null : null\n return {\n id: comment.id,\n taskId: comment.taskId,\n body: comment.body,\n authorUserId,\n authorName: meta?.name ?? null,\n authorEmail: meta?.email ?? null,\n createdAt: comment.createdAt ? comment.createdAt.toISOString() : null,\n updatedAt: comment.updatedAt ? comment.updatedAt.toISOString() : null,\n }\n}\n\nasync function runGuards(\n route: RouteContext,\n operation: 'create' | 'update' | 'delete',\n resourceId: string | null,\n mutationPayload: Record<string, unknown> | null,\n) {\n return runRouteMutationGuards({\n container: route.ctx.container,\n req: route.ctx.request as Request,\n auth: {\n userId: route.scope.userId,\n tenantId: route.scope.tenantId,\n organizationId: route.scope.organizationId,\n },\n input: {\n resourceKind: RESOURCE_KIND,\n resourceId,\n operation,\n mutationPayload,\n },\n })\n}\n\nfunction withOperationHeader(\n response: NextResponse,\n logEntry: {\n id?: string\n undoToken?: string | null\n commandId?: string\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n createdAt?: Date | string | null\n } | null | undefined,\n fallbackResourceId: string,\n): NextResponse {\n if (!logEntry?.undoToken || !logEntry.id || !logEntry.commandId) return response\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 ?? RESOURCE_KIND,\n resourceId: logEntry.resourceId ?? fallbackResourceId,\n executedAt:\n logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n return response\n}\n\nasync function errorResponse(err: unknown, message: string): Promise<Response> {\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({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })\n}\n\nexport async function GET(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const url = new URL(req.url)\n const query = listQuerySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n const [comments, total] = await em.findAndCount(\n StaffTimeTaskComment,\n {\n taskId: route.taskId,\n tenantId: route.scope.tenantId,\n organizationId: route.scope.organizationId,\n deletedAt: null,\n },\n {\n // Oldest first \u2014 the drawer reads the thread top to bottom.\n orderBy: { createdAt: 'asc', id: 'asc' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n )\n\n const authors = await resolveAuthorMeta(\n route,\n comments.map((comment) => comment.authorUserId).filter((id): id is string => typeof id === 'string'),\n )\n\n return NextResponse.json({\n items: comments.map((comment) => serializeComment(comment, authors)),\n total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: query.pageSize > 0 ? Math.ceil(total / query.pageSize) : 0,\n })\n } catch (err) {\n return errorResponse(err, 'staff.timesheets.tasks.comments.GET failed')\n }\n}\n\nexport async function POST(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const raw = await readJsonSafe(req, {})\n const parsedBody = createBodySchema.parse(raw ?? {})\n // `authorUserId` is never forwarded: whatever the client sent is dropped\n // here and the command stamps the authenticated caller instead.\n const input = parseScopedCommandInput(\n staffTimeTaskCommentCreateSchema,\n { taskId: route.taskId, body: parsedBody.body },\n route.ctx,\n route.translate,\n )\n\n const guardResult = await runGuards(route, 'create', route.taskId, input as unknown as Record<string, unknown>)\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = route.ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n typeof input,\n { commentId: string; taskId: string; authorUserId: string | null }\n >(staffTimeTaskCommentCommandIds.create, { input, ctx: route.ctx })\n\n await guardResult.runAfterSuccess()\n\n const response = NextResponse.json(\n {\n id: result?.commentId ?? null,\n taskId: route.taskId,\n authorUserId: result?.authorUserId ?? null,\n },\n { status: 201 },\n )\n return withOperationHeader(response, logEntry, result?.commentId ?? route.taskId)\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return errorResponse(err, 'staff.timesheets.tasks.comments.POST failed')\n }\n}\n\nexport async function PUT(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const raw = await readJsonSafe(req, {})\n const parsedBody = updateBodySchema.parse(raw ?? {})\n await requireCommentOnTask(route, parsedBody.id)\n\n const input = staffTimeTaskCommentUpdateSchema.parse({ id: parsedBody.id, body: parsedBody.body })\n\n const guardResult = await runGuards(route, 'update', input.id, input as unknown as Record<string, unknown>)\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = route.ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<typeof input, { commentId: string; taskId: string }>(\n staffTimeTaskCommentCommandIds.update,\n { input, ctx: route.ctx },\n )\n\n await guardResult.runAfterSuccess()\n\n return withOperationHeader(NextResponse.json({ ok: true }), logEntry, input.id)\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return errorResponse(err, 'staff.timesheets.tasks.comments.PUT failed')\n }\n}\n\nexport async function DELETE(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const url = new URL(req.url)\n const raw = await readJsonSafe(req, {})\n const rawId = url.searchParams.get('id') ?? (raw as { id?: unknown } | null)?.id\n const id = z.string().uuid().parse(typeof rawId === 'string' ? rawId : undefined)\n await requireCommentOnTask(route, id)\n\n const guardResult = await runGuards(route, 'delete', id, { id })\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = route.ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<{ id: string }, { commentId: string; taskId: string }>(\n staffTimeTaskCommentCommandIds.delete,\n { input: { id }, ctx: route.ctx },\n )\n\n await guardResult.runAfterSuccess()\n\n return withOperationHeader(NextResponse.json({ ok: true }), logEntry, id)\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return errorResponse(err, 'staff.timesheets.tasks.comments.DELETE failed')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Task comments',\n methods: {\n GET: {\n summary: 'List task comments',\n description:\n 'Returns the task comment thread oldest-first, with the author display name resolved for rendering. Answers 404 \u2014 identical to a task that does not exist \u2014 when the task belongs to a project the caller cannot see.',\n query: listQuerySchema,\n responses: [{ status: 200, description: 'Comment thread', schema: listResponseSchema }],\n errors: [\n { status: 400, description: 'Missing task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.tasks.view' },\n { status: 404, description: 'Task not found or not accessible' },\n ],\n },\n POST: {\n summary: 'Post a task comment',\n description:\n 'Adds a comment to the task. The author is stamped from the authenticated caller \u2014 an authorUserId supplied by the client is ignored.',\n requestBody: { contentType: 'application/json', schema: createBodySchema },\n responses: [{ status: 201, description: 'Comment created', schema: createResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload, missing task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.tasks.manage' },\n { status: 404, description: 'Task not found or not accessible' },\n ],\n },\n PUT: {\n summary: 'Edit a task comment',\n description:\n 'Updates a comment body. Only the comment author may edit, unless the caller holds staff.timesheets.manage_all. Honours the optimistic-lock header.',\n requestBody: { contentType: 'application/json', schema: updateBodySchema },\n responses: [{ status: 200, description: 'Comment updated', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload, missing task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Not the author and missing staff.timesheets.manage_all' },\n { status: 404, description: 'Task or comment not found or not accessible' },\n { status: 409, description: 'Optimistic lock conflict' },\n ],\n },\n DELETE: {\n summary: 'Delete a task comment',\n description:\n 'Soft-deletes a comment so its audit trail survives. Only the comment author may delete, unless the caller holds staff.timesheets.manage_all.',\n requestBody: { contentType: 'application/json', schema: z.object({ id: z.string().uuid() }) },\n responses: [{ status: 200, description: 'Comment deleted', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Missing comment id, task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Not the author and missing staff.timesheets.manage_all' },\n { status: 404, description: 'Task or comment not found or not accessible' },\n { status: 409, description: 'Optimistic lock conflict' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Task comments (T3.4b) \u2014 the \"Komentarze (2)\" thread in the task drawer of\n * screen 7: avatar, author name, timestamp, body, and a composer that posts on\n * \u2318\u21B5.\n *\n * Hand-written rather than `makeCrudRoute`, for one reason that decides\n * everything else: **a caller who cannot see the task's project must not be able\n * to tell that the task exists.** A CRUD list would answer such a request with\n * an empty page \u2014 which is a different response from \"no such task\" and\n * therefore an existence oracle for every task id in the tenant. Here every\n * method funnels through the same `requireAccessibleTask` call before it touches\n * a comment, so \"not yours\" and \"does not exist\" produce one and the same 404\n * body, byte for byte.\n *\n * Because the route is hand-written it wires `runRouteMutationGuards` itself\n * (`packages/core/AGENTS.md` \u00A7 API Routes) \u2014 the same guard set every\n * `makeCrudRoute` write runs \u2014 and the writes go through the task-comment\n * commands so audit, undo, events and indexing stay consistent.\n *\n * Two more things the thread depends on:\n *\n * - **Oldest first.** Screen 7 reads as a conversation, so the list sorts by\n * `created_at ASC` with an id tiebreak, not the newest-first default a grid\n * would want.\n * - **The author's display name is resolved for rendering**, reusing the same\n * `User` lookup `api/comments.ts` uses for the team-member timeline rather\n * than inventing a cross-module join. A name that cannot be resolved degrades\n * to `null` and the thread still renders \u2014 the comment is the record, the\n * name is decoration.\n */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { CrudHttpError, 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 { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { StaffTimeTaskComment } from '../../../../../data/entities'\nimport {\n staffTimeTaskCommentCreateSchema,\n staffTimeTaskCommentUpdateRequestSchema,\n} from '../../../../../data/validators'\nimport { staffTimeTaskCommentCommandIds } from '../../../../../commands/timesheets-task-comments'\nimport { STAFF_TIME_TRACKING_RESOURCE_KINDS } from '../../../../guards'\nimport {\n loadTagProjectAccess as loadTaskProjectAccess,\n requireAccessibleTask,\n type TagAssignmentScope as TaskAccessScope,\n} from '../../../tags/access'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/tasks/comments' })\n\nconst RESOURCE_KIND = STAFF_TIME_TRACKING_RESOURCE_KINDS.timeTaskComment\n\nconst VIEW_FEATURE = 'staff.timesheets.tasks.view'\nconst MANAGE_FEATURE = 'staff.timesheets.tasks.manage'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [VIEW_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n PUT: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [MANAGE_FEATURE] },\n}\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n})\n\nconst commentItemSchema = z.object({\n id: z.string().uuid(),\n taskId: z.string().uuid(),\n body: z.string(),\n authorUserId: z.string().uuid().nullable(),\n authorName: z.string().nullable(),\n authorEmail: z.string().nullable(),\n visibility: z.enum(['internal', 'customer']),\n authorKind: z.enum(['staff', 'customer']),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n})\n\nconst listResponseSchema = z.object({\n items: z.array(commentItemSchema),\n total: z.number().int(),\n page: z.number().int(),\n pageSize: z.number().int(),\n totalPages: z.number().int(),\n})\n\nconst createResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n taskId: z.string().uuid(),\n authorUserId: z.string().uuid().nullable(),\n})\n\nconst okResponseSchema = z.object({ ok: z.boolean() })\n\nconst visibilitySchema = z.enum(['internal', 'customer'])\nconst createBodySchema = z.object({\n body: z.string().trim().min(1).max(5000),\n visibility: visibilitySchema.optional(),\n})\nconst updateBodySchema = z.object({\n id: z.string().uuid(),\n body: z.string().trim().min(1).max(5000).optional(),\n visibility: visibilitySchema.optional(),\n})\n\ntype RouteContext = {\n ctx: CommandRuntimeContext\n scope: TaskAccessScope\n translate: (key: string, fallback?: string) => string\n taskId: string\n}\n\n/**\n * The task id comes from the path segment. Read from the URL rather than the\n * handler's `params` argument so the route behaves identically under the app's\n * catch-all dispatcher and under a direct call in tests.\n */\nfunction extractTaskId(req: Request): string | null {\n try {\n const match = new URL(req.url).pathname.match(/\\/tasks\\/([^/]+)\\/comments/)\n const id = match?.[1] ? decodeURIComponent(match[1]) : null\n return id && id.length > 0 ? id : null\n } catch {\n return null\n }\n}\n\nasync function buildRouteContext(req: Request): Promise<RouteContext> {\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 organizationScope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope,\n selectedOrganizationId: resolveSingleOrganizationIdOrDeny(organizationScope, auth) ?? null,\n organizationIds: organizationScope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const tenantId = auth.tenantId ?? null\n const organizationId = ctx.selectedOrganizationId ?? auth.orgId ?? null\n const userId = auth.sub ?? null\n if (!tenantId || !organizationId || !userId) {\n throw new CrudHttpError(400, {\n error: translate('staff.errors.missingScope', 'Missing tenant or organization scope.'),\n })\n }\n\n const taskId = extractTaskId(req)\n if (!taskId) {\n throw new CrudHttpError(400, {\n error: translate('staff.time_tracking.taskComments.errors.taskIdRequired', 'Task id is required.'),\n })\n }\n\n return { ctx, scope: { tenantId, organizationId, userId }, translate, taskId }\n}\n\n/**\n * The single gate every method passes through. Resolving project access before\n * the task is loaded \u2014 and answering both misses with one body \u2014 is what keeps\n * the endpoint from confirming that a task the caller may not see exists.\n */\nasync function requireTaskAccess(route: RouteContext): Promise<void> {\n const access = await loadTaskProjectAccess(route.ctx.container, route.scope)\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n await requireAccessibleTask(em, route.taskId, route.scope, access, route.translate)\n}\n\nfunction commentNotFoundError(translate: (key: string, fallback?: string) => string): CrudHttpError {\n return new CrudHttpError(404, {\n error: translate('staff.time_tracking.taskComments.errors.notFound', 'Comment not found or not accessible.'),\n })\n}\n\n/**\n * A comment addressed through another task's URL is refused exactly like a\n * comment that does not exist, so the path segment cannot be swapped to reach a\n * thread the caller has no route to.\n */\nasync function requireCommentOnTask(route: RouteContext, commentId: string): Promise<StaffTimeTaskComment> {\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n const comment = await em.findOne(StaffTimeTaskComment, {\n id: commentId,\n taskId: route.taskId,\n tenantId: route.scope.tenantId,\n organizationId: route.scope.organizationId,\n deletedAt: null,\n })\n if (!comment) throw commentNotFoundError(route.translate)\n return comment\n}\n\ntype AuthorMeta = { name: string | null; email: string | null }\n\nasync function resolveAuthorMeta(\n route: RouteContext,\n authorUserIds: string[],\n): Promise<Map<string, AuthorMeta>> {\n const map = new Map<string, AuthorMeta>()\n const unique = Array.from(new Set(authorUserIds))\n if (unique.length === 0) return map\n try {\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n const users = await findWithDecryption(\n em,\n User,\n { id: { $in: unique } },\n undefined,\n { tenantId: route.scope.tenantId, organizationId: route.scope.organizationId },\n )\n for (const user of users) {\n const name = typeof user.name === 'string' && user.name.trim().length > 0 ? user.name.trim() : null\n map.set(user.id, { name, email: user.email ?? null })\n }\n } catch (err) {\n // A thread that renders without display names is still a usable thread.\n logger.warn('staff.timesheets.task comments failed to resolve author metadata', { err })\n }\n return map\n}\n\nfunction serializeComment(comment: StaffTimeTaskComment, authors: Map<string, AuthorMeta>) {\n const authorUserId = comment.authorUserId ?? null\n const meta = authorUserId ? authors.get(authorUserId) ?? null : null\n const authorKind = comment.authorKind === 'customer' ? 'customer' : 'staff'\n const customerName = comment.authorDisplaySnapshot?.displayName ?? null\n return {\n id: comment.id,\n taskId: comment.taskId,\n body: comment.body,\n authorUserId,\n authorName: authorKind === 'customer' ? customerName : meta?.name ?? null,\n authorEmail: authorKind === 'customer' ? null : meta?.email ?? null,\n visibility: comment.visibility === 'customer' ? 'customer' : 'internal',\n authorKind,\n createdAt: comment.createdAt ? comment.createdAt.toISOString() : null,\n updatedAt: comment.updatedAt ? comment.updatedAt.toISOString() : null,\n }\n}\n\nasync function runGuards(\n route: RouteContext,\n operation: 'create' | 'update' | 'delete',\n resourceId: string | null,\n mutationPayload: Record<string, unknown> | null,\n) {\n return runRouteMutationGuards({\n container: route.ctx.container,\n req: route.ctx.request as Request,\n auth: {\n userId: route.scope.userId,\n tenantId: route.scope.tenantId,\n organizationId: route.scope.organizationId,\n },\n input: {\n resourceKind: RESOURCE_KIND,\n resourceId,\n operation,\n mutationPayload,\n },\n })\n}\n\nfunction withOperationHeader(\n response: NextResponse,\n logEntry: {\n id?: string\n undoToken?: string | null\n commandId?: string\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n createdAt?: Date | string | null\n } | null | undefined,\n fallbackResourceId: string,\n): NextResponse {\n if (!logEntry?.undoToken || !logEntry.id || !logEntry.commandId) return response\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 ?? RESOURCE_KIND,\n resourceId: logEntry.resourceId ?? fallbackResourceId,\n executedAt:\n logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n return response\n}\n\nasync function errorResponse(err: unknown, message: string): Promise<Response> {\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({ error: translate('staff.errors.internal', 'Internal server error') }, { status: 500 })\n}\n\nexport async function GET(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const url = new URL(req.url)\n const query = listQuerySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n\n const em = (route.ctx.container.resolve('em') as EntityManager).fork()\n const [comments, total] = await em.findAndCount(\n StaffTimeTaskComment,\n {\n taskId: route.taskId,\n tenantId: route.scope.tenantId,\n organizationId: route.scope.organizationId,\n deletedAt: null,\n },\n {\n // Oldest first \u2014 the drawer reads the thread top to bottom.\n orderBy: { createdAt: 'asc', id: 'asc' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n )\n\n const authors = await resolveAuthorMeta(\n route,\n comments.map((comment) => comment.authorUserId).filter((id): id is string => typeof id === 'string'),\n )\n\n return NextResponse.json({\n items: comments.map((comment) => serializeComment(comment, authors)),\n total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: query.pageSize > 0 ? Math.ceil(total / query.pageSize) : 0,\n })\n } catch (err) {\n return errorResponse(err, 'staff.timesheets.tasks.comments.GET failed')\n }\n}\n\nexport async function POST(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const raw = await readJsonSafe(req, {})\n const parsedBody = createBodySchema.parse(raw ?? {})\n // `authorUserId` is never forwarded: whatever the client sent is dropped\n // here and the command stamps the authenticated caller instead.\n const input = parseScopedCommandInput(\n staffTimeTaskCommentCreateSchema,\n { taskId: route.taskId, body: parsedBody.body, visibility: parsedBody.visibility ?? 'internal' },\n route.ctx,\n route.translate,\n )\n\n const guardResult = await runGuards(route, 'create', route.taskId, input as unknown as Record<string, unknown>)\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = route.ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n typeof input,\n { commentId: string; taskId: string; authorUserId: string | null }\n >(staffTimeTaskCommentCommandIds.create, { input, ctx: route.ctx })\n\n await guardResult.runAfterSuccess()\n\n const response = NextResponse.json(\n {\n id: result?.commentId ?? null,\n taskId: route.taskId,\n authorUserId: result?.authorUserId ?? null,\n },\n { status: 201 },\n )\n return withOperationHeader(response, logEntry, result?.commentId ?? route.taskId)\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return errorResponse(err, 'staff.timesheets.tasks.comments.POST failed')\n }\n}\n\nexport async function PUT(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const raw = await readJsonSafe(req, {})\n const parsedBody = updateBodySchema.parse(raw ?? {})\n await requireCommentOnTask(route, parsedBody.id)\n\n const input = staffTimeTaskCommentUpdateRequestSchema.parse({\n id: parsedBody.id,\n body: parsedBody.body,\n visibility: parsedBody.visibility,\n })\n\n const guardResult = await runGuards(route, 'update', input.id, input as unknown as Record<string, unknown>)\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = route.ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<typeof input, { commentId: string; taskId: string }>(\n staffTimeTaskCommentCommandIds.update,\n { input, ctx: route.ctx },\n )\n\n await guardResult.runAfterSuccess()\n\n return withOperationHeader(NextResponse.json({ ok: true }), logEntry, input.id)\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return errorResponse(err, 'staff.timesheets.tasks.comments.PUT failed')\n }\n}\n\nexport async function DELETE(req: Request) {\n try {\n const route = await buildRouteContext(req)\n await requireTaskAccess(route)\n\n const url = new URL(req.url)\n const raw = await readJsonSafe(req, {})\n const rawId = url.searchParams.get('id') ?? (raw as { id?: unknown } | null)?.id\n const id = z.string().uuid().parse(typeof rawId === 'string' ? rawId : undefined)\n await requireCommentOnTask(route, id)\n\n const guardResult = await runGuards(route, 'delete', id, { id })\n if (!guardResult.ok) return guardResult.response\n\n const commandBus = route.ctx.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<{ id: string }, { commentId: string; taskId: string }>(\n staffTimeTaskCommentCommandIds.delete,\n { input: { id }, ctx: route.ctx },\n )\n\n await guardResult.runAfterSuccess()\n\n return withOperationHeader(NextResponse.json({ ok: true }), logEntry, id)\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return errorResponse(err, 'staff.timesheets.tasks.comments.DELETE failed')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Task comments',\n methods: {\n GET: {\n summary: 'List task comments',\n description:\n 'Returns the task comment thread oldest-first, with the author display name resolved for rendering. Answers 404 \u2014 identical to a task that does not exist \u2014 when the task belongs to a project the caller cannot see.',\n query: listQuerySchema,\n responses: [{ status: 200, description: 'Comment thread', schema: listResponseSchema }],\n errors: [\n { status: 400, description: 'Missing task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.tasks.view' },\n { status: 404, description: 'Task not found or not accessible' },\n ],\n },\n POST: {\n summary: 'Post a task comment',\n description:\n 'Adds a comment to the task. The author is stamped from the authenticated caller \u2014 an authorUserId supplied by the client is ignored.',\n requestBody: { contentType: 'application/json', schema: createBodySchema },\n responses: [{ status: 201, description: 'Comment created', schema: createResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload, missing task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.tasks.manage' },\n { status: 404, description: 'Task not found or not accessible' },\n ],\n },\n PUT: {\n summary: 'Edit a task comment',\n description:\n 'Updates a comment body. Only the comment author may edit, unless the caller holds staff.timesheets.manage_all. Honours the optimistic-lock header.',\n requestBody: { contentType: 'application/json', schema: updateBodySchema },\n responses: [{ status: 200, description: 'Comment updated', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload, missing task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Not the author and missing staff.timesheets.manage_all' },\n { status: 404, description: 'Task or comment not found or not accessible' },\n { status: 409, description: 'Optimistic lock conflict' },\n ],\n },\n DELETE: {\n summary: 'Delete a task comment',\n description:\n 'Soft-deletes a comment so its audit trail survives. Only the comment author may delete, unless the caller holds staff.timesheets.manage_all.',\n requestBody: { contentType: 'application/json', schema: z.object({ id: z.string().uuid() }) },\n responses: [{ status: 200, description: 'Comment deleted', schema: okResponseSchema }],\n errors: [\n { status: 400, description: 'Missing comment id, task id or organization scope' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Not the author and missing staff.timesheets.manage_all' },\n { status: 404, description: 'Task or comment not found or not accessible' },\n { status: 409, description: 'Optimistic lock conflict' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AA8BA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAClD,SAAS,kCAAkC;AAE3C,SAAS,0CAA0C;AACnD,SAAS,eAAe,uBAAuB;AAC/C,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAE7B,SAAS,YAAY;AACrB,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,sCAAsC;AAC/C,SAAS,0CAA0C;AACnD;AAAA,EACE,wBAAwB;AAAA,EACxB;AAAA,OAEK;AAEP,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,gCAAgC,CAAC;AAEzF,MAAM,gBAAgB,mCAAmC;AAEzD,MAAM,eAAe;AACrB,MAAM,iBAAiB;AAEhB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,YAAY,EAAE;AAAA,EAC1D,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,cAAc,EAAE;AAAA,EAC7D,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,cAAc,EAAE;AAAA,EAC5D,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,cAAc,EAAE;AACjE;AAEA,MAAM,kBAAkB,EAAE,OAAO;AAAA,EAC/B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AACxD,CAAC;AAED,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,MAAM,EAAE,OAAO;AAAA,EACf,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACzC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,YAAY,EAAE,KAAK,CAAC,YAAY,UAAU,CAAC;AAAA,EAC3C,YAAY,EAAE,KAAK,CAAC,SAAS,UAAU,CAAC;AAAA,EACxC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,CAAC;AAED,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,OAAO,EAAE,MAAM,iBAAiB;AAAA,EAChC,OAAO,EAAE,OAAO,EAAE,IAAI;AAAA,EACtB,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,EACrB,UAAU,EAAE,OAAO,EAAE,IAAI;AAAA,EACzB,YAAY,EAAE,OAAO,EAAE,IAAI;AAC7B,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAC3C,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAErD,MAAM,mBAAmB,EAAE,KAAK,CAAC,YAAY,UAAU,CAAC;AACxD,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;AAAA,EACvC,YAAY,iBAAiB,SAAS;AACxC,CAAC;AACD,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EAClD,YAAY,iBAAiB,SAAS;AACxC,CAAC;AAcD,SAAS,cAAc,KAA6B;AAClD,MAAI;AACF,UAAM,QAAQ,IAAI,IAAI,IAAI,GAAG,EAAE,SAAS,MAAM,4BAA4B;AAC1E,UAAM,KAAK,QAAQ,CAAC,IAAI,mBAAmB,MAAM,CAAC,CAAC,IAAI;AACvD,WAAO,MAAM,GAAG,SAAS,IAAI,KAAK;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,kBAAkB,KAAqC;AACpE,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI,CAAC,KAAM,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,6BAA6B,cAAc,EAAE,CAAC;AAEzG,QAAM,oBAAoB,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACpG,QAAM,MAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA,wBAAwB,kCAAkC,mBAAmB,IAAI,KAAK;AAAA,IACtF,iBAAiB,mBAAmB,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAC9E,SAAS;AAAA,EACX;AAEA,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,iBAAiB,IAAI,0BAA0B,KAAK,SAAS;AACnE,QAAM,SAAS,KAAK,OAAO;AAC3B,MAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,QAAQ;AAC3C,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO,UAAU,6BAA6B,uCAAuC;AAAA,IACvF,CAAC;AAAA,EACH;AAEA,QAAM,SAAS,cAAc,GAAG;AAChC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO,UAAU,0DAA0D,sBAAsB;AAAA,IACnG,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,KAAK,OAAO,EAAE,UAAU,gBAAgB,OAAO,GAAG,WAAW,OAAO;AAC/E;AAOA,eAAe,kBAAkB,OAAoC;AACnE,QAAM,SAAS,MAAM,sBAAsB,MAAM,IAAI,WAAW,MAAM,KAAK;AAC3E,QAAM,KAAM,MAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACrE,QAAM,sBAAsB,IAAI,MAAM,QAAQ,MAAM,OAAO,QAAQ,MAAM,SAAS;AACpF;AAEA,SAAS,qBAAqB,WAAsE;AAClG,SAAO,IAAI,cAAc,KAAK;AAAA,IAC5B,OAAO,UAAU,oDAAoD,sCAAsC;AAAA,EAC7G,CAAC;AACH;AAOA,eAAe,qBAAqB,OAAqB,WAAkD;AACzG,QAAM,KAAM,MAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACrE,QAAM,UAAU,MAAM,GAAG,QAAQ,sBAAsB;AAAA,IACrD,IAAI;AAAA,IACJ,QAAQ,MAAM;AAAA,IACd,UAAU,MAAM,MAAM;AAAA,IACtB,gBAAgB,MAAM,MAAM;AAAA,IAC5B,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,QAAS,OAAM,qBAAqB,MAAM,SAAS;AACxD,SAAO;AACT;AAIA,eAAe,kBACb,OACA,eACkC;AAClC,QAAM,MAAM,oBAAI,IAAwB;AACxC,QAAM,SAAS,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC;AAChD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI;AACF,UAAM,KAAM,MAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACrE,UAAM,QAAQ,MAAM;AAAA,MAClB;AAAA,MACA;AAAA,MACA,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE;AAAA,MACtB;AAAA,MACA,EAAE,UAAU,MAAM,MAAM,UAAU,gBAAgB,MAAM,MAAM,eAAe;AAAA,IAC/E;AACA,eAAW,QAAQ,OAAO;AACxB,YAAM,OAAO,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,KAAK,EAAE,SAAS,IAAI,KAAK,KAAK,KAAK,IAAI;AAC/F,UAAI,IAAI,KAAK,IAAI,EAAE,MAAM,OAAO,KAAK,SAAS,KAAK,CAAC;AAAA,IACtD;AAAA,EACF,SAAS,KAAK;AAEZ,WAAO,KAAK,oEAAoE,EAAE,IAAI,CAAC;AAAA,EACzF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAA+B,SAAkC;AACzF,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,OAAO,eAAe,QAAQ,IAAI,YAAY,KAAK,OAAO;AAChE,QAAM,aAAa,QAAQ,eAAe,aAAa,aAAa;AACpE,QAAM,eAAe,QAAQ,uBAAuB,eAAe;AACnE,SAAO;AAAA,IACL,IAAI,QAAQ;AAAA,IACZ,QAAQ,QAAQ;AAAA,IAChB,MAAM,QAAQ;AAAA,IACd;AAAA,IACA,YAAY,eAAe,aAAa,eAAe,MAAM,QAAQ;AAAA,IACrE,aAAa,eAAe,aAAa,OAAO,MAAM,SAAS;AAAA,IAC/D,YAAY,QAAQ,eAAe,aAAa,aAAa;AAAA,IAC7D;AAAA,IACA,WAAW,QAAQ,YAAY,QAAQ,UAAU,YAAY,IAAI;AAAA,IACjE,WAAW,QAAQ,YAAY,QAAQ,UAAU,YAAY,IAAI;AAAA,EACnE;AACF;AAEA,eAAe,UACb,OACA,WACA,YACA,iBACA;AACA,SAAO,uBAAuB;AAAA,IAC5B,WAAW,MAAM,IAAI;AAAA,IACrB,KAAK,MAAM,IAAI;AAAA,IACf,MAAM;AAAA,MACJ,QAAQ,MAAM,MAAM;AAAA,MACpB,UAAU,MAAM,MAAM;AAAA,MACtB,gBAAgB,MAAM,MAAM;AAAA,IAC9B;AAAA,IACA,OAAO;AAAA,MACL,cAAc;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,oBACP,UACA,UASA,oBACc;AACd,MAAI,CAAC,UAAU,aAAa,CAAC,SAAS,MAAM,CAAC,SAAS,UAAW,QAAO;AACxE,WAAS,QAAQ;AAAA,IACf;AAAA,IACA,2BAA2B;AAAA,MACzB,IAAI,SAAS;AAAA,MACb,WAAW,SAAS;AAAA,MACpB,WAAW,SAAS;AAAA,MACpB,aAAa,SAAS,eAAe;AAAA,MACrC,cAAc,SAAS,gBAAgB;AAAA,MACvC,YAAY,SAAS,cAAc;AAAA,MACnC,YACE,SAAS,qBAAqB,OAC1B,SAAS,UAAU,YAAY,IAC/B,OAAO,SAAS,cAAc,WAC5B,SAAS,aACT,oBAAI,KAAK,GAAE,YAAY;AAAA,IACjC,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,eAAe,cAAc,KAAc,SAAoC;AAC7E,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,KAAK,EAAE,OAAO,UAAU,yBAAyB,uBAAuB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAClH;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,MAAM,kBAAkB,GAAG;AACzC,UAAM,kBAAkB,KAAK;AAE7B,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,QAAQ,gBAAgB,MAAM;AAAA,MAClC,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,MACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAChD,CAAC;AAED,UAAM,KAAM,MAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACrE,UAAM,CAAC,UAAU,KAAK,IAAI,MAAM,GAAG;AAAA,MACjC;AAAA,MACA;AAAA,QACE,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM,MAAM;AAAA,QACtB,gBAAgB,MAAM,MAAM;AAAA,QAC5B,WAAW;AAAA,MACb;AAAA,MACA;AAAA;AAAA,QAEE,SAAS,EAAE,WAAW,OAAO,IAAI,MAAM;AAAA,QACvC,OAAO,MAAM;AAAA,QACb,SAAS,MAAM,OAAO,KAAK,MAAM;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA,SAAS,IAAI,CAAC,YAAY,QAAQ,YAAY,EAAE,OAAO,CAAC,OAAqB,OAAO,OAAO,QAAQ;AAAA,IACrG;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,SAAS,IAAI,CAAC,YAAY,iBAAiB,SAAS,OAAO,CAAC;AAAA,MACnE;AAAA,MACA,MAAM,MAAM;AAAA,MACZ,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM,WAAW,IAAI,KAAK,KAAK,QAAQ,MAAM,QAAQ,IAAI;AAAA,IACvE,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,cAAc,KAAK,4CAA4C;AAAA,EACxE;AACF;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,QAAQ,MAAM,kBAAkB,GAAG;AACzC,UAAM,kBAAkB,KAAK;AAE7B,UAAM,MAAM,MAAM,aAAa,KAAK,CAAC,CAAC;AACtC,UAAM,aAAa,iBAAiB,MAAM,OAAO,CAAC,CAAC;AAGnD,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,EAAE,QAAQ,MAAM,QAAQ,MAAM,WAAW,MAAM,YAAY,WAAW,cAAc,WAAW;AAAA,MAC/F,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAEA,UAAM,cAAc,MAAM,UAAU,OAAO,UAAU,MAAM,QAAQ,KAA2C;AAC9G,QAAI,CAAC,YAAY,GAAI,QAAO,YAAY;AAExC,UAAM,aAAa,MAAM,IAAI,UAAU,QAAQ,YAAY;AAC3D,UAAM,EAAE,QAAQ,SAAS,IAAI,MAAM,WAAW,QAG5C,+BAA+B,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAElE,UAAM,YAAY,gBAAgB;AAElC,UAAM,WAAW,aAAa;AAAA,MAC5B;AAAA,QACE,IAAI,QAAQ,aAAa;AAAA,QACzB,QAAQ,MAAM;AAAA,QACd,cAAc,QAAQ,gBAAgB;AAAA,MACxC;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AACA,WAAO,oBAAoB,UAAU,UAAU,QAAQ,aAAa,MAAM,MAAM;AAAA,EAClF,SAAS,KAAK;AACZ,UAAM,uBAAuB,mCAAmC,GAAG;AACnE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,WAAO,cAAc,KAAK,6CAA6C;AAAA,EACzE;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,QAAQ,MAAM,kBAAkB,GAAG;AACzC,UAAM,kBAAkB,KAAK;AAE7B,UAAM,MAAM,MAAM,aAAa,KAAK,CAAC,CAAC;AACtC,UAAM,aAAa,iBAAiB,MAAM,OAAO,CAAC,CAAC;AACnD,UAAM,qBAAqB,OAAO,WAAW,EAAE;AAE/C,UAAM,QAAQ,wCAAwC,MAAM;AAAA,MAC1D,IAAI,WAAW;AAAA,MACf,MAAM,WAAW;AAAA,MACjB,YAAY,WAAW;AAAA,IACzB,CAAC;AAED,UAAM,cAAc,MAAM,UAAU,OAAO,UAAU,MAAM,IAAI,KAA2C;AAC1G,QAAI,CAAC,YAAY,GAAI,QAAO,YAAY;AAExC,UAAM,aAAa,MAAM,IAAI,UAAU,QAAQ,YAAY;AAC3D,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW;AAAA,MACpC,+BAA+B;AAAA,MAC/B,EAAE,OAAO,KAAK,MAAM,IAAI;AAAA,IAC1B;AAEA,UAAM,YAAY,gBAAgB;AAElC,WAAO,oBAAoB,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC,GAAG,UAAU,MAAM,EAAE;AAAA,EAChF,SAAS,KAAK;AACZ,UAAM,uBAAuB,mCAAmC,GAAG;AACnE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,WAAO,cAAc,KAAK,4CAA4C;AAAA,EACxE;AACF;AAEA,eAAsB,OAAO,KAAc;AACzC,MAAI;AACF,UAAM,QAAQ,MAAM,kBAAkB,GAAG;AACzC,UAAM,kBAAkB,KAAK;AAE7B,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,MAAM,MAAM,aAAa,KAAK,CAAC,CAAC;AACtC,UAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,KAAM,KAAiC;AAC9E,UAAM,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,UAAU,WAAW,QAAQ,MAAS;AAChF,UAAM,qBAAqB,OAAO,EAAE;AAEpC,UAAM,cAAc,MAAM,UAAU,OAAO,UAAU,IAAI,EAAE,GAAG,CAAC;AAC/D,QAAI,CAAC,YAAY,GAAI,QAAO,YAAY;AAExC,UAAM,aAAa,MAAM,IAAI,UAAU,QAAQ,YAAY;AAC3D,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW;AAAA,MACpC,+BAA+B;AAAA,MAC/B,EAAE,OAAO,EAAE,GAAG,GAAG,KAAK,MAAM,IAAI;AAAA,IAClC;AAEA,UAAM,YAAY,gBAAgB;AAElC,WAAO,oBAAoB,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC,GAAG,UAAU,EAAE;AAAA,EAC1E,SAAS,KAAK;AACZ,UAAM,uBAAuB,mCAAmC,GAAG;AACnE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,WAAO,cAAc,KAAK,+CAA+C;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,OAAO;AAAA,MACP,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,kBAAkB,QAAQ,mBAAmB,CAAC;AAAA,MACtF,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,wCAAwC;AAAA,QACpE,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,sCAAsC;AAAA,QAClE,EAAE,QAAQ,KAAK,aAAa,mCAAmC;AAAA,MACjE;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa,EAAE,aAAa,oBAAoB,QAAQ,iBAAiB;AAAA,MACzE,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,qBAAqB,CAAC;AAAA,MACzF,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,yDAAyD;AAAA,QACrF,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,wCAAwC;AAAA,QACpE,EAAE,QAAQ,KAAK,aAAa,mCAAmC;AAAA,MACjE;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa,EAAE,aAAa,oBAAoB,QAAQ,iBAAiB;AAAA,MACzE,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,iBAAiB,CAAC;AAAA,MACrF,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,yDAAyD;AAAA,QACrF,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,yDAAyD;AAAA,QACrF,EAAE,QAAQ,KAAK,aAAa,8CAA8C;AAAA,QAC1E,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aACE;AAAA,MACF,aAAa,EAAE,aAAa,oBAAoB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;AAAA,MAC5F,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,iBAAiB,CAAC;AAAA,MACrF,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oDAAoD;AAAA,QAChF,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,yDAAyD;AAAA,QACrF,EAAE,QAAQ,KAAK,aAAa,8CAA8C;AAAA,QAC1E,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,32 @@
1
+ import {
2
+ TASK_ASSIGNMENT_VIEW_FEATURE,
3
+ createTaskAssignmentPostHandler,
4
+ taskAssignmentResponseSchema
5
+ } from "../../../../../lib/time-tracking/taskAssignmentRoute.js";
6
+ const metadata = {
7
+ POST: { requireAuth: true, requireFeatures: [TASK_ASSIGNMENT_VIEW_FEATURE] }
8
+ };
9
+ const POST = createTaskAssignmentPostHandler("release");
10
+ const openApi = {
11
+ tag: "Staff",
12
+ summary: "Release a task you hold",
13
+ methods: {
14
+ POST: {
15
+ summary: "Release a task you hold",
16
+ description: "Clears the assignee of a task the caller is assigned to, making it claimable again; the project assigners are notified. Only the current assignee may release (403 otherwise); managers keep using PUT /api/staff/timesheets/tasks.",
17
+ responses: [{ status: 200, description: "Release a task you hold", schema: taskAssignmentResponseSchema }],
18
+ errors: [
19
+ { status: 401, description: "Unauthorized" },
20
+ { status: 403, description: "The caller is not the current assignee" },
21
+ { status: 404, description: "Task not found, or not on a project the caller can see" },
22
+ { status: 409, description: "The task changed since the version the caller sent, or it is no longer assigned to the caller (`task_assignment_changed`)" }
23
+ ]
24
+ }
25
+ }
26
+ };
27
+ export {
28
+ POST,
29
+ metadata,
30
+ openApi
31
+ };
32
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../../src/modules/staff/api/timesheets/tasks/%5Bid%5D/release/route.ts"],
4
+ "sourcesContent": ["import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n TASK_ASSIGNMENT_VIEW_FEATURE,\n createTaskAssignmentPostHandler,\n taskAssignmentResponseSchema,\n} from '../../../../../lib/time-tracking/taskAssignmentRoute'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [TASK_ASSIGNMENT_VIEW_FEATURE] },\n}\n\nexport const POST = createTaskAssignmentPostHandler('release')\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Release a task you hold',\n methods: {\n POST: {\n summary: 'Release a task you hold',\n description: 'Clears the assignee of a task the caller is assigned to, making it claimable again; the project assigners are notified. Only the current assignee may release (403 otherwise); managers keep using PUT /api/staff/timesheets/tasks.',\n responses: [{ status: 200, description: 'Release a task you hold', schema: taskAssignmentResponseSchema }],\n errors: [\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'The caller is not the current assignee' },\n { status: 404, description: 'Task not found, or not on a project the caller can see' },\n { status: 409, description: 'The task changed since the version the caller sent, or it is no longer assigned to the caller (`task_assignment_changed`)' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEO,MAAM,OAAO,gCAAgC,SAAS;AAEtD,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,6BAA6B,CAAC;AAAA,MACzG,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,yCAAyC;AAAA,QACrE,EAAE,QAAQ,KAAK,aAAa,yDAAyD;AAAA,QACrF,EAAE,QAAQ,KAAK,aAAa,4HAA4H;AAAA,MAC1J;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -5,12 +5,12 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
5
  import { resolveCrudRecordId, parseScopedCommandInput } from "@open-mercato/shared/lib/api/scoped";
6
6
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
7
7
  import { createLogger } from "@open-mercato/shared/lib/logger";
8
- import { StaffTimeTask, StaffTimeTaskTag } from "../../../data/entities.js";
8
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
9
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
10
+ import { StaffSupportTicket, StaffTimeTask, StaffTimeTaskStatus, StaffTimeTaskTag } from "../../../data/entities.js";
9
11
  import { staffTimeTaskCreateSchema, staffTimeTaskUpdateSchema } from "../../../data/validators.js";
10
12
  import { staffTimeTaskCommandIds } from "../../../commands/timesheets-tasks.js";
11
- import { resolveFeatureAccess } from "../../../lib/time-tracking/featureAccess.js";
12
- import { MANAGE_PROJECTS_FEATURE, resolveProjectAccess } from "../../../lib/time-tracking/access.js";
13
- import { readTimeTrackingSettings } from "../../../lib/time-tracking/settings.js";
13
+ import { resolveListProjectAccess } from "../../../lib/time-tracking/listProjectAccess.js";
14
14
  import { sanitizeSearchTerm, parseBooleanFlag } from "../../helpers.js";
15
15
  import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from "../../openapi.js";
16
16
  const logger = createLogger("staff").child({ component: "api/timesheets/tasks" });
@@ -29,6 +29,8 @@ const F = {
29
29
  position: "position",
30
30
  created_by_user_id: "created_by_user_id",
31
31
  closed_at: "closed_at",
32
+ source: "source",
33
+ customer_id: "customer_id",
32
34
  created_at: "created_at",
33
35
  updated_at: "updated_at"
34
36
  };
@@ -54,52 +56,26 @@ const listSchema = z.object({
54
56
  parentTaskId: z.string().uuid().optional(),
55
57
  tagIds: z.string().optional(),
56
58
  topLevelOnly: z.string().optional(),
59
+ unassigned: z.string().optional(),
60
+ source: z.enum(["internal", "portal"]).optional(),
61
+ customerId: z.string().uuid().optional(),
62
+ supportCategory: z.string().optional(),
63
+ supportReference: z.string().optional(),
64
+ portalStatus: z.enum(["received", "in_progress", "resolved"]).optional(),
65
+ supportUnnotified: z.string().optional(),
57
66
  sortField: z.string().optional(),
58
67
  sortDir: z.enum(["asc", "desc"]).optional()
59
68
  }).passthrough();
60
69
  const IMPOSSIBLE_ID = "00000000-0000-0000-0000-000000000000";
61
- const DENIED_ACCESS = { canManageAll: false, projectIds: [], staffMemberId: null };
62
- const accessByRequest = /* @__PURE__ */ new WeakMap();
63
- async function resolveAssignmentGraceDays(container, tenantId) {
64
- try {
65
- const configService = container.resolve("moduleConfigService");
66
- const settings = await readTimeTrackingSettings(configService, { tenantId });
67
- return settings.access.assignmentGraceDays;
68
- } catch {
69
- return null;
70
- }
71
- }
72
- async function loadProjectAccess(container, userId, tenantId, organizationId) {
73
- if (!tenantId || !organizationId) return { ...DENIED_ACCESS };
74
- const scope = { tenantId, organizationId };
75
- try {
76
- const access = await resolveFeatureAccess(container, userId, [MANAGE_PROJECTS_FEATURE], scope);
77
- const em = container.resolve("em");
78
- return await resolveProjectAccess({
79
- em: em.fork(),
80
- userId,
81
- tenantId,
82
- organizationId,
83
- canManageAll: access.allowed,
84
- userFeatures: access.grantedFeatures,
85
- assignmentGraceDays: await resolveAssignmentGraceDays(container, tenantId)
86
- });
87
- } catch (err) {
88
- logger.error("staff.timesheets.tasks access resolution failed", { err });
89
- return { ...DENIED_ACCESS };
90
- }
70
+ const SUPPORT_FILTER_MAX_TASK_IDS = 1e3;
71
+ const supportFilterTruncatedRequests = /* @__PURE__ */ new WeakSet();
72
+ function markTaskListTotalCapped(payload, ctx) {
73
+ if (ctx.request && supportFilterTruncatedRequests.has(ctx.request)) payload.totalIsCapped = true;
91
74
  }
92
- async function resolveListProjectAccess(ctx) {
93
- const tenantId = ctx.auth?.tenantId ?? null;
94
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
95
- const load = () => loadProjectAccess(ctx.container, ctx.auth?.sub ?? null, tenantId, organizationId);
96
- const request = ctx.request;
97
- if (!request) return load();
98
- const cached = accessByRequest.get(request);
99
- if (cached) return cached;
100
- const pending = load();
101
- accessByRequest.set(request, pending);
102
- return pending;
75
+ function intersectIds(current, next) {
76
+ if (current === null) return next;
77
+ const allowed = new Set(next);
78
+ return current.filter((id) => allowed.has(id));
103
79
  }
104
80
  function splitIdList(value) {
105
81
  if (typeof value !== "string" || value.trim().length === 0) return null;
@@ -129,6 +105,50 @@ async function resolveTaskIdsForTags(ctx, tagIds) {
129
105
  return [...seen.entries()].filter(([, tags]) => tags.size >= required).map(([taskId]) => taskId);
130
106
  } catch (err) {
131
107
  logger.error("staff.timesheets.tasks tag filter resolution failed", { err });
108
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.task_tag_filter_failed" });
109
+ return [];
110
+ }
111
+ }
112
+ async function resolveTaskIdsForSupport(ctx, criteria) {
113
+ const tenantId = ctx.auth?.tenantId ?? null;
114
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
115
+ if (!tenantId || !organizationId) return [];
116
+ try {
117
+ const em = ctx.container.resolve("em").fork();
118
+ const where = { tenantId, organizationId, deletedAt: null };
119
+ if (criteria.supportCategory) where.categoryValue = criteria.supportCategory;
120
+ if (criteria.supportReference) where.supportReference = { $ilike: `${escapeLikePattern(criteria.supportReference)}%` };
121
+ if (criteria.supportUnnotified) where.unnotifiedAt = { $ne: null };
122
+ const rows = await findWithDecryption(
123
+ em,
124
+ StaffSupportTicket,
125
+ where,
126
+ { fields: ["taskId"], limit: SUPPORT_FILTER_MAX_TASK_IDS + 1, orderBy: { createdAt: "desc" } },
127
+ { tenantId, organizationId }
128
+ );
129
+ if (rows.length > SUPPORT_FILTER_MAX_TASK_IDS && ctx.request) supportFilterTruncatedRequests.add(ctx.request);
130
+ return Array.from(new Set(rows.slice(0, SUPPORT_FILTER_MAX_TASK_IDS).map((row) => row.taskId)));
131
+ } catch (err) {
132
+ logger.error("staff.timesheets.tasks support filter resolution failed", { err });
133
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.task_support_filter_failed" });
134
+ return [];
135
+ }
136
+ }
137
+ async function resolveStatusIdsForPortalStatus(ctx, portalStatus) {
138
+ const tenantId = ctx.auth?.tenantId ?? null;
139
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
140
+ if (!tenantId || !organizationId) return [];
141
+ try {
142
+ const em = ctx.container.resolve("em").fork();
143
+ const where = { tenantId, organizationId, deletedAt: null };
144
+ if (portalStatus === "resolved") where.isDone = true;
145
+ else if (portalStatus === "received") Object.assign(where, { isDone: false, isDefault: true });
146
+ else Object.assign(where, { isDone: false, isDefault: false });
147
+ const rows = await em.find(StaffTimeTaskStatus, where, { fields: ["id"] });
148
+ return rows.map((row) => row.id);
149
+ } catch (err) {
150
+ logger.error("staff.timesheets.tasks portal status filter resolution failed", { err });
151
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.task_portal_status_filter_failed" });
132
152
  return [];
133
153
  }
134
154
  }
@@ -138,12 +158,23 @@ async function buildTaskListFilters(query, ctx) {
138
158
  for (const raw of [query.ids, query.id]) {
139
159
  const parsed = splitIdList(raw);
140
160
  if (parsed === null) continue;
141
- narrowIds = narrowIds ? narrowIds.filter((id) => parsed.includes(id)) : parsed;
161
+ narrowIds = intersectIds(narrowIds, parsed);
142
162
  }
143
163
  const requestedTagIds = splitIdList(query.tagIds);
144
164
  if (requestedTagIds !== null) {
145
165
  const tagged = requestedTagIds.length === 0 ? [] : await resolveTaskIdsForTags(ctx, requestedTagIds);
146
- narrowIds = narrowIds ? narrowIds.filter((id) => tagged.includes(id)) : tagged;
166
+ narrowIds = intersectIds(narrowIds, tagged);
167
+ }
168
+ const supportCategory = typeof query.supportCategory === "string" ? query.supportCategory.trim() : "";
169
+ const supportReference = sanitizeSearchTerm(query.supportReference) ?? "";
170
+ const supportUnnotified = parseBooleanFlag(query.supportUnnotified);
171
+ if (supportCategory || supportReference || supportUnnotified) {
172
+ const supportTaskIds = await resolveTaskIdsForSupport(ctx, {
173
+ supportCategory: supportCategory || void 0,
174
+ supportReference: supportReference || void 0,
175
+ supportUnnotified
176
+ });
177
+ narrowIds = intersectIds(narrowIds, supportTaskIds);
147
178
  }
148
179
  const access = await resolveListProjectAccess(ctx);
149
180
  let projectIds = null;
@@ -170,10 +201,17 @@ async function buildTaskListFilters(query, ctx) {
170
201
  }
171
202
  if (typeof query.taskStatusId === "string" && query.taskStatusId.trim().length > 0) {
172
203
  filters[F.task_status_id] = query.taskStatusId.trim();
204
+ } else if (query.portalStatus) {
205
+ const statusIds = await resolveStatusIdsForPortalStatus(ctx, query.portalStatus);
206
+ filters[F.task_status_id] = { $in: statusIds.length > 0 ? statusIds : [IMPOSSIBLE_ID] };
173
207
  }
174
208
  if (typeof query.assigneeStaffMemberId === "string" && query.assigneeStaffMemberId.trim().length > 0) {
175
209
  filters[F.assignee_staff_member_id] = query.assigneeStaffMemberId.trim();
210
+ } else if (parseBooleanFlag(query.unassigned)) {
211
+ filters[F.assignee_staff_member_id] = { $eq: null };
176
212
  }
213
+ if (query.source) filters[F.source] = query.source;
214
+ if (query.customerId) filters[F.customer_id] = query.customerId;
177
215
  if (typeof query.parentTaskId === "string" && query.parentTaskId.trim().length > 0) {
178
216
  filters[F.parent_task_id] = query.parentTaskId.trim();
179
217
  } else if (parseBooleanFlag(query.topLevelOnly)) {
@@ -204,6 +242,8 @@ const taskListFields = [
204
242
  F.position,
205
243
  F.created_by_user_id,
206
244
  F.closed_at,
245
+ F.source,
246
+ F.customer_id,
207
247
  F.created_at,
208
248
  F.updated_at
209
249
  ];
@@ -238,6 +278,9 @@ const crud = makeCrudRoute({
238
278
  buildFilters: buildTaskListFilters
239
279
  },
240
280
  enrichers: { entityId: "staff:staff_time_task" },
281
+ hooks: {
282
+ afterList: (payload, ctx) => markTaskListTotalCapped(payload, ctx)
283
+ },
241
284
  actions: {
242
285
  create: {
243
286
  commandId: staffTimeTaskCommandIds.create,
@@ -289,6 +332,8 @@ const taskListItemSchema = z.object({
289
332
  position: z.number().int().nullable().optional(),
290
333
  created_by_user_id: z.string().uuid().nullable().optional(),
291
334
  closed_at: z.string().nullable().optional(),
335
+ source: z.enum(["internal", "portal"]).nullable().optional(),
336
+ customer_id: z.string().uuid().nullable().optional(),
292
337
  created_at: z.string().nullable().optional(),
293
338
  updated_at: z.string().nullable().optional(),
294
339
  /** This task's own entries, in raw minutes. */
@@ -323,7 +368,9 @@ export {
323
368
  GET,
324
369
  POST,
325
370
  PUT,
371
+ SUPPORT_FILTER_MAX_TASK_IDS,
326
372
  buildTaskListFilters,
373
+ markTaskListTotalCapped,
327
374
  metadata,
328
375
  openApi,
329
376
  resolveListProjectAccess,