@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
@@ -21,11 +21,16 @@
21
21
  *
22
22
  * Every rule here is tenant-global by decision §10; the card on the right says so,
23
23
  * because "why can't I set this per project" is otherwise a support ticket.
24
+ *
25
+ * The form above is the "Time tracking" tab. Modules add further tabs through the
26
+ * `staff.time_tracking.settings:tabs` spot (staff's own Support routing is one); a tab
27
+ * owns its saves, so the Save bar lives inside this tab only, and `?tab=` deep-links.
24
28
  */
25
29
 
26
- import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
30
+ import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
27
31
  import { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'
28
32
  import * as React from 'react'
33
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
29
34
  import { AlertTriangle, History, Info, Save } from 'lucide-react'
30
35
  import { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'
31
36
  import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
@@ -35,6 +40,7 @@ import { Input } from '@open-mercato/ui/primitives/input'
35
40
  import { Label } from '@open-mercato/ui/primitives/label'
36
41
  import { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'
37
42
  import { SwitchField } from '@open-mercato/ui/primitives/switch-field'
43
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
38
44
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
39
45
  import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
40
46
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
@@ -57,11 +63,17 @@ import {
57
63
  draftRounding,
58
64
  formatSignedMinutes,
59
65
  isSettingsDraftDirty,
66
+ rebaseSettingsDraft,
60
67
  settingsDraftErrors,
61
68
  toSettingsDraft,
62
69
  toSettingsPayload,
63
70
  type TimeTrackingSettingsDraft,
64
71
  } from '../../../../lib/time-tracking-ui/timeTrackingSettingsForm'
72
+ import {
73
+ TIME_TRACKING_SETTINGS_GENERAL_TAB,
74
+ TIME_TRACKING_SETTINGS_TAB_PARAM,
75
+ type TimeTrackingSettingsTabContext,
76
+ } from '../../../../lib/time-tracking-ui/settingsTabs'
65
77
 
66
78
  const logger = createLogger('staff').child({ component: 'time-tracking/settings' })
67
79
 
@@ -123,7 +135,12 @@ export default function TimeTrackingSettingsPage() {
123
135
  const latestLoadRef = React.useRef(0)
124
136
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
125
137
  const canManage = hasFeature(payload?.grantedFeatures, MANAGE_FEATURE)
138
+ const router = useRouter()
139
+ const pathname = usePathname()
140
+ const searchParams = useSearchParams()
141
+ const activeTab = searchParams?.get(TIME_TRACKING_SETTINGS_TAB_PARAM) ?? TIME_TRACKING_SETTINGS_GENERAL_TAB
126
142
 
143
+ const [savedSettings, setSavedSettings] = React.useState<TimeTrackingSettings | null>(null)
127
144
  const [baseline, setBaseline] = React.useState<TimeTrackingSettingsDraft | null>(null)
128
145
  const [draft, setDraft] = React.useState<TimeTrackingSettingsDraft | null>(null)
129
146
  const draftRef = React.useRef(draft)
@@ -157,6 +174,7 @@ export default function TimeTrackingSettingsPage() {
157
174
  return
158
175
  }
159
176
  const next = toSettingsDraft(call.result)
177
+ setSavedSettings(call.result)
160
178
  const pending = draftRef.current
161
179
  const previous = baselineRef.current
162
180
  const loadedScope = loadedScopeRef.current
@@ -246,6 +264,57 @@ export default function TimeTrackingSettingsPage() {
246
264
  }),
247
265
  [canManage, contributedValues, setContributedSetting],
248
266
  )
267
+
268
+ const handleTabSettingsSaved = React.useCallback(
269
+ (saved: TimeTrackingSettings) => {
270
+ const nextBaseline = toSettingsDraft(saved)
271
+ setSavedSettings(saved)
272
+ setDraft((current) => (current && baseline ? rebaseSettingsDraft(current, baseline, nextBaseline) : nextBaseline))
273
+ setBaseline(nextBaseline)
274
+ },
275
+ [baseline],
276
+ )
277
+ const tabInjectionContext = React.useMemo<TimeTrackingSettingsTabContext | null>(
278
+ () =>
279
+ savedSettings
280
+ ? {
281
+ moduleId: TIME_TRACKING_SETTINGS_MODULE_ID,
282
+ canManage,
283
+ settings: savedSettings,
284
+ onSettingsSaved: handleTabSettingsSaved,
285
+ }
286
+ : null,
287
+ [canManage, handleTabSettingsSaved, savedSettings],
288
+ )
289
+ const { widgets: tabWidgets } = useInjectionWidgets<TimeTrackingSettingsTabContext | null>(
290
+ extensionPoints.hosts.timeTrackingSettingsTabs.spotId,
291
+ { context: tabInjectionContext, triggerOnLoad: true },
292
+ )
293
+ const injectedTabs = React.useMemo(
294
+ () =>
295
+ tabWidgets
296
+ .filter((widget) => (widget.placement?.kind ?? 'tab') === 'tab')
297
+ .map((widget) => {
298
+ const tabId = widget.placement?.groupId ?? widget.widgetId
299
+ const groupLabel = widget.placement?.groupLabel
300
+ const label = groupLabel ? t(groupLabel, widget.module.metadata.title ?? tabId) : widget.module.metadata.title ?? tabId
301
+ const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0
302
+ return { tabId, label, priority, Widget: widget.module.Widget }
303
+ })
304
+ .sort((left, right) => right.priority - left.priority),
305
+ [t, tabWidgets],
306
+ )
307
+ const selectedTab = injectedTabs.some((tab) => tab.tabId === activeTab) ? activeTab : TIME_TRACKING_SETTINGS_GENERAL_TAB
308
+ const selectTab = React.useCallback(
309
+ (tabId: string) => {
310
+ const params = new URLSearchParams(searchParams?.toString() ?? '')
311
+ if (tabId === TIME_TRACKING_SETTINGS_GENERAL_TAB) params.delete(TIME_TRACKING_SETTINGS_TAB_PARAM)
312
+ else params.set(TIME_TRACKING_SETTINGS_TAB_PARAM, tabId)
313
+ const query = params.toString()
314
+ router.replace(query ? `${pathname}?${query}` : pathname ?? '')
315
+ },
316
+ [pathname, router, searchParams],
317
+ )
249
318
  const canSave = canManage && isDirty && fieldErrors.length === 0 && !isSaving && !isLoading
250
319
 
251
320
  /**
@@ -315,6 +384,7 @@ export default function TimeTrackingSettingsPage() {
315
384
  return
316
385
  }
317
386
  const saved = toSettingsDraft(call.result)
387
+ setSavedSettings(call.result)
318
388
  setBaseline(saved)
319
389
  setDraft(saved)
320
390
  flash(t('staff.time_tracking.settings.saved', 'Settings saved.'), 'success')
@@ -425,351 +495,378 @@ export default function TimeTrackingSettingsPage() {
425
495
  )
426
496
  }
427
497
 
428
- return (
429
- <Page>
430
- <PageHeader
431
- title={t('staff.time_tracking.settings.title', 'Module settings')}
432
- description={t(
433
- 'staff.time_tracking.settings.subtitle',
434
- 'Rules shared by all customers and projects',
435
- )}
436
- />
437
- <PageBody>
438
- <div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
439
- <div className="flex flex-col gap-6">
440
- <Card>
441
- <CardHeader>
442
- <CardTitle>{t('staff.time_tracking.settings.rounding.title', 'Time rounding')}</CardTitle>
443
- <p className="text-sm text-muted-foreground">
444
- {t(
445
- 'staff.time_tracking.settings.rounding.description',
446
- 'Applies to all projects. The rounded value is the one cost is calculated from.',
447
- )}
448
- </p>
449
- </CardHeader>
450
- <CardContent className="flex flex-col gap-4">
451
- <div className="grid gap-4 sm:grid-cols-2">
452
- <div className="flex flex-col gap-2">
453
- <Label>{t('staff.time_tracking.settings.rounding.unit', 'Unit')}</Label>
454
- <SegmentedControl
455
- value={String(draft.roundingUnitMinutes)}
456
- onValueChange={(value) => {
457
- const parsed = Number(value)
458
- if (!ROUNDING_UNIT_OPTIONS.includes(parsed as RoundingUnitMinutes)) return
459
- patchDraft({ roundingUnitMinutes: parsed as RoundingUnitMinutes })
460
- }}
461
- disabled={!canManage}
462
- aria-label={t('staff.time_tracking.settings.rounding.unit', 'Unit')}
463
- >
464
- {ROUNDING_UNIT_OPTIONS.map((unit) => (
465
- <SegmentedControlItem key={unit} value={String(unit)}>
466
- {unit === 0
467
- ? t('staff.time_tracking.settings.rounding.unit.none', 'None')
468
- : t('staff.time_tracking.settings.rounding.unit.minutes', '{count} min', {
469
- count: unit,
470
- })}
471
- </SegmentedControlItem>
472
- ))}
473
- </SegmentedControl>
474
- </div>
475
- <div className="flex flex-col gap-2">
476
- <Label>{t('staff.time_tracking.settings.rounding.direction', 'Direction')}</Label>
477
- <SegmentedControl
478
- value={draft.roundingDirection}
479
- onValueChange={(value) => {
480
- if (value !== 'up' && value !== 'nearest') return
481
- patchDraft({ roundingDirection: value })
482
- }}
483
- disabled={!canManage || draft.roundingUnitMinutes === 0}
484
- aria-label={t('staff.time_tracking.settings.rounding.direction', 'Direction')}
485
- >
486
- <SegmentedControlItem value="up">
487
- {t('staff.time_tracking.settings.rounding.direction.up', 'Up')}
488
- </SegmentedControlItem>
489
- <SegmentedControlItem value="nearest">
490
- {t('staff.time_tracking.settings.rounding.direction.nearest', 'To nearest')}
498
+ const generalSettings = (
499
+ <>
500
+ <div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
501
+ <div className="flex flex-col gap-6">
502
+ <Card>
503
+ <CardHeader>
504
+ <CardTitle>{t('staff.time_tracking.settings.rounding.title', 'Time rounding')}</CardTitle>
505
+ <p className="text-sm text-muted-foreground">
506
+ {t(
507
+ 'staff.time_tracking.settings.rounding.description',
508
+ 'Applies to all projects. The rounded value is the one cost is calculated from.',
509
+ )}
510
+ </p>
511
+ </CardHeader>
512
+ <CardContent className="flex flex-col gap-4">
513
+ <div className="grid gap-4 sm:grid-cols-2">
514
+ <div className="flex flex-col gap-2">
515
+ <Label>{t('staff.time_tracking.settings.rounding.unit', 'Unit')}</Label>
516
+ <SegmentedControl
517
+ value={String(draft.roundingUnitMinutes)}
518
+ onValueChange={(value) => {
519
+ const parsed = Number(value)
520
+ if (!ROUNDING_UNIT_OPTIONS.includes(parsed as RoundingUnitMinutes)) return
521
+ patchDraft({ roundingUnitMinutes: parsed as RoundingUnitMinutes })
522
+ }}
523
+ disabled={!canManage}
524
+ aria-label={t('staff.time_tracking.settings.rounding.unit', 'Unit')}
525
+ >
526
+ {ROUNDING_UNIT_OPTIONS.map((unit) => (
527
+ <SegmentedControlItem key={unit} value={String(unit)}>
528
+ {unit === 0
529
+ ? t('staff.time_tracking.settings.rounding.unit.none', 'None')
530
+ : t('staff.time_tracking.settings.rounding.unit.minutes', '{count} min', {
531
+ count: unit,
532
+ })}
491
533
  </SegmentedControlItem>
492
- </SegmentedControl>
493
- </div>
534
+ ))}
535
+ </SegmentedControl>
494
536
  </div>
495
-
496
- <Alert status="information" icon={<Info aria-hidden="true" />}>
497
- <AlertTitle>
498
- {t('staff.time_tracking.settings.rounding.examples.title', 'Examples at the current setting')}
499
- </AlertTitle>
500
- <AlertDescription>
501
- <span className="font-mono text-sm" data-testid="rounding-examples">
502
- {examples
503
- .map((example) => `${example.rawLabel} → ${example.roundedLabel}`)
504
- .join(' · ')}
505
- </span>
506
- </AlertDescription>
507
- </Alert>
508
-
509
- <div className="flex flex-col gap-2 rounded-md border border-border p-4">
510
- <span className="text-sm font-medium">
511
- {t(
512
- 'staff.time_tracking.settings.retro.title',
513
- 'Apply retroactively to existing entries',
514
- )}
515
- </span>
516
- <span className="text-xs text-muted-foreground">
517
- {t(
518
- 'staff.time_tracking.settings.retro.hint',
519
- 'Saving changes new entries only. Recalculating existing ones is a separate, explicit action — and it never changes entries locked in closed reports.',
520
- )}
521
- </span>
522
- <div>
523
- <Button
524
- type="button"
525
- variant="outline"
526
- onClick={() => void handleReapply()}
527
- disabled={!canManage || isReapplying || isDirty}
528
- data-testid="reapply-rounding"
529
- >
530
- {isReapplying ? <Spinner className="size-4" /> : <History className="size-4" aria-hidden="true" />}
531
- {t('staff.time_tracking.settings.retro.cta', 'Reapply to existing entries')}
532
- </Button>
533
- </div>
534
- {isDirty ? (
535
- <span className="text-xs text-muted-foreground">
536
- {t(
537
- 'staff.time_tracking.settings.retro.saveFirst',
538
- 'Save the rule first — the recalculation uses the saved rule, not the one on screen.',
539
- )}
540
- </span>
541
- ) : null}
537
+ <div className="flex flex-col gap-2">
538
+ <Label>{t('staff.time_tracking.settings.rounding.direction', 'Direction')}</Label>
539
+ <SegmentedControl
540
+ value={draft.roundingDirection}
541
+ onValueChange={(value) => {
542
+ if (value !== 'up' && value !== 'nearest') return
543
+ patchDraft({ roundingDirection: value })
544
+ }}
545
+ disabled={!canManage || draft.roundingUnitMinutes === 0}
546
+ aria-label={t('staff.time_tracking.settings.rounding.direction', 'Direction')}
547
+ >
548
+ <SegmentedControlItem value="up">
549
+ {t('staff.time_tracking.settings.rounding.direction.up', 'Up')}
550
+ </SegmentedControlItem>
551
+ <SegmentedControlItem value="nearest">
552
+ {t('staff.time_tracking.settings.rounding.direction.nearest', 'To nearest')}
553
+ </SegmentedControlItem>
554
+ </SegmentedControl>
542
555
  </div>
543
- </CardContent>
544
- </Card>
545
-
546
- <Card>
547
- <CardHeader>
548
- <CardTitle>{t('staff.time_tracking.settings.defaults.title', 'Entry defaults')}</CardTitle>
549
- </CardHeader>
550
- <CardContent className="flex flex-col gap-4">
551
- <SwitchField
552
- label={t('staff.time_tracking.settings.defaults.billable', 'New entries are billable')}
553
- description={t(
554
- 'staff.time_tracking.settings.defaults.billableHint',
555
- 'A project can override this.',
556
- )}
557
- checked={draft.defaultsBillable}
558
- onCheckedChange={(value) => patchDraft({ defaultsBillable: value === true })}
559
- disabled={!canManage}
560
- />
561
- <SwitchField
562
- label={t(
563
- 'staff.time_tracking.settings.defaults.chainStart',
564
- 'Start = end of the previous entry',
556
+ </div>
557
+
558
+ <Alert status="information" icon={<Info aria-hidden="true" />}>
559
+ <AlertTitle>
560
+ {t('staff.time_tracking.settings.rounding.examples.title', 'Examples at the current setting')}
561
+ </AlertTitle>
562
+ <AlertDescription>
563
+ <span className="font-mono text-sm" data-testid="rounding-examples">
564
+ {examples
565
+ .map((example) => `${example.rawLabel} → ${example.roundedLabel}`)
566
+ .join(' · ')}
567
+ </span>
568
+ </AlertDescription>
569
+ </Alert>
570
+
571
+ <div className="flex flex-col gap-2 rounded-md border border-border p-4">
572
+ <span className="text-sm font-medium">
573
+ {t(
574
+ 'staff.time_tracking.settings.retro.title',
575
+ 'Apply retroactively to existing entries',
565
576
  )}
566
- description={t(
567
- 'staff.time_tracking.settings.defaults.chainStartHint',
568
- 'A suggestion when adding another entry on the same day.',
577
+ </span>
578
+ <span className="text-xs text-muted-foreground">
579
+ {t(
580
+ 'staff.time_tracking.settings.retro.hint',
581
+ 'Saving changes new entries only. Recalculating existing ones is a separate, explicit action — and it never changes entries locked in closed reports.',
569
582
  )}
570
- checked={draft.defaultsChainStartFromPreviousEnd}
571
- onCheckedChange={(value) =>
572
- patchDraft({ defaultsChainStartFromPreviousEnd: value === true })
573
- }
574
- disabled={!canManage}
575
- />
576
- <div className="flex flex-col gap-2">
577
- <Label htmlFor="time-tracking-daily-hours">
578
- {t('staff.time_tracking.settings.targets.dailyHours', 'Daily hours target')}
579
- </Label>
580
- <div className="flex items-center gap-2">
581
- <Input
582
- id="time-tracking-daily-hours"
583
- className="max-w-28"
584
- inputMode="decimal"
585
- value={draft.dailyHoursText}
586
- onChange={(event) => patchDraft({ dailyHoursText: event.target.value })}
587
- disabled={!canManage}
588
- aria-invalid={fieldErrors.includes('dailyHours')}
589
- />
590
- <span className="text-sm text-muted-foreground">
591
- {t('staff.time_tracking.settings.targets.hoursUnit', 'h')}
592
- </span>
593
- </div>
583
+ </span>
584
+ <div>
585
+ <Button
586
+ type="button"
587
+ variant="outline"
588
+ onClick={() => void handleReapply()}
589
+ disabled={!canManage || isReapplying || isDirty}
590
+ data-testid="reapply-rounding"
591
+ >
592
+ {isReapplying ? <Spinner className="size-4" /> : <History className="size-4" aria-hidden="true" />}
593
+ {t('staff.time_tracking.settings.retro.cta', 'Reapply to existing entries')}
594
+ </Button>
595
+ </div>
596
+ {isDirty ? (
594
597
  <span className="text-xs text-muted-foreground">
595
598
  {t(
596
- 'staff.time_tracking.settings.targets.dailyHoursHint',
597
- 'Scales the load bars in the timesheet. Leave empty to turn targets off.',
599
+ 'staff.time_tracking.settings.retro.saveFirst',
600
+ 'Save the rule first — the recalculation uses the saved rule, not the one on screen.',
598
601
  )}
599
602
  </span>
600
- {fieldErrors.includes('dailyHours') ? (
601
- <span className="text-xs text-status-error-text">
602
- {t(
603
- 'staff.time_tracking.settings.targets.dailyHoursError',
604
- 'Enter a number of hours between 0 and 24, or leave the field empty.',
605
- )}
606
- </span>
607
- ) : null}
608
- </div>
609
- </CardContent>
610
- </Card>
611
-
612
- <Card>
613
- <CardHeader>
614
- <CardTitle>{t('staff.time_tracking.settings.warnings.title', 'Warnings')}</CardTitle>
615
- </CardHeader>
616
- <CardContent className="flex flex-col gap-4">
617
- <SwitchField
618
- label={t('staff.time_tracking.settings.warnings.overlap', 'Warn about overlapping entries')}
619
- description={t('staff.time_tracking.settings.warnings.overlapHint', 'A warning, not a block.')}
620
- checked={draft.warningsOverlap}
621
- onCheckedChange={(value) => patchDraft({ warningsOverlap: value === true })}
622
- disabled={!canManage}
623
- />
624
- <SwitchField
625
- label={t('staff.time_tracking.settings.warnings.runningTimer', 'Remind about a running timer')}
626
- description={t(
627
- 'staff.time_tracking.settings.warnings.runningTimerHint',
628
- 'An unobtrusive reminder when leaving the module and after 8 h of continuous running.',
629
- )}
630
- checked={draft.warningsRunningTimer}
631
- onCheckedChange={(value) => patchDraft({ warningsRunningTimer: value === true })}
632
- disabled={!canManage}
633
- />
634
- </CardContent>
635
- </Card>
636
-
637
- <Card>
638
- <CardHeader>
639
- <CardTitle>{t('staff.time_tracking.settings.access.title', 'Project access')}</CardTitle>
640
- </CardHeader>
641
- <CardContent className="flex flex-col gap-2">
642
- <Label htmlFor="time-tracking-grace-days">
643
- {t(
644
- 'staff.time_tracking.settings.access.graceDays',
645
- 'Access grace period after an assignment ends',
646
- )}
603
+ ) : null}
604
+ </div>
605
+ </CardContent>
606
+ </Card>
607
+
608
+ <Card>
609
+ <CardHeader>
610
+ <CardTitle>{t('staff.time_tracking.settings.defaults.title', 'Entry defaults')}</CardTitle>
611
+ </CardHeader>
612
+ <CardContent className="flex flex-col gap-4">
613
+ <SwitchField
614
+ label={t('staff.time_tracking.settings.defaults.billable', 'New entries are billable')}
615
+ description={t(
616
+ 'staff.time_tracking.settings.defaults.billableHint',
617
+ 'A project can override this.',
618
+ )}
619
+ checked={draft.defaultsBillable}
620
+ onCheckedChange={(value) => patchDraft({ defaultsBillable: value === true })}
621
+ disabled={!canManage}
622
+ />
623
+ <SwitchField
624
+ label={t(
625
+ 'staff.time_tracking.settings.defaults.chainStart',
626
+ 'Start = end of the previous entry',
627
+ )}
628
+ description={t(
629
+ 'staff.time_tracking.settings.defaults.chainStartHint',
630
+ 'A suggestion when adding another entry on the same day.',
631
+ )}
632
+ checked={draft.defaultsChainStartFromPreviousEnd}
633
+ onCheckedChange={(value) =>
634
+ patchDraft({ defaultsChainStartFromPreviousEnd: value === true })
635
+ }
636
+ disabled={!canManage}
637
+ />
638
+ <div className="flex flex-col gap-2">
639
+ <Label htmlFor="time-tracking-daily-hours">
640
+ {t('staff.time_tracking.settings.targets.dailyHours', 'Daily hours target')}
647
641
  </Label>
648
642
  <div className="flex items-center gap-2">
649
643
  <Input
650
- id="time-tracking-grace-days"
644
+ id="time-tracking-daily-hours"
651
645
  className="max-w-28"
652
- inputMode="numeric"
653
- value={draft.assignmentGraceDaysText}
654
- onChange={(event) => patchDraft({ assignmentGraceDaysText: event.target.value })}
646
+ inputMode="decimal"
647
+ value={draft.dailyHoursText}
648
+ onChange={(event) => patchDraft({ dailyHoursText: event.target.value })}
655
649
  disabled={!canManage}
656
- aria-invalid={fieldErrors.includes('assignmentGraceDays')}
650
+ aria-invalid={fieldErrors.includes('dailyHours')}
657
651
  />
658
652
  <span className="text-sm text-muted-foreground">
659
- {t('staff.time_tracking.settings.access.daysUnit', 'days')}
653
+ {t('staff.time_tracking.settings.targets.hoursUnit', 'h')}
660
654
  </span>
661
655
  </div>
662
656
  <span className="text-xs text-muted-foreground">
663
657
  {t(
664
- 'staff.time_tracking.settings.access.graceDaysHint',
665
- 'A member keeps access to a project for this many days after their assignment end date, so the last week of work can still be logged and corrected. 0 ends access on the end date itself.',
658
+ 'staff.time_tracking.settings.targets.dailyHoursHint',
659
+ 'Scales the load bars in the timesheet. Leave empty to turn targets off.',
666
660
  )}
667
661
  </span>
668
- {fieldErrors.includes('assignmentGraceDays') ? (
662
+ {fieldErrors.includes('dailyHours') ? (
669
663
  <span className="text-xs text-status-error-text">
670
664
  {t(
671
- 'staff.time_tracking.settings.access.graceDaysError',
672
- 'Enter a whole number of days between 0 and 365.',
665
+ 'staff.time_tracking.settings.targets.dailyHoursError',
666
+ 'Enter a number of hours between 0 and 24, or leave the field empty.',
673
667
  )}
674
668
  </span>
675
669
  ) : null}
676
- </CardContent>
677
- </Card>
678
-
679
- <InjectionSpot
680
- spotId={extensionPoints.hosts.timeTrackingSettingsSections.spotId}
681
- context={settingsInjectionContext}
682
- data={draft}
683
- />
684
- </div>
685
-
686
- <div className="flex flex-col gap-6">
687
- <Alert status="warning" icon={<AlertTriangle aria-hidden="true" />}>
688
- <AlertTitle>
689
- {t('staff.time_tracking.settings.impact.title', 'Changing rounding affects amounts')}
690
- </AlertTitle>
691
- <AlertDescription>
692
- {impactState === 'loading' && !impact ? (
693
- <span className="text-sm">
694
- {t('staff.time_tracking.settings.impact.loading', 'Calculating the impact…')}
695
- </span>
696
- ) : impactState === 'error' ? (
670
+ </div>
671
+ </CardContent>
672
+ </Card>
673
+
674
+ <Card>
675
+ <CardHeader>
676
+ <CardTitle>{t('staff.time_tracking.settings.warnings.title', 'Warnings')}</CardTitle>
677
+ </CardHeader>
678
+ <CardContent className="flex flex-col gap-4">
679
+ <SwitchField
680
+ label={t('staff.time_tracking.settings.warnings.overlap', 'Warn about overlapping entries')}
681
+ description={t('staff.time_tracking.settings.warnings.overlapHint', 'A warning, not a block.')}
682
+ checked={draft.warningsOverlap}
683
+ onCheckedChange={(value) => patchDraft({ warningsOverlap: value === true })}
684
+ disabled={!canManage}
685
+ />
686
+ <SwitchField
687
+ label={t('staff.time_tracking.settings.warnings.runningTimer', 'Remind about a running timer')}
688
+ description={t(
689
+ 'staff.time_tracking.settings.warnings.runningTimerHint',
690
+ 'An unobtrusive reminder when leaving the module and after 8 h of continuous running.',
691
+ )}
692
+ checked={draft.warningsRunningTimer}
693
+ onCheckedChange={(value) => patchDraft({ warningsRunningTimer: value === true })}
694
+ disabled={!canManage}
695
+ />
696
+ </CardContent>
697
+ </Card>
698
+
699
+ <Card>
700
+ <CardHeader>
701
+ <CardTitle>{t('staff.time_tracking.settings.access.title', 'Project access')}</CardTitle>
702
+ </CardHeader>
703
+ <CardContent className="flex flex-col gap-2">
704
+ <Label htmlFor="time-tracking-grace-days">
705
+ {t(
706
+ 'staff.time_tracking.settings.access.graceDays',
707
+ 'Access grace period after an assignment ends',
708
+ )}
709
+ </Label>
710
+ <div className="flex items-center gap-2">
711
+ <Input
712
+ id="time-tracking-grace-days"
713
+ className="max-w-28"
714
+ inputMode="numeric"
715
+ value={draft.assignmentGraceDaysText}
716
+ onChange={(event) => patchDraft({ assignmentGraceDaysText: event.target.value })}
717
+ disabled={!canManage}
718
+ aria-invalid={fieldErrors.includes('assignmentGraceDays')}
719
+ />
720
+ <span className="text-sm text-muted-foreground">
721
+ {t('staff.time_tracking.settings.access.daysUnit', 'days')}
722
+ </span>
723
+ </div>
724
+ <span className="text-xs text-muted-foreground">
725
+ {t(
726
+ 'staff.time_tracking.settings.access.graceDaysHint',
727
+ 'A member keeps access to a project for this many days after their assignment end date, so the last week of work can still be logged and corrected. 0 ends access on the end date itself.',
728
+ )}
729
+ </span>
730
+ {fieldErrors.includes('assignmentGraceDays') ? (
731
+ <span className="text-xs text-status-error-text">
732
+ {t(
733
+ 'staff.time_tracking.settings.access.graceDaysError',
734
+ 'Enter a whole number of days between 0 and 365.',
735
+ )}
736
+ </span>
737
+ ) : null}
738
+ </CardContent>
739
+ </Card>
740
+
741
+ <InjectionSpot
742
+ spotId={extensionPoints.hosts.timeTrackingSettingsSections.spotId}
743
+ context={settingsInjectionContext}
744
+ data={draft}
745
+ />
746
+ </div>
747
+
748
+ <div className="flex flex-col gap-6">
749
+ <Alert status="warning" icon={<AlertTriangle aria-hidden="true" />}>
750
+ <AlertTitle>
751
+ {t('staff.time_tracking.settings.impact.title', 'Changing rounding affects amounts')}
752
+ </AlertTitle>
753
+ <AlertDescription>
754
+ {impactState === 'loading' && !impact ? (
755
+ <span className="text-sm">
756
+ {t('staff.time_tracking.settings.impact.loading', 'Calculating the impact…')}
757
+ </span>
758
+ ) : impactState === 'error' ? (
759
+ <span className="text-sm">
760
+ {t('staff.time_tracking.settings.impact.error', 'The impact preview is unavailable.')}
761
+ </span>
762
+ ) : impact && impact.projected.entryCount > 0 ? (
763
+ <div className="flex flex-col gap-1" data-testid="rounding-impact">
697
764
  <span className="text-sm">
698
- {t('staff.time_tracking.settings.impact.error', 'The impact preview is unavailable.')}
765
+ {t(
766
+ 'staff.time_tracking.settings.impact.body',
767
+ 'With the current {count} entries from the last {days} days, {rule} gives {rounded} ({delta} against raw time). Closed reports stay unchanged.',
768
+ {
769
+ count: impact.projected.entryCount,
770
+ days: impact.windowDays,
771
+ rule: ruleLabel,
772
+ rounded: formatDuration(impact.projected.roundedMinutes, 'clock'),
773
+ delta: formatSignedMinutes(impact.projected.deltaMinutes),
774
+ },
775
+ )}
699
776
  </span>
700
- ) : impact && impact.projected.entryCount > 0 ? (
701
- <div className="flex flex-col gap-1" data-testid="rounding-impact">
702
- <span className="text-sm">
777
+ {impact.lockedEntryCount > 0 ? (
778
+ <span className="text-xs">
703
779
  {t(
704
- 'staff.time_tracking.settings.impact.body',
705
- 'With the current {count} entries from the last {days} days, {rule} gives {rounded} ({delta} against raw time). Closed reports stay unchanged.',
706
- {
707
- count: impact.projected.entryCount,
708
- days: impact.windowDays,
709
- rule: ruleLabel,
710
- rounded: formatDuration(impact.projected.roundedMinutes, 'clock'),
711
- delta: formatSignedMinutes(impact.projected.deltaMinutes),
712
- },
780
+ 'staff.time_tracking.settings.impact.locked',
781
+ '{count} entries in the window are locked in closed reports and are excluded.',
782
+ { count: impact.lockedEntryCount },
713
783
  )}
714
784
  </span>
715
- {impact.lockedEntryCount > 0 ? (
716
- <span className="text-xs">
717
- {t(
718
- 'staff.time_tracking.settings.impact.locked',
719
- '{count} entries in the window are locked in closed reports and are excluded.',
720
- { count: impact.lockedEntryCount },
721
- )}
722
- </span>
723
- ) : null}
724
- </div>
725
- ) : (
726
- <span className="text-sm">
727
- {t(
728
- 'staff.time_tracking.settings.impact.empty',
729
- 'There are no unlocked entries in the last {days} days to project against.',
730
- { days: impact?.windowDays ?? 90 },
731
- )}
732
- </span>
733
- )}
734
- </AlertDescription>
735
- </Alert>
736
-
737
- <Card>
738
- <CardHeader>
739
- <CardTitle>{t('staff.time_tracking.settings.scope.title', 'Settings scope')}</CardTitle>
740
- </CardHeader>
741
- <CardContent className="flex flex-col gap-2">
785
+ ) : null}
786
+ </div>
787
+ ) : (
742
788
  <span className="text-sm">
743
789
  {t(
744
- 'staff.time_tracking.settings.scope.body',
745
- 'These rules are global — they cannot be set per customer or per project.',
790
+ 'staff.time_tracking.settings.impact.empty',
791
+ 'There are no unlocked entries in the last {days} days to project against.',
792
+ { days: impact?.windowDays ?? 90 },
746
793
  )}
747
794
  </span>
748
- <span className="text-xs text-muted-foreground">
749
- {t(
750
- 'staff.time_tracking.settings.scope.hint',
751
- 'The rate and currency stay at project level, and a rate override stays at entry level.',
752
- )}
753
- </span>
754
- </CardContent>
755
- </Card>
756
- </div>
795
+ )}
796
+ </AlertDescription>
797
+ </Alert>
798
+
799
+ <Card>
800
+ <CardHeader>
801
+ <CardTitle>{t('staff.time_tracking.settings.scope.title', 'Settings scope')}</CardTitle>
802
+ </CardHeader>
803
+ <CardContent className="flex flex-col gap-2">
804
+ <span className="text-sm">
805
+ {t(
806
+ 'staff.time_tracking.settings.scope.body',
807
+ 'These rules are global — they cannot be set per customer or per project.',
808
+ )}
809
+ </span>
810
+ <span className="text-xs text-muted-foreground">
811
+ {t(
812
+ 'staff.time_tracking.settings.scope.hint',
813
+ 'The rate and currency stay at project level, and a rate override stays at entry level.',
814
+ )}
815
+ </span>
816
+ </CardContent>
817
+ </Card>
757
818
  </div>
819
+ </div>
820
+
821
+ <div className="mt-6 flex items-center justify-end gap-2">
822
+ <Button
823
+ type="button"
824
+ variant="ghost"
825
+ onClick={() => setDraft(baseline)}
826
+ disabled={!isDirty || isSaving || isLoading}
827
+ >
828
+ {t('staff.time_tracking.settings.actions.cancel', 'Cancel')}
829
+ </Button>
830
+ <Button type="button" onClick={() => void handleSave()} disabled={!canSave} data-testid="save-settings">
831
+ {isSaving ? <Spinner className="size-4" /> : <Save className="size-4" aria-hidden="true" />}
832
+ {t('staff.time_tracking.settings.actions.save', 'Save')}
833
+ </Button>
834
+ </div>
835
+ </>
836
+ )
758
837
 
759
- <div className="mt-6 flex items-center justify-end gap-2">
760
- <Button
761
- type="button"
762
- variant="ghost"
763
- onClick={() => setDraft(baseline)}
764
- disabled={!isDirty || isSaving || isLoading}
765
- >
766
- {t('staff.time_tracking.settings.actions.cancel', 'Cancel')}
767
- </Button>
768
- <Button type="button" onClick={() => void handleSave()} disabled={!canSave} data-testid="save-settings">
769
- {isSaving ? <Spinner className="size-4" /> : <Save className="size-4" aria-hidden="true" />}
770
- {t('staff.time_tracking.settings.actions.save', 'Save')}
771
- </Button>
772
- </div>
838
+ return (
839
+ <Page>
840
+ <PageHeader
841
+ title={t('staff.time_tracking.settings.title', 'Module settings')}
842
+ description={t(
843
+ 'staff.time_tracking.settings.subtitle',
844
+ 'Rules shared by all customers and projects',
845
+ )}
846
+ />
847
+ <PageBody>
848
+ {injectedTabs.length > 0 ? (
849
+ <Tabs value={selectedTab} onValueChange={selectTab} variant="underline">
850
+ <TabsList aria-label={t('staff.time_tracking.settings.tabs.label', 'Settings sections')} className="mb-4">
851
+ <TabsTrigger value={TIME_TRACKING_SETTINGS_GENERAL_TAB}>
852
+ {t('staff.time_tracking.settings.tabs.general', 'Time tracking')}
853
+ </TabsTrigger>
854
+ {injectedTabs.map((tab) => (
855
+ <TabsTrigger key={tab.tabId} value={tab.tabId}>
856
+ {tab.label}
857
+ </TabsTrigger>
858
+ ))}
859
+ </TabsList>
860
+ <TabsContent value={TIME_TRACKING_SETTINGS_GENERAL_TAB}>{generalSettings}</TabsContent>
861
+ {injectedTabs.map((tab) => (
862
+ <TabsContent key={tab.tabId} value={tab.tabId}>
863
+ {tabInjectionContext ? <tab.Widget context={tabInjectionContext} /> : null}
864
+ </TabsContent>
865
+ ))}
866
+ </Tabs>
867
+ ) : (
868
+ generalSettings
869
+ )}
773
870
  </PageBody>
774
871
  {ConfirmDialogElement}
775
872
  </Page>