@open-mercato/core 0.7.1-develop.7178.1.ab93fbaf4e → 0.7.1-develop.7179.1.e8c03264b6

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 (792) 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/staff/acl.js +9 -1
  41. package/dist/modules/staff/acl.js.map +2 -2
  42. package/dist/modules/staff/ai-agents.js +142 -0
  43. package/dist/modules/staff/ai-agents.js.map +7 -0
  44. package/dist/modules/staff/ai-tools/time-tracking-pack.js +348 -0
  45. package/dist/modules/staff/ai-tools/time-tracking-pack.js.map +7 -0
  46. package/dist/modules/staff/ai-tools/types.js +54 -0
  47. package/dist/modules/staff/ai-tools/types.js.map +7 -0
  48. package/dist/modules/staff/ai-tools.js +8 -0
  49. package/dist/modules/staff/ai-tools.js.map +7 -0
  50. package/dist/modules/staff/analytics.js +84 -1
  51. package/dist/modules/staff/analytics.js.map +2 -2
  52. package/dist/modules/staff/api/guards.js +37 -3
  53. package/dist/modules/staff/api/guards.js.map +2 -2
  54. package/dist/modules/staff/api/leave-requests/accept/route.js +1 -3
  55. package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
  56. package/dist/modules/staff/api/leave-requests/reject/route.js +1 -3
  57. package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
  58. package/dist/modules/staff/api/portal/time-reports/[id]/route.js +131 -0
  59. package/dist/modules/staff/api/portal/time-reports/[id]/route.js.map +7 -0
  60. package/dist/modules/staff/api/portal/time-reports/route.js +179 -0
  61. package/dist/modules/staff/api/portal/time-reports/route.js.map +7 -0
  62. package/dist/modules/staff/api/team-members/self/route.js +1 -3
  63. package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
  64. package/dist/modules/staff/api/team-members/tags/assign/route.js +1 -3
  65. package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
  66. package/dist/modules/staff/api/team-members/tags/unassign/route.js +1 -3
  67. package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
  68. package/dist/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.js +174 -0
  69. package/dist/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.js.map +7 -0
  70. package/dist/modules/staff/api/timesheets/access-requests/route.js +226 -0
  71. package/dist/modules/staff/api/timesheets/access-requests/route.js.map +7 -0
  72. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +15 -8
  73. package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +2 -2
  74. package/dist/modules/staff/api/timesheets/my-projects/route.js +13 -1
  75. package/dist/modules/staff/api/timesheets/my-projects/route.js.map +2 -2
  76. package/dist/modules/staff/api/timesheets/my-work/myWorkAggregate.js +42 -0
  77. package/dist/modules/staff/api/timesheets/my-work/myWorkAggregate.js.map +7 -0
  78. package/dist/modules/staff/api/timesheets/my-work/route.js +383 -0
  79. package/dist/modules/staff/api/timesheets/my-work/route.js.map +7 -0
  80. package/dist/modules/staff/api/timesheets/projects/kpis/route.js +14 -2
  81. package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +2 -2
  82. package/dist/modules/staff/api/timesheets/reports/[id]/close/route.js +168 -0
  83. package/dist/modules/staff/api/timesheets/reports/[id]/close/route.js.map +7 -0
  84. package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js +246 -0
  85. package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js.map +7 -0
  86. package/dist/modules/staff/api/timesheets/reports/[id]/sheet/route.js +183 -0
  87. package/dist/modules/staff/api/timesheets/reports/[id]/sheet/route.js.map +7 -0
  88. package/dist/modules/staff/api/timesheets/reports/[id]/unlock/route.js +164 -0
  89. package/dist/modules/staff/api/timesheets/reports/[id]/unlock/route.js.map +7 -0
  90. package/dist/modules/staff/api/timesheets/reports/preview/route.js +360 -0
  91. package/dist/modules/staff/api/timesheets/reports/preview/route.js.map +7 -0
  92. package/dist/modules/staff/api/timesheets/reports/route.js +316 -0
  93. package/dist/modules/staff/api/timesheets/reports/route.js.map +7 -0
  94. package/dist/modules/staff/api/timesheets/reports/shared.js +63 -0
  95. package/dist/modules/staff/api/timesheets/reports/shared.js.map +7 -0
  96. package/dist/modules/staff/api/timesheets/settings/reapply-rounding/route.js +164 -0
  97. package/dist/modules/staff/api/timesheets/settings/reapply-rounding/route.js.map +7 -0
  98. package/dist/modules/staff/api/timesheets/settings/rounding-impact/route.js +168 -0
  99. package/dist/modules/staff/api/timesheets/settings/rounding-impact/route.js.map +7 -0
  100. package/dist/modules/staff/api/timesheets/settings/route.js +199 -0
  101. package/dist/modules/staff/api/timesheets/settings/route.js.map +7 -0
  102. package/dist/modules/staff/api/timesheets/tags/access.js +76 -0
  103. package/dist/modules/staff/api/timesheets/tags/access.js.map +7 -0
  104. package/dist/modules/staff/api/timesheets/tags/entry-assignments/route.js +235 -0
  105. package/dist/modules/staff/api/timesheets/tags/entry-assignments/route.js.map +7 -0
  106. package/dist/modules/staff/api/timesheets/tags/route.js +160 -0
  107. package/dist/modules/staff/api/timesheets/tags/route.js.map +7 -0
  108. package/dist/modules/staff/api/timesheets/tags/task-assignments/route.js +233 -0
  109. package/dist/modules/staff/api/timesheets/tags/task-assignments/route.js.map +7 -0
  110. package/dist/modules/staff/api/timesheets/task-statuses/route.js +188 -0
  111. package/dist/modules/staff/api/timesheets/task-statuses/route.js.map +7 -0
  112. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +399 -0
  113. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +7 -0
  114. package/dist/modules/staff/api/timesheets/tasks/[id]/status/route.js +210 -0
  115. package/dist/modules/staff/api/timesheets/tasks/[id]/status/route.js.map +7 -0
  116. package/dist/modules/staff/api/timesheets/tasks/route.js +333 -0
  117. package/dist/modules/staff/api/timesheets/tasks/route.js.map +7 -0
  118. package/dist/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.js +228 -0
  119. package/dist/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.js.map +7 -0
  120. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +29 -10
  121. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +2 -2
  122. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +31 -18
  123. package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +2 -2
  124. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +21 -7
  125. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  126. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +27 -7
  127. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +2 -2
  128. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +215 -32
  129. package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +2 -2
  130. package/dist/modules/staff/api/timesheets/time-entries/copy-day/route.js +378 -0
  131. package/dist/modules/staff/api/timesheets/time-entries/copy-day/route.js.map +7 -0
  132. package/dist/modules/staff/api/timesheets/time-entries/overlaps/route.js +340 -0
  133. package/dist/modules/staff/api/timesheets/time-entries/overlaps/route.js.map +7 -0
  134. package/dist/modules/staff/api/timesheets/time-entries/route.js +238 -26
  135. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
  136. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +23 -8
  137. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +2 -2
  138. package/dist/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.js +209 -0
  139. package/dist/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.js.map +7 -0
  140. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +59 -2
  141. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  142. package/dist/modules/staff/api/timesheets/time-projects/route.js +264 -22
  143. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  144. package/dist/modules/staff/backend/staff/time-tracking/board/page.js +110 -0
  145. package/dist/modules/staff/backend/staff/time-tracking/board/page.js.map +7 -0
  146. package/dist/modules/staff/backend/staff/time-tracking/board/page.meta.js +18 -0
  147. package/dist/modules/staff/backend/staff/time-tracking/board/page.meta.js.map +7 -0
  148. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +981 -0
  149. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +7 -0
  150. package/dist/modules/staff/backend/staff/time-tracking/entries/page.meta.js +18 -0
  151. package/dist/modules/staff/backend/staff/time-tracking/entries/page.meta.js.map +7 -0
  152. package/dist/modules/staff/backend/staff/time-tracking/page.js +583 -0
  153. package/dist/modules/staff/backend/staff/time-tracking/page.js.map +7 -0
  154. package/dist/modules/staff/backend/staff/time-tracking/page.meta.js +15 -0
  155. package/dist/modules/staff/backend/staff/time-tracking/page.meta.js.map +7 -0
  156. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.js +65 -0
  157. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.js.map +7 -0
  158. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.js +16 -0
  159. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.js.map +7 -0
  160. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +229 -0
  161. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +7 -0
  162. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.js +16 -0
  163. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.js.map +7 -0
  164. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +1014 -0
  165. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +7 -0
  166. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.js +16 -0
  167. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.js.map +7 -0
  168. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.js +97 -0
  169. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.js.map +7 -0
  170. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.meta.js +16 -0
  171. package/dist/modules/staff/backend/staff/time-tracking/projects/create/page.meta.js.map +7 -0
  172. package/dist/modules/staff/backend/staff/time-tracking/projects/page.js +1040 -0
  173. package/dist/modules/staff/backend/staff/time-tracking/projects/page.js.map +7 -0
  174. package/dist/modules/staff/backend/staff/time-tracking/projects/page.meta.js +19 -0
  175. package/dist/modules/staff/backend/staff/time-tracking/projects/page.meta.js.map +7 -0
  176. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +353 -0
  177. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +7 -0
  178. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +457 -0
  179. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +7 -0
  180. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.js +16 -0
  181. package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.js.map +7 -0
  182. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.js +697 -0
  183. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.js.map +7 -0
  184. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.meta.js +16 -0
  185. package/dist/modules/staff/backend/staff/time-tracking/reports/create/page.meta.js.map +7 -0
  186. package/dist/modules/staff/backend/staff/time-tracking/reports/page.js +174 -0
  187. package/dist/modules/staff/backend/staff/time-tracking/reports/page.js.map +7 -0
  188. package/dist/modules/staff/backend/staff/time-tracking/reports/page.meta.js +18 -0
  189. package/dist/modules/staff/backend/staff/time-tracking/reports/page.meta.js.map +7 -0
  190. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +546 -0
  191. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +7 -0
  192. package/dist/modules/staff/backend/staff/time-tracking/settings/page.meta.js +18 -0
  193. package/dist/modules/staff/backend/staff/time-tracking/settings/page.meta.js.map +7 -0
  194. package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js +710 -0
  195. package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js.map +7 -0
  196. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.js +646 -0
  197. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.js.map +7 -0
  198. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.meta.js +18 -0
  199. package/dist/modules/staff/backend/staff/time-tracking/timesheet/page.meta.js.map +7 -0
  200. package/dist/modules/staff/backend/staff/timesheets/page.js +4 -793
  201. package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
  202. package/dist/modules/staff/backend/staff/timesheets/page.meta.js +2 -7
  203. package/dist/modules/staff/backend/staff/timesheets/page.meta.js.map +2 -2
  204. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js +5 -136
  205. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.js.map +2 -2
  206. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js +1 -7
  207. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.js.map +2 -2
  208. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +5 -445
  209. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +3 -3
  210. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js +1 -7
  211. package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.js.map +2 -2
  212. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js +4 -76
  213. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.js.map +2 -2
  214. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js +1 -7
  215. package/dist/modules/staff/backend/staff/timesheets/projects/create/page.meta.js.map +2 -2
  216. package/dist/modules/staff/backend/staff/timesheets/projects/page.js +4 -613
  217. package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
  218. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js +2 -17
  219. package/dist/modules/staff/backend/staff/timesheets/projects/page.meta.js.map +2 -2
  220. package/dist/modules/staff/ce.js +40 -0
  221. package/dist/modules/staff/ce.js.map +2 -2
  222. package/dist/modules/staff/cli.js +166 -1
  223. package/dist/modules/staff/cli.js.map +2 -2
  224. package/dist/modules/staff/commands/index.js +5 -0
  225. package/dist/modules/staff/commands/index.js.map +2 -2
  226. package/dist/modules/staff/commands/timesheets-entries.js +569 -21
  227. package/dist/modules/staff/commands/timesheets-entries.js.map +3 -3
  228. package/dist/modules/staff/commands/timesheets-projects.js +450 -7
  229. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  230. package/dist/modules/staff/commands/timesheets-reports.js +1036 -0
  231. package/dist/modules/staff/commands/timesheets-reports.js.map +7 -0
  232. package/dist/modules/staff/commands/timesheets-tags.js +1015 -0
  233. package/dist/modules/staff/commands/timesheets-tags.js.map +7 -0
  234. package/dist/modules/staff/commands/timesheets-task-comments.js +438 -0
  235. package/dist/modules/staff/commands/timesheets-task-comments.js.map +7 -0
  236. package/dist/modules/staff/commands/timesheets-task-statuses.js +723 -0
  237. package/dist/modules/staff/commands/timesheets-task-statuses.js.map +7 -0
  238. package/dist/modules/staff/commands/timesheets-tasks.js +834 -0
  239. package/dist/modules/staff/commands/timesheets-tasks.js.map +7 -0
  240. package/dist/modules/staff/data/enrichers.js +426 -12
  241. package/dist/modules/staff/data/enrichers.js.map +2 -2
  242. package/dist/modules/staff/data/entities.js +552 -3
  243. package/dist/modules/staff/data/entities.js.map +2 -2
  244. package/dist/modules/staff/data/extensions.js +41 -0
  245. package/dist/modules/staff/data/extensions.js.map +7 -0
  246. package/dist/modules/staff/data/validators.js +254 -9
  247. package/dist/modules/staff/data/validators.js.map +2 -2
  248. package/dist/modules/staff/di.js +42 -1
  249. package/dist/modules/staff/di.js.map +2 -2
  250. package/dist/modules/staff/events.js +84 -6
  251. package/dist/modules/staff/events.js.map +2 -2
  252. package/dist/modules/staff/events.payloads.js +158 -0
  253. package/dist/modules/staff/events.payloads.js.map +7 -0
  254. package/dist/modules/staff/extension-points.js +261 -0
  255. package/dist/modules/staff/extension-points.js.map +7 -0
  256. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +138 -0
  257. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +7 -0
  258. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.js +12 -0
  259. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.js.map +7 -0
  260. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +133 -0
  261. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +7 -0
  262. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.js +19 -0
  263. package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.js.map +7 -0
  264. package/dist/modules/staff/lib/crud.js +11 -1
  265. package/dist/modules/staff/lib/crud.js.map +2 -2
  266. package/dist/modules/staff/lib/time-tracking/access.js +92 -0
  267. package/dist/modules/staff/lib/time-tracking/access.js.map +7 -0
  268. package/dist/modules/staff/lib/time-tracking/billability.js +39 -0
  269. package/dist/modules/staff/lib/time-tracking/billability.js.map +7 -0
  270. package/dist/modules/staff/lib/time-tracking/capacity.js +68 -0
  271. package/dist/modules/staff/lib/time-tracking/capacity.js.map +7 -0
  272. package/dist/modules/staff/lib/time-tracking/componentContracts.js +15 -0
  273. package/dist/modules/staff/lib/time-tracking/componentContracts.js.map +7 -0
  274. package/dist/modules/staff/lib/time-tracking/cost.js +82 -0
  275. package/dist/modules/staff/lib/time-tracking/cost.js.map +7 -0
  276. package/dist/modules/staff/lib/time-tracking/duration.js +55 -0
  277. package/dist/modules/staff/lib/time-tracking/duration.js.map +7 -0
  278. package/dist/modules/staff/lib/time-tracking/featureAccess.js +41 -0
  279. package/dist/modules/staff/lib/time-tracking/featureAccess.js.map +7 -0
  280. package/dist/modules/staff/lib/time-tracking/interval.js +72 -0
  281. package/dist/modules/staff/lib/time-tracking/interval.js.map +7 -0
  282. package/dist/modules/staff/lib/time-tracking/migrateProjectCodes.js +88 -0
  283. package/dist/modules/staff/lib/time-tracking/migrateProjectCodes.js.map +7 -0
  284. package/dist/modules/staff/lib/time-tracking/moneyVisibility.js +10 -0
  285. package/dist/modules/staff/lib/time-tracking/moneyVisibility.js.map +7 -0
  286. package/dist/modules/staff/lib/time-tracking/overlap.js +87 -0
  287. package/dist/modules/staff/lib/time-tracking/overlap.js.map +7 -0
  288. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +27 -0
  289. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +7 -0
  290. package/dist/modules/staff/lib/time-tracking/portalReports.js +16 -0
  291. package/dist/modules/staff/lib/time-tracking/portalReports.js.map +7 -0
  292. package/dist/modules/staff/lib/time-tracking/projectCode.js +135 -0
  293. package/dist/modules/staff/lib/time-tracking/projectCode.js.map +7 -0
  294. package/dist/modules/staff/lib/time-tracking/reapplyRounding.js +187 -0
  295. package/dist/modules/staff/lib/time-tracking/reapplyRounding.js.map +7 -0
  296. package/dist/modules/staff/lib/time-tracking/recalculationRunner.js +80 -0
  297. package/dist/modules/staff/lib/time-tracking/recalculationRunner.js.map +7 -0
  298. package/dist/modules/staff/lib/time-tracking/recalculations.js +56 -0
  299. package/dist/modules/staff/lib/time-tracking/recalculations.js.map +7 -0
  300. package/dist/modules/staff/lib/time-tracking/registries/invoke.js +63 -0
  301. package/dist/modules/staff/lib/time-tracking/registries/invoke.js.map +7 -0
  302. package/dist/modules/staff/lib/time-tracking/registries/registry.js +62 -0
  303. package/dist/modules/staff/lib/time-tracking/registries/registry.js.map +7 -0
  304. package/dist/modules/staff/lib/time-tracking/registries/scope.js +21 -0
  305. package/dist/modules/staff/lib/time-tracking/registries/scope.js.map +7 -0
  306. package/dist/modules/staff/lib/time-tracking/rollup.js +34 -0
  307. package/dist/modules/staff/lib/time-tracking/rollup.js.map +7 -0
  308. package/dist/modules/staff/lib/time-tracking/rounding.js +66 -0
  309. package/dist/modules/staff/lib/time-tracking/rounding.js.map +7 -0
  310. package/dist/modules/staff/lib/time-tracking/roundingImpact.js +39 -0
  311. package/dist/modules/staff/lib/time-tracking/roundingImpact.js.map +7 -0
  312. package/dist/modules/staff/lib/time-tracking/settingKeys.js +214 -0
  313. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +7 -0
  314. package/dist/modules/staff/lib/time-tracking/settings.js +87 -0
  315. package/dist/modules/staff/lib/time-tracking/settings.js.map +7 -0
  316. package/dist/modules/staff/lib/time-tracking/sqlInClause.js +9 -0
  317. package/dist/modules/staff/lib/time-tracking/sqlInClause.js.map +7 -0
  318. package/dist/modules/staff/lib/time-tracking/timeEntrySources.js +48 -0
  319. package/dist/modules/staff/lib/time-tracking/timeEntrySources.js.map +7 -0
  320. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +283 -0
  321. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +7 -0
  322. package/dist/modules/staff/lib/time-tracking-ui/CustomerPicker.js +324 -0
  323. package/dist/modules/staff/lib/time-tracking-ui/CustomerPicker.js.map +7 -0
  324. package/dist/modules/staff/lib/time-tracking-ui/DurationInput.js +121 -0
  325. package/dist/modules/staff/lib/time-tracking-ui/DurationInput.js.map +7 -0
  326. package/dist/modules/staff/lib/time-tracking-ui/EntryDetailsFields.js +188 -0
  327. package/dist/modules/staff/lib/time-tracking-ui/EntryDetailsFields.js.map +7 -0
  328. package/dist/modules/staff/lib/time-tracking-ui/EntryFilterBar.js +89 -0
  329. package/dist/modules/staff/lib/time-tracking-ui/EntryFilterBar.js.map +7 -0
  330. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +855 -0
  331. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +7 -0
  332. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +360 -0
  333. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +7 -0
  334. package/dist/modules/staff/lib/time-tracking-ui/KanbanColumn.js +259 -0
  335. package/dist/modules/staff/lib/time-tracking-ui/KanbanColumn.js.map +7 -0
  336. package/dist/modules/staff/lib/time-tracking-ui/NewTaskDialog.js +238 -0
  337. package/dist/modules/staff/lib/time-tracking-ui/NewTaskDialog.js.map +7 -0
  338. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +130 -0
  339. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +7 -0
  340. package/dist/modules/staff/lib/time-tracking-ui/PeriodSelector.js +98 -0
  341. package/dist/modules/staff/lib/time-tracking-ui/PeriodSelector.js.map +7 -0
  342. package/dist/modules/staff/lib/time-tracking-ui/PhasePlaceholder.js +27 -0
  343. package/dist/modules/staff/lib/time-tracking-ui/PhasePlaceholder.js.map +7 -0
  344. package/dist/modules/staff/lib/time-tracking-ui/ProjectCodeField.js +166 -0
  345. package/dist/modules/staff/lib/time-tracking-ui/ProjectCodeField.js.map +7 -0
  346. package/dist/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.js +295 -0
  347. package/dist/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.js.map +7 -0
  348. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +212 -0
  349. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +7 -0
  350. package/dist/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.js +729 -0
  351. package/dist/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.js.map +7 -0
  352. package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +217 -0
  353. package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +7 -0
  354. package/dist/modules/staff/lib/time-tracking-ui/TagPicker.js +158 -0
  355. package/dist/modules/staff/lib/time-tracking-ui/TagPicker.js.map +7 -0
  356. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +224 -0
  357. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +7 -0
  358. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +93 -0
  359. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +7 -0
  360. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +1196 -0
  361. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +7 -0
  362. package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +176 -0
  363. package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +7 -0
  364. package/dist/modules/staff/lib/time-tracking-ui/TaskPicker.js +307 -0
  365. package/dist/modules/staff/lib/time-tracking-ui/TaskPicker.js.map +7 -0
  366. package/dist/modules/staff/lib/time-tracking-ui/TaskQuickLog.js +284 -0
  367. package/dist/modules/staff/lib/time-tracking-ui/TaskQuickLog.js.map +7 -0
  368. package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js +73 -0
  369. package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js.map +7 -0
  370. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDialog.js +1499 -0
  371. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDialog.js.map +7 -0
  372. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.js +95 -0
  373. package/dist/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.js.map +7 -0
  374. package/dist/modules/staff/lib/time-tracking-ui/TimesheetCalendar.js +188 -0
  375. package/dist/modules/staff/lib/time-tracking-ui/TimesheetCalendar.js.map +7 -0
  376. package/dist/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.js +61 -0
  377. package/dist/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.js.map +7 -0
  378. package/dist/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.js +45 -0
  379. package/dist/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.js.map +7 -0
  380. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +176 -0
  381. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +7 -0
  382. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +82 -0
  383. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +7 -0
  384. package/dist/modules/staff/lib/time-tracking-ui/boardSummary.js +5 -0
  385. package/dist/modules/staff/lib/time-tracking-ui/boardSummary.js.map +7 -0
  386. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +152 -0
  387. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +7 -0
  388. package/dist/modules/staff/lib/time-tracking-ui/projectTeamAssignment.js +92 -0
  389. package/dist/modules/staff/lib/time-tracking-ui/projectTeamAssignment.js.map +7 -0
  390. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +111 -0
  391. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +7 -0
  392. package/dist/modules/staff/lib/time-tracking-ui/timeEntryDialogState.js +243 -0
  393. package/dist/modules/staff/lib/time-tracking-ui/timeEntryDialogState.js.map +7 -0
  394. package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js +151 -0
  395. package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js.map +7 -0
  396. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +105 -0
  397. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +7 -0
  398. package/dist/modules/staff/lib/time-tracking-ui/timesheetData.js +132 -0
  399. package/dist/modules/staff/lib/time-tracking-ui/timesheetData.js.map +7 -0
  400. package/dist/modules/staff/lib/time-tracking-ui/timesheetPeriod.js +205 -0
  401. package/dist/modules/staff/lib/time-tracking-ui/timesheetPeriod.js.map +7 -0
  402. package/dist/modules/staff/lib/time-tracking-ui/timesheetTargets.js +53 -0
  403. package/dist/modules/staff/lib/time-tracking-ui/timesheetTargets.js.map +7 -0
  404. package/dist/modules/staff/lib/time-tracking-ui/useBoardFilters.js +77 -0
  405. package/dist/modules/staff/lib/time-tracking-ui/useBoardFilters.js.map +7 -0
  406. package/dist/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.js +151 -0
  407. package/dist/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.js.map +7 -0
  408. package/dist/modules/staff/lib/timeTrackingSeeds.js +809 -0
  409. package/dist/modules/staff/lib/timeTrackingSeeds.js.map +7 -0
  410. package/dist/modules/staff/lib/timesheets/timeEntryCacheInvalidation.js +2 -3
  411. package/dist/modules/staff/lib/timesheets/timeEntryCacheInvalidation.js.map +2 -2
  412. package/dist/modules/staff/lib/timesheets/timeEntryDecoration.js +152 -0
  413. package/dist/modules/staff/lib/timesheets/timeEntryDecoration.js.map +7 -0
  414. package/dist/modules/staff/lib/timesheets-projects/budgetBurn.js +36 -0
  415. package/dist/modules/staff/lib/timesheets-projects/budgetBurn.js.map +7 -0
  416. package/dist/modules/staff/lib/timesheets-projects/budgetThreshold.js +66 -0
  417. package/dist/modules/staff/lib/timesheets-projects/budgetThreshold.js.map +7 -0
  418. package/dist/modules/staff/lib/timesheets-projects/budgetThresholdState.js +78 -0
  419. package/dist/modules/staff/lib/timesheets-projects/budgetThresholdState.js.map +7 -0
  420. package/dist/modules/staff/lib/timesheets-projects/computeProjectFinancials.js +81 -0
  421. package/dist/modules/staff/lib/timesheets-projects/computeProjectFinancials.js.map +7 -0
  422. package/dist/modules/staff/lib/timesheets-projects/reportSelection.js +44 -0
  423. package/dist/modules/staff/lib/timesheets-projects/reportSelection.js.map +7 -0
  424. package/dist/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.js +169 -0
  425. package/dist/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.js.map +7 -0
  426. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.js +22 -0
  427. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.js.map +7 -0
  428. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js +51 -1
  429. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectCard.js.map +2 -2
  430. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.js +70 -0
  431. package/dist/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.js.map +7 -0
  432. package/dist/modules/staff/lib/timesheets-reports/buildReportSheet.js +63 -0
  433. package/dist/modules/staff/lib/timesheets-reports/buildReportSheet.js.map +7 -0
  434. package/dist/modules/staff/lib/timesheets-reports/loadReportData.js +170 -0
  435. package/dist/modules/staff/lib/timesheets-reports/loadReportData.js.map +7 -0
  436. package/dist/modules/staff/lib/timesheets-reports/pdf.js +205 -0
  437. package/dist/modules/staff/lib/timesheets-reports/pdf.js.map +7 -0
  438. package/dist/modules/staff/lib/timesheets-reports/reportApprovalPolicies.js +68 -0
  439. package/dist/modules/staff/lib/timesheets-reports/reportApprovalPolicies.js.map +7 -0
  440. package/dist/modules/staff/lib/timesheets-reports/reportConfigData.js +135 -0
  441. package/dist/modules/staff/lib/timesheets-reports/reportConfigData.js.map +7 -0
  442. package/dist/modules/staff/lib/timesheets-reports/reportConfigState.js +48 -0
  443. package/dist/modules/staff/lib/timesheets-reports/reportConfigState.js.map +7 -0
  444. package/dist/modules/staff/lib/timesheets-reports/reportExport.js +262 -0
  445. package/dist/modules/staff/lib/timesheets-reports/reportExport.js.map +7 -0
  446. package/dist/modules/staff/lib/timesheets-reports/reportExportFormats.js +34 -0
  447. package/dist/modules/staff/lib/timesheets-reports/reportExportFormats.js.map +7 -0
  448. package/dist/modules/staff/lib/timesheets-reports/reportGroupings.js +79 -0
  449. package/dist/modules/staff/lib/timesheets-reports/reportGroupings.js.map +7 -0
  450. package/dist/modules/staff/lib/timesheets-reports/reportLabels.js +11 -0
  451. package/dist/modules/staff/lib/timesheets-reports/reportLabels.js.map +7 -0
  452. package/dist/modules/staff/lib/timesheets-reports/reportReference.js +83 -0
  453. package/dist/modules/staff/lib/timesheets-reports/reportReference.js.map +7 -0
  454. package/dist/modules/staff/lib/timesheets-reports/reportRows.js +42 -0
  455. package/dist/modules/staff/lib/timesheets-reports/reportRows.js.map +7 -0
  456. package/dist/modules/staff/lib/timesheets-reports/reportSheetData.js +107 -0
  457. package/dist/modules/staff/lib/timesheets-reports/reportSheetData.js.map +7 -0
  458. package/dist/modules/staff/lib/timesheets-reports/reportTotals.js +289 -0
  459. package/dist/modules/staff/lib/timesheets-reports/reportTotals.js.map +7 -0
  460. package/dist/modules/staff/lib/timesheets-reports/xlsx.js +145 -0
  461. package/dist/modules/staff/lib/timesheets-reports/xlsx.js.map +7 -0
  462. package/dist/modules/staff/lib/timesheets-tasks/computeTaskRollups.js +116 -0
  463. package/dist/modules/staff/lib/timesheets-tasks/computeTaskRollups.js.map +7 -0
  464. package/dist/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.js +50 -0
  465. package/dist/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.js.map +7 -0
  466. package/dist/modules/staff/lib/timesheets-tasks/seedProjectStatuses.js +27 -0
  467. package/dist/modules/staff/lib/timesheets-tasks/seedProjectStatuses.js.map +7 -0
  468. package/dist/modules/staff/lib/timesheets-tasks/statusPositions.js +65 -0
  469. package/dist/modules/staff/lib/timesheets-tasks/statusPositions.js.map +7 -0
  470. package/dist/modules/staff/lib/timesheets-tasks/statusSlug.js +31 -0
  471. package/dist/modules/staff/lib/timesheets-tasks/statusSlug.js.map +7 -0
  472. package/dist/modules/staff/lib/timesheets-tasks/taskHierarchy.js +31 -0
  473. package/dist/modules/staff/lib/timesheets-tasks/taskHierarchy.js.map +7 -0
  474. package/dist/modules/staff/lib/timesheets-tasks/taskHoursTotals.js +17 -0
  475. package/dist/modules/staff/lib/timesheets-tasks/taskHoursTotals.js.map +7 -0
  476. package/dist/modules/staff/lib/timesheets-tasks/taskReference.js +51 -0
  477. package/dist/modules/staff/lib/timesheets-tasks/taskReference.js.map +7 -0
  478. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js +11 -4
  479. package/dist/modules/staff/lib/timesheets-ui/CreateProjectDialog.js.map +2 -2
  480. package/dist/modules/staff/lib/timesheets-ui/ListView.js +369 -111
  481. package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
  482. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +47 -2
  483. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  484. package/dist/modules/staff/migrations/Migration20260813073131_staff.js +79 -0
  485. package/dist/modules/staff/migrations/Migration20260813073131_staff.js.map +7 -0
  486. package/dist/modules/staff/migrations/Migration20260824143357_staff.js +15 -0
  487. package/dist/modules/staff/migrations/Migration20260824143357_staff.js.map +7 -0
  488. package/dist/modules/staff/notifications.client.js +16 -0
  489. package/dist/modules/staff/notifications.client.js.map +7 -0
  490. package/dist/modules/staff/notifications.handlers.js +80 -0
  491. package/dist/modules/staff/notifications.handlers.js.map +7 -0
  492. package/dist/modules/staff/notifications.js +168 -0
  493. package/dist/modules/staff/notifications.js.map +2 -2
  494. package/dist/modules/staff/search.js +159 -1
  495. package/dist/modules/staff/search.js.map +2 -2
  496. package/dist/modules/staff/setup.js +20 -1
  497. package/dist/modules/staff/setup.js.map +2 -2
  498. package/dist/modules/staff/subscribers/time-project-access-requested-notification.js +68 -0
  499. package/dist/modules/staff/subscribers/time-project-access-requested-notification.js.map +7 -0
  500. package/dist/modules/staff/subscribers/time-project-budget-threshold-notification.js +149 -0
  501. package/dist/modules/staff/subscribers/time-project-budget-threshold-notification.js.map +7 -0
  502. package/dist/modules/staff/subscribers/time-report-approved-notification.js +78 -0
  503. package/dist/modules/staff/subscribers/time-report-approved-notification.js.map +7 -0
  504. package/dist/modules/staff/subscribers/time-report-portal-broadcast.js +78 -0
  505. package/dist/modules/staff/subscribers/time-report-portal-broadcast.js.map +7 -0
  506. package/dist/modules/staff/translations.js +4 -1
  507. package/dist/modules/staff/translations.js.map +2 -2
  508. package/dist/modules/staff/widgets/components.js +7 -0
  509. package/dist/modules/staff/widgets/components.js.map +7 -0
  510. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +5 -4
  511. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
  512. package/dist/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.js +135 -0
  513. package/dist/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.js.map +7 -0
  514. package/dist/modules/staff/widgets/notifications/index.js +5 -0
  515. package/dist/modules/staff/widgets/notifications/index.js.map +7 -0
  516. package/dist/modules/staff/workers/timesheets-reapply-rounding.js +43 -0
  517. package/dist/modules/staff/workers/timesheets-reapply-rounding.js.map +7 -0
  518. package/generated/entities/staff_time_entry/index.ts +7 -0
  519. package/generated/entities/staff_time_entry_tag/index.ts +6 -0
  520. package/generated/entities/staff_time_project/index.ts +8 -0
  521. package/generated/entities/staff_time_report/index.ts +27 -0
  522. package/generated/entities/staff_time_report_entry/index.ts +12 -0
  523. package/generated/entities/staff_time_report_event/index.ts +9 -0
  524. package/generated/entities/staff_time_report_project/index.ts +6 -0
  525. package/generated/entities/staff_time_tag/index.ts +9 -0
  526. package/generated/entities/staff_time_task/index.ts +17 -0
  527. package/generated/entities/staff_time_task_comment/index.ts +9 -0
  528. package/generated/entities/staff_time_task_status/index.ts +13 -0
  529. package/generated/entities/staff_time_task_tag/index.ts +6 -0
  530. package/generated/entities.ids.generated.ts +11 -1
  531. package/generated/entity-fields-registry.ts +149 -0
  532. package/package.json +7 -7
  533. package/src/helpers/integration/timesheetFixtures.ts +31 -1
  534. package/src/modules/auth/README.md +1 -1
  535. package/src/modules/auth/cli.ts +35 -1
  536. package/src/modules/auth/i18n/de.json +7 -0
  537. package/src/modules/auth/i18n/en.json +7 -0
  538. package/src/modules/auth/i18n/es.json +7 -0
  539. package/src/modules/auth/i18n/ko.json +7 -0
  540. package/src/modules/auth/i18n/pl.json +7 -0
  541. package/src/modules/auth/lib/backendChrome.tsx +1 -0
  542. package/src/modules/customer_accounts/lib/customerRoleAcls.ts +79 -0
  543. package/src/modules/customer_accounts/setup.ts +1 -45
  544. package/src/modules/staff/AGENTS.md +769 -1
  545. package/src/modules/staff/acl.ts +9 -0
  546. package/src/modules/staff/ai-agents.ts +189 -0
  547. package/src/modules/staff/ai-tools/time-tracking-pack.ts +494 -0
  548. package/src/modules/staff/ai-tools/types.ts +82 -0
  549. package/src/modules/staff/ai-tools.ts +13 -0
  550. package/src/modules/staff/analytics.ts +113 -0
  551. package/src/modules/staff/api/guards.ts +85 -3
  552. package/src/modules/staff/api/leave-requests/accept/route.ts +0 -2
  553. package/src/modules/staff/api/leave-requests/reject/route.ts +0 -2
  554. package/src/modules/staff/api/portal/time-reports/[id]/route.ts +182 -0
  555. package/src/modules/staff/api/portal/time-reports/route.ts +270 -0
  556. package/src/modules/staff/api/team-members/self/route.ts +0 -2
  557. package/src/modules/staff/api/team-members/tags/assign/route.ts +0 -2
  558. package/src/modules/staff/api/team-members/tags/unassign/route.ts +0 -2
  559. package/src/modules/staff/api/timesheets/_shared/withTimesheetInterceptors.ts +292 -0
  560. package/src/modules/staff/api/timesheets/access-requests/route.ts +299 -0
  561. package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +15 -7
  562. package/src/modules/staff/api/timesheets/my-projects/route.ts +14 -1
  563. package/src/modules/staff/api/timesheets/my-work/myWorkAggregate.ts +90 -0
  564. package/src/modules/staff/api/timesheets/my-work/route.ts +475 -0
  565. package/src/modules/staff/api/timesheets/projects/kpis/route.ts +15 -2
  566. package/src/modules/staff/api/timesheets/reports/[id]/close/route.ts +203 -0
  567. package/src/modules/staff/api/timesheets/reports/[id]/export/route.ts +315 -0
  568. package/src/modules/staff/api/timesheets/reports/[id]/sheet/route.ts +220 -0
  569. package/src/modules/staff/api/timesheets/reports/[id]/unlock/route.ts +208 -0
  570. package/src/modules/staff/api/timesheets/reports/preview/route.ts +449 -0
  571. package/src/modules/staff/api/timesheets/reports/route.ts +386 -0
  572. package/src/modules/staff/api/timesheets/reports/shared.ts +121 -0
  573. package/src/modules/staff/api/timesheets/settings/reapply-rounding/route.ts +205 -0
  574. package/src/modules/staff/api/timesheets/settings/rounding-impact/route.ts +226 -0
  575. package/src/modules/staff/api/timesheets/settings/route.ts +242 -0
  576. package/src/modules/staff/api/timesheets/tags/access.ts +134 -0
  577. package/src/modules/staff/api/timesheets/tags/entry-assignments/route.ts +290 -0
  578. package/src/modules/staff/api/timesheets/tags/route.ts +177 -0
  579. package/src/modules/staff/api/timesheets/tags/task-assignments/route.ts +294 -0
  580. package/src/modules/staff/api/timesheets/task-statuses/route.ts +232 -0
  581. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +532 -0
  582. package/src/modules/staff/api/timesheets/tasks/[id]/status/route.ts +271 -0
  583. package/src/modules/staff/api/timesheets/tasks/route.ts +443 -0
  584. package/src/modules/staff/api/timesheets/time-entries/[id]/duplicate/route.ts +306 -0
  585. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +32 -10
  586. package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +32 -17
  587. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +27 -6
  588. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +36 -6
  589. package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +331 -34
  590. package/src/modules/staff/api/timesheets/time-entries/copy-day/route.ts +533 -0
  591. package/src/modules/staff/api/timesheets/time-entries/overlaps/route.ts +457 -0
  592. package/src/modules/staff/api/timesheets/time-entries/route.ts +378 -24
  593. package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +31 -9
  594. package/src/modules/staff/api/timesheets/time-projects/[id]/change-currency/route.ts +246 -0
  595. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +62 -2
  596. package/src/modules/staff/api/timesheets/time-projects/route.ts +397 -22
  597. package/src/modules/staff/backend/staff/time-tracking/board/page.meta.ts +14 -0
  598. package/src/modules/staff/backend/staff/time-tracking/board/page.tsx +151 -0
  599. package/src/modules/staff/backend/staff/time-tracking/entries/page.meta.ts +14 -0
  600. package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +1229 -0
  601. package/src/modules/staff/backend/staff/time-tracking/page.meta.ts +11 -0
  602. package/src/modules/staff/backend/staff/time-tracking/page.tsx +823 -0
  603. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.meta.ts +12 -0
  604. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/board/page.tsx +81 -0
  605. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.meta.ts +12 -0
  606. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +275 -0
  607. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.meta.ts +12 -0
  608. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +1420 -0
  609. package/src/modules/staff/backend/staff/time-tracking/projects/create/page.meta.ts +12 -0
  610. package/src/modules/staff/backend/staff/time-tracking/projects/create/page.tsx +109 -0
  611. package/src/modules/staff/backend/staff/time-tracking/projects/page.meta.ts +15 -0
  612. package/src/modules/staff/backend/staff/time-tracking/projects/page.tsx +1254 -0
  613. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +466 -0
  614. package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.meta.ts +12 -0
  615. package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +632 -0
  616. package/src/modules/staff/backend/staff/time-tracking/reports/create/page.meta.ts +12 -0
  617. package/src/modules/staff/backend/staff/time-tracking/reports/create/page.tsx +867 -0
  618. package/src/modules/staff/backend/staff/time-tracking/reports/page.meta.ts +14 -0
  619. package/src/modules/staff/backend/staff/time-tracking/reports/page.tsx +246 -0
  620. package/src/modules/staff/backend/staff/time-tracking/settings/page.meta.ts +14 -0
  621. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +701 -0
  622. package/src/modules/staff/backend/staff/time-tracking/timesheet/GridView.tsx +953 -0
  623. package/src/modules/staff/backend/staff/time-tracking/timesheet/page.meta.ts +14 -0
  624. package/src/modules/staff/backend/staff/time-tracking/timesheet/page.tsx +800 -0
  625. package/src/modules/staff/backend/staff/timesheets/page.meta.ts +6 -6
  626. package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1010
  627. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.meta.ts +0 -6
  628. package/src/modules/staff/backend/staff/timesheets/projects/[id]/edit/page.tsx +4 -158
  629. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.meta.ts +0 -6
  630. package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +4 -609
  631. package/src/modules/staff/backend/staff/timesheets/projects/create/page.meta.ts +0 -6
  632. package/src/modules/staff/backend/staff/timesheets/projects/create/page.tsx +3 -89
  633. package/src/modules/staff/backend/staff/timesheets/projects/page.meta.ts +2 -19
  634. package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +3 -778
  635. package/src/modules/staff/ce.ts +63 -0
  636. package/src/modules/staff/cli.ts +207 -1
  637. package/src/modules/staff/commands/index.ts +5 -0
  638. package/src/modules/staff/commands/timesheets-entries.ts +1006 -28
  639. package/src/modules/staff/commands/timesheets-projects.ts +593 -7
  640. package/src/modules/staff/commands/timesheets-reports.ts +1329 -0
  641. package/src/modules/staff/commands/timesheets-tags.ts +1315 -0
  642. package/src/modules/staff/commands/timesheets-task-comments.ts +583 -0
  643. package/src/modules/staff/commands/timesheets-task-statuses.ts +889 -0
  644. package/src/modules/staff/commands/timesheets-tasks.ts +1121 -0
  645. package/src/modules/staff/data/enrichers.ts +675 -10
  646. package/src/modules/staff/data/entities.ts +494 -2
  647. package/src/modules/staff/data/extensions.ts +68 -0
  648. package/src/modules/staff/data/validators.ts +348 -3
  649. package/src/modules/staff/di.ts +96 -0
  650. package/src/modules/staff/events.payloads.ts +268 -0
  651. package/src/modules/staff/events.ts +87 -5
  652. package/src/modules/staff/extension-points.ts +308 -0
  653. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.meta.ts +10 -0
  654. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +225 -0
  655. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.meta.ts +17 -0
  656. package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +182 -0
  657. package/src/modules/staff/i18n/de.json +899 -0
  658. package/src/modules/staff/i18n/en.json +899 -0
  659. package/src/modules/staff/i18n/es.json +899 -0
  660. package/src/modules/staff/i18n/ko.json +899 -0
  661. package/src/modules/staff/i18n/pl.json +899 -0
  662. package/src/modules/staff/lib/crud.ts +14 -3
  663. package/src/modules/staff/lib/time-tracking/access.ts +155 -0
  664. package/src/modules/staff/lib/time-tracking/billability.ts +68 -0
  665. package/src/modules/staff/lib/time-tracking/capacity.ts +124 -0
  666. package/src/modules/staff/lib/time-tracking/componentContracts.ts +26 -0
  667. package/src/modules/staff/lib/time-tracking/cost.ts +144 -0
  668. package/src/modules/staff/lib/time-tracking/duration.ts +68 -0
  669. package/src/modules/staff/lib/time-tracking/featureAccess.ts +115 -0
  670. package/src/modules/staff/lib/time-tracking/interval.ts +90 -0
  671. package/src/modules/staff/lib/time-tracking/migrateProjectCodes.ts +161 -0
  672. package/src/modules/staff/lib/time-tracking/moneyVisibility.ts +37 -0
  673. package/src/modules/staff/lib/time-tracking/overlap.ts +159 -0
  674. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +76 -0
  675. package/src/modules/staff/lib/time-tracking/portalReports.ts +31 -0
  676. package/src/modules/staff/lib/time-tracking/projectCode.ts +221 -0
  677. package/src/modules/staff/lib/time-tracking/reapplyRounding.ts +313 -0
  678. package/src/modules/staff/lib/time-tracking/recalculationRunner.ts +118 -0
  679. package/src/modules/staff/lib/time-tracking/recalculations.ts +135 -0
  680. package/src/modules/staff/lib/time-tracking/registries/invoke.ts +122 -0
  681. package/src/modules/staff/lib/time-tracking/registries/registry.ts +124 -0
  682. package/src/modules/staff/lib/time-tracking/registries/scope.ts +56 -0
  683. package/src/modules/staff/lib/time-tracking/rollup.ts +59 -0
  684. package/src/modules/staff/lib/time-tracking/rounding.ts +126 -0
  685. package/src/modules/staff/lib/time-tracking/roundingImpact.ts +75 -0
  686. package/src/modules/staff/lib/time-tracking/settingKeys.ts +274 -0
  687. package/src/modules/staff/lib/time-tracking/settings.ts +168 -0
  688. package/src/modules/staff/lib/time-tracking/sqlInClause.ts +22 -0
  689. package/src/modules/staff/lib/time-tracking/timeEntrySources.ts +85 -0
  690. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +360 -0
  691. package/src/modules/staff/lib/time-tracking-ui/CustomerPicker.tsx +427 -0
  692. package/src/modules/staff/lib/time-tracking-ui/DurationInput.tsx +192 -0
  693. package/src/modules/staff/lib/time-tracking-ui/EntryDetailsFields.tsx +224 -0
  694. package/src/modules/staff/lib/time-tracking-ui/EntryFilterBar.tsx +144 -0
  695. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +1065 -0
  696. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +435 -0
  697. package/src/modules/staff/lib/time-tracking-ui/KanbanColumn.tsx +316 -0
  698. package/src/modules/staff/lib/time-tracking-ui/NewTaskDialog.tsx +291 -0
  699. package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +162 -0
  700. package/src/modules/staff/lib/time-tracking-ui/PeriodSelector.tsx +149 -0
  701. package/src/modules/staff/lib/time-tracking-ui/PhasePlaceholder.tsx +31 -0
  702. package/src/modules/staff/lib/time-tracking-ui/ProjectCodeField.tsx +230 -0
  703. package/src/modules/staff/lib/time-tracking-ui/ProjectCurrencyField.tsx +432 -0
  704. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +323 -0
  705. package/src/modules/staff/lib/time-tracking-ui/ProjectTeamDrawer.tsx +930 -0
  706. package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +357 -0
  707. package/src/modules/staff/lib/time-tracking-ui/TagPicker.tsx +195 -0
  708. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +243 -0
  709. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +137 -0
  710. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +1405 -0
  711. package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +260 -0
  712. package/src/modules/staff/lib/time-tracking-ui/TaskPicker.tsx +435 -0
  713. package/src/modules/staff/lib/time-tracking-ui/TaskQuickLog.tsx +296 -0
  714. package/src/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx +97 -0
  715. package/src/modules/staff/lib/time-tracking-ui/TimeEntryDialog.tsx +1929 -0
  716. package/src/modules/staff/lib/time-tracking-ui/TimeEntryDurationCell.tsx +126 -0
  717. package/src/modules/staff/lib/time-tracking-ui/TimesheetCalendar.tsx +265 -0
  718. package/src/modules/staff/lib/time-tracking-ui/TimesheetPeriodFooter.tsx +78 -0
  719. package/src/modules/staff/lib/time-tracking-ui/TimesheetViewSwitch.tsx +70 -0
  720. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +231 -0
  721. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +128 -0
  722. package/src/modules/staff/lib/time-tracking-ui/boardSummary.ts +20 -0
  723. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +228 -0
  724. package/src/modules/staff/lib/time-tracking-ui/projectTeamAssignment.ts +143 -0
  725. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +194 -0
  726. package/src/modules/staff/lib/time-tracking-ui/timeEntryDialogState.ts +400 -0
  727. package/src/modules/staff/lib/time-tracking-ui/timeEntryListData.ts +271 -0
  728. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +168 -0
  729. package/src/modules/staff/lib/time-tracking-ui/timesheetData.ts +225 -0
  730. package/src/modules/staff/lib/time-tracking-ui/timesheetPeriod.ts +278 -0
  731. package/src/modules/staff/lib/time-tracking-ui/timesheetTargets.ts +93 -0
  732. package/src/modules/staff/lib/time-tracking-ui/useBoardFilters.ts +107 -0
  733. package/src/modules/staff/lib/time-tracking-ui/useTimesheetPreferences.ts +245 -0
  734. package/src/modules/staff/lib/timeTrackingSeeds.ts +1037 -0
  735. package/src/modules/staff/lib/timesheets/timeEntryCacheInvalidation.ts +11 -10
  736. package/src/modules/staff/lib/timesheets/timeEntryDecoration.ts +256 -0
  737. package/src/modules/staff/lib/timesheets-projects/budgetBurn.ts +60 -0
  738. package/src/modules/staff/lib/timesheets-projects/budgetThreshold.ts +135 -0
  739. package/src/modules/staff/lib/timesheets-projects/budgetThresholdState.ts +137 -0
  740. package/src/modules/staff/lib/timesheets-projects/computeProjectFinancials.ts +142 -0
  741. package/src/modules/staff/lib/timesheets-projects/reportSelection.ts +82 -0
  742. package/src/modules/staff/lib/timesheets-projects-ui/AssignMembersDialog.tsx +237 -0
  743. package/src/modules/staff/lib/timesheets-projects-ui/ProjectBudgetCell.tsx +33 -0
  744. package/src/modules/staff/lib/timesheets-projects-ui/ProjectCard.tsx +56 -1
  745. package/src/modules/staff/lib/timesheets-projects-ui/ProjectsBulkActionsBar.tsx +90 -0
  746. package/src/modules/staff/lib/timesheets-reports/buildReportSheet.ts +125 -0
  747. package/src/modules/staff/lib/timesheets-reports/loadReportData.ts +251 -0
  748. package/src/modules/staff/lib/timesheets-reports/pdf.ts +263 -0
  749. package/src/modules/staff/lib/timesheets-reports/reportApprovalPolicies.ts +137 -0
  750. package/src/modules/staff/lib/timesheets-reports/reportConfigData.ts +247 -0
  751. package/src/modules/staff/lib/timesheets-reports/reportConfigState.ts +89 -0
  752. package/src/modules/staff/lib/timesheets-reports/reportExport.ts +373 -0
  753. package/src/modules/staff/lib/timesheets-reports/reportExportFormats.ts +71 -0
  754. package/src/modules/staff/lib/timesheets-reports/reportGroupings.ts +153 -0
  755. package/src/modules/staff/lib/timesheets-reports/reportLabels.ts +23 -0
  756. package/src/modules/staff/lib/timesheets-reports/reportReference.ts +146 -0
  757. package/src/modules/staff/lib/timesheets-reports/reportRows.ts +89 -0
  758. package/src/modules/staff/lib/timesheets-reports/reportSheetData.ts +196 -0
  759. package/src/modules/staff/lib/timesheets-reports/reportTotals.ts +544 -0
  760. package/src/modules/staff/lib/timesheets-reports/xlsx.ts +207 -0
  761. package/src/modules/staff/lib/timesheets-tasks/computeTaskRollups.ts +238 -0
  762. package/src/modules/staff/lib/timesheets-tasks/defaultStatusTemplate.ts +90 -0
  763. package/src/modules/staff/lib/timesheets-tasks/seedProjectStatuses.ts +43 -0
  764. package/src/modules/staff/lib/timesheets-tasks/statusPositions.ts +132 -0
  765. package/src/modules/staff/lib/timesheets-tasks/statusSlug.ts +43 -0
  766. package/src/modules/staff/lib/timesheets-tasks/taskHierarchy.ts +99 -0
  767. package/src/modules/staff/lib/timesheets-tasks/taskHoursTotals.ts +42 -0
  768. package/src/modules/staff/lib/timesheets-tasks/taskReference.ts +108 -0
  769. package/src/modules/staff/lib/timesheets-ui/CreateProjectDialog.tsx +18 -4
  770. package/src/modules/staff/lib/timesheets-ui/ListView.tsx +504 -149
  771. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +50 -2
  772. package/src/modules/staff/migrations/.snapshot-open-mercato.json +3244 -228
  773. package/src/modules/staff/migrations/Migration20260813073131_staff.ts +94 -0
  774. package/src/modules/staff/migrations/Migration20260824143357_staff.ts +17 -0
  775. package/src/modules/staff/notifications.client.ts +16 -0
  776. package/src/modules/staff/notifications.handlers.ts +88 -0
  777. package/src/modules/staff/notifications.ts +163 -0
  778. package/src/modules/staff/search.ts +191 -1
  779. package/src/modules/staff/setup.ts +22 -0
  780. package/src/modules/staff/subscribers/time-project-access-requested-notification.ts +95 -0
  781. package/src/modules/staff/subscribers/time-project-budget-threshold-notification.ts +226 -0
  782. package/src/modules/staff/subscribers/time-report-approved-notification.ts +116 -0
  783. package/src/modules/staff/subscribers/time-report-portal-broadcast.ts +120 -0
  784. package/src/modules/staff/translations.ts +17 -0
  785. package/src/modules/staff/widgets/components.ts +34 -0
  786. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +6 -5
  787. package/src/modules/staff/widgets/notifications/TimeProjectAccessRequestedRenderer.tsx +138 -0
  788. package/src/modules/staff/widgets/notifications/index.ts +1 -0
  789. package/src/modules/staff/workers/timesheets-reapply-rounding.ts +59 -0
  790. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js +0 -123
  791. package/dist/modules/staff/backend/staff/timesheets/projects/projectFormConfig.js.map +0 -7
  792. package/src/modules/staff/backend/staff/timesheets/projects/projectFormConfig.ts +0 -138
@@ -0,0 +1,1405 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
6
+ import { Plus, X } from 'lucide-react'
7
+ import {
8
+ Drawer,
9
+ DrawerBody,
10
+ DrawerContent,
11
+ DrawerDescription,
12
+ DrawerFooter,
13
+ DrawerHeader,
14
+ DrawerTitle,
15
+ } from '@open-mercato/ui/primitives/drawer'
16
+ import { Avatar } from '@open-mercato/ui/primitives/avatar'
17
+ import { Badge } from '@open-mercato/ui/primitives/badge'
18
+ import { Button } from '@open-mercato/ui/primitives/button'
19
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
20
+ import { Input } from '@open-mercato/ui/primitives/input'
21
+ import { Progress } from '@open-mercato/ui/primitives/progress'
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@open-mercato/ui/primitives/select'
29
+ import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
30
+ import { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
31
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
32
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
33
+ import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
34
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
35
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
36
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
37
+ import { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'
38
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
39
+ import { formatCurrency, formatDate } from '@open-mercato/ui/utils/format'
40
+ import { hasFeature } from '@open-mercato/shared/security/features'
41
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
42
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
43
+ import { createLogger } from '@open-mercato/shared/lib/logger'
44
+ import { TaskCommentThread } from './TaskCommentThread'
45
+ import { TaskQuickLog, type QuickLogSubmission } from './TaskQuickLog'
46
+ import { TagPicker, tagChipStyle } from './TagPicker'
47
+ import { slugifyProjectName } from '../time-tracking/projectCode'
48
+ import type { TagOption } from './timeEntryDialogState'
49
+ import {
50
+ computeSubtaskProgress,
51
+ formatBoardMinutes,
52
+ readItems,
53
+ readString,
54
+ readTotal,
55
+ sortByPosition,
56
+ toBoardStatus,
57
+ toBoardTask,
58
+ type BoardApiRow,
59
+ type BoardStatus,
60
+ type BoardTask,
61
+ } from './kanbanBoardData'
62
+ import {
63
+ childMinutes,
64
+ formatElapsed,
65
+ formatSplitMinutes,
66
+ resolveDefaultStatusId,
67
+ resolveDoneStatusId,
68
+ summarizeEntries,
69
+ toDrawerComment,
70
+ toDrawerEntry,
71
+ todayIsoDate,
72
+ buildEntryClocks,
73
+ type DrawerComment,
74
+ type DrawerEntry,
75
+ } from './taskDrawerData'
76
+
77
+ const logger = createLogger('staff').child({ component: 'TaskDrawer' })
78
+
79
+ const TASK_ENTITY_ID = 'staff:staff_time_task'
80
+
81
+ type TaskDetailInjectionContext = {
82
+ entityId: string
83
+ recordId: string
84
+ taskId: string
85
+ timeProjectId: string | null
86
+ retryLastMutation: () => Promise<boolean>
87
+ }
88
+
89
+ export const TASK_DRAWER_MUTATION_CONTEXT_ID = 'staff.time_tracking.task_drawer'
90
+ export const RATES_FEATURE = 'staff.timesheets.rates.view'
91
+ export const TASKS_MANAGE_FEATURE = 'staff.timesheets.tasks.manage'
92
+ export const ENTRIES_LIST_PATH = '/backend/staff/time-tracking/entries'
93
+
94
+ const DRAWER_QUERY_ROOT = ['staff', 'time-tracking', 'task-drawer'] as const
95
+ const DIRECTORY_PAGE_SIZE = 100
96
+ const CHILD_PAGE_SIZE = 100
97
+ const COMMENT_PAGE_SIZE = 50
98
+ const ENTRY_PAGE_SIZE = 100
99
+ /** Bounded sweep: the entries route has no `taskId` filter (see the docblock). */
100
+ const MAX_ENTRY_PAGES = 3
101
+ const RECENT_ENTRY_COUNT = 5
102
+ const ELAPSED_TICK_MS = 1000
103
+
104
+ export type TaskDrawerProps = {
105
+ taskId: string
106
+ open: boolean
107
+ onOpenChange: (open: boolean) => void
108
+ /** Shown in the header line next to the created date; the host already knows it. */
109
+ projectName?: string | null
110
+ /** `?panel=time` — "Add time" on a board card lands with the quick log focused. */
111
+ focusPanel?: string | null
112
+ /** Lets the host (the board) refetch after a write that changed hours or status. */
113
+ onChanged?: () => void
114
+ }
115
+
116
+ type LoadedTask = { task: BoardTask; createdAt: string | null }
117
+
118
+ type RunningTimer = { entryId: string | null; taskId: string | null; startedAt: string | null }
119
+
120
+ type ProjectEntries = { rows: BoardApiRow[]; partial: boolean }
121
+
122
+ async function fetchProjectEntries(projectId: string): Promise<ProjectEntries> {
123
+ const rows: BoardApiRow[] = []
124
+ for (let page = 1; page <= MAX_ENTRY_PAGES; page += 1) {
125
+ const call = await apiCall<Record<string, unknown>>(
126
+ `/api/staff/timesheets/time-entries?projectId=${encodeURIComponent(projectId)}&page=${page}&pageSize=${ENTRY_PAGE_SIZE}&sortField=date&sortDir=desc`,
127
+ )
128
+ if (!call.ok) return { rows, partial: true }
129
+ const items = readItems(call.result)
130
+ rows.push(...items)
131
+ if (items.length < ENTRY_PAGE_SIZE) return { rows, partial: false }
132
+ if (rows.length >= readTotal(call.result, rows.length)) return { rows, partial: false }
133
+ }
134
+ return { rows, partial: true }
135
+ }
136
+
137
+ /**
138
+ * The task drawer of screen 7 (T3.7, US-C3/C4/C5, US-D4).
139
+ *
140
+ * Four decisions carry the design intent:
141
+ *
142
+ * 1. **Subtasks are child tasks, and ticking one is a status move** (D-2, and
143
+ * the decision that supersedes the mockup's note 4). There is no boolean on
144
+ * a task, so the checkbox PATCHes the child to the project's first `is_done`
145
+ * column and un-ticking returns it to the `is_default` one. Each row carries
146
+ * its own assignee and its own hours because a subtask holds time like any
147
+ * other task; the parent's headline is the inclusive rollup that already
148
+ * contains them.
149
+ * 2. **Depth is capped at one.** A child task's drawer renders no subtask
150
+ * section at all: the server answers `400 subtask_depth_exceeded`, so an
151
+ * "add subtask" affordance there could only ever fail.
152
+ * 3. **Properties save per field, so each write carries its own version.** A
153
+ * blur PATCHes with `buildOptimisticLockHeader(versionRef.current)` and then refreshes
154
+ * the token — from the status route's `updatedAt` when it returns one, by
155
+ * refetching the row when it does not (the CRUD update answers `{ok:true}`).
156
+ * Saves are chained on `saveChainRef` so the second field never leaves with
157
+ * the version the first one just invalidated.
158
+ * 4. **One timer at a time** (note 2, US-D4). Starting here while a timer runs
159
+ * on another task asks first, then stops that one before starting this one.
160
+ *
161
+ * Two known gaps, both other tasks' work, are handled honestly rather than faked:
162
+ * the task projection carries no tag ids yet (T3.10), so the tag row can add but
163
+ * cannot list what the server already has; and the entries route has no `taskId`
164
+ * filter, so the recent-entries list and the billable split come from a bounded
165
+ * sweep of the project's entries and say so when the bound is hit.
166
+ */
167
+ export function TaskDrawer({
168
+ taskId,
169
+ open,
170
+ onOpenChange,
171
+ projectName,
172
+ focusPanel,
173
+ onChanged,
174
+ }: TaskDrawerProps) {
175
+ const t = useT()
176
+ const router = useRouter()
177
+ const queryClient = useQueryClient()
178
+ const scopeVersion = useOrganizationScopeVersion()
179
+ const { payload } = useBackendChrome()
180
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
181
+
182
+ const canSeeMoney = hasFeature(payload?.grantedFeatures, RATES_FEATURE)
183
+ const canManage = hasFeature(payload?.grantedFeatures, TASKS_MANAGE_FEATURE)
184
+
185
+ const [tagIds, setTagIds] = React.useState<string[]>([])
186
+ const [subtaskDraft, setSubtaskDraft] = React.useState('')
187
+ const [composingSubtask, setComposingSubtask] = React.useState(false)
188
+ const [busy, setBusy] = React.useState(false)
189
+ const [timerBusy, setTimerBusy] = React.useState(false)
190
+ const [now, setNow] = React.useState(() => Date.now())
191
+
192
+ const saveChainRef = React.useRef<Promise<void>>(Promise.resolve())
193
+ /**
194
+ * The lock token lives in a ref, not in state: a save queued behind another one
195
+ * must read the version at the moment it *runs*, not the one its closure was
196
+ * built with, or a second blur landing before a re-render would still leave
197
+ * with the token the first blur just invalidated.
198
+ */
199
+ const versionRef = React.useRef<string | null>(null)
200
+
201
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
202
+ formId: string
203
+ resourceKind: string
204
+ resourceId: string
205
+ retryLastMutation: () => Promise<boolean>
206
+ }>({
207
+ contextId: TASK_DRAWER_MUTATION_CONTEXT_ID,
208
+ blockedMessage: t('staff.time_tracking.taskDrawer.properties.saveError', 'Could not save the change.'),
209
+ })
210
+
211
+ const taskQuery = useQuery<LoadedTask | null>({
212
+ queryKey: [...DRAWER_QUERY_ROOT, 'task', `scope:${scopeVersion}`, taskId],
213
+ enabled: open && taskId.length > 0,
214
+ staleTime: 5_000,
215
+ queryFn: async () => {
216
+ const call = await apiCall<Record<string, unknown>>(
217
+ `/api/staff/timesheets/tasks?id=${encodeURIComponent(taskId)}&pageSize=1`,
218
+ )
219
+ if (!call.ok) throw new Error('[internal] task request failed')
220
+ const row = readItems(call.result)[0]
221
+ if (!row) return null
222
+ const parsed = toBoardTask(row)
223
+ return parsed ? { task: parsed, createdAt: readString(row, 'created_at', 'createdAt') } : null
224
+ },
225
+ })
226
+
227
+ const task = taskQuery.data?.task ?? null
228
+ const createdAt = taskQuery.data?.createdAt ?? null
229
+ const projectId = task?.timeProjectId ?? null
230
+
231
+ const taskInjectionContext = React.useMemo<TaskDetailInjectionContext>(
232
+ () => ({
233
+ entityId: TASK_ENTITY_ID,
234
+ recordId: taskId,
235
+ taskId,
236
+ timeProjectId: projectId,
237
+ retryLastMutation,
238
+ }),
239
+ [projectId, retryLastMutation, taskId],
240
+ )
241
+ const isChildTask = !!task?.parentTaskId
242
+
243
+ React.useEffect(() => {
244
+ if (!task) return
245
+ versionRef.current = task.updatedAt
246
+ setTagIds(task.tagIds)
247
+ }, [task])
248
+
249
+ const statusesQuery = useQuery<BoardStatus[]>({
250
+ queryKey: [...DRAWER_QUERY_ROOT, 'statuses', `scope:${scopeVersion}`, projectId ?? 'none'],
251
+ enabled: open && !!projectId,
252
+ staleTime: 30_000,
253
+ queryFn: async () => {
254
+ if (!projectId) return []
255
+ const call = await apiCall<Record<string, unknown>>(
256
+ `/api/staff/timesheets/task-statuses?timeProjectId=${encodeURIComponent(projectId)}&page=1&pageSize=${DIRECTORY_PAGE_SIZE}&sortField=position&sortDir=asc`,
257
+ )
258
+ if (!call.ok) return []
259
+ const statuses = readItems(call.result)
260
+ .map(toBoardStatus)
261
+ .filter((status): status is BoardStatus => status !== null)
262
+ return sortByPosition(statuses)
263
+ },
264
+ })
265
+
266
+ const statuses = React.useMemo(() => statusesQuery.data ?? [], [statusesQuery.data])
267
+
268
+ const childrenQuery = useQuery<BoardTask[]>({
269
+ queryKey: [...DRAWER_QUERY_ROOT, 'children', `scope:${scopeVersion}`, taskId],
270
+ enabled: open && !isChildTask && !!task,
271
+ staleTime: 10_000,
272
+ queryFn: async () => {
273
+ const call = await apiCall<Record<string, unknown>>(
274
+ `/api/staff/timesheets/tasks?parentTaskId=${encodeURIComponent(taskId)}&page=1&pageSize=${CHILD_PAGE_SIZE}&sortField=position&sortDir=asc`,
275
+ )
276
+ if (!call.ok) return []
277
+ const rows = readItems(call.result)
278
+ .map(toBoardTask)
279
+ .filter((row): row is BoardTask => row !== null)
280
+ return sortByPosition(rows)
281
+ },
282
+ })
283
+
284
+ const children = React.useMemo(() => childrenQuery.data ?? [], [childrenQuery.data])
285
+
286
+ const commentsQuery = useQuery<DrawerComment[]>({
287
+ queryKey: [...DRAWER_QUERY_ROOT, 'comments', `scope:${scopeVersion}`, taskId],
288
+ enabled: open && taskId.length > 0,
289
+ staleTime: 10_000,
290
+ queryFn: async () => {
291
+ const call = await apiCall<Record<string, unknown>>(
292
+ `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/comments?page=1&pageSize=${COMMENT_PAGE_SIZE}`,
293
+ )
294
+ if (!call.ok) throw new Error('[internal] comments request failed')
295
+ return readItems(call.result)
296
+ .map(toDrawerComment)
297
+ .filter((comment): comment is DrawerComment => comment !== null)
298
+ },
299
+ })
300
+
301
+ const entriesQuery = useQuery<ProjectEntries>({
302
+ queryKey: [...DRAWER_QUERY_ROOT, 'entries', `scope:${scopeVersion}`, projectId ?? 'none'],
303
+ enabled: open && !!projectId,
304
+ staleTime: 10_000,
305
+ queryFn: async () => (projectId ? fetchProjectEntries(projectId) : { rows: [], partial: false }),
306
+ })
307
+
308
+ const selfQuery = useQuery<string | null>({
309
+ queryKey: [...DRAWER_QUERY_ROOT, 'self', `scope:${scopeVersion}`],
310
+ enabled: open,
311
+ staleTime: 300_000,
312
+ queryFn: async () => {
313
+ const call = await apiCall<{ member?: { id?: string | null } | null }>('/api/staff/team-members/self')
314
+ return call.ok ? call.result?.member?.id ?? null : null
315
+ },
316
+ })
317
+
318
+ const selfStaffMemberId = selfQuery.data ?? null
319
+
320
+ const membersQuery = useQuery<{ id: string; name: string }[]>({
321
+ queryKey: [...DRAWER_QUERY_ROOT, 'members', `scope:${scopeVersion}`],
322
+ enabled: open,
323
+ staleTime: 300_000,
324
+ queryFn: async () => {
325
+ const call = await apiCall<Record<string, unknown>>(
326
+ `/api/staff/team-members?page=1&pageSize=${DIRECTORY_PAGE_SIZE}&isActive=true`,
327
+ )
328
+ if (!call.ok) return []
329
+ return readItems(call.result)
330
+ .map((row) => {
331
+ const id = readString(row, 'id')
332
+ if (!id) return null
333
+ return { id, name: readString(row, 'display_name', 'displayName') ?? id }
334
+ })
335
+ .filter((member): member is { id: string; name: string } => member !== null)
336
+ },
337
+ })
338
+
339
+ const members = React.useMemo(() => membersQuery.data ?? [], [membersQuery.data])
340
+
341
+ const memberNames = React.useMemo(() => {
342
+ const map = new Map<string, string>()
343
+ for (const member of members) map.set(member.id, member.name)
344
+ return map
345
+ }, [members])
346
+
347
+ const tagsQuery = useQuery<TagOption[]>({
348
+ queryKey: [...DRAWER_QUERY_ROOT, 'tags', `scope:${scopeVersion}`],
349
+ enabled: open,
350
+ staleTime: 300_000,
351
+ queryFn: async () => {
352
+ const call = await apiCall<Record<string, unknown>>(
353
+ `/api/staff/timesheets/tags?page=1&pageSize=${DIRECTORY_PAGE_SIZE}&sortField=label&sortDir=asc`,
354
+ )
355
+ if (!call.ok) return []
356
+ return readItems(call.result)
357
+ .map((row) => {
358
+ const id = readString(row, 'id')
359
+ if (!id) return null
360
+ // The colour has to travel with the tag or the drawer's chips render
361
+ // grey while the entry dialog's render tinted from the same rows.
362
+ return { id, label: readString(row, 'label') ?? id, color: readString(row, 'color') }
363
+ })
364
+ .filter((tag): tag is TagOption => tag !== null)
365
+ },
366
+ })
367
+
368
+ const tagOptions = React.useMemo(() => tagsQuery.data ?? [], [tagsQuery.data])
369
+
370
+ const runningTimerQuery = useQuery<RunningTimer>({
371
+ queryKey: [...DRAWER_QUERY_ROOT, 'running-timer', `scope:${scopeVersion}`, selfStaffMemberId ?? 'none'],
372
+ enabled: open && !!selfStaffMemberId,
373
+ staleTime: 15_000,
374
+ refetchInterval: 30_000,
375
+ queryFn: async () => {
376
+ if (!selfStaffMemberId) return { entryId: null, taskId: null, startedAt: null }
377
+ const call = await apiCall<Record<string, unknown>>(
378
+ `/api/staff/timesheets/time-entries?staffMemberId=${encodeURIComponent(selfStaffMemberId)}&running=true&pageSize=${ENTRY_PAGE_SIZE}`,
379
+ )
380
+ if (!call.ok) return { entryId: null, taskId: null, startedAt: null }
381
+ for (const row of readItems(call.result)) {
382
+ const startedAt = readString(row, 'started_at', 'startedAt')
383
+ const endedAt = readString(row, 'ended_at', 'endedAt')
384
+ if (!startedAt || endedAt) continue
385
+ return {
386
+ entryId: readString(row, 'id'),
387
+ taskId: readString(row, 'task_id', 'taskId'),
388
+ startedAt,
389
+ }
390
+ }
391
+ return { entryId: null, taskId: null, startedAt: null }
392
+ },
393
+ })
394
+
395
+ const runningTimer = runningTimerQuery.data ?? { entryId: null, taskId: null, startedAt: null }
396
+ const timerRunningHere = !!runningTimer.entryId && runningTimer.taskId === taskId
397
+
398
+ React.useEffect(() => {
399
+ if (!open || !timerRunningHere) return
400
+ const handle = setInterval(() => setNow(Date.now()), ELAPSED_TICK_MS)
401
+ return () => clearInterval(handle)
402
+ }, [open, timerRunningHere])
403
+
404
+ const doneStatusIds = React.useMemo(
405
+ () => new Set(statuses.filter((status) => status.isDone).map((status) => status.id)),
406
+ [statuses],
407
+ )
408
+
409
+ // `doneChildCount` does not exist on the API yet, so the drawer derives it the
410
+ // way the board does — from the children it already fetched for the list. One
411
+ // extra page of rows per open, no extra request.
412
+ const progress = React.useMemo(
413
+ () => computeSubtaskProgress(children, doneStatusIds).get(taskId) ?? { total: 0, done: 0 },
414
+ [children, doneStatusIds, taskId],
415
+ )
416
+
417
+ const subtaskTotal = Math.max(progress.total, task?.childCount ?? 0)
418
+
419
+ const taskIdsForEntries = React.useMemo(() => {
420
+ const ids = new Set<string>([taskId])
421
+ for (const child of children) ids.add(child.id)
422
+ return ids
423
+ }, [children, taskId])
424
+
425
+ const entries = React.useMemo<DrawerEntry[]>(() => {
426
+ const rows = entriesQuery.data?.rows ?? []
427
+ return rows
428
+ .map(toDrawerEntry)
429
+ .filter((entry): entry is DrawerEntry => entry !== null && !!entry.taskId && taskIdsForEntries.has(entry.taskId))
430
+ }, [entriesQuery.data, taskIdsForEntries])
431
+
432
+ const totals = React.useMemo(
433
+ () => summarizeEntries(entries, entriesQuery.data?.partial ?? false),
434
+ [entries, entriesQuery.data],
435
+ )
436
+
437
+ const invalidate = React.useCallback(
438
+ (part: 'task' | 'children' | 'entries' | 'comments') => {
439
+ queryClient.invalidateQueries({ queryKey: [...DRAWER_QUERY_ROOT, part] }).catch(() => {})
440
+ },
441
+ [queryClient],
442
+ )
443
+
444
+ const refreshVersion = React.useCallback(async (): Promise<void> => {
445
+ const refreshed = await taskQuery.refetch()
446
+ const updatedAt = refreshed.data?.task.updatedAt ?? null
447
+ if (updatedAt) versionRef.current = updatedAt
448
+ }, [taskQuery])
449
+
450
+ const reportError = React.useCallback(
451
+ (error: unknown, fallback: string) => {
452
+ if (surfaceRecordConflict(error, t)) {
453
+ invalidate('task')
454
+ invalidate('children')
455
+ return
456
+ }
457
+ logger.error('staff.time_tracking task drawer mutation failed', { err: error })
458
+ flash(error instanceof Error && error.message ? error.message : fallback, 'error')
459
+ },
460
+ [invalidate, t],
461
+ )
462
+
463
+ const handleQuickLog = React.useCallback(
464
+ async ({ minutes, description, date, isBillable, startClock, endClock }: QuickLogSubmission): Promise<boolean> => {
465
+ if (!selfStaffMemberId || !projectId) {
466
+ flash(t('staff.time_tracking.taskDrawer.quickLog.error', 'Could not save the time entry.'), 'error')
467
+ return false
468
+ }
469
+ setBusy(true)
470
+ try {
471
+ await runMutation({
472
+ operation: () =>
473
+ apiCallOrThrow(
474
+ '/api/staff/timesheets/time-entries',
475
+ {
476
+ method: 'POST',
477
+ headers: { 'content-type': 'application/json' },
478
+ body: JSON.stringify({
479
+ // US-C5 defaults — today, me, billable — unless the Details block
480
+ // overrode them. The person is never overridable here: logging
481
+ // for somebody else is a different act with different permissions.
482
+ staffMemberId: selfStaffMemberId,
483
+ date,
484
+ durationMinutes: minutes,
485
+ taskId,
486
+ timeProjectId: projectId,
487
+ isBillable,
488
+ description,
489
+ // The clocks are sent as a pair or not at all. A lone
490
+ // `started_at` is indistinguishable from a running timer —
491
+ // `started_at IS NOT NULL AND ended_at IS NULL` is exactly how
492
+ // the list identifies one — so an entry with a start and no end
493
+ // shows up as a timer running since that moment, and stopping it
494
+ // fails because no timer segment was ever opened for it.
495
+ //
496
+ // The duration is authoritative here, so the missing half is
497
+ // derived from it rather than left blank: the same 2-of-3
498
+ // arithmetic the full entry form does.
499
+ ...buildEntryClocks({ date, startClock, endClock, minutes }),
500
+ source: 'manual',
501
+ }),
502
+ },
503
+ { errorMessage: t('staff.time_tracking.taskDrawer.quickLog.error', 'Could not save the time entry.') },
504
+ ),
505
+ context: {
506
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
507
+ resourceKind: 'staff.timesheets.time_entry',
508
+ resourceId: taskId,
509
+ retryLastMutation,
510
+ },
511
+ })
512
+ flash(t('staff.time_tracking.taskDrawer.quickLog.saved', 'Time logged.'), 'success')
513
+ invalidate('task')
514
+ invalidate('entries')
515
+ onChanged?.()
516
+ return true
517
+ } catch (error) {
518
+ reportError(error, t('staff.time_tracking.taskDrawer.quickLog.error', 'Could not save the time entry.'))
519
+ return false
520
+ } finally {
521
+ setBusy(false)
522
+ }
523
+ },
524
+ [invalidate, onChanged, projectId, reportError, retryLastMutation, runMutation, selfStaffMemberId, t, taskId],
525
+ )
526
+
527
+ const stopTimerEntry = React.useCallback(
528
+ async (entryId: string) => {
529
+ await runMutation({
530
+ operation: () =>
531
+ apiCallOrThrow(
532
+ `/api/staff/timesheets/time-entries/${encodeURIComponent(entryId)}/timer-stop`,
533
+ { method: 'POST' },
534
+ { errorMessage: t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.') },
535
+ ),
536
+ context: {
537
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
538
+ resourceKind: 'staff.timesheets.time_entry',
539
+ resourceId: entryId,
540
+ retryLastMutation,
541
+ },
542
+ })
543
+ },
544
+ [retryLastMutation, runMutation, t],
545
+ )
546
+
547
+ const handleStopTimer = React.useCallback(async () => {
548
+ if (!runningTimer.entryId) return
549
+ setTimerBusy(true)
550
+ try {
551
+ await stopTimerEntry(runningTimer.entryId)
552
+ await runningTimerQuery.refetch()
553
+ invalidate('task')
554
+ invalidate('entries')
555
+ onChanged?.()
556
+ } catch (error) {
557
+ reportError(error, t('staff.time_tracking.board.timer.stopError', 'Could not stop the timer.'))
558
+ } finally {
559
+ setTimerBusy(false)
560
+ }
561
+ }, [invalidate, onChanged, reportError, runningTimer.entryId, runningTimerQuery, stopTimerEntry, t])
562
+
563
+ const handleStartTimer = React.useCallback(async () => {
564
+ if (!selfStaffMemberId || !projectId) return
565
+ // Note 2 / US-D4: one timer at a time. A timer running on another task is
566
+ // stopped only after the person says so — never silently.
567
+ if (runningTimer.entryId && runningTimer.taskId !== taskId) {
568
+ const confirmed = await confirm({
569
+ title: t('staff.time_tracking.taskDrawer.timer.switchTitle', 'A timer is running on another task'),
570
+ text: t(
571
+ 'staff.time_tracking.taskDrawer.timer.switchText',
572
+ 'Only one timer can run at a time. Stop the other one and start it here?',
573
+ ),
574
+ })
575
+ if (!confirmed) return
576
+ }
577
+ setTimerBusy(true)
578
+ try {
579
+ if (runningTimer.entryId && runningTimer.taskId !== taskId) {
580
+ await stopTimerEntry(runningTimer.entryId)
581
+ }
582
+ const started = await runMutation({
583
+ operation: () =>
584
+ apiCallOrThrow<Record<string, unknown>>(
585
+ '/api/staff/timesheets/time-entries/start-timer',
586
+ {
587
+ method: 'POST',
588
+ headers: { 'content-type': 'application/json' },
589
+ body: JSON.stringify({
590
+ staffMemberId: selfStaffMemberId,
591
+ timeProjectId: projectId,
592
+ date: todayIsoDate(),
593
+ notes: task?.title ?? null,
594
+ }),
595
+ },
596
+ { errorMessage: t('staff.time_tracking.board.timer.startError', 'Could not start the timer.') },
597
+ ),
598
+ context: {
599
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
600
+ resourceKind: 'staff.timesheets.time_entry',
601
+ resourceId: taskId,
602
+ retryLastMutation,
603
+ },
604
+ })
605
+ const entryId = typeof started?.result?.id === 'string' ? started.result.id : null
606
+ if (entryId) {
607
+ // `start-timer` is atomic but takes no task, so the link is a second
608
+ // write — the board does the same. A failure leaves a running,
609
+ // project-level timer rather than no timer at all.
610
+ const linked = await apiCall('/api/staff/timesheets/time-entries', {
611
+ method: 'PUT',
612
+ headers: { 'content-type': 'application/json' },
613
+ body: JSON.stringify({ id: entryId, taskId }),
614
+ })
615
+ if (!linked.ok) {
616
+ flash(
617
+ t(
618
+ 'staff.time_tracking.board.timer.linkWarning',
619
+ 'The timer is running but is not linked to this task yet.',
620
+ ),
621
+ 'warning',
622
+ )
623
+ }
624
+ }
625
+ await runningTimerQuery.refetch()
626
+ onChanged?.()
627
+ } catch (error) {
628
+ reportError(error, t('staff.time_tracking.board.timer.startError', 'Could not start the timer.'))
629
+ } finally {
630
+ setTimerBusy(false)
631
+ }
632
+ }, [
633
+ confirm,
634
+ onChanged,
635
+ projectId,
636
+ reportError,
637
+ retryLastMutation,
638
+ runMutation,
639
+ runningTimer.entryId,
640
+ runningTimer.taskId,
641
+ runningTimerQuery,
642
+ selfStaffMemberId,
643
+ stopTimerEntry,
644
+ t,
645
+ task?.title,
646
+ taskId,
647
+ ])
648
+
649
+ /**
650
+ * Property saves are serialized. Each one leaves with the version the previous
651
+ * one produced, so the second blur cannot 409 against the first.
652
+ */
653
+ const runSerialized = React.useCallback(async (job: () => Promise<void>): Promise<void> => {
654
+ const chained = saveChainRef.current.then(job, job)
655
+ saveChainRef.current = chained.catch(() => {})
656
+ return chained
657
+ }, [])
658
+
659
+ const saveTaskField = React.useCallback(
660
+ async (patch: Record<string, unknown>) => {
661
+ if (!task) return
662
+ setBusy(true)
663
+ try {
664
+ await runSerialized(async () => {
665
+ await runMutation({
666
+ operation: () =>
667
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(versionRef.current), () =>
668
+ apiCallOrThrow(
669
+ '/api/staff/timesheets/tasks',
670
+ {
671
+ method: 'PUT',
672
+ headers: { 'content-type': 'application/json' },
673
+ body: JSON.stringify({ id: taskId, ...patch }),
674
+ },
675
+ {
676
+ errorMessage: t(
677
+ 'staff.time_tracking.taskDrawer.properties.saveError',
678
+ 'Could not save the change.',
679
+ ),
680
+ },
681
+ ),
682
+ ),
683
+ context: {
684
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
685
+ resourceKind: 'staff.timesheets.task',
686
+ resourceId: taskId,
687
+ retryLastMutation,
688
+ },
689
+ })
690
+ // The CRUD update answers `{ok:true}`, so the new version comes from
691
+ // the row itself — and it must land before the next field saves.
692
+ await refreshVersion()
693
+ })
694
+ onChanged?.()
695
+ } catch (error) {
696
+ reportError(error, t('staff.time_tracking.taskDrawer.properties.saveError', 'Could not save the change.'))
697
+ } finally {
698
+ setBusy(false)
699
+ }
700
+ },
701
+ [onChanged, refreshVersion, reportError, retryLastMutation, runMutation, runSerialized, t, task, taskId],
702
+ )
703
+
704
+ const handleStatusChange = React.useCallback(
705
+ (statusId: string) => {
706
+ if (!task || statusId === task.taskStatusId) return
707
+ setBusy(true)
708
+ void runSerialized(async () => {
709
+ try {
710
+ const moved = await runMutation({
711
+ operation: () =>
712
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(versionRef.current), () =>
713
+ apiCallOrThrow<Record<string, unknown>>(
714
+ `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/status`,
715
+ {
716
+ method: 'PATCH',
717
+ headers: { 'content-type': 'application/json' },
718
+ body: JSON.stringify({ taskStatusId: statusId }),
719
+ },
720
+ { errorMessage: t('staff.time_tracking.board.move.error', 'Could not move the task.') },
721
+ ),
722
+ ),
723
+ context: {
724
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
725
+ resourceKind: 'staff.timesheets.task',
726
+ resourceId: taskId,
727
+ retryLastMutation,
728
+ },
729
+ })
730
+ // This route does return the new version, so the token refreshes from
731
+ // the response instead of costing a refetch.
732
+ const updatedAt = typeof moved?.result?.updatedAt === 'string' ? moved.result.updatedAt : null
733
+ if (updatedAt) versionRef.current = updatedAt
734
+ else await refreshVersion()
735
+ invalidate('task')
736
+ onChanged?.()
737
+ } catch (error) {
738
+ reportError(error, t('staff.time_tracking.board.move.error', 'Could not move the task.'))
739
+ } finally {
740
+ setBusy(false)
741
+ }
742
+ })
743
+ },
744
+ [
745
+ invalidate,
746
+ onChanged,
747
+ refreshVersion,
748
+ reportError,
749
+ retryLastMutation,
750
+ runMutation,
751
+ runSerialized,
752
+ t,
753
+ task,
754
+ taskId,
755
+ ],
756
+ )
757
+
758
+ const handleAssigneeChange = React.useCallback(
759
+ (value: string) => {
760
+ void saveTaskField({ assigneeStaffMemberId: value === '' ? null : value })
761
+ },
762
+ [saveTaskField],
763
+ )
764
+
765
+ const handleToggleSubtask = React.useCallback(
766
+ async (child: BoardTask, done: boolean) => {
767
+ const targetStatusId = done ? resolveDoneStatusId(statuses) : resolveDefaultStatusId(statuses)
768
+ if (!targetStatusId) return
769
+ setBusy(true)
770
+ try {
771
+ await runMutation({
772
+ operation: () =>
773
+ // The child's own version, not the parent's — this write touches the
774
+ // child row.
775
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(child.updatedAt), () =>
776
+ apiCallOrThrow(
777
+ `/api/staff/timesheets/tasks/${encodeURIComponent(child.id)}/status`,
778
+ {
779
+ method: 'PATCH',
780
+ headers: { 'content-type': 'application/json' },
781
+ body: JSON.stringify({ taskStatusId: targetStatusId }),
782
+ },
783
+ {
784
+ errorMessage: t(
785
+ 'staff.time_tracking.taskDrawer.subtasks.toggleError',
786
+ 'Could not change the subtask status.',
787
+ ),
788
+ },
789
+ ),
790
+ ),
791
+ context: {
792
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
793
+ resourceKind: 'staff.timesheets.task',
794
+ resourceId: child.id,
795
+ retryLastMutation,
796
+ },
797
+ })
798
+ invalidate('children')
799
+ invalidate('task')
800
+ onChanged?.()
801
+ } catch (error) {
802
+ reportError(
803
+ error,
804
+ t('staff.time_tracking.taskDrawer.subtasks.toggleError', 'Could not change the subtask status.'),
805
+ )
806
+ } finally {
807
+ setBusy(false)
808
+ }
809
+ },
810
+ [invalidate, onChanged, reportError, retryLastMutation, runMutation, statuses, t],
811
+ )
812
+
813
+ const handleAddSubtask = React.useCallback(async () => {
814
+ const title = subtaskDraft.trim()
815
+ if (!title || !projectId) {
816
+ setComposingSubtask(false)
817
+ setSubtaskDraft('')
818
+ return
819
+ }
820
+ setBusy(true)
821
+ try {
822
+ await runMutation({
823
+ operation: () =>
824
+ apiCallOrThrow(
825
+ '/api/staff/timesheets/tasks',
826
+ {
827
+ method: 'POST',
828
+ headers: { 'content-type': 'application/json' },
829
+ body: JSON.stringify({
830
+ timeProjectId: projectId,
831
+ parentTaskId: taskId,
832
+ title,
833
+ assigneeStaffMemberId: selfStaffMemberId,
834
+ }),
835
+ },
836
+ {
837
+ errorMessage: t('staff.time_tracking.taskDrawer.subtasks.addError', 'Could not add the subtask.'),
838
+ },
839
+ ),
840
+ context: {
841
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
842
+ resourceKind: 'staff.timesheets.task',
843
+ resourceId: taskId,
844
+ retryLastMutation,
845
+ },
846
+ })
847
+ setSubtaskDraft('')
848
+ setComposingSubtask(false)
849
+ invalidate('children')
850
+ invalidate('task')
851
+ onChanged?.()
852
+ } catch (error) {
853
+ reportError(error, t('staff.time_tracking.taskDrawer.subtasks.addError', 'Could not add the subtask.'))
854
+ } finally {
855
+ setBusy(false)
856
+ }
857
+ }, [
858
+ invalidate,
859
+ onChanged,
860
+ projectId,
861
+ reportError,
862
+ retryLastMutation,
863
+ runMutation,
864
+ selfStaffMemberId,
865
+ subtaskDraft,
866
+ t,
867
+ taskId,
868
+ ])
869
+
870
+ const handlePostComment = React.useCallback(
871
+ async (body: string): Promise<boolean> => {
872
+ setBusy(true)
873
+ try {
874
+ await runMutation({
875
+ operation: () =>
876
+ apiCallOrThrow(
877
+ `/api/staff/timesheets/tasks/${encodeURIComponent(taskId)}/comments`,
878
+ {
879
+ method: 'POST',
880
+ headers: { 'content-type': 'application/json' },
881
+ body: JSON.stringify({ body }),
882
+ },
883
+ { errorMessage: t('staff.time_tracking.taskDrawer.comments.error', 'Could not post the comment.') },
884
+ ),
885
+ context: {
886
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
887
+ resourceKind: 'staff.timesheets.task_comment',
888
+ resourceId: taskId,
889
+ retryLastMutation,
890
+ },
891
+ })
892
+ invalidate('comments')
893
+ return true
894
+ } catch (error) {
895
+ reportError(error, t('staff.time_tracking.taskDrawer.comments.error', 'Could not post the comment.'))
896
+ return false
897
+ } finally {
898
+ setBusy(false)
899
+ }
900
+ },
901
+ [invalidate, reportError, retryLastMutation, runMutation, t, taskId],
902
+ )
903
+
904
+ const handleTagChange = React.useCallback(
905
+ async (tagId: string, assign: boolean) => {
906
+ setBusy(true)
907
+ const previous = tagIds
908
+ setTagIds((current) =>
909
+ assign ? Array.from(new Set([...current, tagId])) : current.filter((id) => id !== tagId),
910
+ )
911
+ try {
912
+ await runMutation({
913
+ operation: () =>
914
+ apiCallOrThrow(
915
+ '/api/staff/timesheets/tags/task-assignments',
916
+ {
917
+ method: assign ? 'POST' : 'DELETE',
918
+ headers: { 'content-type': 'application/json' },
919
+ body: JSON.stringify({ taskId, tagIds: [tagId] }),
920
+ },
921
+ { errorMessage: t('staff.time_tracking.taskDrawer.properties.tagError', 'Could not save the tags.') },
922
+ ),
923
+ context: {
924
+ formId: TASK_DRAWER_MUTATION_CONTEXT_ID,
925
+ resourceKind: 'staff.timesheets.task_tag',
926
+ resourceId: taskId,
927
+ retryLastMutation,
928
+ },
929
+ })
930
+ onChanged?.()
931
+ } catch (error) {
932
+ setTagIds(previous)
933
+ reportError(error, t('staff.time_tracking.taskDrawer.properties.tagError', 'Could not save the tags.'))
934
+ } finally {
935
+ setBusy(false)
936
+ }
937
+ },
938
+ [onChanged, reportError, retryLastMutation, runMutation, t, tagIds, taskId],
939
+ )
940
+
941
+ /**
942
+ * Creates the tag and hands it back so the picker can assign it through the
943
+ * drawer's own `handleTagChange` — the task-assignments write is untouched, the
944
+ * new row simply becomes assignable without leaving the drawer. The list is
945
+ * invalidated rather than patched so the server-assigned colour arrives with it.
946
+ */
947
+ const createTag = React.useCallback(
948
+ async (label: string): Promise<TagOption | null> => {
949
+ try {
950
+ const created = await apiCallOrThrow<Record<string, unknown>>(
951
+ '/api/staff/timesheets/tags',
952
+ {
953
+ method: 'POST',
954
+ headers: { 'content-type': 'application/json' },
955
+ body: JSON.stringify({ label, slug: slugifyProjectName(label).toLowerCase() }),
956
+ },
957
+ {
958
+ errorMessage: t(
959
+ 'staff.time_tracking.entryDialog.errors.createTag',
960
+ 'Could not create the tag.',
961
+ ),
962
+ },
963
+ )
964
+ const id = typeof created?.result?.id === 'string' ? created.result.id : null
965
+ if (!id) return null
966
+ await queryClient.invalidateQueries({ queryKey: [...DRAWER_QUERY_ROOT, 'tags'] })
967
+ return { id, label, color: null }
968
+ } catch (error) {
969
+ logger.warn('staff.time_tracking task drawer tag create failed', { err: error })
970
+ flash(t('staff.time_tracking.entryDialog.errors.createTag', 'Could not create the tag.'), 'error')
971
+ return null
972
+ }
973
+ },
974
+ [queryClient, t],
975
+ )
976
+
977
+ const status = React.useMemo(
978
+ () => statuses.find((entry) => entry.id === task?.taskStatusId) ?? null,
979
+ [statuses, task?.taskStatusId],
980
+ )
981
+
982
+ const statusVariant = status?.isDone ? 'success' : status?.isDefault ? 'neutral' : 'info'
983
+
984
+ const recentEntries = React.useMemo(() => entries.slice(0, RECENT_ENTRY_COUNT), [entries])
985
+
986
+ const assignedTags = React.useMemo<TagOption[]>(() => {
987
+ const byId = new Map(tagOptions.map((tag) => [tag.id, tag]))
988
+ return tagIds.map((id) => byId.get(id) ?? { id, label: id, color: null })
989
+ }, [tagIds, tagOptions])
990
+
991
+ const availableTags = React.useMemo(
992
+ () => tagOptions.filter((tag) => !tagIds.includes(tag.id)),
993
+ [tagIds, tagOptions],
994
+ )
995
+
996
+ const doneStatusId = resolveDoneStatusId(statuses)
997
+
998
+ const body = (() => {
999
+ if (taskQuery.isLoading) {
1000
+ return <LoadingMessage label={t('staff.time_tracking.taskDrawer.loading', 'Loading the task…')} />
1001
+ }
1002
+ if (taskQuery.isError) {
1003
+ return <ErrorMessage label={t('staff.time_tracking.taskDrawer.loadError', 'Could not load the task.')} />
1004
+ }
1005
+ if (!task) {
1006
+ return (
1007
+ <ErrorMessage
1008
+ label={t('staff.time_tracking.tasks.errors.notFound', 'Task not found or not accessible.')}
1009
+ />
1010
+ )
1011
+ }
1012
+
1013
+ return (
1014
+ <div className="flex flex-col gap-6">
1015
+ <TaskQuickLog
1016
+ onLog={handleQuickLog}
1017
+ logging={busy}
1018
+ timerRunningHere={timerRunningHere}
1019
+ elapsed={timerRunningHere ? formatElapsed(runningTimer.startedAt, now) : null}
1020
+ timerBusy={timerBusy}
1021
+ onStartTimer={() => { void handleStartTimer() }}
1022
+ onStopTimer={() => { void handleStopTimer() }}
1023
+ autoFocus={focusPanel === 'time'}
1024
+ disabled={!canManage}
1025
+ today={todayIsoDate()}
1026
+ />
1027
+
1028
+ <section className="flex flex-col gap-2" data-testid="task-drawer-logged">
1029
+ <div className="flex items-baseline justify-between gap-3">
1030
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1031
+ {t('staff.time_tracking.taskDrawer.logged.title', 'Logged time')}
1032
+ </h3>
1033
+ <span className="font-mono text-base font-semibold tabular-nums text-foreground" data-testid="task-drawer-logged-total">
1034
+ {formatBoardMinutes(task.loggedMinutes)}
1035
+ </span>
1036
+ </div>
1037
+ <p className="text-xs text-muted-foreground" data-testid="task-drawer-logged-split">
1038
+ {t('staff.time_tracking.taskDrawer.logged.split', 'own {own} · subtasks {children}', {
1039
+ own: formatSplitMinutes(task.ownMinutes),
1040
+ children: formatSplitMinutes(childMinutes(task.loggedMinutes, task.ownMinutes)),
1041
+ })}
1042
+ </p>
1043
+ <div className="flex flex-wrap items-center gap-2">
1044
+ <Badge variant="success">
1045
+ {t('staff.time_tracking.taskDrawer.logged.billable', 'billable {hours}', {
1046
+ hours: formatBoardMinutes(totals.billableMinutes),
1047
+ })}
1048
+ </Badge>
1049
+ <Badge variant="neutral">
1050
+ {t('staff.time_tracking.taskDrawer.logged.nonBillable', 'non-billable {hours}', {
1051
+ hours: formatBoardMinutes(totals.nonBillableMinutes),
1052
+ })}
1053
+ </Badge>
1054
+ {canSeeMoney && totals.cost !== null ? (
1055
+ <Badge variant="info" data-testid="task-drawer-cost">
1056
+ {t('staff.time_tracking.taskDrawer.logged.cost', 'cost {amount}', {
1057
+ amount: formatCurrency(totals.cost, totals.currencyCode) ?? String(totals.cost),
1058
+ })}
1059
+ </Badge>
1060
+ ) : null}
1061
+ </div>
1062
+ {totals.partial ? (
1063
+ <p className="text-xs text-muted-foreground">
1064
+ {t(
1065
+ 'staff.time_tracking.taskDrawer.logged.partialHint',
1066
+ 'The split is counted from the most recent entries in this project.',
1067
+ )}
1068
+ </p>
1069
+ ) : null}
1070
+ </section>
1071
+
1072
+ {isChildTask ? null : (
1073
+ <section className="flex flex-col gap-3" data-testid="task-drawer-subtasks">
1074
+ <div className="flex items-baseline justify-between gap-3">
1075
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1076
+ {t('staff.time_tracking.taskDrawer.subtasks.title', 'Subtasks')}
1077
+ </h3>
1078
+ <span className="font-mono text-xs tabular-nums text-muted-foreground">
1079
+ {t('staff.time_tracking.taskDrawer.subtasks.count', '{done}/{total}', {
1080
+ done: progress.done,
1081
+ total: subtaskTotal,
1082
+ })}
1083
+ </span>
1084
+ </div>
1085
+ {subtaskTotal > 0 ? (
1086
+ <Progress value={subtaskTotal > 0 ? (progress.done / subtaskTotal) * 100 : 0} max={100} />
1087
+ ) : null}
1088
+ {children.length === 0 ? (
1089
+ <p className="text-xs text-muted-foreground">
1090
+ {t('staff.time_tracking.taskDrawer.subtasks.empty', 'No subtasks yet.')}
1091
+ </p>
1092
+ ) : (
1093
+ <ul className="flex flex-col">
1094
+ {children.map((child) => {
1095
+ const done = (child.taskStatusId && doneStatusIds.has(child.taskStatusId)) || !!child.closedAt
1096
+ const assigneeName = child.assigneeStaffMemberId
1097
+ ? memberNames.get(child.assigneeStaffMemberId) ?? child.assigneeStaffMemberId
1098
+ : t('staff.time_tracking.taskDrawer.properties.unassigned', 'Unassigned')
1099
+ return (
1100
+ <li
1101
+ key={child.id}
1102
+ className="flex items-center gap-2.5 border-b border-border py-2 last:border-b-0"
1103
+ data-subtask-id={child.id}
1104
+ >
1105
+ <Checkbox
1106
+ checked={done}
1107
+ disabled={!canManage || busy || !doneStatusId}
1108
+ aria-label={t('staff.time_tracking.taskDrawer.subtasks.toggle', 'Mark {title} as done', {
1109
+ title: child.title,
1110
+ })}
1111
+ onCheckedChange={(next) => { void handleToggleSubtask(child, next === true) }}
1112
+ />
1113
+ <span
1114
+ className={`min-w-0 flex-1 truncate text-sm ${done ? 'text-muted-foreground line-through' : 'text-foreground'}`}
1115
+ >
1116
+ {child.title}
1117
+ </span>
1118
+ <Avatar label={assigneeName} size="xs" />
1119
+ <span className="font-mono text-xs tabular-nums text-muted-foreground">
1120
+ {formatBoardMinutes(child.loggedMinutes)}
1121
+ </span>
1122
+ </li>
1123
+ )
1124
+ })}
1125
+ </ul>
1126
+ )}
1127
+ {!doneStatusId && children.length > 0 ? (
1128
+ <p className="text-xs text-muted-foreground">
1129
+ {t(
1130
+ 'staff.time_tracking.taskDrawer.subtasks.noDoneColumn',
1131
+ 'This project has no done column, so a subtask has no way to close.',
1132
+ )}
1133
+ </p>
1134
+ ) : null}
1135
+ {canManage ? (
1136
+ composingSubtask ? (
1137
+ <Input
1138
+ autoFocus
1139
+ size="sm"
1140
+ value={subtaskDraft}
1141
+ disabled={busy}
1142
+ onChange={(event) => setSubtaskDraft(event.target.value)}
1143
+ onKeyDown={(event) => {
1144
+ if (event.key === 'Enter') {
1145
+ event.preventDefault()
1146
+ void handleAddSubtask()
1147
+ }
1148
+ if (event.key === 'Escape') {
1149
+ event.preventDefault()
1150
+ event.stopPropagation()
1151
+ setComposingSubtask(false)
1152
+ setSubtaskDraft('')
1153
+ }
1154
+ }}
1155
+ placeholder={t('staff.time_tracking.taskDrawer.subtasks.placeholder', 'Subtask title')}
1156
+ aria-label={t('staff.time_tracking.taskDrawer.subtasks.add', 'Add subtask')}
1157
+ data-testid="task-drawer-subtask-input"
1158
+ />
1159
+ ) : (
1160
+ <Button
1161
+ type="button"
1162
+ variant="ghost"
1163
+ size="sm"
1164
+ className="self-start"
1165
+ onClick={() => setComposingSubtask(true)}
1166
+ data-testid="task-drawer-add-subtask"
1167
+ >
1168
+ <Plus className="size-3.5" aria-hidden="true" />
1169
+ {t('staff.time_tracking.taskDrawer.subtasks.add', 'Add subtask')}
1170
+ </Button>
1171
+ )
1172
+ ) : null}
1173
+ </section>
1174
+ )}
1175
+
1176
+ <section className="flex flex-col gap-3" data-testid="task-drawer-entries">
1177
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1178
+ {t('staff.time_tracking.taskDrawer.entries.title', 'Time entries ({count})', { count: entries.length })}
1179
+ </h3>
1180
+ {recentEntries.length === 0 ? (
1181
+ <p className="text-xs text-muted-foreground">
1182
+ {t('staff.time_tracking.taskDrawer.entries.empty', 'No time entries yet.')}
1183
+ </p>
1184
+ ) : (
1185
+ <ul className="flex flex-col">
1186
+ {recentEntries.map((entry) => (
1187
+ <li
1188
+ key={entry.id}
1189
+ className="flex items-center gap-3 border-b border-border py-2 text-sm last:border-b-0"
1190
+ data-entry-id={entry.id}
1191
+ >
1192
+ <span className="w-20 shrink-0 text-xs text-muted-foreground">
1193
+ {formatDate(entry.date) ?? ''}
1194
+ </span>
1195
+ <span className="min-w-0 flex-1 truncate text-foreground">
1196
+ {entry.description
1197
+ ?? t('staff.time_tracking.taskDrawer.entries.noDescription', 'No description')}
1198
+ </span>
1199
+ <span className="font-mono text-xs tabular-nums text-muted-foreground">
1200
+ {formatBoardMinutes(entry.durationMinutes)}
1201
+ </span>
1202
+ </li>
1203
+ ))}
1204
+ </ul>
1205
+ )}
1206
+ <Button
1207
+ type="button"
1208
+ variant="ghost"
1209
+ size="sm"
1210
+ className="self-start"
1211
+ onClick={() => router.push(`${ENTRIES_LIST_PATH}?taskId=${encodeURIComponent(taskId)}`)}
1212
+ >
1213
+ {t('staff.time_tracking.taskDrawer.entries.showAll', 'Show all')}
1214
+ </Button>
1215
+ </section>
1216
+
1217
+ <TaskCommentThread
1218
+ comments={commentsQuery.data ?? []}
1219
+ loading={commentsQuery.isLoading}
1220
+ loadError={commentsQuery.isError}
1221
+ posting={busy}
1222
+ canComment={canManage}
1223
+ onPost={handlePostComment}
1224
+ />
1225
+
1226
+ <section className="flex flex-col gap-3" data-testid="task-drawer-properties">
1227
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1228
+ {t('staff.time_tracking.taskDrawer.properties.title', 'Properties')}
1229
+ </h3>
1230
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
1231
+ <div className="flex flex-col gap-1.5">
1232
+ <span className="text-xs font-medium text-muted-foreground">
1233
+ {t('staff.time_tracking.taskDrawer.properties.status', 'Status')}
1234
+ </span>
1235
+ <Select
1236
+ value={task.taskStatusId ?? undefined}
1237
+ disabled={!canManage || busy}
1238
+ onValueChange={handleStatusChange}
1239
+ >
1240
+ <SelectTrigger
1241
+ aria-label={t('staff.time_tracking.taskDrawer.properties.status', 'Status')}
1242
+ data-testid="task-drawer-status-select"
1243
+ >
1244
+ <SelectValue />
1245
+ </SelectTrigger>
1246
+ <SelectContent>
1247
+ {statuses.map((entry) => (
1248
+ <SelectItem key={entry.id} value={entry.id}>
1249
+ {entry.name}
1250
+ </SelectItem>
1251
+ ))}
1252
+ </SelectContent>
1253
+ </Select>
1254
+ </div>
1255
+ <div className="flex flex-col gap-1.5">
1256
+ <span className="text-xs font-medium text-muted-foreground">
1257
+ {t('staff.time_tracking.taskDrawer.properties.assignee', 'Assignee')}
1258
+ </span>
1259
+ <Select
1260
+ value={task.assigneeStaffMemberId ?? undefined}
1261
+ disabled={!canManage || busy}
1262
+ onValueChange={handleAssigneeChange}
1263
+ >
1264
+ <SelectTrigger
1265
+ aria-label={t('staff.time_tracking.taskDrawer.properties.assignee', 'Assignee')}
1266
+ data-testid="task-drawer-assignee-select"
1267
+ >
1268
+ <SelectValue
1269
+ placeholder={t('staff.time_tracking.taskDrawer.properties.unassigned', 'Unassigned')}
1270
+ />
1271
+ </SelectTrigger>
1272
+ <SelectContent>
1273
+ {members.map((member) => (
1274
+ <SelectItem key={member.id} value={member.id}>
1275
+ {member.name}
1276
+ </SelectItem>
1277
+ ))}
1278
+ </SelectContent>
1279
+ </Select>
1280
+ </div>
1281
+ </div>
1282
+
1283
+ <div className="flex flex-col gap-1.5">
1284
+ <span className="text-xs font-medium text-muted-foreground">
1285
+ {t('staff.time_tracking.taskDrawer.properties.tags', 'Tags')}
1286
+ </span>
1287
+ <div className="flex flex-wrap items-center gap-2">
1288
+ {assignedTags.map((tag) => (
1289
+ <Badge key={tag.id} variant="neutral" className="gap-1" style={tagChipStyle(tag)}>
1290
+ {tag.label}
1291
+ {canManage ? (
1292
+ <button
1293
+ type="button"
1294
+ aria-label={t('staff.time_tracking.taskDrawer.properties.removeTag', 'Remove tag {label}', {
1295
+ label: tag.label,
1296
+ })}
1297
+ onClick={() => { void handleTagChange(tag.id, false) }}
1298
+ >
1299
+ <X className="size-3" aria-hidden="true" />
1300
+ </button>
1301
+ ) : null}
1302
+ </Badge>
1303
+ ))}
1304
+ {canManage ? (
1305
+ <TagPicker
1306
+ available={availableTags}
1307
+ disabled={busy}
1308
+ onAssign={(id) => { void handleTagChange(id, true) }}
1309
+ onCreate={createTag}
1310
+ labels={{
1311
+ add: t('staff.time_tracking.taskDrawer.properties.addTag', 'Add tag'),
1312
+ create: (name) =>
1313
+ t('staff.time_tracking.entryDialog.createTag', 'Create "{name}"', { name }),
1314
+ empty: t('staff.time_tracking.entryDialog.noTags', 'No matching tag'),
1315
+ }}
1316
+ testIdPrefix="task-drawer"
1317
+ />
1318
+ ) : null}
1319
+ </div>
1320
+ <p className="text-xs text-muted-foreground">
1321
+ {t(
1322
+ 'staff.time_tracking.taskDrawer.properties.tagsPending',
1323
+ 'Tags already saved on this task appear here once the task list returns them.',
1324
+ )}
1325
+ </p>
1326
+ </div>
1327
+ <InjectionSpot
1328
+ spotId={extensionPoints.hosts.taskDetailSidebar.spotId}
1329
+ context={taskInjectionContext}
1330
+ data={task}
1331
+ />
1332
+ </section>
1333
+
1334
+ <InjectionSpot
1335
+ spotId={extensionPoints.hosts.taskDetailTabs.spotId}
1336
+ context={taskInjectionContext}
1337
+ data={task}
1338
+ />
1339
+ </div>
1340
+ )
1341
+ })()
1342
+
1343
+ const createdLabel = formatDate(createdAt)
1344
+ const headerLine = [
1345
+ projectName ?? null,
1346
+ createdLabel
1347
+ ? t('staff.time_tracking.taskDrawer.created', 'created {date}', { date: createdLabel })
1348
+ : null,
1349
+ ]
1350
+ .filter((part): part is string => !!part)
1351
+ .join(' · ')
1352
+
1353
+ return (
1354
+ <Drawer open={open} onOpenChange={onOpenChange}>
1355
+ <DrawerContent
1356
+ className="sm:max-w-xl"
1357
+ closeAriaLabel={t('staff.time_tracking.taskDrawer.close', 'Close')}
1358
+ data-testid="task-drawer"
1359
+ >
1360
+ <DrawerHeader>
1361
+ <div className="flex min-w-0 flex-1 flex-col gap-1.5">
1362
+ <div className="flex items-center gap-2">
1363
+ {status ? <Badge variant={statusVariant}>{status.name}</Badge> : null}
1364
+ {task?.reference ? (
1365
+ <span className="text-xs text-muted-foreground">{task.reference}</span>
1366
+ ) : null}
1367
+ <InjectionSpot
1368
+ spotId={extensionPoints.hosts.taskDetailStatusBadges.spotId}
1369
+ context={taskInjectionContext}
1370
+ data={task}
1371
+ />
1372
+ </div>
1373
+ <DrawerTitle className="truncate">{task?.title ?? ''}</DrawerTitle>
1374
+ <DrawerDescription>{headerLine}</DrawerDescription>
1375
+ <InjectionSpot
1376
+ spotId={extensionPoints.hosts.taskDetailHeader.spotId}
1377
+ context={taskInjectionContext}
1378
+ data={task}
1379
+ />
1380
+ </div>
1381
+ </DrawerHeader>
1382
+ <DrawerBody>{body}</DrawerBody>
1383
+ <DrawerFooter
1384
+ leading={
1385
+ <span className="text-xs text-muted-foreground">
1386
+ {t('staff.time_tracking.taskDrawer.autoSave', 'Changes are saved automatically')}
1387
+ </span>
1388
+ }
1389
+ >
1390
+ <InjectionSpot
1391
+ spotId={extensionPoints.hosts.taskDetailFooter.spotId}
1392
+ context={taskInjectionContext}
1393
+ data={task}
1394
+ />
1395
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
1396
+ {t('staff.time_tracking.taskDrawer.close', 'Close')}
1397
+ </Button>
1398
+ </DrawerFooter>
1399
+ {ConfirmDialogElement}
1400
+ </DrawerContent>
1401
+ </Drawer>
1402
+ )
1403
+ }
1404
+
1405
+ export default TaskDrawer