@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,1420 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Badge } from '@open-mercato/ui/primitives/badge'
7
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
+ import { Input } from '@open-mercato/ui/primitives/input'
9
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
10
+ import { LookupSelect } from '@open-mercato/ui/backend/inputs/LookupSelect'
11
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
12
+ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
13
+ import { deleteCrud, createCrud } from '@open-mercato/ui/backend/utils/crud'
14
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
+ import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
18
+ import { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'
19
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
20
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
21
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
22
+ import { LoadingMessage } from '@open-mercato/ui/backend/detail'
23
+ import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
24
+ import { ArrowLeft, Plus, Pencil, ChevronDown, Trash2, User, Users } from 'lucide-react'
25
+ import Link from 'next/link'
26
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
27
+ import { createLogger } from '@open-mercato/shared/lib/logger'
28
+ import { formatCurrency } from '@open-mercato/ui/utils/format'
29
+ import { KpiCard, Sparkline } from '@open-mercato/ui/backend/charts'
30
+ import { ProjectTeamDrawer } from '../../../../../lib/time-tracking-ui/ProjectTeamDrawer'
31
+ import { NoProjectAccess } from '../../../../../lib/time-tracking-ui/NoProjectAccess'
32
+ import { ProjectBudgetCell } from '../../../../../lib/timesheets-projects-ui/ProjectBudgetCell'
33
+ import { computeBudgetBurn } from '../../../../../lib/timesheets-projects/budgetBurn'
34
+ import { resolveProjectColorHex } from '../../../../../lib/timesheets-ui/colors'
35
+
36
+ const logger = createLogger('staff')
37
+
38
+ const BACK_HREF = '/backend/staff/time-tracking/projects'
39
+
40
+ const EMPLOYEE_MUTATION_CONTEXT_ID = 'staff-time-project-detail:employees'
41
+ const EMPLOYEE_RESOURCE_KIND = 'staff.time_project_member'
42
+
43
+ type EmployeeMutationContext = {
44
+ formId: string
45
+ resourceKind: string
46
+ resourceId: string
47
+ retryLastMutation: () => Promise<boolean>
48
+ }
49
+
50
+ /**
51
+ * Mirrors `NO_PROJECT_ACCESS_REASON` from
52
+ * `api/timesheets/time-projects/route.ts`. The route module is server-only, so
53
+ * the literal is restated here rather than imported into the client bundle;
54
+ * `__tests__/no-project-access-contract.test.ts` pins the two together.
55
+ */
56
+ const NO_PROJECT_ACCESS_REASON = 'no_project_access'
57
+
58
+ const PROJECT_ENTITY_ID = 'staff:staff_time_project'
59
+
60
+ type ProjectDetailInjectionContext = {
61
+ entityId: string
62
+ recordId: string | null
63
+ projectId: string | null
64
+ path: string | null
65
+ retryLastMutation: () => Promise<boolean>
66
+ }
67
+
68
+ type ProjectRecord = {
69
+ id: string
70
+ name: string
71
+ code: string
72
+ description?: string | null
73
+ projectType?: string | null
74
+ project_type?: string | null
75
+ startDate?: string | null
76
+ start_date?: string | null
77
+ costCenter?: string | null
78
+ cost_center?: string | null
79
+ status?: string | null
80
+ customerId?: string | null
81
+ customer_id?: string | null
82
+ } & Record<string, unknown>
83
+
84
+ type ProjectResponse = {
85
+ items?: ProjectRecord[]
86
+ }
87
+
88
+ function readDenialReason(payload: unknown): string | null {
89
+ if (!payload || typeof payload !== 'object') return null
90
+ const reason = (payload as { reason?: unknown }).reason
91
+ return typeof reason === 'string' ? reason : null
92
+ }
93
+
94
+ type EmployeeAssignment = {
95
+ id: string
96
+ staffMemberId: string
97
+ role: string | null
98
+ status: string | null
99
+ assignedStartDate: string | null
100
+ assignedEndDate: string | null
101
+ displayName: string | null
102
+ teamName: string | null
103
+ }
104
+
105
+ type EmployeesResponse = {
106
+ items?: Array<Record<string, unknown>>
107
+ total?: number
108
+ }
109
+
110
+ type StaffMemberRecord = {
111
+ id: string
112
+ display_name?: string
113
+ displayName?: string
114
+ team?: { id: string; name: string } | null
115
+ }
116
+
117
+ type StaffMembersResponse = {
118
+ items?: StaffMemberRecord[]
119
+ }
120
+
121
+ type ProjectEntryRow = {
122
+ id: string
123
+ date: string
124
+ description: string | null
125
+ durationMinutes: number
126
+ isBillable: boolean
127
+ taskId: string | null
128
+ staffMemberId: string | null
129
+ }
130
+
131
+ type ProjectTaskRow = {
132
+ id: string
133
+ reference: string | null
134
+ title: string
135
+ statusId: string | null
136
+ assigneeStaffMemberId: string | null
137
+ loggedMinutes: number | null
138
+ }
139
+
140
+ /**
141
+ * A group heading inside the reference rail. The rail replaces three full-width
142
+ * cards, so the groups carry the separation those card borders used to.
143
+ */
144
+ function RailGroup({ label, first }: { label: string; first?: boolean }) {
145
+ return (
146
+ <div
147
+ className={`border-b pb-1.5 text-overline font-semibold uppercase tracking-widest text-muted-foreground ${
148
+ first ? '' : 'pt-4'
149
+ }`}
150
+ >
151
+ {label}
152
+ </div>
153
+ )
154
+ }
155
+
156
+ function RailFact({ label, value }: { label: string; value: React.ReactNode }) {
157
+ return (
158
+ <div className="flex items-baseline justify-between gap-3 border-b py-2 text-sm last:border-b-0">
159
+ <dt className="shrink-0 text-muted-foreground">{label}</dt>
160
+ <dd className="text-right font-medium tabular-nums">{value}</dd>
161
+ </div>
162
+ )
163
+ }
164
+
165
+ export default function TimesheetProjectDetailPage({ params }: { params?: { id?: string } }) {
166
+ const projectId = params?.id
167
+ const t = useT()
168
+ const scopeVersion = useOrganizationScopeVersion()
169
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
170
+ const router = useRouter()
171
+ const pathname = usePathname()
172
+ const searchParams = useSearchParams()
173
+
174
+ const [project, setProject] = React.useState<ProjectRecord | null>(null)
175
+ const [loading, setLoading] = React.useState(true)
176
+ const [error, setError] = React.useState<string | null>(null)
177
+ const [isNotFound, setIsNotFound] = React.useState(false)
178
+ const [accessDenied, setAccessDenied] = React.useState(false)
179
+
180
+ const [employees, setEmployees] = React.useState<EmployeeAssignment[]>([])
181
+ const [employeesLoading, setEmployeesLoading] = React.useState(false)
182
+ const [expandedCards, setExpandedCards] = React.useState<Set<string>>(new Set())
183
+ const [reloadToken, setReloadToken] = React.useState(0)
184
+
185
+ // Tabs govern the working column only — the details rail stays visible on every
186
+ // tab, because reference facts are what you check *while* reading the tab you
187
+ // are on, not a destination of their own.
188
+ const [activeTab, setActiveTab] = React.useState<string>('team')
189
+ const [recentEntries, setRecentEntries] = React.useState<ProjectEntryRow[]>([])
190
+ const [recentEntriesLoading, setRecentEntriesLoading] = React.useState(false)
191
+ const [projectTasks, setProjectTasks] = React.useState<ProjectTaskRow[]>([])
192
+ const [projectTasksLoading, setProjectTasksLoading] = React.useState(false)
193
+
194
+ const [canManageProjects, setCanManageProjects] = React.useState(false)
195
+ // Money is gated on the same feature the portfolio gates it on; a rate hidden
196
+ // in the list must not be readable one click deeper.
197
+ const [canSeeMoney, setCanSeeMoney] = React.useState(false)
198
+
199
+ React.useEffect(() => {
200
+ let cancelled = false
201
+ void (async () => {
202
+ try {
203
+ const res = await apiCall<{ ok: boolean; granted: string[] }>('/api/auth/feature-check', {
204
+ method: 'POST',
205
+ headers: { 'content-type': 'application/json' },
206
+ body: JSON.stringify({
207
+ features: ['staff.timesheets.projects.manage', 'staff.timesheets.rates.view'],
208
+ }),
209
+ })
210
+ if (!cancelled) {
211
+ const granted = new Set(res.result?.granted ?? [])
212
+ setCanManageProjects(granted.has('staff.timesheets.projects.manage'))
213
+ setCanSeeMoney(granted.has('staff.timesheets.rates.view'))
214
+ }
215
+ } catch {
216
+ // default: no manage access, no money
217
+ }
218
+ })()
219
+ return () => { cancelled = true }
220
+ }, [])
221
+
222
+ const [addDialogOpen, setAddDialogOpen] = React.useState(false)
223
+ const [addStaffMemberId, setAddStaffMemberId] = React.useState<string | null>(null)
224
+ const [addRole, setAddRole] = React.useState('')
225
+ const [addStartDate, setAddStartDate] = React.useState('')
226
+ const [addSaving, setAddSaving] = React.useState(false)
227
+ const [staffLookupError, setStaffLookupError] = React.useState<string | null>(null)
228
+
229
+ // Fetched per tab rather than up front: a project page opened to check a rate
230
+ // should not pay for two lists nobody looked at.
231
+ React.useEffect(() => {
232
+ if (!projectId || activeTab !== 'time') return
233
+ let cancelled = false
234
+ void (async () => {
235
+ setRecentEntriesLoading(true)
236
+ try {
237
+ const res = await apiCall<{ items?: Array<Record<string, unknown>> }>(
238
+ `/api/staff/timesheets/time-entries?projectId=${encodeURIComponent(projectId)}&page=1&pageSize=25&sortField=date&sortDir=desc`,
239
+ )
240
+ if (cancelled || !res.ok) return
241
+ const items = Array.isArray(res.result?.items) ? res.result.items : []
242
+ setRecentEntries(
243
+ items
244
+ .map((row): ProjectEntryRow | null => {
245
+ const id = typeof row.id === 'string' ? row.id : null
246
+ if (!id) return null
247
+ const raw = row as Record<string, unknown>
248
+ const minutes = raw.duration_minutes ?? raw.durationMinutes
249
+ return {
250
+ id,
251
+ date: typeof raw.date === 'string' ? raw.date.slice(0, 10) : '',
252
+ description:
253
+ typeof raw.description === 'string'
254
+ ? raw.description
255
+ : typeof raw.notes === 'string'
256
+ ? raw.notes
257
+ : null,
258
+ durationMinutes: typeof minutes === 'number' ? minutes : Number(minutes ?? 0) || 0,
259
+ isBillable: raw.is_billable === true || raw.isBillable === true,
260
+ taskId: typeof (raw.task_id ?? raw.taskId) === 'string' ? String(raw.task_id ?? raw.taskId) : null,
261
+ staffMemberId:
262
+ typeof (raw.staff_member_id ?? raw.staffMemberId) === 'string'
263
+ ? String(raw.staff_member_id ?? raw.staffMemberId)
264
+ : null,
265
+ }
266
+ })
267
+ .filter((row): row is ProjectEntryRow => row !== null),
268
+ )
269
+ } catch (err) {
270
+ logger.warn('staff.timesheets.projects.detail recent entries failed', { err })
271
+ } finally {
272
+ if (!cancelled) setRecentEntriesLoading(false)
273
+ }
274
+ })()
275
+ return () => { cancelled = true }
276
+ }, [projectId, activeTab, reloadToken])
277
+
278
+ React.useEffect(() => {
279
+ if (!projectId || activeTab !== 'tasks') return
280
+ let cancelled = false
281
+ void (async () => {
282
+ setProjectTasksLoading(true)
283
+ try {
284
+ const res = await apiCall<{ items?: Array<Record<string, unknown>> }>(
285
+ // `timeProjectId`, not `projectId`: the tasks list schema declares the
286
+ // former and is `.passthrough()`, so the wrong name is accepted, never
287
+ // read, and the tab silently lists every task in the tenant.
288
+ `/api/staff/timesheets/tasks?timeProjectId=${encodeURIComponent(projectId)}&page=1&pageSize=50&sortField=reference&sortDir=asc`,
289
+ )
290
+ if (cancelled || !res.ok) return
291
+ const items = Array.isArray(res.result?.items) ? res.result.items : []
292
+ setProjectTasks(
293
+ items
294
+ .map((row): ProjectTaskRow | null => {
295
+ const id = typeof row.id === 'string' ? row.id : null
296
+ const title = typeof row.title === 'string' ? row.title : null
297
+ if (!id || !title) return null
298
+ const raw = row as Record<string, unknown>
299
+ const logged = raw.loggedMinutes ?? raw.logged_minutes
300
+ return {
301
+ id,
302
+ reference: typeof raw.reference === 'string' ? raw.reference : null,
303
+ title,
304
+ statusId:
305
+ typeof (raw.task_status_id ?? raw.taskStatusId) === 'string'
306
+ ? String(raw.task_status_id ?? raw.taskStatusId)
307
+ : null,
308
+ assigneeStaffMemberId:
309
+ typeof (raw.assignee_staff_member_id ?? raw.assigneeStaffMemberId) === 'string'
310
+ ? String(raw.assignee_staff_member_id ?? raw.assigneeStaffMemberId)
311
+ : null,
312
+ loggedMinutes: typeof logged === 'number' ? logged : null,
313
+ }
314
+ })
315
+ .filter((row): row is ProjectTaskRow => row !== null),
316
+ )
317
+ } catch (err) {
318
+ logger.warn('staff.timesheets.projects.detail tasks failed', { err })
319
+ } finally {
320
+ if (!cancelled) setProjectTasksLoading(false)
321
+ }
322
+ })()
323
+ return () => { cancelled = true }
324
+ }, [projectId, activeTab, reloadToken])
325
+
326
+ const employeeNameById = React.useMemo(
327
+ () => new Map(employees.map((emp) => [emp.staffMemberId, emp.displayName ?? ''])),
328
+ [employees],
329
+ )
330
+ const activeCount = employees.filter((emp) => emp.status === 'active').length
331
+ const inactiveCount = employees.length - activeCount
332
+
333
+ // --- Load project ---
334
+ React.useEffect(() => {
335
+ if (!projectId) return
336
+ let cancelled = false
337
+ async function loadProject() {
338
+ setLoading(true)
339
+ setError(null)
340
+ setIsNotFound(false)
341
+ setAccessDenied(false)
342
+ try {
343
+ const queryParams = new URLSearchParams({ page: '1', pageSize: '1', ids: projectId! })
344
+ const call = await apiCall<ProjectResponse>(
345
+ `/api/staff/timesheets/time-projects?${queryParams.toString()}`,
346
+ )
347
+ // Screen 17: the route answers 404 with this discriminator for a project
348
+ // the caller is not a member of, without naming it.
349
+ if (call.status === 404 && readDenialReason(call.result) === NO_PROJECT_ACCESS_REASON) {
350
+ if (!cancelled) setAccessDenied(true)
351
+ return
352
+ }
353
+ if (!call.ok) {
354
+ throw new Error(t('staff.timesheets.projects.errors.load', 'Failed to load project.'))
355
+ }
356
+ const payload = call.result
357
+ const record = Array.isArray(payload?.items) ? payload.items[0] : null
358
+ if (!record) {
359
+ if (!cancelled) setIsNotFound(true)
360
+ return
361
+ }
362
+ if (!cancelled) setProject(record)
363
+ } catch (loadError) {
364
+ if (!cancelled) {
365
+ setError(loadError instanceof Error ? loadError.message : t('staff.timesheets.projects.errors.load', 'Failed to load project.'))
366
+ }
367
+ } finally {
368
+ if (!cancelled) setLoading(false)
369
+ }
370
+ }
371
+ loadProject()
372
+ return () => { cancelled = true }
373
+ }, [projectId, t, scopeVersion])
374
+
375
+ // --- Load employees with name resolution ---
376
+ const loadEmployees = React.useCallback(async () => {
377
+ if (!projectId || accessDenied) return
378
+ setEmployeesLoading(true)
379
+ try {
380
+ const payload = await readApiResultOrThrow<EmployeesResponse>(
381
+ `/api/staff/timesheets/time-projects/${projectId}/employees?page=1&pageSize=100`,
382
+ undefined,
383
+ { errorMessage: t('staff.timesheets.projects.employees.empty', 'No employees assigned yet.'), fallback: { items: [], total: 0 } },
384
+ )
385
+ const items = Array.isArray(payload.items) ? payload.items : []
386
+
387
+ const staffMemberIds = items
388
+ .map((item) => String(item.staff_member_id ?? item.staffMemberId ?? ''))
389
+ .filter((id) => id.length > 0)
390
+
391
+ let staffMap = new Map<string, StaffMemberRecord>()
392
+ if (staffMemberIds.length > 0) {
393
+ try {
394
+ const staffRes = await apiCall<StaffMembersResponse>(
395
+ `/api/staff/team-members?ids=${staffMemberIds.join(',')}&pageSize=100`,
396
+ )
397
+ if (staffRes.ok) {
398
+ const staffItems = Array.isArray(staffRes.result?.items) ? staffRes.result.items : []
399
+ staffMap = new Map(staffItems.map((member) => [member.id, member]))
400
+ }
401
+ } catch {
402
+ // name resolution failed (e.g. 403 for employees) — show IDs as fallback
403
+ }
404
+ }
405
+
406
+ const mapped: EmployeeAssignment[] = items.map((item) => {
407
+ const staffMemberId = String(item.staff_member_id ?? item.staffMemberId ?? '')
408
+ const staff = staffMap.get(staffMemberId)
409
+ return {
410
+ id: String(item.id ?? ''),
411
+ staffMemberId,
412
+ role: typeof item.role === 'string' ? item.role : null,
413
+ status: typeof item.status === 'string' ? item.status : null,
414
+ assignedStartDate: String(item.assigned_start_date ?? item.assignedStartDate ?? ''),
415
+ assignedEndDate: typeof (item.assigned_end_date ?? item.assignedEndDate) === 'string'
416
+ ? String(item.assigned_end_date ?? item.assignedEndDate)
417
+ : null,
418
+ displayName: staff?.display_name ?? staff?.displayName ?? null,
419
+ teamName: staff?.team?.name ?? null,
420
+ }
421
+ })
422
+
423
+ setEmployees(mapped)
424
+ } catch (loadError) {
425
+ logger.error('staff.timesheets.projects.employees.list', { err: loadError })
426
+ } finally {
427
+ setEmployeesLoading(false)
428
+ }
429
+ }, [projectId, accessDenied, t])
430
+
431
+ React.useEffect(() => {
432
+ void loadEmployees()
433
+ }, [loadEmployees, reloadToken, scopeVersion])
434
+
435
+ // --- Toggle card expand/collapse ---
436
+ const toggleCard = React.useCallback((employeeId: string) => {
437
+ setExpandedCards((prev) => {
438
+ const next = new Set(prev)
439
+ if (next.has(employeeId)) next.delete(employeeId)
440
+ else next.add(employeeId)
441
+ return next
442
+ })
443
+ }, [])
444
+
445
+ const { runMutation, retryLastMutation } = useGuardedMutation<EmployeeMutationContext>({
446
+ contextId: EMPLOYEE_MUTATION_CONTEXT_ID,
447
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
448
+ })
449
+
450
+ const projectInjectionContext = React.useMemo<ProjectDetailInjectionContext>(
451
+ () => ({
452
+ entityId: PROJECT_ENTITY_ID,
453
+ recordId: projectId ?? null,
454
+ projectId: projectId ?? null,
455
+ path: pathname ?? null,
456
+ retryLastMutation,
457
+ }),
458
+ [pathname, projectId, retryLastMutation],
459
+ )
460
+
461
+ const { widgets: injectedTabWidgets } = useInjectionWidgets<ProjectDetailInjectionContext>(
462
+ extensionPoints.hosts.projectDetailTabs.spotId,
463
+ { context: projectInjectionContext, triggerOnLoad: true },
464
+ )
465
+
466
+ const employeeMutationContext = React.useCallback(
467
+ (resourceId: string): EmployeeMutationContext => ({
468
+ formId: EMPLOYEE_MUTATION_CONTEXT_ID,
469
+ resourceKind: EMPLOYEE_RESOURCE_KIND,
470
+ resourceId,
471
+ retryLastMutation,
472
+ }),
473
+ [retryLastMutation],
474
+ )
475
+
476
+ // optimistic-lock-exempt: the employee add/remove calls below mutate the
477
+ // time-project ↔ staff-member assignment junction (createCrud/deleteCrud of a
478
+ // membership row), not a shared editable aggregate. Junction add/remove is exempt
479
+ // from OSS optimistic locking per the data-integrity contract.
480
+ // --- Remove employee ---
481
+ const handleRemoveEmployee = React.useCallback(async (emp: EmployeeAssignment) => {
482
+ const confirmed = await confirm({
483
+ title: t('staff.timesheets.projects.employees.remove', 'Remove'),
484
+ text: t('staff.timesheets.projects.employees.removeConfirm', 'Remove this employee from the project?'),
485
+ variant: 'destructive',
486
+ })
487
+ if (!confirmed) return
488
+ try {
489
+ await runMutation({
490
+ operation: () =>
491
+ deleteCrud(
492
+ `staff/timesheets/time-projects/${projectId}/employees`,
493
+ emp.id,
494
+ { errorMessage: t('staff.timesheets.projects.employees.removeError', 'Failed to remove employee.') },
495
+ ),
496
+ context: employeeMutationContext(emp.id),
497
+ mutationPayload: { id: emp.id },
498
+ })
499
+ flash(t('staff.timesheets.projects.employees.removed', 'Employee removed.'), 'success')
500
+ setReloadToken((token) => token + 1)
501
+ } catch (removeError) {
502
+ if (surfaceRecordConflict(removeError, t)) return
503
+ flash(t('staff.timesheets.projects.employees.removeError', 'Failed to remove employee.'), 'error')
504
+ }
505
+ }, [projectId, confirm, employeeMutationContext, runMutation, t])
506
+
507
+ // --- Add employee dialog ---
508
+ const openAddDialog = React.useCallback(() => {
509
+ setAddStaffMemberId(null)
510
+ setAddRole('')
511
+ setAddStartDate(new Date().toISOString().slice(0, 10))
512
+ setStaffLookupError(null)
513
+ setAddDialogOpen(true)
514
+ }, [])
515
+
516
+ // A failed lookup is not an empty roster: swallowing it here left the picker
517
+ // claiming "No team members found" while the request had actually errored.
518
+ const staffLookupErrorMessage = t(
519
+ 'staff.timesheets.projects.employees.loadError',
520
+ 'Could not load team members. Check your connection and try again.',
521
+ )
522
+
523
+ const fetchStaffMembers = React.useCallback(async (query: string) => {
524
+ const params = new URLSearchParams({ search: query, pageSize: '20', isActive: 'true' })
525
+ try {
526
+ const payload = await readApiResultOrThrow<StaffMembersResponse>(
527
+ `/api/staff/team-members?${params.toString()}`,
528
+ undefined,
529
+ { errorMessage: staffLookupErrorMessage, fallback: { items: [] } },
530
+ )
531
+ const items = Array.isArray(payload.items) ? payload.items : []
532
+ setStaffLookupError(null)
533
+ return items.map((member) => ({
534
+ id: member.id,
535
+ title: member.display_name ?? member.displayName ?? member.id,
536
+ subtitle: member.team?.name ?? null,
537
+ }))
538
+ } catch (lookupError) {
539
+ logger.error('staff.timesheets.projects.employees.lookup', { err: lookupError })
540
+ setStaffLookupError(staffLookupErrorMessage)
541
+ throw lookupError
542
+ }
543
+ }, [staffLookupErrorMessage])
544
+
545
+ const handleAddEmployee = React.useCallback(async () => {
546
+ if (!projectId || !addStaffMemberId || !addStartDate) return
547
+ const payload = {
548
+ staffMemberId: addStaffMemberId,
549
+ timeProjectId: projectId,
550
+ role: addRole.trim() || null,
551
+ assignedStartDate: addStartDate,
552
+ }
553
+ setAddSaving(true)
554
+ try {
555
+ await runMutation({
556
+ operation: () =>
557
+ createCrud(`staff/timesheets/time-projects/${projectId}/employees`, payload, {
558
+ errorMessage: t('staff.timesheets.projects.employees.addError', 'Failed to add employee.'),
559
+ }),
560
+ context: employeeMutationContext(addStaffMemberId),
561
+ mutationPayload: payload,
562
+ })
563
+ flash(t('staff.timesheets.projects.employees.added', 'Employee added.'), 'success')
564
+ setAddDialogOpen(false)
565
+ setReloadToken((token) => token + 1)
566
+ } catch (addError) {
567
+ if (surfaceRecordConflict(addError, t)) return
568
+ flash(addError instanceof Error ? addError.message : t('staff.timesheets.projects.employees.addError', 'Failed to add employee.'), 'error')
569
+ } finally {
570
+ setAddSaving(false)
571
+ }
572
+ }, [projectId, addStaffMemberId, addRole, addStartDate, employeeMutationContext, runMutation, t])
573
+
574
+ // Screen 5: the team drawer lives behind `?panel=team` so the project form
575
+ // link and the access-request notification (D-6, `&requesterUserId=`) can both
576
+ // deep-link into it. Assignment is a Team-Leader affordance, so the panel only
577
+ // mounts for holders of `staff.timesheets.projects.manage`.
578
+ const teamPanelRequested = searchParams?.get('panel') === 'team'
579
+ const teamDrawerOpen = teamPanelRequested && canManageProjects
580
+ const requesterUserId = searchParams?.get('requesterUserId') ?? null
581
+
582
+ const openTeamDrawer = React.useCallback(() => {
583
+ router.replace(`${pathname}?panel=team`)
584
+ }, [pathname, router])
585
+
586
+ const handleTeamDrawerOpenChange = React.useCallback((next: boolean) => {
587
+ if (next) openTeamDrawer()
588
+ else router.replace(pathname ?? BACK_HREF)
589
+ }, [openTeamDrawer, pathname, router])
590
+
591
+ const handleTeamSaved = React.useCallback(() => {
592
+ setReloadToken((token) => token + 1)
593
+ }, [])
594
+
595
+ const handleAddDialogKeyDown = React.useCallback((event: React.KeyboardEvent) => {
596
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
597
+ event.preventDefault()
598
+ void handleAddEmployee()
599
+ }
600
+ }, [handleAddEmployee])
601
+
602
+ // --- Render ---
603
+ if (loading) {
604
+ return (
605
+ <Page>
606
+ <PageBody>
607
+ <LoadingMessage label={t('staff.timesheets.projects.loading', 'Loading project...')} />
608
+ </PageBody>
609
+ </Page>
610
+ )
611
+ }
612
+
613
+ // Screen 17 — the caller may not open this project. The guard state names
614
+ // neither the customer nor the project (note 1), and the surrounding shell
615
+ // (sidebar, topbar) is untouched because the caller still has other projects
616
+ // (note 3).
617
+ if (accessDenied) {
618
+ return <NoProjectAccess timeProjectId={projectId} />
619
+ }
620
+
621
+ if (isNotFound) {
622
+ return (
623
+ <Page>
624
+ <PageBody>
625
+ <RecordNotFoundState
626
+ label={t('staff.timesheets.projects.errors.notFound', 'Project not found.')}
627
+ backHref={BACK_HREF}
628
+ backLabel={t('staff.timesheets.projects.actions.backToList', 'Back to projects')}
629
+ />
630
+ </PageBody>
631
+ </Page>
632
+ )
633
+ }
634
+
635
+ if (error || !project) {
636
+ return (
637
+ <Page>
638
+ <PageBody>
639
+ <ErrorMessage label={error ?? t('staff.timesheets.projects.errors.load', 'Failed to load project.')} />
640
+ </PageBody>
641
+ </Page>
642
+ )
643
+ }
644
+
645
+ const projectStatus = project.status ?? 'active'
646
+ const projectType = project.projectType ?? project.project_type ?? null
647
+ const projectStartDate = project.startDate ?? project.start_date ?? null
648
+ const projectCode = project.code
649
+
650
+ // Everything below already arrives in the list projection this page reads with
651
+ // `?ids=`; the card used to render five of the ~22 columns and drop the rest,
652
+ // so rate, budget, customer and cost centre were only visible by opening Edit.
653
+ const readString = (...keys: string[]): string | null => {
654
+ for (const key of keys) {
655
+ const raw = (project as Record<string, unknown>)[key]
656
+ if (typeof raw === 'string' && raw.trim().length > 0) return raw.trim()
657
+ if (typeof raw === 'number' && Number.isFinite(raw)) return String(raw)
658
+ }
659
+ return null
660
+ }
661
+ const readNumber = (...keys: string[]): number | null => {
662
+ for (const key of keys) {
663
+ const raw = (project as Record<string, unknown>)[key]
664
+ const parsed = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN
665
+ if (Number.isFinite(parsed)) return parsed
666
+ }
667
+ return null
668
+ }
669
+ const readBool = (...keys: string[]): boolean | null => {
670
+ for (const key of keys) {
671
+ const raw = (project as Record<string, unknown>)[key]
672
+ if (typeof raw === 'boolean') return raw
673
+ }
674
+ return null
675
+ }
676
+
677
+ const enrichment = ((project as Record<string, unknown>)._staff ?? {}) as Record<string, unknown>
678
+ const readEnriched = (key: string): number | null => {
679
+ const raw = enrichment[key]
680
+ return typeof raw === 'number' && Number.isFinite(raw) ? raw : null
681
+ }
682
+
683
+ const customerSnapshot = ((project as Record<string, unknown>).customerSnapshot
684
+ ?? (project as Record<string, unknown>).customer_snapshot) as Record<string, unknown> | null | undefined
685
+ const customerName =
686
+ (typeof customerSnapshot?.name === 'string' ? customerSnapshot.name : null)
687
+ ?? (typeof enrichment.customerName === 'string' ? enrichment.customerName : null)
688
+ const customerId = readString('customerId', 'customer_id')
689
+ const projectColor = readString('color')
690
+ const costCenter = readString('costCenter', 'cost_center')
691
+ const currencyCode = readString('currencyCode', 'currency_code')
692
+ const hourlyRate = readNumber('hourlyRate', 'hourly_rate')
693
+ const billableByDefault = readBool('billableByDefault', 'billable_by_default')
694
+ const createdAt = readString('createdAt', 'created_at')
695
+ const updatedAt = readString('updatedAt', 'updated_at')
696
+ const entryCount = readNumber('entryCount')
697
+ const currencyLocked = readBool('currencyLocked') ?? false
698
+ const lockedEntryCount = readNumber('lockedEntryCount')
699
+ const totalMinutes = readEnriched('totalMinutes') ?? 0
700
+ const billableMinutes = readEnriched('billableMinutes')
701
+ const cost = readEnriched('cost')
702
+ const nonBillableMinutes = billableMinutes === null ? null : Math.max(0, totalMinutes - billableMinutes)
703
+ const billablePercent =
704
+ billableMinutes === null || totalMinutes <= 0 ? null : Math.round((billableMinutes / totalMinutes) * 100)
705
+ const hoursTrend = Array.isArray(enrichment.hoursTrend)
706
+ ? (enrichment.hoursTrend as unknown[]).filter((value): value is number => typeof value === 'number')
707
+ : []
708
+
709
+ const budgetKind = readString('budgetKind', 'budget_kind')
710
+ const budgetValue = readNumber('budgetValue', 'budget_value')
711
+ const budgetWarnAt = readNumber('budgetWarnAtPercent', 'budget_warn_at_percent')
712
+ const budgetBurn = computeBudgetBurn({
713
+ kind: budgetKind as 'none' | 'hours' | 'amount' | null,
714
+ budgetValue,
715
+ warnAtPercent: budgetWarnAt,
716
+ totalMinutes,
717
+ cost,
718
+ })
719
+
720
+ const formatHours = (minutes: number | null): string => {
721
+ if (minutes === null) return '—'
722
+ const sign = minutes < 0 ? '-' : ''
723
+ const abs = Math.abs(minutes)
724
+ return `${sign}${Math.floor(abs / 60)}:${String(abs % 60).padStart(2, '0')}`
725
+ }
726
+ const formatDateTime = (value: string | null): string => {
727
+ if (!value) return '—'
728
+ // The query engine returns `2026-05-24 00:00:00+00` — a space separator and a
729
+ // two-digit offset, neither of which `Date` accepts, so both need widening
730
+ // before parsing or every timestamp renders raw.
731
+ //
732
+ // The offset is only widened when a clock precedes it: anchoring on the
733
+ // trailing `-24` alone also matches the day of a date-only `2026-05-24`,
734
+ // which turns a valid date into `2026-05-24:00` and renders it raw.
735
+ const normalized = value
736
+ .replace(' ', 'T')
737
+ .replace(/(T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)([+-]\d{2})$/, '$1$2:00')
738
+ const parsed = new Date(normalized)
739
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()
740
+ }
741
+ const budgetUsageLabel = budgetBurn
742
+ ? budgetBurn.kind === 'hours'
743
+ ? t('staff.timesheets.projects.detail.budgetHours', '{percent}% of {value} h', {
744
+ percent: budgetBurn.percent,
745
+ value: budgetBurn.budgetValue,
746
+ })
747
+ : t('staff.timesheets.projects.detail.budgetAmount', '{percent}% of {value}', {
748
+ percent: budgetBurn.percent,
749
+ value: formatCurrency(budgetBurn.budgetValue, currencyCode ?? undefined) ?? String(budgetBurn.budgetValue),
750
+ })
751
+ : null
752
+
753
+ const injectedTabs = injectedTabWidgets
754
+ .filter((widget) => (widget.placement?.kind ?? 'tab') === 'tab')
755
+ .map((widget) => {
756
+ const tabId = widget.placement?.groupId ?? widget.widgetId
757
+ const groupLabel = widget.placement?.groupLabel
758
+ const label = groupLabel ? t(groupLabel, groupLabel) : widget.module.metadata.title ?? tabId
759
+ const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0
760
+ return { tabId, label, priority, Widget: widget.module.Widget }
761
+ })
762
+ .sort((left, right) => right.priority - left.priority)
763
+
764
+ return (
765
+ <Page>
766
+ <PageBody>
767
+ <div className="space-y-6">
768
+ {/*
769
+ * Identity first: colour, name, and the two states worth knowing before
770
+ * anything else — is it running, and is the budget in trouble. The code,
771
+ * customer, type and start date sit under it as one meta line rather than
772
+ * four labelled rows, because none of them is a decision on its own.
773
+ */}
774
+ <div className="flex flex-wrap items-start justify-between gap-4">
775
+ <div className="flex min-w-0 items-start gap-3">
776
+ <Button variant="ghost" size="icon" asChild>
777
+ <Link href={BACK_HREF}>
778
+ <ArrowLeft className="h-4 w-4" aria-hidden />
779
+ </Link>
780
+ </Button>
781
+ <div className="flex min-w-0 flex-col gap-1.5">
782
+ <div className="flex flex-wrap items-center gap-2">
783
+ <span
784
+ className="size-3 shrink-0 rounded-sm border border-border"
785
+ style={{ backgroundColor: resolveProjectColorHex(projectColor, project.name) }}
786
+ aria-hidden
787
+ />
788
+ <h1 className="text-xl font-semibold">{project.name}</h1>
789
+ <Badge
790
+ variant={projectStatus === 'active' ? 'success' : projectStatus === 'on_hold' ? 'warning' : 'neutral'}
791
+ className="capitalize"
792
+ >
793
+ {projectStatus}
794
+ </Badge>
795
+ {canSeeMoney && budgetBurn && budgetBurn.tone !== 'accent' ? (
796
+ <Badge variant={budgetBurn.tone === 'destructive' ? 'destructive' : 'warning'}>
797
+ {t('staff.timesheets.projects.detail.budgetBadge', 'Budget {percent}%', {
798
+ percent: budgetBurn.percent,
799
+ })}
800
+ </Badge>
801
+ ) : null}
802
+ <InjectionSpot
803
+ spotId={extensionPoints.hosts.projectDetailStatusBadges.spotId}
804
+ context={projectInjectionContext}
805
+ data={project}
806
+ />
807
+ </div>
808
+ <div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
809
+ <span className="rounded-sm border border-border bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground">
810
+ {projectCode}
811
+ </span>
812
+ {customerName ? (
813
+ <>
814
+ <span aria-hidden>·</span>
815
+ {customerId ? (
816
+ <Link className="underline underline-offset-2" href={`/backend/customers/companies/${customerId}`}>
817
+ {customerName}
818
+ </Link>
819
+ ) : (
820
+ <span>{customerName}</span>
821
+ )}
822
+ </>
823
+ ) : null}
824
+ {projectType ? (<><span aria-hidden>·</span><span>{projectType}</span></>) : null}
825
+ {projectStartDate ? (
826
+ <>
827
+ <span aria-hidden>·</span>
828
+ <span>
829
+ {t('staff.timesheets.projects.detail.startedOn', 'Started {date}', {
830
+ date: formatDateTime(projectStartDate),
831
+ })}
832
+ </span>
833
+ </>
834
+ ) : null}
835
+ </div>
836
+ </div>
837
+ </div>
838
+ {canManageProjects && (
839
+ <div className="flex items-center gap-2">
840
+ <Button variant="outline" size="sm" onClick={openTeamDrawer}>
841
+ <Users className="mr-2 h-4 w-4" aria-hidden />
842
+ {t('staff.time_tracking.projects.team.manage', 'Manage team')}
843
+ </Button>
844
+ <Button variant="outline" size="sm" asChild>
845
+ <Link href={`/backend/staff/time-tracking/projects/${projectId}/edit`}>
846
+ <Pencil className="mr-2 h-4 w-4" aria-hidden />
847
+ {t('staff.timesheets.projects.form.actions.edit', 'Edit Project')}
848
+ </Link>
849
+ </Button>
850
+ </div>
851
+ )}
852
+ <InjectionSpot
853
+ spotId={extensionPoints.hosts.projectDetailHeader.spotId}
854
+ context={projectInjectionContext}
855
+ data={project}
856
+ />
857
+ </div>
858
+
859
+ {/*
860
+ * Summary before detail. Four figures across the full width, in the order
861
+ * somebody asks them: how much time, how much of it billable, what it
862
+ * cost, how close the budget is. The three stacked label/value cards this
863
+ * replaces gave all fourteen fields identical weight.
864
+ */}
865
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
866
+ <KpiCard
867
+ title={t('staff.timesheets.projects.detail.loggedHours', 'Logged hours')}
868
+ value={totalMinutes / 60}
869
+ formatValue={() => formatHours(totalMinutes)}
870
+ footer={
871
+ hoursTrend.length > 0 ? (
872
+ <Sparkline
873
+ values={hoursTrend}
874
+ ariaLabel={t('staff.timesheets.projects.detail.trendAria', 'Hours per week, last 7 weeks')}
875
+ />
876
+ ) : undefined
877
+ }
878
+ headerAction={
879
+ entryCount !== null ? (
880
+ <span className="text-xs text-muted-foreground">
881
+ {entryCount === 1
882
+ ? t('staff.timesheets.projects.detail.entryCountOne', '1 entry')
883
+ : t('staff.timesheets.projects.detail.entryCountShort', '{count} entries', { count: entryCount })}
884
+ </span>
885
+ ) : undefined
886
+ }
887
+ />
888
+ <KpiCard
889
+ title={t('staff.timesheets.projects.detail.billableHours', 'Billable hours')}
890
+ value={(billableMinutes ?? 0) / 60}
891
+ formatValue={() => formatHours(billableMinutes)}
892
+ headerAction={
893
+ billablePercent !== null ? (
894
+ <Badge variant={billablePercent >= 80 ? 'success' : 'neutral'}>{`${billablePercent}%`}</Badge>
895
+ ) : undefined
896
+ }
897
+ footer={
898
+ nonBillableMinutes !== null ? (
899
+ <span className="text-xs text-muted-foreground">
900
+ {t('staff.timesheets.projects.detail.nonBillable', '{hours} non-billable', {
901
+ hours: formatHours(nonBillableMinutes),
902
+ })}
903
+ </span>
904
+ ) : undefined
905
+ }
906
+ />
907
+ {canSeeMoney ? (
908
+ <KpiCard
909
+ title={t('staff.timesheets.projects.detail.cost', 'Cost to date')}
910
+ value={cost}
911
+ formatValue={(value) => formatCurrency(value, currencyCode ?? undefined) ?? String(value)}
912
+ footer={
913
+ hourlyRate !== null ? (
914
+ <span className="text-xs text-muted-foreground">
915
+ {t('staff.timesheets.projects.detail.costBasis', '{hours} × {rate}', {
916
+ hours: formatHours(billableMinutes),
917
+ rate: `${formatCurrency(hourlyRate, currencyCode ?? undefined) ?? hourlyRate}/h`,
918
+ })}
919
+ </span>
920
+ ) : undefined
921
+ }
922
+ />
923
+ ) : null}
924
+ {canSeeMoney ? (
925
+ <div className="rounded-lg border bg-card p-4">
926
+ <div className="flex items-center justify-between gap-2">
927
+ <span className="text-sm font-medium text-muted-foreground">
928
+ {t('staff.timesheets.projects.form.budget', 'Budget')}
929
+ </span>
930
+ {budgetBurn ? (
931
+ <Badge
932
+ variant={
933
+ budgetBurn.tone === 'destructive'
934
+ ? 'destructive'
935
+ : budgetBurn.tone === 'warning'
936
+ ? 'warning'
937
+ : 'neutral'
938
+ }
939
+ >
940
+ {`${budgetBurn.percent}%`}
941
+ </Badge>
942
+ ) : (
943
+ <Badge variant="neutral">{t('staff.timesheets.projects.detail.noBudget', 'No budget')}</Badge>
944
+ )}
945
+ </div>
946
+ <div className="mt-2 space-y-2">
947
+ <p className="text-2xl font-semibold tabular-nums">
948
+ {budgetBurn ? `${budgetBurn.percent}%` : '—'}
949
+ </p>
950
+ <ProjectBudgetCell
951
+ burn={budgetBurn}
952
+ usageLabel={budgetUsageLabel}
953
+ noBudgetLabel={t('staff.timesheets.projects.detail.budgetHint', 'Set one to track burn')}
954
+ ariaLabel={t('staff.timesheets.projects.detail.budgetUsage', 'Budget usage')}
955
+ />
956
+ </div>
957
+ </div>
958
+ ) : null}
959
+ </div>
960
+
961
+ {/*
962
+ * Working content left, reference right. Everything static — rate,
963
+ * currency, cost centre, timestamps — collapses into one rail instead of
964
+ * three full-width cards, which is what left the right half of a wide
965
+ * screen carrying nothing.
966
+ */}
967
+ <div className="grid items-start gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)]">
968
+ <Tabs
969
+ value={activeTab}
970
+ onValueChange={setActiveTab}
971
+ variant="underline"
972
+ className="min-w-0"
973
+ >
974
+ <TabsList aria-label={t('staff.timesheets.projects.detail.tabs', 'Project sections')}>
975
+ <TabsTrigger value="team" count={activeCount}>
976
+ {t('staff.timesheets.projects.detail.tabTeam', 'Team')}
977
+ </TabsTrigger>
978
+ <TabsTrigger value="time" count={entryCount ?? undefined}>
979
+ {t('staff.timesheets.projects.detail.tabTime', 'Time')}
980
+ </TabsTrigger>
981
+ <TabsTrigger value="tasks">
982
+ {t('staff.timesheets.projects.detail.tabTasks', 'Tasks')}
983
+ </TabsTrigger>
984
+ {injectedTabs.map((tab) => (
985
+ <TabsTrigger key={tab.tabId} value={tab.tabId}>
986
+ {tab.label}
987
+ </TabsTrigger>
988
+ ))}
989
+ </TabsList>
990
+
991
+ <TabsContent value="team">
992
+ {/* Assigned Employees — collapsible cards */}
993
+ <div className="space-y-3">
994
+ <div className="flex items-center justify-between">
995
+ <h2 className="sr-only">
996
+ {t('staff.timesheets.projects.employees.title', 'Assigned Employees')}
997
+ </h2>
998
+ <span />
999
+ {canManageProjects && (
1000
+ <Button size="sm" onClick={openAddDialog}>
1001
+ <Plus className="mr-2 h-4 w-4" aria-hidden />
1002
+ {t('staff.timesheets.projects.add_employee', 'Add Employee')}
1003
+ </Button>
1004
+ )}
1005
+ </div>
1006
+
1007
+ {employeesLoading ? (
1008
+ <LoadingMessage label={t('staff.timesheets.projects.employees.loading', 'Loading employees...')} />
1009
+ ) : employees.length === 0 ? (
1010
+ <div className="rounded-lg border border-dashed p-8 text-center">
1011
+ <User className="mx-auto h-8 w-8 text-muted-foreground/50" aria-hidden />
1012
+ <p className="mt-2 text-sm text-muted-foreground">
1013
+ {t('staff.timesheets.projects.employees.empty', 'No employees assigned yet.')}
1014
+ </p>
1015
+ </div>
1016
+ ) : (
1017
+ <div className="space-y-2">
1018
+ {employees.map((emp) => {
1019
+ const isExpanded = expandedCards.has(emp.id)
1020
+ const isInactive = emp.status !== 'active'
1021
+ return (
1022
+ <div
1023
+ key={emp.id}
1024
+ className={`rounded-lg border ${isInactive ? 'opacity-60' : ''}`}
1025
+ >
1026
+ {/* Collapsed view: name, role, status badge, start date, expand toggle */}
1027
+ <button
1028
+ type="button"
1029
+ className="flex w-full items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
1030
+ onClick={() => toggleCard(emp.id)}
1031
+ >
1032
+ <div className="flex items-center gap-3 min-w-0">
1033
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted">
1034
+ <User className="h-4 w-4 text-muted-foreground" aria-hidden />
1035
+ </div>
1036
+ <div className="min-w-0">
1037
+ <p className="text-sm font-medium truncate">
1038
+ {emp.displayName ?? emp.staffMemberId}
1039
+ </p>
1040
+ {emp.role ? (
1041
+ <p className="text-xs text-muted-foreground truncate">{emp.role}</p>
1042
+ ) : null}
1043
+ </div>
1044
+ </div>
1045
+ <div className="flex items-center gap-3 shrink-0">
1046
+ <Badge
1047
+ variant={(emp.status ?? 'active') === 'active' ? 'success' : 'neutral'}
1048
+ className="capitalize"
1049
+ >
1050
+ {emp.status ?? 'active'}
1051
+ </Badge>
1052
+ {emp.assignedStartDate ? (
1053
+ <span className="text-xs text-muted-foreground hidden sm:inline">
1054
+ {emp.assignedStartDate}
1055
+ </span>
1056
+ ) : null}
1057
+ <ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${isExpanded ? 'rotate-180' : ''}`} aria-hidden />
1058
+ </div>
1059
+ </button>
1060
+
1061
+ {/* Expanded view: assignment details */}
1062
+ {isExpanded ? (
1063
+ <div className="border-t px-4 pb-4 pt-3 space-y-3">
1064
+ <dl className="grid grid-cols-2 gap-3 text-sm">
1065
+ <div>
1066
+ <dt className="font-medium text-muted-foreground">
1067
+ {t('staff.timesheets.projects.employees.role', 'Role')}
1068
+ </dt>
1069
+ <dd>{emp.role || '-'}</dd>
1070
+ </div>
1071
+ <div>
1072
+ <dt className="font-medium text-muted-foreground">
1073
+ {t('staff.timesheets.projects.employees.status', 'Status')}
1074
+ </dt>
1075
+ <dd className="capitalize">{emp.status ?? 'active'}</dd>
1076
+ </div>
1077
+ <div>
1078
+ <dt className="font-medium text-muted-foreground">
1079
+ {t('staff.timesheets.projects.employees.startDate', 'Assignment start')}
1080
+ </dt>
1081
+ <dd>{emp.assignedStartDate || '-'}</dd>
1082
+ </div>
1083
+ <div>
1084
+ <dt className="font-medium text-muted-foreground">
1085
+ {t('staff.timesheets.projects.employees.endDate', 'Assignment end')}
1086
+ </dt>
1087
+ <dd>{emp.assignedEndDate || '-'}</dd>
1088
+ </div>
1089
+ {emp.teamName ? (
1090
+ <div>
1091
+ <dt className="font-medium text-muted-foreground">
1092
+ {t('staff.timesheets.projects.employees.department', 'Department')}
1093
+ </dt>
1094
+ <dd>{emp.teamName}</dd>
1095
+ </div>
1096
+ ) : null}
1097
+ </dl>
1098
+ {canManageProjects && (
1099
+ <div className="flex justify-end">
1100
+ <Button
1101
+ variant="outline"
1102
+ size="sm"
1103
+ className="text-destructive hover:text-destructive"
1104
+ onClick={() => { void handleRemoveEmployee(emp) }}
1105
+ >
1106
+ <Trash2 className="mr-2 h-3.5 w-3.5" aria-hidden />
1107
+ {t('staff.timesheets.projects.employees.remove', 'Remove')}
1108
+ </Button>
1109
+ </div>
1110
+ )}
1111
+ </div>
1112
+ ) : null}
1113
+ </div>
1114
+ )
1115
+ })}
1116
+ </div>
1117
+ )}
1118
+ </div>
1119
+ </TabsContent>
1120
+
1121
+ <TabsContent value="time">
1122
+ {recentEntriesLoading ? (
1123
+ <LoadingMessage label={t('staff.timesheets.projects.detail.loadingTime', 'Loading time entries…')} />
1124
+ ) : recentEntries.length === 0 ? (
1125
+ <div className="rounded-lg border border-dashed p-8 text-center">
1126
+ <p className="text-sm font-medium">
1127
+ {t('staff.timesheets.projects.detail.noTime', 'No time logged yet')}
1128
+ </p>
1129
+ <p className="mt-1 text-xs text-muted-foreground">
1130
+ {t('staff.timesheets.projects.detail.noTimeHint', 'Hours logged against this project appear here.')}
1131
+ </p>
1132
+ </div>
1133
+ ) : (
1134
+ <div className="overflow-x-auto rounded-lg border">
1135
+ <table className="w-full text-sm">
1136
+ <thead>
1137
+ <tr className="border-b bg-muted/40 text-left">
1138
+ <th className="px-3 py-2 font-medium text-muted-foreground">
1139
+ {t('staff.timesheets.projects.detail.colDate', 'Date')}
1140
+ </th>
1141
+ <th className="px-3 py-2 font-medium text-muted-foreground">
1142
+ {t('staff.timesheets.projects.detail.colWho', 'Person')}
1143
+ </th>
1144
+ <th className="px-3 py-2 font-medium text-muted-foreground">
1145
+ {t('staff.timesheets.projects.detail.colDescription', 'Description')}
1146
+ </th>
1147
+ <th className="px-3 py-2 text-right font-medium text-muted-foreground">
1148
+ {t('staff.timesheets.projects.detail.colHours', 'Hours')}
1149
+ </th>
1150
+ </tr>
1151
+ </thead>
1152
+ <tbody>
1153
+ {recentEntries.map((entry) => (
1154
+ <tr key={entry.id} className="border-b last:border-b-0">
1155
+ <td className="whitespace-nowrap px-3 py-2">{entry.date}</td>
1156
+ <td className="whitespace-nowrap px-3 py-2">
1157
+ {employeeNameById.get(entry.staffMemberId ?? '') ?? '—'}
1158
+ </td>
1159
+ <td className="px-3 py-2">
1160
+ <span className="flex items-center gap-2">
1161
+ <span className="truncate">{entry.description ?? '—'}</span>
1162
+ {entry.isBillable ? null : (
1163
+ <Badge variant="neutral">
1164
+ {t('staff.timesheets.projects.detail.nonBillableTag', 'non-billable')}
1165
+ </Badge>
1166
+ )}
1167
+ </span>
1168
+ </td>
1169
+ <td className="whitespace-nowrap px-3 py-2 text-right font-mono tabular-nums">
1170
+ {formatHours(entry.durationMinutes)}
1171
+ </td>
1172
+ </tr>
1173
+ ))}
1174
+ </tbody>
1175
+ </table>
1176
+ </div>
1177
+ )}
1178
+ </TabsContent>
1179
+
1180
+ <TabsContent value="tasks">
1181
+ {projectTasksLoading ? (
1182
+ <LoadingMessage label={t('staff.timesheets.projects.detail.loadingTasks', 'Loading tasks…')} />
1183
+ ) : projectTasks.length === 0 ? (
1184
+ <div className="rounded-lg border border-dashed p-8 text-center">
1185
+ <p className="text-sm font-medium">
1186
+ {t('staff.timesheets.projects.detail.noTasks', 'No tasks yet')}
1187
+ </p>
1188
+ </div>
1189
+ ) : (
1190
+ <div className="divide-y rounded-lg border">
1191
+ {projectTasks.map((task) => (
1192
+ <div key={task.id} className="flex items-center gap-3 px-3 py-2 text-sm">
1193
+ {task.reference ? (
1194
+ <span className="shrink-0 rounded-sm border border-border bg-muted px-1.5 py-0.5 font-mono text-xs">
1195
+ {task.reference}
1196
+ </span>
1197
+ ) : null}
1198
+ <span className="min-w-0 flex-1 truncate">{task.title}</span>
1199
+ <span className="shrink-0 text-xs text-muted-foreground">
1200
+ {employeeNameById.get(task.assigneeStaffMemberId ?? '') ?? ''}
1201
+ </span>
1202
+ {task.loggedMinutes !== null ? (
1203
+ <span className="shrink-0 font-mono text-xs tabular-nums text-muted-foreground">
1204
+ {formatHours(task.loggedMinutes)}
1205
+ </span>
1206
+ ) : null}
1207
+ </div>
1208
+ ))}
1209
+ </div>
1210
+ )}
1211
+ </TabsContent>
1212
+
1213
+ {injectedTabs.map((tab) => (
1214
+ <TabsContent key={tab.tabId} value={tab.tabId}>
1215
+ <tab.Widget context={projectInjectionContext} data={project} />
1216
+ </TabsContent>
1217
+ ))}
1218
+ </Tabs>
1219
+
1220
+ {/* Reference rail — one card, grouped, no longer three stacked sections. */}
1221
+ <div className="rounded-lg border bg-card">
1222
+ <div className="border-b px-4 py-3">
1223
+ <h2 className="text-sm font-semibold">
1224
+ {t('staff.timesheets.projects.detail.details', 'Details')}
1225
+ </h2>
1226
+ </div>
1227
+ <div className="px-4 py-3">
1228
+ {canSeeMoney ? (
1229
+ <>
1230
+ <RailGroup label={t('staff.timesheets.projects.detail.commercials', 'Billing')} first />
1231
+ <dl>
1232
+ <RailFact
1233
+ label={t('staff.timesheets.projects.form.hourlyRate', 'Hourly rate')}
1234
+ value={
1235
+ hourlyRate === null
1236
+ ? '—'
1237
+ : `${formatCurrency(hourlyRate, currencyCode ?? undefined) ?? hourlyRate} / h`
1238
+ }
1239
+ />
1240
+ <RailFact
1241
+ label={t('staff.timesheets.projects.form.currency', 'Currency')}
1242
+ value={
1243
+ <span className="inline-flex items-center gap-1.5">
1244
+ {currencyCode ?? '—'}
1245
+ {currencyLocked ? (
1246
+ <Badge
1247
+ variant="neutral"
1248
+ title={t(
1249
+ 'staff.timesheets.projects.detail.currencyLockedHint',
1250
+ 'Locked because hours are already logged against this project.',
1251
+ )}
1252
+ >
1253
+ {t('staff.timesheets.projects.detail.locked', 'locked')}
1254
+ </Badge>
1255
+ ) : null}
1256
+ </span>
1257
+ }
1258
+ />
1259
+ <RailFact
1260
+ label={t('staff.timesheets.projects.form.billableByDefault', 'Billable by default')}
1261
+ value={
1262
+ billableByDefault === null ? (
1263
+ '—'
1264
+ ) : (
1265
+ <Badge variant={billableByDefault ? 'success' : 'neutral'}>
1266
+ {billableByDefault
1267
+ ? t('staff.timesheets.projects.detail.yes', 'Yes')
1268
+ : t('staff.timesheets.projects.detail.no', 'No')}
1269
+ </Badge>
1270
+ )
1271
+ }
1272
+ />
1273
+ </dl>
1274
+ </>
1275
+ ) : null}
1276
+
1277
+ <RailGroup
1278
+ label={t('staff.timesheets.projects.detail.delivery', 'Delivery')}
1279
+ first={!canSeeMoney}
1280
+ />
1281
+ <dl>
1282
+ <RailFact
1283
+ label={t('staff.timesheets.projects.detail.entryCount', 'Time entries')}
1284
+ value={entryCount ?? '—'}
1285
+ />
1286
+ <RailFact
1287
+ label={t('staff.timesheets.projects.detail.lockedEntries', 'Locked in reports')}
1288
+ value={lockedEntryCount ?? '—'}
1289
+ />
1290
+ </dl>
1291
+
1292
+ <RailGroup label={t('staff.timesheets.projects.detail.administration', 'Administration')} />
1293
+ <dl>
1294
+ <RailFact
1295
+ label={t('staff.timesheets.projects.form.costCenter', 'Cost center')}
1296
+ value={costCenter ?? '—'}
1297
+ />
1298
+ <RailFact
1299
+ label={t('staff.timesheets.projects.form.color', 'Project color')}
1300
+ value={
1301
+ <span className="inline-flex items-center gap-1.5 capitalize">
1302
+ <span
1303
+ className="size-2.5 rounded-full border border-border"
1304
+ style={{ backgroundColor: resolveProjectColorHex(projectColor, project.name) }}
1305
+ aria-hidden
1306
+ />
1307
+ {projectColor ?? t('staff.timesheets.projects.detail.autoColor', 'Auto')}
1308
+ </span>
1309
+ }
1310
+ />
1311
+ <RailFact
1312
+ label={t('staff.timesheets.projects.detail.createdAt', 'Created')}
1313
+ value={formatDateTime(createdAt)}
1314
+ />
1315
+ <RailFact
1316
+ label={t('staff.timesheets.projects.detail.updatedAt', 'Last updated')}
1317
+ value={formatDateTime(updatedAt)}
1318
+ />
1319
+ </dl>
1320
+
1321
+ {project.description ? (
1322
+ <>
1323
+ <RailGroup label={t('staff.timesheets.projects.form.description', 'Description')} />
1324
+ <p className="whitespace-pre-wrap pt-2 text-sm text-muted-foreground">{project.description}</p>
1325
+ </>
1326
+ ) : null}
1327
+ <InjectionSpot
1328
+ spotId={extensionPoints.hosts.projectDetailSidebar.spotId}
1329
+ context={projectInjectionContext}
1330
+ data={project}
1331
+ />
1332
+ </div>
1333
+ </div>
1334
+ </div>
1335
+ <InjectionSpot
1336
+ spotId={extensionPoints.hosts.projectDetailFooter.spotId}
1337
+ context={projectInjectionContext}
1338
+ data={project}
1339
+ />
1340
+ </div>
1341
+
1342
+ {ConfirmDialogElement}
1343
+
1344
+ {projectId && teamDrawerOpen ? (
1345
+ <ProjectTeamDrawer
1346
+ projectId={projectId}
1347
+ projectName={project.name}
1348
+ open={teamDrawerOpen}
1349
+ onOpenChange={handleTeamDrawerOpenChange}
1350
+ preselectUserId={requesterUserId}
1351
+ onSaved={handleTeamSaved}
1352
+ />
1353
+ ) : null}
1354
+
1355
+ {/* Add Employee Dialog */}
1356
+ <Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
1357
+ <DialogContent className="sm:max-w-md" onKeyDown={handleAddDialogKeyDown}>
1358
+ <DialogHeader>
1359
+ <DialogTitle>{t('staff.timesheets.projects.add_employee', 'Add Employee')}</DialogTitle>
1360
+ </DialogHeader>
1361
+ <div className="space-y-4 pt-2">
1362
+ <div className="space-y-2">
1363
+ <label className="text-sm font-medium">
1364
+ {t('staff.timesheets.projects.employees.selectEmployee', 'Select Employee')}
1365
+ </label>
1366
+ <LookupSelect
1367
+ value={addStaffMemberId}
1368
+ onChange={setAddStaffMemberId}
1369
+ fetchItems={fetchStaffMembers}
1370
+ searchPlaceholder={t('staff.timesheets.projects.employees.searchEmployee', 'Search team members...')}
1371
+ emptyLabel={t('staff.timesheets.projects.employees.noResults', 'No team members found')}
1372
+ />
1373
+ {staffLookupError ? (
1374
+ <p className="text-xs text-status-error-text" role="alert">
1375
+ {staffLookupError}
1376
+ </p>
1377
+ ) : null}
1378
+ </div>
1379
+ <div className="space-y-2">
1380
+ <label className="text-sm font-medium" htmlFor="add-role">
1381
+ {t('staff.timesheets.projects.employees.roleOnProject', 'Role on Project')}
1382
+ </label>
1383
+ <Input
1384
+ id="add-role"
1385
+ value={addRole}
1386
+ onChange={(event) => setAddRole(event.target.value)}
1387
+ placeholder={t('staff.timesheets.projects.employees.rolePlaceholder', 'e.g. Developer, Designer...')}
1388
+ />
1389
+ </div>
1390
+ <div className="space-y-2">
1391
+ <label className="text-sm font-medium" htmlFor="add-start-date">
1392
+ {t('staff.timesheets.projects.employees.assignmentStartDate', 'Assignment Start Date')}
1393
+ </label>
1394
+ <Input
1395
+ id="add-start-date"
1396
+ type="date"
1397
+ value={addStartDate}
1398
+ onChange={(event) => setAddStartDate(event.target.value)}
1399
+ />
1400
+ </div>
1401
+ <div className="flex justify-end gap-2 pt-2">
1402
+ <Button variant="outline" size="sm" onClick={() => setAddDialogOpen(false)}>
1403
+ {t('staff.timesheets.projects.form.actions.cancel', 'Cancel')}
1404
+ </Button>
1405
+ <Button
1406
+ size="sm"
1407
+ disabled={addSaving || !addStaffMemberId || !addStartDate}
1408
+ onClick={handleAddEmployee}
1409
+ >
1410
+ {addSaving ? <Spinner className="size-4" /> : null}
1411
+ {t('staff.timesheets.projects.add_employee', 'Add Employee')}
1412
+ </Button>
1413
+ </div>
1414
+ </div>
1415
+ </DialogContent>
1416
+ </Dialog>
1417
+ </PageBody>
1418
+ </Page>
1419
+ )
1420
+ }