@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
@@ -30,13 +30,12 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
30
30
  import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
31
31
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
32
32
  import { createLogger } from '@open-mercato/shared/lib/logger'
33
- import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
34
- import { StaffTimeTask, StaffTimeTaskTag } from '../../../data/entities'
33
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
34
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
35
+ import { StaffSupportTicket, StaffTimeTask, StaffTimeTaskStatus, StaffTimeTaskTag } from '../../../data/entities'
35
36
  import { staffTimeTaskCreateSchema, staffTimeTaskUpdateSchema } from '../../../data/validators'
36
37
  import { staffTimeTaskCommandIds } from '../../../commands/timesheets-tasks'
37
- import { resolveFeatureAccess } from '../../../lib/time-tracking/featureAccess'
38
- import { MANAGE_PROJECTS_FEATURE, resolveProjectAccess, type ProjectAccess } from '../../../lib/time-tracking/access'
39
- import { readTimeTrackingSettings } from '../../../lib/time-tracking/settings'
38
+ import { resolveListProjectAccess } from '../../../lib/time-tracking/listProjectAccess'
40
39
  import { sanitizeSearchTerm, parseBooleanFlag } from '../../helpers'
41
40
  import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../../openapi'
42
41
 
@@ -57,6 +56,8 @@ const F = {
57
56
  position: 'position',
58
57
  created_by_user_id: 'created_by_user_id',
59
58
  closed_at: 'closed_at',
59
+ source: 'source',
60
+ customer_id: 'customer_id',
60
61
  created_at: 'created_at',
61
62
  updated_at: 'updated_at',
62
63
  } as const
@@ -87,6 +88,13 @@ const listSchema = z
87
88
  parentTaskId: z.string().uuid().optional(),
88
89
  tagIds: z.string().optional(),
89
90
  topLevelOnly: z.string().optional(),
91
+ unassigned: z.string().optional(),
92
+ source: z.enum(['internal', 'portal']).optional(),
93
+ customerId: z.string().uuid().optional(),
94
+ supportCategory: z.string().optional(),
95
+ supportReference: z.string().optional(),
96
+ portalStatus: z.enum(['received', 'in_progress', 'resolved']).optional(),
97
+ supportUnnotified: z.string().optional(),
90
98
  sortField: z.string().optional(),
91
99
  sortDir: z.enum(['asc', 'desc']).optional(),
92
100
  })
@@ -100,71 +108,30 @@ type TaskListQuery = z.infer<typeof listSchema>
100
108
  */
101
109
  const IMPOSSIBLE_ID = '00000000-0000-0000-0000-000000000000'
102
110
 
103
- const DENIED_ACCESS: ProjectAccess = { canManageAll: false, projectIds: [], staffMemberId: null }
111
+ export { resolveListProjectAccess }
104
112
 
105
- type RbacServiceLike = {
106
- getGrantedFeatures?: (
107
- userId: string,
108
- options: { tenantId: string | null; organizationId: string | null },
109
- ) => Promise<string[]>
110
- }
111
-
112
- type ContainerLike = { resolve: (name: string) => unknown }
113
+ /**
114
+ * Upper bound on the task ids the support filters materialize into the `id IN (...)`
115
+ * narrowing. The newest matching tickets win; an older match beyond the cap drops out
116
+ * of the filtered list instead of growing an unbounded parameter list.
117
+ */
118
+ export const SUPPORT_FILTER_MAX_TASK_IDS = 1000
113
119
 
114
- /** One access decision per request, shared by every filter that needs it. */
115
- const accessByRequest = new WeakMap<Request, Promise<ProjectAccess>>()
120
+ /**
121
+ * Requests whose support-filter narrowing hit `SUPPORT_FILTER_MAX_TASK_IDS`. The list's
122
+ * `afterList` hook reports them as `totalIsCapped`, so a truncated support filter is
123
+ * never presented as the complete result.
124
+ */
125
+ const supportFilterTruncatedRequests = new WeakSet<Request>()
116
126
 
117
- async function resolveAssignmentGraceDays(container: ContainerLike, tenantId: string): Promise<number | null> {
118
- try {
119
- const configService = container.resolve('moduleConfigService') as ModuleConfigService
120
- const settings = await readTimeTrackingSettings(configService, { tenantId })
121
- return settings.access.assignmentGraceDays
122
- } catch {
123
- // Fail safe to the documented default rather than widening the window.
124
- return null
125
- }
127
+ export function markTaskListTotalCapped(payload: Record<string, unknown>, ctx: { request?: Request }): void {
128
+ if (ctx.request && supportFilterTruncatedRequests.has(ctx.request)) payload.totalIsCapped = true
126
129
  }
127
130
 
128
- async function loadProjectAccess(
129
- container: ContainerLike,
130
- userId: string | null,
131
- tenantId: string | null,
132
- organizationId: string | null,
133
- ): Promise<ProjectAccess> {
134
- if (!tenantId || !organizationId) return { ...DENIED_ACCESS }
135
- const scope = { tenantId, organizationId }
136
- try {
137
- // One lookup, one authority, and a failure that says so. The previous
138
- // `catch → []` could not tell "denied" from "could not ask", so an RBAC
139
- // hiccup silently demoted a manager to their own memberships.
140
- const access = await resolveFeatureAccess(container, userId, [MANAGE_PROJECTS_FEATURE], scope)
141
- const em = container.resolve('em') as EntityManager
142
- return await resolveProjectAccess({
143
- em: em.fork(),
144
- userId,
145
- tenantId,
146
- organizationId,
147
- canManageAll: access.allowed,
148
- userFeatures: access.grantedFeatures,
149
- assignmentGraceDays: await resolveAssignmentGraceDays(container, tenantId),
150
- })
151
- } catch (err) {
152
- logger.error('staff.timesheets.tasks access resolution failed', { err })
153
- return { ...DENIED_ACCESS }
154
- }
155
- }
156
-
157
- export async function resolveListProjectAccess(ctx: CrudCtx): Promise<ProjectAccess> {
158
- const tenantId = ctx.auth?.tenantId ?? null
159
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
160
- const load = () => loadProjectAccess(ctx.container, ctx.auth?.sub ?? null, tenantId, organizationId)
161
- const request = ctx.request
162
- if (!request) return load()
163
- const cached = accessByRequest.get(request)
164
- if (cached) return cached
165
- const pending = load()
166
- accessByRequest.set(request, pending)
167
- return pending
131
+ function intersectIds(current: string[] | null, next: string[]): string[] {
132
+ if (current === null) return next
133
+ const allowed = new Set(next)
134
+ return current.filter((id) => allowed.has(id))
168
135
  }
169
136
 
170
137
  function splitIdList(value: unknown): string[] | null {
@@ -210,6 +177,65 @@ async function resolveTaskIdsForTags(ctx: CrudCtx, tagIds: string[]): Promise<st
210
177
  return [...seen.entries()].filter(([, tags]) => tags.size >= required).map(([taskId]) => taskId)
211
178
  } catch (err) {
212
179
  logger.error('staff.timesheets.tasks tag filter resolution failed', { err })
180
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.task_tag_filter_failed' })
181
+ return []
182
+ }
183
+ }
184
+
185
+ type SupportFilterCriteria = {
186
+ supportCategory?: string
187
+ supportReference?: string
188
+ supportUnnotified?: boolean
189
+ }
190
+
191
+ /**
192
+ * Task ids whose support ticket row matches the requested support criteria. Like the
193
+ * tag lookup, a failed lookup narrows to nothing rather than widening the page.
194
+ */
195
+ async function resolveTaskIdsForSupport(ctx: CrudCtx, criteria: SupportFilterCriteria): Promise<string[]> {
196
+ const tenantId = ctx.auth?.tenantId ?? null
197
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
198
+ if (!tenantId || !organizationId) return []
199
+ try {
200
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
201
+ const where: Record<string, unknown> = { tenantId, organizationId, deletedAt: null }
202
+ if (criteria.supportCategory) where.categoryValue = criteria.supportCategory
203
+ if (criteria.supportReference) where.supportReference = { $ilike: `${escapeLikePattern(criteria.supportReference)}%` }
204
+ if (criteria.supportUnnotified) where.unnotifiedAt = { $ne: null }
205
+ const rows = await findWithDecryption(
206
+ em,
207
+ StaffSupportTicket,
208
+ where,
209
+ { fields: ['taskId'], limit: SUPPORT_FILTER_MAX_TASK_IDS + 1, orderBy: { createdAt: 'desc' } },
210
+ { tenantId, organizationId },
211
+ )
212
+ if (rows.length > SUPPORT_FILTER_MAX_TASK_IDS && ctx.request) supportFilterTruncatedRequests.add(ctx.request)
213
+ return Array.from(new Set(rows.slice(0, SUPPORT_FILTER_MAX_TASK_IDS).map((row) => row.taskId)))
214
+ } catch (err) {
215
+ logger.error('staff.timesheets.tasks support filter resolution failed', { err })
216
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.task_support_filter_failed' })
217
+ return []
218
+ }
219
+ }
220
+
221
+ async function resolveStatusIdsForPortalStatus(
222
+ ctx: CrudCtx,
223
+ portalStatus: 'received' | 'in_progress' | 'resolved',
224
+ ): Promise<string[]> {
225
+ const tenantId = ctx.auth?.tenantId ?? null
226
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
227
+ if (!tenantId || !organizationId) return []
228
+ try {
229
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
230
+ const where: Record<string, unknown> = { tenantId, organizationId, deletedAt: null }
231
+ if (portalStatus === 'resolved') where.isDone = true
232
+ else if (portalStatus === 'received') Object.assign(where, { isDone: false, isDefault: true })
233
+ else Object.assign(where, { isDone: false, isDefault: false })
234
+ const rows = await em.find(StaffTimeTaskStatus, where, { fields: ['id'] })
235
+ return rows.map((row) => row.id)
236
+ } catch (err) {
237
+ logger.error('staff.timesheets.tasks portal status filter resolution failed', { err })
238
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.task_portal_status_filter_failed' })
213
239
  return []
214
240
  }
215
241
  }
@@ -228,13 +254,25 @@ export async function buildTaskListFilters(
228
254
  for (const raw of [query.ids, query.id]) {
229
255
  const parsed = splitIdList(raw)
230
256
  if (parsed === null) continue
231
- narrowIds = narrowIds ? narrowIds.filter((id) => parsed.includes(id)) : parsed
257
+ narrowIds = intersectIds(narrowIds, parsed)
232
258
  }
233
259
 
234
260
  const requestedTagIds = splitIdList(query.tagIds)
235
261
  if (requestedTagIds !== null) {
236
262
  const tagged = requestedTagIds.length === 0 ? [] : await resolveTaskIdsForTags(ctx, requestedTagIds)
237
- narrowIds = narrowIds ? narrowIds.filter((id) => tagged.includes(id)) : tagged
263
+ narrowIds = intersectIds(narrowIds, tagged)
264
+ }
265
+
266
+ const supportCategory = typeof query.supportCategory === 'string' ? query.supportCategory.trim() : ''
267
+ const supportReference = sanitizeSearchTerm(query.supportReference) ?? ''
268
+ const supportUnnotified = parseBooleanFlag(query.supportUnnotified)
269
+ if (supportCategory || supportReference || supportUnnotified) {
270
+ const supportTaskIds = await resolveTaskIdsForSupport(ctx, {
271
+ supportCategory: supportCategory || undefined,
272
+ supportReference: supportReference || undefined,
273
+ supportUnnotified,
274
+ })
275
+ narrowIds = intersectIds(narrowIds, supportTaskIds)
238
276
  }
239
277
 
240
278
  const access = await resolveListProjectAccess(ctx)
@@ -266,10 +304,17 @@ export async function buildTaskListFilters(
266
304
 
267
305
  if (typeof query.taskStatusId === 'string' && query.taskStatusId.trim().length > 0) {
268
306
  filters[F.task_status_id] = query.taskStatusId.trim()
307
+ } else if (query.portalStatus) {
308
+ const statusIds = await resolveStatusIdsForPortalStatus(ctx, query.portalStatus)
309
+ filters[F.task_status_id] = { $in: statusIds.length > 0 ? statusIds : [IMPOSSIBLE_ID] }
269
310
  }
270
311
  if (typeof query.assigneeStaffMemberId === 'string' && query.assigneeStaffMemberId.trim().length > 0) {
271
312
  filters[F.assignee_staff_member_id] = query.assigneeStaffMemberId.trim()
313
+ } else if (parseBooleanFlag(query.unassigned)) {
314
+ filters[F.assignee_staff_member_id] = { $eq: null }
272
315
  }
316
+ if (query.source) filters[F.source] = query.source
317
+ if (query.customerId) filters[F.customer_id] = query.customerId
273
318
  if (typeof query.parentTaskId === 'string' && query.parentTaskId.trim().length > 0) {
274
319
  // The drawer's checklist: one parent's children, in position order.
275
320
  filters[F.parent_task_id] = query.parentTaskId.trim()
@@ -320,6 +365,8 @@ export const taskListFields = [
320
365
  F.position,
321
366
  F.created_by_user_id,
322
367
  F.closed_at,
368
+ F.source,
369
+ F.customer_id,
323
370
  F.created_at,
324
371
  F.updated_at,
325
372
  ] as const
@@ -355,6 +402,9 @@ const crud = makeCrudRoute({
355
402
  buildFilters: buildTaskListFilters,
356
403
  },
357
404
  enrichers: { entityId: 'staff:staff_time_task' },
405
+ hooks: {
406
+ afterList: (payload, ctx) => markTaskListTotalCapped(payload, ctx),
407
+ },
358
408
  actions: {
359
409
  create: {
360
410
  commandId: staffTimeTaskCommandIds.create,
@@ -408,6 +458,8 @@ export const taskListItemSchema = z.object({
408
458
  position: z.number().int().nullable().optional(),
409
459
  created_by_user_id: z.string().uuid().nullable().optional(),
410
460
  closed_at: z.string().nullable().optional(),
461
+ source: z.enum(['internal', 'portal']).nullable().optional(),
462
+ customer_id: z.string().uuid().nullable().optional(),
411
463
  created_at: z.string().nullable().optional(),
412
464
  updated_at: z.string().nullable().optional(),
413
465
  /** This task's own entries, in raw minutes. */
@@ -31,6 +31,7 @@ const F = {
31
31
  status: 'status',
32
32
  assigned_start_date: 'assigned_start_date',
33
33
  assigned_end_date: 'assigned_end_date',
34
+ is_assigner: 'is_assigner',
34
35
  created_at: 'created_at',
35
36
  updated_at: 'updated_at',
36
37
  deleted_at: 'deleted_at',
@@ -90,6 +91,7 @@ const crud = makeCrudRoute({
90
91
  F.status,
91
92
  F.assigned_start_date,
92
93
  F.assigned_end_date,
94
+ F.is_assigner,
93
95
  F.created_at,
94
96
  F.updated_at,
95
97
  ],
@@ -201,6 +203,7 @@ const projectMemberListItemSchema = z.object({
201
203
  status: z.string().nullable().optional(),
202
204
  assigned_start_date: z.string().nullable().optional(),
203
205
  assigned_end_date: z.string().nullable().optional(),
206
+ is_assigner: z.boolean().nullable().optional(),
204
207
  created_at: z.string().nullable().optional(),
205
208
  updated_at: z.string().nullable().optional(),
206
209
  })
@@ -47,6 +47,7 @@ const F = {
47
47
  budget_kind: 'budget_kind',
48
48
  budget_value: 'budget_value',
49
49
  budget_warn_at_percent: 'budget_warn_at_percent',
50
+ default_assignee_staff_member_id: 'default_assignee_staff_member_id',
50
51
  created_at: 'created_at',
51
52
  updated_at: 'updated_at',
52
53
  deleted_at: 'deleted_at',
@@ -338,6 +339,7 @@ export const timeProjectListFields = [
338
339
  F.budget_kind,
339
340
  F.budget_value,
340
341
  F.budget_warn_at_percent,
342
+ F.default_assignee_staff_member_id,
341
343
  F.created_at,
342
344
  F.updated_at,
343
345
  ] as const
@@ -563,6 +565,7 @@ export const timeProjectListItemSchema = z.object({
563
565
  budget_kind: z.string().nullable().optional(),
564
566
  budget_value: z.union([z.string(), z.number()]).nullable().optional(),
565
567
  budget_warn_at_percent: z.number().int().nullable().optional(),
568
+ default_assignee_staff_member_id: z.string().uuid().nullable().optional(),
566
569
  created_at: z.string().nullable().optional(),
567
570
  updated_at: z.string().nullable().optional(),
568
571
  entryCount: z.number().int().optional(),
@@ -89,6 +89,7 @@ function toValues(record: ProjectRecord): ProjectFormValues {
89
89
  budgetValue: readString(record, 'budgetValue', 'budget_value'),
90
90
  budgetWarnAtPercent:
91
91
  readString(record, 'budgetWarnAtPercent', 'budget_warn_at_percent') ?? String(DEFAULT_BUDGET_WARN_AT_PERCENT),
92
+ defaultAssigneeStaffMemberId: readString(record, 'defaultAssigneeStaffMemberId', 'default_assignee_staff_member_id') ?? null,
92
93
  projectType: readString(record, 'projectType', 'project_type'),
93
94
  color: readString(record, 'color'),
94
95
  startDate: readString(record, 'startDate', 'start_date'),
@@ -11,8 +11,10 @@ import { Input } from '@open-mercato/ui/primitives/input'
11
11
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
12
12
  import { LookupSelect } from '@open-mercato/ui/backend/inputs/LookupSelect'
13
13
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
14
- import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
15
- import { deleteCrud, createCrud } from '@open-mercato/ui/backend/utils/crud'
14
+ import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
15
+ import { deleteCrud, createCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
16
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
17
+ import { Switch } from '@open-mercato/ui/primitives/switch'
16
18
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
17
19
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
18
20
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
@@ -102,6 +104,9 @@ type EmployeeAssignment = {
102
104
  assignedEndDate: string | null
103
105
  displayName: string | null
104
106
  teamName: string | null
107
+ isAssigner: boolean
108
+ hasUserAccount: boolean | null
109
+ updatedAt: string | null
105
110
  }
106
111
 
107
112
  type EmployeesResponse = {
@@ -113,6 +118,8 @@ type StaffMemberRecord = {
113
118
  id: string
114
119
  display_name?: string
115
120
  displayName?: string
121
+ user_id?: string | null
122
+ userId?: string | null
116
123
  team?: { id: string; name: string } | null
117
124
  }
118
125
 
@@ -475,6 +482,9 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
475
482
  : null,
476
483
  displayName: staff?.display_name ?? staff?.displayName ?? null,
477
484
  teamName: staff?.team?.name ?? null,
485
+ isAssigner: item.is_assigner === true || item.isAssigner === true,
486
+ hasUserAccount: staff ? Boolean(staff.user_id ?? staff.userId) : null,
487
+ updatedAt: typeof (item.updated_at ?? item.updatedAt) === 'string' ? String(item.updated_at ?? item.updatedAt) : null,
478
488
  }
479
489
  })
480
490
 
@@ -562,6 +572,37 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
562
572
  }
563
573
  }, [projectId, confirm, employeeMutationContext, runMutation, t])
564
574
 
575
+ const handleToggleAssigner = React.useCallback(async (emp: EmployeeAssignment, nextValue: boolean) => {
576
+ const payload = { id: emp.id, isAssigner: nextValue }
577
+ try {
578
+ await runMutation({
579
+ operation: () =>
580
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(emp.updatedAt), () =>
581
+ updateCrud(`staff/timesheets/time-projects/${projectId}/employees`, payload, {
582
+ errorMessage: t('staff.time_tracking.assigners.saveError', 'Could not change the assigner setting.'),
583
+ }),
584
+ ),
585
+ context: employeeMutationContext(emp.id),
586
+ mutationPayload: payload,
587
+ })
588
+ flash(
589
+ nextValue
590
+ ? t('staff.time_tracking.assigners.enabled', 'Member is now an assigner.')
591
+ : t('staff.time_tracking.assigners.disabled', 'Member is no longer an assigner.'),
592
+ 'success',
593
+ )
594
+ setReloadToken((token) => token + 1)
595
+ } catch (toggleError) {
596
+ if (surfaceRecordConflict(toggleError, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return
597
+ flash(
598
+ toggleError instanceof Error && toggleError.message
599
+ ? toggleError.message
600
+ : t('staff.time_tracking.assigners.saveError', 'Could not change the assigner setting.'),
601
+ 'error',
602
+ )
603
+ }
604
+ }, [projectId, employeeMutationContext, runMutation, t])
605
+
565
606
  // --- Add employee dialog ---
566
607
  const openAddDialog = React.useCallback(() => {
567
608
  setAddStaffMemberId(null)
@@ -1104,6 +1145,11 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
1104
1145
  </div>
1105
1146
  </div>
1106
1147
  <div className="flex items-center gap-3 shrink-0">
1148
+ {emp.isAssigner ? (
1149
+ <Badge variant="info" data-testid={`project-member-assigner-badge-${emp.id}`}>
1150
+ {t('staff.time_tracking.assigners.badge', 'Assigner')}
1151
+ </Badge>
1152
+ ) : null}
1107
1153
  <Badge
1108
1154
  variant={(emp.status ?? 'active') === 'active' ? 'success' : 'neutral'}
1109
1155
  className="capitalize"
@@ -1155,6 +1201,25 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
1155
1201
  <dd>{emp.teamName}</dd>
1156
1202
  </div>
1157
1203
  ) : null}
1204
+ <div className="col-span-2">
1205
+ <dt className="font-medium text-muted-foreground">
1206
+ {t('staff.time_tracking.assigners.label', 'Assigner')}
1207
+ </dt>
1208
+ <dd className="flex items-center gap-2 pt-1">
1209
+ <Switch
1210
+ checked={emp.isAssigner}
1211
+ disabled={!canManageProjects || emp.hasUserAccount === false}
1212
+ onCheckedChange={(checked) => { void handleToggleAssigner(emp, checked === true) }}
1213
+ aria-label={t('staff.time_tracking.assigners.label', 'Assigner')}
1214
+ data-testid={`project-member-assigner-switch-${emp.id}`}
1215
+ />
1216
+ <span className="text-xs text-muted-foreground">
1217
+ {emp.hasUserAccount === false
1218
+ ? t('staff.time_tracking.assigners.needsUser', 'Only members with a user account can be assigners.')
1219
+ : t('staff.time_tracking.assigners.help', 'Assigners are notified about new and released work and can claim unassigned tasks.')}
1220
+ </span>
1221
+ </dd>
1222
+ </div>
1158
1223
  </dl>
1159
1224
  {canManageProjects && (
1160
1225
  <div className="flex justify-end">
@@ -15,6 +15,7 @@ import {
15
15
  ProjectBudgetSection,
16
16
  ProjectRoundingNotice,
17
17
  ProjectTeamSection,
18
+ ProjectDefaultAssigneeField,
18
19
  type ProjectBudgetKind,
19
20
  } from '../../../../lib/time-tracking-ui/ProjectFormSections'
20
21
 
@@ -43,6 +44,7 @@ export type ProjectFormValues = {
43
44
  startDate?: string | null
44
45
  costCenter?: string | null
45
46
  status?: string
47
+ defaultAssigneeStaffMemberId?: string | null
46
48
  /**
47
49
  * Custom-field values rendered by `CrudForm` from `entityIds`. They are keyed
48
50
  * `cf_<key>` / `cf:<key>` and are opaque here: this form neither declares nor
@@ -112,6 +114,7 @@ export function createProjectFormSchema() {
112
114
  startDate: z.string().optional().nullable(),
113
115
  costCenter: z.string().max(100).optional().nullable(),
114
116
  status: z.enum(['active', 'on_hold', 'completed']).optional(),
117
+ defaultAssigneeStaffMemberId: z.string().optional().nullable(),
115
118
  })
116
119
  }
117
120
 
@@ -381,9 +384,22 @@ export function createProjectFormGroups(
381
384
  id: 'team',
382
385
  column: 2,
383
386
  title: t('staff.time_tracking.projects.form.groupTeam', 'Team'),
384
- component: (ctx) => React.createElement(ProjectTeamSection, {
385
- projectId: typeof ctx.values.id === 'string' ? ctx.values.id : null,
386
- }),
387
+ component: (ctx) => {
388
+ const projectId = typeof ctx.values.id === 'string' ? ctx.values.id : null
389
+ const defaultAssignee = ctx.values.defaultAssigneeStaffMemberId
390
+ return React.createElement(
391
+ React.Fragment,
392
+ null,
393
+ React.createElement(ProjectTeamSection, { projectId }),
394
+ projectId
395
+ ? React.createElement(ProjectDefaultAssigneeField, {
396
+ projectId,
397
+ value: typeof defaultAssignee === 'string' && defaultAssignee.length > 0 ? defaultAssignee : null,
398
+ onChange: (next: string | null) => ctx.setValue('defaultAssigneeStaffMemberId', next),
399
+ })
400
+ : null,
401
+ )
402
+ },
387
403
  },
388
404
  {
389
405
  id: 'rounding',
@@ -456,6 +472,9 @@ export function buildProjectPayload(values: ProjectFormValues): Record<string, u
456
472
  payload.currencyCode = trimmedOrNull(values.currencyCode)
457
473
  } else {
458
474
  payload.id = values.id
475
+ if (values.defaultAssigneeStaffMemberId !== undefined) {
476
+ payload.defaultAssigneeStaffMemberId = values.defaultAssigneeStaffMemberId || null
477
+ }
459
478
  }
460
479
  if (values.status) payload.status = values.status
461
480
  // The form renders custom-field inputs whenever `entityIds` is passed, but this