@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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/staff/lib/timeTrackingSeeds.ts"],
4
+ "sourcesContent": ["/**\n * Demo data for the time-tracking consulting suite.\n *\n * The module has a cold-start problem the team examples do not: a fresh tenant\n * lands on an empty portfolio, an empty board, an empty week grid and an empty\n * report list, and every screen that makes the module worth having only means\n * something once hours exist. This seed fills all four at once \u2014 four projects\n * against real customers, a populated Kanban board, ten weeks of logged time and\n * two reports, one of them closed so the lock behaviour is visible without\n * anybody having to close one first.\n *\n * Everything is derived from a fixed pseudo-random sequence rather than\n * `Math.random`, so two people demoing the same build see the same hours and the\n * same totals, and a screenshot stays true after a re-seed.\n *\n * Idempotency is a single check on the demo project codes: this is example data,\n * not reference data, so it is written once and afterwards left alone rather\n * than reconciled field by field.\n *\n * **Indexing.** Rows are written straight through the EntityManager rather than\n * through the module's commands, so none of them reach the query index. Listing\n * is unaffected \u2014 the engine reads base tables \u2014 but `$ilike` filters are\n * answered from the token index, so search returns nothing for seeded data until\n * `mercato query_index reindex` has run. The CLI command says so on success; a\n * caller invoking this function directly has to know.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { CustomerEntity } from '@open-mercato/core/modules/customers/data/entities'\nimport {\n StaffTeamMember,\n StaffTimeEntry,\n StaffTimeEntryTag,\n StaffTimeProject,\n StaffTimeProjectMember,\n StaffTimeReport,\n StaffTimeReportEntry,\n StaffTimeReportEvent,\n StaffTimeReportProject,\n StaffTimeTag,\n StaffTimeTask,\n StaffTimeTaskComment,\n StaffTimeTaskStatus,\n StaffTimeTaskTag,\n} from '../data/entities'\nimport { seedProjectTaskStatuses } from './timesheets-tasks/seedProjectStatuses'\nimport { formatTaskReference } from './timesheets-tasks/taskReference'\nimport { allocateReportReference, highestReportSequenceNumber } from './timesheets-reports/reportReference'\nimport { entryAmount, round2, sumAmounts } from './time-tracking/cost'\nimport { DEFAULT_ROUNDING_SETTINGS, roundMinutes } from './time-tracking/rounding'\nimport type { ProjectColorKey } from './timesheets-ui/colors'\n\nexport type StaffTimeTrackingSeedScope = { tenantId: string; organizationId: string }\n\ntype ProjectSeed = {\n code: string\n name: string\n description: string\n projectType: string\n color: ProjectColorKey\n status: 'active' | 'on_hold' | 'completed'\n hourlyRate: number\n currencyCode: string\n billableByDefault: boolean\n budgetKind: 'none' | 'hours' | 'amount'\n budgetValue: number | null\n costCenter: string\n customerIndex: number\n startedDaysAgo: number\n /** Members by index into the resolved staff list, first one is the lead. */\n memberIndexes: number[]\n memberRoles: string[]\n}\n\ntype TaskSeed = {\n statusSlug: string\n title: string\n description: string\n assigneeIndex: number | null\n tagSlugs: string[]\n comments?: string[]\n subtasks?: { statusSlug: string; title: string; assigneeIndex: number | null }[]\n}\n\ntype TagSeed = { slug: string; label: string; color: ProjectColorKey }\n\nconst DEMO_TAGS: TagSeed[] = [\n { slug: 'client-call', label: 'Client call', color: 'blue' },\n { slug: 'development', label: 'Development', color: 'indigo' },\n { slug: 'bugfix', label: 'Bugfix', color: 'red' },\n { slug: 'research', label: 'Research', color: 'purple' },\n { slug: 'internal', label: 'Internal', color: 'slate' },\n]\n\n/**\n * Budgets are sized against the hours this file generates so the portfolio shows\n * one of each state rather than four identical red bars: Atlas and Nova sit\n * comfortably inside, Apollo runs into the 80% warning, Orbit is over. The exact\n * percentages drift a little with the weekday the seed runs on, which is fine \u2014\n * the states do not.\n */\nconst DEMO_PROJECTS: ProjectSeed[] = [\n {\n code: 'APOLLO',\n name: 'Apollo \u2014 Website Redesign',\n description:\n 'Full redesign of the public site and booking flow, delivered in three milestones with a fixed launch date.',\n projectType: 'Fixed scope',\n color: 'blue',\n status: 'active',\n hourlyRate: 150,\n currencyCode: 'USD',\n billableByDefault: true,\n budgetKind: 'hours',\n budgetValue: 720,\n costCenter: 'CC-100',\n customerIndex: 0,\n startedDaysAgo: 84,\n memberIndexes: [0, 1, 3],\n memberRoles: ['Project manager', 'Frontend engineer', 'UX designer'],\n },\n {\n code: 'ATLAS',\n name: 'Atlas \u2014 Data Platform Integration',\n description:\n 'Ingest pipeline, warehouse modelling and a reporting API against the customer analytics platform.',\n projectType: 'Time & materials',\n color: 'emerald',\n status: 'active',\n hourlyRate: 175,\n currencyCode: 'USD',\n billableByDefault: true,\n budgetKind: 'amount',\n budgetValue: 100000,\n costCenter: 'CC-200',\n customerIndex: 1,\n startedDaysAgo: 63,\n memberIndexes: [0, 4, 2],\n memberRoles: ['Tech lead', 'DevOps engineer', 'Product manager'],\n },\n {\n code: 'ORBIT',\n name: 'Orbit \u2014 Support Retainer',\n description:\n 'Monthly retainer covering incident response, small enhancements and a standing weekly check-in call.',\n projectType: 'Retainer',\n color: 'orange',\n status: 'active',\n hourlyRate: 120,\n currencyCode: 'USD',\n billableByDefault: true,\n budgetKind: 'hours',\n budgetValue: 360,\n costCenter: 'CC-300',\n customerIndex: 2,\n startedDaysAgo: 70,\n memberIndexes: [4, 1],\n memberRoles: ['Support lead', 'Frontend engineer'],\n },\n {\n code: 'NOVA',\n name: 'Nova \u2014 ERP Migration Discovery',\n description:\n 'Discovery phase for the ERP migration. Paused while the customer finalises the vendor short-list.',\n projectType: 'Discovery',\n color: 'purple',\n status: 'on_hold',\n hourlyRate: 165,\n currencyCode: 'USD',\n billableByDefault: true,\n budgetKind: 'hours',\n budgetValue: 240,\n costCenter: 'CC-400',\n customerIndex: 0,\n startedDaysAgo: 49,\n memberIndexes: [2, 0],\n memberRoles: ['Product manager', 'Solution architect'],\n },\n]\n\nconst DEMO_TASKS: Record<string, TaskSeed[]> = {\n APOLLO: [\n {\n statusSlug: 'done',\n title: 'Discovery workshop with the client',\n description: 'Two half-day sessions covering the current booking funnel and the drop-off points.',\n assigneeIndex: 0,\n tagSlugs: ['client-call', 'research'],\n comments: ['Recording and notes shared with the client the same afternoon.'],\n },\n {\n statusSlug: 'done',\n title: 'Information architecture and sitemap',\n description: 'Consolidate 40 pages into 18 and agree the new navigation.',\n assigneeIndex: 3,\n tagSlugs: ['research'],\n },\n {\n statusSlug: 'in-review',\n title: 'Design system and component library',\n description: 'Tokens, typography scale and the twelve components the new pages are built from.',\n assigneeIndex: 3,\n tagSlugs: ['development'],\n comments: ['Waiting on brand sign-off for the secondary palette.'],\n subtasks: [\n { statusSlug: 'done', title: 'Colour and typography tokens', assigneeIndex: 3 },\n { statusSlug: 'in-review', title: 'Form and input components', assigneeIndex: 1 },\n ],\n },\n {\n statusSlug: 'in-progress',\n title: 'Booking flow rebuild',\n description: 'Rebuild the four-step booking flow on the new component library.',\n assigneeIndex: 1,\n tagSlugs: ['development'],\n subtasks: [\n { statusSlug: 'done', title: 'Step 1 \u2014 date and party size', assigneeIndex: 1 },\n { statusSlug: 'in-progress', title: 'Step 2 \u2014 table selection', assigneeIndex: 1 },\n { statusSlug: 'backlog', title: 'Step 3 \u2014 guest details', assigneeIndex: null },\n ],\n },\n {\n statusSlug: 'in-progress',\n title: 'Fix layout shift on the gallery page',\n description: 'Images without intrinsic dimensions push the footer around on first paint.',\n assigneeIndex: 1,\n tagSlugs: ['bugfix'],\n },\n {\n statusSlug: 'backlog',\n title: 'Analytics and conversion tracking',\n description: 'Event map for the new funnel plus the dashboard the client reviews weekly.',\n assigneeIndex: 0,\n tagSlugs: ['development'],\n },\n {\n statusSlug: 'backlog',\n title: 'Content migration',\n description: 'Move and re-edit the existing copy into the new page templates.',\n assigneeIndex: null,\n tagSlugs: [],\n },\n ],\n ATLAS: [\n {\n statusSlug: 'done',\n title: 'Source system audit',\n description: 'Catalogue the six source systems, their volumes and their refresh windows.',\n assigneeIndex: 0,\n tagSlugs: ['research'],\n },\n {\n statusSlug: 'in-progress',\n title: 'Ingest pipeline',\n description: 'Incremental loads with replay, running on the customer scheduler.',\n assigneeIndex: 4,\n tagSlugs: ['development'],\n comments: [\n 'Replay from an arbitrary watermark works; still tuning the batch size for the largest table.',\n ],\n subtasks: [\n { statusSlug: 'done', title: 'Connector framework', assigneeIndex: 4 },\n { statusSlug: 'in-progress', title: 'Incremental watermarks', assigneeIndex: 4 },\n ],\n },\n {\n statusSlug: 'in-progress',\n title: 'Warehouse dimensional model',\n description: 'Star schema for orders, customers and merchandising decisions.',\n assigneeIndex: 0,\n tagSlugs: ['development'],\n },\n {\n statusSlug: 'in-review',\n title: 'Reporting API contract',\n description: 'The endpoints the customer BI layer reads, versioned and documented.',\n assigneeIndex: 2,\n tagSlugs: ['client-call'],\n },\n {\n statusSlug: 'backlog',\n title: 'Data quality monitors',\n description: 'Freshness, row-count and null-rate checks with alerting into the customer channel.',\n assigneeIndex: 4,\n tagSlugs: ['development'],\n },\n {\n statusSlug: 'backlog',\n title: 'Handover documentation',\n description: 'Runbook, architecture note and the on-call escalation path.',\n assigneeIndex: null,\n tagSlugs: ['internal'],\n },\n ],\n ORBIT: [\n {\n statusSlug: 'done',\n title: 'March incident \u2014 checkout timeouts',\n description: 'Connection pool exhaustion under the promotion traffic spike.',\n assigneeIndex: 4,\n tagSlugs: ['bugfix'],\n comments: ['Root cause and the pool sizing change written up for the customer.'],\n },\n {\n statusSlug: 'in-progress',\n title: 'Monthly dependency and security updates',\n description: 'Patch level, CVE review and a staged rollout.',\n assigneeIndex: 4,\n tagSlugs: ['internal'],\n },\n {\n statusSlug: 'in-progress',\n title: 'Weekly check-in call',\n description: 'Standing Thursday call \u2014 backlog triage and the week ahead.',\n assigneeIndex: 1,\n tagSlugs: ['client-call'],\n },\n {\n statusSlug: 'backlog',\n title: 'Search relevance tuning',\n description: 'Synonyms and boosting for the ten highest-volume queries.',\n assigneeIndex: 1,\n tagSlugs: ['development'],\n },\n ],\n NOVA: [\n {\n statusSlug: 'done',\n title: 'Stakeholder interviews',\n description: 'Nine interviews across finance, warehouse and customer service.',\n assigneeIndex: 2,\n tagSlugs: ['client-call', 'research'],\n },\n {\n statusSlug: 'in-review',\n title: 'Process gap analysis',\n description: 'Where the current ERP is worked around, and what that costs per month.',\n assigneeIndex: 2,\n tagSlugs: ['research'],\n comments: ['Draft with the client; feedback expected once the vendor short-list closes.'],\n },\n {\n statusSlug: 'backlog',\n title: 'Migration options and estimates',\n description: 'Three options with cost, risk and a rough delivery window each.',\n assigneeIndex: 0,\n tagSlugs: [],\n },\n ],\n}\n\nconst ENTRY_NOTES = [\n 'Pairing session on the open items',\n 'Implementation and self-review',\n 'Client call and follow-up notes',\n 'Code review and feedback round',\n 'Debugging the reported regression',\n 'Refinement and estimates for the next sprint',\n 'Documentation and handover notes',\n 'Environment and deployment work',\n 'Design review with the team',\n 'Investigating the reported edge case',\n]\n\nconst NONBILLABLE_NOTES = [\n 'Internal team sync',\n 'Knowledge sharing session',\n 'Onboarding a new team member',\n]\n\n/** Names used by `seedStaffTeamExamples`, in the order the demo projects expect them. */\nconst PREFERRED_MEMBER_NAMES = [\n 'Alex Chen',\n 'Priya Nair',\n 'Marta Lopez',\n 'Samir Haddad',\n 'Jordan Kim',\n]\n\nconst WEEKS_OF_HISTORY = 10\n\n/**\n * Deterministic LCG. The demo has to produce the same hours on every machine \u2014\n * a screenshot of the portfolio should still be true after somebody re-seeds.\n */\nfunction createRandom(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (Math.imul(state, 1664525) + 1013904223) >>> 0\n return state / 0x1_0000_0000\n }\n}\n\nfunction pick<T>(random: () => number, values: readonly T[]): T {\n return values[Math.floor(random() * values.length) % values.length]\n}\n\nfunction startOfUtcDay(date: Date): Date {\n return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))\n}\n\nfunction addDays(date: Date, days: number): Date {\n const next = new Date(date.getTime())\n next.setUTCDate(next.getUTCDate() + days)\n return next\n}\n\nfunction isWeekend(date: Date): boolean {\n const day = date.getUTCDay()\n return day === 0 || day === 6\n}\n\nfunction atMinutes(date: Date, minutesFromMidnight: number): Date {\n return new Date(date.getTime() + minutesFromMidnight * 60_000)\n}\n\ntype ResolvedCustomer = { id: string | null; name: string }\n\n/**\n * Customers are read softly. The projects are worth having even in a tenant that\n * never seeded the CRM examples, so a missing customer degrades to a snapshot\n * name with no id rather than skipping the project \u2014 the portfolio still renders,\n * and only the edit form asks for a customer to be picked.\n */\nasync function resolveCustomers(\n em: EntityManager,\n scope: StaffTimeTrackingSeedScope,\n): Promise<ResolvedCustomer[]> {\n const fallback: ResolvedCustomer[] = [\n { id: null, name: 'Copperleaf Design Co.' },\n { id: null, name: 'Harborview Analytics' },\n { id: null, name: 'Northwind Retail Group' },\n ]\n try {\n const entities = await findWithDecryption(\n em,\n CustomerEntity,\n { tenantId: scope.tenantId, organizationId: scope.organizationId, kind: 'company', deletedAt: null },\n { limit: 25 },\n scope,\n )\n const named = entities\n .map((entity) => ({ id: entity.id, name: (entity.displayName ?? '').trim() }))\n .filter((entity) => entity.name.length > 0)\n .sort((left, right) => left.name.localeCompare(right.name))\n if (named.length === 0) return fallback\n return fallback.map((entry, index) => named[index % named.length] ?? entry)\n } catch {\n return fallback\n }\n}\n\nasync function resolveMembers(\n em: EntityManager,\n scope: StaffTimeTrackingSeedScope,\n): Promise<StaffTeamMember[]> {\n const members = await findWithDecryption(\n em,\n StaffTeamMember,\n { tenantId: scope.tenantId, organizationId: scope.organizationId, isActive: true, deletedAt: null },\n { limit: 50 },\n scope,\n )\n if (members.length === 0) return []\n const byName = new Map(members.map((member) => [(member.displayName ?? '').trim(), member]))\n const preferred = PREFERRED_MEMBER_NAMES.map((name) => byName.get(name)).filter(\n (member): member is StaffTeamMember => Boolean(member),\n )\n const remaining = members.filter((member) => !preferred.includes(member))\n const ordered = [...preferred, ...remaining]\n // Every project seed indexes up to five members; a smaller team wraps around so\n // a two-person tenant still gets every project staffed.\n return ordered\n}\n\nfunction memberAt(members: StaffTeamMember[], index: number): StaffTeamMember {\n return members[index % members.length]\n}\n\nexport async function seedStaffTimeTrackingExamples(\n em: EntityManager,\n scope: StaffTimeTrackingSeedScope,\n): Promise<boolean> {\n const codes = DEMO_PROJECTS.map((project) => project.code)\n const existing = await em.find(StaffTimeProject, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n code: { $in: codes },\n deletedAt: null,\n })\n if (existing.length > 0) return false\n\n const members = await resolveMembers(em, scope)\n if (members.length === 0) return false\n\n const customers = await resolveCustomers(em, scope)\n const users = await findWithDecryption(\n em,\n User,\n { tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },\n { limit: 10 },\n scope,\n )\n const actorUserId = [...users]\n .sort((left, right) => (left.email ?? '').localeCompare(right.email ?? ''))[0]?.id ?? null\n\n const random = createRandom(20260812)\n const now = new Date()\n const today = startOfUtcDay(now)\n\n const tagBySlug = new Map<string, StaffTimeTag>()\n for (const seed of DEMO_TAGS) {\n const tag = em.create(StaffTimeTag, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n slug: seed.slug,\n label: seed.label,\n color: seed.color,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(tag)\n tagBySlug.set(seed.slug, tag)\n }\n await em.flush()\n\n type ProjectContext = {\n seed: ProjectSeed\n project: StaffTimeProject\n statusBySlug: Map<string, StaffTimeTaskStatus>\n tasks: StaffTimeTask[]\n /** Tasks that can carry time, keyed by the staff member they are assigned to. */\n tasksByMemberId: Map<string, StaffTimeTask[]>\n memberIds: string[]\n }\n\n const contexts: ProjectContext[] = []\n\n for (const seed of DEMO_PROJECTS) {\n const customer = customers[seed.customerIndex] ?? customers[0]\n const startDate = addDays(today, -seed.startedDaysAgo)\n const project = em.create(StaffTimeProject, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n name: seed.name,\n code: seed.code,\n description: seed.description,\n projectType: seed.projectType,\n color: seed.color,\n status: seed.status,\n customerId: customer.id,\n customerSnapshot: { name: customer.name, kind: 'company' },\n ownerUserId: actorUserId,\n costCenter: seed.costCenter,\n startDate,\n hourlyRate: seed.hourlyRate.toFixed(4),\n currencyCode: seed.currencyCode,\n billableByDefault: seed.billableByDefault,\n budgetKind: seed.budgetKind,\n budgetValue: seed.budgetValue === null ? null : seed.budgetValue.toFixed(4),\n budgetWarnAtPercent: 80,\n budgetAlertedAtPercent: null,\n createdAt: startDate,\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(project)\n await em.flush()\n\n const statuses = seedProjectTaskStatuses(\n em,\n { tenantId: scope.tenantId, organizationId: scope.organizationId, timeProjectId: project.id },\n (_key, fallback) => fallback,\n )\n await em.flush()\n const statusBySlug = new Map(statuses.map((status) => [status.slug, status]))\n\n const memberIds: string[] = []\n seed.memberIndexes.forEach((memberIndex, position) => {\n const member = memberAt(members, memberIndex)\n if (memberIds.includes(member.id)) return\n memberIds.push(member.id)\n em.persist(\n em.create(StaffTimeProjectMember, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n timeProjectId: project.id,\n staffMemberId: member.id,\n role: seed.memberRoles[position] ?? 'Consultant',\n status: 'active',\n // The lead and the first collaborator are pinned into the weekly grid so\n // the grid has rows the moment it opens.\n showInGrid: position < 2,\n assignedStartDate: startDate,\n assignedEndDate: null,\n createdAt: startDate,\n updatedAt: now,\n deletedAt: null,\n }),\n )\n })\n await em.flush()\n\n const taskSeeds = DEMO_TASKS[seed.code] ?? []\n const tasks: StaffTimeTask[] = []\n const tasksByMemberId = new Map<string, StaffTimeTask[]>()\n let sequenceNumber = 0\n\n const registerTask = (task: StaffTimeTask, assigneeId: string | null) => {\n tasks.push(task)\n if (!assigneeId) return\n const bucket = tasksByMemberId.get(assigneeId) ?? []\n bucket.push(task)\n tasksByMemberId.set(assigneeId, bucket)\n }\n\n for (const taskSeed of taskSeeds) {\n const status = statusBySlug.get(taskSeed.statusSlug) ?? statuses[0]\n const assignee =\n taskSeed.assigneeIndex === null ? null : memberAt(members, seed.memberIndexes[taskSeed.assigneeIndex] ?? taskSeed.assigneeIndex)\n sequenceNumber += 1\n const createdAt = addDays(startDate, Math.floor(random() * 10))\n const parent = em.create(StaffTimeTask, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n timeProjectId: project.id,\n parentTaskId: null,\n taskStatusId: status.id,\n sequenceNumber,\n reference: formatTaskReference(seed.code, sequenceNumber),\n title: taskSeed.title,\n description: taskSeed.description,\n assigneeStaffMemberId: assignee?.id ?? null,\n position: sequenceNumber * 1000,\n createdByUserId: actorUserId,\n closedAt: status.isDone ? addDays(today, -Math.floor(random() * 20) - 1) : null,\n createdAt,\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(parent)\n await em.flush()\n registerTask(parent, assignee?.id ?? null)\n\n for (const slug of taskSeed.tagSlugs) {\n const tag = tagBySlug.get(slug)\n if (!tag) continue\n em.persist(\n em.create(StaffTimeTaskTag, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n tagId: tag.id,\n taskId: parent.id,\n createdAt: now,\n }),\n )\n }\n\n for (const body of taskSeed.comments ?? []) {\n em.persist(\n em.create(StaffTimeTaskComment, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n taskId: parent.id,\n body,\n authorUserId: actorUserId,\n createdAt: addDays(createdAt, 2),\n updatedAt: addDays(createdAt, 2),\n deletedAt: null,\n }),\n )\n }\n\n for (const subtaskSeed of taskSeed.subtasks ?? []) {\n const subtaskStatus = statusBySlug.get(subtaskSeed.statusSlug) ?? statuses[0]\n const subtaskAssignee =\n subtaskSeed.assigneeIndex === null\n ? null\n : memberAt(members, seed.memberIndexes[subtaskSeed.assigneeIndex] ?? subtaskSeed.assigneeIndex)\n sequenceNumber += 1\n const subtask = em.create(StaffTimeTask, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n timeProjectId: project.id,\n parentTaskId: parent.id,\n taskStatusId: subtaskStatus.id,\n sequenceNumber,\n reference: formatTaskReference(seed.code, sequenceNumber),\n title: subtaskSeed.title,\n description: null,\n assigneeStaffMemberId: subtaskAssignee?.id ?? null,\n position: sequenceNumber * 1000,\n createdByUserId: actorUserId,\n closedAt: subtaskStatus.isDone ? addDays(today, -Math.floor(random() * 15) - 1) : null,\n createdAt: addDays(createdAt, 1),\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(subtask)\n await em.flush()\n registerTask(subtask, subtaskAssignee?.id ?? null)\n }\n await em.flush()\n }\n\n contexts.push({ seed, project, statusBySlug, tasks, tasksByMemberId, memberIds })\n }\n\n // --- Logged time -------------------------------------------------------\n //\n // Ten weeks of weekdays. Each member works the projects they are assigned to,\n // chaining entries from 09:00 so a day never overlaps itself \u2014 the same\n // invariant the entry command enforces on the write path.\n\n // A paused project stopped accruing time when it was paused, but it did accrue\n // some before \u2014 a project on hold with no history at all reads as a data bug.\n const pausedAfter = addDays(today, -21)\n\n const projectsByMemberId = new Map<string, ProjectContext[]>()\n for (const context of contexts) {\n for (const memberId of context.memberIds) {\n const bucket = projectsByMemberId.get(memberId) ?? []\n bucket.push(context)\n projectsByMemberId.set(memberId, bucket)\n }\n }\n\n const entriesByProjectId = new Map<string, StaffTimeEntry[]>()\n const firstDay = addDays(today, -(WEEKS_OF_HISTORY * 7) + 1)\n\n for (let offset = 0; offset < WEEKS_OF_HISTORY * 7; offset += 1) {\n const day = addDays(firstDay, offset)\n if (isWeekend(day)) continue\n\n for (const [memberId, memberProjects] of projectsByMemberId) {\n // Not everybody logs every day \u2014 a demo with a perfect grid reads as fake\n // and hides the \"missing time\" states the week view is built to show.\n if (random() < 0.12) continue\n\n // Only projects that had actually started, and had not been paused, on this\n // day. Filtering up front keeps the day's total intact: picking first and\n // discarding after would quietly turn a full day into a half one.\n const eligible = memberProjects.filter((context) => {\n if (day < startOfUtcDay(context.project.startDate ?? day)) return false\n return !(context.seed.status === 'on_hold' && day > pausedAfter)\n })\n if (eligible.length === 0) continue\n\n // A worked day, then split across projects \u2014 6.5 to 9 hours against the\n // 8-hour default target, so the week view shows over, under and on target.\n let remainingMinutes = (13 + Math.floor(random() * 6)) * 30\n const entryCount = random() < 0.5 ? 2 : 3\n let cursorMinutes = 9 * 60 + Math.floor(random() * 4) * 15\n\n for (let index = 0; index < entryCount; index += 1) {\n const context = eligible[Math.floor(random() * eligible.length) % eligible.length]\n\n const candidates = context.tasksByMemberId.get(memberId) ?? context.tasks\n const task = candidates.length > 0 ? pick(random, candidates) : null\n\n const isLast = index === entryCount - 1\n const share = Math.round(remainingMinutes / (entryCount - index) / 30) * 30\n const durationMinutes = Math.max(30, isLast ? remainingMinutes : share)\n remainingMinutes -= durationMinutes\n const isBillable = random() > 0.14\n const startedAt = atMinutes(day, cursorMinutes)\n const endedAt = atMinutes(day, cursorMinutes + durationMinutes)\n // A short break before the next block, so a day reads like a day.\n cursorMinutes += durationMinutes + 15 + Math.floor(random() * 3) * 15\n\n const entry = em.create(StaffTimeEntry, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n staffMemberId: memberId,\n date: day,\n durationMinutes,\n startedAt,\n endedAt,\n notes: isBillable ? pick(random, ENTRY_NOTES) : pick(random, NONBILLABLE_NOTES),\n timeProjectId: context.project.id,\n taskId: task?.id ?? null,\n customerId: context.project.customerId ?? null,\n dealId: null,\n orderId: null,\n isBillable,\n roundedMinutes: roundMinutes(durationMinutes, DEFAULT_ROUNDING_SETTINGS),\n rateOverrideAmount: null,\n rateCurrencyCode: context.project.currencyCode ?? null,\n lockedReportId: null,\n lockedAt: null,\n source: random() < 0.25 ? 'timer' : 'manual',\n createdAt: endedAt,\n updatedAt: endedAt,\n deletedAt: null,\n })\n em.persist(entry)\n\n const bucket = entriesByProjectId.get(context.project.id) ?? []\n bucket.push(entry)\n entriesByProjectId.set(context.project.id, bucket)\n\n if (random() < 0.35) {\n const tag = pick(random, DEMO_TAGS)\n const tagRow = tagBySlug.get(tag.slug)\n if (tagRow) {\n await em.flush()\n em.persist(\n em.create(StaffTimeEntryTag, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n tagId: tagRow.id,\n timeEntryId: entry.id,\n createdAt: endedAt,\n }),\n )\n }\n }\n }\n }\n await em.flush()\n }\n await em.flush()\n\n await seedDemoReports({ em, scope, contexts, entriesByProjectId, actorUserId, today, now })\n\n return true\n}\n\ntype ReportSeedArgs = {\n em: EntityManager\n scope: StaffTimeTrackingSeedScope\n contexts: {\n seed: ProjectSeed\n project: StaffTimeProject\n }[]\n entriesByProjectId: Map<string, StaffTimeEntry[]>\n actorUserId: string | null\n today: Date\n now: Date\n}\n\n/**\n * Two reports: last month closed, this month still a draft.\n *\n * The closed one is written frozen \u2014 the same per-entry values\n * `staff.timesheets.reports.close` would have computed \u2014 and locks the entries it\n * covers. That is the state worth demoing: a report list with a locked period in\n * it shows why the lock exists, and the entries it froze render read-only in the\n * week grid without anybody having to close a report first.\n */\nasync function seedDemoReports(args: ReportSeedArgs): Promise<void> {\n const { em, scope, contexts, entriesByProjectId, actorUserId, today, now } = args\n\n const monthStart = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 1))\n const previousMonthStart = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth() - 1, 1))\n const previousMonthEnd = addDays(monthStart, -1)\n\n const existingReferences = await em.find(\n StaffTimeReport,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n { fields: ['reference'] },\n )\n const year = today.getUTCFullYear()\n let sequence = highestReportSequenceNumber(\n existingReferences.map((report) => report.reference),\n year,\n )\n\n const byCustomer = new Map<string, typeof contexts>()\n for (const context of contexts) {\n const customerId = context.project.customerId\n if (!customerId) continue\n const bucket = byCustomer.get(customerId) ?? []\n bucket.push(context)\n byCustomer.set(customerId, bucket)\n }\n if (byCustomer.size === 0) return\n\n const customerGroups = [...byCustomer.entries()]\n\n const createReport = async (\n group: (typeof customerGroups)[number],\n options: {\n title: string\n periodFrom: Date\n periodTo: Date\n close: boolean\n },\n ): Promise<void> => {\n const [customerId, groupContexts] = group\n const first = groupContexts[0]\n sequence += 1\n const { reference } = allocateReportReference(year, sequence - 1)\n const currencyCode = first.project.currencyCode ?? 'USD'\n\n const report = em.create(StaffTimeReport, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n customerId,\n customerSnapshot: first.project.customerSnapshot ?? null,\n reference,\n title: options.title,\n periodKind: 'month',\n periodFrom: options.periodFrom,\n periodTo: options.periodTo,\n currencyCode,\n grouping: 'project_task',\n nonbillableMode: 'separate',\n includeAlreadyReported: false,\n showRates: true,\n roundingUnitMinutes: DEFAULT_ROUNDING_SETTINGS.unitMinutes,\n roundingDirection: DEFAULT_ROUNDING_SETTINGS.direction,\n status: 'draft',\n totalBillableMinutes: null,\n totalNonbillableMinutes: null,\n totalAmount: null,\n closedAt: null,\n closedByUserId: null,\n createdByUserId: actorUserId,\n createdAt: options.periodTo,\n updatedAt: now,\n deletedAt: null,\n })\n em.persist(report)\n await em.flush()\n\n for (const context of groupContexts) {\n em.persist(\n em.create(StaffTimeReportProject, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n reportId: report.id,\n timeProjectId: context.project.id,\n createdAt: now,\n }),\n )\n }\n await em.flush()\n\n if (!options.close) return\n\n const included = groupContexts.flatMap((context) => {\n const rate = Number(context.project.hourlyRate ?? 0)\n return (entriesByProjectId.get(context.project.id) ?? [])\n .filter((entry) => entry.date >= options.periodFrom && entry.date <= options.periodTo)\n .map((entry) => ({ entry, rate }))\n })\n if (included.length === 0) return\n\n const closedAt = addDays(options.periodTo, 2)\n const amounts: (number | null)[] = []\n let billableMinutes = 0\n let nonbillableMinutes = 0\n\n for (const { entry, rate } of included) {\n const minutes = entry.roundedMinutes ?? entry.durationMinutes\n const amount = entryAmount(\n { isBillable: entry.isBillable, roundedMinutes: minutes, rateOverrideAmount: null },\n { hourlyRate: rate },\n )\n amounts.push(amount)\n if (entry.isBillable) billableMinutes += minutes\n else nonbillableMinutes += minutes\n\n em.persist(\n em.create(StaffTimeReportEntry, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n reportId: report.id,\n timeEntryId: entry.id,\n frozenRawMinutes: entry.durationMinutes,\n frozenRoundedMinutes: minutes,\n frozenRateAmount: entry.isBillable ? String(round2(rate)) : null,\n frozenCurrencyCode: currencyCode,\n frozenAmount: amount === null ? null : amount.toFixed(2),\n frozenIsBillable: entry.isBillable,\n createdAt: closedAt,\n }),\n )\n\n entry.lockedReportId = report.id\n entry.lockedAt = closedAt\n entry.updatedAt = closedAt\n }\n\n const totalAmount = sumAmounts(amounts)\n report.status = 'closed'\n report.closedAt = closedAt\n report.closedByUserId = actorUserId\n report.totalBillableMinutes = billableMinutes\n report.totalNonbillableMinutes = nonbillableMinutes\n report.totalAmount = totalAmount.toFixed(2)\n report.updatedAt = closedAt\n\n em.persist(\n em.create(StaffTimeReportEvent, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n reportId: report.id,\n eventType: 'closed',\n reason: null,\n actorUserId,\n metadata: {\n frozenEntryCount: included.length,\n lockedEntryCount: included.length,\n totalAmount,\n currencyCode,\n roundingUnitMinutes: DEFAULT_ROUNDING_SETTINGS.unitMinutes,\n roundingDirection: DEFAULT_ROUNDING_SETTINGS.direction,\n },\n createdAt: closedAt,\n }),\n )\n await em.flush()\n }\n\n const monthLabel = (date: Date) =>\n `${date.toLocaleString('en-US', { month: 'long', timeZone: 'UTC' })} ${date.getUTCFullYear()}`\n\n await createReport(customerGroups[0], {\n title: `${monthLabel(previousMonthStart)} \u2014 time and materials`,\n periodFrom: previousMonthStart,\n periodTo: previousMonthEnd,\n close: true,\n })\n\n const draftGroup = customerGroups[1] ?? customerGroups[0]\n await createReport(draftGroup, {\n title: `${monthLabel(monthStart)} \u2014 work in progress`,\n periodFrom: monthStart,\n periodTo: today,\n close: false,\n })\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,12 +1,11 @@
1
1
  import {
2
2
  canonicalizeResourceTag,
3
- deriveResourceFromCommandId,
4
3
  invalidateCrudCache
5
4
  } from "@open-mercato/shared/lib/crud/cache";
6
5
  import { createLogger } from "@open-mercato/shared/lib/logger";
7
- import { staffTimeEntryCommandIds } from "../crud.js";
6
+ import { staffTimeEntryCrudEvents } from "../crud.js";
8
7
  const logger = createLogger("staff").child({ component: "timesheets-cache" });
9
- const staffTimeEntryCacheResource = canonicalizeResourceTag(deriveResourceFromCommandId(staffTimeEntryCommandIds.create)) ?? "staff.timesheet";
8
+ const staffTimeEntryCacheResource = canonicalizeResourceTag(`${staffTimeEntryCrudEvents.module}.${staffTimeEntryCrudEvents.entity}`) ?? "staff.timesheets.time.entry";
10
9
  async function invalidateStaffTimeEntryCache(container, identifiers, fallbackTenant, reason) {
11
10
  try {
12
11
  await invalidateCrudCache(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/staff/lib/timesheets/timeEntryCacheInvalidation.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n canonicalizeResourceTag,\n deriveResourceFromCommandId,\n invalidateCrudCache,\n type CrudCacheIdentifiers,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { staffTimeEntryCommandIds } from '../crud'\n\nconst logger = createLogger('staff').child({ component: 'timesheets-cache' })\n\n/**\n * The resource tag the CRUD list cache stores time-entry payloads under.\n *\n * `makeCrudRoute` derives it from the create action's command id (the route declares\n * no `events` config), which singularizes the command's SECOND segment \u2014 so the tag\n * is `staff.timesheet`, NOT the `staff.time_entry` the entity name suggests. Deriving\n * it here through the same shared helpers keeps the flush tag equal to the store tag;\n * a hand-typed literal is how a custom write route silently flushes nothing (#3143,\n * #3711). The literal fallback only guards against a null derivation at runtime \u2014 the\n * unit test pins the derived value so drift fails in CI instead of in production.\n */\nexport const staffTimeEntryCacheResource =\n canonicalizeResourceTag(deriveResourceFromCommandId(staffTimeEntryCommandIds.create)) ?? 'staff.timesheet'\n\n/**\n * Flush the cached time-entry collections and record entries after a committed write.\n *\n * Custom write routes that mutate `StaffTimeEntry` through the EntityManager bypass\n * `makeCrudRoute`'s own POST/PUT/DELETE handlers and the command bus, so neither of\n * the platform's two `invalidateCrudCache` call sites runs for them. Without this the\n * opt-in CRUD list cache (`ENABLE_CRUD_API_CACHE`) keeps serving the pre-write payload\n * and the weekly timesheet grid reloads without the rows it just saved (#4970).\n *\n * MUST be called after the transaction commits, never inside it. Because the write is\n * already committed by then, a failing cache backend is logged rather than thrown \u2014 the\n * command bus guards its own invalidation the same way. Surfacing it would turn a\n * successful write into an error response and invite a duplicating client retry, while\n * swallowing it costs at most one TTL of staleness.\n */\nexport async function invalidateStaffTimeEntryCache(\n container: AwilixContainer,\n identifiers: CrudCacheIdentifiers,\n fallbackTenant: string | null,\n reason: string,\n): Promise<void> {\n try {\n await invalidateCrudCache(\n container,\n staffTimeEntryCacheResource,\n identifiers,\n fallbackTenant,\n reason,\n )\n } catch (err) {\n logger.warn('Time-entry cache invalidation failed', {\n resource: staffTimeEntryCacheResource,\n recordId: identifiers.id ?? null,\n reason,\n err,\n })\n }\n}\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AAEzC,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAarE,MAAM,8BACX,wBAAwB,4BAA4B,yBAAyB,MAAM,CAAC,KAAK;AAiB3F,eAAsB,8BACpB,WACA,aACA,gBACA,QACe;AACf,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,KAAK,wCAAwC;AAAA,MAClD,UAAU;AAAA,MACV,UAAU,YAAY,MAAM;AAAA,MAC5B;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport {\n canonicalizeResourceTag,\n invalidateCrudCache,\n type CrudCacheIdentifiers,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { staffTimeEntryCrudEvents } from '../crud'\n\nconst logger = createLogger('staff').child({ component: 'timesheets-cache' })\n\n/**\n * The resource tag the CRUD list cache stores time-entry payloads under.\n *\n * `resolveResourceAliasesList` in the CRUD factory prefers the route's `events`\n * config over the create command id, so the tag follows `staff.timesheets.time_entry`\n * canonicalized (underscores become dots) rather than the command-derived\n * `staff.timesheet`. Deriving it here from the very object the route passes as its\n * `events` config keeps the flush tag equal to the store tag; a hand-typed literal is\n * how a custom write route silently flushes nothing (#3143, #3711). The literal\n * fallback only guards against a null derivation at runtime \u2014 the unit test pins the\n * derived value so drift fails in CI instead of in production.\n */\nexport const staffTimeEntryCacheResource =\n canonicalizeResourceTag(`${staffTimeEntryCrudEvents.module}.${staffTimeEntryCrudEvents.entity}`)\n ?? 'staff.timesheets.time.entry'\n\n/**\n * Flush the cached time-entry collections and record entries after a committed write.\n *\n * Custom write routes that mutate `StaffTimeEntry` through the EntityManager bypass\n * `makeCrudRoute`'s own POST/PUT/DELETE handlers and the command bus, so neither of\n * the platform's two `invalidateCrudCache` call sites runs for them. Without this the\n * opt-in CRUD list cache (`ENABLE_CRUD_API_CACHE`) keeps serving the pre-write payload\n * and the weekly timesheet grid reloads without the rows it just saved (#4970).\n *\n * MUST be called after the transaction commits, never inside it. Because the write is\n * already committed by then, a failing cache backend is logged rather than thrown \u2014 the\n * command bus guards its own invalidation the same way. Surfacing it would turn a\n * successful write into an error response and invite a duplicating client retry, while\n * swallowing it costs at most one TTL of staleness.\n */\nexport async function invalidateStaffTimeEntryCache(\n container: AwilixContainer,\n identifiers: CrudCacheIdentifiers,\n fallbackTenant: string | null,\n reason: string,\n): Promise<void> {\n try {\n await invalidateCrudCache(\n container,\n staffTimeEntryCacheResource,\n identifiers,\n fallbackTenant,\n reason,\n )\n } catch (err) {\n logger.warn('Time-entry cache invalidation failed', {\n resource: staffTimeEntryCacheResource,\n recordId: identifiers.id ?? null,\n reason,\n err,\n })\n }\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AAEzC,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAcrE,MAAM,8BACX,wBAAwB,GAAG,yBAAyB,MAAM,IAAI,yBAAyB,MAAM,EAAE,KAC5F;AAiBL,eAAsB,8BACpB,WACA,aACA,gBACA,QACe;AACf,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,KAAK,wCAAwC;AAAA,MAClD,UAAU;AAAA,MACV,UAAU,YAAY,MAAM;AAAA,MAC5B;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,152 @@
1
+ import { StaffTimeEntry, StaffTimeEntryTag, StaffTimeProject, StaffTimeTag } from "../../data/entities.js";
2
+ import { entryAmount } from "../time-tracking/cost.js";
3
+ const FIELD = {
4
+ id: ["id", "id"],
5
+ roundedMinutes: ["rounded_minutes", "roundedMinutes"],
6
+ notes: ["notes", "notes"],
7
+ timeProjectId: ["time_project_id", "timeProjectId"],
8
+ isBillable: ["is_billable", "isBillable"],
9
+ rateOverrideAmount: ["rate_override_amount", "rateOverrideAmount"],
10
+ rateCurrencyCode: ["rate_currency_code", "rateCurrencyCode"],
11
+ lockedReportId: ["locked_report_id", "lockedReportId"]
12
+ };
13
+ function readValue(row, field) {
14
+ const snake = row[field[0]];
15
+ if (snake !== void 0) return snake;
16
+ return row[field[1]];
17
+ }
18
+ function toNullableString(value) {
19
+ return typeof value === "string" && value.length > 0 ? value : null;
20
+ }
21
+ function toNullableNumber(value) {
22
+ if (value === null || value === void 0) return null;
23
+ const parsed = typeof value === "number" ? value : Number(value);
24
+ return Number.isFinite(parsed) ? parsed : null;
25
+ }
26
+ function toBoolean(value, fallback) {
27
+ if (typeof value === "boolean") return value;
28
+ if (value === "true" || value === 1) return true;
29
+ if (value === "false" || value === 0) return false;
30
+ return fallback;
31
+ }
32
+ async function loadEntryTags(em, entryIds, tenantId, organizationId) {
33
+ const byEntryId = /* @__PURE__ */ new Map();
34
+ if (entryIds.length === 0) return byEntryId;
35
+ const assignments = await em.find(StaffTimeEntryTag, {
36
+ timeEntryId: { $in: entryIds },
37
+ tenantId,
38
+ organizationId
39
+ });
40
+ if (assignments.length === 0) return byEntryId;
41
+ const tags = await em.find(StaffTimeTag, {
42
+ id: { $in: Array.from(new Set(assignments.map((row) => row.tagId))) },
43
+ tenantId,
44
+ organizationId,
45
+ deletedAt: null
46
+ });
47
+ const tagById = new Map(tags.map((tag) => [tag.id, tag]));
48
+ for (const assignment of assignments) {
49
+ const tag = tagById.get(assignment.tagId);
50
+ if (!tag) continue;
51
+ const list = byEntryId.get(assignment.timeEntryId) ?? [];
52
+ list.push({ id: tag.id, slug: tag.slug, label: tag.label, color: tag.color ?? null });
53
+ byEntryId.set(assignment.timeEntryId, list);
54
+ }
55
+ return byEntryId;
56
+ }
57
+ async function loadEntryMoney(em, entryIds, tenantId, organizationId) {
58
+ const byId = /* @__PURE__ */ new Map();
59
+ if (entryIds.length === 0) return byId;
60
+ const entries = await em.find(StaffTimeEntry, {
61
+ id: { $in: entryIds },
62
+ tenantId,
63
+ organizationId
64
+ });
65
+ for (const entry of entries) {
66
+ byId.set(entry.id, {
67
+ rateOverrideAmount: entry.rateOverrideAmount ?? null,
68
+ rateCurrencyCode: entry.rateCurrencyCode ?? null
69
+ });
70
+ }
71
+ return byId;
72
+ }
73
+ async function loadProjectMoney(em, projectIds, tenantId, organizationId) {
74
+ const byId = /* @__PURE__ */ new Map();
75
+ if (projectIds.length === 0) return byId;
76
+ const projects = await em.find(StaffTimeProject, {
77
+ id: { $in: projectIds },
78
+ tenantId,
79
+ organizationId,
80
+ deletedAt: null
81
+ });
82
+ for (const project of projects) {
83
+ byId.set(project.id, {
84
+ hourlyRate: toNullableNumber(project.hourlyRate),
85
+ currencyCode: toNullableString(project.currencyCode)
86
+ });
87
+ }
88
+ return byId;
89
+ }
90
+ async function decorateTimeEntryRows(rows, scope) {
91
+ if (rows.length === 0) return;
92
+ const { tenantId, organizationId, canSeeRates } = scope;
93
+ let tagsByEntryId = /* @__PURE__ */ new Map();
94
+ let moneyByEntryId = /* @__PURE__ */ new Map();
95
+ let moneyByProjectId = /* @__PURE__ */ new Map();
96
+ if (tenantId && organizationId) {
97
+ try {
98
+ const em = scope.em.fork();
99
+ const entryIds = rows.map((row) => readValue(row, FIELD.id)).filter((value) => typeof value === "string");
100
+ tagsByEntryId = await loadEntryTags(em, entryIds, tenantId, organizationId);
101
+ if (canSeeRates) {
102
+ moneyByEntryId = await loadEntryMoney(em, entryIds, tenantId, organizationId);
103
+ const projectIds = Array.from(
104
+ new Set(
105
+ rows.map((row) => readValue(row, FIELD.timeProjectId)).filter((value) => typeof value === "string")
106
+ )
107
+ );
108
+ moneyByProjectId = await loadProjectMoney(em, projectIds, tenantId, organizationId);
109
+ }
110
+ } catch (err) {
111
+ scope.onError?.(err);
112
+ }
113
+ }
114
+ for (const row of rows) {
115
+ const entryId = readValue(row, FIELD.id);
116
+ const lockedReportId = toNullableString(readValue(row, FIELD.lockedReportId));
117
+ const roundedMinutes = toNullableNumber(readValue(row, FIELD.roundedMinutes));
118
+ const isBillable = toBoolean(readValue(row, FIELD.isBillable), true);
119
+ row.description = toNullableString(readValue(row, FIELD.notes));
120
+ row.roundedMinutes = roundedMinutes;
121
+ row.isLocked = lockedReportId !== null;
122
+ row.lockedReportId = lockedReportId;
123
+ row.tags = typeof entryId === "string" ? tagsByEntryId.get(entryId) ?? [] : [];
124
+ if (!canSeeRates) {
125
+ delete row[FIELD.rateOverrideAmount[0]];
126
+ delete row[FIELD.rateOverrideAmount[1]];
127
+ delete row[FIELD.rateCurrencyCode[0]];
128
+ delete row[FIELD.rateCurrencyCode[1]];
129
+ continue;
130
+ }
131
+ const projectId = readValue(row, FIELD.timeProjectId);
132
+ const money = typeof projectId === "string" ? moneyByProjectId.get(projectId) : void 0;
133
+ const stored = typeof entryId === "string" ? moneyByEntryId.get(entryId) : void 0;
134
+ const rateOverrideAmount = stored ? stored.rateOverrideAmount : readValue(row, FIELD.rateOverrideAmount) ?? null;
135
+ const rateCurrencyCode = stored ? stored.rateCurrencyCode : toNullableString(readValue(row, FIELD.rateCurrencyCode));
136
+ row[FIELD.rateOverrideAmount[0]] = rateOverrideAmount;
137
+ row[FIELD.rateCurrencyCode[0]] = rateCurrencyCode;
138
+ row.currencyCode = toNullableString(rateCurrencyCode) ?? money?.currencyCode ?? null;
139
+ row.cost = entryAmount(
140
+ {
141
+ isBillable,
142
+ roundedMinutes: roundedMinutes ?? 0,
143
+ rateOverrideAmount: toNullableNumber(rateOverrideAmount)
144
+ },
145
+ { hourlyRate: money?.hourlyRate ?? null }
146
+ );
147
+ }
148
+ }
149
+ export {
150
+ decorateTimeEntryRows
151
+ };
152
+ //# sourceMappingURL=timeEntryDecoration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/timesheets/timeEntryDecoration.ts"],
4
+ "sourcesContent": ["/**\n * The consulting-suite response fields on a time-entry row.\n *\n * This used to be a route-private `hooks.afterList` on\n * `/api/staff/timesheets/time-entries`, which made it invisible to the extension\n * system: a third-party enricher for `staff:staff_time_entry` could not see what\n * the hook had already added, and could not compose with it. The logic lives here so\n * the declared enricher in `data/enrichers.ts` owns it and the route keeps only its\n * deprecated wrapper.\n *\n * Money is ADDED for a caller holding `staff.timesheets.rates.view` and is absent\n * for everyone else \u2014 `cost`, `currencyCode` and the stored rate override are absent\n * from the payload rather than blanked, which is the module-wide rule\n * (`staff/AGENTS.md` \u2192 ACL feature IDs).\n *\n * **The addition is the whole gate, and that is deliberate.** The list route's\n * projection does not select `rate_override_amount` or `rate_currency_code` at all,\n * so an unentitled caller's response carries no money for the same reason a\n * `SELECT` that never named the column carries none. Stripping money after the fact\n * only holds while the stripper runs, and this decoration runs as a NON-critical\n * response enricher: `enricher-runner.ts` leaves the items untouched when one times\n * out or throws, so a subtractive gate here would fail OPEN under load. Reading the\n * two columns here, only for an entitled caller, is fail-closed by construction.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { StaffTimeEntry, StaffTimeEntryTag, StaffTimeProject, StaffTimeTag } from '../../data/entities'\nimport { entryAmount } from '../time-tracking/cost'\n\nexport type TimeEntryTagSummary = {\n id: string\n slug: string\n label: string\n color: string | null\n}\n\nexport type TimeEntryDecorationScope = {\n em: EntityManager\n tenantId: string | null\n organizationId: string | null\n canSeeRates: boolean\n /** Called with a decoration failure; the rows themselves are already correct and scoped. */\n onError?: (err: unknown) => void\n}\n\ntype EntryRow = Record<string, unknown>\n\nconst FIELD = {\n id: ['id', 'id'],\n roundedMinutes: ['rounded_minutes', 'roundedMinutes'],\n notes: ['notes', 'notes'],\n timeProjectId: ['time_project_id', 'timeProjectId'],\n isBillable: ['is_billable', 'isBillable'],\n rateOverrideAmount: ['rate_override_amount', 'rateOverrideAmount'],\n rateCurrencyCode: ['rate_currency_code', 'rateCurrencyCode'],\n lockedReportId: ['locked_report_id', 'lockedReportId'],\n} as const\n\nfunction readValue(row: EntryRow, field: readonly [string, string]): unknown {\n const snake = row[field[0]]\n if (snake !== undefined) return snake\n return row[field[1]]\n}\n\nfunction toNullableString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction toNullableNumber(value: unknown): number | null {\n if (value === null || value === undefined) return null\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction toBoolean(value: unknown, fallback: boolean): boolean {\n if (typeof value === 'boolean') return value\n if (value === 'true' || value === 1) return true\n if (value === 'false' || value === 0) return false\n return fallback\n}\n\nasync function loadEntryTags(\n em: EntityManager,\n entryIds: string[],\n tenantId: string,\n organizationId: string,\n): Promise<Map<string, TimeEntryTagSummary[]>> {\n const byEntryId = new Map<string, TimeEntryTagSummary[]>()\n if (entryIds.length === 0) return byEntryId\n const assignments = await em.find(StaffTimeEntryTag, {\n timeEntryId: { $in: entryIds },\n tenantId,\n organizationId,\n })\n if (assignments.length === 0) return byEntryId\n const tags = await em.find(StaffTimeTag, {\n id: { $in: Array.from(new Set(assignments.map((row) => row.tagId))) },\n tenantId,\n organizationId,\n deletedAt: null,\n })\n const tagById = new Map(tags.map((tag) => [tag.id, tag]))\n for (const assignment of assignments) {\n const tag = tagById.get(assignment.tagId)\n if (!tag) continue\n const list = byEntryId.get(assignment.timeEntryId) ?? []\n list.push({ id: tag.id, slug: tag.slug, label: tag.label, color: tag.color ?? null })\n byEntryId.set(assignment.timeEntryId, list)\n }\n return byEntryId\n}\n\n/**\n * The two money columns the list projection deliberately does not select, read back\n * only for a caller entitled to them. The rate override keeps the `numeric` string\n * MikroORM hands back, so the wire format of `rate_override_amount` is the one the\n * published response schema always documented.\n */\ntype EntryMoney = { rateOverrideAmount: string | number | null; rateCurrencyCode: string | null }\n\nasync function loadEntryMoney(\n em: EntityManager,\n entryIds: string[],\n tenantId: string,\n organizationId: string,\n): Promise<Map<string, EntryMoney>> {\n const byId = new Map<string, EntryMoney>()\n if (entryIds.length === 0) return byId\n const entries = await em.find(StaffTimeEntry, {\n id: { $in: entryIds },\n tenantId,\n organizationId,\n })\n for (const entry of entries) {\n byId.set(entry.id, {\n rateOverrideAmount: entry.rateOverrideAmount ?? null,\n rateCurrencyCode: entry.rateCurrencyCode ?? null,\n })\n }\n return byId\n}\n\ntype ProjectMoney = { hourlyRate: number | null; currencyCode: string | null }\n\nasync function loadProjectMoney(\n em: EntityManager,\n projectIds: string[],\n tenantId: string,\n organizationId: string,\n): Promise<Map<string, ProjectMoney>> {\n const byId = new Map<string, ProjectMoney>()\n if (projectIds.length === 0) return byId\n const projects = await em.find(StaffTimeProject, {\n id: { $in: projectIds },\n tenantId,\n organizationId,\n deletedAt: null,\n })\n for (const project of projects) {\n byId.set(project.id, {\n hourlyRate: toNullableNumber(project.hourlyRate),\n currencyCode: toNullableString(project.currencyCode),\n })\n }\n return byId\n}\n\n/**\n * Decorates the given rows IN PLACE. Callers that must not touch their input pass\n * shallow copies \u2014 which is what the enricher does, so its contract stays additive.\n */\nexport async function decorateTimeEntryRows(\n rows: EntryRow[],\n scope: TimeEntryDecorationScope,\n): Promise<void> {\n if (rows.length === 0) return\n\n const { tenantId, organizationId, canSeeRates } = scope\n let tagsByEntryId = new Map<string, TimeEntryTagSummary[]>()\n let moneyByEntryId = new Map<string, EntryMoney>()\n let moneyByProjectId = new Map<string, ProjectMoney>()\n\n if (tenantId && organizationId) {\n try {\n const em = scope.em.fork()\n const entryIds = rows\n .map((row) => readValue(row, FIELD.id))\n .filter((value): value is string => typeof value === 'string')\n tagsByEntryId = await loadEntryTags(em, entryIds, tenantId, organizationId)\n if (canSeeRates) {\n moneyByEntryId = await loadEntryMoney(em, entryIds, tenantId, organizationId)\n const projectIds = Array.from(\n new Set(\n rows\n .map((row) => readValue(row, FIELD.timeProjectId))\n .filter((value): value is string => typeof value === 'string'),\n ),\n )\n moneyByProjectId = await loadProjectMoney(em, projectIds, tenantId, organizationId)\n }\n } catch (err) {\n // A decoration failure must not fail the list; the entry rows themselves are\n // already correct and scoped.\n scope.onError?.(err)\n }\n }\n\n for (const row of rows) {\n const entryId = readValue(row, FIELD.id)\n const lockedReportId = toNullableString(readValue(row, FIELD.lockedReportId))\n const roundedMinutes = toNullableNumber(readValue(row, FIELD.roundedMinutes))\n const isBillable = toBoolean(readValue(row, FIELD.isBillable), true)\n\n // The published alias: same value, second name, both keys returned.\n row.description = toNullableString(readValue(row, FIELD.notes))\n row.roundedMinutes = roundedMinutes\n row.isLocked = lockedReportId !== null\n row.lockedReportId = lockedReportId\n row.tags = typeof entryId === 'string' ? tagsByEntryId.get(entryId) ?? [] : []\n\n if (!canSeeRates) {\n // Belt and braces: the projection selects neither column, so there is\n // normally nothing here to remove. A caller that assembled the row itself\n // still loses the money keys rather than passing them through.\n delete row[FIELD.rateOverrideAmount[0]]\n delete row[FIELD.rateOverrideAmount[1]]\n delete row[FIELD.rateCurrencyCode[0]]\n delete row[FIELD.rateCurrencyCode[1]]\n continue\n }\n\n const projectId = readValue(row, FIELD.timeProjectId)\n const money = typeof projectId === 'string' ? moneyByProjectId.get(projectId) : undefined\n const stored = typeof entryId === 'string' ? moneyByEntryId.get(entryId) : undefined\n const rateOverrideAmount = stored\n ? stored.rateOverrideAmount\n : (readValue(row, FIELD.rateOverrideAmount) as string | number | null | undefined) ?? null\n const rateCurrencyCode = stored\n ? stored.rateCurrencyCode\n : toNullableString(readValue(row, FIELD.rateCurrencyCode))\n\n row[FIELD.rateOverrideAmount[0]] = rateOverrideAmount\n row[FIELD.rateCurrencyCode[0]] = rateCurrencyCode\n row.currencyCode = toNullableString(rateCurrencyCode) ?? money?.currencyCode ?? null\n // A non-billable entry has no price at all \u2014 `null`, never `0`, which would\n // read as free work rather than work that is out of scope.\n row.cost = entryAmount(\n {\n isBillable,\n roundedMinutes: roundedMinutes ?? 0,\n rateOverrideAmount: toNullableNumber(rateOverrideAmount),\n },\n { hourlyRate: money?.hourlyRate ?? null },\n )\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,36 @@
1
+ const DEFAULT_BUDGET_WARN_AT_PERCENT = 80;
2
+ function clampPercent(value) {
3
+ if (!Number.isFinite(value) || value < 0) return 0;
4
+ return Math.min(100, value);
5
+ }
6
+ function resolveBudgetWarnAtPercent(warnAtPercent) {
7
+ if (typeof warnAtPercent !== "number" || !Number.isFinite(warnAtPercent)) {
8
+ return DEFAULT_BUDGET_WARN_AT_PERCENT;
9
+ }
10
+ if (warnAtPercent <= 0 || warnAtPercent > 100) return DEFAULT_BUDGET_WARN_AT_PERCENT;
11
+ return warnAtPercent;
12
+ }
13
+ function computeBudgetBurn(input) {
14
+ const kind = input.kind;
15
+ if (kind !== "hours" && kind !== "amount") return null;
16
+ const budgetValue = typeof input.budgetValue === "number" ? input.budgetValue : null;
17
+ if (budgetValue === null || !Number.isFinite(budgetValue) || budgetValue <= 0) return null;
18
+ const usedValue = kind === "hours" ? Math.max(0, input.totalMinutes) / 60 : Math.max(0, input.cost ?? 0);
19
+ const percent = Math.round(usedValue / budgetValue * 100);
20
+ const warnAt = resolveBudgetWarnAtPercent(input.warnAtPercent);
21
+ const tone = percent >= 100 ? "destructive" : percent >= warnAt ? "warning" : "accent";
22
+ return {
23
+ kind,
24
+ budgetValue,
25
+ usedValue,
26
+ percent,
27
+ barPercent: clampPercent(percent),
28
+ tone
29
+ };
30
+ }
31
+ export {
32
+ DEFAULT_BUDGET_WARN_AT_PERCENT,
33
+ computeBudgetBurn,
34
+ resolveBudgetWarnAtPercent
35
+ };
36
+ //# sourceMappingURL=budgetBurn.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/timesheets-projects/budgetBurn.ts"],
4
+ "sourcesContent": ["export type ProjectBudgetKind = 'none' | 'hours' | 'amount'\n\nexport type BudgetBurnTone = 'accent' | 'warning' | 'destructive'\n\nexport type ProjectBudgetBurn = {\n kind: 'hours' | 'amount'\n budgetValue: number\n usedValue: number\n percent: number\n barPercent: number\n tone: BudgetBurnTone\n}\n\nexport type BudgetBurnInput = {\n kind: ProjectBudgetKind | null | undefined\n budgetValue: number | null | undefined\n warnAtPercent: number | null | undefined\n totalMinutes: number\n cost: number | null\n}\n\nexport const DEFAULT_BUDGET_WARN_AT_PERCENT = 80\n\nfunction clampPercent(value: number): number {\n if (!Number.isFinite(value) || value < 0) return 0\n return Math.min(100, value)\n}\n\nexport function resolveBudgetWarnAtPercent(warnAtPercent: number | null | undefined): number {\n if (typeof warnAtPercent !== 'number' || !Number.isFinite(warnAtPercent)) {\n return DEFAULT_BUDGET_WARN_AT_PERCENT\n }\n if (warnAtPercent <= 0 || warnAtPercent > 100) return DEFAULT_BUDGET_WARN_AT_PERCENT\n return warnAtPercent\n}\n\nexport function computeBudgetBurn(input: BudgetBurnInput): ProjectBudgetBurn | null {\n const kind = input.kind\n if (kind !== 'hours' && kind !== 'amount') return null\n const budgetValue = typeof input.budgetValue === 'number' ? input.budgetValue : null\n if (budgetValue === null || !Number.isFinite(budgetValue) || budgetValue <= 0) return null\n\n const usedValue =\n kind === 'hours'\n ? Math.max(0, input.totalMinutes) / 60\n : Math.max(0, input.cost ?? 0)\n\n const percent = Math.round((usedValue / budgetValue) * 100)\n const warnAt = resolveBudgetWarnAtPercent(input.warnAtPercent)\n const tone: BudgetBurnTone = percent >= 100 ? 'destructive' : percent >= warnAt ? 'warning' : 'accent'\n\n return {\n kind,\n budgetValue,\n usedValue,\n percent,\n barPercent: clampPercent(percent),\n tone,\n }\n}\n"],
5
+ "mappings": "AAqBO,MAAM,iCAAiC;AAE9C,SAAS,aAAa,OAAuB;AAC3C,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,EAAG,QAAO;AACjD,SAAO,KAAK,IAAI,KAAK,KAAK;AAC5B;AAEO,SAAS,2BAA2B,eAAkD;AAC3F,MAAI,OAAO,kBAAkB,YAAY,CAAC,OAAO,SAAS,aAAa,GAAG;AACxE,WAAO;AAAA,EACT;AACA,MAAI,iBAAiB,KAAK,gBAAgB,IAAK,QAAO;AACtD,SAAO;AACT;AAEO,SAAS,kBAAkB,OAAkD;AAClF,QAAM,OAAO,MAAM;AACnB,MAAI,SAAS,WAAW,SAAS,SAAU,QAAO;AAClD,QAAM,cAAc,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc;AAChF,MAAI,gBAAgB,QAAQ,CAAC,OAAO,SAAS,WAAW,KAAK,eAAe,EAAG,QAAO;AAEtF,QAAM,YACJ,SAAS,UACL,KAAK,IAAI,GAAG,MAAM,YAAY,IAAI,KAClC,KAAK,IAAI,GAAG,MAAM,QAAQ,CAAC;AAEjC,QAAM,UAAU,KAAK,MAAO,YAAY,cAAe,GAAG;AAC1D,QAAM,SAAS,2BAA2B,MAAM,aAAa;AAC7D,QAAM,OAAuB,WAAW,MAAM,gBAAgB,WAAW,SAAS,YAAY;AAE9F,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,aAAa,OAAO;AAAA,IAChC;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ computeBudgetBurn,
3
+ resolveBudgetWarnAtPercent
4
+ } from "./budgetBurn.js";
5
+ const FULL_BUDGET_PERCENT = 100;
6
+ function normalizeAlertedAtPercent(value) {
7
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
8
+ const rounded = Math.round(value);
9
+ if (rounded <= 0) return null;
10
+ return Math.min(rounded, FULL_BUDGET_PERCENT);
11
+ }
12
+ function hasBillingCurrency(currencyCode) {
13
+ return typeof currencyCode === "string" && currencyCode.trim().length > 0;
14
+ }
15
+ function evaluateBudgetThreshold(input) {
16
+ const alertedAtPercent = normalizeAlertedAtPercent(input.budgetAlertedAtPercent);
17
+ const burn = computeBudgetBurn({
18
+ kind: input.budgetKind,
19
+ budgetValue: input.budgetValue,
20
+ warnAtPercent: input.budgetWarnAtPercent,
21
+ totalMinutes: input.totalMinutes,
22
+ cost: input.cost
23
+ });
24
+ if (!burn) {
25
+ return {
26
+ status: "disabled",
27
+ kind: null,
28
+ budgetValue: null,
29
+ usedValue: null,
30
+ percent: null,
31
+ crossedThresholdPercent: null,
32
+ nextAlertedAtPercent: null,
33
+ shouldPersistAlertedAt: alertedAtPercent !== null
34
+ };
35
+ }
36
+ if (burn.kind === "amount" && (input.cost === null || input.cost === void 0 || !hasBillingCurrency(input.currencyCode))) {
37
+ return {
38
+ status: "unmeasurable",
39
+ kind: burn.kind,
40
+ budgetValue: burn.budgetValue,
41
+ usedValue: null,
42
+ percent: null,
43
+ crossedThresholdPercent: null,
44
+ nextAlertedAtPercent: alertedAtPercent,
45
+ shouldPersistAlertedAt: false
46
+ };
47
+ }
48
+ const warnAtPercent = resolveBudgetWarnAtPercent(input.budgetWarnAtPercent);
49
+ const reachedThresholdPercent = burn.percent >= FULL_BUDGET_PERCENT ? FULL_BUDGET_PERCENT : burn.percent >= warnAtPercent ? warnAtPercent : null;
50
+ const crossedThresholdPercent = reachedThresholdPercent !== null && (alertedAtPercent === null || reachedThresholdPercent > alertedAtPercent) ? reachedThresholdPercent : null;
51
+ return {
52
+ status: "evaluated",
53
+ kind: burn.kind,
54
+ budgetValue: burn.budgetValue,
55
+ usedValue: burn.usedValue,
56
+ percent: burn.percent,
57
+ crossedThresholdPercent,
58
+ nextAlertedAtPercent: reachedThresholdPercent,
59
+ shouldPersistAlertedAt: reachedThresholdPercent !== alertedAtPercent
60
+ };
61
+ }
62
+ export {
63
+ FULL_BUDGET_PERCENT,
64
+ evaluateBudgetThreshold
65
+ };
66
+ //# sourceMappingURL=budgetThreshold.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/timesheets-projects/budgetThreshold.ts"],
4
+ "sourcesContent": ["import {\n computeBudgetBurn,\n resolveBudgetWarnAtPercent,\n type ProjectBudgetKind,\n} from './budgetBurn'\n\nexport const FULL_BUDGET_PERCENT = 100\n\n/**\n * Outcome of comparing a project's usage against its budget.\n *\n * - `disabled` \u2014 the project has no measurable budget at all (`budget_kind: 'none'`,\n * a null budget, or a budget of zero or less). Never notifies, and clears any\n * marker left behind by a budget that has since been switched off.\n * - `unmeasurable` \u2014 the budget is an amount budget but the project cannot price its\n * hours (no rate anywhere, or no currency). Degrades quietly: no notification and\n * the marker is left untouched, because this is an inability to measure rather\n * than usage genuinely dropping.\n * - `evaluated` \u2014 usage was compared against the thresholds.\n */\nexport type BudgetThresholdStatus = 'disabled' | 'unmeasurable' | 'evaluated'\n\nexport type BudgetThresholdInput = {\n budgetKind: ProjectBudgetKind | null | undefined\n budgetValue: number | null | undefined\n budgetWarnAtPercent: number | null | undefined\n budgetAlertedAtPercent: number | null | undefined\n totalMinutes: number\n cost: number | null\n currencyCode?: string | null\n}\n\nexport type BudgetThresholdEvaluation = {\n status: BudgetThresholdStatus\n kind: 'hours' | 'amount' | null\n budgetValue: number | null\n usedValue: number | null\n percent: number | null\n /** The threshold to announce now, or null when nothing new was crossed. */\n crossedThresholdPercent: number | null\n /** The value `budget_alerted_at_percent` should hold after this write. */\n nextAlertedAtPercent: number | null\n shouldPersistAlertedAt: boolean\n}\n\nfunction normalizeAlertedAtPercent(value: number | null | undefined): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value)) return null\n const rounded = Math.round(value)\n if (rounded <= 0) return null\n return Math.min(rounded, FULL_BUDGET_PERCENT)\n}\n\nfunction hasBillingCurrency(currencyCode: string | null | undefined): boolean {\n return typeof currencyCode === 'string' && currencyCode.trim().length > 0\n}\n\n/**\n * Decides whether a budget threshold notification is due for a project.\n *\n * Two thresholds exist: the project's `budget_warn_at_percent` (80 by default) and\n * 100. A notification fires only on an UPWARD move into a threshold band that\n * `budget_alerted_at_percent` has not already recorded, so repeated writes at the\n * same usage level stay silent. When one write jumps past both thresholds, only the\n * highest one is announced \u2014 a single \"budget exhausted\" alert beats two alerts\n * describing the same write.\n *\n * The marker follows usage back down: when usage falls under the band it was raised\n * for (an entry deleted or corrected), the marker is lowered to the band usage is\n * actually in \u2014 null when it is under every threshold \u2014 so a later re-crossing\n * notifies again. Moving down never notifies.\n *\n * Percent is taken from `computeBudgetBurn` so the notification and the burn bar on\n * screens 3 and 4 can never disagree about whether the project is at 80%.\n */\nexport function evaluateBudgetThreshold(input: BudgetThresholdInput): BudgetThresholdEvaluation {\n const alertedAtPercent = normalizeAlertedAtPercent(input.budgetAlertedAtPercent)\n\n const burn = computeBudgetBurn({\n kind: input.budgetKind,\n budgetValue: input.budgetValue,\n warnAtPercent: input.budgetWarnAtPercent,\n totalMinutes: input.totalMinutes,\n cost: input.cost,\n })\n\n if (!burn) {\n return {\n status: 'disabled',\n kind: null,\n budgetValue: null,\n usedValue: null,\n percent: null,\n crossedThresholdPercent: null,\n nextAlertedAtPercent: null,\n shouldPersistAlertedAt: alertedAtPercent !== null,\n }\n }\n\n if (burn.kind === 'amount' && (input.cost === null || input.cost === undefined || !hasBillingCurrency(input.currencyCode))) {\n return {\n status: 'unmeasurable',\n kind: burn.kind,\n budgetValue: burn.budgetValue,\n usedValue: null,\n percent: null,\n crossedThresholdPercent: null,\n nextAlertedAtPercent: alertedAtPercent,\n shouldPersistAlertedAt: false,\n }\n }\n\n const warnAtPercent = resolveBudgetWarnAtPercent(input.budgetWarnAtPercent)\n const reachedThresholdPercent =\n burn.percent >= FULL_BUDGET_PERCENT\n ? FULL_BUDGET_PERCENT\n : burn.percent >= warnAtPercent\n ? warnAtPercent\n : null\n\n const crossedThresholdPercent =\n reachedThresholdPercent !== null && (alertedAtPercent === null || reachedThresholdPercent > alertedAtPercent)\n ? reachedThresholdPercent\n : null\n\n return {\n status: 'evaluated',\n kind: burn.kind,\n budgetValue: burn.budgetValue,\n usedValue: burn.usedValue,\n percent: burn.percent,\n crossedThresholdPercent,\n nextAlertedAtPercent: reachedThresholdPercent,\n shouldPersistAlertedAt: reachedThresholdPercent !== alertedAtPercent,\n }\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAEA,MAAM,sBAAsB;AAuCnC,SAAS,0BAA0B,OAAiD;AAClF,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,QAAM,UAAU,KAAK,MAAM,KAAK;AAChC,MAAI,WAAW,EAAG,QAAO;AACzB,SAAO,KAAK,IAAI,SAAS,mBAAmB;AAC9C;AAEA,SAAS,mBAAmB,cAAkD;AAC5E,SAAO,OAAO,iBAAiB,YAAY,aAAa,KAAK,EAAE,SAAS;AAC1E;AAoBO,SAAS,wBAAwB,OAAwD;AAC9F,QAAM,mBAAmB,0BAA0B,MAAM,sBAAsB;AAE/E,QAAM,OAAO,kBAAkB;AAAA,IAC7B,MAAM,MAAM;AAAA,IACZ,aAAa,MAAM;AAAA,IACnB,eAAe,MAAM;AAAA,IACrB,cAAc,MAAM;AAAA,IACpB,MAAM,MAAM;AAAA,EACd,CAAC;AAED,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW;AAAA,MACX,SAAS;AAAA,MACT,yBAAyB;AAAA,MACzB,sBAAsB;AAAA,MACtB,wBAAwB,qBAAqB;AAAA,IAC/C;AAAA,EACF;AAEA,MAAI,KAAK,SAAS,aAAa,MAAM,SAAS,QAAQ,MAAM,SAAS,UAAa,CAAC,mBAAmB,MAAM,YAAY,IAAI;AAC1H,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,WAAW;AAAA,MACX,SAAS;AAAA,MACT,yBAAyB;AAAA,MACzB,sBAAsB;AAAA,MACtB,wBAAwB;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,gBAAgB,2BAA2B,MAAM,mBAAmB;AAC1E,QAAM,0BACJ,KAAK,WAAW,sBACZ,sBACA,KAAK,WAAW,gBACd,gBACA;AAER,QAAM,0BACJ,4BAA4B,SAAS,qBAAqB,QAAQ,0BAA0B,oBACxF,0BACA;AAEN,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,WAAW,KAAK;AAAA,IAChB,SAAS,KAAK;AAAA,IACd;AAAA,IACA,sBAAsB;AAAA,IACtB,wBAAwB,4BAA4B;AAAA,EACtD;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,78 @@
1
+ const BUDGET_KINDS = ["none", "hours", "amount"];
2
+ function toNullableNumber(value) {
3
+ if (value === null || value === void 0) return null;
4
+ const parsed = typeof value === "number" ? value : Number(value);
5
+ return Number.isFinite(parsed) ? parsed : null;
6
+ }
7
+ function toBudgetKind(value) {
8
+ return BUDGET_KINDS.find((kind) => kind === value) ?? "none";
9
+ }
10
+ async function loadTimeProjectBudgetStateForEntry(scope) {
11
+ const sql = `
12
+ SELECT
13
+ project.id AS time_project_id,
14
+ project.name AS name,
15
+ project.owner_user_id AS owner_user_id,
16
+ project.budget_kind AS budget_kind,
17
+ project.budget_value AS budget_value,
18
+ project.budget_warn_at_percent AS budget_warn_at_percent,
19
+ project.budget_alerted_at_percent AS budget_alerted_at_percent,
20
+ project.hourly_rate AS hourly_rate,
21
+ project.currency_code AS currency_code
22
+ FROM staff_time_entries entry
23
+ JOIN staff_time_projects project
24
+ ON project.id = entry.time_project_id
25
+ AND project.tenant_id = entry.tenant_id
26
+ AND project.organization_id = entry.organization_id
27
+ AND project.deleted_at IS NULL
28
+ WHERE entry.id = ?
29
+ AND entry.tenant_id = ?
30
+ AND entry.organization_id = ?
31
+ LIMIT 1
32
+ `;
33
+ const result = await scope.em.getConnection().execute(sql, [
34
+ scope.timeEntryId,
35
+ scope.tenantId,
36
+ scope.organizationId
37
+ ]);
38
+ const rows = Array.isArray(result) ? result : [];
39
+ const row = rows[0];
40
+ if (!row) return null;
41
+ return {
42
+ timeProjectId: row.time_project_id,
43
+ name: row.name ?? "",
44
+ ownerUserId: row.owner_user_id ?? null,
45
+ budgetKind: toBudgetKind(row.budget_kind),
46
+ budgetValue: toNullableNumber(row.budget_value),
47
+ budgetWarnAtPercent: toNullableNumber(row.budget_warn_at_percent),
48
+ budgetAlertedAtPercent: toNullableNumber(row.budget_alerted_at_percent),
49
+ hourlyRate: toNullableNumber(row.hourly_rate),
50
+ currencyCode: row.currency_code ?? null
51
+ };
52
+ }
53
+ async function claimBudgetThresholdAlert(scope) {
54
+ const sql = `
55
+ UPDATE staff_time_projects
56
+ SET budget_alerted_at_percent = ?
57
+ WHERE id = ?
58
+ AND tenant_id = ?
59
+ AND organization_id = ?
60
+ AND deleted_at IS NULL
61
+ AND budget_alerted_at_percent IS NOT DISTINCT FROM ?
62
+ RETURNING id
63
+ `;
64
+ const result = await scope.em.getConnection().execute(sql, [
65
+ scope.nextAlertedAtPercent,
66
+ scope.timeProjectId,
67
+ scope.tenantId,
68
+ scope.organizationId,
69
+ scope.expectedAlertedAtPercent
70
+ ]);
71
+ const rows = Array.isArray(result) ? result : [];
72
+ return rows.length > 0;
73
+ }
74
+ export {
75
+ claimBudgetThresholdAlert,
76
+ loadTimeProjectBudgetStateForEntry
77
+ };
78
+ //# sourceMappingURL=budgetThresholdState.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/timesheets-projects/budgetThresholdState.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ProjectBudgetKind } from './budgetBurn'\n\nexport type TimeProjectBudgetState = {\n timeProjectId: string\n name: string\n ownerUserId: string | null\n budgetKind: ProjectBudgetKind\n budgetValue: number | null\n budgetWarnAtPercent: number | null\n budgetAlertedAtPercent: number | null\n hourlyRate: number | null\n currencyCode: string | null\n}\n\nexport type TimeProjectBudgetScope = {\n em: EntityManager\n tenantId: string\n organizationId: string\n}\n\ntype BudgetStateRow = {\n time_project_id: string\n name: string | null\n owner_user_id: string | null\n budget_kind: string | null\n budget_value: string | number | null\n budget_warn_at_percent: string | number | null\n budget_alerted_at_percent: string | number | null\n hourly_rate: string | number | null\n currency_code: string | null\n}\n\nconst BUDGET_KINDS: readonly ProjectBudgetKind[] = ['none', 'hours', 'amount']\n\nfunction toNullableNumber(value: string | number | null | undefined): number | null {\n if (value === null || value === undefined) return null\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction toBudgetKind(value: string | null | undefined): ProjectBudgetKind {\n return BUDGET_KINDS.find((kind) => kind === value) ?? 'none'\n}\n\n/**\n * Loads the budget state of the project a time entry belongs to, in one scoped\n * round trip. The entry is looked up without a `deleted_at` filter on purpose: a\n * delete is exactly the write that can push usage back under a threshold, and the\n * soft-deleted row is still what tells us which project to re-evaluate.\n */\nexport async function loadTimeProjectBudgetStateForEntry(\n scope: TimeProjectBudgetScope & { timeEntryId: string },\n): Promise<TimeProjectBudgetState | null> {\n const sql = `\n SELECT\n project.id AS time_project_id,\n project.name AS name,\n project.owner_user_id AS owner_user_id,\n project.budget_kind AS budget_kind,\n project.budget_value AS budget_value,\n project.budget_warn_at_percent AS budget_warn_at_percent,\n project.budget_alerted_at_percent AS budget_alerted_at_percent,\n project.hourly_rate AS hourly_rate,\n project.currency_code AS currency_code\n FROM staff_time_entries entry\n JOIN staff_time_projects project\n ON project.id = entry.time_project_id\n AND project.tenant_id = entry.tenant_id\n AND project.organization_id = entry.organization_id\n AND project.deleted_at IS NULL\n WHERE entry.id = ?\n AND entry.tenant_id = ?\n AND entry.organization_id = ?\n LIMIT 1\n `\n\n const result = await scope.em.getConnection().execute(sql, [\n scope.timeEntryId,\n scope.tenantId,\n scope.organizationId,\n ])\n const rows = (Array.isArray(result) ? result : []) as BudgetStateRow[]\n const row = rows[0]\n if (!row) return null\n\n return {\n timeProjectId: row.time_project_id,\n name: row.name ?? '',\n ownerUserId: row.owner_user_id ?? null,\n budgetKind: toBudgetKind(row.budget_kind),\n budgetValue: toNullableNumber(row.budget_value),\n budgetWarnAtPercent: toNullableNumber(row.budget_warn_at_percent),\n budgetAlertedAtPercent: toNullableNumber(row.budget_alerted_at_percent),\n hourlyRate: toNullableNumber(row.hourly_rate),\n currencyCode: row.currency_code ?? null,\n }\n}\n\n/**\n * Compare-and-swap on `budget_alerted_at_percent`, the column that makes the budget\n * notification idempotent. Returns true only for the caller that actually moved the\n * marker, so two concurrent entry writes crossing the same threshold produce one\n * notification rather than two.\n *\n * `updated_at` is deliberately left alone: the marker is platform bookkeeping, not a\n * user edit, and bumping the optimistic-lock version here would make an unrelated\n * open project form fail to save with a spurious 409.\n */\nexport async function claimBudgetThresholdAlert(\n scope: TimeProjectBudgetScope & {\n timeProjectId: string\n expectedAlertedAtPercent: number | null\n nextAlertedAtPercent: number | null\n },\n): Promise<boolean> {\n const sql = `\n UPDATE staff_time_projects\n SET budget_alerted_at_percent = ?\n WHERE id = ?\n AND tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND budget_alerted_at_percent IS NOT DISTINCT FROM ?\n RETURNING id\n `\n\n const result = await scope.em.getConnection().execute(sql, [\n scope.nextAlertedAtPercent,\n scope.timeProjectId,\n scope.tenantId,\n scope.organizationId,\n scope.expectedAlertedAtPercent,\n ])\n const rows = (Array.isArray(result) ? result : []) as Array<{ id: string }>\n return rows.length > 0\n}\n"],
5
+ "mappings": "AAiCA,MAAM,eAA6C,CAAC,QAAQ,SAAS,QAAQ;AAE7E,SAAS,iBAAiB,OAA0D;AAClF,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAM,SAAS,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAC/D,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAEA,SAAS,aAAa,OAAqD;AACzE,SAAO,aAAa,KAAK,CAAC,SAAS,SAAS,KAAK,KAAK;AACxD;AAQA,eAAsB,mCACpB,OACwC;AACxC,QAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBZ,QAAM,SAAS,MAAM,MAAM,GAAG,cAAc,EAAE,QAAQ,KAAK;AAAA,IACzD,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AACD,QAAM,OAAQ,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC;AAChD,QAAM,MAAM,KAAK,CAAC;AAClB,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO;AAAA,IACL,eAAe,IAAI;AAAA,IACnB,MAAM,IAAI,QAAQ;AAAA,IAClB,aAAa,IAAI,iBAAiB;AAAA,IAClC,YAAY,aAAa,IAAI,WAAW;AAAA,IACxC,aAAa,iBAAiB,IAAI,YAAY;AAAA,IAC9C,qBAAqB,iBAAiB,IAAI,sBAAsB;AAAA,IAChE,wBAAwB,iBAAiB,IAAI,yBAAyB;AAAA,IACtE,YAAY,iBAAiB,IAAI,WAAW;AAAA,IAC5C,cAAc,IAAI,iBAAiB;AAAA,EACrC;AACF;AAYA,eAAsB,0BACpB,OAKkB;AAClB,QAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWZ,QAAM,SAAS,MAAM,MAAM,GAAG,cAAc,EAAE,QAAQ,KAAK;AAAA,IACzD,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AACD,QAAM,OAAQ,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC;AAChD,SAAO,KAAK,SAAS;AACvB;",
6
+ "names": []
7
+ }