@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
@@ -0,0 +1,1329 @@
1
+ /**
2
+ * Customer report commands — screens 13, 14 and 15.
3
+ *
4
+ * A report is the one artefact in this module a client can dispute, so the rules
5
+ * it enforces are financial rather than cosmetic:
6
+ *
7
+ * 1. **One customer, one currency (risk R2).** Every selected project must carry
8
+ * the report's `customerId` and they must all agree on a currency code. A
9
+ * mismatch is refused with the offending project names rather than silently
10
+ * averaging two currencies into one number.
11
+ * 2. **The reference is allocated once and never reused** — see
12
+ * `lib/timesheets-reports/reportReference.ts`. It is printed on a PDF the
13
+ * client keeps.
14
+ * 3. **A closed report is immutable.** Editing or deleting one is refused with
15
+ * `409 report_closed`; the way back is the unlock command, which is gated on
16
+ * its own feature (`staff.timesheets.reports.unlock`) precisely so "who may
17
+ * restate billed time" is answerable without inventing an admin role.
18
+ *
19
+ * Money is never computed here. `lib/timesheets-reports/reportTotals.ts` owns
20
+ * D-7 (round at the entry, then sum upward) and the close command below feeds
21
+ * its per-entry results straight into the freeze records.
22
+ */
23
+
24
+ import type { EntityManager } from '@mikro-orm/postgresql'
25
+ import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
26
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
27
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
28
+ import { createLogger } from '@open-mercato/shared/lib/logger'
29
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
30
+ import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
31
+ import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
32
+ import type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
33
+ import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
34
+ import {
35
+ StaffTimeEntry,
36
+ StaffTimeProject,
37
+ StaffTimeReport,
38
+ StaffTimeReportEntry,
39
+ StaffTimeReportEvent,
40
+ StaffTimeReportProject,
41
+ } from '../data/entities'
42
+ import {
43
+ staffTimeReportCloseSchema,
44
+ staffTimeReportCreateSchema,
45
+ staffTimeReportUnlockSchema,
46
+ staffTimeReportUpdateSchema,
47
+ type StaffTimeReportCloseInput,
48
+ type StaffTimeReportCreateInput,
49
+ type StaffTimeReportUnlockInput,
50
+ type StaffTimeReportUpdateInput,
51
+ } from '../data/validators'
52
+ import { readTimeTrackingSettings } from '../lib/time-tracking/settings'
53
+ import {
54
+ allocateReportReference,
55
+ highestReportSequenceNumber,
56
+ isReportReferenceConflict,
57
+ reportReferencePrefix,
58
+ reportReferenceYear,
59
+ withReportReferenceRetry,
60
+ } from '../lib/timesheets-reports/reportReference'
61
+ import { loadReportData } from '../lib/timesheets-reports/loadReportData'
62
+ import { reportSheetLabels } from '../lib/timesheets-reports/reportLabels'
63
+ import {
64
+ computeReportTotals,
65
+ resolveEntryValues,
66
+ resolveReportCurrency,
67
+ selectIncludedEntries,
68
+ } from '../lib/timesheets-reports/reportTotals'
69
+ import { emitStaffEvent } from '../events'
70
+ import {
71
+ applyScopeToWhere,
72
+ commandActorScope,
73
+ commandInputScope,
74
+ ensureOrganizationScope,
75
+ ensureTenantScope,
76
+ extractUndoPayload,
77
+ scopedStaffSnapshotWhere,
78
+ staffSnapshotScopeFromContext,
79
+ staffSnapshotScopeFromSnapshot,
80
+ type StaffCommandScope,
81
+ type StaffSnapshotScope,
82
+ } from './shared'
83
+
84
+ export const staffTimeReportCommandIds = {
85
+ create: 'staff.timesheets.reports.create',
86
+ update: 'staff.timesheets.reports.update',
87
+ delete: 'staff.timesheets.reports.delete',
88
+ close: 'staff.timesheets.reports.close',
89
+ unlock: 'staff.timesheets.reports.unlock',
90
+ } as const
91
+
92
+ export const STAFF_TIME_REPORT_RESOURCE_KIND = 'staff.timesheets.time_report'
93
+
94
+ export const REPORT_CURRENCY_CONFLICT_CODE = 'report_currency_conflict'
95
+ export const REPORT_PROJECT_CUSTOMER_MISMATCH_CODE = 'report_project_customer_mismatch'
96
+ export const REPORT_PROJECT_NOT_FOUND_CODE = 'report_project_not_found'
97
+ export const REPORT_CLOSED_CODE = 'report_closed'
98
+ export const REPORT_NOT_CLOSED_CODE = 'report_not_closed'
99
+ export const REPORT_REFERENCE_CONFLICT_CODE = 'report_reference_conflict'
100
+ export const REPORT_EMPTY_CODE = 'report_empty'
101
+
102
+ const logger = createLogger('staff').child({ component: 'commands/timesheets-reports' })
103
+
104
+ const reportCrudIndexer: CrudIndexerConfig<StaffTimeReport> = {
105
+ entityType: 'staff:staff_time_report',
106
+ }
107
+
108
+ const reportCrudEvents: CrudEventsConfig<StaffTimeReport> = {
109
+ module: 'staff',
110
+ entity: 'timesheets.time_report',
111
+ persistent: true,
112
+ buildPayload: (ctx) => ({
113
+ id: ctx.identifiers.id,
114
+ organizationId: ctx.identifiers.organizationId,
115
+ tenantId: ctx.identifiers.tenantId,
116
+ }),
117
+ }
118
+
119
+ type Translate = (key: string, fallback: string) => string
120
+
121
+ export type ReportSnapshot = {
122
+ id: string
123
+ tenantId: string
124
+ organizationId: string
125
+ customerId: string
126
+ customerSnapshot: Record<string, unknown> | null
127
+ reference: string
128
+ title: string
129
+ periodKind: string
130
+ periodFrom: string | null
131
+ periodTo: string | null
132
+ currencyCode: string
133
+ grouping: string
134
+ nonbillableMode: string
135
+ includeAlreadyReported: boolean
136
+ showRates: boolean
137
+ roundingUnitMinutes: number
138
+ roundingDirection: string
139
+ status: string
140
+ totalBillableMinutes: number | null
141
+ totalNonbillableMinutes: number | null
142
+ totalAmount: string | null
143
+ closedAt: string | null
144
+ closedByUserId: string | null
145
+ timeProjectIds: string[]
146
+ deletedAt: string | null
147
+ }
148
+
149
+ type ReportUndoPayload = {
150
+ before?: ReportSnapshot | null
151
+ after?: ReportSnapshot | null
152
+ }
153
+
154
+ const DIFFED_FIELDS = [
155
+ 'title',
156
+ 'periodKind',
157
+ 'periodFrom',
158
+ 'periodTo',
159
+ 'currencyCode',
160
+ 'grouping',
161
+ 'nonbillableMode',
162
+ 'includeAlreadyReported',
163
+ 'showRates',
164
+ 'status',
165
+ 'totalBillableMinutes',
166
+ 'totalNonbillableMinutes',
167
+ 'totalAmount',
168
+ ] as const
169
+
170
+ function toIsoDate(value: Date | string | null | undefined): string | null {
171
+ if (!value) return null
172
+ if (value instanceof Date) {
173
+ if (Number.isNaN(value.getTime())) return null
174
+ const year = value.getFullYear()
175
+ const month = String(value.getMonth() + 1).padStart(2, '0')
176
+ const day = String(value.getDate()).padStart(2, '0')
177
+ return `${year}-${month}-${day}`
178
+ }
179
+ const text = String(value)
180
+ return text.length >= 10 ? text.slice(0, 10) : text
181
+ }
182
+
183
+ function snapshotScopeWhere(scope?: StaffSnapshotScope | null): { tenantId?: string; organizationId?: string } {
184
+ const where: { tenantId?: string; organizationId?: string } = {}
185
+ if (scope?.tenantId) where.tenantId = scope.tenantId
186
+ if (scope?.organizationId) where.organizationId = scope.organizationId
187
+ return where
188
+ }
189
+
190
+ export async function loadReportProjectIds(
191
+ em: EntityManager,
192
+ reportId: string,
193
+ scope?: StaffSnapshotScope | null,
194
+ ): Promise<string[]> {
195
+ const rows = await em.find(StaffTimeReportProject, { reportId, ...snapshotScopeWhere(scope) })
196
+ return rows.map((row) => row.timeProjectId)
197
+ }
198
+
199
+ function toSnapshot(report: StaffTimeReport, timeProjectIds: string[]): ReportSnapshot {
200
+ return {
201
+ id: report.id,
202
+ tenantId: report.tenantId,
203
+ organizationId: report.organizationId,
204
+ customerId: report.customerId,
205
+ customerSnapshot: report.customerSnapshot ?? null,
206
+ reference: report.reference,
207
+ title: report.title,
208
+ periodKind: report.periodKind,
209
+ periodFrom: toIsoDate(report.periodFrom),
210
+ periodTo: toIsoDate(report.periodTo),
211
+ currencyCode: report.currencyCode,
212
+ grouping: report.grouping,
213
+ nonbillableMode: report.nonbillableMode,
214
+ includeAlreadyReported: report.includeAlreadyReported ?? false,
215
+ showRates: report.showRates ?? true,
216
+ roundingUnitMinutes: report.roundingUnitMinutes ?? 0,
217
+ roundingDirection: report.roundingDirection ?? 'up',
218
+ status: report.status,
219
+ totalBillableMinutes: report.totalBillableMinutes ?? null,
220
+ totalNonbillableMinutes: report.totalNonbillableMinutes ?? null,
221
+ totalAmount: report.totalAmount ?? null,
222
+ closedAt: report.closedAt ? report.closedAt.toISOString() : null,
223
+ closedByUserId: report.closedByUserId ?? null,
224
+ timeProjectIds,
225
+ deletedAt: report.deletedAt ? report.deletedAt.toISOString() : null,
226
+ }
227
+ }
228
+
229
+ export async function loadReportSnapshot(
230
+ em: EntityManager,
231
+ id: string,
232
+ scope?: StaffSnapshotScope | null,
233
+ ): Promise<ReportSnapshot | null> {
234
+ const report = await em.findOne(StaffTimeReport, scopedStaffSnapshotWhere(id, scope))
235
+ if (!report) return null
236
+ const projectIds = await loadReportProjectIds(em, report.id, scope)
237
+ return toSnapshot(report, projectIds)
238
+ }
239
+
240
+ export function reportNotFoundError(translate: Translate): CrudHttpError {
241
+ return new CrudHttpError(404, {
242
+ error: translate('staff.time_tracking.reports.errors.notFound', 'Report not found or not accessible.'),
243
+ })
244
+ }
245
+
246
+ export function reportClosedError(translate: Translate, reference: string | null): CrudHttpError {
247
+ return new CrudHttpError(409, {
248
+ code: REPORT_CLOSED_CODE,
249
+ error: translate(
250
+ 'staff.time_tracking.reports.errors.closed',
251
+ 'This report is closed. Unlock it before changing anything.',
252
+ ),
253
+ reference: reference ?? null,
254
+ })
255
+ }
256
+
257
+ function projectsRequiredError(translate: Translate): CrudHttpError {
258
+ const message = translate(
259
+ 'staff.time_tracking.reports.errors.projectsRequired',
260
+ 'Pick at least one project for this report.',
261
+ )
262
+ return new CrudHttpError(422, { error: message, fieldErrors: { timeProjectIds: message } })
263
+ }
264
+
265
+ function projectNotFoundError(translate: Translate, missingIds: string[]): CrudHttpError {
266
+ const message = translate(
267
+ 'staff.time_tracking.reports.errors.projectNotFound',
268
+ 'Some of the selected projects are not available.',
269
+ )
270
+ return new CrudHttpError(422, {
271
+ code: REPORT_PROJECT_NOT_FOUND_CODE,
272
+ error: message,
273
+ fieldErrors: { timeProjectIds: message },
274
+ missingProjectIds: missingIds,
275
+ })
276
+ }
277
+
278
+ function customerMismatchError(translate: Translate, offenders: string[]): CrudHttpError {
279
+ const message = translate(
280
+ 'staff.time_tracking.reports.errors.customerMismatch',
281
+ 'A report always covers one customer. These projects belong to someone else: {projects}.',
282
+ ).replace('{projects}', offenders.join(', '))
283
+ return new CrudHttpError(422, {
284
+ code: REPORT_PROJECT_CUSTOMER_MISMATCH_CODE,
285
+ error: message,
286
+ fieldErrors: { timeProjectIds: message },
287
+ offenders,
288
+ })
289
+ }
290
+
291
+ /**
292
+ * Risk R2 refused at the cheapest point. The body names both the currencies and
293
+ * the projects carrying them, because "pick one currency" is unactionable
294
+ * without knowing which project is the odd one out.
295
+ */
296
+ export function currencyConflictError(
297
+ translate: Translate,
298
+ currencies: string[],
299
+ offenders: Array<{ id: string; name: string; currencyCode: string | null }>,
300
+ ): CrudHttpError {
301
+ const message = translate(
302
+ 'staff.time_tracking.reports.errors.currencyConflict',
303
+ 'A report always covers one currency. Selected projects use {currencies}.',
304
+ ).replace('{currencies}', currencies.join(', '))
305
+ return new CrudHttpError(422, {
306
+ code: REPORT_CURRENCY_CONFLICT_CODE,
307
+ error: message,
308
+ fieldErrors: { timeProjectIds: message },
309
+ currencies,
310
+ offenders: offenders.map((project) => ({
311
+ id: project.id,
312
+ name: project.name,
313
+ currencyCode: project.currencyCode ?? null,
314
+ })),
315
+ })
316
+ }
317
+
318
+ function referenceConflictError(translate: Translate): CrudHttpError {
319
+ return new CrudHttpError(409, {
320
+ code: REPORT_REFERENCE_CONFLICT_CODE,
321
+ error: translate(
322
+ 'staff.time_tracking.reports.errors.referenceConflict',
323
+ 'Could not allocate a report number right now. Try again.',
324
+ ),
325
+ })
326
+ }
327
+
328
+ export type ResolvedReportProjects = {
329
+ projects: StaffTimeProject[]
330
+ currencyCode: string
331
+ }
332
+
333
+ /**
334
+ * The single validation gate every write shares: the projects exist in scope,
335
+ * they all belong to the report's customer, and they agree on a currency.
336
+ */
337
+ export async function resolveReportProjects(
338
+ em: EntityManager,
339
+ timeProjectIds: readonly string[],
340
+ customerId: string,
341
+ scope: StaffCommandScope,
342
+ translate: Translate,
343
+ ): Promise<ResolvedReportProjects> {
344
+ const ids = Array.from(new Set(timeProjectIds.filter((id) => typeof id === 'string' && id.length > 0)))
345
+ if (ids.length === 0) throw projectsRequiredError(translate)
346
+
347
+ const projects = await em.find(
348
+ StaffTimeProject,
349
+ applyScopeToWhere<StaffTimeProject>({ id: { $in: ids }, deletedAt: null }, scope),
350
+ )
351
+ if (projects.length !== ids.length) {
352
+ const found = new Set(projects.map((project) => project.id))
353
+ throw projectNotFoundError(
354
+ translate,
355
+ ids.filter((id) => !found.has(id)),
356
+ )
357
+ }
358
+
359
+ const mismatched = projects.filter((project) => (project.customerId ?? null) !== customerId)
360
+ if (mismatched.length > 0) {
361
+ throw customerMismatchError(
362
+ translate,
363
+ mismatched.map((project) => project.name),
364
+ )
365
+ }
366
+
367
+ const resolution = resolveReportCurrency(
368
+ projects.map((project) => ({
369
+ id: project.id,
370
+ name: project.name,
371
+ currencyCode: project.currencyCode ?? null,
372
+ })),
373
+ )
374
+ if (!resolution.ok) {
375
+ throw currencyConflictError(translate, resolution.currencies, resolution.offenders)
376
+ }
377
+
378
+ return { projects, currencyCode: resolution.currencyCode ?? '' }
379
+ }
380
+
381
+ /**
382
+ * D-9: the report carries the customer as an FK id plus a denormalized snapshot,
383
+ * so the sheet header resolves a client name without joining the customers
384
+ * module — and keeps resolving it if that customer is later removed. The
385
+ * snapshot is taken from the selected projects, which already carry one, rather
386
+ * than reaching across the module boundary a second time.
387
+ */
388
+ export function deriveCustomerSnapshot(projects: readonly StaffTimeProject[]): Record<string, unknown> | null {
389
+ for (const project of projects) {
390
+ const snapshot = project.customerSnapshot
391
+ if (snapshot && typeof snapshot === 'object' && Object.keys(snapshot).length > 0) {
392
+ return snapshot as Record<string, unknown>
393
+ }
394
+ }
395
+ return null
396
+ }
397
+
398
+ async function readRoundingSnapshot(
399
+ ctx: CommandRuntimeContext,
400
+ tenantId: string,
401
+ ): Promise<{ unitMinutes: number; direction: string }> {
402
+ try {
403
+ const configService = ctx.container.resolve('moduleConfigService') as ModuleConfigService
404
+ const settings = await readTimeTrackingSettings(configService, { tenantId })
405
+ return { unitMinutes: settings.rounding.unitMinutes, direction: settings.rounding.direction }
406
+ } catch (err) {
407
+ // A draft that cannot read the rule still has to exist; close re-stamps it.
408
+ logger.warn('staff.timesheets.reports rounding snapshot unavailable', { err })
409
+ return { unitMinutes: 0, direction: 'up' }
410
+ }
411
+ }
412
+
413
+ async function replaceReportProjects(
414
+ em: EntityManager,
415
+ report: StaffTimeReport,
416
+ projects: readonly StaffTimeProject[],
417
+ ): Promise<void> {
418
+ const existing = await em.find(StaffTimeReportProject, { reportId: report.id })
419
+ for (const row of existing) em.remove(row)
420
+ for (const project of projects) {
421
+ em.persist(
422
+ em.create(StaffTimeReportProject, {
423
+ tenantId: report.tenantId,
424
+ organizationId: report.organizationId,
425
+ reportId: report.id,
426
+ timeProjectId: project.id,
427
+ createdAt: new Date(),
428
+ }),
429
+ )
430
+ }
431
+ }
432
+
433
+ function actorUserId(ctx: CommandRuntimeContext): string | null {
434
+ return typeof ctx.auth?.sub === 'string' ? ctx.auth.sub : null
435
+ }
436
+
437
+ /**
438
+ * The highest report number the organization has EVER handed out this year,
439
+ * soft-deleted rows included. The unique index covers live rows only, so a
440
+ * deleted `RAP-2026-0042` would otherwise be reissued behind a PDF a client
441
+ * already holds.
442
+ */
443
+ async function readHighestReportSequence(
444
+ em: EntityManager,
445
+ year: number,
446
+ scope: StaffCommandScope,
447
+ ): Promise<number> {
448
+ const rows = await em.find(
449
+ StaffTimeReport,
450
+ applyScopeToWhere<StaffTimeReport>({ reference: { $like: `${reportReferencePrefix(year)}%` } }, scope),
451
+ )
452
+ return highestReportSequenceNumber(
453
+ rows.map((row) => row.reference),
454
+ year,
455
+ )
456
+ }
457
+
458
+ const createReportCommand: CommandHandler<StaffTimeReportCreateInput, { reportId: string }> = {
459
+ id: staffTimeReportCommandIds.create,
460
+ async execute(rawInput, ctx) {
461
+ const parsed = staffTimeReportCreateSchema.parse(rawInput)
462
+ ensureTenantScope(ctx, parsed.tenantId)
463
+ ensureOrganizationScope(ctx, parsed.organizationId)
464
+ const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
465
+ const { translate } = await resolveTranslations()
466
+ const baseEm = ctx.container.resolve('em') as EntityManager
467
+ const createdByUserId = actorUserId(ctx)
468
+ const rounding = await readRoundingSnapshot(ctx, parsed.tenantId)
469
+ const issuedAt = new Date()
470
+ const year = reportReferenceYear(issuedAt)
471
+
472
+ let record: StaffTimeReport
473
+ try {
474
+ record = await withReportReferenceRetry(async () => {
475
+ // A fresh fork per attempt: a lost race aborts the transaction, so the
476
+ // retry re-reads the highest reference rather than reusing a stale max.
477
+ const em = baseEm.fork()
478
+ let created: StaffTimeReport | null = null
479
+ let plan: { projects: StaffTimeProject[]; currencyCode: string; highest: number } | null = null
480
+
481
+ await withAtomicFlush(
482
+ em,
483
+ [
484
+ async () => {
485
+ const resolved = await resolveReportProjects(
486
+ em,
487
+ parsed.timeProjectIds,
488
+ parsed.customerId,
489
+ scope,
490
+ translate,
491
+ )
492
+ plan = {
493
+ projects: resolved.projects,
494
+ currencyCode: resolved.currencyCode,
495
+ highest: await readHighestReportSequence(em, year, scope),
496
+ }
497
+ },
498
+ () => {
499
+ if (!plan) throw reportNotFoundError(translate)
500
+ const allocation = allocateReportReference(year, plan.highest)
501
+ const now = new Date()
502
+ created = em.create(StaffTimeReport, {
503
+ tenantId: parsed.tenantId,
504
+ organizationId: parsed.organizationId,
505
+ customerId: parsed.customerId,
506
+ customerSnapshot: deriveCustomerSnapshot(plan.projects),
507
+ reference: allocation.reference,
508
+ title: parsed.title,
509
+ periodKind: parsed.periodKind,
510
+ periodFrom: parsed.periodFrom,
511
+ periodTo: parsed.periodTo,
512
+ currencyCode: plan.currencyCode,
513
+ grouping: parsed.grouping,
514
+ nonbillableMode: parsed.nonbillableMode,
515
+ includeAlreadyReported: parsed.includeAlreadyReported,
516
+ showRates: parsed.showRates,
517
+ roundingUnitMinutes: rounding.unitMinutes,
518
+ roundingDirection: rounding.direction,
519
+ status: 'draft',
520
+ createdByUserId,
521
+ createdAt: now,
522
+ updatedAt: now,
523
+ deletedAt: null,
524
+ })
525
+ em.persist(created)
526
+ },
527
+ async () => {
528
+ if (!created || !plan) return
529
+ await replaceReportProjects(em, created, plan.projects)
530
+ },
531
+ ],
532
+ { transaction: true, label: staffTimeReportCommandIds.create },
533
+ )
534
+
535
+ if (!created) throw reportNotFoundError(translate)
536
+ return created as StaffTimeReport
537
+ })
538
+ } catch (err) {
539
+ if (isReportReferenceConflict(err)) throw referenceConflictError(translate)
540
+ throw err
541
+ }
542
+
543
+ await emitCrudSideEffects({
544
+ dataEngine: ctx.container.resolve('dataEngine'),
545
+ action: 'created',
546
+ entity: record,
547
+ identifiers: { id: record.id, organizationId: record.organizationId, tenantId: record.tenantId },
548
+ events: reportCrudEvents,
549
+ indexer: reportCrudIndexer,
550
+ })
551
+
552
+ return { reportId: record.id }
553
+ },
554
+ captureAfter: async (_input, result, ctx) => {
555
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
556
+ const snapshot = await loadReportSnapshot(em, result.reportId, staffSnapshotScopeFromContext(ctx))
557
+ if (!snapshot) return null
558
+ return { snapshot }
559
+ },
560
+ buildLog: async ({ result, ctx }) => {
561
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
562
+ const snapshot = await loadReportSnapshot(em, result.reportId, staffSnapshotScopeFromContext(ctx))
563
+ if (!snapshot) return null
564
+ const { translate } = await resolveTranslations()
565
+ return {
566
+ actionLabel: translate('staff.audit.timesheets.reports.create', 'Create report'),
567
+ resourceKind: STAFF_TIME_REPORT_RESOURCE_KIND,
568
+ resourceId: snapshot.id,
569
+ tenantId: snapshot.tenantId,
570
+ organizationId: snapshot.organizationId,
571
+ snapshotAfter: snapshot,
572
+ payload: { undo: { after: snapshot } satisfies ReportUndoPayload },
573
+ }
574
+ },
575
+ undo: async ({ logEntry, ctx }) => {
576
+ const payload = extractUndoPayload<ReportUndoPayload>(logEntry)
577
+ const after = payload?.after
578
+ if (!after) return
579
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
580
+ const report = await em.findOne(
581
+ StaffTimeReport,
582
+ scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)),
583
+ )
584
+ if (!report) return
585
+ // A report that has since been closed froze entries; undoing its creation
586
+ // would strand those locks, so the undo declines rather than half-unwinding.
587
+ if (report.status === 'closed') return
588
+ report.deletedAt = new Date()
589
+ report.updatedAt = new Date()
590
+ await em.flush()
591
+
592
+ await emitCrudUndoSideEffects({
593
+ dataEngine: ctx.container.resolve('dataEngine'),
594
+ action: 'deleted',
595
+ entity: report,
596
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
597
+ events: reportCrudEvents,
598
+ indexer: reportCrudIndexer,
599
+ })
600
+ },
601
+ }
602
+
603
+ const updateReportCommand: CommandHandler<StaffTimeReportUpdateInput, { reportId: string }> = {
604
+ id: staffTimeReportCommandIds.update,
605
+ async prepare(rawInput, ctx) {
606
+ const parsed = staffTimeReportUpdateSchema.safeParse(rawInput)
607
+ if (!parsed.success) return {}
608
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
609
+ const before = await loadReportSnapshot(em, parsed.data.id, staffSnapshotScopeFromContext(ctx))
610
+ if (!before) return {}
611
+ return { before: { snapshot: before } }
612
+ },
613
+ async execute(rawInput, ctx) {
614
+ const parsed = staffTimeReportUpdateSchema.parse(rawInput)
615
+ const { translate } = await resolveTranslations()
616
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
617
+ const scope = commandActorScope(ctx)
618
+
619
+ const report = await em.findOne(
620
+ StaffTimeReport,
621
+ applyScopeToWhere<StaffTimeReport>({ id: parsed.id, deletedAt: null }, scope),
622
+ )
623
+ if (!report) throw reportNotFoundError(translate)
624
+ ensureTenantScope(ctx, report.tenantId)
625
+ ensureOrganizationScope(ctx, report.organizationId)
626
+ if (report.status === 'closed') throw reportClosedError(translate, report.reference)
627
+
628
+ const targetCustomerId = parsed.customerId ?? report.customerId
629
+ let resolved: ResolvedReportProjects | null = null
630
+
631
+ await withAtomicFlush(
632
+ em,
633
+ [
634
+ async () => {
635
+ const needsProjectCheck =
636
+ parsed.timeProjectIds !== undefined || (parsed.customerId !== undefined && parsed.customerId !== report.customerId)
637
+ if (!needsProjectCheck) return
638
+ const ids = parsed.timeProjectIds ?? (await loadReportProjectIds(em, report.id, scope))
639
+ resolved = await resolveReportProjects(em, ids, targetCustomerId, scope, translate)
640
+ },
641
+ () => {
642
+ const now = new Date()
643
+ if (parsed.title !== undefined) report.title = parsed.title
644
+ if (parsed.periodKind !== undefined) report.periodKind = parsed.periodKind
645
+ if (parsed.periodFrom !== undefined) report.periodFrom = parsed.periodFrom
646
+ if (parsed.periodTo !== undefined) report.periodTo = parsed.periodTo
647
+ if (parsed.grouping !== undefined) report.grouping = parsed.grouping
648
+ if (parsed.nonbillableMode !== undefined) report.nonbillableMode = parsed.nonbillableMode
649
+ if (parsed.includeAlreadyReported !== undefined) {
650
+ report.includeAlreadyReported = parsed.includeAlreadyReported
651
+ }
652
+ if (parsed.showRates !== undefined) report.showRates = parsed.showRates
653
+ if (parsed.customerId !== undefined) report.customerId = parsed.customerId
654
+ if (resolved) {
655
+ report.currencyCode = resolved.currencyCode
656
+ report.customerSnapshot = deriveCustomerSnapshot(resolved.projects) ?? report.customerSnapshot ?? null
657
+ }
658
+ report.updatedAt = now
659
+ },
660
+ async () => {
661
+ if (!resolved || parsed.timeProjectIds === undefined) return
662
+ await replaceReportProjects(em, report, resolved.projects)
663
+ },
664
+ ],
665
+ { transaction: true, label: staffTimeReportCommandIds.update },
666
+ )
667
+
668
+ await emitCrudSideEffects({
669
+ dataEngine: ctx.container.resolve('dataEngine'),
670
+ action: 'updated',
671
+ entity: report,
672
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
673
+ events: reportCrudEvents,
674
+ indexer: reportCrudIndexer,
675
+ })
676
+
677
+ return { reportId: report.id }
678
+ },
679
+ buildLog: async ({ snapshots, ctx }) => {
680
+ const before = (snapshots.before as { snapshot?: ReportSnapshot } | undefined)?.snapshot
681
+ if (!before) return null
682
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
683
+ const after = await loadReportSnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))
684
+ if (!after) return null
685
+ const { translate } = await resolveTranslations()
686
+ return {
687
+ actionLabel: translate('staff.audit.timesheets.reports.update', 'Update report'),
688
+ resourceKind: STAFF_TIME_REPORT_RESOURCE_KIND,
689
+ resourceId: before.id,
690
+ tenantId: before.tenantId,
691
+ organizationId: before.organizationId,
692
+ snapshotBefore: before,
693
+ snapshotAfter: after,
694
+ changes: buildChanges(
695
+ before as unknown as Record<string, unknown>,
696
+ after as unknown as Record<string, unknown>,
697
+ [...DIFFED_FIELDS],
698
+ ),
699
+ payload: { undo: { before, after } satisfies ReportUndoPayload },
700
+ }
701
+ },
702
+ undo: async ({ logEntry, ctx }) => {
703
+ const payload = extractUndoPayload<ReportUndoPayload>(logEntry)
704
+ const before = payload?.before
705
+ if (!before) return
706
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
707
+ const report = await em.findOne(
708
+ StaffTimeReport,
709
+ scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)),
710
+ )
711
+ if (!report) return
712
+ if (report.status === 'closed') return
713
+
714
+ const projects = await em.find(StaffTimeProject, {
715
+ id: { $in: before.timeProjectIds },
716
+ tenantId: before.tenantId,
717
+ organizationId: before.organizationId,
718
+ })
719
+
720
+ await withAtomicFlush(
721
+ em,
722
+ [
723
+ () => {
724
+ report.customerId = before.customerId
725
+ report.customerSnapshot = before.customerSnapshot
726
+ report.title = before.title
727
+ report.periodKind = before.periodKind as StaffTimeReport['periodKind']
728
+ if (before.periodFrom) report.periodFrom = new Date(before.periodFrom)
729
+ if (before.periodTo) report.periodTo = new Date(before.periodTo)
730
+ report.currencyCode = before.currencyCode
731
+ report.grouping = before.grouping as StaffTimeReport['grouping']
732
+ report.nonbillableMode = before.nonbillableMode as StaffTimeReport['nonbillableMode']
733
+ report.includeAlreadyReported = before.includeAlreadyReported
734
+ report.showRates = before.showRates
735
+ report.updatedAt = new Date()
736
+ },
737
+ async () => {
738
+ if (projects.length !== before.timeProjectIds.length) return
739
+ await replaceReportProjects(em, report, projects)
740
+ },
741
+ ],
742
+ { transaction: true, label: `${staffTimeReportCommandIds.update}.undo` },
743
+ )
744
+
745
+ await emitCrudUndoSideEffects({
746
+ dataEngine: ctx.container.resolve('dataEngine'),
747
+ action: 'updated',
748
+ entity: report,
749
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
750
+ events: reportCrudEvents,
751
+ indexer: reportCrudIndexer,
752
+ })
753
+ },
754
+ }
755
+
756
+ const deleteReportCommand: CommandHandler<{ id?: string }, { reportId: string }> = {
757
+ id: staffTimeReportCommandIds.delete,
758
+ async prepare(input, ctx) {
759
+ const id = input?.id
760
+ if (!id) return {}
761
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
762
+ const before = await loadReportSnapshot(em, id, staffSnapshotScopeFromContext(ctx))
763
+ if (!before) return {}
764
+ return { before: { snapshot: before } }
765
+ },
766
+ async execute(input, ctx) {
767
+ const { translate } = await resolveTranslations()
768
+ const id = input?.id
769
+ if (!id) {
770
+ throw new CrudHttpError(400, {
771
+ error: translate('staff.time_tracking.reports.errors.idRequired', 'Report id is required.'),
772
+ })
773
+ }
774
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
775
+ const scope = commandActorScope(ctx)
776
+ const report = await em.findOne(
777
+ StaffTimeReport,
778
+ applyScopeToWhere<StaffTimeReport>({ id, deletedAt: null }, scope),
779
+ )
780
+ if (!report) throw reportNotFoundError(translate)
781
+ ensureTenantScope(ctx, report.tenantId)
782
+ ensureOrganizationScope(ctx, report.organizationId)
783
+ // Deleting a closed report would strand every entry it froze: their
784
+ // `locked_report_id` would point at a row nothing can unlock.
785
+ if (report.status === 'closed') throw reportClosedError(translate, report.reference)
786
+
787
+ const now = new Date()
788
+ report.deletedAt = now
789
+ report.updatedAt = now
790
+ await em.flush()
791
+
792
+ await emitCrudSideEffects({
793
+ dataEngine: ctx.container.resolve('dataEngine'),
794
+ action: 'deleted',
795
+ entity: report,
796
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
797
+ events: reportCrudEvents,
798
+ indexer: reportCrudIndexer,
799
+ })
800
+
801
+ return { reportId: report.id }
802
+ },
803
+ buildLog: async ({ snapshots }) => {
804
+ const before = (snapshots.before as { snapshot?: ReportSnapshot } | undefined)?.snapshot
805
+ if (!before) return null
806
+ const { translate } = await resolveTranslations()
807
+ return {
808
+ actionLabel: translate('staff.audit.timesheets.reports.delete', 'Delete report'),
809
+ resourceKind: STAFF_TIME_REPORT_RESOURCE_KIND,
810
+ resourceId: before.id,
811
+ tenantId: before.tenantId,
812
+ organizationId: before.organizationId,
813
+ snapshotBefore: before,
814
+ payload: { undo: { before } satisfies ReportUndoPayload },
815
+ }
816
+ },
817
+ undo: async ({ logEntry, ctx }) => {
818
+ const payload = extractUndoPayload<ReportUndoPayload>(logEntry)
819
+ const before = payload?.before
820
+ if (!before) return
821
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
822
+ const report = await em.findOne(
823
+ StaffTimeReport,
824
+ scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)),
825
+ )
826
+ if (!report) return
827
+ report.deletedAt = null
828
+ report.updatedAt = new Date()
829
+ await em.flush()
830
+
831
+ await emitCrudUndoSideEffects({
832
+ dataEngine: ctx.container.resolve('dataEngine'),
833
+ action: 'created',
834
+ entity: report,
835
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
836
+ events: reportCrudEvents,
837
+ indexer: reportCrudIndexer,
838
+ })
839
+ },
840
+ }
841
+
842
+ registerCommand(createReportCommand)
843
+ registerCommand(updateReportCommand)
844
+ registerCommand(deleteReportCommand)
845
+
846
+ /**
847
+ * US-G3 — close & lock. This is the only place a lock is created: exporting a
848
+ * PDF freezes nothing (screen 14 note 5), because "in a generated/sent report"
849
+ * (§9) is ambiguous and a download that silently locks a team's timesheet is the
850
+ * worse reading of it.
851
+ *
852
+ * Everything the close writes happens inside ONE `withAtomicFlush({ transaction:
853
+ * true })`: the per-entry freeze records, the `locked_report_id` stamps, the
854
+ * frozen report totals and the `closed` audit event. A partial close would leave
855
+ * entries frozen against a report that never closed — unlockable by nobody,
856
+ * because the unlock command refuses a draft.
857
+ *
858
+ * Two rules that are easy to get wrong:
859
+ *
860
+ * - **The freeze records come from the same `selectIncludedEntries` /
861
+ * `resolveEntryValues` pair the sheet printed**, so what is frozen is by
862
+ * construction what the client was shown. The amounts are already rounded at
863
+ * the entry (D-7), which is what lets `total_amount` be an exact sum.
864
+ * - **An entry already locked by another report keeps that lock.** When the
865
+ * D-5 opt-in pulls a previously billed hour into a second report, this report
866
+ * records what it billed, but ownership of the lock stays with the report
867
+ * that froze it first — otherwise unlocking the earlier report would leave
868
+ * the hour editable while a later closed report still quotes it.
869
+ */
870
+ export type StaffTimeReportCloseResult = {
871
+ reportId: string
872
+ lockedEntryCount: number
873
+ totalAmount: number
874
+ totalBillableMinutes: number
875
+ totalNonbillableMinutes: number
876
+ }
877
+
878
+ function reportNotClosedError(translate: Translate): CrudHttpError {
879
+ return new CrudHttpError(409, {
880
+ code: REPORT_NOT_CLOSED_CODE,
881
+ error: translate(
882
+ 'staff.time_tracking.reports.errors.notClosed',
883
+ 'This report is not closed, so there is nothing to unlock.',
884
+ ),
885
+ })
886
+ }
887
+
888
+ function reportEmptyError(translate: Translate): CrudHttpError {
889
+ return new CrudHttpError(422, {
890
+ code: REPORT_EMPTY_CODE,
891
+ error: translate(
892
+ 'staff.time_tracking.reports.errors.empty',
893
+ 'This report covers no time entries, so there is nothing to freeze. Adjust the period or the project selection first.',
894
+ ),
895
+ })
896
+ }
897
+
898
+ const closeReportCommand: CommandHandler<StaffTimeReportCloseInput, StaffTimeReportCloseResult> = {
899
+ id: staffTimeReportCommandIds.close,
900
+ async prepare(rawInput, ctx) {
901
+ const parsed = staffTimeReportCloseSchema.safeParse(rawInput)
902
+ if (!parsed.success) return {}
903
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
904
+ const before = await loadReportSnapshot(em, parsed.data.id, staffSnapshotScopeFromContext(ctx))
905
+ if (!before) return {}
906
+ return { before: { snapshot: before } }
907
+ },
908
+ async execute(rawInput, ctx) {
909
+ const parsed = staffTimeReportCloseSchema.parse(rawInput)
910
+ const { translate } = await resolveTranslations()
911
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
912
+ const scope = commandActorScope(ctx)
913
+
914
+ const report = await em.findOne(
915
+ StaffTimeReport,
916
+ applyScopeToWhere<StaffTimeReport>({ id: parsed.id, deletedAt: null }, scope),
917
+ )
918
+ if (!report) throw reportNotFoundError(translate)
919
+ ensureTenantScope(ctx, report.tenantId)
920
+ ensureOrganizationScope(ctx, report.organizationId)
921
+ if (report.status === 'closed') throw reportClosedError(translate, report.reference)
922
+
923
+ const dataScope = { tenantId: report.tenantId, organizationId: report.organizationId }
924
+ const rounding = await readRoundingSnapshot(ctx, report.tenantId)
925
+ const actorId = actorUserId(ctx)
926
+ const labels = reportSheetLabels(translate)
927
+
928
+ type ClosePlanItem = {
929
+ entry: ReturnType<typeof selectIncludedEntries>[number]
930
+ row: StaffTimeEntry | null
931
+ values: ReturnType<typeof resolveEntryValues>
932
+ currencyCode: string
933
+ }
934
+
935
+ let lockedEntryCount = 0
936
+ let totals: ReturnType<typeof computeReportTotals> | null = null
937
+ let closePlan: ClosePlanItem[] = []
938
+
939
+ await withAtomicFlush(
940
+ em,
941
+ [
942
+ async () => {
943
+ const timeProjectIds = await loadReportProjectIds(em, report.id, dataScope)
944
+ const data = await loadReportData({
945
+ em,
946
+ scope: dataScope,
947
+ timeProjectIds,
948
+ periodFrom: report.periodFrom,
949
+ periodTo: report.periodTo,
950
+ })
951
+ const options = {
952
+ grouping: report.grouping,
953
+ nonbillableMode: report.nonbillableMode,
954
+ includeAlreadyReported: report.includeAlreadyReported ?? false,
955
+ } as const
956
+ const included = selectIncludedEntries(data.entries, options, report.id)
957
+ if (included.length === 0) throw reportEmptyError(translate)
958
+
959
+ totals = computeReportTotals({
960
+ entries: data.entries,
961
+ projects: data.projects,
962
+ directory: data.directory,
963
+ options,
964
+ currentReportId: report.id,
965
+ labels,
966
+ })
967
+
968
+ const projectById = new Map(data.projects.map((project) => [project.id, project]))
969
+ const entryRows = await em.find(StaffTimeEntry, {
970
+ id: { $in: included.map((entry) => entry.id) },
971
+ tenantId: report.tenantId,
972
+ organizationId: report.organizationId,
973
+ })
974
+ const entryById = new Map(entryRows.map((row) => [row.id, row]))
975
+
976
+ closePlan = included.map((entry) => ({
977
+ entry,
978
+ row: entryById.get(entry.id) ?? null,
979
+ values: resolveEntryValues(entry, projectById.get(entry.timeProjectId) ?? null),
980
+ currencyCode:
981
+ projectById.get(entry.timeProjectId)?.currencyCode ?? report.currencyCode ?? '',
982
+ }))
983
+ },
984
+ () => {
985
+ if (!totals) throw reportEmptyError(translate)
986
+ const now = new Date()
987
+ for (const item of closePlan) {
988
+ em.persist(
989
+ em.create(StaffTimeReportEntry, {
990
+ tenantId: report.tenantId,
991
+ organizationId: report.organizationId,
992
+ reportId: report.id,
993
+ timeEntryId: item.entry.id,
994
+ frozenRawMinutes: item.values.rawMinutes,
995
+ frozenRoundedMinutes: item.values.minutes,
996
+ frozenRateAmount: item.values.rate === null ? null : String(item.values.rate),
997
+ frozenCurrencyCode: item.currencyCode,
998
+ frozenAmount: item.values.amount === null ? null : item.values.amount.toFixed(2),
999
+ frozenIsBillable: item.values.isBillable,
1000
+ createdAt: now,
1001
+ }),
1002
+ )
1003
+ const row = item.row
1004
+ if (!row) continue
1005
+ // Ownership of an existing lock is never transferred — see the note
1006
+ // on this command.
1007
+ if (row.lockedReportId) continue
1008
+ row.lockedReportId = report.id
1009
+ row.lockedAt = now
1010
+ row.updatedAt = now
1011
+ lockedEntryCount += 1
1012
+ }
1013
+
1014
+ report.status = 'closed'
1015
+ report.closedAt = now
1016
+ report.closedByUserId = actorId
1017
+ report.roundingUnitMinutes = rounding.unitMinutes
1018
+ report.roundingDirection = rounding.direction
1019
+ report.totalBillableMinutes = totals.billableMinutes
1020
+ report.totalNonbillableMinutes = totals.nonbillableMinutes
1021
+ report.totalAmount = totals.totalAmount.toFixed(2)
1022
+ report.updatedAt = now
1023
+
1024
+ em.persist(
1025
+ em.create(StaffTimeReportEvent, {
1026
+ tenantId: report.tenantId,
1027
+ organizationId: report.organizationId,
1028
+ reportId: report.id,
1029
+ eventType: 'closed',
1030
+ reason: null,
1031
+ actorUserId: actorId,
1032
+ metadata: {
1033
+ frozenEntryCount: closePlan.length,
1034
+ lockedEntryCount,
1035
+ totalAmount: totals.totalAmount,
1036
+ currencyCode: report.currencyCode,
1037
+ roundingUnitMinutes: rounding.unitMinutes,
1038
+ roundingDirection: rounding.direction,
1039
+ },
1040
+ createdAt: now,
1041
+ }),
1042
+ )
1043
+ },
1044
+ ],
1045
+ { transaction: true, label: staffTimeReportCommandIds.close },
1046
+ )
1047
+
1048
+ const resolved = totals as ReturnType<typeof computeReportTotals> | null
1049
+ const result: StaffTimeReportCloseResult = {
1050
+ reportId: report.id,
1051
+ lockedEntryCount,
1052
+ totalAmount: resolved?.totalAmount ?? 0,
1053
+ totalBillableMinutes: resolved?.billableMinutes ?? 0,
1054
+ totalNonbillableMinutes: resolved?.nonbillableMinutes ?? 0,
1055
+ }
1056
+
1057
+ await emitCrudSideEffects({
1058
+ dataEngine: ctx.container.resolve('dataEngine'),
1059
+ action: 'updated',
1060
+ entity: report,
1061
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
1062
+ events: reportCrudEvents,
1063
+ indexer: reportCrudIndexer,
1064
+ })
1065
+
1066
+ // A failed broadcast must never fail a committed close.
1067
+ //
1068
+ // The payload is spelled out rather than spreading `result` because this event is
1069
+ // `clientBroadcast: true`: the DOM Event Bridge delivers it to every user of the
1070
+ // organization with no feature check, and `result.totalAmount` is money, which is
1071
+ // gated on `staff.timesheets.rates.view`. The minute totals and the locked-entry
1072
+ // count are not money and are what a live report screen needs; the amount is read
1073
+ // back from the report itself by a caller entitled to it.
1074
+ //
1075
+ // What else that unfiltered audience decides, field by field:
1076
+ //
1077
+ // - `customerId` is DROPPED. It is the one field that links a report to a named
1078
+ // client, and one organization serves many; a tenant's whole staff learning
1079
+ // which client was billed this week is a disclosure with no consumer asking
1080
+ // for it. The schema always declared it optional, both in-repo subscribers
1081
+ // re-read the report from the database, and the portal mirror
1082
+ // (`time_report.portal_published`) resolves the customer itself.
1083
+ // - `reference` and the two minute totals STAY. They are stable identifiers and
1084
+ // non-money aggregates, they are the published webhook contract an external
1085
+ // billing system codes against, and they are what a live reports screen needs
1086
+ // to refresh a row. Narrowing them further is not expressible here: the DOM
1087
+ // bridge can only pin an audience by user or ROLE id, and this module
1088
+ // deliberately gates on immutable feature ids rather than mutable roles.
1089
+ void emitStaffEvent('staff.timesheets.time_report.closed', {
1090
+ id: report.id,
1091
+ tenantId: report.tenantId,
1092
+ organizationId: report.organizationId,
1093
+ reference: report.reference,
1094
+ reportId: result.reportId,
1095
+ lockedEntryCount: result.lockedEntryCount,
1096
+ totalBillableMinutes: result.totalBillableMinutes,
1097
+ totalNonbillableMinutes: result.totalNonbillableMinutes,
1098
+ }).catch((err) => {
1099
+ logger.error('staff.timesheets emit time_report.closed failed', { err })
1100
+ })
1101
+
1102
+ // The close froze `lockedEntryCount` entries. The entry-scoped subscribers key
1103
+ // off a single record id, so this batch-level notice carries none and reaches
1104
+ // only the ones that care about a period being frozen.
1105
+ if (lockedEntryCount > 0) {
1106
+ void emitStaffEvent('staff.timesheets.time_entry.locked', {
1107
+ tenantId: report.tenantId,
1108
+ organizationId: report.organizationId,
1109
+ reportId: report.id,
1110
+ lockedEntryCount,
1111
+ }).catch((err) => {
1112
+ logger.error('staff.timesheets emit time_entry.locked failed', { err })
1113
+ })
1114
+ }
1115
+
1116
+ return result
1117
+ },
1118
+ buildLog: async ({ snapshots, ctx }) => {
1119
+ const before = (snapshots.before as { snapshot?: ReportSnapshot } | undefined)?.snapshot
1120
+ if (!before) return null
1121
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
1122
+ const after = await loadReportSnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))
1123
+ if (!after) return null
1124
+ const { translate } = await resolveTranslations()
1125
+ return {
1126
+ actionLabel: translate('staff.audit.timesheets.reports.close', 'Close and lock report'),
1127
+ resourceKind: STAFF_TIME_REPORT_RESOURCE_KIND,
1128
+ resourceId: before.id,
1129
+ tenantId: before.tenantId,
1130
+ organizationId: before.organizationId,
1131
+ snapshotBefore: before,
1132
+ snapshotAfter: after,
1133
+ changes: buildChanges(
1134
+ before as unknown as Record<string, unknown>,
1135
+ after as unknown as Record<string, unknown>,
1136
+ [...DIFFED_FIELDS],
1137
+ ),
1138
+ // Deliberately no `undo` payload: unwinding a close is the unlock command,
1139
+ // which requires a reason and its own feature. A silent undo of a billing
1140
+ // freeze is exactly what US-G3 forbids.
1141
+ }
1142
+ },
1143
+ }
1144
+
1145
+ /**
1146
+ * US-G3's other half — unlock, screen 15.
1147
+ *
1148
+ * Three things make it an "explicit, audited action" rather than a confirm
1149
+ * dialog: it takes a mandatory reason, it appends an `unlocked` event carrying
1150
+ * that reason and the actor, and it is gated on `staff.timesheets.reports.unlock`
1151
+ * — a feature distinct from `staff.timesheets.lock`, so "who may restate billed
1152
+ * time" can be answered by withholding it from a Team Leader without inventing
1153
+ * the admin role §10 rejects.
1154
+ *
1155
+ * The freeze records are DELETED rather than kept. Two reasons: `staff_time_
1156
+ * report_entries` is what makes D-5 answerable ("is this hour already billed?"),
1157
+ * so leaving stale rows behind after the report reopened would keep hours out of
1158
+ * the next invoice for no reason; and the unique `(report_id, time_entry_id)`
1159
+ * index would otherwise refuse the re-close. The numbers they held are copied
1160
+ * into the `unlocked` event's metadata, so the audit trail keeps what was
1161
+ * frozen.
1162
+ */
1163
+ export type StaffTimeReportUnlockResult = {
1164
+ reportId: string
1165
+ unlockedEntryCount: number
1166
+ }
1167
+
1168
+ const unlockReportCommand: CommandHandler<StaffTimeReportUnlockInput, StaffTimeReportUnlockResult> = {
1169
+ id: staffTimeReportCommandIds.unlock,
1170
+ async prepare(rawInput, ctx) {
1171
+ const parsed = staffTimeReportUnlockSchema.safeParse(rawInput)
1172
+ if (!parsed.success) return {}
1173
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
1174
+ const before = await loadReportSnapshot(em, parsed.data.id, staffSnapshotScopeFromContext(ctx))
1175
+ if (!before) return {}
1176
+ return { before: { snapshot: before } }
1177
+ },
1178
+ async execute(rawInput, ctx) {
1179
+ const parsed = staffTimeReportUnlockSchema.parse(rawInput)
1180
+ const { translate } = await resolveTranslations()
1181
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
1182
+ const scope = commandActorScope(ctx)
1183
+
1184
+ const report = await em.findOne(
1185
+ StaffTimeReport,
1186
+ applyScopeToWhere<StaffTimeReport>({ id: parsed.id, deletedAt: null }, scope),
1187
+ )
1188
+ if (!report) throw reportNotFoundError(translate)
1189
+ ensureTenantScope(ctx, report.tenantId)
1190
+ ensureOrganizationScope(ctx, report.organizationId)
1191
+ if (report.status !== 'closed') throw reportNotClosedError(translate)
1192
+
1193
+ const actorId = actorUserId(ctx)
1194
+ let unlockedEntryCount = 0
1195
+ let frozenEntries: StaffTimeReportEntry[] = []
1196
+ let lockedRows: StaffTimeEntry[] = []
1197
+ const frozenTotals = {
1198
+ totalAmount: report.totalAmount,
1199
+ totalBillableMinutes: report.totalBillableMinutes ?? null,
1200
+ totalNonbillableMinutes: report.totalNonbillableMinutes ?? null,
1201
+ }
1202
+
1203
+ await withAtomicFlush(
1204
+ em,
1205
+ [
1206
+ async () => {
1207
+ frozenEntries = await em.find(StaffTimeReportEntry, {
1208
+ reportId: report.id,
1209
+ tenantId: report.tenantId,
1210
+ organizationId: report.organizationId,
1211
+ })
1212
+ lockedRows = await em.find(StaffTimeEntry, {
1213
+ lockedReportId: report.id,
1214
+ tenantId: report.tenantId,
1215
+ organizationId: report.organizationId,
1216
+ })
1217
+ },
1218
+ () => {
1219
+ const now = new Date()
1220
+ for (const row of lockedRows) {
1221
+ row.lockedReportId = null
1222
+ row.lockedAt = null
1223
+ row.updatedAt = now
1224
+ unlockedEntryCount += 1
1225
+ }
1226
+ for (const frozen of frozenEntries) em.remove(frozen)
1227
+
1228
+ report.status = 'draft'
1229
+ report.closedAt = null
1230
+ report.closedByUserId = null
1231
+ report.totalAmount = null
1232
+ report.totalBillableMinutes = null
1233
+ report.totalNonbillableMinutes = null
1234
+ report.updatedAt = now
1235
+
1236
+ em.persist(
1237
+ em.create(StaffTimeReportEvent, {
1238
+ tenantId: report.tenantId,
1239
+ organizationId: report.organizationId,
1240
+ reportId: report.id,
1241
+ eventType: 'unlocked',
1242
+ reason: parsed.reason,
1243
+ actorUserId: actorId,
1244
+ metadata: {
1245
+ unlockedEntryCount,
1246
+ frozenEntryCount: frozenEntries.length,
1247
+ frozenTotalAmount: frozenTotals.totalAmount,
1248
+ frozenBillableMinutes: frozenTotals.totalBillableMinutes,
1249
+ frozenNonbillableMinutes: frozenTotals.totalNonbillableMinutes,
1250
+ currencyCode: report.currencyCode,
1251
+ },
1252
+ createdAt: now,
1253
+ }),
1254
+ )
1255
+ },
1256
+ ],
1257
+ { transaction: true, label: staffTimeReportCommandIds.unlock },
1258
+ )
1259
+
1260
+ await emitCrudSideEffects({
1261
+ dataEngine: ctx.container.resolve('dataEngine'),
1262
+ action: 'updated',
1263
+ entity: report,
1264
+ identifiers: { id: report.id, organizationId: report.organizationId, tenantId: report.tenantId },
1265
+ events: reportCrudEvents,
1266
+ indexer: reportCrudIndexer,
1267
+ })
1268
+
1269
+ // `reason` is deliberately NOT on the wire.
1270
+ //
1271
+ // This event is `clientBroadcast: true`, and the DOM Event Bridge filters a
1272
+ // broadcast by tenant + organization only — no feature check — so every
1273
+ // signed-in user of the organization receives whatever the payload carries.
1274
+ // `reason` is mandatory operator prose of up to 2000 characters explaining why
1275
+ // a client's billing was reopened; broadcasting it organization-wide is a
1276
+ // disclosure the screen it comes from (`staff.timesheets.reports.unlock`) does
1277
+ // not make. It is already persisted verbatim on the `StaffTimeReportEvent`
1278
+ // audit row above, which is read behind the ACL, so nothing is lost.
1279
+ void emitStaffEvent('staff.timesheets.time_report.unlocked', {
1280
+ id: report.id,
1281
+ tenantId: report.tenantId,
1282
+ organizationId: report.organizationId,
1283
+ reference: report.reference,
1284
+ actorUserId: actorId,
1285
+ unlockedEntryCount,
1286
+ }).catch((err) => {
1287
+ logger.error('staff.timesheets emit time_report.unlocked failed', { err })
1288
+ })
1289
+
1290
+ if (unlockedEntryCount > 0) {
1291
+ void emitStaffEvent('staff.timesheets.time_entry.unlocked', {
1292
+ tenantId: report.tenantId,
1293
+ organizationId: report.organizationId,
1294
+ reportId: report.id,
1295
+ unlockedEntryCount,
1296
+ }).catch((err) => {
1297
+ logger.error('staff.timesheets emit time_entry.unlocked failed', { err })
1298
+ })
1299
+ }
1300
+
1301
+ return { reportId: report.id, unlockedEntryCount }
1302
+ },
1303
+ buildLog: async ({ snapshots, ctx, input }) => {
1304
+ const before = (snapshots.before as { snapshot?: ReportSnapshot } | undefined)?.snapshot
1305
+ if (!before) return null
1306
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
1307
+ const after = await loadReportSnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before))
1308
+ if (!after) return null
1309
+ const { translate } = await resolveTranslations()
1310
+ return {
1311
+ actionLabel: translate('staff.audit.timesheets.reports.unlock', 'Unlock report entries'),
1312
+ resourceKind: STAFF_TIME_REPORT_RESOURCE_KIND,
1313
+ resourceId: before.id,
1314
+ tenantId: before.tenantId,
1315
+ organizationId: before.organizationId,
1316
+ snapshotBefore: before,
1317
+ snapshotAfter: after,
1318
+ changes: buildChanges(
1319
+ before as unknown as Record<string, unknown>,
1320
+ after as unknown as Record<string, unknown>,
1321
+ [...DIFFED_FIELDS],
1322
+ ),
1323
+ payload: { reason: input?.reason ?? null },
1324
+ }
1325
+ },
1326
+ }
1327
+
1328
+ registerCommand(closeReportCommand)
1329
+ registerCommand(unlockReportCommand)