@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,1254 @@
1
+ "use client"
2
+
3
+ import { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'
4
+ import * as React from 'react'
5
+ import Link from 'next/link'
6
+ import { useRouter, useSearchParams } from 'next/navigation'
7
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
8
+ import type { SortingState } from '@tanstack/react-table'
9
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
10
+ import { DataTable, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
11
+ import { FilterBar } from '@open-mercato/ui/backend/FilterBar'
12
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterOverlay'
13
+ import { RowActions } from '@open-mercato/ui/backend/RowActions'
14
+ import { Button } from '@open-mercato/ui/primitives/button'
15
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
16
+ import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
17
+ import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
18
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
19
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
20
+ import { createCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
21
+ import { groupBulkDeleteFailures, runBulkDelete } from '@open-mercato/ui/backend/utils/bulkDelete'
22
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
23
+ import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
24
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
25
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
26
+ import { formatCurrency, formatDate } from '@open-mercato/ui/utils/format'
27
+ import { hasAllFeatures } from '@open-mercato/shared/security/features'
28
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
29
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
30
+ import { formatDateTime } from '@open-mercato/shared/lib/time'
31
+ import { ProjectColorDot } from '../../../../lib/timesheets-ui/ProjectColorDot'
32
+ import { resolveProjectColorHex } from '../../../../lib/timesheets-ui/colors'
33
+ import { formatDuration } from '../../../../lib/time-tracking/duration'
34
+ import {
35
+ ProjectsKpiStrip,
36
+ type PmKpis,
37
+ type CollabKpis,
38
+ } from '../../../../lib/timesheets-projects-ui/ProjectsKpiStrip'
39
+ import { SavedViewTabs } from '../../../../lib/timesheets-projects-ui/SavedViewTabs'
40
+ import { HoursSparkline } from '../../../../lib/timesheets-projects-ui/HoursSparkline'
41
+ import {
42
+ ProjectMembersAvatarStack,
43
+ type AvatarMember,
44
+ } from '../../../../lib/timesheets-projects-ui/ProjectMembersAvatarStack'
45
+ import { ViewModeToggle } from '../../../../lib/timesheets-projects-ui/ViewModeToggle'
46
+ import {
47
+ useProjectsViewMode,
48
+ type ProjectsViewMode,
49
+ } from '../../../../lib/timesheets-projects-ui/useProjectsViewMode'
50
+ import {
51
+ ProjectCard,
52
+ type ProjectCardData,
53
+ type ProjectCardLabels,
54
+ } from '../../../../lib/timesheets-projects-ui/ProjectCard'
55
+ import { ProjectBudgetCell } from '../../../../lib/timesheets-projects-ui/ProjectBudgetCell'
56
+ import { ProjectsBulkActionsBar } from '../../../../lib/timesheets-projects-ui/ProjectsBulkActionsBar'
57
+ import {
58
+ AssignMembersDialog,
59
+ type AssignMembersSubmitInput,
60
+ } from '../../../../lib/timesheets-projects-ui/AssignMembersDialog'
61
+ import { computeBudgetBurn, type ProjectBudgetKind } from '../../../../lib/timesheets-projects/budgetBurn'
62
+ import { resolveReportSelection } from '../../../../lib/timesheets-projects/reportSelection'
63
+ import { createLogger } from '@open-mercato/shared/lib/logger'
64
+
65
+ const logger = createLogger('staff')
66
+
67
+ const PAGE_SIZE = 50
68
+ const INCLUDE_FIELDS = 'hoursWeek,hoursTrend,members,myRole'
69
+ const MANAGE_FEATURE = 'staff.timesheets.projects.manage'
70
+ const RATES_FEATURE = 'staff.timesheets.rates.view'
71
+ const REPORTS_FEATURES = ['staff.timesheets.reports.view', 'staff.timesheets.reports.manage']
72
+ const REPORTS_HREF = '/backend/staff/time-tracking/reports'
73
+
74
+ type ProjectBudgetPayload = {
75
+ kind?: unknown
76
+ value?: unknown
77
+ warnAtPercent?: unknown
78
+ }
79
+
80
+ type StaffEnrichment = {
81
+ hoursWeek?: number
82
+ hoursTrend?: number[]
83
+ myRole?: string | null
84
+ members?: AvatarMember[]
85
+ memberCount?: number
86
+ customerName?: string | null
87
+ totalMinutes?: number
88
+ billableMinutes?: number
89
+ budget?: ProjectBudgetPayload
90
+ hourlyRate?: number | null
91
+ cost?: number | null
92
+ }
93
+
94
+ type ProjectRow = {
95
+ id: string
96
+ name: string
97
+ code: string | null
98
+ customerId: string | null
99
+ customerName: string | null
100
+ status: string
101
+ projectType: string | null
102
+ color: string | null
103
+ startDate: string | null
104
+ updatedAt: string | null
105
+ currencyCode: string | null
106
+ hoursWeek: number
107
+ hoursTrend: number[]
108
+ myRole: string | null
109
+ members: AvatarMember[]
110
+ memberCount: number
111
+ totalMinutes: number
112
+ hourlyRate: number | null
113
+ cost: number | null
114
+ budgetKind: ProjectBudgetKind
115
+ budgetValue: number | null
116
+ budgetWarnAtPercent: number | null
117
+ }
118
+
119
+ type ProjectsResponse = {
120
+ items?: Array<Record<string, unknown>>
121
+ total?: number
122
+ totalIsCapped?: boolean
123
+ totalPages?: number
124
+ }
125
+
126
+ type KpisResponse = PmKpis | CollabKpis
127
+
128
+ const STATUS_VARIANTS: Record<string, StatusBadgeVariant> = {
129
+ active: 'success',
130
+ on_hold: 'warning',
131
+ completed: 'neutral',
132
+ }
133
+
134
+ function readString(item: Record<string, unknown>, camel: string, snake: string): string | null {
135
+ const camelValue = item[camel]
136
+ if (typeof camelValue === 'string') return camelValue
137
+ const snakeValue = item[snake]
138
+ return typeof snakeValue === 'string' ? snakeValue : null
139
+ }
140
+
141
+ function readNumber(value: unknown): number | null {
142
+ if (typeof value === 'number' && Number.isFinite(value)) return value
143
+ if (typeof value === 'string' && value.trim().length > 0) {
144
+ const parsed = Number(value)
145
+ if (Number.isFinite(parsed)) return parsed
146
+ }
147
+ return null
148
+ }
149
+
150
+ function readBudgetKind(value: unknown): ProjectBudgetKind {
151
+ return value === 'hours' || value === 'amount' ? value : 'none'
152
+ }
153
+
154
+ function formatRelativeTime(iso: string | null, fallback: string, t: TranslateFn): string {
155
+ if (!iso) return fallback
156
+ const parsed = new Date(iso)
157
+ if (Number.isNaN(parsed.getTime())) return fallback
158
+ const diffMs = Date.now() - parsed.getTime()
159
+ const minutes = Math.round(diffMs / 60000)
160
+ if (minutes < 1) return t('staff.timesheets.projects.portfolio.relativeTime.justNow', 'just now')
161
+ if (minutes < 60) return t('staff.timesheets.projects.portfolio.relativeTime.minutesAgo', '{minutes}m ago', { minutes })
162
+ const hours = Math.round(minutes / 60)
163
+ if (hours < 24) return t('staff.timesheets.projects.portfolio.relativeTime.hoursAgo', '{hours}h ago', { hours })
164
+ const days = Math.round(hours / 24)
165
+ if (days < 14) return t('staff.timesheets.projects.portfolio.relativeTime.daysAgo', '{days}d ago', { days })
166
+ return formatDateTime(iso) ?? fallback
167
+ }
168
+
169
+ function mapApiProject(item: Record<string, unknown>): ProjectRow {
170
+ const id = typeof item.id === 'string' ? item.id : ''
171
+ const name = typeof item.name === 'string' ? item.name : id
172
+ const code = typeof item.code === 'string' && item.code.trim().length ? item.code.trim() : null
173
+ const customerId = readString(item, 'customerId', 'customer_id')
174
+ const status = typeof item.status === 'string' ? item.status : 'active'
175
+ const projectType = readString(item, 'projectType', 'project_type')
176
+ const color = typeof item.color === 'string' ? item.color : null
177
+ const startDate = readString(item, 'startDate', 'start_date')
178
+ const updatedAt = readString(item, 'updatedAt', 'updated_at')
179
+ const currencyCode = readString(item, 'currencyCode', 'currency_code')
180
+ const enrichment = (item as { _staff?: StaffEnrichment })._staff ?? ({} as StaffEnrichment)
181
+ const hoursWeek = typeof enrichment.hoursWeek === 'number' ? enrichment.hoursWeek : 0
182
+ const hoursTrend = Array.isArray(enrichment.hoursTrend)
183
+ ? enrichment.hoursTrend.filter((v): v is number => typeof v === 'number')
184
+ : []
185
+ const members = Array.isArray(enrichment.members)
186
+ ? enrichment.members
187
+ .filter((m): m is AvatarMember => !!m && typeof m === 'object' && typeof (m as AvatarMember).id === 'string')
188
+ .map((m) => ({
189
+ id: m.id,
190
+ name: m.name ?? '',
191
+ initials: m.initials ?? '',
192
+ avatarUrl: m.avatarUrl ?? null,
193
+ }))
194
+ : []
195
+ const memberCount = typeof enrichment.memberCount === 'number' ? enrichment.memberCount : members.length
196
+ const myRole = typeof enrichment.myRole === 'string' ? enrichment.myRole : null
197
+ const customerName =
198
+ typeof enrichment.customerName === 'string' && enrichment.customerName.length > 0
199
+ ? enrichment.customerName
200
+ : readString(item, 'customerName', 'customer_name')
201
+
202
+ return withDataTableNamespaces(
203
+ {
204
+ id,
205
+ name,
206
+ code,
207
+ customerId,
208
+ customerName,
209
+ status,
210
+ projectType,
211
+ color,
212
+ startDate,
213
+ updatedAt,
214
+ currencyCode,
215
+ hoursWeek,
216
+ hoursTrend,
217
+ myRole,
218
+ members,
219
+ memberCount,
220
+ totalMinutes: typeof enrichment.totalMinutes === 'number' ? enrichment.totalMinutes : 0,
221
+ hourlyRate: readNumber(enrichment.hourlyRate),
222
+ cost: readNumber(enrichment.cost),
223
+ budgetKind: readBudgetKind(enrichment.budget?.kind),
224
+ budgetValue: readNumber(enrichment.budget?.value),
225
+ budgetWarnAtPercent: readNumber(enrichment.budget?.warnAtPercent),
226
+ },
227
+ item,
228
+ )
229
+ }
230
+
231
+ export default function TimesheetProjectsPage() {
232
+ const t = useT()
233
+ const router = useRouter()
234
+ const searchParams = useSearchParams()
235
+ const scopeVersion = useOrganizationScopeVersion()
236
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
237
+ const { payload: chromePayload } = useBackendChrome()
238
+
239
+ const [rows, setRows] = React.useState<ProjectRow[]>([])
240
+ const [page, setPage] = React.useState(1)
241
+ const [total, setTotal] = React.useState(0)
242
+ const [totalIsCapped, setTotalIsCapped] = React.useState(false)
243
+ const [totalPages, setTotalPages] = React.useState(1)
244
+ const [sorting, setSorting] = React.useState<SortingState>([{ id: 'updatedAt', desc: true }])
245
+ const [search, setSearch] = React.useState('')
246
+ const [filterValues, setFilterValues] = React.useState<FilterValues>({})
247
+ const [isLoading, setIsLoading] = React.useState(true)
248
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
249
+ const hasLoadedOnceRef = React.useRef(false)
250
+ const [reloadToken, setReloadToken] = React.useState(0)
251
+ const [kpis, setKpis] = React.useState<KpisResponse | null>(null)
252
+ const [isLoadingKpis, setIsLoadingKpis] = React.useState(true)
253
+ const [selectedIds, setSelectedIds] = React.useState<string[]>([])
254
+ const [assignDialogOpen, setAssignDialogOpen] = React.useState(false)
255
+ const [isAssigning, setIsAssigning] = React.useState(false)
256
+
257
+ const activeTab = searchParams.get('tab') ?? 'all'
258
+ const urlViewMode = searchParams.get('view')
259
+ const [viewMode, setViewMode] = useProjectsViewMode({
260
+ userKey: null,
261
+ urlOverride: urlViewMode,
262
+ })
263
+
264
+ const grantedFeatures = React.useMemo(
265
+ () => chromePayload?.grantedFeatures ?? [],
266
+ [chromePayload?.grantedFeatures],
267
+ )
268
+ const canManageProjects = hasAllFeatures(grantedFeatures, [MANAGE_FEATURE])
269
+ const canViewRates = hasAllFeatures(grantedFeatures, [RATES_FEATURE])
270
+ const canCreateReports = hasAllFeatures(grantedFeatures, REPORTS_FEATURES)
271
+ const canSelectRows = canManageProjects || canCreateReports
272
+
273
+ const labels = React.useMemo(
274
+ () => ({
275
+ title: t('staff.timesheets.projects.page.title', 'Projects'),
276
+ table: {
277
+ name: t('staff.timesheets.projects.table.name', 'Project'),
278
+ status: t('staff.timesheets.projects.table.status', 'Status'),
279
+ type: t('staff.timesheets.projects.table.type', 'Type'),
280
+ updatedAt: t('staff.timesheets.projects.table.updatedAt', 'Updated'),
281
+ empty: t('staff.timesheets.projects.table.empty', 'No projects yet.'),
282
+ search: t('staff.timesheets.projects.table.search', 'Search projects...'),
283
+ team: t('staff.timesheets.projects.portfolio.team', 'Team'),
284
+ myRole: t('staff.timesheets.projects.portfolio.myRole', 'My role'),
285
+ hoursWeek: t('staff.timesheets.projects.portfolio.hoursWeek', 'Hours / week'),
286
+ myHoursWeek: t('staff.timesheets.projects.portfolio.myHoursWeek', 'My hours / week'),
287
+ customer: t('staff.time_tracking.projects.columns.customer', 'Customer'),
288
+ rate: t('staff.time_tracking.projects.columns.rate', 'Rate'),
289
+ hours: t('staff.time_tracking.projects.columns.hours', 'Hours'),
290
+ cost: t('staff.time_tracking.projects.columns.cost', 'Cost'),
291
+ budget: t('staff.time_tracking.projects.columns.budget', 'Budget'),
292
+ selectRow: t('staff.time_tracking.projects.selection.selectRow', 'Select project'),
293
+ selectAll: t('staff.time_tracking.projects.selection.selectAll', 'Select all projects on this page'),
294
+ },
295
+ actions: {
296
+ add: t('staff.timesheets.projects.actions.add', 'Add Project'),
297
+ addFirst: t('staff.timesheets.projects.actions.addFirst', '+ Add first project'),
298
+ viewDetails: t('staff.timesheets.projects.actions.viewDetails', 'View Details'),
299
+ delete: t('staff.timesheets.projects.actions.delete', 'Delete'),
300
+ deleteConfirm: t('staff.timesheets.projects.actions.deleteConfirm', 'Delete project "{{name}}"?'),
301
+ refresh: t('staff.timesheets.projects.actions.refresh', 'Refresh'),
302
+ assignCustomer: t('staff.time_tracking.projects.actions.assignCustomer', 'Assign a customer'),
303
+ },
304
+ messages: {
305
+ deleted: t('staff.timesheets.projects.messages.deleted', 'Project deleted.'),
306
+ },
307
+ errors: {
308
+ load: t('staff.timesheets.projects.errors.load', 'Failed to load projects.'),
309
+ delete: t('staff.timesheets.projects.errors.delete', 'Failed to delete project.'),
310
+ },
311
+ statuses: {
312
+ all: t('staff.timesheets.projects.statuses.all', 'All'),
313
+ active: t('staff.timesheets.projects.statuses.active', 'Active'),
314
+ on_hold: t('staff.timesheets.projects.statuses.onHold', 'On Hold'),
315
+ completed: t('staff.timesheets.projects.statuses.completed', 'Completed'),
316
+ },
317
+ tabs: {
318
+ all: t('staff.timesheets.projects.portfolio.tabs.all', 'All'),
319
+ active: t('staff.timesheets.projects.portfolio.tabs.active', 'Active'),
320
+ onHold: t('staff.timesheets.projects.portfolio.tabs.onHold', 'On Hold'),
321
+ completed: t('staff.timesheets.projects.portfolio.tabs.completed', 'Completed'),
322
+ mine: t('staff.timesheets.projects.portfolio.tabs.mine', 'Mine'),
323
+ },
324
+ viewMode: {
325
+ table: t('staff.timesheets.projects.portfolio.viewMode.table', 'Table'),
326
+ cards: t('staff.timesheets.projects.portfolio.viewMode.cards', 'Cards'),
327
+ },
328
+ kpi: {
329
+ totalProjects: t('staff.timesheets.projects.portfolio.kpi.totalProjects', 'Total Projects'),
330
+ hoursWeek: t('staff.timesheets.projects.portfolio.kpi.hoursWeek', 'Hours this week'),
331
+ hoursWeekSub: t('staff.timesheets.projects.portfolio.kpi.hoursWeekSub', 'vs previous week'),
332
+ assignedToMe: t('staff.timesheets.projects.portfolio.kpi.assignedToMe', 'Assigned to me'),
333
+ hoursMonth: t('staff.timesheets.projects.portfolio.kpi.hoursMonth', 'Hours this month'),
334
+ hoursMonthSub: t('staff.timesheets.projects.portfolio.kpi.hoursMonthSub', 'vs previous month'),
335
+ teamActive: t('staff.timesheets.projects.portfolio.kpi.teamActive', 'Active team'),
336
+ teamActiveSub: t('staff.timesheets.projects.portfolio.kpi.teamActiveSub', 'Members with entries this month'),
337
+ myProjects: t('staff.timesheets.projects.portfolio.kpi.myProjects', 'My projects'),
338
+ myHoursWeek: t('staff.timesheets.projects.portfolio.kpi.myHoursWeek', 'My hours this week'),
339
+ myHoursMonth: t('staff.timesheets.projects.portfolio.kpi.myHoursMonth', 'My hours this month'),
340
+ deltaFlat: t('staff.timesheets.projects.portfolio.kpi.deltaFlat', 'no change'),
341
+ noPrevious: t('staff.timesheets.projects.portfolio.kpi.noPrevious', 'no previous data'),
342
+ },
343
+ card: {
344
+ hoursPanelPm: t('staff.timesheets.projects.portfolio.card.hoursPanelPm', 'Team hours · last 7w'),
345
+ hoursPanelCollab: t('staff.timesheets.projects.portfolio.card.hoursPanelCollab', 'My hours · last 7w'),
346
+ sparklineAria: t('staff.timesheets.projects.portfolio.sparkline.ariaLabel', 'Hours per week, last 7 weeks'),
347
+ role: t('staff.timesheets.projects.portfolio.card.role', 'Role'),
348
+ },
349
+ emptyState: {
350
+ noProjects: t('staff.timesheets.projects.portfolio.emptyState.noProjects', 'No projects yet.'),
351
+ noAssignments: t(
352
+ 'staff.timesheets.projects.portfolio.emptyState.noAssignments',
353
+ "You aren't assigned to any projects yet. Ask a PM to add you.",
354
+ ),
355
+ noMatches: t('staff.timesheets.projects.portfolio.emptyState.noMatches', 'No projects match these filters.'),
356
+ },
357
+ budget: {
358
+ none: t('staff.time_tracking.projects.budget.none', 'no budget'),
359
+ aria: t('staff.time_tracking.projects.budget.ariaLabel', 'Budget usage'),
360
+ },
361
+ bulk: {
362
+ report: t('staff.time_tracking.projects.bulk.report', 'Report from selected'),
363
+ assignMembers: t('staff.time_tracking.projects.bulk.assignMembers', 'Assign members'),
364
+ clear: t('staff.time_tracking.projects.bulk.clear', 'Clear'),
365
+ },
366
+ assign: {
367
+ title: t('staff.time_tracking.projects.assignMembers.title', 'Assign members'),
368
+ searchPlaceholder: t('staff.timesheets.projects.employees.searchEmployee', 'Search team members...'),
369
+ empty: t('staff.timesheets.projects.employees.noResults', 'No team members found'),
370
+ loadError: t(
371
+ 'staff.time_tracking.projects.assignMembers.loadError',
372
+ 'Could not load team members. Check your connection and try again.',
373
+ ),
374
+ loading: t('staff.timesheets.projects.employees.loading', 'Loading employees...'),
375
+ role: t('staff.timesheets.projects.employees.roleOnProject', 'Role on Project'),
376
+ rolePlaceholder: t('staff.timesheets.projects.employees.rolePlaceholder', 'e.g. Developer, Designer...'),
377
+ startDate: t('staff.timesheets.projects.employees.assignmentStartDate', 'Assignment Start Date'),
378
+ cancel: t('staff.timesheets.projects.form.actions.cancel', 'Cancel'),
379
+ confirm: t('staff.time_tracking.projects.assignMembers.confirm', 'Assign'),
380
+ error: t('staff.time_tracking.projects.assignMembers.error', 'Failed to assign members.'),
381
+ progressName: t('staff.time_tracking.projects.assignMembers.progressName', 'Assign members to projects'),
382
+ },
383
+ }),
384
+ [t],
385
+ )
386
+
387
+ const kpiLabels = React.useMemo(
388
+ () => ({
389
+ totalProjects: labels.kpi.totalProjects,
390
+ totalProjectsSub: ({ active, onHold }: { active: number; onHold: number }) =>
391
+ `${active} ${labels.statuses.active.toLowerCase()} · ${onHold} ${labels.statuses.on_hold.toLowerCase()}`,
392
+ hoursWeek: labels.kpi.hoursWeek,
393
+ hoursWeekSub: labels.kpi.hoursWeekSub,
394
+ assignedToMe: labels.kpi.assignedToMe,
395
+ assignedToMeSub: (active: number) => `${active} ${labels.statuses.active.toLowerCase()}`,
396
+ hoursMonth: labels.kpi.hoursMonth,
397
+ hoursMonthSub: labels.kpi.hoursMonthSub,
398
+ teamActive: labels.kpi.teamActive,
399
+ teamActiveSub: labels.kpi.teamActiveSub,
400
+ myProjects: labels.kpi.myProjects,
401
+ myProjectsSub: (active: number) => `${active} ${labels.statuses.active.toLowerCase()}`,
402
+ myHoursWeek: labels.kpi.myHoursWeek,
403
+ myHoursMonth: labels.kpi.myHoursMonth,
404
+ deltaUp: (pct: number) => `up ${pct}%`,
405
+ deltaDown: (pct: number) => `down ${pct}%`,
406
+ deltaFlat: labels.kpi.deltaFlat,
407
+ noPrevious: labels.kpi.noPrevious,
408
+ }),
409
+ [labels],
410
+ )
411
+
412
+ const isPmRole = kpis?.role === 'pm'
413
+
414
+ const filters = React.useMemo<FilterDef[]>(
415
+ () => [
416
+ {
417
+ id: 'status',
418
+ label: labels.table.status,
419
+ type: 'select',
420
+ options: [
421
+ { value: 'active', label: labels.statuses.active },
422
+ { value: 'on_hold', label: labels.statuses.on_hold },
423
+ { value: 'completed', label: labels.statuses.completed },
424
+ ],
425
+ },
426
+ ],
427
+ [labels.table.status, labels.statuses],
428
+ )
429
+
430
+ const tabs = React.useMemo(() => {
431
+ const base = [
432
+ { id: 'all', label: labels.tabs.all },
433
+ { id: 'active', label: labels.tabs.active },
434
+ ]
435
+ if (isPmRole) {
436
+ base.push({ id: 'on_hold', label: labels.tabs.onHold })
437
+ }
438
+ base.push({ id: 'completed', label: labels.tabs.completed })
439
+ if (isPmRole) {
440
+ base.push({ id: 'mine', label: labels.tabs.mine })
441
+ }
442
+ return base
443
+ }, [labels.tabs, isPmRole])
444
+
445
+ const statusFromTab = (tabId: string): string | null => {
446
+ if (tabId === 'active' || tabId === 'on_hold' || tabId === 'completed') return tabId
447
+ return null
448
+ }
449
+ const mineFromTab = (tabId: string): boolean => tabId === 'mine' || !isPmRole
450
+
451
+ const loadKpis = React.useCallback(async () => {
452
+ setIsLoadingKpis(true)
453
+ try {
454
+ const payload = await readApiResultOrThrow<KpisResponse>(
455
+ '/api/staff/timesheets/projects/kpis',
456
+ undefined,
457
+ {
458
+ errorMessage: labels.errors.load,
459
+ fallback: null as unknown as KpisResponse,
460
+ },
461
+ )
462
+ setKpis(payload)
463
+ } catch {
464
+ setKpis(null)
465
+ } finally {
466
+ setIsLoadingKpis(false)
467
+ }
468
+ }, [labels.errors.load])
469
+
470
+ const loadProjects = React.useCallback(async () => {
471
+ if (hasLoadedOnceRef.current) {
472
+ setIsRefreshing(true)
473
+ } else {
474
+ setIsLoading(true)
475
+ }
476
+ try {
477
+ const params = new URLSearchParams({
478
+ page: String(page),
479
+ pageSize: String(PAGE_SIZE),
480
+ include: INCLUDE_FIELDS,
481
+ })
482
+ const sort = sorting[0]
483
+ if (sort?.id) {
484
+ params.set('sortField', sort.id)
485
+ params.set('sortDir', sort.desc ? 'desc' : 'asc')
486
+ }
487
+ if (search.trim()) params.set('q', search.trim())
488
+ const tabStatus = statusFromTab(activeTab)
489
+ if (tabStatus) params.set('status', tabStatus)
490
+ else if (typeof filterValues.status === 'string' && filterValues.status.length > 0) {
491
+ params.set('status', filterValues.status)
492
+ }
493
+ if (mineFromTab(activeTab)) params.set('mine', '1')
494
+
495
+ const payload = await readApiResultOrThrow<ProjectsResponse>(
496
+ `/api/staff/timesheets/time-projects?${params.toString()}`,
497
+ undefined,
498
+ { errorMessage: labels.errors.load, fallback: { items: [], total: 0, totalPages: 1 } },
499
+ )
500
+ const items = Array.isArray(payload.items) ? payload.items : []
501
+ setRows(items.map(mapApiProject))
502
+ setTotal(typeof payload.total === 'number' ? payload.total : items.length)
503
+ setTotalIsCapped(payload.totalIsCapped === true)
504
+ setTotalPages(
505
+ typeof payload.totalPages === 'number'
506
+ ? payload.totalPages
507
+ : Math.max(1, Math.ceil(items.length / PAGE_SIZE)),
508
+ )
509
+ } catch (error) {
510
+ logger.error('staff.timesheets.projects.list', { err: error })
511
+ flash(labels.errors.load, 'error')
512
+ } finally {
513
+ setIsLoading(false)
514
+ setIsRefreshing(false)
515
+ hasLoadedOnceRef.current = true
516
+ }
517
+ }, [labels.errors.load, page, search, sorting, filterValues.status, activeTab, isPmRole])
518
+
519
+ React.useEffect(() => {
520
+ void loadKpis()
521
+ }, [loadKpis, scopeVersion, reloadToken])
522
+
523
+ React.useEffect(() => {
524
+ void loadProjects()
525
+ }, [loadProjects, scopeVersion, reloadToken])
526
+
527
+ React.useEffect(() => {
528
+ setSelectedIds([])
529
+ }, [page, activeTab, search, filterValues.status, scopeVersion])
530
+
531
+ const selectedRows = React.useMemo(
532
+ () => rows.filter((row) => selectedIds.includes(row.id)),
533
+ [rows, selectedIds],
534
+ )
535
+
536
+ const reportSelection = React.useMemo(
537
+ () =>
538
+ resolveReportSelection(
539
+ selectedRows.map((row) => ({
540
+ id: row.id,
541
+ name: row.name,
542
+ customerId: row.customerId,
543
+ customerName: row.customerName,
544
+ currencyCode: row.currencyCode,
545
+ })),
546
+ ),
547
+ [selectedRows],
548
+ )
549
+
550
+ const reportBlockedReason = React.useMemo(() => {
551
+ if (reportSelection.ok) return null
552
+ const offenders = reportSelection.offenders.join(', ')
553
+ if (reportSelection.reason === 'multiple_customers') {
554
+ return t(
555
+ 'staff.time_tracking.projects.bulk.reportBlocked.multipleCustomers',
556
+ 'A report always covers one customer. Selected: {customers}.',
557
+ { customers: offenders },
558
+ )
559
+ }
560
+ if (reportSelection.reason === 'multiple_currencies') {
561
+ return t(
562
+ 'staff.time_tracking.projects.bulk.reportBlocked.multipleCurrencies',
563
+ 'A report always covers one currency. Selected: {currencies}.',
564
+ { currencies: offenders },
565
+ )
566
+ }
567
+ if (reportSelection.reason === 'missing_customer') {
568
+ return t(
569
+ 'staff.time_tracking.projects.bulk.reportBlocked.missingCustomer',
570
+ 'These projects have no customer yet: {projects}.',
571
+ { projects: offenders },
572
+ )
573
+ }
574
+ return null
575
+ }, [reportSelection, t])
576
+
577
+ const handleTabSelect = React.useCallback(
578
+ (id: string) => {
579
+ const params = new URLSearchParams(searchParams.toString())
580
+ if (id === 'all') params.delete('tab')
581
+ else params.set('tab', id)
582
+ router.replace(`?${params.toString()}`)
583
+ setPage(1)
584
+ },
585
+ [router, searchParams],
586
+ )
587
+
588
+ const handleViewModeChange = React.useCallback(
589
+ (next: ProjectsViewMode) => {
590
+ setViewMode(next)
591
+ const params = new URLSearchParams(searchParams.toString())
592
+ if (next === 'table') params.delete('view')
593
+ else params.set('view', next)
594
+ router.replace(`?${params.toString()}`)
595
+ },
596
+ [router, searchParams, setViewMode],
597
+ )
598
+
599
+ const handleSearchChange = React.useCallback((value: string) => {
600
+ setSearch(value)
601
+ setPage(1)
602
+ }, [])
603
+
604
+ const handleFiltersApply = React.useCallback((values: FilterValues) => {
605
+ setFilterValues(values)
606
+ setPage(1)
607
+ }, [])
608
+
609
+ const handleFiltersClear = React.useCallback(() => {
610
+ setFilterValues({})
611
+ setPage(1)
612
+ }, [])
613
+
614
+ const handleRefresh = React.useCallback(() => {
615
+ setReloadToken((token) => token + 1)
616
+ }, [])
617
+
618
+ const projectsMutationContextId = 'staff-time-projects-list:assign-members'
619
+ const { runMutation: runProjectsMutation, retryLastMutation: retryProjectsMutation } = useGuardedMutation<{
620
+ formId: string
621
+ resourceKind: string
622
+ resourceId?: string
623
+ retryLastMutation: () => Promise<boolean>
624
+ }>({
625
+ contextId: projectsMutationContextId,
626
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
627
+ })
628
+
629
+ const handleDelete = React.useCallback(
630
+ async (entry: ProjectRow) => {
631
+ const message = labels.actions.deleteConfirm.replace('{{name}}', entry.name)
632
+ const confirmed = await confirm({
633
+ title: labels.actions.delete,
634
+ text: message,
635
+ variant: 'destructive',
636
+ })
637
+ if (!confirmed) return
638
+ try {
639
+ await runProjectsMutation({
640
+ operation: () =>
641
+ withScopedApiRequestHeaders(
642
+ buildOptimisticLockHeader(entry.updatedAt),
643
+ () => deleteCrud('staff/timesheets/time-projects', entry.id, { errorMessage: labels.errors.delete }),
644
+ ),
645
+ context: {
646
+ formId: projectsMutationContextId,
647
+ resourceKind: 'staff.time_project',
648
+ resourceId: entry.id,
649
+ retryLastMutation: retryProjectsMutation,
650
+ },
651
+ mutationPayload: { id: entry.id },
652
+ })
653
+ flash(labels.messages.deleted, 'success')
654
+ handleRefresh()
655
+ } catch (error) {
656
+ if (surfaceRecordConflict(error, t)) { handleRefresh(); return }
657
+ logger.error('staff.timesheets.projects.delete', { err: error })
658
+ flash(labels.errors.delete, 'error')
659
+ }
660
+ },
661
+ [confirm, handleRefresh, projectsMutationContextId, retryProjectsMutation, runProjectsMutation, t, labels.actions.deleteConfirm, labels.actions.delete, labels.errors.delete, labels.messages.deleted],
662
+ )
663
+
664
+ const handleAssignMembers = React.useCallback(
665
+ async (input: AssignMembersSubmitInput) => {
666
+ const targets = selectedRows
667
+ if (targets.length === 0 || input.staffMemberIds.length === 0) return
668
+ setIsAssigning(true)
669
+ try {
670
+ const { succeeded, failures } = await runProjectsMutation({
671
+ operation: () =>
672
+ runBulkDelete(
673
+ targets,
674
+ async (row) => {
675
+ for (const staffMemberId of input.staffMemberIds) {
676
+ await createCrud(
677
+ `staff/timesheets/time-projects/${row.id}/employees`,
678
+ {
679
+ timeProjectId: row.id,
680
+ staffMemberId,
681
+ role: input.role,
682
+ assignedStartDate: input.assignedStartDate,
683
+ },
684
+ { errorMessage: labels.assign.error },
685
+ )
686
+ }
687
+ },
688
+ {
689
+ fallbackErrorMessage: labels.assign.error,
690
+ logTag: 'staff.timesheets.projects.assignMembers',
691
+ progress: {
692
+ jobType: 'staff.timesheets.projects.bulk_assign_members',
693
+ name: labels.assign.progressName,
694
+ },
695
+ },
696
+ ),
697
+ context: {
698
+ formId: projectsMutationContextId,
699
+ resourceKind: 'staff.time_project_member',
700
+ retryLastMutation: retryProjectsMutation,
701
+ },
702
+ mutationPayload: {
703
+ projectIds: targets.map((row) => row.id),
704
+ staffMemberIds: input.staffMemberIds,
705
+ },
706
+ })
707
+ if (succeeded.length > 0) {
708
+ flash(
709
+ t('staff.time_tracking.projects.assignMembers.success', 'Members assigned to {count} projects.', {
710
+ count: succeeded.length,
711
+ }),
712
+ 'success',
713
+ )
714
+ }
715
+ for (const group of groupBulkDeleteFailures(failures)) {
716
+ flash(group.sampleMessage, 'error')
717
+ }
718
+ setAssignDialogOpen(false)
719
+ setSelectedIds([])
720
+ handleRefresh()
721
+ } catch (error) {
722
+ logger.error('staff.timesheets.projects.assignMembers', { err: error })
723
+ flash(labels.assign.error, 'error')
724
+ } finally {
725
+ setIsAssigning(false)
726
+ }
727
+ },
728
+ [
729
+ projectsMutationContextId,
730
+ handleRefresh,
731
+ labels.assign.error,
732
+ labels.assign.progressName,
733
+ retryProjectsMutation,
734
+ runProjectsMutation,
735
+ selectedRows,
736
+ t,
737
+ ],
738
+ )
739
+
740
+ const handleReportFromSelected = React.useCallback(() => {
741
+ if (!reportSelection.ok) return
742
+ const params = new URLSearchParams({
743
+ customerId: reportSelection.customerId,
744
+ projectIds: reportSelection.projectIds.join(','),
745
+ })
746
+ router.push(`${REPORTS_HREF}?${params.toString()}`)
747
+ }, [reportSelection, router])
748
+
749
+ const toggleRowSelection = React.useCallback((id: string, checked: boolean) => {
750
+ setSelectedIds((prev) => (checked ? Array.from(new Set([...prev, id])) : prev.filter((value) => value !== id)))
751
+ }, [])
752
+
753
+ const toggleAllSelection = React.useCallback(
754
+ (checked: boolean) => {
755
+ setSelectedIds(checked ? rows.map((row) => row.id) : [])
756
+ },
757
+ [rows],
758
+ )
759
+
760
+ const allSelected = rows.length > 0 && selectedIds.length === rows.length
761
+ const someSelected = selectedIds.length > 0 && !allSelected
762
+
763
+ const columns = React.useMemo<ColumnDef<ProjectRow>[]>(() => {
764
+ const rightHeader = (label: string) => () => <span className="block text-right">{label}</span>
765
+ const defs: ColumnDef<ProjectRow>[] = []
766
+
767
+ if (canSelectRows) {
768
+ defs.push({
769
+ id: 'select',
770
+ accessorKey: 'id',
771
+ header: () => (
772
+ <Checkbox
773
+ checked={allSelected ? true : someSelected ? 'indeterminate' : false}
774
+ onCheckedChange={(checked) => toggleAllSelection(Boolean(checked))}
775
+ aria-label={labels.table.selectAll}
776
+ />
777
+ ),
778
+ enableSorting: false,
779
+ meta: { priority: 1 },
780
+ cell: ({ row }) => (
781
+ <Checkbox
782
+ checked={selectedIds.includes(row.original.id)}
783
+ onCheckedChange={(checked) => toggleRowSelection(row.original.id, Boolean(checked))}
784
+ onClick={(event) => event.stopPropagation()}
785
+ aria-label={labels.table.selectRow}
786
+ />
787
+ ),
788
+ })
789
+ }
790
+
791
+ defs.push({
792
+ accessorKey: 'name',
793
+ header: labels.table.name,
794
+ meta: { priority: 1 },
795
+ cell: ({ row }) => {
796
+ const startDate = row.original.startDate ? formatDate(row.original.startDate) : null
797
+ return (
798
+ <div className="flex items-center gap-2.5">
799
+ <ProjectColorDot colorKey={row.original.color} projectName={row.original.name} size="sm" />
800
+ <div className="flex min-w-0 flex-col">
801
+ <span className="truncate text-sm font-medium text-foreground">{row.original.name}</span>
802
+ <span className="truncate font-mono text-xs text-muted-foreground">
803
+ {row.original.code ?? '—'}
804
+ {startDate ? ` · ${startDate}` : ''}
805
+ </span>
806
+ </div>
807
+ </div>
808
+ )
809
+ },
810
+ })
811
+
812
+ defs.push({
813
+ accessorKey: 'customerName',
814
+ id: 'customerName',
815
+ header: labels.table.customer,
816
+ enableSorting: false,
817
+ meta: { priority: 2 },
818
+ cell: ({ row }) => {
819
+ if (row.original.customerName) {
820
+ return <span className="text-sm text-muted-foreground">{row.original.customerName}</span>
821
+ }
822
+ // Only a project that genuinely has no customer gets the call to action.
823
+ // A row that carries the FK but no resolvable name (customer removed,
824
+ // customers module absent) is unnamed, not unassigned — inviting an
825
+ // assignment there would misstate the record.
826
+ if (canManageProjects && !row.original.customerId) {
827
+ return (
828
+ <Link
829
+ href={`/backend/staff/time-tracking/projects/${row.original.id}/edit`}
830
+ className="text-sm text-primary underline-offset-2 hover:underline"
831
+ onClick={(event) => event.stopPropagation()}
832
+ >
833
+ {labels.actions.assignCustomer}
834
+ </Link>
835
+ )
836
+ }
837
+ return <span className="text-xs text-muted-foreground/70">—</span>
838
+ },
839
+ })
840
+
841
+ defs.push({
842
+ accessorKey: 'status',
843
+ header: labels.table.status,
844
+ meta: { priority: 3 },
845
+ cell: ({ row }) => (
846
+ <StatusBadge variant={STATUS_VARIANTS[row.original.status] ?? 'neutral'} dot>
847
+ {labels.statuses[row.original.status as keyof typeof labels.statuses] ?? row.original.status}
848
+ </StatusBadge>
849
+ ),
850
+ })
851
+
852
+ defs.push(
853
+ isPmRole
854
+ ? {
855
+ accessorKey: 'members',
856
+ id: 'members',
857
+ header: labels.table.team,
858
+ enableSorting: false,
859
+ meta: { priority: 4 },
860
+ cell: ({ row }) => (
861
+ <ProjectMembersAvatarStack
862
+ members={row.original.members}
863
+ total={row.original.memberCount}
864
+ peopleCountLabel={`${row.original.memberCount}`}
865
+ />
866
+ ),
867
+ }
868
+ : {
869
+ accessorKey: 'myRole',
870
+ header: labels.table.myRole,
871
+ enableSorting: false,
872
+ meta: { priority: 4 },
873
+ cell: ({ row }) =>
874
+ row.original.myRole ? (
875
+ <span className="text-sm text-foreground">{row.original.myRole}</span>
876
+ ) : (
877
+ <span className="text-xs text-muted-foreground/70">—</span>
878
+ ),
879
+ },
880
+ )
881
+
882
+ if (canViewRates) {
883
+ defs.push({
884
+ accessorKey: 'hourlyRate',
885
+ id: 'hourlyRate',
886
+ header: rightHeader(labels.table.rate),
887
+ enableSorting: false,
888
+ meta: { priority: 5 },
889
+ cell: ({ row }) => (
890
+ <span className="block text-right font-mono text-sm tabular-nums text-foreground">
891
+ {formatCurrency(row.original.hourlyRate, row.original.currencyCode) ?? '—'}
892
+ </span>
893
+ ),
894
+ })
895
+ }
896
+
897
+ defs.push({
898
+ accessorKey: 'totalMinutes',
899
+ id: 'totalMinutes',
900
+ header: rightHeader(labels.table.hours),
901
+ enableSorting: false,
902
+ meta: { priority: 5 },
903
+ cell: ({ row }) => (
904
+ <span className="block text-right font-mono text-sm font-semibold tabular-nums text-foreground">
905
+ {row.original.totalMinutes > 0 ? formatDuration(row.original.totalMinutes, 'clock') : '—'}
906
+ </span>
907
+ ),
908
+ })
909
+
910
+ if (canViewRates) {
911
+ defs.push({
912
+ accessorKey: 'cost',
913
+ id: 'cost',
914
+ header: rightHeader(labels.table.cost),
915
+ enableSorting: false,
916
+ meta: { priority: 5 },
917
+ cell: ({ row }) => (
918
+ <span className="block text-right font-mono text-sm tabular-nums text-foreground">
919
+ {formatCurrency(row.original.cost, row.original.currencyCode) ?? '—'}
920
+ </span>
921
+ ),
922
+ })
923
+ }
924
+
925
+ defs.push({
926
+ accessorKey: 'budgetValue',
927
+ id: 'budget',
928
+ header: labels.table.budget,
929
+ enableSorting: false,
930
+ meta: { priority: 5 },
931
+ cell: ({ row }) => {
932
+ if (row.original.budgetKind === 'amount' && !canViewRates) {
933
+ return <span className="text-xs text-muted-foreground">—</span>
934
+ }
935
+ const burn = computeBudgetBurn({
936
+ kind: row.original.budgetKind,
937
+ budgetValue: row.original.budgetValue,
938
+ warnAtPercent: row.original.budgetWarnAtPercent,
939
+ totalMinutes: row.original.totalMinutes,
940
+ cost: row.original.cost,
941
+ })
942
+ const budgetLabel = burn
943
+ ? burn.kind === 'hours'
944
+ ? t('staff.time_tracking.projects.budget.hoursValue', '{value} h', {
945
+ value: Math.round(burn.budgetValue * 10) / 10,
946
+ })
947
+ : formatCurrency(burn.budgetValue, row.original.currencyCode) ?? `${burn.budgetValue}`
948
+ : null
949
+ return (
950
+ <ProjectBudgetCell
951
+ burn={burn}
952
+ usageLabel={
953
+ burn
954
+ ? t('staff.time_tracking.projects.budget.usage', '{percent}% of {value}', {
955
+ percent: burn.percent,
956
+ value: budgetLabel ?? '',
957
+ })
958
+ : null
959
+ }
960
+ noBudgetLabel={labels.budget.none}
961
+ ariaLabel={labels.budget.aria}
962
+ />
963
+ )
964
+ },
965
+ })
966
+
967
+ defs.push({
968
+ accessorKey: 'hoursWeek',
969
+ header: isPmRole ? labels.table.hoursWeek : labels.table.myHoursWeek,
970
+ enableSorting: false,
971
+ meta: { priority: 6 },
972
+ cell: ({ row }) => {
973
+ const stripe = resolveProjectColorHex(row.original.color, row.original.name)
974
+ return (
975
+ <div className="flex items-center justify-end gap-2">
976
+ <HoursSparkline
977
+ values={row.original.hoursTrend}
978
+ color={stripe}
979
+ ariaLabel={labels.card.sparklineAria}
980
+ />
981
+ <span className="text-xs font-medium tabular-nums text-foreground">
982
+ {row.original.hoursWeek > 0 ? `${row.original.hoursWeek}h` : '—'}
983
+ </span>
984
+ </div>
985
+ )
986
+ },
987
+ })
988
+
989
+ defs.push({
990
+ accessorKey: 'projectType',
991
+ header: labels.table.type,
992
+ meta: { priority: 7 },
993
+ cell: ({ row }) =>
994
+ row.original.projectType ? (
995
+ <span className="text-sm text-foreground">{row.original.projectType}</span>
996
+ ) : (
997
+ <span className="text-xs text-muted-foreground/70">—</span>
998
+ ),
999
+ })
1000
+
1001
+ defs.push({
1002
+ accessorKey: 'updatedAt',
1003
+ header: labels.table.updatedAt,
1004
+ meta: { priority: 8 },
1005
+ cell: ({ row }) => (
1006
+ <span className="text-xs text-muted-foreground">
1007
+ {formatRelativeTime(row.original.updatedAt, '—', t)}
1008
+ </span>
1009
+ ),
1010
+ })
1011
+
1012
+ return defs
1013
+ }, [
1014
+ allSelected,
1015
+ canManageProjects,
1016
+ canSelectRows,
1017
+ canViewRates,
1018
+ isPmRole,
1019
+ labels,
1020
+ selectedIds,
1021
+ someSelected,
1022
+ t,
1023
+ toggleAllSelection,
1024
+ toggleRowSelection,
1025
+ ])
1026
+
1027
+ const cardLabels = React.useMemo<ProjectCardLabels>(
1028
+ () => ({
1029
+ hoursPanelPm: labels.card.hoursPanelPm,
1030
+ hoursPanelCollab: labels.card.hoursPanelCollab,
1031
+ sparklineAria: labels.card.sparklineAria,
1032
+ peopleCount: (count: number) => `${count}`,
1033
+ role: labels.card.role,
1034
+ noCustomer: '—',
1035
+ statuses: labels.statuses,
1036
+ }),
1037
+ [labels.card, labels.statuses],
1038
+ )
1039
+
1040
+ const emptyStateCopy = React.useMemo(() => {
1041
+ const hasFiltersApplied = activeTab !== 'all' || search.trim().length > 0 || Object.values(filterValues).some(Boolean)
1042
+ if (hasFiltersApplied) return labels.emptyState.noMatches
1043
+ if (!canManageProjects) return labels.emptyState.noAssignments
1044
+ return labels.emptyState.noProjects
1045
+ }, [activeTab, search, filterValues, canManageProjects, labels.emptyState])
1046
+
1047
+ const cardsData: ProjectCardData[] = rows.map((row) => ({
1048
+ id: row.id,
1049
+ name: row.name,
1050
+ code: row.code,
1051
+ customerName: row.customerName,
1052
+ color: row.color,
1053
+ status: row.status,
1054
+ hoursWeek: row.hoursWeek,
1055
+ hoursTrend: row.hoursTrend,
1056
+ members: row.members,
1057
+ memberCount: row.memberCount,
1058
+ myRole: row.myRole,
1059
+ updatedAt: row.updatedAt,
1060
+ }))
1061
+
1062
+ const tableToolbar = (
1063
+ <FilterBar
1064
+ searchValue={search}
1065
+ onSearchChange={handleSearchChange}
1066
+ searchPlaceholder={labels.table.search}
1067
+ filters={filters}
1068
+ values={filterValues}
1069
+ onApply={handleFiltersApply}
1070
+ onClear={handleFiltersClear}
1071
+ trailingItems={
1072
+ canSelectRows ? (
1073
+ <ProjectsBulkActionsBar
1074
+ selectedCount={selectedIds.length}
1075
+ labels={{
1076
+ selectedCount: t('staff.time_tracking.projects.bulk.selectedCount', '{count} selected', {
1077
+ count: selectedIds.length,
1078
+ }),
1079
+ report: labels.bulk.report,
1080
+ assignMembers: labels.bulk.assignMembers,
1081
+ clear: labels.bulk.clear,
1082
+ }}
1083
+ canReport={canCreateReports}
1084
+ canAssignMembers={canManageProjects}
1085
+ reportBlockedReason={reportBlockedReason}
1086
+ isBusy={isAssigning}
1087
+ onReport={handleReportFromSelected}
1088
+ onAssignMembers={() => setAssignDialogOpen(true)}
1089
+ onClear={() => setSelectedIds([])}
1090
+ />
1091
+ ) : null
1092
+ }
1093
+ />
1094
+ )
1095
+
1096
+ return (
1097
+ <Page>
1098
+ <PageBody>
1099
+ <div className="mb-4">
1100
+ <ProjectsKpiStrip kpis={kpis} labels={kpiLabels} isLoading={isLoadingKpis} />
1101
+ </div>
1102
+
1103
+ <div className="mb-3 flex flex-wrap items-center justify-between gap-3">
1104
+ <SavedViewTabs
1105
+ tabs={tabs}
1106
+ activeId={activeTab}
1107
+ onSelect={handleTabSelect}
1108
+ ariaLabel={t('staff.timesheets.projects.portfolio.savedViews.ariaLabel', 'Saved views')}
1109
+ />
1110
+ <ViewModeToggle
1111
+ mode={viewMode}
1112
+ onChange={handleViewModeChange}
1113
+ tableLabel={labels.viewMode.table}
1114
+ cardsLabel={labels.viewMode.cards}
1115
+ ariaLabel={t('staff.timesheets.projects.portfolio.viewMode.ariaLabel', 'View mode')}
1116
+ />
1117
+ </div>
1118
+
1119
+ {viewMode === 'cards' ? (
1120
+ <div>
1121
+ {isLoading ? (
1122
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
1123
+ {Array.from({ length: 6 }).map((_, idx) => (
1124
+ <div key={idx} className="h-48 animate-pulse rounded-lg border border-border bg-muted/40" />
1125
+ ))}
1126
+ </div>
1127
+ ) : rows.length === 0 ? (
1128
+ <div className="rounded-lg border border-dashed border-border bg-card p-10 text-center">
1129
+ <p className="text-sm text-muted-foreground">{emptyStateCopy}</p>
1130
+ {canManageProjects ? (
1131
+ <div className="mt-3">
1132
+ <Button asChild size="sm">
1133
+ <Link href="/backend/staff/time-tracking/projects/create">
1134
+ {labels.actions.addFirst}
1135
+ </Link>
1136
+ </Button>
1137
+ </div>
1138
+ ) : null}
1139
+ </div>
1140
+ ) : (
1141
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
1142
+ {cardsData.map((card) => (
1143
+ <ProjectCard
1144
+ key={card.id}
1145
+ data={card}
1146
+ labels={cardLabels}
1147
+ showTeam={isPmRole}
1148
+ href={`/backend/staff/time-tracking/projects/${card.id}`}
1149
+ />
1150
+ ))}
1151
+ </div>
1152
+ )}
1153
+ </div>
1154
+ ) : (
1155
+ <DataTable<ProjectRow>
1156
+ extensionTableId={extensionPoints.hosts.timeProjectsTable.tableId}
1157
+ title={labels.title}
1158
+ titleHeadingLevel={1}
1159
+ data={rows}
1160
+ columns={columns}
1161
+ isLoading={isLoading}
1162
+ toolbar={tableToolbar}
1163
+ emptyState={
1164
+ <div className="py-12 text-center">
1165
+ <p className="text-sm text-muted-foreground mb-4">{emptyStateCopy}</p>
1166
+ {canManageProjects ? (
1167
+ <Button asChild size="sm">
1168
+ <Link href="/backend/staff/time-tracking/projects/create">{labels.actions.addFirst}</Link>
1169
+ </Button>
1170
+ ) : null}
1171
+ </div>
1172
+ }
1173
+ actions={
1174
+ canManageProjects ? (
1175
+ <Button asChild size="sm">
1176
+ <Link href="/backend/staff/time-tracking/projects/create">{labels.actions.add}</Link>
1177
+ </Button>
1178
+ ) : undefined
1179
+ }
1180
+ refreshButton={{
1181
+ label: labels.actions.refresh,
1182
+ onRefresh: handleRefresh,
1183
+ isRefreshing: isLoading || isRefreshing,
1184
+ }}
1185
+ sortable
1186
+ sorting={sorting}
1187
+ onSortingChange={setSorting}
1188
+ pagination={{
1189
+ page,
1190
+ pageSize: PAGE_SIZE,
1191
+ total,
1192
+ totalIsCapped,
1193
+ totalPages,
1194
+ onPageChange: setPage,
1195
+ }}
1196
+ stickyActionsColumn
1197
+ rowActions={(row) => (
1198
+ <RowActions
1199
+ items={[
1200
+ {
1201
+ id: 'view',
1202
+ label: labels.actions.viewDetails,
1203
+ href: `/backend/staff/time-tracking/projects/${row.id}`,
1204
+ },
1205
+ ...(canManageProjects
1206
+ ? [
1207
+ {
1208
+ id: 'delete',
1209
+ label: labels.actions.delete,
1210
+ destructive: true,
1211
+ onSelect: () => {
1212
+ void handleDelete(row)
1213
+ },
1214
+ },
1215
+ ]
1216
+ : []),
1217
+ ]}
1218
+ />
1219
+ )}
1220
+ onRowClick={(row) => router.push(`/backend/staff/time-tracking/projects/${row.id}`)}
1221
+ />
1222
+ )}
1223
+ </PageBody>
1224
+ {canManageProjects ? (
1225
+ <AssignMembersDialog
1226
+ open={assignDialogOpen}
1227
+ onOpenChange={setAssignDialogOpen}
1228
+ isSaving={isAssigning}
1229
+ labels={{
1230
+ title: labels.assign.title,
1231
+ description: t(
1232
+ 'staff.time_tracking.projects.assignMembers.description',
1233
+ 'Selected projects: {count}.',
1234
+ { count: selectedIds.length },
1235
+ ),
1236
+ searchPlaceholder: labels.assign.searchPlaceholder,
1237
+ empty: labels.assign.empty,
1238
+ loadError: labels.assign.loadError,
1239
+ loading: labels.assign.loading,
1240
+ role: labels.assign.role,
1241
+ rolePlaceholder: labels.assign.rolePlaceholder,
1242
+ startDate: labels.assign.startDate,
1243
+ cancel: labels.assign.cancel,
1244
+ confirm: labels.assign.confirm,
1245
+ }}
1246
+ onConfirm={(input) => {
1247
+ void handleAssignMembers(input)
1248
+ }}
1249
+ />
1250
+ ) : null}
1251
+ {ConfirmDialogElement}
1252
+ </Page>
1253
+ )
1254
+ }