@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
@@ -11,12 +11,14 @@ import {
11
11
  useStaffMemberDirectory,
12
12
  useTagDirectory,
13
13
  useTagLabels,
14
+ useUnassignedTaskCount,
14
15
  type BoardSelf,
15
16
  } from './boardDirectory'
16
17
  import {
17
18
  countActiveBoardFilters,
18
19
  withAssigneeFilter,
19
20
  withStatusFilter,
21
+ withUnassignedFilter,
20
22
  withTagFilter,
21
23
  withoutTagFilter,
22
24
  type BoardFilters,
@@ -43,6 +45,7 @@ type BoardFilterSurfaceProps = {
43
45
  onChange: (next: BoardFilters) => void
44
46
  self: BoardSelf | null
45
47
  statuses: readonly BoardStatus[]
48
+ timeProjectId?: string | null
46
49
  }
47
50
 
48
51
  function chipClassName(dashed: boolean): string {
@@ -122,6 +125,7 @@ export function AddBoardFilterPopover({
122
125
  onChange,
123
126
  self,
124
127
  statuses,
128
+ timeProjectId,
125
129
  trigger,
126
130
  testId = 'board-filter-add',
127
131
  }: BoardFilterSurfaceProps & {
@@ -140,6 +144,11 @@ export function AddBoardFilterPopover({
140
144
  )
141
145
 
142
146
  const assignedToMeLabel = t('staff.time_tracking.board.filters.assignedToMe', 'Assigned to me')
147
+ const unassignedCount = useUnassignedTaskCount(timeProjectId)
148
+ const unassignedLabel =
149
+ typeof unassignedCount.data === 'number'
150
+ ? t('staff.time_tracking.board.filters.unassignedWithCount', 'Unassigned ({count})', { count: unassignedCount.data })
151
+ : t('staff.time_tracking.board.filters.unassigned', 'Unassigned')
143
152
 
144
153
  return (
145
154
  <Popover open={open} onOpenChange={setOpen}>
@@ -164,6 +173,16 @@ export function AddBoardFilterPopover({
164
173
  />
165
174
  ) : null}
166
175
 
176
+ <PickerRow
177
+ label={unassignedLabel}
178
+ selected={filters.unassigned}
179
+ testId="board-filter-unassigned"
180
+ onSelect={() => {
181
+ onChange(withUnassignedFilter(filters, !filters.unassigned))
182
+ setOpen(false)
183
+ }}
184
+ />
185
+
167
186
  {otherMembers.length > 0 ? (
168
187
  <div className="mt-2 flex flex-col gap-0.5">
169
188
  <span className="px-2.5 py-1 text-xs font-semibold uppercase leading-normal tracking-wide text-muted-foreground">
@@ -256,8 +275,10 @@ export function BoardFilterChips({
256
275
  onChange,
257
276
  self,
258
277
  statuses,
278
+ timeProjectId,
259
279
  }: BoardFilterSurfaceProps): React.ReactElement {
260
280
  const t = useT()
281
+ const unassignedCount = useUnassignedTaskCount(filters.unassigned ? timeProjectId : null)
261
282
 
262
283
  const assigneeIds = React.useMemo(
263
284
  () =>
@@ -300,6 +321,19 @@ export function BoardFilterChips({
300
321
  />
301
322
  ) : null}
302
323
 
324
+ {filters.unassigned ? (
325
+ <FilterChip
326
+ label={
327
+ typeof unassignedCount.data === 'number'
328
+ ? t('staff.time_tracking.board.filters.unassignedWithCount', 'Unassigned ({count})', { count: unassignedCount.data })
329
+ : t('staff.time_tracking.board.filters.unassigned', 'Unassigned')
330
+ }
331
+ removeLabel={removeLabel(t('staff.time_tracking.board.filters.unassigned', 'Unassigned'))}
332
+ testId="board-filter-chip-unassigned"
333
+ onRemove={() => onChange(withUnassignedFilter(filters, false))}
334
+ />
335
+ ) : null}
336
+
303
337
  {activeStatus ? (
304
338
  <FilterChip
305
339
  label={activeStatus.name}
@@ -327,6 +361,7 @@ export function BoardFilterChips({
327
361
  onChange={onChange}
328
362
  self={self}
329
363
  statuses={statuses}
364
+ timeProjectId={timeProjectId}
330
365
  trigger={
331
366
  <Button
332
367
  type="button"
@@ -346,7 +381,7 @@ export function BoardFilterChips({
346
381
  type="button"
347
382
  variant="link"
348
383
  size="2xs"
349
- onClick={() => onChange({ assigneeStaffMemberId: null, taskStatusId: null, tagIds: [] })}
384
+ onClick={() => onChange({ assigneeStaffMemberId: null, taskStatusId: null, tagIds: [], unassigned: false })}
350
385
  data-testid="board-filter-clear"
351
386
  className="h-auto px-1 text-xs leading-normal text-muted-foreground hover:text-foreground"
352
387
  >
@@ -71,6 +71,7 @@ import {
71
71
  type BoardApiRow,
72
72
  type BoardTask,
73
73
  } from './kanbanBoardData'
74
+ import { SUPPORT_ACTIVITY_EVENT } from './supportTicketData'
74
75
 
75
76
  const logger = createLogger('staff').child({ component: 'KanbanBoard' })
76
77
 
@@ -81,6 +82,7 @@ export const CREATE_MUTATION_CONTEXT_ID = 'staff.time_tracking.board.create'
81
82
  export const MANAGE_COLUMNS_FEATURE = 'staff.timesheets.projects.manage'
82
83
  export const MANAGE_TASKS_FEATURE = 'staff.timesheets.tasks.manage'
83
84
  export const TASK_STATUS_CHANGED_EVENT = 'staff.timesheets.time_task.status_changed'
85
+ export const TASK_ASSIGNEE_CHANGED_EVENT = 'staff.timesheets.time_task.assignee_changed'
84
86
 
85
87
  /** Window a locally-originated move stays "mine" for, so a lost SSE cannot wedge the filter. */
86
88
  const OWN_MOVE_ECHO_TTL_MS = 15_000
@@ -364,6 +366,10 @@ export function KanbanBoard({
364
366
  queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'children'] }).catch(() => {})
365
367
  }, [queryClient])
366
368
 
369
+ const invalidateUnassignedCount = React.useCallback(() => {
370
+ queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'unassigned-count'] }).catch(() => {})
371
+ }, [queryClient])
372
+
367
373
  const invalidateStatuses = React.useCallback(() => {
368
374
  queryClient.invalidateQueries({ queryKey: [...BOARD_QUERY_ROOT, 'statuses'] }).catch(() => {})
369
375
  }, [queryClient])
@@ -406,6 +412,30 @@ export function KanbanBoard({
406
412
  [consumeOwnMove, invalidateBoard, timeProjectId],
407
413
  )
408
414
 
415
+ useAppEvent(
416
+ TASK_ASSIGNEE_CHANGED_EVENT,
417
+ (event) => {
418
+ const eventPayload = event.payload as Record<string, unknown> | null | undefined
419
+ const projectId = typeof eventPayload?.timeProjectId === 'string' ? eventPayload.timeProjectId : null
420
+ if (projectId && projectId !== timeProjectId) return
421
+ invalidateBoard()
422
+ invalidateUnassignedCount()
423
+ },
424
+ [invalidateBoard, invalidateUnassignedCount, timeProjectId],
425
+ )
426
+
427
+ useAppEvent(
428
+ SUPPORT_ACTIVITY_EVENT,
429
+ (event) => {
430
+ const eventPayload = event.payload as Record<string, unknown> | null | undefined
431
+ const projectId = typeof eventPayload?.projectId === 'string' ? eventPayload.projectId : null
432
+ if (projectId && projectId !== timeProjectId) return
433
+ invalidateBoard()
434
+ invalidateUnassignedCount()
435
+ },
436
+ [invalidateBoard, invalidateUnassignedCount, timeProjectId],
437
+ )
438
+
409
439
  const setTaskPending = React.useCallback((taskId: string, pending: boolean) => {
410
440
  setPendingTaskIds((previous) => {
411
441
  const next = new Set(previous)
@@ -227,6 +227,15 @@ function KanbanCardImpl({
227
227
  >
228
228
  <div className="text-sm font-semibold leading-normal text-foreground">{task.title}</div>
229
229
 
230
+ {task.supportReference ? (
231
+ <div className="flex flex-wrap items-center gap-1" data-testid="task-card-support">
232
+ <Badge variant="brand" size="sm">
233
+ {t('staff.time_tracking.board.card.customerRequest', 'Customer request')}
234
+ </Badge>
235
+ <span className="text-xs font-medium text-muted-foreground">{task.supportReference}</span>
236
+ </div>
237
+ ) : null}
238
+
230
239
  {tags.length > 0 ? (
231
240
  <div className="flex flex-wrap gap-1">
232
241
  {tags.map((tag) => (
@@ -8,9 +8,21 @@ import { Avatar } from '@open-mercato/ui/primitives/avatar'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
9
  import { Input } from '@open-mercato/ui/primitives/input'
10
10
  import { Label } from '@open-mercato/ui/primitives/label'
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
11
12
  import { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'
12
13
  import { SwitchField } from '@open-mercato/ui/primitives/switch-field'
13
14
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
15
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
16
+ import { resolveGraceDays } from './projectTeamAssignment'
17
+ import {
18
+ DEFAULT_ASSIGNEE_PAGE_SIZE,
19
+ chunkIds,
20
+ classifyDefaultAssigneeCandidates,
21
+ collectProjectMemberships,
22
+ readProjectMembershipPage,
23
+ resolveSelectedDefaultAssignee,
24
+ type ProjectMemberRow,
25
+ } from './projectDefaultAssignee'
14
26
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
27
  import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
16
28
 
@@ -122,12 +134,7 @@ export function ProjectBudgetSection({
122
134
  )
123
135
  }
124
136
 
125
- type ProjectMemberRow = {
126
- id?: unknown
127
- staffMemberId?: unknown
128
- staff_member_id?: unknown
129
- status?: unknown
130
- }
137
+ export { isUsableDefaultAssigneeMembership } from './projectDefaultAssignee'
131
138
 
132
139
  type TeamMemberRow = {
133
140
  id?: unknown
@@ -321,3 +328,175 @@ export function ProjectRoundingNotice() {
321
328
 
322
329
  /** Exported for unit tests only. */
323
330
  export const __testing = { memberLabel }
331
+
332
+ const NO_DEFAULT_ASSIGNEE = '__none__'
333
+
334
+ export type ProjectDefaultAssigneeFieldProps = {
335
+ projectId: string | null
336
+ value: string | null
337
+ onChange: (value: string | null) => void
338
+ disabled?: boolean
339
+ }
340
+
341
+ type DefaultAssigneeOption = { id: string; label: string; active: boolean }
342
+
343
+ /**
344
+ * The project's default assignee for customer portal tickets. Only active project
345
+ * members are offered; a stored default whose membership has since gone inactive or
346
+ * been removed stays visible with a warning, because new tickets then fall to the
347
+ * project's assigners instead of failing.
348
+ */
349
+ export function ProjectDefaultAssigneeField({ projectId, value, onChange, disabled }: ProjectDefaultAssigneeFieldProps) {
350
+ const t = useT()
351
+ const [options, setOptions] = React.useState<DefaultAssigneeOption[]>([])
352
+ const [loadState, setLoadState] = React.useState<'loading' | 'loaded' | 'failed'>('loading')
353
+ const [reloadToken, setReloadToken] = React.useState(0)
354
+ const [truncated, setTruncated] = React.useState(false)
355
+ const [extraLabels, setExtraLabels] = React.useState<Map<string, string>>(new Map())
356
+ const valueRef = React.useRef(value)
357
+ valueRef.current = value
358
+
359
+ React.useEffect(() => {
360
+ if (!projectId) {
361
+ setOptions([])
362
+ setTruncated(false)
363
+ setExtraLabels(new Map())
364
+ setLoadState('loaded')
365
+ return
366
+ }
367
+ let cancelled = false
368
+ setLoadState('loading')
369
+ async function load() {
370
+ try {
371
+ const [collection, settingsRes] = await Promise.all([
372
+ collectProjectMemberships(async (page) => {
373
+ const membersRes = await apiCall<{ items?: unknown; totalPages?: unknown }>(
374
+ `/api/staff/timesheets/time-projects/${projectId}/employees?page=${page}&pageSize=${DEFAULT_ASSIGNEE_PAGE_SIZE}`,
375
+ )
376
+ return readProjectMembershipPage(membersRes)
377
+ }),
378
+ apiCall<{ access?: { assignmentGraceDays?: unknown } | null }>('/api/staff/timesheets/settings'),
379
+ ])
380
+ const graceDays = resolveGraceDays(settingsRes.ok ? settingsRes.result?.access?.assignmentGraceDays : null)
381
+ const candidates = classifyDefaultAssigneeCandidates(collection.rows, graceDays)
382
+ if (!cancelled) setTruncated(collection.truncated)
383
+ const selectedId = valueRef.current
384
+ const labelIds = Array.from(new Set([
385
+ ...candidates.map((candidate) => candidate.staffMemberId),
386
+ ...(selectedId ? [selectedId] : []),
387
+ ]))
388
+ if (labelIds.length === 0) {
389
+ if (!cancelled) {
390
+ setOptions([])
391
+ setExtraLabels(new Map())
392
+ setLoadState('loaded')
393
+ }
394
+ return
395
+ }
396
+ const staffResults = await Promise.all(
397
+ chunkIds(labelIds).map((ids) =>
398
+ apiCall<{ items?: TeamMemberRow[] }>(
399
+ `/api/staff/team-members?ids=${ids.join(',')}&pageSize=${DEFAULT_ASSIGNEE_PAGE_SIZE}`,
400
+ ),
401
+ ),
402
+ )
403
+ const staffRows = staffResults.flatMap((staffRes) => (Array.isArray(staffRes.result?.items) ? staffRes.result.items : []))
404
+ const labels = new Map(
405
+ staffRows
406
+ .map((row) => [readString(row.id), memberLabel(row)] as const)
407
+ .filter((entry): entry is readonly [string, string] => entry[0] !== null),
408
+ )
409
+ if (!cancelled) {
410
+ setOptions(candidates.map((candidate) => ({
411
+ id: candidate.staffMemberId,
412
+ label: labels.get(candidate.staffMemberId) || candidate.staffMemberId,
413
+ active: candidate.active,
414
+ })))
415
+ setExtraLabels(labels)
416
+ setLoadState('loaded')
417
+ }
418
+ } catch {
419
+ if (!cancelled) setLoadState('failed')
420
+ }
421
+ }
422
+ void load()
423
+ return () => { cancelled = true }
424
+ }, [projectId, reloadToken])
425
+
426
+ const retryLoad = React.useCallback(() => setReloadToken((token) => token + 1), [])
427
+
428
+ const selection = resolveSelectedDefaultAssignee(
429
+ options.map((option) => ({ staffMemberId: option.id, active: option.active })),
430
+ value,
431
+ truncated,
432
+ )
433
+ const selected = value ? options.find((option) => option.id === value) ?? null : null
434
+ const selectedLabel = selected?.label ?? (value ? extraLabels.get(value) || value : null)
435
+ const loadFailed = loadState === 'failed'
436
+ const activeOptions = loadFailed ? [] : options.filter((option) => option.active)
437
+ const showWarning = loadState === 'loaded' && selection.unusable
438
+
439
+ return (
440
+ <div className="flex flex-col gap-2" data-testid="project-default-assignee">
441
+ <Label htmlFor="project-default-assignee-select">
442
+ {t('staff.time_tracking.assigners.defaultAssignee', 'Default assignee for portal tickets')}
443
+ </Label>
444
+ <Select
445
+ value={value ?? NO_DEFAULT_ASSIGNEE}
446
+ disabled={disabled || !projectId}
447
+ onValueChange={(next) => onChange(next === NO_DEFAULT_ASSIGNEE ? null : next)}
448
+ >
449
+ <SelectTrigger id="project-default-assignee-select" data-testid="project-default-assignee-select">
450
+ <SelectValue />
451
+ </SelectTrigger>
452
+ <SelectContent>
453
+ <SelectItem value={NO_DEFAULT_ASSIGNEE}>
454
+ {t('staff.time_tracking.assigners.defaultAssigneeNone', 'No default assignee')}
455
+ </SelectItem>
456
+ {activeOptions.map((option) => (
457
+ <SelectItem key={option.id} value={option.id}>
458
+ {option.label}
459
+ </SelectItem>
460
+ ))}
461
+ {value && (loadFailed || !selected || !selected.active) ? (
462
+ <SelectItem value={value}>{selectedLabel ?? value}</SelectItem>
463
+ ) : null}
464
+ </SelectContent>
465
+ </Select>
466
+ <p className="text-xs text-muted-foreground">
467
+ {t(
468
+ 'staff.time_tracking.assigners.defaultAssigneeHelp',
469
+ 'New customer portal tickets routed to this project are assigned to this member. Tasks created on the board stay assigned to their creator.',
470
+ )}
471
+ </p>
472
+ {loadFailed ? (
473
+ <ErrorMessage
474
+ label={t(
475
+ 'staff.time_tracking.assigners.defaultAssigneeLoadFailed',
476
+ 'Could not load the project members, so the default assignee cannot be checked right now.',
477
+ )}
478
+ action={(
479
+ <Button type="button" variant="outline" size="sm" onClick={retryLoad} data-testid="project-default-assignee-retry">
480
+ {t('staff.time_tracking.assigners.defaultAssigneeRetry', 'Retry')}
481
+ </Button>
482
+ )}
483
+ />
484
+ ) : null}
485
+ {showWarning ? (
486
+ <Alert status="warning">
487
+ <AlertDescription>
488
+ {t(
489
+ 'staff.time_tracking.assigners.defaultAssigneeInactive',
490
+ 'This member is no longer an active project member. New tickets will be left unassigned and the assigners notified.',
491
+ )}
492
+ </AlertDescription>
493
+ </Alert>
494
+ ) : null}
495
+ {!projectId ? (
496
+ <p className="text-xs text-muted-foreground">
497
+ {t('staff.time_tracking.assigners.defaultAssigneeAfterCreate', 'Save the project and add members to choose a default assignee.')}
498
+ </p>
499
+ ) : null}
500
+ </div>
501
+ )
502
+ }
@@ -0,0 +1,188 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
5
+ import { Download, Paperclip } from 'lucide-react'
6
+ import { Badge } from '@open-mercato/ui/primitives/badge'
7
+ import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
9
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
10
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
+ import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
12
+ import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
13
+ import { hasFeature } from '@open-mercato/shared/security/features'
14
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
15
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
16
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
17
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
18
+ import { TASK_DRAWER_QUERY_ROOT } from './taskDrawerData'
19
+
20
+ const ENTITY_ID = 'staff:staff_time_task'
21
+ const CUSTOMER_VISIBLE_TAG = 'customer-visible'
22
+ const CUSTOMER_UPLOAD_PREFIX = 'customer-upload:'
23
+ const MUTATION_CONTEXT_ID = 'staff.task_drawer.support_attachments'
24
+ const ATTACHMENTS_MANAGE_FEATURE = 'attachments.manage'
25
+
26
+ type StaffAttachment = {
27
+ id: string
28
+ url: string
29
+ fileName: string
30
+ fileSize: number
31
+ tags?: string[]
32
+ }
33
+
34
+ type SupportAttachmentsSectionProps = {
35
+ taskId: string
36
+ }
37
+
38
+ function formatSize(bytes: number): string {
39
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
40
+ if (bytes < 1024) return `${bytes} B`
41
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
42
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
43
+ }
44
+
45
+ /**
46
+ * Files on a portal ticket. Customer uploads and files staff chose to share carry
47
+ * the `customer-visible` tag and are the only ones the portal lists; everything
48
+ * else stays internal. "Share with customer" is unchecked by default.
49
+ */
50
+ export function SupportAttachmentsSection({ taskId }: SupportAttachmentsSectionProps) {
51
+ const t = useT()
52
+ const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
53
+ contextId: MUTATION_CONTEXT_ID,
54
+ blockedMessage: t('staff.support.attachments.uploadFailed', 'Could not upload the file.'),
55
+ })
56
+ const { payload } = useBackendChrome()
57
+ const canUpload = hasFeature(payload?.grantedFeatures, ATTACHMENTS_MANAGE_FEATURE)
58
+ const queryClient = useQueryClient()
59
+ const scopeVersion = useOrganizationScopeVersion()
60
+ const [uploading, setUploading] = React.useState(false)
61
+ const [share, setShare] = React.useState(false)
62
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
63
+
64
+ const attachmentsQueryKey = React.useMemo(
65
+ () => [...TASK_DRAWER_QUERY_ROOT, 'attachments', `scope:${scopeVersion}`, taskId],
66
+ [scopeVersion, taskId],
67
+ )
68
+ const attachmentsQuery = useQuery<StaffAttachment[]>({
69
+ queryKey: attachmentsQueryKey,
70
+ enabled: taskId.length > 0,
71
+ queryFn: async () => {
72
+ const response = await apiCall<{ items?: StaffAttachment[] }>(
73
+ `/api/attachments?entityId=${encodeURIComponent(ENTITY_ID)}&recordId=${encodeURIComponent(taskId)}`,
74
+ )
75
+ if (!response.ok) throw new Error('[internal] attachments request failed')
76
+ return response.result?.items ?? []
77
+ },
78
+ })
79
+ const items = attachmentsQuery.data ?? []
80
+
81
+ const refreshAttachments = React.useCallback(async () => {
82
+ await queryClient.invalidateQueries({ queryKey: attachmentsQueryKey })
83
+ }, [attachmentsQueryKey, queryClient])
84
+
85
+ const upload = React.useCallback(async (list: FileList | null) => {
86
+ if (!list || !list.length || !canUpload) return
87
+ setUploading(true)
88
+ try {
89
+ for (const file of Array.from(list)) {
90
+ const form = new FormData()
91
+ form.set('entityId', ENTITY_ID)
92
+ form.set('recordId', taskId)
93
+ form.set('file', file)
94
+ if (share) form.set('tags', JSON.stringify([CUSTOMER_VISIBLE_TAG]))
95
+ const result = await runMutation({
96
+ operation: () => apiCall('/api/attachments', { method: 'POST', body: form }),
97
+ // Like the rest of the task drawer, an upload carries no record-lock
98
+ // resource: it adds a row rather than editing one.
99
+ context: {
100
+ formId: `${MUTATION_CONTEXT_ID}:${taskId}`,
101
+ retryLastMutation,
102
+ },
103
+ mutationPayload: { taskId, fileName: file.name, shared: share },
104
+ })
105
+ if (!result.ok) flash(t('staff.support.attachments.uploadFailed', 'Could not upload the file.'), 'error')
106
+ }
107
+ await refreshAttachments()
108
+ } catch {
109
+ flash(t('staff.support.attachments.uploadFailed', 'Could not upload the file.'), 'error')
110
+ } finally {
111
+ setUploading(false)
112
+ if (inputRef.current) inputRef.current.value = ''
113
+ }
114
+ }, [canUpload, refreshAttachments, retryLastMutation, runMutation, share, t, taskId])
115
+
116
+ return (
117
+ <section className="space-y-2" data-testid="support-attachments-section">
118
+ <div className="flex items-center justify-between gap-2">
119
+ <h3 className="text-sm font-semibold text-foreground">{t('staff.support.attachments.title', 'Ticket files')}</h3>
120
+ {canUpload ? (
121
+ <div className="flex items-center gap-3">
122
+ <label className="flex items-center gap-2 text-sm text-muted-foreground">
123
+ <Checkbox
124
+ checked={share}
125
+ onCheckedChange={(value) => setShare(value === true)}
126
+ aria-label={t('staff.support.attachments.shareWithCustomer', 'Share with customer')}
127
+ />
128
+ {t('staff.support.attachments.shareWithCustomer', 'Share with customer')}
129
+ </label>
130
+ <input
131
+ ref={inputRef}
132
+ type="file"
133
+ multiple
134
+ className="sr-only"
135
+ data-testid="support-attachments-input"
136
+ onChange={(event) => void upload(event.target.files)}
137
+ />
138
+ <Button type="button" variant="outline" size="sm" disabled={uploading} onClick={() => inputRef.current?.click()}>
139
+ {uploading ? <Spinner className="mr-2 h-4 w-4" /> : <Paperclip className="mr-2 h-4 w-4" />}
140
+ {t('staff.support.attachments.upload', 'Upload')}
141
+ </Button>
142
+ </div>
143
+ ) : null}
144
+ </div>
145
+ {attachmentsQuery.isLoading ? (
146
+ <LoadingMessage label={t('staff.support.attachments.loading', 'Loading files…')} />
147
+ ) : attachmentsQuery.isError ? (
148
+ <ErrorMessage
149
+ label={t('staff.support.attachments.loadFailed', 'Could not load the ticket files.')}
150
+ action={(
151
+ <Button type="button" variant="outline" size="sm" onClick={() => { void attachmentsQuery.refetch() }}>
152
+ {t('staff.support.attachments.retry', 'Try again')}
153
+ </Button>
154
+ )}
155
+ />
156
+ ) : items.length === 0 ? (
157
+ <p className="text-sm text-muted-foreground">{t('staff.support.attachments.empty', 'No files on this ticket.')}</p>
158
+ ) : (
159
+ <ul className="space-y-1">
160
+ {items.map((item) => {
161
+ const tags = item.tags ?? []
162
+ const visible = tags.includes(CUSTOMER_VISIBLE_TAG)
163
+ const fromCustomer = tags.some((tag) => tag.startsWith(CUSTOMER_UPLOAD_PREFIX))
164
+ return (
165
+ <li key={item.id} className="flex items-center justify-between gap-2 text-sm">
166
+ <a className="flex min-w-0 items-center gap-2 text-foreground hover:underline" href={item.url}>
167
+ <Download className="h-4 w-4 shrink-0" />
168
+ <span className="truncate">{item.fileName}</span>
169
+ <span className="shrink-0 text-muted-foreground">{formatSize(item.fileSize)}</span>
170
+ </a>
171
+ <div className="flex shrink-0 gap-1">
172
+ {fromCustomer ? (
173
+ <Badge variant="outline">{t('staff.support.attachments.fromCustomer', 'From customer')}</Badge>
174
+ ) : null}
175
+ <Badge variant={visible ? 'secondary' : 'outline'}>
176
+ {visible
177
+ ? t('staff.support.attachments.visible', 'Visible to customer')
178
+ : t('staff.support.attachments.internal', 'Internal')}
179
+ </Badge>
180
+ </div>
181
+ </li>
182
+ )
183
+ })}
184
+ </ul>
185
+ )}
186
+ </section>
187
+ )
188
+ }
@@ -0,0 +1,102 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Badge } from '@open-mercato/ui/primitives/badge'
5
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { SUPPORT_PORTAL_STATUS_VARIANT, type SupportTicketView } from './supportTicketData'
8
+
9
+ export function supportPortalStatusLabel(
10
+ t: ReturnType<typeof useT>,
11
+ status: SupportTicketView['portalStatus'],
12
+ ): string {
13
+ if (status === 'resolved') return t('staff.support.tickets.status.resolved', 'Resolved')
14
+ if (status === 'in_progress') return t('staff.support.tickets.status.inProgress', 'In progress')
15
+ return t('staff.support.tickets.status.received', 'Received')
16
+ }
17
+
18
+ type SupportRequestPanelProps = {
19
+ ticket: SupportTicketView
20
+ }
21
+
22
+ /**
23
+ * The customer request behind a portal ticket, as the customer wrote it. The task
24
+ * title and description above are staff-editable working copies; the portal only
25
+ * ever shows this original.
26
+ */
27
+ export function SupportRequestPanel({ ticket }: SupportRequestPanelProps) {
28
+ const t = useT()
29
+ const rows: Array<{ label: string; value: React.ReactNode }> = [
30
+ { label: t('staff.time_tracking.taskDrawer.support.reference', 'Ticket'), value: ticket.supportReference },
31
+ {
32
+ label: t('staff.time_tracking.taskDrawer.support.customer', 'Customer'),
33
+ value: ticket.customerName ?? t('staff.time_tracking.taskDrawer.support.unknown', 'Unknown'),
34
+ },
35
+ {
36
+ label: t('staff.time_tracking.taskDrawer.support.reporter', 'Reported by'),
37
+ value: ticket.reporterName ?? t('staff.time_tracking.taskDrawer.support.unknown', 'Unknown'),
38
+ },
39
+ { label: t('staff.time_tracking.taskDrawer.support.category', 'Category'), value: ticket.categoryLabel },
40
+ {
41
+ label: t('staff.time_tracking.taskDrawer.support.routedVia', 'Routed by'),
42
+ value:
43
+ ticket.routedVia === 'category'
44
+ ? t('staff.time_tracking.taskDrawer.support.routedViaCategory', 'Category rule')
45
+ : t('staff.time_tracking.taskDrawer.support.routedViaFallback', 'Fallback project'),
46
+ },
47
+ {
48
+ label: t('staff.time_tracking.taskDrawer.support.portalStatus', 'Customer sees'),
49
+ value: (
50
+ <StatusBadge variant={SUPPORT_PORTAL_STATUS_VARIANT[ticket.portalStatus]} dot>
51
+ {supportPortalStatusLabel(t, ticket.portalStatus)}
52
+ </StatusBadge>
53
+ ),
54
+ },
55
+ ]
56
+ return (
57
+ <section
58
+ className="flex flex-col gap-3 rounded-lg border border-border bg-muted px-4 py-3"
59
+ data-testid="task-drawer-support-panel"
60
+ >
61
+ <div className="flex flex-wrap items-center gap-2">
62
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
63
+ {t('staff.time_tracking.taskDrawer.support.title', 'Customer request')}
64
+ </h3>
65
+ {ticket.unnotifiedAt ? (
66
+ <Badge variant="warning" size="sm">
67
+ {t('staff.support.tickets.nobodyNotified', 'Nobody notified')}
68
+ </Badge>
69
+ ) : null}
70
+ </div>
71
+ <dl className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm sm:grid-cols-2">
72
+ {rows.map((row) => (
73
+ <div key={row.label} className="flex flex-col gap-0.5">
74
+ <dt className="text-xs text-muted-foreground">{row.label}</dt>
75
+ <dd className="text-foreground">{row.value}</dd>
76
+ </div>
77
+ ))}
78
+ </dl>
79
+ {ticket.requestSubject || ticket.requestBody ? (
80
+ <div className="flex flex-col gap-1 border-t border-border pt-3">
81
+ <span className="text-xs text-muted-foreground">
82
+ {t('staff.time_tracking.taskDrawer.support.originalRequest', 'Original request')}
83
+ </span>
84
+ {ticket.requestSubject ? (
85
+ <p className="text-sm font-medium text-foreground">{ticket.requestSubject}</p>
86
+ ) : null}
87
+ {ticket.requestBody ? (
88
+ <p className="whitespace-pre-wrap break-words text-sm text-foreground">{ticket.requestBody}</p>
89
+ ) : null}
90
+ </div>
91
+ ) : null}
92
+ <p className="text-xs text-muted-foreground" data-testid="task-drawer-support-hint">
93
+ {t(
94
+ 'staff.time_tracking.taskDrawer.support.titleHint',
95
+ 'The customer sees their original request, not the task title and description.',
96
+ )}
97
+ </p>
98
+ </section>
99
+ )
100
+ }
101
+
102
+ export default SupportRequestPanel