@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,1229 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Screen 10 — the working list of time entries.
5
+ *
6
+ * The five notes on the mockup are the five decisions this page exists to make,
7
+ * and none of them is decoration:
8
+ *
9
+ * 1. **The `Czas` cell edits in place** (US-D5). Correcting a duration is the
10
+ * single most common thing anybody does to a logged entry, so it costs one
11
+ * click, not a dialog round trip. The field is the shared `DurationInput`
12
+ * in its compact variant, so `1h 40m` means here exactly what it means in
13
+ * the entry dialog and the timesheet grid.
14
+ * 2. **Delete is optimistic and reversible** (US-D5). The row leaves the table
15
+ * immediately and the platform's undo bar — fed by the `x-om-operation`
16
+ * header the command layer already attaches to the response — offers
17
+ * "Cofnij" for a few seconds. The restore is the command's own `undo`,
18
+ * never a row rebuilt from client state, which is why an entry restored
19
+ * this way comes back with its tags, its rounding and its audit trail
20
+ * intact. The page watches the operation store and refetches the moment its
21
+ * token is undone, so the row reappears from the server.
22
+ * 3. **A rate override is visible on the row** (US-F2). Without the badge two
23
+ * rows with the same duration and different costs look like a bug.
24
+ * 4. **A locked row is inert** (US-G3): no checkbox, no edit, no delete, a
25
+ * lock badge, and exactly one action — the report that froze it. Unlocking
26
+ * happens in the report (screen 15), not here.
27
+ * 5. **`Koszt` is empty for a non-billable row, not `0,00`.** The list route
28
+ * already answers `null`; a zero would read as free work rather than as
29
+ * work that is out of scope.
30
+ *
31
+ * The page is also the landing spot for two deep links — the task drawer's
32
+ * "show all entries" (`?taskId=`) and the entry dialog's jump to an overlapping
33
+ * entry (`?ids=`). Either one replaces the default "my entries, this week"
34
+ * seeding and shows up as a removable chip, because a list that quietly ignored
35
+ * the link would answer a question nobody asked.
36
+ *
37
+ * Two things beyond the notes. Money columns exist only for a holder of
38
+ * `staff.timesheets.rates.view` — the response has no `cost` for anybody else,
39
+ * so the column is absent rather than blanked. And the footer never adds two
40
+ * currencies together; see `summarizeTimeEntries`.
41
+ */
42
+
43
+ import { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'
44
+ import * as React from 'react'
45
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
46
+ import { Copy, Lock, Plus } from 'lucide-react'
47
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
48
+ import type { SortingState } from '@tanstack/react-table'
49
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
50
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
51
+ import { RowActions } from '@open-mercato/ui/backend/RowActions'
52
+ import { Button } from '@open-mercato/ui/primitives/button'
53
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
54
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
55
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterOverlay'
56
+ import {
57
+ EntryFilterChips,
58
+ EntryFilterPresets,
59
+ matchActivePreset,
60
+ type AppliedFilterChip,
61
+ type EntryFilterPreset,
62
+ } from '../../../../lib/time-tracking-ui/EntryFilterBar'
63
+ import {
64
+ apiCall,
65
+ apiCallOrThrow,
66
+ readApiResultOrThrow,
67
+ withScopedApiRequestHeaders,
68
+ } from '@open-mercato/ui/backend/utils/apiCall'
69
+ import { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
70
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
71
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
72
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
73
+ import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
74
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
75
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
76
+ import { useOperationStore } from '@open-mercato/ui/backend/operations/store'
77
+ import { formatCurrency } from '@open-mercato/ui/utils/format'
78
+ import { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
79
+ import { hasFeature } from '@open-mercato/shared/security/features'
80
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
81
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
82
+ import { createLogger } from '@open-mercato/shared/lib/logger'
83
+ import { TimeEntryDialog } from '../../../../lib/time-tracking-ui/TimeEntryDialog'
84
+ import { TimeEntryDurationCell } from '../../../../lib/time-tracking-ui/TimeEntryDurationCell'
85
+ import { TimeEntriesSummaryFooter } from '../../../../lib/time-tracking-ui/TimeEntriesSummaryFooter'
86
+ import {
87
+ collectDirectoryIds,
88
+ currentWeekRange,
89
+ formatEntryClockRange,
90
+ formatEntryDay,
91
+ isEntryLockedError,
92
+ readCopyDayTargetConflict,
93
+ summarizeTimeEntries,
94
+ toTimeEntryListRow,
95
+ type TimeEntryDirectory,
96
+ type TimeEntryListRow,
97
+ } from '../../../../lib/time-tracking-ui/timeEntryListData'
98
+ import {
99
+ readRowItems,
100
+ shiftIsoDate,
101
+ todayIsoDate,
102
+ toProjectOption,
103
+ toTaskOption,
104
+ } from '../../../../lib/time-tracking-ui/timeEntryDialogState'
105
+ import { formatDuration } from '../../../../lib/time-tracking/duration'
106
+
107
+ const logger = createLogger('staff').child({ component: 'time-tracking/entries' })
108
+
109
+ const PAGE_SIZE = 50
110
+ const DIRECTORY_PAGE_SIZE = 100
111
+ const ENTRIES_API_PATH = 'staff/timesheets/time-entries'
112
+ const ENTRIES_API = `/api/${ENTRIES_API_PATH}`
113
+ const RATES_FEATURE = 'staff.timesheets.rates.view'
114
+ const MANAGE_OWN_FEATURE = 'staff.timesheets.manage_own'
115
+ const MANAGE_ALL_FEATURE = 'staff.timesheets.manage_all'
116
+ /**
117
+ * Query parameters other screens use to hand this list a question: the task
118
+ * drawer's "show every entry for this task" (`?taskId=`) and the entry dialog's
119
+ * jump to a conflicting entry (`?ids=`). Both are read once on mount and become
120
+ * ordinary filter values, so they carry a removable chip like anything else.
121
+ */
122
+ const DEEP_LINK_PARAMS = ['taskId', 'ids'] as const
123
+ const REPORTS_PATH = '/backend/staff/time-tracking/reports'
124
+ const MUTATION_CONTEXT_ID = 'staff.time_tracking.entriesList'
125
+ const RESOURCE_KIND = 'staff.timesheets.time_entry'
126
+
127
+ type EntriesResponse = {
128
+ items?: Array<Record<string, unknown>>
129
+ total?: number
130
+ totalIsCapped?: boolean
131
+ totalPages?: number
132
+ }
133
+
134
+ type CopyDayResponse = {
135
+ copied?: number
136
+ toDate?: string
137
+ created?: Array<Record<string, unknown>>
138
+ skipped?: Array<Record<string, unknown>>
139
+ }
140
+
141
+ type MutationContext = {
142
+ formId: string
143
+ resourceKind: string
144
+ resourceId: string
145
+ retryLastMutation: () => Promise<boolean>
146
+ }
147
+
148
+ function readUndoToken(response: Response | null | undefined): string | null {
149
+ const header = response?.headers?.get?.('x-om-operation') ?? null
150
+ return deserializeOperationMetadata(header)?.undoToken ?? null
151
+ }
152
+
153
+ async function loadDirectory(
154
+ taskIds: readonly string[],
155
+ projectIds: readonly string[],
156
+ ): Promise<TimeEntryDirectory> {
157
+ const taskTitles = new Map<string, string>()
158
+ const projectLabels = new Map<string, string>()
159
+ const requests: Array<Promise<void>> = []
160
+
161
+ if (taskIds.length > 0) {
162
+ const params = new URLSearchParams({ page: '1', pageSize: String(DIRECTORY_PAGE_SIZE), ids: taskIds.join(',') })
163
+ requests.push(
164
+ readApiResultOrThrow<Record<string, unknown>>(`/api/staff/timesheets/tasks?${params.toString()}`, undefined, {
165
+ allowNullResult: true,
166
+ }).then((payload) => {
167
+ for (const row of readRowItems(payload)) {
168
+ const option = toTaskOption(row)
169
+ if (option) taskTitles.set(option.id, option.title)
170
+ }
171
+ }),
172
+ )
173
+ }
174
+
175
+ if (projectIds.length > 0) {
176
+ const params = new URLSearchParams({
177
+ page: '1',
178
+ pageSize: String(DIRECTORY_PAGE_SIZE),
179
+ ids: projectIds.join(','),
180
+ })
181
+ requests.push(
182
+ readApiResultOrThrow<Record<string, unknown>>(
183
+ `/api/staff/timesheets/time-projects?${params.toString()}`,
184
+ undefined,
185
+ { allowNullResult: true },
186
+ ).then((payload) => {
187
+ for (const row of readRowItems(payload)) {
188
+ const option = toProjectOption(row)
189
+ if (!option) continue
190
+ projectLabels.set(option.id, option.customerName ? `${option.customerName} — ${option.name}` : option.name)
191
+ }
192
+ }),
193
+ )
194
+ }
195
+
196
+ await Promise.all(requests)
197
+ return { taskTitles, projectLabels }
198
+ }
199
+
200
+ export default function TimeTrackingEntriesPage() {
201
+ const t = useT()
202
+ const router = useRouter()
203
+ const pathname = usePathname()
204
+ const searchParams = useSearchParams()
205
+ const scopeVersion = useOrganizationScopeVersion()
206
+ const { payload } = useBackendChrome()
207
+ const canSeeMoney = hasFeature(payload?.grantedFeatures, RATES_FEATURE)
208
+ const canManage = hasFeature(payload?.grantedFeatures, MANAGE_OWN_FEATURE)
209
+ // Reading somebody else's entries is `manage_all`, not `manage_own`: the list
210
+ // route narrows every other caller back to their own rows, so offering them a
211
+ // person filter promises a lookup the server will answer with nothing.
212
+ const canManageAll = hasFeature(payload?.grantedFeatures, MANAGE_ALL_FEATURE)
213
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
214
+
215
+ const deepLinkTaskId = searchParams?.get('taskId')?.trim() ?? ''
216
+ const deepLinkEntryIds = searchParams?.get('ids')?.trim() ?? ''
217
+
218
+ const defaultRange = React.useMemo(() => currentWeekRange(), [])
219
+ const [filterValues, setFilterValues] = React.useState<FilterValues>(() => {
220
+ // A deep link is the entire question the reader arrived with, so it replaces
221
+ // the default week instead of being intersected with it — an entry logged
222
+ // last month would otherwise be filtered straight back out of the list the
223
+ // link exists to show.
224
+ if (deepLinkTaskId) return { taskId: deepLinkTaskId }
225
+ if (deepLinkEntryIds) return { ids: deepLinkEntryIds }
226
+ return { period: { from: defaultRange.from, to: defaultRange.to } }
227
+ })
228
+ // The list opens on your own timesheet. Someone holding `manage_all` sees every
229
+ // entry in the organization otherwise, which is a page about other people.
230
+ const [selfStaffMemberId, setSelfStaffMemberId] = React.useState<string | null>(null)
231
+ const [projectOptions, setProjectOptions] = React.useState<{ value: string; label: string }[]>([])
232
+ const [customerOptions, setCustomerOptions] = React.useState<{ value: string; label: string }[]>([])
233
+ const [tagOptions, setTagOptions] = React.useState<{ value: string; label: string }[]>([])
234
+ const [peopleOptions, setPeopleOptions] = React.useState<{ value: string; label: string }[]>([])
235
+ // A deep link also outranks the "my entries" seeding the self lookup performs
236
+ // once it resolves; the entry being pointed at is frequently someone else's.
237
+ const personSeededRef = React.useRef(Boolean(deepLinkTaskId || deepLinkEntryIds))
238
+ const [page, setPage] = React.useState(1)
239
+ const [sorting, setSorting] = React.useState<SortingState>([{ id: 'date', desc: true }])
240
+ const [rows, setRows] = React.useState<TimeEntryListRow[]>([])
241
+ const [total, setTotal] = React.useState(0)
242
+ const [totalIsCapped, setTotalIsCapped] = React.useState(false)
243
+ const [totalPages, setTotalPages] = React.useState(1)
244
+ const [isLoading, setIsLoading] = React.useState(true)
245
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
246
+ const [reloadToken, setReloadToken] = React.useState(0)
247
+ const [selectedIds, setSelectedIds] = React.useState<string[]>([])
248
+ const [savingEntryId, setSavingEntryId] = React.useState<string | null>(null)
249
+ const [dialogOpen, setDialogOpen] = React.useState(false)
250
+ const [dialogEntryId, setDialogEntryId] = React.useState<string | null>(null)
251
+ const hasLoadedOnceRef = React.useRef(false)
252
+
253
+ const labels = React.useMemo(
254
+ () => ({
255
+ title: t('staff.time_tracking.entries.title', 'Time entries'),
256
+ add: t('staff.time_tracking.entries.actions.add', 'Add entry'),
257
+ copyDay: t('staff.time_tracking.entries.actions.copyDay', 'Copy yesterday'),
258
+ duplicate: t('staff.time_tracking.entries.actions.duplicate', 'Duplicate'),
259
+ delete: t('staff.time_tracking.entries.actions.delete', 'Delete'),
260
+ showReport: t('staff.time_tracking.entries.actions.showReport', 'Show the report'),
261
+ refresh: t('staff.time_tracking.entries.actions.refresh', 'Refresh'),
262
+ columns: {
263
+ date: t('staff.time_tracking.entries.columns.date', 'Date'),
264
+ task: t('staff.time_tracking.entries.columns.task', 'Task / description'),
265
+ project: t('staff.time_tracking.entries.columns.project', 'Project'),
266
+ clock: t('staff.time_tracking.entries.columns.clock', 'Hours'),
267
+ duration: t('staff.time_tracking.entries.columns.duration', 'Time'),
268
+ cost: t('staff.time_tracking.entries.columns.cost', 'Cost'),
269
+ },
270
+ selectAll: t('staff.time_tracking.entries.table.selectAll', 'Select all entries'),
271
+ selectRow: t('staff.time_tracking.entries.table.selectRow', 'Select entry'),
272
+ durationAria: t('staff.time_tracking.entries.table.durationAria', 'Entry duration'),
273
+ noTask: t('staff.time_tracking.entries.table.noTask', 'No task'),
274
+ empty: t('staff.time_tracking.entries.empty', 'No time entries in this period.'),
275
+ badges: {
276
+ locked: t('staff.time_tracking.entries.badges.locked', 'locked'),
277
+ nonBillable: t('staff.time_tracking.entries.badges.nonBillable', 'non-billable'),
278
+ },
279
+ filters: {
280
+ period: t('staff.time_tracking.entries.filters.period', 'Period'),
281
+ project: t('staff.time_tracking.entries.filters.project', 'Project'),
282
+ customer: t('staff.time_tracking.entries.filters.customer', 'Customer'),
283
+ person: t('staff.time_tracking.entries.filters.person', 'Person'),
284
+ tag: t('staff.time_tracking.entries.filters.tag', 'Tag'),
285
+ billable: t('staff.time_tracking.entries.filters.billable', 'Billable'),
286
+ billableYes: t('staff.time_tracking.entries.filters.billableYes', 'Billable only'),
287
+ billableNo: t('staff.time_tracking.entries.filters.billableNo', 'Non-billable only'),
288
+ locked: t('staff.time_tracking.entries.filters.locked', 'Report status'),
289
+ lockedYes: t('staff.time_tracking.entries.filters.lockedYes', 'Locked in a report'),
290
+ lockedNo: t('staff.time_tracking.entries.filters.lockedNo', 'Not yet reported'),
291
+ text: t('staff.time_tracking.entries.filters.text', 'Description contains'),
292
+ task: t('staff.time_tracking.entries.filters.task', 'Task'),
293
+ entry: t('staff.time_tracking.entries.filters.entry', 'Entry'),
294
+ entrySelection: t('staff.time_tracking.entries.filters.entrySelection', '{count} entries'),
295
+ clearAll: t('staff.time_tracking.entries.filters.clearAll', 'Clear all'),
296
+ remove: t('staff.time_tracking.entries.filters.remove', 'Remove filter {name}'),
297
+ },
298
+ presets: {
299
+ mineWeek: t('staff.time_tracking.entries.presets.mineWeek', 'Mine, this week'),
300
+ unbilled: t('staff.time_tracking.entries.presets.unbilled', 'Unbilled'),
301
+ nonBillable: t('staff.time_tracking.entries.presets.nonBillable', 'Non-billable'),
302
+ thisMonth: t('staff.time_tracking.entries.presets.thisMonth', 'This month'),
303
+ },
304
+ errors: {
305
+ load: t('staff.time_tracking.entries.errors.load', 'Could not load the time entries.'),
306
+ save: t('staff.time_tracking.entries.errors.save', 'Could not save the time entry.'),
307
+ delete: t('staff.time_tracking.entries.errors.delete', 'Could not delete the time entry.'),
308
+ duplicate: t('staff.time_tracking.entries.errors.duplicate', 'Could not duplicate the time entry.'),
309
+ copyDay: t('staff.time_tracking.entries.errors.copyDay', 'Could not copy the day.'),
310
+ locked: t(
311
+ 'staff.time_tracking.entries.errors.entryLocked',
312
+ 'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',
313
+ ),
314
+ },
315
+ }),
316
+ [t],
317
+ )
318
+
319
+ const handleRefresh = React.useCallback(() => {
320
+ setReloadToken((token) => token + 1)
321
+ }, [])
322
+
323
+ const reportFailure = React.useCallback(
324
+ (error: unknown, fallbackMessage: string, logTag: string) => {
325
+ if (surfaceRecordConflict(error, t)) return
326
+ if (isEntryLockedError(error)) {
327
+ flash(labels.errors.locked, 'error')
328
+ return
329
+ }
330
+ logger.error(logTag, { err: error })
331
+ flash(error instanceof Error && error.message ? error.message : fallbackMessage, 'error')
332
+ },
333
+ [labels.errors.locked, t],
334
+ )
335
+
336
+ const period = (filterValues.period ?? {}) as { from?: string; to?: string }
337
+ const projectFilter = typeof filterValues.projectId === 'string' ? filterValues.projectId : ''
338
+
339
+ // Filter vocabularies come from the API, not from the rows on screen: options
340
+ // built from the loaded page can only narrow to what you can already see, which
341
+ // makes the filter useless for finding anything that is not in front of you.
342
+ React.useEffect(() => {
343
+ let cancelled = false
344
+ void (async () => {
345
+ // `Promise.resolve` rather than `.catch` directly: a filter vocabulary that
346
+ // fails to load must leave the page usable with a shorter list, and the
347
+ // wrapper also tolerates a stub that returns nothing.
348
+ const safeCall = async <T,>(url: string) => {
349
+ try {
350
+ return (await Promise.resolve(apiCall<T>(url))) ?? null
351
+ } catch {
352
+ return null
353
+ }
354
+ }
355
+ const [selfRes, projectsRes, tagsRes, peopleRes] = await Promise.all([
356
+ safeCall<{ member?: { id?: string } | null }>('/api/staff/team-members/self'),
357
+ safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/time-projects?page=1&pageSize=100'),
358
+ safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/tags?page=1&pageSize=100'),
359
+ safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/team-members?page=1&pageSize=100'),
360
+ ])
361
+ if (cancelled) return
362
+
363
+ const selfId = selfRes?.ok ? (selfRes.result?.member?.id ?? null) : null
364
+ if (selfId) {
365
+ setSelfStaffMemberId(selfId)
366
+ if (!personSeededRef.current) {
367
+ personSeededRef.current = true
368
+ setFilterValues((prev) => ({ ...prev, staffMemberId: selfId }))
369
+ }
370
+ }
371
+
372
+ const projects = projectsRes?.ok && Array.isArray(projectsRes.result?.items) ? projectsRes.result.items : []
373
+ const customers = new Map<string, string>()
374
+ setProjectOptions(
375
+ projects
376
+ .map((row: Record<string, unknown>) => {
377
+ const id = typeof row.id === 'string' ? row.id : null
378
+ if (!id) return null
379
+ const name = typeof row.name === 'string' ? row.name : id
380
+ const code = typeof row.code === 'string' ? row.code : null
381
+ const customerId = typeof row.customer_id === 'string' ? row.customer_id : null
382
+ const snapshot = row.customer_snapshot as { name?: unknown } | null | undefined
383
+ if (customerId && typeof snapshot?.name === 'string') customers.set(customerId, snapshot.name)
384
+ return { value: id, label: code ? `${code} · ${name}` : name }
385
+ })
386
+ .filter((option): option is { value: string; label: string } => option !== null),
387
+ )
388
+ setCustomerOptions([...customers].map(([value, label]) => ({ value, label })))
389
+
390
+ const tags = tagsRes?.ok && Array.isArray(tagsRes.result?.items) ? tagsRes.result.items : []
391
+ setTagOptions(
392
+ tags
393
+ .map((row: Record<string, unknown>) => {
394
+ const id = typeof row.id === 'string' ? row.id : null
395
+ const label = typeof row.label === 'string' ? row.label : null
396
+ return id && label ? { value: id, label } : null
397
+ })
398
+ .filter((option): option is { value: string; label: string } => option !== null),
399
+ )
400
+
401
+ const people = peopleRes?.ok && Array.isArray(peopleRes.result?.items) ? peopleRes.result.items : []
402
+ setPeopleOptions(
403
+ people
404
+ .map((row: Record<string, unknown>) => {
405
+ const id = typeof row.id === 'string' ? row.id : null
406
+ const label = typeof row.display_name === 'string'
407
+ ? row.display_name
408
+ : typeof row.displayName === 'string'
409
+ ? row.displayName
410
+ : null
411
+ return id && label ? { value: id, label } : null
412
+ })
413
+ .filter((option): option is { value: string; label: string } => option !== null),
414
+ )
415
+ })()
416
+ return () => { cancelled = true }
417
+ }, [scopeVersion])
418
+
419
+ const presets = React.useMemo<EntryFilterPreset[]>(
420
+ () => [
421
+ {
422
+ id: 'mine-week',
423
+ label: labels.presets.mineWeek,
424
+ build: ({ staffMemberId }) => ({
425
+ period: { from: defaultRange.from, to: defaultRange.to },
426
+ ...(staffMemberId ? { staffMemberId } : {}),
427
+ }),
428
+ },
429
+ {
430
+ id: 'unbilled',
431
+ label: labels.presets.unbilled,
432
+ build: () => ({ locked: 'false', billable: 'true' }),
433
+ },
434
+ { id: 'non-billable', label: labels.presets.nonBillable, build: () => ({ billable: 'false' }) },
435
+ {
436
+ id: 'this-month',
437
+ label: labels.presets.thisMonth,
438
+ build: ({ today }) => {
439
+ const start = `${today.slice(0, 7)}-01`
440
+ return { period: { from: start, to: today } }
441
+ },
442
+ },
443
+ ],
444
+ [defaultRange.from, defaultRange.to, labels.presets],
445
+ )
446
+
447
+ const presetContext = React.useMemo(
448
+ () => ({ staffMemberId: selfStaffMemberId, today: new Date().toISOString().slice(0, 10) }),
449
+ [selfStaffMemberId],
450
+ )
451
+ const activePresetId = React.useMemo(
452
+ () => matchActivePreset(filterValues, presets, presetContext),
453
+ [filterValues, presetContext, presets],
454
+ )
455
+
456
+ const applyPreset = React.useCallback(
457
+ (preset: EntryFilterPreset) => {
458
+ // A preset is a starting point, not a mode: it replaces what it sets and
459
+ // leaves the rest, so narrowing further does not mean rebuilding it.
460
+ setFilterValues((current) => ({ ...current, ...preset.build(presetContext) }))
461
+ setPage(1)
462
+ },
463
+ [presetContext],
464
+ )
465
+
466
+ /**
467
+ * A deep-link filter lives in two places — the filter state and the address
468
+ * bar — so dropping the chip has to drop the parameter too, otherwise the next
469
+ * render (or a reload) puts the narrowing straight back.
470
+ */
471
+ const dropDeepLinkParams = React.useCallback(
472
+ (ids: readonly string[]) => {
473
+ if (!searchParams) return
474
+ const next = new URLSearchParams(searchParams.toString())
475
+ let changed = false
476
+ for (const id of ids) {
477
+ if (!next.has(id)) continue
478
+ next.delete(id)
479
+ changed = true
480
+ }
481
+ if (!changed) return
482
+ const query = next.toString()
483
+ router.replace(query ? `${pathname}?${query}` : pathname)
484
+ },
485
+ [pathname, router, searchParams],
486
+ )
487
+
488
+ const removeChip = React.useCallback(
489
+ (id: string) => {
490
+ setFilterValues((current) => {
491
+ const next = { ...current }
492
+ delete next[id]
493
+ return next
494
+ })
495
+ setPage(1)
496
+ dropDeepLinkParams([id])
497
+ },
498
+ [dropDeepLinkParams],
499
+ )
500
+
501
+ const clearAllFilters = React.useCallback(() => {
502
+ setFilterValues({})
503
+ setPage(1)
504
+ dropDeepLinkParams(DEEP_LINK_PARAMS)
505
+ }, [dropDeepLinkParams])
506
+
507
+ const loadEntries = React.useCallback(async () => {
508
+ if (hasLoadedOnceRef.current) setIsRefreshing(true)
509
+ else setIsLoading(true)
510
+ try {
511
+ const params = new URLSearchParams({ page: String(page), pageSize: String(PAGE_SIZE) })
512
+ const sort = sorting[0]
513
+ params.set('sortField', sort?.id ?? 'date')
514
+ params.set('sortDir', sort?.desc === false ? 'asc' : 'desc')
515
+ if (period.from) params.set('from', period.from)
516
+ if (period.to) params.set('to', period.to)
517
+ if (projectFilter) params.set('projectId', projectFilter)
518
+ const readFilter = (key: string): string =>
519
+ typeof filterValues[key] === 'string' ? (filterValues[key] as string) : ''
520
+ const person = readFilter('staffMemberId')
521
+ if (person) params.set('staffMemberId', person)
522
+ const billable = readFilter('billable')
523
+ if (billable) params.set('billable', billable)
524
+ const lockedFilter = readFilter('locked')
525
+ if (lockedFilter) params.set('locked', lockedFilter)
526
+ const customer = readFilter('customerId')
527
+ if (customer) params.set('customerId', customer)
528
+ const tag = readFilter('tagIds')
529
+ if (tag) params.set('tagIds', tag)
530
+ const text = readFilter('q')
531
+ if (text.trim()) params.set('q', text.trim())
532
+ const taskFilter = readFilter('taskId')
533
+ if (taskFilter) params.set('taskId', taskFilter)
534
+ const entryIdsFilter = readFilter('ids')
535
+ if (entryIdsFilter) params.set('ids', entryIdsFilter)
536
+
537
+ const listPayload = await readApiResultOrThrow<EntriesResponse>(
538
+ `${ENTRIES_API}?${params.toString()}`,
539
+ undefined,
540
+ { errorMessage: labels.errors.load, fallback: { items: [], total: 0, totalPages: 1 } },
541
+ )
542
+ const items = Array.isArray(listPayload.items) ? listPayload.items : []
543
+ const bare = items
544
+ .map((item) => toTimeEntryListRow(item))
545
+ .filter((row): row is TimeEntryListRow => row !== null)
546
+ const { taskIds, projectIds } = collectDirectoryIds(bare)
547
+ let directory: TimeEntryDirectory = { taskTitles: new Map(), projectLabels: new Map() }
548
+ try {
549
+ directory = await loadDirectory(taskIds, projectIds)
550
+ } catch (error) {
551
+ logger.warn('staff.time_tracking.entries directory load failed', { err: error })
552
+ }
553
+ setRows(
554
+ items
555
+ .map((item) => toTimeEntryListRow(item, directory))
556
+ .filter((row): row is TimeEntryListRow => row !== null),
557
+ )
558
+ setTotal(typeof listPayload.total === 'number' ? listPayload.total : items.length)
559
+ setTotalIsCapped(listPayload.totalIsCapped === true)
560
+ setTotalPages(
561
+ typeof listPayload.totalPages === 'number'
562
+ ? listPayload.totalPages
563
+ : Math.max(1, Math.ceil(items.length / PAGE_SIZE)),
564
+ )
565
+ } catch (error) {
566
+ logger.error('staff.time_tracking.entries list failed', { err: error })
567
+ flash(labels.errors.load, 'error')
568
+ } finally {
569
+ setIsLoading(false)
570
+ setIsRefreshing(false)
571
+ hasLoadedOnceRef.current = true
572
+ }
573
+ }, [labels.errors.load, page, period.from, period.to, projectFilter, sorting, filterValues])
574
+
575
+ React.useEffect(() => {
576
+ void loadEntries()
577
+ }, [loadEntries, scopeVersion, reloadToken])
578
+
579
+ React.useEffect(() => {
580
+ setSelectedIds([])
581
+ }, [page, period.from, period.to, projectFilter, scopeVersion])
582
+
583
+ /**
584
+ * Note 2's other half. The undo bar is AppShell's, so the click that restores
585
+ * an entry never reaches this page — the operation store is what does. When a
586
+ * token this page deleted shows up in the store's `undone` list the row is
587
+ * back on the server, so the list is refetched rather than reconstructed.
588
+ */
589
+ const undoneTokens = useOperationStore((state) => state.undone.map((entry) => entry.undoToken).join('|'))
590
+ const pendingUndoTokensRef = React.useRef<Set<string>>(new Set())
591
+ React.useEffect(() => {
592
+ if (pendingUndoTokensRef.current.size === 0) return
593
+ const undone = new Set(undoneTokens.split('|').filter(Boolean))
594
+ let restored = false
595
+ for (const token of Array.from(pendingUndoTokensRef.current)) {
596
+ if (!undone.has(token)) continue
597
+ pendingUndoTokensRef.current.delete(token)
598
+ restored = true
599
+ }
600
+ if (restored) handleRefresh()
601
+ }, [handleRefresh, undoneTokens])
602
+
603
+ const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({
604
+ contextId: MUTATION_CONTEXT_ID,
605
+ blockedMessage: labels.errors.save,
606
+ })
607
+
608
+ const mutationContext = React.useCallback(
609
+ (resourceId: string): MutationContext => ({
610
+ formId: MUTATION_CONTEXT_ID,
611
+ resourceKind: RESOURCE_KIND,
612
+ resourceId,
613
+ retryLastMutation,
614
+ }),
615
+ [retryLastMutation],
616
+ )
617
+
618
+ const handleDurationCommit = React.useCallback(
619
+ async (entry: TimeEntryListRow, durationMinutes: number) => {
620
+ setSavingEntryId(entry.id)
621
+ try {
622
+ await runMutation({
623
+ operation: () =>
624
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(entry.updatedAt), () =>
625
+ updateCrud(
626
+ ENTRIES_API_PATH,
627
+ { id: entry.id, durationMinutes },
628
+ { errorMessage: labels.errors.save },
629
+ ),
630
+ ),
631
+ context: mutationContext(entry.id),
632
+ mutationPayload: { id: entry.id, durationMinutes },
633
+ })
634
+ handleRefresh()
635
+ } catch (error) {
636
+ reportFailure(error, labels.errors.save, 'staff.time_tracking.entries inline duration save failed')
637
+ handleRefresh()
638
+ } finally {
639
+ setSavingEntryId(null)
640
+ }
641
+ },
642
+ [handleRefresh, labels.errors.save, mutationContext, reportFailure, runMutation],
643
+ )
644
+
645
+ const handleDelete = React.useCallback(
646
+ async (entry: TimeEntryListRow) => {
647
+ const previousRows = rows
648
+ setRows((current) => current.filter((row) => row.id !== entry.id))
649
+ setTotal((current) => Math.max(0, current - 1))
650
+ setSelectedIds((current) => current.filter((id) => id !== entry.id))
651
+ try {
652
+ const call = await runMutation({
653
+ operation: () =>
654
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(entry.updatedAt), () =>
655
+ deleteCrud(ENTRIES_API_PATH, entry.id, { errorMessage: labels.errors.delete }),
656
+ ),
657
+ context: mutationContext(entry.id),
658
+ mutationPayload: { id: entry.id },
659
+ })
660
+ const undoToken = readUndoToken(call.response)
661
+ if (undoToken) pendingUndoTokensRef.current.add(undoToken)
662
+ flash(
663
+ t('staff.time_tracking.entries.messages.deleted', 'Deleted “{name}” ({duration}). Use Undo to restore it.', {
664
+ name: entry.taskTitle ?? entry.description ?? labels.noTask,
665
+ duration: formatDuration(entry.durationMinutes, 'clock'),
666
+ }),
667
+ 'success',
668
+ )
669
+ } catch (error) {
670
+ setRows(previousRows)
671
+ setTotal((current) => current + 1)
672
+ reportFailure(error, labels.errors.delete, 'staff.time_tracking.entries delete failed')
673
+ }
674
+ },
675
+ [labels.errors.delete, labels.noTask, mutationContext, reportFailure, rows, runMutation, t],
676
+ )
677
+
678
+ const handleDuplicate = React.useCallback(
679
+ async (entry: TimeEntryListRow) => {
680
+ try {
681
+ await runMutation({
682
+ operation: () =>
683
+ apiCallOrThrow<Record<string, unknown>>(
684
+ `${ENTRIES_API}/${encodeURIComponent(entry.id)}/duplicate`,
685
+ {
686
+ method: 'POST',
687
+ headers: { 'content-type': 'application/json' },
688
+ body: JSON.stringify({ tagIds: entry.tagIds }),
689
+ },
690
+ { errorMessage: labels.errors.duplicate },
691
+ ),
692
+ context: mutationContext(entry.id),
693
+ mutationPayload: { id: entry.id },
694
+ })
695
+ flash(t('staff.time_tracking.entries.messages.duplicated', 'Entry duplicated.'), 'success')
696
+ handleRefresh()
697
+ } catch (error) {
698
+ reportFailure(error, labels.errors.duplicate, 'staff.time_tracking.entries duplicate failed')
699
+ }
700
+ },
701
+ [handleRefresh, labels.errors.duplicate, mutationContext, reportFailure, runMutation, t],
702
+ )
703
+
704
+ /**
705
+ * "Kopiuj wczorajszy dzień" (screen 1 note 5). The copies are drafts to
706
+ * review, so on success the period narrows to the target day: the user lands
707
+ * on exactly the rows that were created instead of hunting for them.
708
+ *
709
+ * A non-empty target day answers `409 copy_day_target_not_empty` naming what
710
+ * is already there. That is a question, not a dead end — the user is told the
711
+ * count and offered the `allowDuplicates` path, which is the real case the
712
+ * refusal would otherwise block (something already logged today, and
713
+ * yesterday's set wanted on top).
714
+ */
715
+ const runCopyDay = React.useCallback(
716
+ async (fromDate: string, toDate: string, allowDuplicates: boolean) =>
717
+ runMutation({
718
+ operation: () =>
719
+ apiCallOrThrow<CopyDayResponse>(
720
+ `${ENTRIES_API}/copy-day`,
721
+ {
722
+ method: 'POST',
723
+ headers: { 'content-type': 'application/json' },
724
+ body: JSON.stringify({ fromDate, toDate, allowDuplicates }),
725
+ },
726
+ { errorMessage: labels.errors.copyDay },
727
+ ),
728
+ context: mutationContext(toDate),
729
+ mutationPayload: { fromDate, toDate, allowDuplicates },
730
+ }),
731
+ [labels.errors.copyDay, mutationContext, runMutation],
732
+ )
733
+
734
+ const handleCopyDay = React.useCallback(async () => {
735
+ const toDate = todayIsoDate()
736
+ const fromDate = shiftIsoDate(toDate, -1)
737
+ const announce = (result: CopyDayResponse | null) => {
738
+ const copied = typeof result?.copied === 'number' ? result.copied : 0
739
+ const skipped = Array.isArray(result?.skipped) ? result.skipped.length : 0
740
+ if (copied === 0) {
741
+ flash(t('staff.time_tracking.entries.copyDay.nothing', 'Yesterday has no entries to copy.'), 'info')
742
+ } else {
743
+ flash(
744
+ t('staff.time_tracking.entries.copyDay.success', '{count} entries copied as drafts — review them.', {
745
+ count: copied,
746
+ }),
747
+ 'success',
748
+ )
749
+ setFilterValues((current) => ({ ...current, period: { from: toDate, to: toDate } }))
750
+ setPage(1)
751
+ }
752
+ if (skipped > 0) {
753
+ flash(
754
+ t('staff.time_tracking.entries.copyDay.skipped', '{count} entries were skipped because they are locked.', {
755
+ count: skipped,
756
+ }),
757
+ 'warning',
758
+ )
759
+ }
760
+ handleRefresh()
761
+ }
762
+
763
+ try {
764
+ const call = await runCopyDay(fromDate, toDate, false)
765
+ announce(call.result)
766
+ } catch (error) {
767
+ const conflict = readCopyDayTargetConflict(error)
768
+ if (!conflict) {
769
+ reportFailure(error, labels.errors.copyDay, 'staff.time_tracking.entries copy-day failed')
770
+ return
771
+ }
772
+ const confirmed = await confirm({
773
+ title: labels.copyDay,
774
+ text: t(
775
+ 'staff.time_tracking.entries.copyDay.conflict',
776
+ 'Today already has {count} entries. Add yesterday’s copies on top of them?',
777
+ { count: conflict.existingEntryCount },
778
+ ),
779
+ confirmText: t('staff.time_tracking.entries.copyDay.confirmDuplicates', 'Add copies anyway'),
780
+ })
781
+ if (!confirmed) return
782
+ try {
783
+ const retry = await runCopyDay(fromDate, toDate, true)
784
+ announce(retry.result)
785
+ } catch (retryError) {
786
+ reportFailure(retryError, labels.errors.copyDay, 'staff.time_tracking.entries copy-day retry failed')
787
+ }
788
+ }
789
+ }, [confirm, handleRefresh, labels.copyDay, labels.errors.copyDay, reportFailure, runCopyDay, t])
790
+
791
+ const openCreate = React.useCallback(() => {
792
+ setDialogEntryId(null)
793
+ setDialogOpen(true)
794
+ }, [])
795
+
796
+ const openEdit = React.useCallback((entryId: string) => {
797
+ setDialogEntryId(entryId)
798
+ setDialogOpen(true)
799
+ }, [])
800
+
801
+ const selectableRows = React.useMemo(() => rows.filter((row) => !row.isLocked), [rows])
802
+ const allSelected = selectableRows.length > 0 && selectedIds.length === selectableRows.length
803
+ const someSelected = selectedIds.length > 0 && !allSelected
804
+
805
+ const toggleRowSelection = React.useCallback((id: string, checked: boolean) => {
806
+ setSelectedIds((current) => (checked ? Array.from(new Set([...current, id])) : current.filter((value) => value !== id)))
807
+ }, [])
808
+
809
+ const toggleAllSelection = React.useCallback(
810
+ (checked: boolean) => {
811
+ setSelectedIds(checked ? selectableRows.map((row) => row.id) : [])
812
+ },
813
+ [selectableRows],
814
+ )
815
+
816
+ const filters = React.useMemo<FilterDef[]>(
817
+ () => [
818
+ { id: 'period', label: labels.filters.period, type: 'dateRange' },
819
+ { id: 'projectId', label: labels.filters.project, type: 'select', options: projectOptions },
820
+ { id: 'customerId', label: labels.filters.customer, type: 'select', options: customerOptions },
821
+ // Only offered when the caller can actually see somebody else's time; for
822
+ // everyone else the route already scopes to their own entries and the
823
+ // control would be a lever attached to nothing.
824
+ ...(canManageAll && peopleOptions.length > 1
825
+ ? [{ id: 'staffMemberId', label: labels.filters.person, type: 'select' as const, options: peopleOptions }]
826
+ : []),
827
+ { id: 'tagIds', label: labels.filters.tag, type: 'select', options: tagOptions },
828
+ {
829
+ id: 'billable',
830
+ label: labels.filters.billable,
831
+ type: 'select',
832
+ options: [
833
+ { value: 'true', label: labels.filters.billableYes },
834
+ { value: 'false', label: labels.filters.billableNo },
835
+ ],
836
+ },
837
+ {
838
+ id: 'locked',
839
+ label: labels.filters.locked,
840
+ type: 'select',
841
+ options: [
842
+ { value: 'true', label: labels.filters.lockedYes },
843
+ { value: 'false', label: labels.filters.lockedNo },
844
+ ],
845
+ },
846
+ { id: 'q', label: labels.filters.text, type: 'text' },
847
+ ],
848
+ [labels.filters, canManageAll, projectOptions, customerOptions, peopleOptions, tagOptions],
849
+ )
850
+
851
+ /**
852
+ * Filters that can be applied but own no control in the overlay: the self
853
+ * filter, which is seeded for everyone but only steerable by a `manage_all`
854
+ * holder, and the two deep-link narrowings. Naming them here is what keeps
855
+ * their chips reading as words instead of as a parameter name and a uuid.
856
+ */
857
+ const unlistedFilters = React.useMemo<FilterDef[]>(
858
+ () => [
859
+ { id: 'staffMemberId', label: labels.filters.person, type: 'select', options: peopleOptions },
860
+ { id: 'taskId', label: labels.filters.task, type: 'text' },
861
+ { id: 'ids', label: labels.filters.entry, type: 'text' },
862
+ ],
863
+ [labels.filters, peopleOptions],
864
+ )
865
+
866
+ /** Applied filters, as the reader would say them out loud. */
867
+ const appliedChips = React.useMemo<AppliedFilterChip[]>(() => {
868
+ const defOf = (id: string) =>
869
+ filters.find((filter) => filter.id === id) ?? unlistedFilters.find((filter) => filter.id === id)
870
+ const label = (id: string) => defOf(id)?.label ?? id
871
+ const optionLabel = (id: string, value: string) =>
872
+ defOf(id)?.options?.find((option) => option.value === value)?.label ?? value
873
+ const chips: AppliedFilterChip[] = []
874
+ for (const [id, raw] of Object.entries(filterValues)) {
875
+ if (raw === undefined || raw === null || raw === '') continue
876
+ if (id === 'period') {
877
+ const range = raw as { from?: string; to?: string }
878
+ if (!range?.from && !range?.to) continue
879
+ chips.push({ id, label: label(id), value: `${range.from ?? '…'} → ${range.to ?? '…'}` })
880
+ continue
881
+ }
882
+ if (typeof raw !== 'string') continue
883
+ // A deep link arrives as an id, so its chip reads the loaded rows back for
884
+ // the task or the entry it names — a uuid on a chip explains nothing.
885
+ if (id === 'taskId') {
886
+ const first = raw.split(',')[0] ?? raw
887
+ const match = rows.find((row) => row.taskId === first)
888
+ chips.push({ id, label: label(id), value: match?.taskTitle ?? first })
889
+ continue
890
+ }
891
+ if (id === 'ids') {
892
+ const list = raw.split(',').map((value) => value.trim()).filter(Boolean)
893
+ if (list.length === 0) continue
894
+ const single = list.length === 1 ? rows.find((row) => row.id === list[0]) : undefined
895
+ chips.push({
896
+ id,
897
+ label: label(id),
898
+ value: single
899
+ ? (single.taskTitle ?? single.description ?? labels.noTask)
900
+ : labels.filters.entrySelection.replace('{count}', String(list.length)),
901
+ })
902
+ continue
903
+ }
904
+ chips.push({ id, label: label(id), value: optionLabel(id, raw) })
905
+ }
906
+ return chips
907
+ }, [filterValues, filters, labels.filters.entrySelection, labels.noTask, rows, unlistedFilters])
908
+
909
+ const columns = React.useMemo<ColumnDef<TimeEntryListRow>[]>(() => {
910
+ const rightHeader = (label: string) => () => <span className="block text-right">{label}</span>
911
+ const defs: ColumnDef<TimeEntryListRow>[] = []
912
+
913
+ if (canManage) {
914
+ defs.push({
915
+ id: 'select',
916
+ accessorKey: 'id',
917
+ header: () => (
918
+ <Checkbox
919
+ checked={allSelected ? true : someSelected ? 'indeterminate' : false}
920
+ onCheckedChange={(checked) => toggleAllSelection(Boolean(checked))}
921
+ aria-label={labels.selectAll}
922
+ />
923
+ ),
924
+ enableSorting: false,
925
+ meta: { priority: 1 },
926
+ cell: ({ row }) =>
927
+ row.original.isLocked ? null : (
928
+ <Checkbox
929
+ checked={selectedIds.includes(row.original.id)}
930
+ onCheckedChange={(checked) => toggleRowSelection(row.original.id, Boolean(checked))}
931
+ onClick={(event) => event.stopPropagation()}
932
+ aria-label={labels.selectRow}
933
+ />
934
+ ),
935
+ })
936
+ }
937
+
938
+ defs.push({
939
+ accessorKey: 'date',
940
+ header: labels.columns.date,
941
+ meta: { priority: 1 },
942
+ cell: ({ row }) => (
943
+ <span className="whitespace-nowrap text-sm text-muted-foreground">
944
+ {formatEntryDay(row.original.date)}
945
+ </span>
946
+ ),
947
+ })
948
+
949
+ defs.push({
950
+ id: 'task',
951
+ accessorKey: 'taskTitle',
952
+ header: labels.columns.task,
953
+ enableSorting: false,
954
+ // `taskTitle` matches none of DataTable's wide-column keys, so it fell to the
955
+ // 150px default and truncated to "Consulting / Worksh…" while the nowrap
956
+ // numeric columns kept their slack. This is the row's primary content.
957
+ meta: { priority: 1, maxWidth: '520px' },
958
+ cell: ({ row }) => {
959
+ const entry = row.original
960
+ const rateOverrideLabel =
961
+ canSeeMoney && entry.rateOverrideAmount !== null
962
+ ? formatCurrency(entry.rateOverrideAmount, entry.currencyCode) ?? String(entry.rateOverrideAmount)
963
+ : null
964
+ return (
965
+ <div className="flex min-w-0 flex-col gap-0.5">
966
+ <div className="flex flex-wrap items-center gap-1.5">
967
+ <span className="truncate text-sm font-medium text-foreground">
968
+ {entry.taskTitle ?? labels.noTask}
969
+ </span>
970
+ {!entry.isBillable ? (
971
+ <StatusBadge variant="neutral">{labels.badges.nonBillable}</StatusBadge>
972
+ ) : null}
973
+ {rateOverrideLabel ? (
974
+ <span data-testid="entry-rate-override">
975
+ <StatusBadge variant="info">
976
+ {t('staff.time_tracking.entries.badges.rateOverride', '{amount}/h', { amount: rateOverrideLabel })}
977
+ </StatusBadge>
978
+ </span>
979
+ ) : null}
980
+ {entry.isLocked ? (
981
+ <span data-testid="entry-locked-badge">
982
+ <StatusBadge variant="neutral">
983
+ <Lock className="h-3 w-3" aria-hidden="true" />
984
+ {labels.badges.locked}
985
+ </StatusBadge>
986
+ </span>
987
+ ) : null}
988
+ </div>
989
+ {entry.description ? (
990
+ <span className="truncate text-xs text-muted-foreground">{entry.description}</span>
991
+ ) : null}
992
+ </div>
993
+ )
994
+ },
995
+ })
996
+
997
+ defs.push({
998
+ id: 'project',
999
+ accessorKey: 'projectLabel',
1000
+ header: labels.columns.project,
1001
+ enableSorting: false,
1002
+ meta: { priority: 2, maxWidth: '240px' },
1003
+ cell: ({ row }) =>
1004
+ row.original.projectLabel ? (
1005
+ <span className="truncate text-sm text-muted-foreground">{row.original.projectLabel}</span>
1006
+ ) : (
1007
+ <span className="text-xs text-muted-foreground/70">—</span>
1008
+ ),
1009
+ })
1010
+
1011
+ defs.push({
1012
+ id: 'clock',
1013
+ accessorKey: 'startText',
1014
+ header: labels.columns.clock,
1015
+ enableSorting: false,
1016
+ meta: { priority: 3, maxWidth: '160px' },
1017
+ cell: ({ row }) => {
1018
+ const range = formatEntryClockRange(row.original)
1019
+ return range ? (
1020
+ <span className="whitespace-nowrap font-mono text-sm tabular-nums text-muted-foreground">{range}</span>
1021
+ ) : (
1022
+ <span className="text-xs text-muted-foreground/70">—</span>
1023
+ )
1024
+ },
1025
+ })
1026
+
1027
+ defs.push({
1028
+ id: 'durationMinutes',
1029
+ accessorKey: 'durationMinutes',
1030
+ header: rightHeader(labels.columns.duration),
1031
+ meta: { priority: 1, maxWidth: '120px' },
1032
+ cell: ({ row }) => (
1033
+ <TimeEntryDurationCell
1034
+ value={row.original.durationMinutes}
1035
+ readOnly={row.original.isLocked || !canManage}
1036
+ saving={savingEntryId === row.original.id}
1037
+ ariaLabel={labels.durationAria}
1038
+ lockedTitle={row.original.isLocked ? labels.errors.locked : undefined}
1039
+ onCommit={(minutes) => handleDurationCommit(row.original, minutes)}
1040
+ />
1041
+ ),
1042
+ })
1043
+
1044
+ if (canSeeMoney) {
1045
+ defs.push({
1046
+ id: 'cost',
1047
+ accessorKey: 'cost',
1048
+ header: rightHeader(labels.columns.cost),
1049
+ enableSorting: false,
1050
+ meta: { priority: 4, maxWidth: '140px' },
1051
+ cell: ({ row }) => {
1052
+ const formatted =
1053
+ row.original.cost === null ? null : formatCurrency(row.original.cost, row.original.currencyCode)
1054
+ return formatted ? (
1055
+ <span
1056
+ className="block text-right font-mono text-sm tabular-nums text-foreground"
1057
+ data-testid="entry-cost"
1058
+ >
1059
+ {formatted}
1060
+ </span>
1061
+ ) : (
1062
+ <span className="block text-right text-xs text-muted-foreground/70" data-testid="entry-cost-empty">
1063
+ —
1064
+ </span>
1065
+ )
1066
+ },
1067
+ })
1068
+ }
1069
+
1070
+ return defs
1071
+ }, [
1072
+ allSelected,
1073
+ canManage,
1074
+ canSeeMoney,
1075
+ handleDurationCommit,
1076
+ labels,
1077
+ savingEntryId,
1078
+ selectedIds,
1079
+ someSelected,
1080
+ t,
1081
+ toggleAllSelection,
1082
+ toggleRowSelection,
1083
+ ])
1084
+
1085
+ const summary = React.useMemo(() => summarizeTimeEntries(rows), [rows])
1086
+
1087
+ return (
1088
+ <Page>
1089
+ <PageBody>
1090
+ <DataTable<TimeEntryListRow>
1091
+ extensionTableId={extensionPoints.hosts.timeEntriesTable.tableId}
1092
+ title={labels.title}
1093
+ titleHeadingLevel={1}
1094
+ data={rows}
1095
+ columns={columns}
1096
+ isLoading={isLoading}
1097
+ filters={filters}
1098
+ filterValues={filterValues}
1099
+ onFiltersApply={(values) => {
1100
+ setFilterValues(values)
1101
+ setPage(1)
1102
+ // The overlay does not carry the deep-link filters, so applying it
1103
+ // drops them — the address bar has to agree with what is on screen.
1104
+ dropDeepLinkParams(DEEP_LINK_PARAMS.filter((id) => !values[id]))
1105
+ }}
1106
+ onFiltersClear={clearAllFilters}
1107
+ activeFilterChips={
1108
+ <div className="flex flex-col gap-2">
1109
+ <EntryFilterPresets
1110
+ presets={presets}
1111
+ activePresetId={activePresetId}
1112
+ onApplyPreset={applyPreset}
1113
+ />
1114
+ <EntryFilterChips
1115
+ chips={appliedChips}
1116
+ onRemove={removeChip}
1117
+ onClearAll={clearAllFilters}
1118
+ clearAllLabel={labels.filters.clearAll}
1119
+ removeLabel={(name) => labels.filters.remove.replace('{name}', name)}
1120
+ />
1121
+ </div>
1122
+ }
1123
+ filterAwareEmptyState={{
1124
+ active: appliedChips.length > 0,
1125
+ entityNamePlural: labels.title,
1126
+ canRemoveLast: appliedChips.length > 0,
1127
+ onClearAll: clearAllFilters,
1128
+ onRemoveLast: () => {
1129
+ const last = appliedChips[appliedChips.length - 1]
1130
+ if (last) removeChip(last.id)
1131
+ },
1132
+ }}
1133
+ emptyState={<div className="py-12 text-center text-sm text-muted-foreground">{labels.empty}</div>}
1134
+ actions={
1135
+ canManage ? (
1136
+ <div className="flex items-center gap-2">
1137
+ <Button size="sm" variant="outline" onClick={() => void handleCopyDay()}>
1138
+ <Copy className="h-4 w-4" aria-hidden="true" />
1139
+ {labels.copyDay}
1140
+ </Button>
1141
+ <Button size="sm" onClick={openCreate}>
1142
+ <Plus className="h-4 w-4" aria-hidden="true" />
1143
+ {labels.add}
1144
+ </Button>
1145
+ </div>
1146
+ ) : undefined
1147
+ }
1148
+ refreshButton={{
1149
+ label: labels.refresh,
1150
+ onRefresh: handleRefresh,
1151
+ isRefreshing: isLoading || isRefreshing,
1152
+ }}
1153
+ sortable
1154
+ manualSorting
1155
+ sorting={sorting}
1156
+ onSortingChange={setSorting}
1157
+ pagination={{
1158
+ page,
1159
+ pageSize: PAGE_SIZE,
1160
+ total,
1161
+ totalIsCapped,
1162
+ totalPages,
1163
+ onPageChange: setPage,
1164
+ }}
1165
+ stickyActionsColumn
1166
+ rowActions={(row) =>
1167
+ row.isLocked ? (
1168
+ <RowActions
1169
+ items={[
1170
+ {
1171
+ id: 'show-report',
1172
+ label: labels.showReport,
1173
+ href: row.lockedReportId
1174
+ ? `${REPORTS_PATH}?id=${encodeURIComponent(row.lockedReportId)}`
1175
+ : REPORTS_PATH,
1176
+ },
1177
+ ]}
1178
+ />
1179
+ ) : canManage ? (
1180
+ <RowActions
1181
+ items={[
1182
+ {
1183
+ id: 'duplicate',
1184
+ label: labels.duplicate,
1185
+ onSelect: () => {
1186
+ void handleDuplicate(row)
1187
+ },
1188
+ },
1189
+ {
1190
+ id: 'delete',
1191
+ label: labels.delete,
1192
+ destructive: true,
1193
+ onSelect: () => {
1194
+ void handleDelete(row)
1195
+ },
1196
+ },
1197
+ ]}
1198
+ />
1199
+ ) : null
1200
+ }
1201
+ onRowClick={(row) => {
1202
+ if (row.isLocked) return
1203
+ openEdit(row.id)
1204
+ }}
1205
+ />
1206
+
1207
+ <TimeEntriesSummaryFooter summary={summary} totalCount={total} canSeeMoney={canSeeMoney} />
1208
+
1209
+ {!canManage ? (
1210
+ <p className="mt-2 text-xs text-muted-foreground">
1211
+ {t('staff.time_tracking.entries.readOnlyNotice', 'You can view these entries but not change them.')}
1212
+ </p>
1213
+ ) : null}
1214
+ </PageBody>
1215
+
1216
+ <TimeEntryDialog
1217
+ open={dialogOpen}
1218
+ onOpenChange={setDialogOpen}
1219
+ entryId={dialogEntryId}
1220
+ onSaved={({ keptOpen }) => {
1221
+ handleRefresh()
1222
+ if (!keptOpen) setDialogOpen(false)
1223
+ }}
1224
+ onShowEntry={(entryId) => openEdit(entryId)}
1225
+ />
1226
+ {ConfirmDialogElement}
1227
+ </Page>
1228
+ )
1229
+ }