@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
@@ -44,6 +44,7 @@ const events = [
44
44
  { id: 'staff.timesheets.time_task.updated', label: 'Time Task Updated', entity: 'time_task', category: 'crud' },
45
45
  { id: 'staff.timesheets.time_task.deleted', label: 'Time Task Deleted', entity: 'time_task', category: 'crud' },
46
46
  { id: 'staff.timesheets.time_task.status_changed', label: 'Time Task Status Changed', entity: 'time_task', category: 'lifecycle', clientBroadcast: true },
47
+ { id: 'staff.timesheets.time_task.assignee_changed', label: 'Time Task Assignee Changed', entity: 'time_task', category: 'lifecycle', clientBroadcast: true },
47
48
  { id: 'staff.timesheets.time_task_status.created', label: 'Time Task Status Created', entity: 'time_task_status', category: 'crud' },
48
49
  { id: 'staff.timesheets.time_task_status.updated', label: 'Time Task Status Updated', entity: 'time_task_status', category: 'crud' },
49
50
  { id: 'staff.timesheets.time_task_status.deleted', label: 'Time Task Status Deleted', entity: 'time_task_status', category: 'crud' },
@@ -120,6 +121,31 @@ const events = [
120
121
  portalBroadcast: true,
121
122
  excludeFromTriggers: true,
122
123
  },
124
+
125
+ // Customer support tickets. `created` and `customer_replied` name the customer, so
126
+ // they stay server-side; `activity_recorded` carries ids only and is what open
127
+ // backoffice screens refetch on. `portal_updated` is emitted only by
128
+ // `lib/support/portalBroadcast.ts` (driven by the `subscribers/support-portal-*.ts`
129
+ // subscribers), always with the ticket's own customer users pinned in
130
+ // `recipientUserIds`, and never when that list is empty.
131
+ { id: 'staff.support_ticket.created', label: 'Support Ticket Created', entity: 'support_ticket', category: 'lifecycle' },
132
+ { id: 'staff.support_ticket.customer_replied', label: 'Support Ticket Customer Replied', entity: 'support_ticket', category: 'lifecycle' },
133
+ {
134
+ id: 'staff.support_ticket.activity_recorded',
135
+ label: 'Support Ticket Activity Recorded',
136
+ entity: 'support_ticket',
137
+ category: 'lifecycle',
138
+ clientBroadcast: true,
139
+ excludeFromTriggers: true,
140
+ },
141
+ {
142
+ id: 'staff.support_ticket.portal_updated',
143
+ label: 'Support Ticket Updated For Portal',
144
+ entity: 'support_ticket',
145
+ category: 'lifecycle',
146
+ portalBroadcast: true,
147
+ excludeFromTriggers: true,
148
+ },
123
149
  ] as const
124
150
 
125
151
  export const eventsConfig = createModuleEvents({
@@ -152,6 +152,7 @@ export const extensionPoints = defineModuleExtensionPoints({
152
152
  myWorkAfterSections: pageHost('staff.my_work:after-sections', 'backend/staff/time-tracking/page.tsx'),
153
153
 
154
154
  timeTrackingSettingsSections: pageHost('staff.time_tracking.settings:sections', 'backend/staff/time-tracking/settings/page.tsx'),
155
+ timeTrackingSettingsTabs: pageHost('staff.time_tracking.settings:tabs', 'backend/staff/time-tracking/settings/page.tsx'),
155
156
 
156
157
  timerBarActions: pageHost('staff.timesheets.timer-bar:actions', 'lib/timesheets-ui/TimerBar.tsx'),
157
158
 
@@ -0,0 +1,10 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ requireCustomerAuth: true,
5
+ requireCustomerFeatures: ['portal.support_tickets.view'],
6
+ titleKey: 'staff.portal.support.detail.title',
7
+ title: 'Support ticket',
8
+ }
9
+
10
+ export default metadata
@@ -0,0 +1,553 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { ArrowLeft, Download, Paperclip, Trash2 } from 'lucide-react'
6
+ import { replaceWithPageReload } from '@open-mercato/shared/lib/navigation/pageReload'
7
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
8
+ import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
10
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
12
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
13
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
14
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
18
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
19
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
20
+ import { PortalCard } from '@open-mercato/ui/portal/components/PortalCard'
21
+ import { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'
22
+ import { usePortalAppEvent } from '@open-mercato/ui/portal/hooks/usePortalAppEvent'
23
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
24
+ import {
25
+ ATTACHMENT_ACCEPT_TYPES,
26
+ resolvePortalMaxUploadBytes,
27
+ validateAttachmentFile,
28
+ } from '@open-mercato/core/modules/staff/lib/support-ui/portalAttachmentValidation'
29
+ import {
30
+ forgetFailedUpload,
31
+ getRetryableUpload,
32
+ readFailedUploadNames,
33
+ rememberFailedUploads,
34
+ } from '@open-mercato/core/modules/staff/lib/support-ui/portalPendingUploads'
35
+ import {
36
+ PORTAL_SUPPORT_STATUS_VARIANTS,
37
+ PORTAL_SUPPORT_UPDATED_EVENT,
38
+ formatFileSize,
39
+ formatPortalDate,
40
+ isPortalSupportAccountNotLinked,
41
+ portalSupportStatusLabel,
42
+ readPortalTicketIdFromEvent,
43
+ type PortalSupportAttachment,
44
+ type PortalSupportOptions,
45
+ type PortalSupportTicketDetail,
46
+ } from '@open-mercato/core/modules/staff/lib/support-ui/portalSupport'
47
+ import { PortalSupportNotLinked } from '@open-mercato/core/modules/staff/lib/support-ui/PortalSupportNotLinked'
48
+
49
+ type Props = { params: { orgSlug: string; id: string } }
50
+
51
+ type UploadOutcome = { status: 'uploaded' } | { status: 'rejected' } | { status: 'failed'; reason: string | null }
52
+
53
+ type UploadErrorBody = { error?: unknown }
54
+
55
+ const MUTATION_CONTEXT_ID = 'staff-portal-support-detail'
56
+ const SPOT_BEFORE = 'portal:staff.support_ticket:before'
57
+ const SPOT_AFTER = 'portal:staff.support_ticket:after'
58
+
59
+ export default function PortalSupportTicketDetailPage({ params }: Props) {
60
+ const t = useT()
61
+ const locale = useLocale()
62
+ const { auth } = usePortalContext()
63
+ const { user, loading } = auth
64
+ const guardedMutation = useGuardedMutation<Record<string, unknown>>({
65
+ contextId: MUTATION_CONTEXT_ID,
66
+ blockedMessage: t('staff.portal.support.blocked', 'This action is not available right now.'),
67
+ })
68
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
69
+
70
+ const [ticket, setTicket] = React.useState<PortalSupportTicketDetail | null>(null)
71
+ const [attachments, setAttachments] = React.useState<PortalSupportAttachment[]>([])
72
+ const [failedUploads, setFailedUploads] = React.useState<string[]>([])
73
+ const [isLoading, setIsLoading] = React.useState(true)
74
+ const [notFound, setNotFound] = React.useState(false)
75
+ const [notLinked, setNotLinked] = React.useState(false)
76
+ const [error, setError] = React.useState<string | null>(null)
77
+ const [reply, setReply] = React.useState('')
78
+ const [replying, setReplying] = React.useState(false)
79
+ const [uploading, setUploading] = React.useState(false)
80
+ const [maxUploadBytes, setMaxUploadBytes] = React.useState(() => resolvePortalMaxUploadBytes(null))
81
+ const fileInputRef = React.useRef<HTMLInputElement | null>(null)
82
+
83
+ React.useEffect(() => {
84
+ if (!loading && !user) replaceWithPageReload(`/${params.orgSlug}/portal/login`)
85
+ }, [loading, user, params.orgSlug])
86
+
87
+ React.useEffect(() => {
88
+ setFailedUploads(readFailedUploadNames(params.id))
89
+ }, [params.id])
90
+
91
+ React.useEffect(() => {
92
+ if (!user) return
93
+ let cancelled = false
94
+ void (async () => {
95
+ try {
96
+ const response = await apiCall<PortalSupportOptions>('/api/staff/portal/support-tickets/options', { credentials: 'include' })
97
+ if (!cancelled && response.ok) setMaxUploadBytes(resolvePortalMaxUploadBytes(response.result?.maxUploadBytes))
98
+ } catch (err) {
99
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_options_failed' })
100
+ }
101
+ })()
102
+ return () => {
103
+ cancelled = true
104
+ }
105
+ }, [user])
106
+
107
+ const loadSeqRef = React.useRef(0)
108
+
109
+ const load = React.useCallback(async () => {
110
+ if (!user) return
111
+ const requestSeq = loadSeqRef.current + 1
112
+ loadSeqRef.current = requestSeq
113
+ const isStale = () => requestSeq !== loadSeqRef.current
114
+ setError(null)
115
+ try {
116
+ const detail = await apiCall<PortalSupportTicketDetail>(`/api/staff/portal/support-tickets/${encodeURIComponent(params.id)}`, {
117
+ credentials: 'include',
118
+ })
119
+ if (isStale()) return
120
+ if (isPortalSupportAccountNotLinked(detail)) {
121
+ setNotLinked(true)
122
+ setTicket(null)
123
+ return
124
+ }
125
+ if (detail.status === 404) {
126
+ setNotFound(true)
127
+ setTicket(null)
128
+ return
129
+ }
130
+ if (!detail.ok || !detail.result) {
131
+ setError(t('staff.portal.support.detail.loadFailed', 'Could not load this ticket.'))
132
+ return
133
+ }
134
+ setNotFound(false)
135
+ setTicket(detail.result)
136
+ setAttachments(detail.result.attachments ?? [])
137
+ } catch (err) {
138
+ if (isStale()) return
139
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_detail_failed' })
140
+ setError(t('staff.portal.support.detail.loadFailed', 'Could not load this ticket.'))
141
+ } finally {
142
+ if (!isStale()) setIsLoading(false)
143
+ }
144
+ }, [user, params.id, t])
145
+
146
+ React.useEffect(() => {
147
+ void load()
148
+ }, [load])
149
+
150
+ usePortalAppEvent(PORTAL_SUPPORT_UPDATED_EVENT, (event) => {
151
+ if (readPortalTicketIdFromEvent(event) === params.id) void load()
152
+ }, [load, params.id])
153
+
154
+ const clearFailed = React.useCallback((fileName: string) => {
155
+ forgetFailedUpload(params.id, fileName)
156
+ setFailedUploads((current) => current.filter((entry) => entry !== fileName))
157
+ }, [params.id])
158
+
159
+ const markFailed = React.useCallback((file: File) => {
160
+ rememberFailedUploads(params.id, [file])
161
+ setFailedUploads((current) => (current.includes(file.name) ? current : [...current, file.name]))
162
+ }, [params.id])
163
+
164
+ const sendReply = React.useCallback(async () => {
165
+ const body = reply.trim()
166
+ if (!ticket || !body || replying) return
167
+ setReplying(true)
168
+ setError(null)
169
+ try {
170
+ const result = await guardedMutation.runMutation({
171
+ operation: () =>
172
+ apiCall<{ ok?: boolean; reopened?: boolean }>(`/api/staff/portal/support-tickets/${encodeURIComponent(ticket.id)}/messages`, {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'application/json' },
175
+ credentials: 'include',
176
+ body: JSON.stringify({ body }),
177
+ }),
178
+ context: {
179
+ moduleId: 'staff',
180
+ entityId: 'staff.support_ticket',
181
+ operation: 'portal_support_message',
182
+ formId: MUTATION_CONTEXT_ID,
183
+ ticketId: ticket.id,
184
+ retryLastMutation: guardedMutation.retryLastMutation,
185
+ },
186
+ mutationPayload: { ticketId: ticket.id, body },
187
+ })
188
+ if (!result.ok) {
189
+ setError(
190
+ result.status === 429
191
+ ? t('staff.portal.support.detail.rateLimited', 'You have sent many messages recently. Please try again later.')
192
+ : t('staff.portal.support.detail.replyFailed', 'Could not send your message.'),
193
+ )
194
+ return
195
+ }
196
+ setReply('')
197
+ flash(
198
+ result.result?.reopened
199
+ ? t('staff.portal.support.detail.reopened', 'Your message was sent and the ticket was reopened.')
200
+ : t('staff.portal.support.detail.replySent', 'Your message was sent.'),
201
+ 'success',
202
+ )
203
+ await load()
204
+ } catch {
205
+ setError(t('staff.portal.support.detail.replyFailed', 'Could not send your message.'))
206
+ } finally {
207
+ setReplying(false)
208
+ }
209
+ }, [ticket, reply, replying, guardedMutation, load, t])
210
+
211
+ const uploadFile = React.useCallback(async (file: File): Promise<UploadOutcome> => {
212
+ if (!ticket) return { status: 'failed', reason: null }
213
+ const validationError = await validateAttachmentFile(file, t, maxUploadBytes)
214
+ if (validationError) {
215
+ flash(`${file.name}: ${validationError}`, 'error')
216
+ return { status: 'rejected' }
217
+ }
218
+ const form = new FormData()
219
+ form.set('file', file)
220
+ const result = await guardedMutation.runMutation({
221
+ operation: () =>
222
+ apiCall<UploadErrorBody>(`/api/staff/portal/support-tickets/${encodeURIComponent(ticket.id)}/attachments`, {
223
+ method: 'POST',
224
+ credentials: 'include',
225
+ body: form,
226
+ }),
227
+ context: {
228
+ moduleId: 'staff',
229
+ entityId: 'attachments.attachment',
230
+ operation: 'portal_support_attachment_upload',
231
+ formId: MUTATION_CONTEXT_ID,
232
+ ticketId: ticket.id,
233
+ retryLastMutation: guardedMutation.retryLastMutation,
234
+ },
235
+ mutationPayload: { ticketId: ticket.id, fileName: file.name },
236
+ })
237
+ if (result.ok) return { status: 'uploaded' }
238
+ const reason = result.result?.error
239
+ return { status: 'failed', reason: typeof reason === 'string' && reason.trim() ? reason.trim() : null }
240
+ }, [ticket, guardedMutation, maxUploadBytes, t])
241
+
242
+ const uploadFiles = React.useCallback(async (files: File[]) => {
243
+ if (!files.length) return
244
+ setUploading(true)
245
+ try {
246
+ for (const file of files) {
247
+ let outcome: UploadOutcome
248
+ try {
249
+ outcome = await uploadFile(file)
250
+ } catch (err) {
251
+ getTelemetryRuntime()?.reportError(err, { module: 'staff', code: 'staff.portal_support_upload_failed' })
252
+ outcome = { status: 'failed', reason: null }
253
+ }
254
+ if (outcome.status === 'uploaded') {
255
+ clearFailed(file.name)
256
+ } else if (outcome.status === 'failed') {
257
+ markFailed(file)
258
+ flash(
259
+ outcome.reason
260
+ ? t('staff.portal.support.attachments.uploadFailedReason', 'Could not upload {name}: {reason}', {
261
+ name: file.name,
262
+ reason: outcome.reason,
263
+ })
264
+ : t('staff.portal.support.attachments.uploadFailed', 'Could not upload {name}.', { name: file.name }),
265
+ 'error',
266
+ )
267
+ }
268
+ }
269
+ await load()
270
+ } finally {
271
+ setUploading(false)
272
+ if (fileInputRef.current) fileInputRef.current.value = ''
273
+ }
274
+ }, [uploadFile, clearFailed, markFailed, load, t])
275
+
276
+ const onFilesPicked = React.useCallback(async (list: FileList | null) => {
277
+ if (!list || !list.length) return
278
+ await uploadFiles(Array.from(list))
279
+ }, [uploadFiles])
280
+
281
+ const retryUpload = React.useCallback(async (fileName: string) => {
282
+ const file = getRetryableUpload(params.id, fileName)
283
+ if (!file) return
284
+ await uploadFiles([file])
285
+ }, [params.id, uploadFiles])
286
+
287
+ const deleteAttachment = React.useCallback(async (attachment: PortalSupportAttachment) => {
288
+ if (!ticket) return
289
+ const confirmed = await confirm({
290
+ title: t('staff.portal.support.attachments.deleteConfirm', 'Delete {name}?', { name: attachment.fileName }),
291
+ variant: 'destructive',
292
+ })
293
+ if (!confirmed) return
294
+ const result = await guardedMutation.runMutation({
295
+ operation: () =>
296
+ apiCall(
297
+ `/api/staff/portal/support-tickets/${encodeURIComponent(ticket.id)}/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,
298
+ { method: 'DELETE', credentials: 'include' },
299
+ ),
300
+ context: {
301
+ moduleId: 'staff',
302
+ entityId: 'attachments.attachment',
303
+ operation: 'portal_support_attachment_delete',
304
+ formId: MUTATION_CONTEXT_ID,
305
+ ticketId: ticket.id,
306
+ retryLastMutation: guardedMutation.retryLastMutation,
307
+ },
308
+ mutationPayload: { ticketId: ticket.id, attachmentId: attachment.id },
309
+ })
310
+ if (!result.ok) {
311
+ flash(t('staff.portal.support.attachments.deleteFailed', 'Could not delete the file.'), 'error')
312
+ return
313
+ }
314
+ await load()
315
+ }, [ticket, confirm, guardedMutation, load, t])
316
+
317
+ const injectionContext = React.useMemo(
318
+ () => ({
319
+ orgSlug: params.orgSlug,
320
+ ticketId: params.id,
321
+ supportReference: ticket?.supportReference ?? null,
322
+ portalStatus: ticket?.portalStatus ?? null,
323
+ resolvedFeatures: auth.resolvedFeatures,
324
+ }),
325
+ [params.orgSlug, params.id, ticket?.supportReference, ticket?.portalStatus, auth.resolvedFeatures],
326
+ )
327
+
328
+ const customerAuthorLabel = (isSelf: boolean, name: string | null) => {
329
+ if (isSelf) return t('staff.portal.support.detail.you', 'You')
330
+ return name ?? t('staff.portal.support.detail.colleague', 'A colleague')
331
+ }
332
+
333
+ if (loading || (isLoading && !ticket && !notFound && !notLinked)) {
334
+ return (
335
+ <div className="flex items-center justify-center py-20">
336
+ <Spinner />
337
+ </div>
338
+ )
339
+ }
340
+ if (!user) return null
341
+
342
+ return (
343
+ <div className="space-y-6">
344
+ <div>
345
+ <Button variant="ghost" asChild>
346
+ <Link href={`/${params.orgSlug}/portal/support`}>
347
+ <ArrowLeft className="mr-2 h-4 w-4" />
348
+ {t('staff.portal.support.backToList', 'All tickets')}
349
+ </Link>
350
+ </Button>
351
+ </div>
352
+
353
+ <InjectionSpot spotId={SPOT_BEFORE} context={injectionContext} />
354
+
355
+ {error ? (
356
+ <ErrorMessage
357
+ label={error}
358
+ action={
359
+ <Button type="button" variant="outline" size="sm" onClick={() => void load()}>
360
+ {t('staff.portal.support.retry', 'Try again')}
361
+ </Button>
362
+ }
363
+ />
364
+ ) : null}
365
+
366
+ {notLinked ? (
367
+ <PortalSupportNotLinked />
368
+ ) : notFound || (!ticket && !error) ? (
369
+ <PortalEmptyState
370
+ title={t('staff.portal.support.detail.notFoundTitle', 'Ticket not found')}
371
+ description={t('staff.portal.support.detail.notFoundDescription', 'This ticket does not exist or is not yours.')}
372
+ />
373
+ ) : !ticket ? null : (
374
+ <>
375
+ <PortalCard>
376
+ <div className="flex flex-wrap items-start justify-between gap-4">
377
+ <div className="min-w-0 space-y-1">
378
+ <p className="text-sm text-muted-foreground">
379
+ {ticket.supportReference} · {ticket.category.label}
380
+ </p>
381
+ <h1 className="text-xl font-semibold text-foreground">{ticket.subject}</h1>
382
+ </div>
383
+ <StatusBadge variant={PORTAL_SUPPORT_STATUS_VARIANTS[ticket.portalStatus] ?? 'neutral'} dot>
384
+ {portalSupportStatusLabel(t, ticket.portalStatus)}
385
+ </StatusBadge>
386
+ </div>
387
+ </PortalCard>
388
+
389
+ <PortalCard>
390
+ <h2 className="mb-3 text-sm font-semibold text-foreground">
391
+ {t('staff.portal.support.detail.conversation', 'Conversation')}
392
+ </h2>
393
+ <ol className="space-y-3" data-testid="support-thread">
394
+ <li className="rounded-md border border-border bg-muted p-3">
395
+ <p className="text-xs text-muted-foreground">
396
+ {customerAuthorLabel(ticket.requester?.isSelf ?? false, ticket.requester?.name ?? null)}
397
+ {' · '}
398
+ {formatPortalDate(ticket.createdAt, locale)}
399
+ </p>
400
+ <p className="mt-1 whitespace-pre-wrap text-sm text-foreground">{ticket.description}</p>
401
+ </li>
402
+ {ticket.messages.map((message) => (
403
+ <li
404
+ key={message.id}
405
+ data-author-kind={message.authorKind}
406
+ className={
407
+ message.authorKind === 'customer'
408
+ ? 'rounded-md border border-border bg-muted p-3'
409
+ : 'rounded-md border border-border bg-card p-3'
410
+ }
411
+ >
412
+ <p className="text-xs text-muted-foreground">
413
+ {message.authorKind === 'customer'
414
+ ? customerAuthorLabel(message.isSelf ?? false, message.authorName)
415
+ : message.authorName
416
+ ? t('staff.portal.support.detail.supportTeamNamed', 'Support team · {name}', { name: message.authorName })
417
+ : t('staff.portal.support.detail.supportTeam', 'Support team')}
418
+ {' · '}
419
+ {formatPortalDate(message.createdAt, locale)}
420
+ </p>
421
+ <p className="mt-1 whitespace-pre-wrap text-sm text-foreground">{message.body}</p>
422
+ </li>
423
+ ))}
424
+ </ol>
425
+
426
+ {ticket.canReply ? (
427
+ <form
428
+ className="mt-4 space-y-2"
429
+ onSubmit={(event) => {
430
+ event.preventDefault()
431
+ void sendReply()
432
+ }}
433
+ >
434
+ <Textarea
435
+ rows={4}
436
+ value={reply}
437
+ maxLength={10000}
438
+ aria-label={t('staff.portal.support.detail.replyLabel', 'Your reply')}
439
+ placeholder={t('staff.portal.support.detail.replyPlaceholder', 'Write a reply')}
440
+ onChange={(event) => setReply(event.target.value)}
441
+ onKeyDown={(event) => {
442
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
443
+ event.preventDefault()
444
+ void sendReply()
445
+ }
446
+ }}
447
+ />
448
+ {ticket.portalStatus === 'resolved' ? (
449
+ <p className="text-xs text-muted-foreground">
450
+ {t('staff.portal.support.detail.reopenHint', 'Replying reopens this ticket.')}
451
+ </p>
452
+ ) : null}
453
+ <div className="flex justify-end">
454
+ <Button type="submit" disabled={!reply.trim() || replying}>
455
+ {replying ? <Spinner className="mr-2 h-4 w-4" /> : null}
456
+ {t('staff.portal.support.detail.send', 'Send reply')}
457
+ </Button>
458
+ </div>
459
+ </form>
460
+ ) : null}
461
+ </PortalCard>
462
+
463
+ <PortalCard>
464
+ <div className="mb-3 flex items-center justify-between gap-2">
465
+ <h2 className="text-sm font-semibold text-foreground">{t('staff.portal.support.attachments.title', 'Files')}</h2>
466
+ {ticket.canReply ? (
467
+ <>
468
+ <input
469
+ ref={fileInputRef}
470
+ type="file"
471
+ multiple
472
+ accept={ATTACHMENT_ACCEPT_TYPES}
473
+ className="sr-only"
474
+ data-testid="support-detail-file-input"
475
+ onChange={(event) => void onFilesPicked(event.target.files)}
476
+ />
477
+ <Button type="button" variant="outline" size="sm" disabled={uploading} onClick={() => fileInputRef.current?.click()}>
478
+ {uploading ? <Spinner className="mr-2 h-4 w-4" /> : <Paperclip className="mr-2 h-4 w-4" />}
479
+ {t('staff.portal.support.attachments.add', 'Add files')}
480
+ </Button>
481
+ </>
482
+ ) : null}
483
+ </div>
484
+ {failedUploads.length ? (
485
+ <div
486
+ className="mb-3 space-y-2 rounded-md border border-status-error-border bg-status-error-bg p-3"
487
+ data-testid="support-failed-uploads"
488
+ >
489
+ <p className="text-sm text-status-error-text">
490
+ {t('staff.portal.support.attachments.failedHeading', 'These files could not be uploaded.')}
491
+ </p>
492
+ <ul className="space-y-1 text-sm text-status-error-text">
493
+ {failedUploads.map((name) => {
494
+ const retryable = ticket.canReply && getRetryableUpload(params.id, name) !== null
495
+ return (
496
+ <li key={name} className="flex flex-wrap items-center justify-between gap-2">
497
+ <span className="truncate">{name}</span>
498
+ {retryable ? (
499
+ <Button
500
+ type="button"
501
+ variant="outline"
502
+ size="sm"
503
+ disabled={uploading}
504
+ onClick={() => void retryUpload(name)}
505
+ >
506
+ {t('staff.portal.support.attachments.retry', 'Retry')}
507
+ </Button>
508
+ ) : (
509
+ <span className="text-xs">
510
+ {t('staff.portal.support.attachments.reattach', 'Please attach this file again.')}
511
+ </span>
512
+ )}
513
+ </li>
514
+ )
515
+ })}
516
+ </ul>
517
+ </div>
518
+ ) : null}
519
+ {attachments.length === 0 ? (
520
+ <p className="text-sm text-muted-foreground">{t('staff.portal.support.attachments.empty', 'No files yet.')}</p>
521
+ ) : (
522
+ <ul className="space-y-2" data-testid="support-attachments">
523
+ {attachments.map((attachment) => (
524
+ <li key={attachment.id} className="flex items-center justify-between gap-2 text-sm">
525
+ <a className="flex min-w-0 items-center gap-2 text-foreground hover:underline" href={attachment.downloadUrl}>
526
+ <Download className="h-4 w-4 shrink-0" />
527
+ <span className="truncate">{attachment.fileName}</span>
528
+ <span className="shrink-0 text-muted-foreground">{formatFileSize(t, attachment.fileSize)}</span>
529
+ </a>
530
+ {ticket.canReply && attachment.canDelete ? (
531
+ <IconButton
532
+ type="button"
533
+ variant="ghost"
534
+ size="sm"
535
+ aria-label={t('staff.portal.support.attachments.delete', 'Delete file')}
536
+ onClick={() => void deleteAttachment(attachment)}
537
+ >
538
+ <Trash2 className="h-4 w-4" />
539
+ </IconButton>
540
+ ) : null}
541
+ </li>
542
+ ))}
543
+ </ul>
544
+ )}
545
+ </PortalCard>
546
+ </>
547
+ )}
548
+
549
+ <InjectionSpot spotId={SPOT_AFTER} context={injectionContext} />
550
+ {ConfirmDialogElement}
551
+ </div>
552
+ )
553
+ }
@@ -0,0 +1,10 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ requireCustomerAuth: true,
5
+ requireCustomerFeatures: ['portal.support_tickets.manage'],
6
+ titleKey: 'staff.portal.support.new.title',
7
+ title: 'New support ticket',
8
+ }
9
+
10
+ export default metadata