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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -13,6 +13,7 @@ import {
13
13
  DrawerHeader,
14
14
  DrawerTitle,
15
15
  } from '@open-mercato/ui/primitives/drawer'
16
+ import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
16
17
  import { Avatar } from '@open-mercato/ui/primitives/avatar'
17
18
  import { Badge } from '@open-mercato/ui/primitives/badge'
18
19
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -28,7 +29,11 @@ import {
28
29
  } from '@open-mercato/ui/primitives/select'
29
30
  import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
30
31
  import { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
31
- import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
32
+ import {
33
+ buildOptimisticLockHeader,
34
+ extractOptimisticLockConflict,
35
+ extractRecordLockConflict,
36
+ } from '@open-mercato/ui/backend/utils/optimisticLock'
32
37
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
33
38
  import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
34
39
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
@@ -41,7 +46,11 @@ import { hasFeature } from '@open-mercato/shared/security/features'
41
46
  import { useT } from '@open-mercato/shared/lib/i18n/context'
42
47
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
43
48
  import { createLogger } from '@open-mercato/shared/lib/logger'
44
- import { TaskCommentThread } from './TaskCommentThread'
49
+ import { TaskCommentThread, type CommentVisibility } from './TaskCommentThread'
50
+ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
51
+ import { SupportRequestPanel } from './SupportRequestPanel'
52
+ import { SupportAttachmentsSection } from './SupportAttachmentsSection'
53
+ import { readSupportTicket, SUPPORT_ACTIVITY_EVENT, type SupportTicketView } from './supportTicketData'
45
54
  import { TaskQuickLog, type QuickLogSubmission } from './TaskQuickLog'
46
55
  import { TagPicker, tagChipStyle } from './TagPicker'
47
56
  import { slugifyProjectName } from '../time-tracking/projectCode'
@@ -70,6 +79,9 @@ import {
70
79
  toDrawerEntry,
71
80
  todayIsoDate,
72
81
  buildEntryClocks,
82
+ isAssignmentEventForTask,
83
+ TASK_ASSIGNMENT_CHANGED_EVENT,
84
+ TASK_DRAWER_QUERY_ROOT,
73
85
  type DrawerComment,
74
86
  type DrawerEntry,
75
87
  } from './taskDrawerData'
@@ -87,11 +99,32 @@ type TaskDetailInjectionContext = {
87
99
  }
88
100
 
89
101
  export const TASK_DRAWER_MUTATION_CONTEXT_ID = 'staff.time_tracking.task_drawer'
102
+
103
+ /**
104
+ * The drawer's writes carry no `resourceKind`/`resourceId`, so the enterprise
105
+ * record-lock widget leaves them alone: every property saves on its own (the
106
+ * spec's per-field saves), and the `updated_at` version each save sends is what
107
+ * guards concurrent edits. A lock taken per save, never released and never
108
+ * resolvable from here, could only block the user (#8045, #8101). The `formId`
109
+ * is still scoped to the task so no guarded-mutation state crosses tasks.
110
+ */
111
+ function taskDrawerFormId(taskId: string): string {
112
+ return `${TASK_DRAWER_MUTATION_CONTEXT_ID}:${taskId}`
113
+ }
114
+
115
+ const ASSIGNMENT_RACE_CODES = new Set(['task_already_claimed', 'task_assignment_changed'])
116
+
117
+ function isAssignmentRace(error: unknown): boolean {
118
+ if (extractOptimisticLockConflict(error) || extractRecordLockConflict(error)) return true
119
+ if (!error || typeof error !== 'object') return false
120
+ const code = (error as { code?: unknown }).code
121
+ return typeof code === 'string' && ASSIGNMENT_RACE_CODES.has(code)
122
+ }
90
123
  export const RATES_FEATURE = 'staff.timesheets.rates.view'
91
124
  export const TASKS_MANAGE_FEATURE = 'staff.timesheets.tasks.manage'
92
125
  export const ENTRIES_LIST_PATH = '/backend/staff/time-tracking/entries'
93
126
 
94
- const DRAWER_QUERY_ROOT = ['staff', 'time-tracking', 'task-drawer'] as const
127
+ const DRAWER_QUERY_ROOT = TASK_DRAWER_QUERY_ROOT
95
128
  const DIRECTORY_PAGE_SIZE = 100
96
129
  const CHILD_PAGE_SIZE = 100
97
130
  const COMMENT_PAGE_SIZE = 50
@@ -113,7 +146,21 @@ export type TaskDrawerProps = {
113
146
  onChanged?: () => void
114
147
  }
115
148
 
116
- type LoadedTask = { task: BoardTask; createdAt: string | null }
149
+ type TaskAssignmentAbility = { canClaim: boolean; canRelease: boolean }
150
+
151
+ type LoadedTask = {
152
+ task: BoardTask
153
+ createdAt: string | null
154
+ support: SupportTicketView | null
155
+ assignment: TaskAssignmentAbility
156
+ }
157
+
158
+ function readTaskAssignmentAbility(row: BoardApiRow): TaskAssignmentAbility {
159
+ const block = (row as Record<string, unknown>)._staffAssignment
160
+ if (!block || typeof block !== 'object') return { canClaim: false, canRelease: false }
161
+ const ability = block as Record<string, unknown>
162
+ return { canClaim: ability.canClaim === true, canRelease: ability.canRelease === true }
163
+ }
117
164
 
118
165
  type RunningTimer = { entryId: string | null; taskId: string | null; startedAt: string | null }
119
166
 
@@ -197,16 +244,22 @@ export function TaskDrawer({
197
244
  * with the token the first blur just invalidated.
198
245
  */
199
246
  const versionRef = React.useRef<string | null>(null)
247
+ const [notice, setNotice] = React.useState<{ status: 'warning' | 'error'; message: string } | null>(null)
200
248
 
201
- const { runMutation, retryLastMutation } = useGuardedMutation<{
249
+ const { runMutation: runGuardedMutation, retryLastMutation } = useGuardedMutation<{
202
250
  formId: string
203
- resourceKind: string
204
- resourceId: string
205
251
  retryLastMutation: () => Promise<boolean>
206
252
  }>({
207
253
  contextId: TASK_DRAWER_MUTATION_CONTEXT_ID,
208
254
  blockedMessage: t('staff.time_tracking.taskDrawer.properties.saveError', 'Could not save the change.'),
209
255
  })
256
+ const runMutation = React.useCallback<typeof runGuardedMutation>(
257
+ (input) => {
258
+ setNotice(null)
259
+ return runGuardedMutation(input)
260
+ },
261
+ [runGuardedMutation],
262
+ )
210
263
 
211
264
  const taskQuery = useQuery<LoadedTask | null>({
212
265
  queryKey: [...DRAWER_QUERY_ROOT, 'task', `scope:${scopeVersion}`, taskId],
@@ -220,12 +273,20 @@ export function TaskDrawer({
220
273
  const row = readItems(call.result)[0]
221
274
  if (!row) return null
222
275
  const parsed = toBoardTask(row)
223
- return parsed ? { task: parsed, createdAt: readString(row, 'created_at', 'createdAt') } : null
276
+ return parsed
277
+ ? {
278
+ task: parsed,
279
+ createdAt: readString(row, 'created_at', 'createdAt'),
280
+ support: readSupportTicket(row),
281
+ assignment: readTaskAssignmentAbility(row),
282
+ }
283
+ : null
224
284
  },
225
285
  })
226
286
 
227
287
  const task = taskQuery.data?.task ?? null
228
288
  const createdAt = taskQuery.data?.createdAt ?? null
289
+ const support = taskQuery.data?.support ?? null
229
290
  const projectId = task?.timeProjectId ?? null
230
291
 
231
292
  const taskInjectionContext = React.useMemo<TaskDetailInjectionContext>(
@@ -246,6 +307,10 @@ export function TaskDrawer({
246
307
  setTagIds(task.tagIds)
247
308
  }, [task])
248
309
 
310
+ React.useEffect(() => {
311
+ setNotice(null)
312
+ }, [taskId])
313
+
249
314
  const statusesQuery = useQuery<BoardStatus[]>({
250
315
  queryKey: [...DRAWER_QUERY_ROOT, 'statuses', `scope:${scopeVersion}`, projectId ?? 'none'],
251
316
  enabled: open && !!projectId,
@@ -435,7 +500,7 @@ export function TaskDrawer({
435
500
  )
436
501
 
437
502
  const invalidate = React.useCallback(
438
- (part: 'task' | 'children' | 'entries' | 'comments') => {
503
+ (part: 'task' | 'children' | 'entries' | 'comments' | 'attachments') => {
439
504
  queryClient.invalidateQueries({ queryKey: [...DRAWER_QUERY_ROOT, part] }).catch(() => {})
440
505
  },
441
506
  [queryClient],
@@ -447,17 +512,36 @@ export function TaskDrawer({
447
512
  if (updatedAt) versionRef.current = updatedAt
448
513
  }, [taskQuery])
449
514
 
515
+ /**
516
+ * A failed save is reported inside the drawer, which covers the page-level
517
+ * conflict bar. It is a conflict when the server says so, or when the task
518
+ * moved underneath the save (a guard can refuse it before any request leaves);
519
+ * either way the drawer refetches, so the next attempt starts from what it shows.
520
+ */
450
521
  const reportError = React.useCallback(
451
522
  (error: unknown, fallback: string) => {
523
+ const conflictNotice = {
524
+ status: 'warning' as const,
525
+ message: t(
526
+ 'staff.time_tracking.taskDrawer.conflict',
527
+ 'Someone else changed this task. It now shows the latest version. Check it and try again.',
528
+ ),
529
+ }
530
+ const seenVersion = versionRef.current
531
+ invalidate('children')
452
532
  if (surfaceRecordConflict(error, t)) {
533
+ setNotice(conflictNotice)
453
534
  invalidate('task')
454
- invalidate('children')
455
535
  return
456
536
  }
457
537
  logger.error('staff.time_tracking task drawer mutation failed', { err: error })
458
- flash(error instanceof Error && error.message ? error.message : fallback, 'error')
538
+ setNotice({ status: 'error', message: error instanceof Error && error.message ? error.message : fallback })
539
+ void taskQuery.refetch().then((refreshed) => {
540
+ const currentVersion = refreshed.data?.task.updatedAt ?? null
541
+ if (seenVersion && currentVersion && currentVersion !== seenVersion) setNotice(conflictNotice)
542
+ })
459
543
  },
460
- [invalidate, t],
544
+ [invalidate, t, taskQuery],
461
545
  )
462
546
 
463
547
  const handleQuickLog = React.useCallback(
@@ -503,9 +587,7 @@ export function TaskDrawer({
503
587
  { errorMessage: t('staff.time_tracking.taskDrawer.quickLog.error', 'Could not save the time entry.') },
504
588
  ),
505
589
  context: {
506
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
507
- resourceKind: 'staff.timesheets.time_entry',
508
- resourceId: taskId,
590
+ formId: taskDrawerFormId(taskId),
509
591
  retryLastMutation,
510
592
  },
511
593
  })
@@ -534,14 +616,12 @@ export function TaskDrawer({
534
616
  { errorMessage: t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.') },
535
617
  ),
536
618
  context: {
537
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
538
- resourceKind: 'staff.timesheets.time_entry',
539
- resourceId: entryId,
619
+ formId: taskDrawerFormId(taskId),
540
620
  retryLastMutation,
541
621
  },
542
622
  })
543
623
  },
544
- [retryLastMutation, runMutation, t],
624
+ [retryLastMutation, runMutation, t, taskId],
545
625
  )
546
626
 
547
627
  const handleStopTimer = React.useCallback(async () => {
@@ -596,9 +676,7 @@ export function TaskDrawer({
596
676
  { errorMessage: t('staff.time_tracking.board.timer.startError', 'Could not start the timer.') },
597
677
  ),
598
678
  context: {
599
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
600
- resourceKind: 'staff.timesheets.time_entry',
601
- resourceId: taskId,
679
+ formId: taskDrawerFormId(taskId),
602
680
  retryLastMutation,
603
681
  },
604
682
  })
@@ -681,9 +759,7 @@ export function TaskDrawer({
681
759
  ),
682
760
  ),
683
761
  context: {
684
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
685
- resourceKind: 'staff.timesheets.task',
686
- resourceId: taskId,
762
+ formId: taskDrawerFormId(taskId),
687
763
  retryLastMutation,
688
764
  },
689
765
  })
@@ -721,9 +797,7 @@ export function TaskDrawer({
721
797
  ),
722
798
  ),
723
799
  context: {
724
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
725
- resourceKind: 'staff.timesheets.task',
726
- resourceId: taskId,
800
+ formId: taskDrawerFormId(taskId),
727
801
  retryLastMutation,
728
802
  },
729
803
  })
@@ -762,6 +836,85 @@ export function TaskDrawer({
762
836
  [saveTaskField],
763
837
  )
764
838
 
839
+ const canClaim = taskQuery.data?.assignment.canClaim === true
840
+ const canRelease = taskQuery.data?.assignment.canRelease === true
841
+
842
+ const runAssignmentAction = React.useCallback(
843
+ (action: 'claim' | 'release') => {
844
+ if (!task) return
845
+ const errorMessage =
846
+ action === 'claim'
847
+ ? t('staff.time_tracking.taskDrawer.assignment.claimError', 'Could not claim the task.')
848
+ : t('staff.time_tracking.taskDrawer.assignment.releaseError', 'Could not release the task.')
849
+ setBusy(true)
850
+ void runSerialized(async () => {
851
+ try {
852
+ // No optimistic-lock header: claim and release are atomic conditional
853
+ // updates on the assignee, so a lost race answers with its own reason
854
+ // instead of a generic "record changed".
855
+ const done = await runMutation({
856
+ operation: () =>
857
+ apiCallOrThrow<Record<string, unknown>>(
858
+ `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/${action}`,
859
+ { method: 'POST' },
860
+ { errorMessage },
861
+ ),
862
+ context: {
863
+ formId: taskDrawerFormId(taskId),
864
+ retryLastMutation,
865
+ },
866
+ })
867
+ const updatedAt = typeof done?.result?.updatedAt === 'string' ? done.result.updatedAt : null
868
+ if (updatedAt) versionRef.current = updatedAt
869
+ else await refreshVersion()
870
+ invalidate('task')
871
+ flash(
872
+ action === 'claim'
873
+ ? t('staff.time_tracking.taskDrawer.assignment.claimed', 'Task claimed.')
874
+ : t('staff.time_tracking.taskDrawer.assignment.released', 'Task released.'),
875
+ 'success',
876
+ )
877
+ onChanged?.()
878
+ } catch (error) {
879
+ if (!isAssignmentRace(error)) {
880
+ reportError(error, errorMessage)
881
+ return
882
+ }
883
+ const refreshed = await taskQuery.refetch()
884
+ const assigneeId = refreshed.data?.task.assigneeStaffMemberId ?? null
885
+ const takenByOther = action === 'claim' && assigneeId !== null && assigneeId !== selfStaffMemberId
886
+ setNotice({
887
+ status: 'warning',
888
+ message: takenByOther
889
+ ? t('staff.timesheets.errors.taskAlreadyClaimed', 'Someone else already took this task.')
890
+ : t(
891
+ 'staff.time_tracking.taskDrawer.conflict',
892
+ 'Someone else changed this task. It now shows the latest version. Check it and try again.',
893
+ ),
894
+ })
895
+ invalidate('task')
896
+ onChanged?.()
897
+ } finally {
898
+ setBusy(false)
899
+ }
900
+ })
901
+ },
902
+ [
903
+ invalidate,
904
+ onChanged,
905
+ refreshVersion,
906
+ reportError,
907
+ retryLastMutation,
908
+ runMutation,
909
+ runSerialized,
910
+ selfStaffMemberId,
911
+ t,
912
+ task,
913
+ taskId,
914
+ taskQuery,
915
+ ],
916
+ )
917
+
765
918
  const handleToggleSubtask = React.useCallback(
766
919
  async (child: BoardTask, done: boolean) => {
767
920
  const targetStatusId = done ? resolveDoneStatusId(statuses) : resolveDefaultStatusId(statuses)
@@ -789,9 +942,7 @@ export function TaskDrawer({
789
942
  ),
790
943
  ),
791
944
  context: {
792
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
793
- resourceKind: 'staff.timesheets.task',
794
- resourceId: child.id,
945
+ formId: taskDrawerFormId(taskId),
795
946
  retryLastMutation,
796
947
  },
797
948
  })
@@ -807,7 +958,7 @@ export function TaskDrawer({
807
958
  setBusy(false)
808
959
  }
809
960
  },
810
- [invalidate, onChanged, reportError, retryLastMutation, runMutation, statuses, t],
961
+ [invalidate, onChanged, reportError, retryLastMutation, runMutation, statuses, t, taskId],
811
962
  )
812
963
 
813
964
  const handleAddSubtask = React.useCallback(async () => {
@@ -838,9 +989,7 @@ export function TaskDrawer({
838
989
  },
839
990
  ),
840
991
  context: {
841
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
842
- resourceKind: 'staff.timesheets.task',
843
- resourceId: taskId,
992
+ formId: taskDrawerFormId(taskId),
844
993
  retryLastMutation,
845
994
  },
846
995
  })
@@ -867,8 +1016,65 @@ export function TaskDrawer({
867
1016
  taskId,
868
1017
  ])
869
1018
 
1019
+ useAppEvent(
1020
+ SUPPORT_ACTIVITY_EVENT,
1021
+ (event) => {
1022
+ const eventTaskId = (event.payload as { taskId?: unknown } | undefined)?.taskId
1023
+ if (!open || eventTaskId !== taskId) return
1024
+ invalidate('task')
1025
+ invalidate('comments')
1026
+ invalidate('attachments')
1027
+ },
1028
+ [invalidate, open, taskId],
1029
+ )
1030
+
1031
+ useAppEvent(
1032
+ TASK_ASSIGNMENT_CHANGED_EVENT,
1033
+ (event) => {
1034
+ if (!open || !isAssignmentEventForTask(event.payload, taskId)) return
1035
+ invalidate('task')
1036
+ },
1037
+ [invalidate, open, taskId],
1038
+ )
1039
+
1040
+ const handleMakeInternal = React.useCallback(
1041
+ async (comment: DrawerComment): Promise<void> => {
1042
+ setBusy(true)
1043
+ try {
1044
+ await runMutation({
1045
+ operation: () =>
1046
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(comment.updatedAt), () =>
1047
+ apiCallOrThrow(
1048
+ `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/comments`,
1049
+ {
1050
+ method: 'PUT',
1051
+ headers: { 'content-type': 'application/json' },
1052
+ body: JSON.stringify({ id: comment.id, visibility: 'internal' }),
1053
+ },
1054
+ { errorMessage: t('staff.time_tracking.taskComments.makeInternalError', 'Could not change the reply.') },
1055
+ ),
1056
+ ),
1057
+ context: {
1058
+ formId: taskDrawerFormId(taskId),
1059
+ retryLastMutation,
1060
+ },
1061
+ })
1062
+ invalidate('comments')
1063
+ } catch (error) {
1064
+ if (surfaceRecordConflict(error, t)) {
1065
+ invalidate('comments')
1066
+ return
1067
+ }
1068
+ reportError(error, t('staff.time_tracking.taskComments.makeInternalError', 'Could not change the reply.'))
1069
+ } finally {
1070
+ setBusy(false)
1071
+ }
1072
+ },
1073
+ [invalidate, reportError, retryLastMutation, runMutation, t, taskId],
1074
+ )
1075
+
870
1076
  const handlePostComment = React.useCallback(
871
- async (body: string): Promise<boolean> => {
1077
+ async (body: string, visibility: CommentVisibility = 'internal'): Promise<boolean> => {
872
1078
  setBusy(true)
873
1079
  try {
874
1080
  await runMutation({
@@ -878,14 +1084,12 @@ export function TaskDrawer({
878
1084
  {
879
1085
  method: 'POST',
880
1086
  headers: { 'content-type': 'application/json' },
881
- body: JSON.stringify({ body }),
1087
+ body: JSON.stringify({ body, visibility }),
882
1088
  },
883
1089
  { errorMessage: t('staff.time_tracking.taskDrawer.comments.error', 'Could not post the comment.') },
884
1090
  ),
885
1091
  context: {
886
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
887
- resourceKind: 'staff.timesheets.task_comment',
888
- resourceId: taskId,
1092
+ formId: taskDrawerFormId(taskId),
889
1093
  retryLastMutation,
890
1094
  },
891
1095
  })
@@ -921,9 +1125,7 @@ export function TaskDrawer({
921
1125
  { errorMessage: t('staff.time_tracking.taskDrawer.properties.tagError', 'Could not save the tags.') },
922
1126
  ),
923
1127
  context: {
924
- formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
925
- resourceKind: 'staff.timesheets.task_tag',
926
- resourceId: taskId,
1128
+ formId: taskDrawerFormId(taskId),
927
1129
  retryLastMutation,
928
1130
  },
929
1131
  })
@@ -1012,6 +1214,13 @@ export function TaskDrawer({
1012
1214
 
1013
1215
  return (
1014
1216
  <div className="flex flex-col gap-6">
1217
+ {notice ? (
1218
+ <Alert status={notice.status} data-testid="task-drawer-notice">
1219
+ <AlertDescription>{notice.message}</AlertDescription>
1220
+ </Alert>
1221
+ ) : null}
1222
+ {support ? <SupportRequestPanel ticket={support} /> : null}
1223
+ {support ? <SupportAttachmentsSection taskId={taskId} /> : null}
1015
1224
  <TaskQuickLog
1016
1225
  onLog={handleQuickLog}
1017
1226
  logging={busy}
@@ -1221,6 +1430,8 @@ export function TaskDrawer({
1221
1430
  posting={busy}
1222
1431
  canComment={canManage}
1223
1432
  onPost={handlePostComment}
1433
+ mode={support ? 'support' : 'internal'}
1434
+ onMakeInternal={support ? handleMakeInternal : undefined}
1224
1435
  />
1225
1436
 
1226
1437
  <section className="flex flex-col gap-3" data-testid="task-drawer-properties">
@@ -1277,6 +1488,30 @@ export function TaskDrawer({
1277
1488
  ))}
1278
1489
  </SelectContent>
1279
1490
  </Select>
1491
+ {canClaim ? (
1492
+ <Button
1493
+ type="button"
1494
+ size="sm"
1495
+ variant="outline"
1496
+ disabled={busy}
1497
+ onClick={() => runAssignmentAction('claim')}
1498
+ data-testid="task-drawer-claim"
1499
+ >
1500
+ {t('staff.time_tracking.taskDrawer.assignment.claim', 'Claim')}
1501
+ </Button>
1502
+ ) : null}
1503
+ {canRelease ? (
1504
+ <Button
1505
+ type="button"
1506
+ size="sm"
1507
+ variant="ghost"
1508
+ disabled={busy}
1509
+ onClick={() => runAssignmentAction('release')}
1510
+ data-testid="task-drawer-release"
1511
+ >
1512
+ {t('staff.time_tracking.taskDrawer.assignment.release', 'Release')}
1513
+ </Button>
1514
+ ) : null}
1280
1515
  </div>
1281
1516
  </div>
1282
1517
 
@@ -229,3 +229,24 @@ export function useStaffMemberDirectory(enabled: boolean) {
229
229
  },
230
230
  })
231
231
  }
232
+
233
+ export function boardUnassignedCountQueryKey(scopeVersion: number, timeProjectId: string) {
234
+ return [...BOARD_QUERY_ROOT, 'unassigned-count', `scope:${scopeVersion}`, `project:${timeProjectId}`]
235
+ }
236
+
237
+ /** How many top-level tasks in the project have no assignee — the claimable work the chip counts. */
238
+ export function useUnassignedTaskCount(timeProjectId: string | null | undefined) {
239
+ const scopeVersion = useOrganizationScopeVersion()
240
+ return useQuery<number>({
241
+ queryKey: boardUnassignedCountQueryKey(scopeVersion, timeProjectId ?? ''),
242
+ enabled: !!timeProjectId,
243
+ staleTime: 15_000,
244
+ queryFn: async () => {
245
+ const call = await apiCall<Record<string, unknown>>(
246
+ `/api/staff/timesheets/tasks?timeProjectId=${encodeURIComponent(timeProjectId ?? '')}&unassigned=true&topLevelOnly=true&page=1&pageSize=1`,
247
+ )
248
+ if (!call.ok) throw new Error('[internal] unassigned task count request failed')
249
+ return readTotal(call.result, 0)
250
+ },
251
+ })
252
+ }
@@ -25,12 +25,15 @@ export type BoardFilters = {
25
25
  assigneeStaffMemberId: string | null
26
26
  taskStatusId: string | null
27
27
  tagIds: string[]
28
+ /** Only tasks with no assignee — the claimable work. Mutually exclusive with the assignee filter. */
29
+ unassigned: boolean
28
30
  }
29
31
 
30
32
  export const EMPTY_BOARD_FILTERS: BoardFilters = {
31
33
  assigneeStaffMemberId: null,
32
34
  taskStatusId: null,
33
35
  tagIds: [],
36
+ unassigned: false,
34
37
  }
35
38
 
36
39
  function readOptionalId(value: unknown): string | null {
@@ -50,16 +53,19 @@ export function normalizeBoardFilters(raw: unknown): BoardFilters {
50
53
  ),
51
54
  )
52
55
  : []
56
+ const assigneeStaffMemberId = readOptionalId(source.assigneeStaffMemberId)
53
57
  return {
54
- assigneeStaffMemberId: readOptionalId(source.assigneeStaffMemberId),
58
+ assigneeStaffMemberId,
55
59
  taskStatusId: readOptionalId(source.taskStatusId),
56
60
  tagIds,
61
+ unassigned: assigneeStaffMemberId === null && source.unassigned === true,
57
62
  }
58
63
  }
59
64
 
60
65
  export function countActiveBoardFilters(filters: BoardFilters): number {
61
66
  let count = 0
62
67
  if (filters.assigneeStaffMemberId) count += 1
68
+ if (filters.unassigned) count += 1
63
69
  if (filters.taskStatusId) count += 1
64
70
  return count + filters.tagIds.length
65
71
  }
@@ -80,6 +86,7 @@ export type BoardFilterScope = {
80
86
  */
81
87
  export function boardServerFilterKey(filters: BoardFilters, scope: BoardFilterScope = {}): string {
82
88
  const parts = [`assignee:${filters.assigneeStaffMemberId ?? ''}`]
89
+ if (filters.unassigned) parts.push('unassigned')
83
90
  if (scope.includeStatus) parts.push(`status:${filters.taskStatusId ?? ''}`)
84
91
  parts.push(`tags:${[...filters.tagIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(',')}`)
85
92
  return parts.join('|')
@@ -90,6 +97,8 @@ export function boardServerFilterParams(filters: BoardFilters, scope: BoardFilte
90
97
  let params = ''
91
98
  if (filters.assigneeStaffMemberId) {
92
99
  params += `&assigneeStaffMemberId=${encodeURIComponent(filters.assigneeStaffMemberId)}`
100
+ } else if (filters.unassigned) {
101
+ params += '&unassigned=true'
93
102
  }
94
103
  if (scope.includeStatus && filters.taskStatusId) {
95
104
  params += `&taskStatusId=${encodeURIComponent(filters.taskStatusId)}`
@@ -110,7 +119,12 @@ export function visibleBoardStatusIds<T extends { id: string }>(
110
119
  }
111
120
 
112
121
  export function withAssigneeFilter(filters: BoardFilters, staffMemberId: string | null): BoardFilters {
113
- return { ...filters, assigneeStaffMemberId: readOptionalId(staffMemberId) }
122
+ const assigneeStaffMemberId = readOptionalId(staffMemberId)
123
+ return { ...filters, assigneeStaffMemberId, unassigned: assigneeStaffMemberId ? false : filters.unassigned }
124
+ }
125
+
126
+ export function withUnassignedFilter(filters: BoardFilters, unassigned: boolean): BoardFilters {
127
+ return { ...filters, unassigned, assigneeStaffMemberId: unassigned ? null : filters.assigneeStaffMemberId }
114
128
  }
115
129
 
116
130
  export function withStatusFilter(filters: BoardFilters, taskStatusId: string | null): BoardFilters {
@@ -16,6 +16,7 @@ import { formatDuration } from '../time-tracking/duration'
16
16
  import { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'
17
17
  import { STATUS_POSITION_GAP } from '../timesheets-tasks/statusPositions'
18
18
  import { resolveProjectColorHex } from '../timesheets-ui/colors'
19
+ import { readSupportTicket } from './supportTicketData'
19
20
 
20
21
  export type BoardApiRow = Record<string, unknown>
21
22
 
@@ -44,6 +45,8 @@ export type BoardTask = {
44
45
  closedAt: string | null
45
46
  updatedAt: string | null
46
47
  tagIds: string[]
48
+ /** Set when the task is a customer portal support ticket. */
49
+ supportReference?: string | null
47
50
  }
48
51
 
49
52
  export type SubtaskProgress = {
@@ -123,6 +126,7 @@ export function toBoardTask(row: BoardApiRow): BoardTask | null {
123
126
  closedAt: readString(row, 'closed_at', 'closedAt'),
124
127
  updatedAt: readString(row, 'updated_at', 'updatedAt'),
125
128
  tagIds: readStringArray(row, 'tagIds', 'tag_ids'),
129
+ supportReference: readSupportTicket(row)?.supportReference ?? null,
126
130
  }
127
131
  }
128
132