@open-mercato/core 0.7.1-develop.7178.1.ab93fbaf4e → 0.7.1-develop.7180.1.9717fbbb43

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 (808) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_time_entry/index.js +14 -0
  3. package/dist/generated/entities/staff_time_entry/index.js.map +2 -2
  4. package/dist/generated/entities/staff_time_entry_tag/index.js +15 -0
  5. package/dist/generated/entities/staff_time_entry_tag/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +16 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_report/index.js +57 -0
  9. package/dist/generated/entities/staff_time_report/index.js.map +7 -0
  10. package/dist/generated/entities/staff_time_report_entry/index.js +27 -0
  11. package/dist/generated/entities/staff_time_report_entry/index.js.map +7 -0
  12. package/dist/generated/entities/staff_time_report_event/index.js +21 -0
  13. package/dist/generated/entities/staff_time_report_event/index.js.map +7 -0
  14. package/dist/generated/entities/staff_time_report_project/index.js +15 -0
  15. package/dist/generated/entities/staff_time_report_project/index.js.map +7 -0
  16. package/dist/generated/entities/staff_time_tag/index.js +21 -0
  17. package/dist/generated/entities/staff_time_tag/index.js.map +7 -0
  18. package/dist/generated/entities/staff_time_task/index.js +37 -0
  19. package/dist/generated/entities/staff_time_task/index.js.map +7 -0
  20. package/dist/generated/entities/staff_time_task_comment/index.js +21 -0
  21. package/dist/generated/entities/staff_time_task_comment/index.js.map +7 -0
  22. package/dist/generated/entities/staff_time_task_status/index.js +29 -0
  23. package/dist/generated/entities/staff_time_task_status/index.js.map +7 -0
  24. package/dist/generated/entities/staff_time_task_tag/index.js +15 -0
  25. package/dist/generated/entities/staff_time_task_tag/index.js.map +7 -0
  26. package/dist/generated/entities.ids.generated.js +11 -1
  27. package/dist/generated/entities.ids.generated.js.map +2 -2
  28. package/dist/generated/entity-fields-registry.js +149 -0
  29. package/dist/generated/entity-fields-registry.js.map +2 -2
  30. package/dist/helpers/integration/timesheetFixtures.js +7 -0
  31. package/dist/helpers/integration/timesheetFixtures.js.map +2 -2
  32. package/dist/modules/auth/cli.js +16 -0
  33. package/dist/modules/auth/cli.js.map +2 -2
  34. package/dist/modules/auth/lib/backendChrome.js +1 -0
  35. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  36. package/dist/modules/customer_accounts/lib/customerRoleAcls.js +44 -0
  37. package/dist/modules/customer_accounts/lib/customerRoleAcls.js.map +7 -0
  38. package/dist/modules/customer_accounts/setup.js +1 -28
  39. package/dist/modules/customer_accounts/setup.js.map +2 -2
  40. package/dist/modules/data_sync/api/options.js +2 -0
  41. package/dist/modules/data_sync/api/options.js.map +2 -2
  42. package/dist/modules/data_sync/backend/data-sync/page.js +39 -17
  43. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  44. package/dist/modules/data_sync/lib/start-controls.js +42 -0
  45. package/dist/modules/data_sync/lib/start-controls.js.map +7 -0
  46. package/dist/modules/staff/acl.js +9 -1
  47. package/dist/modules/staff/acl.js.map +2 -2
  48. package/dist/modules/staff/ai-agents.js +142 -0
  49. package/dist/modules/staff/ai-agents.js.map +7 -0
  50. package/dist/modules/staff/ai-tools/time-tracking-pack.js +348 -0
  51. package/dist/modules/staff/ai-tools/time-tracking-pack.js.map +7 -0
  52. package/dist/modules/staff/ai-tools/types.js +54 -0
  53. package/dist/modules/staff/ai-tools/types.js.map +7 -0
  54. package/dist/modules/staff/ai-tools.js +8 -0
  55. package/dist/modules/staff/ai-tools.js.map +7 -0
  56. package/dist/modules/staff/analytics.js +84 -1
  57. package/dist/modules/staff/analytics.js.map +2 -2
  58. package/dist/modules/staff/api/guards.js +37 -3
  59. package/dist/modules/staff/api/guards.js.map +2 -2
  60. package/dist/modules/staff/api/leave-requests/accept/route.js +1 -3
  61. package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
  62. package/dist/modules/staff/api/leave-requests/reject/route.js +1 -3
  63. package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
  64. package/dist/modules/staff/api/portal/time-reports/[id]/route.js +131 -0
  65. package/dist/modules/staff/api/portal/time-reports/[id]/route.js.map +7 -0
  66. package/dist/modules/staff/api/portal/time-reports/route.js +179 -0
  67. package/dist/modules/staff/api/portal/time-reports/route.js.map +7 -0
  68. package/dist/modules/staff/api/team-members/self/route.js +1 -3
  69. package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
  70. package/dist/modules/staff/api/team-members/tags/assign/route.js +1 -3
  71. package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
  72. package/dist/modules/staff/api/team-members/tags/unassign/route.js +1 -3
  73. package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
  74. package/dist/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.js +174 -0
  75. package/dist/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.js.map +7 -0
  76. package/dist/modules/staff/api/timesheets/access-requests/route.js +226 -0
  77. package/dist/modules/staff/api/timesheets/access-requests/route.js.map +7 -0
  78. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +15 -8
  79. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +2 -2
  80. package/dist/modules/staff/api/timesheets/my-projects/route.js +13 -1
  81. package/dist/modules/staff/api/timesheets/my-projects/route.js.map +2 -2
  82. package/dist/modules/staff/api/timesheets/my-work/myWorkAggregate.js +42 -0
  83. package/dist/modules/staff/api/timesheets/my-work/myWorkAggregate.js.map +7 -0
  84. package/dist/modules/staff/api/timesheets/my-work/route.js +383 -0
  85. package/dist/modules/staff/api/timesheets/my-work/route.js.map +7 -0
  86. package/dist/modules/staff/api/timesheets/projects/kpis/route.js +14 -2
  87. package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +2 -2
  88. package/dist/modules/staff/api/timesheets/reports/[id]/close/route.js +168 -0
  89. package/dist/modules/staff/api/timesheets/reports/[id]/close/route.js.map +7 -0
  90. package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js +246 -0
  91. package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js.map +7 -0
  92. package/dist/modules/staff/api/timesheets/reports/[id]/sheet/route.js +183 -0
  93. package/dist/modules/staff/api/timesheets/reports/[id]/sheet/route.js.map +7 -0
  94. package/dist/modules/staff/api/timesheets/reports/[id]/unlock/route.js +164 -0
  95. package/dist/modules/staff/api/timesheets/reports/[id]/unlock/route.js.map +7 -0
  96. package/dist/modules/staff/api/timesheets/reports/preview/route.js +360 -0
  97. package/dist/modules/staff/api/timesheets/reports/preview/route.js.map +7 -0
  98. package/dist/modules/staff/api/timesheets/reports/route.js +316 -0
  99. package/dist/modules/staff/api/timesheets/reports/route.js.map +7 -0
  100. package/dist/modules/staff/api/timesheets/reports/shared.js +63 -0
  101. package/dist/modules/staff/api/timesheets/reports/shared.js.map +7 -0
  102. package/dist/modules/staff/api/timesheets/settings/reapply-rounding/route.js +164 -0
  103. package/dist/modules/staff/api/timesheets/settings/reapply-rounding/route.js.map +7 -0
  104. package/dist/modules/staff/api/timesheets/settings/rounding-impact/route.js +168 -0
  105. package/dist/modules/staff/api/timesheets/settings/rounding-impact/route.js.map +7 -0
  106. package/dist/modules/staff/api/timesheets/settings/route.js +199 -0
  107. package/dist/modules/staff/api/timesheets/settings/route.js.map +7 -0
  108. package/dist/modules/staff/api/timesheets/tags/access.js +76 -0
  109. package/dist/modules/staff/api/timesheets/tags/access.js.map +7 -0
  110. package/dist/modules/staff/api/timesheets/tags/entry-assignments/route.js +235 -0
  111. package/dist/modules/staff/api/timesheets/tags/entry-assignments/route.js.map +7 -0
  112. package/dist/modules/staff/api/timesheets/tags/route.js +160 -0
  113. package/dist/modules/staff/api/timesheets/tags/route.js.map +7 -0
  114. package/dist/modules/staff/api/timesheets/tags/task-assignments/route.js +233 -0
  115. package/dist/modules/staff/api/timesheets/tags/task-assignments/route.js.map +7 -0
  116. package/dist/modules/staff/api/timesheets/task-statuses/route.js +188 -0
  117. package/dist/modules/staff/api/timesheets/task-statuses/route.js.map +7 -0
  118. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +399 -0
  119. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +7 -0
  120. package/dist/modules/staff/api/timesheets/tasks/[id]/status/route.js +210 -0
  121. package/dist/modules/staff/api/timesheets/tasks/[id]/status/route.js.map +7 -0
  122. package/dist/modules/staff/api/timesheets/tasks/route.js +333 -0
  123. package/dist/modules/staff/api/timesheets/tasks/route.js.map +7 -0
  124. package/dist/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.js +228 -0
  125. package/dist/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.js.map +7 -0
  126. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +29 -10
  127. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +2 -2
  128. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +31 -18
  129. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +2 -2
  130. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +21 -7
  131. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  132. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +27 -7
  133. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +2 -2
  134. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +215 -32
  135. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +2 -2
  136. package/dist/modules/staff/api/timesheets/time-entries/copy-day/route.js +378 -0
  137. package/dist/modules/staff/api/timesheets/time-entries/copy-day/route.js.map +7 -0
  138. package/dist/modules/staff/api/timesheets/time-entries/overlaps/route.js +340 -0
  139. package/dist/modules/staff/api/timesheets/time-entries/overlaps/route.js.map +7 -0
  140. package/dist/modules/staff/api/timesheets/time-entries/route.js +238 -26
  141. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
  142. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +23 -8
  143. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +2 -2
  144. package/dist/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.js +209 -0
  145. package/dist/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.js.map +7 -0
  146. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +59 -2
  147. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  148. package/dist/modules/staff/api/timesheets/time-projects/route.js +264 -22
  149. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  150. package/dist/modules/staff/backend/staff/time-tracking/board/page.js +110 -0
  151. package/dist/modules/staff/backend/staff/time-tracking/board/page.js.map +7 -0
  152. package/dist/modules/staff/backend/staff/time-tracking/board/page.meta.js +18 -0
  153. package/dist/modules/staff/backend/staff/time-tracking/board/page.meta.js.map +7 -0
  154. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +981 -0
  155. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +7 -0
  156. package/dist/modules/staff/backend/staff/time-tracking/entries/page.meta.js +18 -0
  157. package/dist/modules/staff/backend/staff/time-tracking/entries/page.meta.js.map +7 -0
  158. package/dist/modules/staff/backend/staff/time-tracking/page.js +583 -0
  159. package/dist/modules/staff/backend/staff/time-tracking/page.js.map +7 -0
  160. package/dist/modules/staff/backend/staff/time-tracking/page.meta.js +15 -0
  161. package/dist/modules/staff/backend/staff/time-tracking/page.meta.js.map +7 -0
  162. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.js +65 -0
  163. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.js.map +7 -0
  164. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.js +16 -0
  165. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.js.map +7 -0
  166. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +229 -0
  167. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +7 -0
  168. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.js +16 -0
  169. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.js.map +7 -0
  170. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +1014 -0
  171. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +7 -0
  172. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.js +16 -0
  173. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.js.map +7 -0
  174. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.js +97 -0
  175. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.js.map +7 -0
  176. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.meta.js +16 -0
  177. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.meta.js.map +7 -0
  178. package/dist/modules/staff/backend/staff/time-tracking/projects/page.js +1040 -0
  179. package/dist/modules/staff/backend/staff/time-tracking/projects/page.js.map +7 -0
  180. package/dist/modules/staff/backend/staff/time-tracking/projects/page.meta.js +19 -0
  181. package/dist/modules/staff/backend/staff/time-tracking/projects/page.meta.js.map +7 -0
  182. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +353 -0
  183. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +7 -0
  184. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +457 -0
  185. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +7 -0
  186. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.js +16 -0
  187. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.js.map +7 -0
  188. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.js +697 -0
  189. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.js.map +7 -0
  190. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.meta.js +16 -0
  191. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.meta.js.map +7 -0
  192. package/dist/modules/staff/backend/staff/time-tracking/reports/page.js +174 -0
  193. package/dist/modules/staff/backend/staff/time-tracking/reports/page.js.map +7 -0
  194. package/dist/modules/staff/backend/staff/time-tracking/reports/page.meta.js +18 -0
  195. package/dist/modules/staff/backend/staff/time-tracking/reports/page.meta.js.map +7 -0
  196. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +546 -0
  197. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +7 -0
  198. package/dist/modules/staff/backend/staff/time-tracking/settings/page.meta.js +18 -0
  199. package/dist/modules/staff/backend/staff/time-tracking/settings/page.meta.js.map +7 -0
  200. package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js +710 -0
  201. package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js.map +7 -0
  202. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.js +646 -0
  203. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.js.map +7 -0
  204. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.meta.js +18 -0
  205. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.meta.js.map +7 -0
  206. package/dist/modules/staff/backend/staff/timesheets/page.js +4 -793
  207. package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
  208. package/dist/modules/staff/backend/staff/timesheets/page.meta.js +2 -7
  209. package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +2 -2
  210. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +5 -136
  211. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +2 -2
  212. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +1 -7
  213. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +2 -2
  214. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +5 -445
  215. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +3 -3
  216. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +1 -7
  217. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +2 -2
  218. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +4 -76
  219. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +2 -2
  220. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +1 -7
  221. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +2 -2
  222. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +4 -613
  223. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
  224. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +2 -17
  225. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +2 -2
  226. package/dist/modules/staff/ce.js +40 -0
  227. package/dist/modules/staff/ce.js.map +2 -2
  228. package/dist/modules/staff/cli.js +166 -1
  229. package/dist/modules/staff/cli.js.map +2 -2
  230. package/dist/modules/staff/commands/index.js +5 -0
  231. package/dist/modules/staff/commands/index.js.map +2 -2
  232. package/dist/modules/staff/commands/timesheets-entries.js +569 -21
  233. package/dist/modules/staff/commands/timesheets-entries.js.map +3 -3
  234. package/dist/modules/staff/commands/timesheets-projects.js +450 -7
  235. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  236. package/dist/modules/staff/commands/timesheets-reports.js +1036 -0
  237. package/dist/modules/staff/commands/timesheets-reports.js.map +7 -0
  238. package/dist/modules/staff/commands/timesheets-tags.js +1015 -0
  239. package/dist/modules/staff/commands/timesheets-tags.js.map +7 -0
  240. package/dist/modules/staff/commands/timesheets-task-comments.js +438 -0
  241. package/dist/modules/staff/commands/timesheets-task-comments.js.map +7 -0
  242. package/dist/modules/staff/commands/timesheets-task-statuses.js +723 -0
  243. package/dist/modules/staff/commands/timesheets-task-statuses.js.map +7 -0
  244. package/dist/modules/staff/commands/timesheets-tasks.js +834 -0
  245. package/dist/modules/staff/commands/timesheets-tasks.js.map +7 -0
  246. package/dist/modules/staff/data/enrichers.js +426 -12
  247. package/dist/modules/staff/data/enrichers.js.map +2 -2
  248. package/dist/modules/staff/data/entities.js +552 -3
  249. package/dist/modules/staff/data/entities.js.map +2 -2
  250. package/dist/modules/staff/data/extensions.js +41 -0
  251. package/dist/modules/staff/data/extensions.js.map +7 -0
  252. package/dist/modules/staff/data/validators.js +254 -9
  253. package/dist/modules/staff/data/validators.js.map +2 -2
  254. package/dist/modules/staff/di.js +42 -1
  255. package/dist/modules/staff/di.js.map +2 -2
  256. package/dist/modules/staff/events.js +84 -6
  257. package/dist/modules/staff/events.js.map +2 -2
  258. package/dist/modules/staff/events.payloads.js +158 -0
  259. package/dist/modules/staff/events.payloads.js.map +7 -0
  260. package/dist/modules/staff/extension-points.js +261 -0
  261. package/dist/modules/staff/extension-points.js.map +7 -0
  262. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +138 -0
  263. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +7 -0
  264. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.js +12 -0
  265. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.js.map +7 -0
  266. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +133 -0
  267. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +7 -0
  268. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.js +19 -0
  269. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.js.map +7 -0
  270. package/dist/modules/staff/lib/crud.js +11 -1
  271. package/dist/modules/staff/lib/crud.js.map +2 -2
  272. package/dist/modules/staff/lib/time-tracking/access.js +92 -0
  273. package/dist/modules/staff/lib/time-tracking/access.js.map +7 -0
  274. package/dist/modules/staff/lib/time-tracking/billability.js +39 -0
  275. package/dist/modules/staff/lib/time-tracking/billability.js.map +7 -0
  276. package/dist/modules/staff/lib/time-tracking/capacity.js +68 -0
  277. package/dist/modules/staff/lib/time-tracking/capacity.js.map +7 -0
  278. package/dist/modules/staff/lib/time-tracking/componentContracts.js +15 -0
  279. package/dist/modules/staff/lib/time-tracking/componentContracts.js.map +7 -0
  280. package/dist/modules/staff/lib/time-tracking/cost.js +82 -0
  281. package/dist/modules/staff/lib/time-tracking/cost.js.map +7 -0
  282. package/dist/modules/staff/lib/time-tracking/duration.js +55 -0
  283. package/dist/modules/staff/lib/time-tracking/duration.js.map +7 -0
  284. package/dist/modules/staff/lib/time-tracking/featureAccess.js +41 -0
  285. package/dist/modules/staff/lib/time-tracking/featureAccess.js.map +7 -0
  286. package/dist/modules/staff/lib/time-tracking/interval.js +72 -0
  287. package/dist/modules/staff/lib/time-tracking/interval.js.map +7 -0
  288. package/dist/modules/staff/lib/time-tracking/migrateProjectCodes.js +88 -0
  289. package/dist/modules/staff/lib/time-tracking/migrateProjectCodes.js.map +7 -0
  290. package/dist/modules/staff/lib/time-tracking/moneyVisibility.js +10 -0
  291. package/dist/modules/staff/lib/time-tracking/moneyVisibility.js.map +7 -0
  292. package/dist/modules/staff/lib/time-tracking/overlap.js +87 -0
  293. package/dist/modules/staff/lib/time-tracking/overlap.js.map +7 -0
  294. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +27 -0
  295. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +7 -0
  296. package/dist/modules/staff/lib/time-tracking/portalReports.js +16 -0
  297. package/dist/modules/staff/lib/time-tracking/portalReports.js.map +7 -0
  298. package/dist/modules/staff/lib/time-tracking/projectCode.js +135 -0
  299. package/dist/modules/staff/lib/time-tracking/projectCode.js.map +7 -0
  300. package/dist/modules/staff/lib/time-tracking/reapplyRounding.js +187 -0
  301. package/dist/modules/staff/lib/time-tracking/reapplyRounding.js.map +7 -0
  302. package/dist/modules/staff/lib/time-tracking/recalculationRunner.js +80 -0
  303. package/dist/modules/staff/lib/time-tracking/recalculationRunner.js.map +7 -0
  304. package/dist/modules/staff/lib/time-tracking/recalculations.js +56 -0
  305. package/dist/modules/staff/lib/time-tracking/recalculations.js.map +7 -0
  306. package/dist/modules/staff/lib/time-tracking/registries/invoke.js +63 -0
  307. package/dist/modules/staff/lib/time-tracking/registries/invoke.js.map +7 -0
  308. package/dist/modules/staff/lib/time-tracking/registries/registry.js +62 -0
  309. package/dist/modules/staff/lib/time-tracking/registries/registry.js.map +7 -0
  310. package/dist/modules/staff/lib/time-tracking/registries/scope.js +21 -0
  311. package/dist/modules/staff/lib/time-tracking/registries/scope.js.map +7 -0
  312. package/dist/modules/staff/lib/time-tracking/rollup.js +34 -0
  313. package/dist/modules/staff/lib/time-tracking/rollup.js.map +7 -0
  314. package/dist/modules/staff/lib/time-tracking/rounding.js +66 -0
  315. package/dist/modules/staff/lib/time-tracking/rounding.js.map +7 -0
  316. package/dist/modules/staff/lib/time-tracking/roundingImpact.js +39 -0
  317. package/dist/modules/staff/lib/time-tracking/roundingImpact.js.map +7 -0
  318. package/dist/modules/staff/lib/time-tracking/settingKeys.js +214 -0
  319. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +7 -0
  320. package/dist/modules/staff/lib/time-tracking/settings.js +87 -0
  321. package/dist/modules/staff/lib/time-tracking/settings.js.map +7 -0
  322. package/dist/modules/staff/lib/time-tracking/sqlInClause.js +9 -0
  323. package/dist/modules/staff/lib/time-tracking/sqlInClause.js.map +7 -0
  324. package/dist/modules/staff/lib/time-tracking/timeEntrySources.js +48 -0
  325. package/dist/modules/staff/lib/time-tracking/timeEntrySources.js.map +7 -0
  326. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +283 -0
  327. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +7 -0
  328. package/dist/modules/staff/lib/time-tracking-ui/CustomerPicker.js +324 -0
  329. package/dist/modules/staff/lib/time-tracking-ui/CustomerPicker.js.map +7 -0
  330. package/dist/modules/staff/lib/time-tracking-ui/DurationInput.js +121 -0
  331. package/dist/modules/staff/lib/time-tracking-ui/DurationInput.js.map +7 -0
  332. package/dist/modules/staff/lib/time-tracking-ui/EntryDetailsFields.js +188 -0
  333. package/dist/modules/staff/lib/time-tracking-ui/EntryDetailsFields.js.map +7 -0
  334. package/dist/modules/staff/lib/time-tracking-ui/EntryFilterBar.js +89 -0
  335. package/dist/modules/staff/lib/time-tracking-ui/EntryFilterBar.js.map +7 -0
  336. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +855 -0
  337. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +7 -0
  338. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +360 -0
  339. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +7 -0
  340. package/dist/modules/staff/lib/time-tracking-ui/KanbanColumn.js +259 -0
  341. package/dist/modules/staff/lib/time-tracking-ui/KanbanColumn.js.map +7 -0
  342. package/dist/modules/staff/lib/time-tracking-ui/NewTaskDialog.js +238 -0
  343. package/dist/modules/staff/lib/time-tracking-ui/NewTaskDialog.js.map +7 -0
  344. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +130 -0
  345. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +7 -0
  346. package/dist/modules/staff/lib/time-tracking-ui/PeriodSelector.js +98 -0
  347. package/dist/modules/staff/lib/time-tracking-ui/PeriodSelector.js.map +7 -0
  348. package/dist/modules/staff/lib/time-tracking-ui/PhasePlaceholder.js +27 -0
  349. package/dist/modules/staff/lib/time-tracking-ui/PhasePlaceholder.js.map +7 -0
  350. package/dist/modules/staff/lib/time-tracking-ui/ProjectCodeField.js +166 -0
  351. package/dist/modules/staff/lib/time-tracking-ui/ProjectCodeField.js.map +7 -0
  352. package/dist/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.js +295 -0
  353. package/dist/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.js.map +7 -0
  354. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +212 -0
  355. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +7 -0
  356. package/dist/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.js +729 -0
  357. package/dist/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.js.map +7 -0
  358. package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +217 -0
  359. package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +7 -0
  360. package/dist/modules/staff/lib/time-tracking-ui/TagPicker.js +158 -0
  361. package/dist/modules/staff/lib/time-tracking-ui/TagPicker.js.map +7 -0
  362. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +224 -0
  363. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +7 -0
  364. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +93 -0
  365. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +7 -0
  366. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +1196 -0
  367. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +7 -0
  368. package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +176 -0
  369. package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +7 -0
  370. package/dist/modules/staff/lib/time-tracking-ui/TaskPicker.js +307 -0
  371. package/dist/modules/staff/lib/time-tracking-ui/TaskPicker.js.map +7 -0
  372. package/dist/modules/staff/lib/time-tracking-ui/TaskQuickLog.js +284 -0
  373. package/dist/modules/staff/lib/time-tracking-ui/TaskQuickLog.js.map +7 -0
  374. package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js +73 -0
  375. package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js.map +7 -0
  376. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDialog.js +1499 -0
  377. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDialog.js.map +7 -0
  378. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.js +95 -0
  379. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.js.map +7 -0
  380. package/dist/modules/staff/lib/time-tracking-ui/TimesheetCalendar.js +188 -0
  381. package/dist/modules/staff/lib/time-tracking-ui/TimesheetCalendar.js.map +7 -0
  382. package/dist/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.js +61 -0
  383. package/dist/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.js.map +7 -0
  384. package/dist/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.js +45 -0
  385. package/dist/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.js.map +7 -0
  386. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +176 -0
  387. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +7 -0
  388. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +82 -0
  389. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +7 -0
  390. package/dist/modules/staff/lib/time-tracking-ui/boardSummary.js +5 -0
  391. package/dist/modules/staff/lib/time-tracking-ui/boardSummary.js.map +7 -0
  392. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +152 -0
  393. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +7 -0
  394. package/dist/modules/staff/lib/time-tracking-ui/projectTeamAssignment.js +92 -0
  395. package/dist/modules/staff/lib/time-tracking-ui/projectTeamAssignment.js.map +7 -0
  396. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +111 -0
  397. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +7 -0
  398. package/dist/modules/staff/lib/time-tracking-ui/timeEntryDialogState.js +243 -0
  399. package/dist/modules/staff/lib/time-tracking-ui/timeEntryDialogState.js.map +7 -0
  400. package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js +151 -0
  401. package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js.map +7 -0
  402. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +105 -0
  403. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +7 -0
  404. package/dist/modules/staff/lib/time-tracking-ui/timesheetData.js +132 -0
  405. package/dist/modules/staff/lib/time-tracking-ui/timesheetData.js.map +7 -0
  406. package/dist/modules/staff/lib/time-tracking-ui/timesheetPeriod.js +205 -0
  407. package/dist/modules/staff/lib/time-tracking-ui/timesheetPeriod.js.map +7 -0
  408. package/dist/modules/staff/lib/time-tracking-ui/timesheetTargets.js +53 -0
  409. package/dist/modules/staff/lib/time-tracking-ui/timesheetTargets.js.map +7 -0
  410. package/dist/modules/staff/lib/time-tracking-ui/useBoardFilters.js +77 -0
  411. package/dist/modules/staff/lib/time-tracking-ui/useBoardFilters.js.map +7 -0
  412. package/dist/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.js +151 -0
  413. package/dist/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.js.map +7 -0
  414. package/dist/modules/staff/lib/timeTrackingSeeds.js +809 -0
  415. package/dist/modules/staff/lib/timeTrackingSeeds.js.map +7 -0
  416. package/dist/modules/staff/lib/timesheets/timeEntryCacheInvalidation.js +2 -3
  417. package/dist/modules/staff/lib/timesheets/timeEntryCacheInvalidation.js.map +2 -2
  418. package/dist/modules/staff/lib/timesheets/timeEntryDecoration.js +152 -0
  419. package/dist/modules/staff/lib/timesheets/timeEntryDecoration.js.map +7 -0
  420. package/dist/modules/staff/lib/timesheets-projects/budgetBurn.js +36 -0
  421. package/dist/modules/staff/lib/timesheets-projects/budgetBurn.js.map +7 -0
  422. package/dist/modules/staff/lib/timesheets-projects/budgetThreshold.js +66 -0
  423. package/dist/modules/staff/lib/timesheets-projects/budgetThreshold.js.map +7 -0
  424. package/dist/modules/staff/lib/timesheets-projects/budgetThresholdState.js +78 -0
  425. package/dist/modules/staff/lib/timesheets-projects/budgetThresholdState.js.map +7 -0
  426. package/dist/modules/staff/lib/timesheets-projects/computeProjectFinancials.js +81 -0
  427. package/dist/modules/staff/lib/timesheets-projects/computeProjectFinancials.js.map +7 -0
  428. package/dist/modules/staff/lib/timesheets-projects/reportSelection.js +44 -0
  429. package/dist/modules/staff/lib/timesheets-projects/reportSelection.js.map +7 -0
  430. package/dist/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.js +169 -0
  431. package/dist/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.js.map +7 -0
  432. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.js +22 -0
  433. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.js.map +7 -0
  434. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +51 -1
  435. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +2 -2
  436. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.js +70 -0
  437. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.js.map +7 -0
  438. package/dist/modules/staff/lib/timesheets-reports/buildReportSheet.js +63 -0
  439. package/dist/modules/staff/lib/timesheets-reports/buildReportSheet.js.map +7 -0
  440. package/dist/modules/staff/lib/timesheets-reports/loadReportData.js +170 -0
  441. package/dist/modules/staff/lib/timesheets-reports/loadReportData.js.map +7 -0
  442. package/dist/modules/staff/lib/timesheets-reports/pdf.js +205 -0
  443. package/dist/modules/staff/lib/timesheets-reports/pdf.js.map +7 -0
  444. package/dist/modules/staff/lib/timesheets-reports/reportApprovalPolicies.js +68 -0
  445. package/dist/modules/staff/lib/timesheets-reports/reportApprovalPolicies.js.map +7 -0
  446. package/dist/modules/staff/lib/timesheets-reports/reportConfigData.js +135 -0
  447. package/dist/modules/staff/lib/timesheets-reports/reportConfigData.js.map +7 -0
  448. package/dist/modules/staff/lib/timesheets-reports/reportConfigState.js +48 -0
  449. package/dist/modules/staff/lib/timesheets-reports/reportConfigState.js.map +7 -0
  450. package/dist/modules/staff/lib/timesheets-reports/reportExport.js +262 -0
  451. package/dist/modules/staff/lib/timesheets-reports/reportExport.js.map +7 -0
  452. package/dist/modules/staff/lib/timesheets-reports/reportExportFormats.js +34 -0
  453. package/dist/modules/staff/lib/timesheets-reports/reportExportFormats.js.map +7 -0
  454. package/dist/modules/staff/lib/timesheets-reports/reportGroupings.js +79 -0
  455. package/dist/modules/staff/lib/timesheets-reports/reportGroupings.js.map +7 -0
  456. package/dist/modules/staff/lib/timesheets-reports/reportLabels.js +11 -0
  457. package/dist/modules/staff/lib/timesheets-reports/reportLabels.js.map +7 -0
  458. package/dist/modules/staff/lib/timesheets-reports/reportReference.js +83 -0
  459. package/dist/modules/staff/lib/timesheets-reports/reportReference.js.map +7 -0
  460. package/dist/modules/staff/lib/timesheets-reports/reportRows.js +42 -0
  461. package/dist/modules/staff/lib/timesheets-reports/reportRows.js.map +7 -0
  462. package/dist/modules/staff/lib/timesheets-reports/reportSheetData.js +107 -0
  463. package/dist/modules/staff/lib/timesheets-reports/reportSheetData.js.map +7 -0
  464. package/dist/modules/staff/lib/timesheets-reports/reportTotals.js +289 -0
  465. package/dist/modules/staff/lib/timesheets-reports/reportTotals.js.map +7 -0
  466. package/dist/modules/staff/lib/timesheets-reports/xlsx.js +145 -0
  467. package/dist/modules/staff/lib/timesheets-reports/xlsx.js.map +7 -0
  468. package/dist/modules/staff/lib/timesheets-tasks/computeTaskRollups.js +116 -0
  469. package/dist/modules/staff/lib/timesheets-tasks/computeTaskRollups.js.map +7 -0
  470. package/dist/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.js +50 -0
  471. package/dist/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.js.map +7 -0
  472. package/dist/modules/staff/lib/timesheets-tasks/seedProjectStatuses.js +27 -0
  473. package/dist/modules/staff/lib/timesheets-tasks/seedProjectStatuses.js.map +7 -0
  474. package/dist/modules/staff/lib/timesheets-tasks/statusPositions.js +65 -0
  475. package/dist/modules/staff/lib/timesheets-tasks/statusPositions.js.map +7 -0
  476. package/dist/modules/staff/lib/timesheets-tasks/statusSlug.js +31 -0
  477. package/dist/modules/staff/lib/timesheets-tasks/statusSlug.js.map +7 -0
  478. package/dist/modules/staff/lib/timesheets-tasks/taskHierarchy.js +31 -0
  479. package/dist/modules/staff/lib/timesheets-tasks/taskHierarchy.js.map +7 -0
  480. package/dist/modules/staff/lib/timesheets-tasks/taskHoursTotals.js +17 -0
  481. package/dist/modules/staff/lib/timesheets-tasks/taskHoursTotals.js.map +7 -0
  482. package/dist/modules/staff/lib/timesheets-tasks/taskReference.js +51 -0
  483. package/dist/modules/staff/lib/timesheets-tasks/taskReference.js.map +7 -0
  484. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +11 -4
  485. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +2 -2
  486. package/dist/modules/staff/lib/timesheets-ui/ListView.js +369 -111
  487. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  488. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +47 -2
  489. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  490. package/dist/modules/staff/migrations/Migration20260813073131_staff.js +79 -0
  491. package/dist/modules/staff/migrations/Migration20260813073131_staff.js.map +7 -0
  492. package/dist/modules/staff/migrations/Migration20260824143357_staff.js +15 -0
  493. package/dist/modules/staff/migrations/Migration20260824143357_staff.js.map +7 -0
  494. package/dist/modules/staff/notifications.client.js +16 -0
  495. package/dist/modules/staff/notifications.client.js.map +7 -0
  496. package/dist/modules/staff/notifications.handlers.js +80 -0
  497. package/dist/modules/staff/notifications.handlers.js.map +7 -0
  498. package/dist/modules/staff/notifications.js +168 -0
  499. package/dist/modules/staff/notifications.js.map +2 -2
  500. package/dist/modules/staff/search.js +159 -1
  501. package/dist/modules/staff/search.js.map +2 -2
  502. package/dist/modules/staff/setup.js +20 -1
  503. package/dist/modules/staff/setup.js.map +2 -2
  504. package/dist/modules/staff/subscribers/time-project-access-requested-notification.js +68 -0
  505. package/dist/modules/staff/subscribers/time-project-access-requested-notification.js.map +7 -0
  506. package/dist/modules/staff/subscribers/time-project-budget-threshold-notification.js +149 -0
  507. package/dist/modules/staff/subscribers/time-project-budget-threshold-notification.js.map +7 -0
  508. package/dist/modules/staff/subscribers/time-report-approved-notification.js +78 -0
  509. package/dist/modules/staff/subscribers/time-report-approved-notification.js.map +7 -0
  510. package/dist/modules/staff/subscribers/time-report-portal-broadcast.js +78 -0
  511. package/dist/modules/staff/subscribers/time-report-portal-broadcast.js.map +7 -0
  512. package/dist/modules/staff/translations.js +4 -1
  513. package/dist/modules/staff/translations.js.map +2 -2
  514. package/dist/modules/staff/widgets/components.js +7 -0
  515. package/dist/modules/staff/widgets/components.js.map +7 -0
  516. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +5 -4
  517. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
  518. package/dist/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.js +135 -0
  519. package/dist/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.js.map +7 -0
  520. package/dist/modules/staff/widgets/notifications/index.js +5 -0
  521. package/dist/modules/staff/widgets/notifications/index.js.map +7 -0
  522. package/dist/modules/staff/workers/timesheets-reapply-rounding.js +43 -0
  523. package/dist/modules/staff/workers/timesheets-reapply-rounding.js.map +7 -0
  524. package/generated/entities/staff_time_entry/index.ts +7 -0
  525. package/generated/entities/staff_time_entry_tag/index.ts +6 -0
  526. package/generated/entities/staff_time_project/index.ts +8 -0
  527. package/generated/entities/staff_time_report/index.ts +27 -0
  528. package/generated/entities/staff_time_report_entry/index.ts +12 -0
  529. package/generated/entities/staff_time_report_event/index.ts +9 -0
  530. package/generated/entities/staff_time_report_project/index.ts +6 -0
  531. package/generated/entities/staff_time_tag/index.ts +9 -0
  532. package/generated/entities/staff_time_task/index.ts +17 -0
  533. package/generated/entities/staff_time_task_comment/index.ts +9 -0
  534. package/generated/entities/staff_time_task_status/index.ts +13 -0
  535. package/generated/entities/staff_time_task_tag/index.ts +6 -0
  536. package/generated/entities.ids.generated.ts +11 -1
  537. package/generated/entity-fields-registry.ts +149 -0
  538. package/package.json +7 -7
  539. package/src/helpers/integration/timesheetFixtures.ts +31 -1
  540. package/src/modules/auth/README.md +1 -1
  541. package/src/modules/auth/cli.ts +35 -1
  542. package/src/modules/auth/i18n/de.json +7 -0
  543. package/src/modules/auth/i18n/en.json +7 -0
  544. package/src/modules/auth/i18n/es.json +7 -0
  545. package/src/modules/auth/i18n/ko.json +7 -0
  546. package/src/modules/auth/i18n/pl.json +7 -0
  547. package/src/modules/auth/lib/backendChrome.tsx +1 -0
  548. package/src/modules/customer_accounts/lib/customerRoleAcls.ts +79 -0
  549. package/src/modules/customer_accounts/setup.ts +1 -45
  550. package/src/modules/data_sync/AGENTS.md +34 -0
  551. package/src/modules/data_sync/api/options.ts +2 -0
  552. package/src/modules/data_sync/backend/data-sync/page.tsx +82 -33
  553. package/src/modules/data_sync/i18n/de.json +1 -0
  554. package/src/modules/data_sync/i18n/en.json +1 -0
  555. package/src/modules/data_sync/i18n/es.json +1 -0
  556. package/src/modules/data_sync/i18n/ko.json +1 -0
  557. package/src/modules/data_sync/i18n/pl.json +1 -0
  558. package/src/modules/data_sync/lib/adapter.ts +45 -0
  559. package/src/modules/data_sync/lib/start-controls.ts +89 -0
  560. package/src/modules/staff/AGENTS.md +769 -1
  561. package/src/modules/staff/acl.ts +9 -0
  562. package/src/modules/staff/ai-agents.ts +189 -0
  563. package/src/modules/staff/ai-tools/time-tracking-pack.ts +494 -0
  564. package/src/modules/staff/ai-tools/types.ts +82 -0
  565. package/src/modules/staff/ai-tools.ts +13 -0
  566. package/src/modules/staff/analytics.ts +113 -0
  567. package/src/modules/staff/api/guards.ts +85 -3
  568. package/src/modules/staff/api/leave-requests/accept/route.ts +0 -2
  569. package/src/modules/staff/api/leave-requests/reject/route.ts +0 -2
  570. package/src/modules/staff/api/portal/time-reports/[id]/route.ts +182 -0
  571. package/src/modules/staff/api/portal/time-reports/route.ts +270 -0
  572. package/src/modules/staff/api/team-members/self/route.ts +0 -2
  573. package/src/modules/staff/api/team-members/tags/assign/route.ts +0 -2
  574. package/src/modules/staff/api/team-members/tags/unassign/route.ts +0 -2
  575. package/src/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.ts +292 -0
  576. package/src/modules/staff/api/timesheets/access-requests/route.ts +299 -0
  577. package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +15 -7
  578. package/src/modules/staff/api/timesheets/my-projects/route.ts +14 -1
  579. package/src/modules/staff/api/timesheets/my-work/myWorkAggregate.ts +90 -0
  580. package/src/modules/staff/api/timesheets/my-work/route.ts +475 -0
  581. package/src/modules/staff/api/timesheets/projects/kpis/route.ts +15 -2
  582. package/src/modules/staff/api/timesheets/reports/[id]/close/route.ts +203 -0
  583. package/src/modules/staff/api/timesheets/reports/[id]/export/route.ts +315 -0
  584. package/src/modules/staff/api/timesheets/reports/[id]/sheet/route.ts +220 -0
  585. package/src/modules/staff/api/timesheets/reports/[id]/unlock/route.ts +208 -0
  586. package/src/modules/staff/api/timesheets/reports/preview/route.ts +449 -0
  587. package/src/modules/staff/api/timesheets/reports/route.ts +386 -0
  588. package/src/modules/staff/api/timesheets/reports/shared.ts +121 -0
  589. package/src/modules/staff/api/timesheets/settings/reapply-rounding/route.ts +205 -0
  590. package/src/modules/staff/api/timesheets/settings/rounding-impact/route.ts +226 -0
  591. package/src/modules/staff/api/timesheets/settings/route.ts +242 -0
  592. package/src/modules/staff/api/timesheets/tags/access.ts +134 -0
  593. package/src/modules/staff/api/timesheets/tags/entry-assignments/route.ts +290 -0
  594. package/src/modules/staff/api/timesheets/tags/route.ts +177 -0
  595. package/src/modules/staff/api/timesheets/tags/task-assignments/route.ts +294 -0
  596. package/src/modules/staff/api/timesheets/task-statuses/route.ts +232 -0
  597. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +532 -0
  598. package/src/modules/staff/api/timesheets/tasks/[id]/status/route.ts +271 -0
  599. package/src/modules/staff/api/timesheets/tasks/route.ts +443 -0
  600. package/src/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.ts +306 -0
  601. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +32 -10
  602. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +32 -17
  603. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +27 -6
  604. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +36 -6
  605. package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +331 -34
  606. package/src/modules/staff/api/timesheets/time-entries/copy-day/route.ts +533 -0
  607. package/src/modules/staff/api/timesheets/time-entries/overlaps/route.ts +457 -0
  608. package/src/modules/staff/api/timesheets/time-entries/route.ts +378 -24
  609. package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +31 -9
  610. package/src/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.ts +246 -0
  611. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +62 -2
  612. package/src/modules/staff/api/timesheets/time-projects/route.ts +397 -22
  613. package/src/modules/staff/backend/staff/time-tracking/board/page.meta.ts +14 -0
  614. package/src/modules/staff/backend/staff/time-tracking/board/page.tsx +151 -0
  615. package/src/modules/staff/backend/staff/time-tracking/entries/page.meta.ts +14 -0
  616. package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +1229 -0
  617. package/src/modules/staff/backend/staff/time-tracking/page.meta.ts +11 -0
  618. package/src/modules/staff/backend/staff/time-tracking/page.tsx +823 -0
  619. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.ts +12 -0
  620. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.tsx +81 -0
  621. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.ts +12 -0
  622. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +275 -0
  623. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.ts +12 -0
  624. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +1420 -0
  625. package/src/modules/staff/backend/staff/time-tracking/projects/create/page.meta.ts +12 -0
  626. package/src/modules/staff/backend/staff/time-tracking/projects/create/page.tsx +109 -0
  627. package/src/modules/staff/backend/staff/time-tracking/projects/page.meta.ts +15 -0
  628. package/src/modules/staff/backend/staff/time-tracking/projects/page.tsx +1254 -0
  629. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +466 -0
  630. package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.ts +12 -0
  631. package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +632 -0
  632. package/src/modules/staff/backend/staff/time-tracking/reports/create/page.meta.ts +12 -0
  633. package/src/modules/staff/backend/staff/time-tracking/reports/create/page.tsx +867 -0
  634. package/src/modules/staff/backend/staff/time-tracking/reports/page.meta.ts +14 -0
  635. package/src/modules/staff/backend/staff/time-tracking/reports/page.tsx +246 -0
  636. package/src/modules/staff/backend/staff/time-tracking/settings/page.meta.ts +14 -0
  637. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +701 -0
  638. package/src/modules/staff/backend/staff/time-tracking/timesheet/GridView.tsx +953 -0
  639. package/src/modules/staff/backend/staff/time-tracking/timesheet/page.meta.ts +14 -0
  640. package/src/modules/staff/backend/staff/time-tracking/timesheet/page.tsx +800 -0
  641. package/src/modules/staff/backend/staff/timesheets/page.meta.ts +6 -6
  642. package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1010
  643. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +0 -6
  644. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +4 -158
  645. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +0 -6
  646. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +4 -609
  647. package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +0 -6
  648. package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +3 -89
  649. package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +2 -19
  650. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +3 -778
  651. package/src/modules/staff/ce.ts +63 -0
  652. package/src/modules/staff/cli.ts +207 -1
  653. package/src/modules/staff/commands/index.ts +5 -0
  654. package/src/modules/staff/commands/timesheets-entries.ts +1006 -28
  655. package/src/modules/staff/commands/timesheets-projects.ts +593 -7
  656. package/src/modules/staff/commands/timesheets-reports.ts +1329 -0
  657. package/src/modules/staff/commands/timesheets-tags.ts +1315 -0
  658. package/src/modules/staff/commands/timesheets-task-comments.ts +583 -0
  659. package/src/modules/staff/commands/timesheets-task-statuses.ts +889 -0
  660. package/src/modules/staff/commands/timesheets-tasks.ts +1121 -0
  661. package/src/modules/staff/data/enrichers.ts +675 -10
  662. package/src/modules/staff/data/entities.ts +494 -2
  663. package/src/modules/staff/data/extensions.ts +68 -0
  664. package/src/modules/staff/data/validators.ts +348 -3
  665. package/src/modules/staff/di.ts +96 -0
  666. package/src/modules/staff/events.payloads.ts +268 -0
  667. package/src/modules/staff/events.ts +87 -5
  668. package/src/modules/staff/extension-points.ts +308 -0
  669. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.ts +10 -0
  670. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +225 -0
  671. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.ts +17 -0
  672. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +182 -0
  673. package/src/modules/staff/i18n/de.json +899 -0
  674. package/src/modules/staff/i18n/en.json +899 -0
  675. package/src/modules/staff/i18n/es.json +899 -0
  676. package/src/modules/staff/i18n/ko.json +899 -0
  677. package/src/modules/staff/i18n/pl.json +899 -0
  678. package/src/modules/staff/lib/crud.ts +14 -3
  679. package/src/modules/staff/lib/time-tracking/access.ts +155 -0
  680. package/src/modules/staff/lib/time-tracking/billability.ts +68 -0
  681. package/src/modules/staff/lib/time-tracking/capacity.ts +124 -0
  682. package/src/modules/staff/lib/time-tracking/componentContracts.ts +26 -0
  683. package/src/modules/staff/lib/time-tracking/cost.ts +144 -0
  684. package/src/modules/staff/lib/time-tracking/duration.ts +68 -0
  685. package/src/modules/staff/lib/time-tracking/featureAccess.ts +115 -0
  686. package/src/modules/staff/lib/time-tracking/interval.ts +90 -0
  687. package/src/modules/staff/lib/time-tracking/migrateProjectCodes.ts +161 -0
  688. package/src/modules/staff/lib/time-tracking/moneyVisibility.ts +37 -0
  689. package/src/modules/staff/lib/time-tracking/overlap.ts +159 -0
  690. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +76 -0
  691. package/src/modules/staff/lib/time-tracking/portalReports.ts +31 -0
  692. package/src/modules/staff/lib/time-tracking/projectCode.ts +221 -0
  693. package/src/modules/staff/lib/time-tracking/reapplyRounding.ts +313 -0
  694. package/src/modules/staff/lib/time-tracking/recalculationRunner.ts +118 -0
  695. package/src/modules/staff/lib/time-tracking/recalculations.ts +135 -0
  696. package/src/modules/staff/lib/time-tracking/registries/invoke.ts +122 -0
  697. package/src/modules/staff/lib/time-tracking/registries/registry.ts +124 -0
  698. package/src/modules/staff/lib/time-tracking/registries/scope.ts +56 -0
  699. package/src/modules/staff/lib/time-tracking/rollup.ts +59 -0
  700. package/src/modules/staff/lib/time-tracking/rounding.ts +126 -0
  701. package/src/modules/staff/lib/time-tracking/roundingImpact.ts +75 -0
  702. package/src/modules/staff/lib/time-tracking/settingKeys.ts +274 -0
  703. package/src/modules/staff/lib/time-tracking/settings.ts +168 -0
  704. package/src/modules/staff/lib/time-tracking/sqlInClause.ts +22 -0
  705. package/src/modules/staff/lib/time-tracking/timeEntrySources.ts +85 -0
  706. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +360 -0
  707. package/src/modules/staff/lib/time-tracking-ui/CustomerPicker.tsx +427 -0
  708. package/src/modules/staff/lib/time-tracking-ui/DurationInput.tsx +192 -0
  709. package/src/modules/staff/lib/time-tracking-ui/EntryDetailsFields.tsx +224 -0
  710. package/src/modules/staff/lib/time-tracking-ui/EntryFilterBar.tsx +144 -0
  711. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +1065 -0
  712. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +435 -0
  713. package/src/modules/staff/lib/time-tracking-ui/KanbanColumn.tsx +316 -0
  714. package/src/modules/staff/lib/time-tracking-ui/NewTaskDialog.tsx +291 -0
  715. package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +162 -0
  716. package/src/modules/staff/lib/time-tracking-ui/PeriodSelector.tsx +149 -0
  717. package/src/modules/staff/lib/time-tracking-ui/PhasePlaceholder.tsx +31 -0
  718. package/src/modules/staff/lib/time-tracking-ui/ProjectCodeField.tsx +230 -0
  719. package/src/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.tsx +432 -0
  720. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +323 -0
  721. package/src/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.tsx +930 -0
  722. package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +357 -0
  723. package/src/modules/staff/lib/time-tracking-ui/TagPicker.tsx +195 -0
  724. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +243 -0
  725. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +137 -0
  726. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +1405 -0
  727. package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +260 -0
  728. package/src/modules/staff/lib/time-tracking-ui/TaskPicker.tsx +435 -0
  729. package/src/modules/staff/lib/time-tracking-ui/TaskQuickLog.tsx +296 -0
  730. package/src/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx +97 -0
  731. package/src/modules/staff/lib/time-tracking-ui/TimeEntryDialog.tsx +1929 -0
  732. package/src/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.tsx +126 -0
  733. package/src/modules/staff/lib/time-tracking-ui/TimesheetCalendar.tsx +265 -0
  734. package/src/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.tsx +78 -0
  735. package/src/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.tsx +70 -0
  736. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +231 -0
  737. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +128 -0
  738. package/src/modules/staff/lib/time-tracking-ui/boardSummary.ts +20 -0
  739. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +228 -0
  740. package/src/modules/staff/lib/time-tracking-ui/projectTeamAssignment.ts +143 -0
  741. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +194 -0
  742. package/src/modules/staff/lib/time-tracking-ui/timeEntryDialogState.ts +400 -0
  743. package/src/modules/staff/lib/time-tracking-ui/timeEntryListData.ts +271 -0
  744. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +168 -0
  745. package/src/modules/staff/lib/time-tracking-ui/timesheetData.ts +225 -0
  746. package/src/modules/staff/lib/time-tracking-ui/timesheetPeriod.ts +278 -0
  747. package/src/modules/staff/lib/time-tracking-ui/timesheetTargets.ts +93 -0
  748. package/src/modules/staff/lib/time-tracking-ui/useBoardFilters.ts +107 -0
  749. package/src/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.ts +245 -0
  750. package/src/modules/staff/lib/timeTrackingSeeds.ts +1037 -0
  751. package/src/modules/staff/lib/timesheets/timeEntryCacheInvalidation.ts +11 -10
  752. package/src/modules/staff/lib/timesheets/timeEntryDecoration.ts +256 -0
  753. package/src/modules/staff/lib/timesheets-projects/budgetBurn.ts +60 -0
  754. package/src/modules/staff/lib/timesheets-projects/budgetThreshold.ts +135 -0
  755. package/src/modules/staff/lib/timesheets-projects/budgetThresholdState.ts +137 -0
  756. package/src/modules/staff/lib/timesheets-projects/computeProjectFinancials.ts +142 -0
  757. package/src/modules/staff/lib/timesheets-projects/reportSelection.ts +82 -0
  758. package/src/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.tsx +237 -0
  759. package/src/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.tsx +33 -0
  760. package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +56 -1
  761. package/src/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.tsx +90 -0
  762. package/src/modules/staff/lib/timesheets-reports/buildReportSheet.ts +125 -0
  763. package/src/modules/staff/lib/timesheets-reports/loadReportData.ts +251 -0
  764. package/src/modules/staff/lib/timesheets-reports/pdf.ts +263 -0
  765. package/src/modules/staff/lib/timesheets-reports/reportApprovalPolicies.ts +137 -0
  766. package/src/modules/staff/lib/timesheets-reports/reportConfigData.ts +247 -0
  767. package/src/modules/staff/lib/timesheets-reports/reportConfigState.ts +89 -0
  768. package/src/modules/staff/lib/timesheets-reports/reportExport.ts +373 -0
  769. package/src/modules/staff/lib/timesheets-reports/reportExportFormats.ts +71 -0
  770. package/src/modules/staff/lib/timesheets-reports/reportGroupings.ts +153 -0
  771. package/src/modules/staff/lib/timesheets-reports/reportLabels.ts +23 -0
  772. package/src/modules/staff/lib/timesheets-reports/reportReference.ts +146 -0
  773. package/src/modules/staff/lib/timesheets-reports/reportRows.ts +89 -0
  774. package/src/modules/staff/lib/timesheets-reports/reportSheetData.ts +196 -0
  775. package/src/modules/staff/lib/timesheets-reports/reportTotals.ts +544 -0
  776. package/src/modules/staff/lib/timesheets-reports/xlsx.ts +207 -0
  777. package/src/modules/staff/lib/timesheets-tasks/computeTaskRollups.ts +238 -0
  778. package/src/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.ts +90 -0
  779. package/src/modules/staff/lib/timesheets-tasks/seedProjectStatuses.ts +43 -0
  780. package/src/modules/staff/lib/timesheets-tasks/statusPositions.ts +132 -0
  781. package/src/modules/staff/lib/timesheets-tasks/statusSlug.ts +43 -0
  782. package/src/modules/staff/lib/timesheets-tasks/taskHierarchy.ts +99 -0
  783. package/src/modules/staff/lib/timesheets-tasks/taskHoursTotals.ts +42 -0
  784. package/src/modules/staff/lib/timesheets-tasks/taskReference.ts +108 -0
  785. package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +18 -4
  786. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +504 -149
  787. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +50 -2
  788. package/src/modules/staff/migrations/.snapshot-open-mercato.json +3244 -228
  789. package/src/modules/staff/migrations/Migration20260813073131_staff.ts +94 -0
  790. package/src/modules/staff/migrations/Migration20260824143357_staff.ts +17 -0
  791. package/src/modules/staff/notifications.client.ts +16 -0
  792. package/src/modules/staff/notifications.handlers.ts +88 -0
  793. package/src/modules/staff/notifications.ts +163 -0
  794. package/src/modules/staff/search.ts +191 -1
  795. package/src/modules/staff/setup.ts +22 -0
  796. package/src/modules/staff/subscribers/time-project-access-requested-notification.ts +95 -0
  797. package/src/modules/staff/subscribers/time-project-budget-threshold-notification.ts +226 -0
  798. package/src/modules/staff/subscribers/time-report-approved-notification.ts +116 -0
  799. package/src/modules/staff/subscribers/time-report-portal-broadcast.ts +120 -0
  800. package/src/modules/staff/translations.ts +17 -0
  801. package/src/modules/staff/widgets/components.ts +34 -0
  802. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +6 -5
  803. package/src/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.tsx +138 -0
  804. package/src/modules/staff/widgets/notifications/index.ts +1 -0
  805. package/src/modules/staff/workers/timesheets-reapply-rounding.ts +59 -0
  806. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +0 -123
  807. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +0 -7
  808. package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +0 -138
@@ -1,4 +1,5 @@
1
- import type { CommandHandler } from '@open-mercato/shared/lib/commands'
1
+ import { z } from 'zod'
2
+ import type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
2
3
  import { registerCommand } from '@open-mercato/shared/lib/commands'
3
4
  import type { EntityManager } from '@mikro-orm/postgresql'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -7,8 +8,28 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
8
  import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
8
9
  import { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'
9
10
  import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
10
- import { StaffTimeEntry, StaffTimeEntrySegment, StaffTimeProject, type StaffTimeEntrySource } from '../data/entities'
11
+ import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
12
+ import {
13
+ StaffTimeEntry,
14
+ StaffTimeEntrySegment,
15
+ StaffTimeProject,
16
+ StaffTimeTask,
17
+ type StaffTimeEntrySource,
18
+ } from '../data/entities'
11
19
  import { emitStaffEvent } from '../events'
20
+ import { deriveInterval, formatClock, parseClock } from '../lib/time-tracking/interval'
21
+ import { roundMinutes } from '../lib/time-tracking/rounding'
22
+ import { resolveBillability } from '../lib/time-tracking/billability'
23
+ import type { ScopedResolverContext } from '../lib/time-tracking/registries/scope'
24
+ import {
25
+ DEFAULT_TIME_TRACKING_SETTINGS,
26
+ readTimeTrackingSettings,
27
+ type TimeTrackingSettings,
28
+ } from '../lib/time-tracking/settings'
29
+ import { resolveProjectAccess, type ProjectAccess } from '../lib/time-tracking/access'
30
+ // Imported for the command ids AND for the registration side effect: the tag
31
+ // commands must be in the registry before this file asks the bus to run one.
32
+ import { staffTimeTagCommandIds, TAG_TARGET_LOCKED_CODE } from './timesheets-tags'
12
33
 
13
34
  const timeEntryCrudIndexer: CrudIndexerConfig<StaffTimeEntry> = {
14
35
  entityType: 'staff:staff_time_entry',
@@ -41,12 +62,20 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
41
62
 
42
63
  const logger = createLogger('staff')
43
64
 
65
+ type ContainerLike = { resolve: (token: string) => unknown }
66
+
67
+ type Translate = (key: string, fallback?: string) => string
68
+
44
69
  type RbacServiceLike = {
45
70
  userHasAllFeatures: (
46
71
  userId: string,
47
72
  required: string[],
48
73
  scope: { tenantId: string | null; organizationId: string | null },
49
74
  ) => Promise<boolean>
75
+ getGrantedFeatures?: (
76
+ userId: string,
77
+ scope: { tenantId: string | null; organizationId: string | null },
78
+ ) => Promise<string[]>
50
79
  }
51
80
 
52
81
  /**
@@ -91,22 +120,25 @@ async function resolveCallerStaffMemberId(
91
120
 
92
121
  /**
93
122
  * Verifies the referenced time project exists and is in-scope (same tenant + org,
94
- * not soft-deleted). Throws 422 if the ID is provided but unresolvable.
95
- * No-op when projectId is null/undefined (timeProjectId is optional on entries).
123
+ * not soft-deleted) and hands the row back — the write path reads its
124
+ * `billableByDefault` and `currencyCode` to seed the entry. Throws 422 if the ID
125
+ * is provided but unresolvable. Returns null when projectId is null/undefined
126
+ * (timeProjectId is optional on entries).
96
127
  */
97
- async function assertTimeProjectInScope(
128
+ async function requireTimeProjectInScope(
98
129
  em: EntityManager,
99
130
  projectId: string | null | undefined,
100
131
  tenantId: string,
101
132
  organizationId: string,
102
- ): Promise<void> {
103
- if (!projectId) return
104
- const exists = await em.findOne(
105
- StaffTimeProject,
106
- { id: projectId, tenantId, organizationId, deletedAt: null },
107
- { fields: ['id'] },
108
- )
109
- if (!exists) {
133
+ ): Promise<StaffTimeProject | null> {
134
+ if (!projectId) return null
135
+ const project = await em.findOne(StaffTimeProject, {
136
+ id: projectId,
137
+ tenantId,
138
+ organizationId,
139
+ deletedAt: null,
140
+ })
141
+ if (!project) {
110
142
  const { translate } = await resolveTranslations()
111
143
  throw new CrudHttpError(422, {
112
144
  error: translate('staff.timesheets.errors.projectNotFound', 'Time project not found or not accessible.'),
@@ -115,6 +147,543 @@ async function assertTimeProjectInScope(
115
147
  },
116
148
  })
117
149
  }
150
+ return project
151
+ }
152
+
153
+ /**
154
+ * Same contract as the project check, for `task_id`. A dangling task reference is
155
+ * refused rather than stored: the task rollups on screens 6 and 7 aggregate entries
156
+ * by `task_id`, so a stale or foreign UUID would silently move hours onto another
157
+ * project's card.
158
+ */
159
+ async function requireTaskInScope(
160
+ em: EntityManager,
161
+ taskId: string | null | undefined,
162
+ tenantId: string,
163
+ organizationId: string,
164
+ ): Promise<StaffTimeTask | null> {
165
+ if (!taskId) return null
166
+ const task = await em.findOne(StaffTimeTask, {
167
+ id: taskId,
168
+ tenantId,
169
+ organizationId,
170
+ deletedAt: null,
171
+ })
172
+ if (!task) {
173
+ const { translate } = await resolveTranslations()
174
+ const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')
175
+ throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })
176
+ }
177
+ return task
178
+ }
179
+
180
+ /**
181
+ * A task belongs to exactly one project, so an entry naming both must name the
182
+ * same one. Letting them disagree would file the hours under a project whose
183
+ * board never shows the task — the same rollup corruption the task-scope check
184
+ * above prevents, one level up.
185
+ */
186
+ async function assertTaskBelongsToProject(
187
+ task: StaffTimeTask | null,
188
+ projectId: string | null,
189
+ ): Promise<void> {
190
+ if (timeEntryTaskMatchesProject(task, projectId)) return
191
+ const { translate } = await resolveTranslations()
192
+ const message = translate('staff.timesheets.errors.taskNotFound', 'Task not found or not accessible.')
193
+ throw new CrudHttpError(422, { error: message, fieldErrors: { taskId: message } })
194
+ }
195
+
196
+ /**
197
+ * The rule the assertion above enforces, as a predicate: a task belongs to
198
+ * exactly one project, so an entry naming both must name the same one. Exported
199
+ * because the grid bulk save validates a whole batch at once and answers with a
200
+ * per-row error list rather than a thrown 422 — it reuses the rule and only
201
+ * differs in how it reports a violation.
202
+ */
203
+ export function timeEntryTaskMatchesProject(
204
+ task: { timeProjectId?: string | null } | null | undefined,
205
+ projectId: string | null | undefined,
206
+ ): boolean {
207
+ if (!task || !projectId) return true
208
+ return task.timeProjectId === projectId
209
+ }
210
+
211
+ /**
212
+ * Screen 8 picks a task and lets the project follow from it ("Projekt i klient
213
+ * wynikają z zadania"), so a write that names only a task still lands on the
214
+ * project the task belongs to — and with it the billable and currency defaults.
215
+ * One derivation for every write path, including the grid bulk save.
216
+ */
217
+ export function resolveTimeEntryProjectId(
218
+ requestedProjectId: string | null | undefined,
219
+ task: { timeProjectId?: string | null } | null | undefined,
220
+ ): string | null {
221
+ return requestedProjectId ?? task?.timeProjectId ?? null
222
+ }
223
+
224
+ /**
225
+ * The billable default chain, in one place: an explicit value wins, then the
226
+ * project's own default — a fixed-price engagement is non-billable however the
227
+ * tenant normally works — and the tenant setting only decides what is left.
228
+ *
229
+ * EP-34 moved that chain into the billability registry's built-in resolver; this
230
+ * stays the module's entry point and forwards the scope so a contributed
231
+ * resolver can be consulted ahead of it.
232
+ */
233
+ export function resolveTimeEntryBillable(params: {
234
+ requested?: boolean | null
235
+ project?: { id?: string | null; billableByDefault?: boolean | null } | null
236
+ settings: TimeTrackingSettings
237
+ scope?: ScopedResolverContext | null
238
+ }): boolean {
239
+ return resolveBillability({
240
+ ...(params.scope ?? {}),
241
+ requested: params.requested,
242
+ project: params.project ?? null,
243
+ settings: params.settings,
244
+ })
245
+ }
246
+
247
+ /**
248
+ * Rounding and the billable default are tenant settings. A settings read that
249
+ * fails must not fail the write, so it degrades to the documented defaults —
250
+ * the same fail-safe the projects and tasks routes apply to the grace window.
251
+ */
252
+ async function readEntrySettingsFrom(
253
+ container: ContainerLike,
254
+ tenantId: string,
255
+ ): Promise<TimeTrackingSettings> {
256
+ try {
257
+ const configService = container.resolve('moduleConfigService') as ModuleConfigService | undefined
258
+ if (!configService?.getRecord) return DEFAULT_TIME_TRACKING_SETTINGS
259
+ return await readTimeTrackingSettings(configService, { tenantId })
260
+ } catch {
261
+ return DEFAULT_TIME_TRACKING_SETTINGS
262
+ }
263
+ }
264
+
265
+ async function readEntrySettings(ctx: CommandRuntimeContext, tenantId: string): Promise<TimeTrackingSettings> {
266
+ return readEntrySettingsFrom(ctx.container, tenantId)
267
+ }
268
+
269
+ /**
270
+ * The same tenant-settings read, for write paths that run outside a command —
271
+ * the grid bulk save reads it ONCE per request (it is identical for every row)
272
+ * and then seeds rounding and the billable default from that one snapshot.
273
+ */
274
+ export async function resolveTimeEntrySettings(
275
+ container: ContainerLike,
276
+ tenantId: string,
277
+ ): Promise<TimeTrackingSettings> {
278
+ return readEntrySettingsFrom(container, tenantId)
279
+ }
280
+
281
+ /**
282
+ * The single rounding entry point for write paths that cannot run inside this
283
+ * command file — the grid bulk save and timer-stop both compute a duration in
284
+ * their own transaction, and D-7 makes `rounded_minutes` the ONLY input to cost,
285
+ * so a duration written without its rounded twin bills nothing. They call this
286
+ * rather than reaching for `roundMinutes` themselves, which keeps the tenant
287
+ * settings read, its fail-safe, and the rule itself in one place.
288
+ */
289
+ export async function resolveRoundedMinutes(
290
+ container: ContainerLike,
291
+ tenantId: string,
292
+ durationMinutes: number,
293
+ organizationId?: string | null,
294
+ ): Promise<number> {
295
+ return roundedMinutesFor(durationMinutes, await readEntrySettingsFrom(container, tenantId), {
296
+ tenantId,
297
+ organizationId: organizationId ?? undefined,
298
+ })
299
+ }
300
+
301
+ async function resolveGrantedFeatures(
302
+ ctx: CommandRuntimeContext,
303
+ tenantId: string,
304
+ organizationId: string,
305
+ ): Promise<string[]> {
306
+ const userId = ctx.auth?.sub
307
+ if (!userId) return []
308
+ try {
309
+ const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined
310
+ if (!rbac?.getGrantedFeatures) return []
311
+ return (await rbac.getGrantedFeatures(userId, { tenantId, organizationId })) ?? []
312
+ } catch {
313
+ // Fail closed: an unreadable grant set is no grant set, so the caller falls
314
+ // back to their project memberships instead of being treated as a manager.
315
+ return []
316
+ }
317
+ }
318
+
319
+ async function resolveEntryProjectAccess(
320
+ em: EntityManager,
321
+ ctx: CommandRuntimeContext,
322
+ tenantId: string,
323
+ organizationId: string,
324
+ settings: TimeTrackingSettings,
325
+ ): Promise<ProjectAccess> {
326
+ return resolveProjectAccess({
327
+ em: em.fork(),
328
+ userId: ctx.auth?.sub ?? null,
329
+ tenantId,
330
+ organizationId,
331
+ userFeatures: await resolveGrantedFeatures(ctx, tenantId, organizationId),
332
+ assignmentGraceDays: settings.access.assignmentGraceDays,
333
+ })
334
+ }
335
+
336
+ /**
337
+ * Every entry write intersects the caller's project access. An entry that names a
338
+ * project is writable by a project manager or an assigned member; an entry with no
339
+ * project belongs to nobody else, so only the member who logged it may touch it.
340
+ * `staff.timesheets.manage_all` stays a superset of both, which is what it has
341
+ * always meant on this route.
342
+ */
343
+ async function assertEntryProjectAccess(params: {
344
+ access: ProjectAccess
345
+ hasManageAll: boolean
346
+ projectId: string | null
347
+ entryStaffMemberId: string
348
+ }): Promise<void> {
349
+ if (params.hasManageAll || params.access.canManageAll) return
350
+ const { translate } = await resolveTranslations()
351
+ if (params.projectId) {
352
+ if (params.access.projectIds.includes(params.projectId)) return
353
+ throw new CrudHttpError(403, {
354
+ error: translate('staff.timesheets.errors.notAssigned', 'You are not assigned to this project.'),
355
+ })
356
+ }
357
+ if (params.access.staffMemberId && params.access.staffMemberId === params.entryStaffMemberId) return
358
+ throw new CrudHttpError(403, {
359
+ error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.'),
360
+ })
361
+ }
362
+
363
+ /**
364
+ * `notes` is the column; `description` is the name every consulting-suite screen
365
+ * uses for it. Both keys are accepted on write and `description` wins when a
366
+ * request carries both — the precedence UPGRADE_NOTES.md already published, and
367
+ * the one that matches intent: a client sending the new key means it.
368
+ * Returns `undefined` when neither key was supplied, so a PUT that omits both
369
+ * leaves the stored note alone.
370
+ *
371
+ * Exported so the grid bulk save applies the SAME precedence — it accepts both
372
+ * keys too, and a second interpretation of which one wins is exactly the kind of
373
+ * divergence that makes one screen's edit disappear on another.
374
+ */
375
+ export function resolveTimeEntryNotesInput(parsed: {
376
+ notes?: string | null
377
+ description?: string | null
378
+ }): string | null | undefined {
379
+ if (parsed.description !== undefined) return parsed.description ?? null
380
+ if (parsed.notes !== undefined) return parsed.notes ?? null
381
+ return undefined
382
+ }
383
+
384
+ /**
385
+ * `rate_override_amount` is `numeric(14,4)`, which MikroORM surfaces as a string;
386
+ * the validator accepts a JSON number. One conversion point keeps the two apart,
387
+ * for the grid bulk save as much as for this file.
388
+ */
389
+ export function toStoredTimeEntryRateOverride(amount: number | null | undefined): string | null {
390
+ return amount === null || amount === undefined ? null : String(amount)
391
+ }
392
+
393
+ /**
394
+ * D-7: `duration_minutes` keeps the RAW value the user entered — it is what the
395
+ * timesheet and the project hour totals display — while `rounded_minutes` is
396
+ * derived from the tenant's rounding rule at write time and is the only input to
397
+ * cost. Storing the rounded value rather than deriving it on read is what stops a
398
+ * later settings change from silently restating money that has already been
399
+ * invoiced (risk R1).
400
+ */
401
+ export function roundedMinutesFor(
402
+ durationMinutes: number,
403
+ settings: TimeTrackingSettings,
404
+ scope?: ScopedResolverContext | null,
405
+ ): number {
406
+ return roundMinutes(durationMinutes, settings.rounding, scope)
407
+ }
408
+
409
+ export type TimeEntryInterval = {
410
+ startedAt: Date | null
411
+ endedAt: Date | null
412
+ durationMinutes: number
413
+ }
414
+
415
+ /**
416
+ * `started_at` / `ended_at` are absolute timestamps; `deriveInterval` reasons in
417
+ * wall-clock minutes. Both directions read and write the SAME basis (UTC), so a
418
+ * timestamp that needs no correction survives the round trip untouched.
419
+ */
420
+ function clockOfTimestamp(value: Date): string {
421
+ return formatClock(value.getUTCHours() * 60 + value.getUTCMinutes())
422
+ }
423
+
424
+ function timestampOnDayOf(reference: Date, clock: string, dayOffset: number): Date {
425
+ const minutesOfDay = parseClock(clock) ?? 0
426
+ return new Date(
427
+ Date.UTC(
428
+ reference.getUTCFullYear(),
429
+ reference.getUTCMonth(),
430
+ reference.getUTCDate() + dayOffset,
431
+ Math.floor(minutesOfDay / 60),
432
+ minutesOfDay % 60,
433
+ 0,
434
+ 0,
435
+ ),
436
+ )
437
+ }
438
+
439
+ /**
440
+ * D-8. An end whose clock is earlier than its start's is a midnight crossing, not
441
+ * a validation error, so the end belongs on the NEXT calendar day while `date`
442
+ * keeps naming the day the work started. A caller that already sent the end on
443
+ * the following day (the entry dialog does) is left exactly as it sent it.
444
+ */
445
+ function alignEndToStart(startedAt: Date, endedAt: Date, endClock: string, crossesMidnight: boolean): Date {
446
+ if (endedAt.getTime() >= startedAt.getTime()) return endedAt
447
+ return timestampOnDayOf(startedAt, endClock, crossesMidnight ? 1 : 0)
448
+ }
449
+
450
+ /**
451
+ * US-D3, on the update path: `duration_minutes`, `started_at` and `ended_at` can
452
+ * each be written on their own, and the server must never store a set that
453
+ * contradicts itself — a two-hour duration on a 09:00–10:00 span is a
454
+ * defensible-invoice problem, not a cosmetic one (D-7 bills the rounded twin of
455
+ * whatever duration lands here).
456
+ *
457
+ * The rule, in one line: **the clocks are the observation and the duration is
458
+ * their summary, so the clocks win — unless the duration is the field the request
459
+ * actually pinned, in which case the untouched clock moves to match it.**
460
+ *
461
+ * - both clocks in the request → the duration is recomputed from them, and a
462
+ * duration sent alongside is advisory (this is what the entry dialog sends);
463
+ * - one clock plus a duration → the OTHER clock is derived from that pair;
464
+ * - one clock alone → the duration is recomputed against the stored counterpart;
465
+ * - the duration alone → `started_at` anchors and `ended_at` shifts to match;
466
+ * - none of the three → every one of them is left exactly as stored.
467
+ *
468
+ * Derivation only ever RESTATES a clock that already exists: with one side null
469
+ * there is nothing for a duration to contradict, and manufacturing the missing
470
+ * side would silently stop a running timer.
471
+ *
472
+ * Exported because the grid bulk save writes durations in its own transaction
473
+ * (per-row, pessimistically locked) and cannot go through this command — it calls
474
+ * the same reconciliation rather than repeating the arithmetic, which is what
475
+ * keeps one rule for one invariant.
476
+ */
477
+ export function reconcileTimeEntryInterval(params: {
478
+ stored: TimeEntryInterval
479
+ startedAt: Date | null | undefined
480
+ endedAt: Date | null | undefined
481
+ durationMinutes: number | undefined
482
+ }): TimeEntryInterval {
483
+ const startProvided = params.startedAt !== undefined
484
+ const endProvided = params.endedAt !== undefined
485
+ const durationProvided = params.durationMinutes !== undefined
486
+ if (!startProvided && !endProvided && !durationProvided) return params.stored
487
+
488
+ const startedAt = startProvided ? params.startedAt ?? null : params.stored.startedAt
489
+ const endedAt = endProvided ? params.endedAt ?? null : params.stored.endedAt
490
+ const durationMinutes = durationProvided
491
+ ? (params.durationMinutes as number)
492
+ : params.stored.durationMinutes
493
+ if (!startedAt || !endedAt) return { startedAt, endedAt, durationMinutes }
494
+
495
+ const startClock = clockOfTimestamp(startedAt)
496
+ const endClock = clockOfTimestamp(endedAt)
497
+ const durationAnchored = durationProvided && !(startProvided && endProvided)
498
+
499
+ if (durationAnchored && !endProvided) {
500
+ const derived = deriveInterval({ start: startClock, durationMinutes })
501
+ return {
502
+ startedAt,
503
+ endedAt: timestampOnDayOf(startedAt, derived.end ?? endClock, derived.crossesMidnight ? 1 : 0),
504
+ durationMinutes,
505
+ }
506
+ }
507
+
508
+ if (durationAnchored && !startProvided) {
509
+ const derived = deriveInterval({ end: endClock, durationMinutes })
510
+ return {
511
+ startedAt: timestampOnDayOf(endedAt, derived.start ?? startClock, derived.crossesMidnight ? -1 : 0),
512
+ endedAt,
513
+ durationMinutes,
514
+ }
515
+ }
516
+
517
+ const derived = deriveInterval({ start: startClock, end: endClock, durationMinutes })
518
+ return {
519
+ startedAt,
520
+ endedAt: alignEndToStart(startedAt, endedAt, endClock, derived.crossesMidnight),
521
+ durationMinutes: derived.durationMinutes ?? durationMinutes,
522
+ }
523
+ }
524
+
525
+ const MS_PER_DAY = 86_400_000
526
+
527
+ function toDateOrNull(value: Date | string | null | undefined): Date | null {
528
+ if (value === null || value === undefined) return null
529
+ const date = value instanceof Date ? value : new Date(value)
530
+ return Number.isNaN(date.getTime()) ? null : date
531
+ }
532
+
533
+ function utcDayIndex(value: Date): number {
534
+ return Math.round(
535
+ Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()) / MS_PER_DAY,
536
+ )
537
+ }
538
+
539
+ /**
540
+ * T4.10(b): moving an entry to another day moves the work with it.
541
+ *
542
+ * `date` names the day the work STARTED (D-8), so an entry re-dated to Tuesday
543
+ * whose clocks stayed on Monday is the same self-contradicting triple T4.9
544
+ * eliminated one field over — a 09:00–10:00 span filed against a day it did not
545
+ * happen on, and a defensible-invoice problem the moment anyone reads the times
546
+ * back. "Move this entry to Tuesday" means the whole observation moves.
547
+ *
548
+ * The clocks are SHIFTED by whole days rather than recomposed from the new date,
549
+ * which preserves two things a recomposition would destroy: the stored offset
550
+ * between `date` and the timestamps (for a tenant east of UTC a 01:00 local start
551
+ * legitimately sits on the previous UTC day), and a midnight crossing (an end
552
+ * already on the following day stays on the following day).
553
+ *
554
+ * Two things it never does, both inherited from the reconciliation rule above:
555
+ *
556
+ * - a clock that does not exist is not manufactured, and
557
+ * - a RUNNING timer — a start with no end — is left exactly where it is, because
558
+ * re-anchoring its start would restate the elapsed time of work still in
559
+ * progress, and nothing about re-dating the row says the timer began later.
560
+ */
561
+ export function rebaseTimeEntryIntervalToDate(params: {
562
+ stored: TimeEntryInterval
563
+ fromDate: Date | string | null | undefined
564
+ toDate: Date | string | null | undefined
565
+ }): TimeEntryInterval {
566
+ const fromDate = toDateOrNull(params.fromDate)
567
+ const toDate = toDateOrNull(params.toDate)
568
+ if (!fromDate || !toDate) return params.stored
569
+
570
+ const { startedAt, endedAt } = params.stored
571
+ if (!startedAt && !endedAt) return params.stored
572
+ if (startedAt && !endedAt) return params.stored
573
+
574
+ const shiftMs = (utcDayIndex(toDate) - utcDayIndex(fromDate)) * MS_PER_DAY
575
+ if (shiftMs === 0) return params.stored
576
+ return {
577
+ startedAt: startedAt ? new Date(startedAt.getTime() + shiftMs) : null,
578
+ endedAt: endedAt ? new Date(endedAt.getTime() + shiftMs) : null,
579
+ durationMinutes: params.stored.durationMinutes,
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Reuses the code the tag commands already refuse a frozen entry with, rather
585
+ * than inventing a second one — a client that learned to handle `time_entry_locked`
586
+ * from `…/tags/entry-assignments` handles it identically everywhere else.
587
+ */
588
+ export const TIME_ENTRY_LOCKED_CODE = TAG_TARGET_LOCKED_CODE
589
+
590
+ export type LockedTimeEntryRef = { id: string; lockedReportId?: string | null }
591
+
592
+ /**
593
+ * The 409 body names the report that froze the entries so screen 10 note 4 can
594
+ * link straight to it, and lists the offending ids so a grid save can mark those
595
+ * cells read-only instead of showing an opaque failure. `lockedReportId` is kept
596
+ * beside the arrays for shape-compatibility with the tag commands' single-target
597
+ * refusal.
598
+ */
599
+ export function buildTimeEntryLockedError(
600
+ lockedEntries: LockedTimeEntryRef[],
601
+ translate: Translate,
602
+ ): CrudHttpError {
603
+ const lockedEntryIds = lockedEntries.map((entry) => entry.id)
604
+ const lockedReportIds = Array.from(
605
+ new Set(
606
+ lockedEntries
607
+ .map((entry) => entry.lockedReportId)
608
+ .filter((reportId): reportId is string => typeof reportId === 'string' && reportId.length > 0),
609
+ ),
610
+ )
611
+ return new CrudHttpError(409, {
612
+ code: TIME_ENTRY_LOCKED_CODE,
613
+ error:
614
+ lockedEntryIds.length > 1
615
+ ? translate(
616
+ 'staff.time_tracking.entries.errors.entriesLocked',
617
+ 'Some of these time entries are locked in a closed report and cannot be changed. Unlock the report first.',
618
+ )
619
+ : translate(
620
+ 'staff.time_tracking.entries.errors.entryLocked',
621
+ 'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',
622
+ ),
623
+ lockedReportId: lockedReportIds[0] ?? null,
624
+ lockedEntryIds,
625
+ lockedReportIds,
626
+ })
627
+ }
628
+
629
+ /**
630
+ * The lock gate (risk R3). It sits in the command layer rather than in each write
631
+ * route because the routes are the part that keeps growing: duplicate, copy-day
632
+ * and the grid bulk save all arrived after the lock did, and any one of them
633
+ * could have forgotten a per-route check. Every command that resolves an entry
634
+ * for mutation runs this immediately after the row is loaded, so a field added to
635
+ * the update command later is frozen without anybody remembering to freeze it.
636
+ *
637
+ * No permission lifts it — not `staff.timesheets.manage_all`. An entry frozen in
638
+ * a closed report has already been billed at those minutes (D-7), so the only way
639
+ * back is the Phase-6 unlock command, which clears `locked_report_id` itself and
640
+ * therefore never reaches this gate.
641
+ */
642
+ export function assertTimeEntryUnlocked(
643
+ entry: { id: string; lockedReportId?: string | null },
644
+ translate: Translate,
645
+ ): void {
646
+ if (!entry.lockedReportId) return
647
+ throw buildTimeEntryLockedError([{ id: entry.id, lockedReportId: entry.lockedReportId }], translate)
648
+ }
649
+
650
+ /**
651
+ * Tags are written through the tag commands, never through the junction table, so
652
+ * an entry write inherits their guarantees unchanged: unknown ids are refused,
653
+ * re-sending a tag is idempotent, an entry frozen in a closed report is refused
654
+ * with 409 `time_entry_locked`, and each change lands in the audit log with its
655
+ * own undo.
656
+ *
657
+ * `tagIds` is a SET, not an append. The assign command answers with the target's
658
+ * full tag list, and that answer is what tells us which tags must then be
659
+ * unassigned — so the desired state is reached without this file ever reading or
660
+ * writing `staff_time_entry_tags` itself.
661
+ */
662
+ async function syncEntryTags(
663
+ ctx: CommandRuntimeContext,
664
+ params: { timeEntryId: string; tenantId: string; organizationId: string; tagIds: string[] },
665
+ ): Promise<void> {
666
+ const bus = ctx.container.resolve('commandBus') as CommandBus
667
+ const desired = Array.from(new Set(params.tagIds))
668
+ const scoped = {
669
+ tenantId: params.tenantId,
670
+ organizationId: params.organizationId,
671
+ timeEntryId: params.timeEntryId,
672
+ }
673
+
674
+ const assigned = await bus.execute<unknown, { tagIds?: string[] }>(staffTimeTagCommandIds.assignEntry, {
675
+ input: { ...scoped, tagIds: desired },
676
+ ctx,
677
+ })
678
+
679
+ const current = assigned?.result?.tagIds ?? desired
680
+ const stale = current.filter((tagId) => !desired.includes(tagId))
681
+ if (stale.length === 0) return
682
+
683
+ await bus.execute(staffTimeTagCommandIds.unassignEntry, {
684
+ input: { ...scoped, tagIds: stale },
685
+ ctx,
686
+ })
118
687
  }
119
688
 
120
689
  type TimeEntrySnapshot = {
@@ -124,13 +693,18 @@ type TimeEntrySnapshot = {
124
693
  staffMemberId: string
125
694
  date: string
126
695
  durationMinutes: number
696
+ roundedMinutes: number | null
127
697
  startedAt: string | null
128
698
  endedAt: string | null
129
699
  notes: string | null
130
700
  timeProjectId: string | null
701
+ taskId: string | null
131
702
  customerId: string | null
132
703
  dealId: string | null
133
704
  orderId: string | null
705
+ isBillable: boolean
706
+ rateOverrideAmount: string | null
707
+ rateCurrencyCode: string | null
134
708
  source: string
135
709
  deletedAt: string | null
136
710
  }
@@ -156,13 +730,18 @@ async function loadTimeEntrySnapshot(em: EntityManager, id: string, scope?: Staf
156
730
  staffMemberId: entry.staffMemberId,
157
731
  date: entry.date instanceof Date ? entry.date.toISOString().split('T')[0] : String(entry.date),
158
732
  durationMinutes: entry.durationMinutes,
733
+ roundedMinutes: entry.roundedMinutes ?? null,
159
734
  startedAt: entry.startedAt ? entry.startedAt.toISOString() : null,
160
735
  endedAt: entry.endedAt ? entry.endedAt.toISOString() : null,
161
736
  notes: entry.notes ?? null,
162
737
  timeProjectId: entry.timeProjectId ?? null,
738
+ taskId: entry.taskId ?? null,
163
739
  customerId: entry.customerId ?? null,
164
740
  dealId: entry.dealId ?? null,
165
741
  orderId: entry.orderId ?? null,
742
+ isBillable: entry.isBillable,
743
+ rateOverrideAmount: entry.rateOverrideAmount ?? null,
744
+ rateCurrencyCode: entry.rateCurrencyCode ?? null,
166
745
  source: entry.source,
167
746
  deletedAt: entry.deletedAt ? entry.deletedAt.toISOString() : null,
168
747
  }
@@ -176,13 +755,18 @@ function timeEntrySeedFromSnapshot(snapshot: TimeEntrySnapshot): Record<string,
176
755
  staffMemberId: snapshot.staffMemberId,
177
756
  date: snapshot.date,
178
757
  durationMinutes: snapshot.durationMinutes,
758
+ roundedMinutes: snapshot.roundedMinutes ?? null,
179
759
  startedAt: snapshot.startedAt ? new Date(snapshot.startedAt) : null,
180
760
  endedAt: snapshot.endedAt ? new Date(snapshot.endedAt) : null,
181
761
  notes: snapshot.notes ?? null,
182
762
  timeProjectId: snapshot.timeProjectId ?? null,
763
+ taskId: snapshot.taskId ?? null,
183
764
  customerId: snapshot.customerId ?? null,
184
765
  dealId: snapshot.dealId ?? null,
185
766
  orderId: snapshot.orderId ?? null,
767
+ isBillable: snapshot.isBillable ?? true,
768
+ rateOverrideAmount: snapshot.rateOverrideAmount ?? null,
769
+ rateCurrencyCode: snapshot.rateCurrencyCode ?? null,
186
770
  source: (snapshot.source ?? 'manual') as StaffTimeEntrySource,
187
771
  createdAt: new Date(),
188
772
  updatedAt: new Date(),
@@ -216,9 +800,20 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
216
800
  effectiveStaffMemberId = callerStaffMemberId
217
801
  }
218
802
 
219
- // Validate referenced timeProjectId is in-scope before persisting.
220
- // Without this check a foreign or stale UUID would produce a dangling reference.
221
- await assertTimeProjectInScope(em, parsed.timeProjectId ?? null, parsed.tenantId, parsed.organizationId)
803
+ // Validate referenced task and timeProjectId are in-scope before persisting.
804
+ // Without these checks a foreign or stale UUID would produce a dangling reference.
805
+ const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)
806
+ const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)
807
+ const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)
808
+ await assertTaskBelongsToProject(task, project?.id ?? null)
809
+
810
+ const settings = await readEntrySettings(ctx, parsed.tenantId)
811
+ await assertEntryProjectAccess({
812
+ access: await resolveEntryProjectAccess(em, ctx, parsed.tenantId, parsed.organizationId, settings),
813
+ hasManageAll: await callerHasManageAll(ctx),
814
+ projectId: project?.id ?? null,
815
+ entryStaffMemberId: effectiveStaffMemberId,
816
+ })
222
817
 
223
818
  const now = new Date()
224
819
  const entry = em.create(StaffTimeEntry, {
@@ -227,13 +822,28 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
227
822
  staffMemberId: effectiveStaffMemberId,
228
823
  date: parsed.date,
229
824
  durationMinutes: parsed.durationMinutes,
825
+ roundedMinutes: roundedMinutesFor(parsed.durationMinutes, settings, {
826
+ tenantId: parsed.tenantId,
827
+ organizationId: parsed.organizationId,
828
+ }),
230
829
  startedAt: parsed.startedAt ?? null,
231
830
  endedAt: parsed.endedAt ?? null,
232
- notes: parsed.notes ?? null,
233
- timeProjectId: parsed.timeProjectId ?? null,
831
+ notes: resolveTimeEntryNotesInput(parsed) ?? null,
832
+ timeProjectId: project?.id ?? null,
833
+ taskId: task?.id ?? null,
234
834
  customerId: parsed.customerId ?? null,
235
835
  dealId: parsed.dealId ?? null,
236
836
  orderId: parsed.orderId ?? null,
837
+ isBillable: resolveTimeEntryBillable({
838
+ requested: parsed.isBillable,
839
+ project,
840
+ settings,
841
+ scope: { tenantId: parsed.tenantId, organizationId: parsed.organizationId },
842
+ }),
843
+ rateOverrideAmount: toStoredTimeEntryRateOverride(parsed.rateOverrideAmount),
844
+ // Snapshotted rather than joined at read time so a later project currency
845
+ // change cannot re-denominate money that has already been reported (D-3).
846
+ rateCurrencyCode: project?.currencyCode ?? null,
237
847
  source: parsed.source ?? 'manual',
238
848
  createdAt: now,
239
849
  updatedAt: now,
@@ -242,6 +852,15 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
242
852
  em.persist(entry)
243
853
  await em.flush()
244
854
 
855
+ if (parsed.tagIds !== undefined) {
856
+ await syncEntryTags(ctx, {
857
+ timeEntryId: entry.id,
858
+ tenantId: parsed.tenantId,
859
+ organizationId: parsed.organizationId,
860
+ tagIds: parsed.tagIds,
861
+ })
862
+ }
863
+
245
864
  await emitCrudSideEffects({
246
865
  dataEngine: ctx.container.resolve('dataEngine'),
247
866
  action: 'created',
@@ -333,7 +952,23 @@ const startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEnt
333
952
  effectiveStaffMemberId = callerStaffMemberId
334
953
  }
335
954
 
336
- await assertTimeProjectInScope(em, parsed.timeProjectId ?? null, parsed.tenantId, parsed.organizationId)
955
+ // Starting a timer on a task is ONE write. The task is validated here, in the
956
+ // same command, so the entry is created already carrying it — the alternative
957
+ // (start, then link) can fail after the timer is running and strand it against
958
+ // the project instead of the task.
959
+ const task = await requireTaskInScope(em, parsed.taskId ?? null, parsed.tenantId, parsed.organizationId)
960
+ // A card knows its task, not always its project, and a task belongs to exactly
961
+ // one project — so the project follows from the task, exactly as it does when
962
+ // an entry is created from screen 8.
963
+ const requestedProjectId = resolveTimeEntryProjectId(parsed.timeProjectId, task)
964
+ const project = await requireTimeProjectInScope(em, requestedProjectId, parsed.tenantId, parsed.organizationId)
965
+ await assertTaskBelongsToProject(task, project?.id ?? null)
966
+
967
+ // A timer entry starts at zero minutes, and D-7 makes `rounded_minutes` the
968
+ // only input to cost — leaving it null here would price the entry at nothing
969
+ // until something else happened to rewrite it. Seeded from the same rule as
970
+ // every other create so timer-stop only ever restates it.
971
+ const settings = await readEntrySettings(ctx, parsed.tenantId)
337
972
 
338
973
  const scopeCtx = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
339
974
 
@@ -374,10 +1009,15 @@ const startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEnt
374
1009
  staffMemberId: effectiveStaffMemberId,
375
1010
  date: parsed.date,
376
1011
  durationMinutes: 0,
1012
+ roundedMinutes: roundedMinutesFor(0, settings, {
1013
+ tenantId: parsed.tenantId,
1014
+ organizationId: parsed.organizationId,
1015
+ }),
377
1016
  startedAt,
378
1017
  endedAt: null,
379
1018
  notes: parsed.notes ?? null,
380
- timeProjectId: parsed.timeProjectId ?? null,
1019
+ timeProjectId: project?.id ?? null,
1020
+ taskId: task?.id ?? null,
381
1021
  customerId: null,
382
1022
  dealId: null,
383
1023
  orderId: null,
@@ -495,9 +1135,15 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
495
1135
  ensureTenantScope(ctx, entry.tenantId)
496
1136
  ensureOrganizationScope(ctx, entry.organizationId)
497
1137
 
1138
+ // Before ownership, before access, before a single field is read off the
1139
+ // request: a frozen entry is frozen for everyone, so nothing below can be
1140
+ // reached with a lock in place.
1141
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
1142
+
498
1143
  // Ownership enforcement: callers without `staff.timesheets.manage_all`
499
1144
  // can only update entries they own.
500
- if (!(await callerHasManageAll(ctx))) {
1145
+ const hasManageAll = await callerHasManageAll(ctx)
1146
+ if (!hasManageAll) {
501
1147
  const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)
502
1148
  if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {
503
1149
  const { translate } = await resolveTranslations()
@@ -507,21 +1153,109 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
507
1153
  }
508
1154
  }
509
1155
 
510
- // Validate referenced timeProjectId is in-scope when it's being changed to a non-null value.
511
- if (parsed.timeProjectId !== undefined && parsed.timeProjectId !== null) {
512
- await assertTimeProjectInScope(em, parsed.timeProjectId, entry.tenantId, entry.organizationId)
1156
+ const task =
1157
+ parsed.taskId !== undefined
1158
+ ? await requireTaskInScope(em, parsed.taskId, entry.tenantId, entry.organizationId)
1159
+ : null
1160
+
1161
+ // A task carries its project (screen 8), so re-pointing an entry at a task
1162
+ // moves the entry with it unless the request names a project explicitly.
1163
+ const nextProjectId =
1164
+ parsed.timeProjectId !== undefined
1165
+ ? parsed.timeProjectId ?? null
1166
+ : task?.timeProjectId ?? entry.timeProjectId ?? null
1167
+ const projectChanged = nextProjectId !== (entry.timeProjectId ?? null)
1168
+ const project = projectChanged
1169
+ ? await requireTimeProjectInScope(em, nextProjectId, entry.tenantId, entry.organizationId)
1170
+ : null
1171
+ await assertTaskBelongsToProject(task, nextProjectId)
1172
+
1173
+ const settings = await readEntrySettings(ctx, entry.tenantId)
1174
+ const access = await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings)
1175
+ // Both ends of a move are checked: a caller may not lift an entry out of a
1176
+ // project they cannot see, nor drop one into a project they cannot see.
1177
+ await assertEntryProjectAccess({
1178
+ access,
1179
+ hasManageAll,
1180
+ projectId: entry.timeProjectId ?? null,
1181
+ entryStaffMemberId: entry.staffMemberId,
1182
+ })
1183
+ if (projectChanged) {
1184
+ await assertEntryProjectAccess({
1185
+ access,
1186
+ hasManageAll,
1187
+ projectId: nextProjectId,
1188
+ entryStaffMemberId: entry.staffMemberId,
1189
+ })
513
1190
  }
514
1191
 
1192
+ const notes = resolveTimeEntryNotesInput(parsed)
1193
+
1194
+ // A `date` move re-anchors the STORED clocks first, so what reconciliation
1195
+ // reasons about is the entry as it will exist on its new day. Order matters:
1196
+ // clocks the request sends explicitly still win over the rebased ones (the
1197
+ // entry dialog rebuilds all three from the new date and would otherwise be
1198
+ // shifted twice), while clocks it omits travel with the entry instead of
1199
+ // being stranded on the old day.
1200
+ const stored = rebaseTimeEntryIntervalToDate({
1201
+ stored: {
1202
+ startedAt: entry.startedAt ?? null,
1203
+ endedAt: entry.endedAt ?? null,
1204
+ durationMinutes: entry.durationMinutes,
1205
+ },
1206
+ fromDate: entry.date,
1207
+ toDate: parsed.date,
1208
+ })
1209
+
1210
+ // The three time fields are settled together, before anything is assigned, so
1211
+ // no combination of them can be stored contradicting itself (US-D3, D-8).
1212
+ const interval = reconcileTimeEntryInterval({
1213
+ stored,
1214
+ startedAt: parsed.startedAt,
1215
+ endedAt: parsed.endedAt,
1216
+ durationMinutes: parsed.durationMinutes,
1217
+ })
1218
+
515
1219
  if (parsed.date !== undefined) entry.date = parsed.date
516
- if (parsed.durationMinutes !== undefined) entry.durationMinutes = parsed.durationMinutes
517
- if (parsed.timeProjectId !== undefined) entry.timeProjectId = parsed.timeProjectId ?? null
1220
+ if (interval.startedAt !== (entry.startedAt ?? null)) entry.startedAt = interval.startedAt
1221
+ if (interval.endedAt !== (entry.endedAt ?? null)) entry.endedAt = interval.endedAt
1222
+ if (parsed.durationMinutes !== undefined || interval.durationMinutes !== entry.durationMinutes) {
1223
+ entry.durationMinutes = interval.durationMinutes
1224
+ // Raw minutes moved — because the duration was written, or because the
1225
+ // clocks were edited and restated it — so the frozen rounded value is
1226
+ // recomputed from the tenant's current rule. D-7 makes it the only input to
1227
+ // cost, so a clock edit that changes the length of the work and leaves the
1228
+ // rounded twin behind would mis-bill it.
1229
+ entry.roundedMinutes = roundedMinutesFor(interval.durationMinutes, settings, {
1230
+ tenantId: entry.tenantId,
1231
+ organizationId: entry.organizationId,
1232
+ })
1233
+ }
1234
+ if (projectChanged) {
1235
+ entry.timeProjectId = nextProjectId
1236
+ entry.rateCurrencyCode = project?.currencyCode ?? null
1237
+ }
1238
+ if (parsed.taskId !== undefined) entry.taskId = task?.id ?? null
518
1239
  if (parsed.customerId !== undefined) entry.customerId = parsed.customerId ?? null
519
1240
  if (parsed.dealId !== undefined) entry.dealId = parsed.dealId ?? null
520
1241
  if (parsed.orderId !== undefined) entry.orderId = parsed.orderId ?? null
521
- if (parsed.notes !== undefined) entry.notes = parsed.notes ?? null
1242
+ if (parsed.isBillable !== undefined) entry.isBillable = parsed.isBillable
1243
+ if (parsed.rateOverrideAmount !== undefined) {
1244
+ entry.rateOverrideAmount = toStoredTimeEntryRateOverride(parsed.rateOverrideAmount)
1245
+ }
1246
+ if (notes !== undefined) entry.notes = notes
522
1247
  entry.updatedAt = new Date()
523
1248
  await em.flush()
524
1249
 
1250
+ if (parsed.tagIds !== undefined) {
1251
+ await syncEntryTags(ctx, {
1252
+ timeEntryId: entry.id,
1253
+ tenantId: entry.tenantId,
1254
+ organizationId: entry.organizationId,
1255
+ tagIds: parsed.tagIds,
1256
+ })
1257
+ }
1258
+
525
1259
  await emitCrudSideEffects({
526
1260
  dataEngine: ctx.container.resolve('dataEngine'),
527
1261
  action: 'updated',
@@ -542,10 +1276,19 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
542
1276
  const changes = buildChanges(before as unknown as Record<string, unknown>, after as unknown as Record<string, unknown>, [
543
1277
  'date',
544
1278
  'durationMinutes',
1279
+ 'roundedMinutes',
1280
+ // A time correction is exactly the edit somebody will later have to justify
1281
+ // on an invoice, so the clocks are named in the trail beside the minutes.
1282
+ 'startedAt',
1283
+ 'endedAt',
545
1284
  'timeProjectId',
1285
+ 'taskId',
546
1286
  'customerId',
547
1287
  'dealId',
548
1288
  'orderId',
1289
+ 'isBillable',
1290
+ 'rateOverrideAmount',
1291
+ 'rateCurrencyCode',
549
1292
  'notes',
550
1293
  'deletedAt',
551
1294
  ])
@@ -576,10 +1319,20 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
576
1319
  if (!entry) return
577
1320
  entry.date = before.date as unknown as Date
578
1321
  entry.durationMinutes = before.durationMinutes
1322
+ entry.roundedMinutes = before.roundedMinutes ?? null
1323
+ // Restored beside the minutes they belong to: now that an update can move the
1324
+ // clocks, an undo that left them where the edit put them would hand back an
1325
+ // entry whose duration and span disagree.
1326
+ entry.startedAt = before.startedAt ? new Date(before.startedAt) : null
1327
+ entry.endedAt = before.endedAt ? new Date(before.endedAt) : null
579
1328
  entry.timeProjectId = before.timeProjectId ?? null
1329
+ entry.taskId = before.taskId ?? null
580
1330
  entry.customerId = before.customerId ?? null
581
1331
  entry.dealId = before.dealId ?? null
582
1332
  entry.orderId = before.orderId ?? null
1333
+ entry.isBillable = before.isBillable ?? true
1334
+ entry.rateOverrideAmount = before.rateOverrideAmount ?? null
1335
+ entry.rateCurrencyCode = before.rateCurrencyCode ?? null
583
1336
  entry.notes = before.notes ?? null
584
1337
  entry.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null
585
1338
  entry.updatedAt = new Date()
@@ -622,9 +1375,14 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
622
1375
  ensureTenantScope(ctx, entry.tenantId)
623
1376
  ensureOrganizationScope(ctx, entry.organizationId)
624
1377
 
1378
+ // A closed report renders these minutes; deleting the row would leave the
1379
+ // report totalling hours that no longer exist.
1380
+ assertTimeEntryUnlocked(entry, (await resolveTranslations()).translate)
1381
+
625
1382
  // Ownership enforcement: callers without `staff.timesheets.manage_all`
626
1383
  // can only delete entries they own.
627
- if (!(await callerHasManageAll(ctx))) {
1384
+ const hasManageAll = await callerHasManageAll(ctx)
1385
+ if (!hasManageAll) {
628
1386
  const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx)
629
1387
  if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {
630
1388
  const { translate } = await resolveTranslations()
@@ -634,6 +1392,14 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
634
1392
  }
635
1393
  }
636
1394
 
1395
+ const settings = await readEntrySettings(ctx, entry.tenantId)
1396
+ await assertEntryProjectAccess({
1397
+ access: await resolveEntryProjectAccess(em, ctx, entry.tenantId, entry.organizationId, settings),
1398
+ hasManageAll,
1399
+ projectId: entry.timeProjectId ?? null,
1400
+ entryStaffMemberId: entry.staffMemberId,
1401
+ })
1402
+
637
1403
  entry.deletedAt = new Date()
638
1404
  entry.updatedAt = new Date()
639
1405
  await em.flush()
@@ -681,13 +1447,18 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
681
1447
  staffMemberId: before.staffMemberId,
682
1448
  date: before.date as unknown as Date,
683
1449
  durationMinutes: before.durationMinutes,
1450
+ roundedMinutes: before.roundedMinutes ?? null,
684
1451
  startedAt: before.startedAt ? new Date(before.startedAt) : null,
685
1452
  endedAt: before.endedAt ? new Date(before.endedAt) : null,
686
1453
  notes: before.notes ?? null,
687
1454
  timeProjectId: before.timeProjectId ?? null,
1455
+ taskId: before.taskId ?? null,
688
1456
  customerId: before.customerId ?? null,
689
1457
  dealId: before.dealId ?? null,
690
1458
  orderId: before.orderId ?? null,
1459
+ isBillable: before.isBillable ?? true,
1460
+ rateOverrideAmount: before.rateOverrideAmount ?? null,
1461
+ rateCurrencyCode: before.rateCurrencyCode ?? null,
691
1462
  source: (before.source ?? 'manual') as StaffTimeEntrySource,
692
1463
  deletedAt: null,
693
1464
  createdAt: new Date(),
@@ -698,13 +1469,18 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
698
1469
  entry.staffMemberId = before.staffMemberId
699
1470
  entry.date = before.date as unknown as Date
700
1471
  entry.durationMinutes = before.durationMinutes
1472
+ entry.roundedMinutes = before.roundedMinutes ?? null
701
1473
  entry.startedAt = before.startedAt ? new Date(before.startedAt) : null
702
1474
  entry.endedAt = before.endedAt ? new Date(before.endedAt) : null
703
1475
  entry.notes = before.notes ?? null
704
1476
  entry.timeProjectId = before.timeProjectId ?? null
1477
+ entry.taskId = before.taskId ?? null
705
1478
  entry.customerId = before.customerId ?? null
706
1479
  entry.dealId = before.dealId ?? null
707
1480
  entry.orderId = before.orderId ?? null
1481
+ entry.isBillable = before.isBillable ?? true
1482
+ entry.rateOverrideAmount = before.rateOverrideAmount ?? null
1483
+ entry.rateCurrencyCode = before.rateCurrencyCode ?? null
708
1484
  entry.source = (before.source ?? 'manual') as StaffTimeEntrySource
709
1485
  entry.deletedAt = null
710
1486
  entry.updatedAt = new Date()
@@ -722,7 +1498,209 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
722
1498
  },
723
1499
  }
724
1500
 
1501
+ export const staffTimeEntryDuplicateSchema = z.object({
1502
+ id: z.string().uuid(),
1503
+ // The copy usually lands on another day (US-D6 "log the same thing again"), so
1504
+ // the caller may move it; omitted, it keeps the source's day.
1505
+ date: z.coerce.date().optional(),
1506
+ // US-D6 ends "…I adjust the duration and the date", so both adjustments travel
1507
+ // with the copy request rather than costing a second round trip that would
1508
+ // leave the unadjusted duration visible in between. Omitted, the copy keeps the
1509
+ // source's minutes. The rounded twin is always recomputed by the create command
1510
+ // from the tenant rule, so an overridden duration can never carry the source's
1511
+ // stale `rounded_minutes` into a new entry (D-7).
1512
+ durationMinutes: z.number().int().min(0).max(1440).optional(),
1513
+ // Tags are NOT read off the source here: this file writes tags only through the
1514
+ // tag commands and never touches `staff_time_entry_tags`, so the caller — which
1515
+ // already has the source's `tags[]` from the list response — passes them on.
1516
+ tagIds: z.array(z.string().uuid()).max(50).optional(),
1517
+ })
1518
+
1519
+ export type StaffTimeEntryDuplicateInput = z.infer<typeof staffTimeEntryDuplicateSchema>
1520
+
1521
+ /**
1522
+ * US-D6. The fourth write path TC-TT-018 exercises. It resolves the SOURCE entry
1523
+ * and runs it past the lock gate before copying: screen 10 note 4 hides every row
1524
+ * action on a locked row, so an API that happily duplicated one would be a side
1525
+ * door back into a frozen report's numbers.
1526
+ *
1527
+ * The copy itself is delegated to the create command rather than re-implemented,
1528
+ * so it inherits ownership enforcement, project access, the billable/currency
1529
+ * defaults, rounding (D-7), tag syncing and the audit entry unchanged.
1530
+ */
1531
+ const duplicateTimeEntryCommand: CommandHandler<StaffTimeEntryDuplicateInput, { timeEntryId: string }> = {
1532
+ id: 'staff.timesheets.time_entries.duplicate',
1533
+ async execute(rawInput, ctx) {
1534
+ const parsed = staffTimeEntryDuplicateSchema.parse(rawInput)
1535
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
1536
+ const scope = commandActorScope(ctx)
1537
+ const source = await findOneWithDecryption(
1538
+ em,
1539
+ StaffTimeEntry,
1540
+ applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),
1541
+ undefined,
1542
+ scopeForDecryption(scope),
1543
+ )
1544
+ if (!source) throw new CrudHttpError(404, { error: 'Time entry not found.' })
1545
+ ensureTenantScope(ctx, source.tenantId)
1546
+ ensureOrganizationScope(ctx, source.organizationId)
1547
+
1548
+ assertTimeEntryUnlocked(source, (await resolveTranslations()).translate)
1549
+
1550
+ const bus = ctx.container.resolve('commandBus') as CommandBus
1551
+ const created = await bus.execute<unknown, { timeEntryId: string }>(createTimeEntryCommand.id, {
1552
+ input: {
1553
+ tenantId: source.tenantId,
1554
+ organizationId: source.organizationId,
1555
+ staffMemberId: source.staffMemberId,
1556
+ date: parsed.date ?? source.date,
1557
+ durationMinutes: parsed.durationMinutes ?? source.durationMinutes,
1558
+ timeProjectId: source.timeProjectId ?? null,
1559
+ taskId: source.taskId ?? null,
1560
+ customerId: source.customerId ?? null,
1561
+ dealId: source.dealId ?? null,
1562
+ orderId: source.orderId ?? null,
1563
+ isBillable: source.isBillable,
1564
+ rateOverrideAmount:
1565
+ source.rateOverrideAmount === null || source.rateOverrideAmount === undefined
1566
+ ? null
1567
+ : Number(source.rateOverrideAmount),
1568
+ description: source.notes ?? null,
1569
+ // A copy is typed, not timed: carrying `started_at`/`ended_at` over would
1570
+ // manufacture an overlap with the entry it was copied from (US-D7).
1571
+ source: 'manual' as StaffTimeEntrySource,
1572
+ ...(parsed.tagIds !== undefined ? { tagIds: parsed.tagIds } : {}),
1573
+ },
1574
+ ctx,
1575
+ })
1576
+
1577
+ const timeEntryId = created?.result?.timeEntryId
1578
+ if (!timeEntryId) throw new CrudHttpError(400, { error: 'Failed to duplicate time entry.' })
1579
+ return { timeEntryId }
1580
+ },
1581
+ }
1582
+
1583
+ export const staffTimeEntryReapplyRoundingSchema = z.object({
1584
+ tenantId: z.string().uuid(),
1585
+ organizationId: z.string().uuid(),
1586
+ entryIds: z.array(z.string().uuid()).min(1).max(500),
1587
+ })
1588
+
1589
+ export type StaffTimeEntryReapplyRoundingInput = z.infer<typeof staffTimeEntryReapplyRoundingSchema>
1590
+
1591
+ export type StaffTimeEntryReapplyRoundingResult = {
1592
+ /** Entries whose `rounded_minutes` actually moved. */
1593
+ updatedCount: number
1594
+ /** Entries already at the current rule — read, compared, left alone. */
1595
+ unchangedCount: number
1596
+ /** Requested ids that were locked, deleted or out of scope, and therefore never loaded. */
1597
+ skippedCount: number
1598
+ }
1599
+
1600
+ /**
1601
+ * Retroactive rounding (T7.3, screen 16 note 3) — restates `rounded_minutes` on a
1602
+ * batch of existing entries under the tenant's current rule.
1603
+ *
1604
+ * Three properties, in the order they matter:
1605
+ *
1606
+ * 1. **A locked entry is unreachable, not merely skipped.** `lockedReportId: null`
1607
+ * is part of the WHERE clause, so an entry frozen into a closed report is never
1608
+ * loaded, never compared and never assigned. The guarantee is structural: there
1609
+ * is no branch in this handler that could write one.
1610
+ * 2. **It is an explicit, batched administrative action**, driven by the
1611
+ * `reapply-rounding` ProgressJob worker, never a side effect of saving the
1612
+ * settings form. Rewriting billing history silently is exactly the failure the
1613
+ * spec's risk R1 is about.
1614
+ * 3. **It writes one derived column and nothing else.** No clock is re-anchored,
1615
+ * no duration is touched — `duration_minutes` remains the raw observation.
1616
+ *
1617
+ * Reachable only by a privileged actor: the queue worker (`systemActor`) or a
1618
+ * super-admin. An interactive caller cannot rewrite a tenant's history through the
1619
+ * command bus by guessing the id; the route that enqueues the job is the gate, and
1620
+ * it is gated on `staff.timesheets.settings.manage`.
1621
+ */
1622
+ const reapplyRoundingCommand: CommandHandler<
1623
+ StaffTimeEntryReapplyRoundingInput,
1624
+ StaffTimeEntryReapplyRoundingResult
1625
+ > = {
1626
+ id: 'staff.timesheets.time_entries.reapply_rounding',
1627
+ async execute(input, ctx) {
1628
+ const parsed = staffTimeEntryReapplyRoundingSchema.parse(input ?? {})
1629
+ if (ctx.systemActor !== true && ctx.auth?.isSuperAdmin !== true) {
1630
+ const { translate } = await resolveTranslations()
1631
+ throw new CrudHttpError(403, { error: translate('staff.errors.forbidden', 'Forbidden') })
1632
+ }
1633
+
1634
+ const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
1635
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
1636
+ const settings = await readEntrySettings(ctx, parsed.tenantId)
1637
+
1638
+ const entries = await em.find(
1639
+ StaffTimeEntry,
1640
+ applyScopeToWhere<StaffTimeEntry>(
1641
+ {
1642
+ id: { $in: parsed.entryIds },
1643
+ deletedAt: null,
1644
+ // Non-negotiable: a frozen entry has already been billed at the value it
1645
+ // carries, so it is excluded at the query, not at an `if`.
1646
+ lockedReportId: null,
1647
+ },
1648
+ scope,
1649
+ ),
1650
+ )
1651
+
1652
+ const changed: StaffTimeEntry[] = []
1653
+ let unchangedCount = 0
1654
+ for (const entry of entries) {
1655
+ const next = roundedMinutesFor(entry.durationMinutes, settings, {
1656
+ tenantId: entry.tenantId,
1657
+ organizationId: entry.organizationId,
1658
+ })
1659
+ if (entry.roundedMinutes === next) {
1660
+ unchangedCount += 1
1661
+ continue
1662
+ }
1663
+ entry.roundedMinutes = next
1664
+ entry.updatedAt = new Date()
1665
+ changed.push(entry)
1666
+ }
1667
+
1668
+ if (changed.length > 0) await em.flush()
1669
+
1670
+ const dataEngine = ctx.container.resolve('dataEngine') as Parameters<
1671
+ typeof emitCrudSideEffects
1672
+ >[0]['dataEngine']
1673
+ for (const entry of changed) {
1674
+ await emitCrudSideEffects({
1675
+ dataEngine,
1676
+ action: 'updated',
1677
+ entity: entry,
1678
+ identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
1679
+ events: staffTimeEntryCrudEvents,
1680
+ indexer: timeEntryCrudIndexer,
1681
+ })
1682
+ }
1683
+
1684
+ return {
1685
+ updatedCount: changed.length,
1686
+ unchangedCount,
1687
+ skippedCount: Math.max(0, parsed.entryIds.length - entries.length),
1688
+ }
1689
+ },
1690
+ }
1691
+
725
1692
  registerCommand(createTimeEntryCommand)
726
1693
  registerCommand(startTimerCommand)
727
1694
  registerCommand(updateTimeEntryCommand)
728
1695
  registerCommand(deleteTimeEntryCommand)
1696
+ registerCommand(duplicateTimeEntryCommand)
1697
+ registerCommand(reapplyRoundingCommand)
1698
+
1699
+ export const staffTimeEntryCommandIds = {
1700
+ create: createTimeEntryCommand.id,
1701
+ startTimer: startTimerCommand.id,
1702
+ update: updateTimeEntryCommand.id,
1703
+ delete: deleteTimeEntryCommand.id,
1704
+ duplicate: duplicateTimeEntryCommand.id,
1705
+ reapplyRounding: reapplyRoundingCommand.id,
1706
+ } as const