@hed-hog/operations 0.0.330 → 0.0.332

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 (320) hide show
  1. package/README.md +5 -5
  2. package/dist/controllers/operations-collaborators.controller.d.ts +58 -213
  3. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  4. package/dist/controllers/operations-collaborators.controller.js +100 -0
  5. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  6. package/dist/controllers/operations-contracts.controller.d.ts +6 -6
  7. package/dist/controllers/operations-projects.controller.d.ts +25 -0
  8. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  9. package/dist/controllers/operations-projects.controller.js +48 -0
  10. package/dist/controllers/operations-projects.controller.js.map +1 -1
  11. package/dist/controllers/operations-reports.controller.d.ts +1 -1
  12. package/dist/controllers/operations-tasks.controller.d.ts +34 -9
  13. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  14. package/dist/controllers/operations-tasks.controller.js +43 -32
  15. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  16. package/dist/controllers/operations-timesheets.controller.d.ts +9 -9
  17. package/dist/dashboard/components/DashboardLayout.d.ts +30 -0
  18. package/dist/dashboard/components/DashboardLayout.d.ts.map +1 -0
  19. package/dist/dashboard/components/DashboardLayout.js +87 -0
  20. package/dist/dashboard/components/DashboardLayout.js.map +1 -0
  21. package/dist/dashboard/components/widget-registry.d.ts +23 -0
  22. package/dist/dashboard/components/widget-registry.d.ts.map +1 -0
  23. package/dist/dashboard/components/widget-registry.js +245 -0
  24. package/dist/dashboard/components/widget-registry.js.map +1 -0
  25. package/dist/dashboard/hooks/useDashboardData.d.ts +20 -0
  26. package/dist/dashboard/hooks/useDashboardData.d.ts.map +1 -0
  27. package/dist/dashboard/hooks/useDashboardData.js +24 -0
  28. package/dist/dashboard/hooks/useDashboardData.js.map +1 -0
  29. package/dist/dashboard/types/widgets.types.d.ts +233 -0
  30. package/dist/dashboard/types/widgets.types.d.ts.map +1 -0
  31. package/dist/dashboard/types/widgets.types.js +6 -0
  32. package/dist/dashboard/types/widgets.types.js.map +1 -0
  33. package/dist/dashboard/widgets/CapacityDistribution.d.ts +23 -0
  34. package/dist/dashboard/widgets/CapacityDistribution.d.ts.map +1 -0
  35. package/dist/dashboard/widgets/CapacityDistribution.js +11 -0
  36. package/dist/dashboard/widgets/CapacityDistribution.js.map +1 -0
  37. package/dist/dashboard/widgets/EffortByProject.d.ts +22 -0
  38. package/dist/dashboard/widgets/EffortByProject.d.ts.map +1 -0
  39. package/dist/dashboard/widgets/EffortByProject.js +11 -0
  40. package/dist/dashboard/widgets/EffortByProject.js.map +1 -0
  41. package/dist/dashboard/widgets/HeadcountByArea.d.ts +24 -0
  42. package/dist/dashboard/widgets/HeadcountByArea.d.ts.map +1 -0
  43. package/dist/dashboard/widgets/HeadcountByArea.js +11 -0
  44. package/dist/dashboard/widgets/HeadcountByArea.js.map +1 -0
  45. package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts +18 -0
  46. package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts.map +1 -0
  47. package/dist/dashboard/widgets/ManagedProjectsStatus.js +12 -0
  48. package/dist/dashboard/widgets/ManagedProjectsStatus.js.map +1 -0
  49. package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts +22 -0
  50. package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts.map +1 -0
  51. package/dist/dashboard/widgets/MyHoursPeriodKpi.js +12 -0
  52. package/dist/dashboard/widgets/MyHoursPeriodKpi.js.map +1 -0
  53. package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts +19 -0
  54. package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts.map +1 -0
  55. package/dist/dashboard/widgets/MyOpenRequestsKpi.js +17 -0
  56. package/dist/dashboard/widgets/MyOpenRequestsKpi.js.map +1 -0
  57. package/dist/dashboard/widgets/MyPendingRequestsList.d.ts +23 -0
  58. package/dist/dashboard/widgets/MyPendingRequestsList.d.ts.map +1 -0
  59. package/dist/dashboard/widgets/MyPendingRequestsList.js +14 -0
  60. package/dist/dashboard/widgets/MyPendingRequestsList.js.map +1 -0
  61. package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts +22 -0
  62. package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts.map +1 -0
  63. package/dist/dashboard/widgets/MyProjectAllocationsKpi.js +11 -0
  64. package/dist/dashboard/widgets/MyProjectAllocationsKpi.js.map +1 -0
  65. package/dist/dashboard/widgets/MyQuickActions.d.ts +23 -0
  66. package/dist/dashboard/widgets/MyQuickActions.d.ts.map +1 -0
  67. package/dist/dashboard/widgets/MyQuickActions.js +18 -0
  68. package/dist/dashboard/widgets/MyQuickActions.js.map +1 -0
  69. package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts +23 -0
  70. package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts.map +1 -0
  71. package/dist/dashboard/widgets/MyRelevantDeadlines.js +22 -0
  72. package/dist/dashboard/widgets/MyRelevantDeadlines.js.map +1 -0
  73. package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts +17 -0
  74. package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts.map +1 -0
  75. package/dist/dashboard/widgets/MyTimesheetStatusKpi.js +11 -0
  76. package/dist/dashboard/widgets/MyTimesheetStatusKpi.js.map +1 -0
  77. package/dist/dashboard/widgets/MyWeeklyJourney.d.ts +21 -0
  78. package/dist/dashboard/widgets/MyWeeklyJourney.d.ts.map +1 -0
  79. package/dist/dashboard/widgets/MyWeeklyJourney.js +19 -0
  80. package/dist/dashboard/widgets/MyWeeklyJourney.js.map +1 -0
  81. package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts +19 -0
  82. package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts.map +1 -0
  83. package/dist/dashboard/widgets/PortfolioCostsKpi.js +12 -0
  84. package/dist/dashboard/widgets/PortfolioCostsKpi.js.map +1 -0
  85. package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts +18 -0
  86. package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts.map +1 -0
  87. package/dist/dashboard/widgets/PortfolioEffortKpi.js +8 -0
  88. package/dist/dashboard/widgets/PortfolioEffortKpi.js.map +1 -0
  89. package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts +22 -0
  90. package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts.map +1 -0
  91. package/dist/dashboard/widgets/PortfolioProjectsKpi.js +56 -0
  92. package/dist/dashboard/widgets/PortfolioProjectsKpi.js.map +1 -0
  93. package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts +19 -0
  94. package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts.map +1 -0
  95. package/dist/dashboard/widgets/PortfolioRiskKpi.js +11 -0
  96. package/dist/dashboard/widgets/PortfolioRiskKpi.js.map +1 -0
  97. package/dist/dashboard/widgets/ProjectStatusOverview.d.ts +19 -0
  98. package/dist/dashboard/widgets/ProjectStatusOverview.d.ts.map +1 -0
  99. package/dist/dashboard/widgets/ProjectStatusOverview.js +18 -0
  100. package/dist/dashboard/widgets/ProjectStatusOverview.js.map +1 -0
  101. package/dist/dashboard/widgets/StrategicDeadlines.d.ts +24 -0
  102. package/dist/dashboard/widgets/StrategicDeadlines.d.ts.map +1 -0
  103. package/dist/dashboard/widgets/StrategicDeadlines.js +22 -0
  104. package/dist/dashboard/widgets/StrategicDeadlines.js.map +1 -0
  105. package/dist/dashboard/widgets/TeamApprovalQueue.d.ts +24 -0
  106. package/dist/dashboard/widgets/TeamApprovalQueue.d.ts.map +1 -0
  107. package/dist/dashboard/widgets/TeamApprovalQueue.js +12 -0
  108. package/dist/dashboard/widgets/TeamApprovalQueue.js.map +1 -0
  109. package/dist/dashboard/widgets/TeamCapacityKpi.d.ts +18 -0
  110. package/dist/dashboard/widgets/TeamCapacityKpi.d.ts.map +1 -0
  111. package/dist/dashboard/widgets/TeamCapacityKpi.js +19 -0
  112. package/dist/dashboard/widgets/TeamCapacityKpi.js.map +1 -0
  113. package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts +22 -0
  114. package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts.map +1 -0
  115. package/dist/dashboard/widgets/TeamHeadcountKpi.js +56 -0
  116. package/dist/dashboard/widgets/TeamHeadcountKpi.js.map +1 -0
  117. package/dist/dashboard/widgets/TeamHoursKpi.d.ts +19 -0
  118. package/dist/dashboard/widgets/TeamHoursKpi.d.ts.map +1 -0
  119. package/dist/dashboard/widgets/TeamHoursKpi.js +13 -0
  120. package/dist/dashboard/widgets/TeamHoursKpi.js.map +1 -0
  121. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts +20 -0
  122. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts.map +1 -0
  123. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js +11 -0
  124. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js.map +1 -0
  125. package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts +18 -0
  126. package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts.map +1 -0
  127. package/dist/dashboard/widgets/TeamUtilizationOverview.js +17 -0
  128. package/dist/dashboard/widgets/TeamUtilizationOverview.js.map +1 -0
  129. package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts +24 -0
  130. package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts.map +1 -0
  131. package/dist/dashboard/widgets/TeamWorkloadAlerts.js +19 -0
  132. package/dist/dashboard/widgets/TeamWorkloadAlerts.js.map +1 -0
  133. package/dist/dashboard/widgets/index.d.ts +24 -0
  134. package/dist/dashboard/widgets/index.d.ts.map +1 -0
  135. package/dist/dashboard/widgets/index.js +54 -0
  136. package/dist/dashboard/widgets/index.js.map +1 -0
  137. package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
  138. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
  139. package/dist/dto/create-collaborator-invoice.dto.js +55 -0
  140. package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
  141. package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
  142. package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
  143. package/dist/dto/create-collaborator-payment.dto.js +50 -0
  144. package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
  145. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  146. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  147. package/dist/dto/create-collaborator.dto.js +0 -6
  148. package/dist/dto/create-collaborator.dto.js.map +1 -1
  149. package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
  150. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
  151. package/dist/dto/list-collaborator-invoice.dto.js +8 -0
  152. package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
  153. package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
  154. package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
  155. package/dist/dto/list-collaborator-payment.dto.js +8 -0
  156. package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
  157. package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
  158. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
  159. package/dist/dto/update-collaborator-invoice.dto.js +9 -0
  160. package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
  161. package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
  162. package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
  163. package/dist/dto/update-collaborator-payment.dto.js +9 -0
  164. package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
  165. package/dist/index.d.ts +2 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +2 -0
  168. package/dist/index.js.map +1 -1
  169. package/dist/operations.controller.d.ts +42 -0
  170. package/dist/operations.controller.d.ts.map +1 -1
  171. package/dist/operations.service.d.ts +258 -268
  172. package/dist/operations.service.d.ts.map +1 -1
  173. package/dist/operations.service.js +2381 -1341
  174. package/dist/operations.service.js.map +1 -1
  175. package/dist/operations.service.spec.js +345 -174
  176. package/dist/operations.service.spec.js.map +1 -1
  177. package/hedhog/data/dashboard_component.yaml +66 -0
  178. package/hedhog/data/dashboard_item.yaml +25 -25
  179. package/hedhog/data/menu.yaml +27 -8
  180. package/hedhog/data/route.yaml +133 -0
  181. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +78 -102
  182. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
  183. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
  184. package/hedhog/frontend/app/_components/collaborator-picker.tsx.ejs +158 -0
  185. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +247 -50
  186. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +643 -450
  187. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +992 -431
  188. package/hedhog/frontend/app/_components/project-file-attachments.tsx.ejs +371 -0
  189. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +558 -386
  190. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +383 -157
  191. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +4 -1
  192. package/hedhog/frontend/app/_components/task-form-fields.tsx.ejs +406 -0
  193. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -784
  194. package/hedhog/frontend/app/_components/task-info-display.tsx.ejs +137 -0
  195. package/hedhog/frontend/app/_components/timesheet-entry-create-sheet.tsx.ejs +306 -0
  196. package/hedhog/frontend/app/_lib/api.ts.ejs +155 -0
  197. package/hedhog/frontend/app/_lib/types.ts.ejs +62 -0
  198. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +0 -2
  199. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +61 -0
  200. package/hedhog/frontend/app/approvals/page.tsx.ejs +6 -1
  201. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +6 -1
  202. package/hedhog/frontend/app/collaborators/page.tsx.ejs +59 -8
  203. package/hedhog/frontend/app/contracts/page.tsx.ejs +29 -8
  204. package/hedhog/frontend/app/dashboard/widgets/CapacityDistribution.tsx.ejs +84 -0
  205. package/hedhog/frontend/app/dashboard/widgets/EffortByProject.tsx.ejs +85 -0
  206. package/hedhog/frontend/app/dashboard/widgets/HeadcountByArea.tsx.ejs +101 -0
  207. package/hedhog/frontend/app/dashboard/widgets/ManagedProjectsStatus.tsx.ejs +113 -0
  208. package/hedhog/frontend/app/dashboard/widgets/MyHoursPeriodKpi.tsx.ejs +87 -0
  209. package/hedhog/frontend/app/dashboard/widgets/MyOpenRequestsKpi.tsx.ejs +97 -0
  210. package/hedhog/frontend/app/dashboard/widgets/MyPendingRequestsList.tsx.ejs +99 -0
  211. package/hedhog/frontend/app/dashboard/widgets/MyProjectAllocationsKpi.tsx.ejs +78 -0
  212. package/hedhog/frontend/app/dashboard/widgets/MyQuickActions.tsx.ejs +130 -0
  213. package/hedhog/frontend/app/dashboard/widgets/MyRelevantDeadlines.tsx.ejs +144 -0
  214. package/hedhog/frontend/app/dashboard/widgets/MyTimesheetStatusKpi.tsx.ejs +78 -0
  215. package/hedhog/frontend/app/dashboard/widgets/MyWeeklyJourney.tsx.ejs +99 -0
  216. package/hedhog/frontend/app/dashboard/widgets/PortfolioCostsKpi.tsx.ejs +112 -0
  217. package/hedhog/frontend/app/dashboard/widgets/PortfolioEffortKpi.tsx.ejs +93 -0
  218. package/hedhog/frontend/app/dashboard/widgets/PortfolioProjectsKpi.tsx.ejs +96 -0
  219. package/hedhog/frontend/app/dashboard/widgets/PortfolioRiskKpi.tsx.ejs +115 -0
  220. package/hedhog/frontend/app/dashboard/widgets/ProjectStatusOverview.tsx.ejs +120 -0
  221. package/hedhog/frontend/app/dashboard/widgets/StrategicDeadlines.tsx.ejs +146 -0
  222. package/hedhog/frontend/app/dashboard/widgets/TeamApprovalQueue.tsx.ejs +108 -0
  223. package/hedhog/frontend/app/dashboard/widgets/TeamCapacityKpi.tsx.ejs +97 -0
  224. package/hedhog/frontend/app/dashboard/widgets/TeamHeadcountKpi.tsx.ejs +100 -0
  225. package/hedhog/frontend/app/dashboard/widgets/TeamHoursKpi.tsx.ejs +104 -0
  226. package/hedhog/frontend/app/dashboard/widgets/TeamPendingApprovalsKpi.tsx.ejs +110 -0
  227. package/hedhog/frontend/app/dashboard/widgets/TeamUtilizationOverview.tsx.ejs +115 -0
  228. package/hedhog/frontend/app/dashboard/widgets/TeamWorkloadAlerts.tsx.ejs +117 -0
  229. package/hedhog/frontend/app/dashboard/widgets/index.ts.ejs +26 -0
  230. package/hedhog/frontend/app/departments/page.tsx.ejs +6 -1
  231. package/hedhog/frontend/app/my-projects/page.tsx.ejs +30 -12
  232. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +286 -125
  233. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +58 -52
  234. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +58 -51
  235. package/hedhog/frontend/app/projects/page.tsx.ejs +415 -33
  236. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +6 -1
  237. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
  238. package/hedhog/frontend/app/time-off/page.tsx.ejs +6 -1
  239. package/hedhog/frontend/app/timesheets/page.tsx.ejs +10 -4
  240. package/hedhog/frontend/messages/en.json +332 -46
  241. package/hedhog/frontend/messages/operations/en.json +61 -52
  242. package/hedhog/frontend/messages/operations/pt.json +59 -43
  243. package/hedhog/frontend/messages/pt.json +332 -46
  244. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +17 -0
  245. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +17 -0
  246. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +17 -0
  247. package/hedhog/frontend/widgets/index.ts.ejs +25 -0
  248. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +17 -0
  249. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +17 -0
  250. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +17 -0
  251. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +17 -0
  252. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +17 -0
  253. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +17 -0
  254. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +17 -0
  255. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +17 -0
  256. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +17 -0
  257. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +17 -0
  258. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +17 -0
  259. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +17 -0
  260. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +17 -0
  261. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +17 -0
  262. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +170 -0
  263. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +17 -0
  264. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +17 -0
  265. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +17 -0
  266. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +17 -0
  267. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +17 -0
  268. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +17 -0
  269. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +17 -0
  270. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +17 -0
  271. package/hedhog/table/operations_collaborator.yaml +8 -13
  272. package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
  273. package/hedhog/table/operations_collaborator_payment.yaml +32 -0
  274. package/hedhog/table/operations_project.yaml +1 -1
  275. package/hedhog/table/operations_project_file.yaml +23 -0
  276. package/hedhog/table/operations_task.yaml +76 -69
  277. package/hedhog/table/operations_task_activity.yaml +51 -0
  278. package/package.json +6 -5
  279. package/src/controllers/operations-collaborators.controller.ts +117 -8
  280. package/src/controllers/operations-projects.controller.ts +41 -8
  281. package/src/controllers/operations-tasks.controller.ts +156 -166
  282. package/src/dashboard/README.md +214 -0
  283. package/src/dashboard/components/DashboardLayout.tsx +131 -0
  284. package/src/dashboard/components/widget-registry.ts +255 -0
  285. package/src/dashboard/hooks/useDashboardData.ts +29 -0
  286. package/src/dashboard/types/widgets.types.ts +237 -0
  287. package/src/dashboard/widgets/CapacityDistribution.tsx +56 -0
  288. package/src/dashboard/widgets/EffortByProject.tsx +51 -0
  289. package/src/dashboard/widgets/HeadcountByArea.tsx +57 -0
  290. package/src/dashboard/widgets/ManagedProjectsStatus.tsx +53 -0
  291. package/src/dashboard/widgets/MyHoursPeriodKpi.tsx +87 -0
  292. package/src/dashboard/widgets/MyOpenRequestsKpi.tsx +51 -0
  293. package/src/dashboard/widgets/MyPendingRequestsList.tsx +63 -0
  294. package/src/dashboard/widgets/MyProjectAllocationsKpi.tsx +57 -0
  295. package/src/dashboard/widgets/MyQuickActions.tsx +62 -0
  296. package/src/dashboard/widgets/MyRelevantDeadlines.tsx +84 -0
  297. package/src/dashboard/widgets/MyTimesheetStatusKpi.tsx +65 -0
  298. package/src/dashboard/widgets/MyWeeklyJourney.tsx +57 -0
  299. package/src/dashboard/widgets/PortfolioCostsKpi.tsx +48 -0
  300. package/src/dashboard/widgets/PortfolioEffortKpi.tsx +41 -0
  301. package/src/dashboard/widgets/PortfolioRiskKpi.tsx +50 -0
  302. package/src/dashboard/widgets/ProjectStatusOverview.tsx +52 -0
  303. package/src/dashboard/widgets/StrategicDeadlines.tsx +93 -0
  304. package/src/dashboard/widgets/TeamApprovalQueue.tsx +70 -0
  305. package/src/dashboard/widgets/TeamCapacityKpi.tsx +50 -0
  306. package/src/dashboard/widgets/TeamHoursKpi.tsx +51 -0
  307. package/src/dashboard/widgets/TeamPendingApprovalsKpi.tsx +53 -0
  308. package/src/dashboard/widgets/TeamUtilizationOverview.tsx +62 -0
  309. package/src/dashboard/widgets/TeamWorkloadAlerts.tsx +81 -0
  310. package/src/dashboard/widgets/index.ts +26 -0
  311. package/src/dto/create-collaborator-invoice.dto.ts +39 -0
  312. package/src/dto/create-collaborator-payment.dto.ts +35 -0
  313. package/src/dto/create-collaborator.dto.ts +4 -11
  314. package/src/dto/list-collaborator-invoice.dto.ts +3 -0
  315. package/src/dto/list-collaborator-payment.dto.ts +3 -0
  316. package/src/dto/update-collaborator-invoice.dto.ts +6 -0
  317. package/src/dto/update-collaborator-payment.dto.ts +6 -0
  318. package/src/index.ts +3 -0
  319. package/src/operations.service.spec.ts +988 -764
  320. package/src/operations.service.ts +4689 -2624
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import { EmptyState, Page } from '@/components/entity-list';
4
- import { RichTextEditor } from '@/components/rich-text-editor';
5
4
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
6
5
  import { Button } from '@/components/ui/button';
7
6
  import { Card, CardContent } from '@/components/ui/card';
@@ -14,10 +13,12 @@ import {
14
13
  import {
15
14
  Dialog,
16
15
  DialogContent,
16
+ DialogDescription,
17
17
  DialogFooter,
18
18
  DialogHeader,
19
19
  DialogTitle,
20
20
  } from '@/components/ui/dialog';
21
+ import { EntityPicker } from '@/components/ui/entity-picker';
21
22
  import { Input } from '@/components/ui/input';
22
23
  import { Label } from '@/components/ui/label';
23
24
  import { Progress } from '@/components/ui/progress';
@@ -95,10 +96,12 @@ import {
95
96
  Plus,
96
97
  Rocket,
97
98
  Search,
99
+ Send,
98
100
  SlidersHorizontal,
99
101
  Timer,
100
102
  Trash2,
101
103
  TrendingUp,
104
+ UserPlus,
102
105
  Users,
103
106
  type LucideIcon,
104
107
  } from 'lucide-react';
@@ -125,15 +128,17 @@ import {
125
128
  YAxis,
126
129
  } from 'recharts';
127
130
  import { fetchOperations, mutateOperations } from '../_lib/api';
128
- import { useMentionItems } from '../_lib/hooks/use-mention-items';
129
131
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
130
132
  import {
131
133
  MASKED_VALUE,
132
134
  useValuesVisibility,
133
135
  } from '../_lib/hooks/use-values-visibility';
134
136
  import type {
137
+ OperationsCollaborator,
138
+ OperationsCollaboratorDetails,
135
139
  OperationsProjectDetails,
136
140
  OperationsTaskOption,
141
+ PaginatedResponse,
137
142
  } from '../_lib/types';
138
143
  import {
139
144
  formatCurrency,
@@ -144,17 +149,16 @@ import {
144
149
  formatPercent,
145
150
  getStatusBadgeClass,
146
151
  } from '../_lib/utils/format';
152
+ import { parseNumberInput } from '../_lib/utils/forms';
147
153
  import { OperationsHeader } from './operations-header';
148
154
  import { ProjectCostsSection } from './project-costs-section';
155
+ import { ProjectFileAttachments } from './project-file-attachments';
149
156
  import { ProjectFormScreen } from './project-form-screen';
150
157
  import { SectionCard } from './section-card';
151
158
  import { StatusBadge } from './status-badge';
152
- import {
153
- TaskCommentsSection,
154
- TaskDetailSheet,
155
- type TaskDetailSheetData,
156
- } from './task-detail-sheet';
157
- import { TaskFileAttachments } from './task-file-attachments';
159
+ import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
160
+ import { TaskFormSheet } from './task-form-sheet';
161
+ import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
158
162
 
159
163
  type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
160
164
 
@@ -175,6 +179,8 @@ type BoardTask = {
175
179
  createdAt: string | null;
176
180
  commentCount: number;
177
181
  fileCount: number;
182
+ doingStartedAt: string | null;
183
+ totalDoingMinutes: number;
178
184
  };
179
185
 
180
186
  type ApiBoardTask = Partial<BoardTask> & {
@@ -184,28 +190,6 @@ type ApiBoardTask = Partial<BoardTask> & {
184
190
  priority?: BoardTask['priority'] | null;
185
191
  };
186
192
 
187
- type TaskFormState = {
188
- name: string;
189
- description: string;
190
- priority: 'low' | 'medium' | 'high';
191
- status: BoardColumnId;
192
- assigneeCollaboratorId: string;
193
- dueDate: string;
194
- estimateHours: string;
195
- tags: string;
196
- };
197
-
198
- const EMPTY_TASK_FORM: TaskFormState = {
199
- name: '',
200
- description: '',
201
- priority: 'medium',
202
- status: 'todo',
203
- assigneeCollaboratorId: 'none',
204
- dueDate: '',
205
- estimateHours: '',
206
- tags: '',
207
- };
208
-
209
193
  type BoardColumns = Record<BoardColumnId, BoardTask[]>;
210
194
 
211
195
  type BoardState = {
@@ -213,6 +197,23 @@ type BoardState = {
213
197
  columns: BoardColumns;
214
198
  };
215
199
 
200
+ type TimesheetEntryPrefill = {
201
+ projectId: number;
202
+ projectAssignmentId?: number | null;
203
+ projectLabel: string;
204
+ taskId: number;
205
+ taskLabel: string;
206
+ };
207
+
208
+ function formatAssignmentNumericValue(value: number) {
209
+ if (!Number.isFinite(value)) {
210
+ return '';
211
+ }
212
+
213
+ const roundedValue = Math.round(value * 100) / 100;
214
+ return String(roundedValue);
215
+ }
216
+
216
217
  const KANBAN_COLUMNS: Array<{ id: BoardColumnId; label: string }> = [
217
218
  { id: 'todo', label: 'Backlog' },
218
219
  { id: 'doing', label: 'Em execução' },
@@ -252,6 +253,8 @@ function apiTaskToBoardTask(row: ApiBoardTask): BoardTask {
252
253
  0,
253
254
  fileCount:
254
255
  ((row as BoardTask & Record<string, unknown>).fileCount as number) ?? 0,
256
+ doingStartedAt: row.doingStartedAt ?? null,
257
+ totalDoingMinutes: row.totalDoingMinutes ?? 0,
255
258
  };
256
259
  }
257
260
 
@@ -264,6 +267,23 @@ function splitTasksByColumn(tasks: BoardTask[]): BoardColumns {
264
267
  };
265
268
  }
266
269
 
270
+ function replaceTaskInColumns(
271
+ columns: BoardColumns,
272
+ task: BoardTask
273
+ ): BoardColumns {
274
+ const nextColumns = {
275
+ todo: columns.todo.filter((item) => item.id !== task.id),
276
+ doing: columns.doing.filter((item) => item.id !== task.id),
277
+ review: columns.review.filter((item) => item.id !== task.id),
278
+ done: columns.done.filter((item) => item.id !== task.id),
279
+ };
280
+
281
+ return {
282
+ ...nextColumns,
283
+ [task.status]: [task, ...nextColumns[task.status]],
284
+ };
285
+ }
286
+
267
287
  const boardChartConfig = {
268
288
  allocation: { label: 'Alocacao', color: 'hsl(201 96% 32%)' },
269
289
  loggedHours: { label: 'Horas', color: 'hsl(166 72% 28%)' },
@@ -376,7 +396,7 @@ function getInitials(value?: string | null) {
376
396
  function getPersonAvatarUrl(avatarId?: number | null) {
377
397
  return typeof avatarId === 'number' && avatarId > 0
378
398
  ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
379
- : '/placeholder.png';
399
+ : undefined;
380
400
  }
381
401
 
382
402
  function getUserPhotoUrl(photoId?: number | null) {
@@ -385,26 +405,6 @@ function getUserPhotoUrl(photoId?: number | null) {
385
405
  : null;
386
406
  }
387
407
 
388
- function normalizeDateInputValue(value?: string | null) {
389
- if (!value) {
390
- return '';
391
- }
392
-
393
- const normalizedValue = String(value).trim();
394
- const directMatch = normalizedValue.match(/^\d{4}-\d{2}-\d{2}/);
395
-
396
- if (directMatch?.[0]) {
397
- return directMatch[0];
398
- }
399
-
400
- const parsedDate = new Date(normalizedValue);
401
- if (Number.isNaN(parsedDate.getTime())) {
402
- return '';
403
- }
404
-
405
- return parsedDate.toISOString().slice(0, 10);
406
- }
407
-
408
408
  function clampPercent(value?: number | null) {
409
409
  if (typeof value !== 'number' || Number.isNaN(value)) {
410
410
  return 0;
@@ -1102,9 +1102,9 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1102
1102
  const commonT = useTranslations('operations.Common');
1103
1103
  const formT = useTranslations('operations.ProjectFormPage');
1104
1104
  const contractT = useTranslations('operations.ContractFormPage');
1105
+ const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
1105
1106
  const { request, currentLocaleCode, getSettingValue } = useApp();
1106
1107
  const access = useOperationsAccess();
1107
- const mentionItems = useMentionItems(request);
1108
1108
  const isLimitedView = !access.isDirector && !access.isSupervisor;
1109
1109
  const { valuesVisible, toggleValuesVisible } = useValuesVisibility();
1110
1110
  const router = useRouter();
@@ -1281,11 +1281,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1281
1281
  );
1282
1282
  const [taskFormOpen, setTaskFormOpen] = useState(false);
1283
1283
  const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
1284
- const [taskFormData, setTaskFormData] =
1285
- useState<TaskFormState>(EMPTY_TASK_FORM);
1286
- const [taskFormLoading, setTaskFormLoading] = useState(false);
1284
+ const [createDefaultStatus, setCreateDefaultStatus] =
1285
+ useState<BoardColumnId>('todo');
1287
1286
  const [deletePromptTask, setDeletePromptTask] =
1288
1287
  useState<TaskDetailSheetData | null>(null);
1288
+ const [deleteProjectDialogOpen, setDeleteProjectDialogOpen] = useState(false);
1289
+ const [deleteProjectConfirmText, setDeleteProjectConfirmText] = useState('');
1290
+ const [deletingProject, setDeletingProject] = useState(false);
1289
1291
  const [inlineCreateColumn, setInlineCreateColumn] =
1290
1292
  useState<BoardColumnId | null>(null);
1291
1293
  const [inlineCreateName, setInlineCreateName] = useState('');
@@ -1303,6 +1305,30 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1303
1305
  const [restoringTaskId, setRestoringTaskId] = useState<number | null>(null);
1304
1306
  const [deletingTaskId, setDeletingTaskId] = useState<number | null>(null);
1305
1307
  const [activeDragTask, setActiveDragTask] = useState<BoardTask | null>(null);
1308
+ const [isTimesheetEntrySheetOpen, setIsTimesheetEntrySheetOpen] =
1309
+ useState(false);
1310
+ const [timesheetPrefill, setTimesheetPrefill] =
1311
+ useState<TimesheetEntryPrefill | null>(null);
1312
+
1313
+ // Assignment management state
1314
+ const [assignmentSheetOpen, setAssignmentSheetOpen] = useState(false);
1315
+ const [editingAssignment, setEditingAssignment] = useState<
1316
+ OperationsProjectDetails['assignments'][0] | null
1317
+ >(null);
1318
+ const [assignmentFormData, setAssignmentFormData] = useState({
1319
+ collaboratorId: '',
1320
+ weeklyHours: '',
1321
+ allocationPercent: '',
1322
+ status: 'active',
1323
+ startDate: '',
1324
+ endDate: '',
1325
+ });
1326
+ const [selectedAssignmentCollaborator, setSelectedAssignmentCollaborator] =
1327
+ useState<OperationsCollaborator | null>(null);
1328
+ const [savingAssignment, setSavingAssignment] = useState(false);
1329
+ const [removingAssignmentId, setRemovingAssignmentId] = useState<
1330
+ number | null
1331
+ >(null);
1306
1332
 
1307
1333
  const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
1308
1334
  const archivedTasks = useMemo(
@@ -1320,6 +1346,29 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1320
1346
  return splitTasksByColumn(apiTasks);
1321
1347
  }, [project, boardState, apiTasks]);
1322
1348
 
1349
+ const editingTask = useMemo(() => {
1350
+ if (!editingTaskId) return null;
1351
+ return (
1352
+ Object.values(taskColumns)
1353
+ .flat()
1354
+ .find((task) => task.id === editingTaskId) ??
1355
+ apiTasks.find((task) => task.id === editingTaskId) ??
1356
+ archivedTasks.find((task) => task.id === editingTaskId) ??
1357
+ null
1358
+ );
1359
+ }, [apiTasks, archivedTasks, editingTaskId, taskColumns]);
1360
+
1361
+ const editingTaskAsOption = useMemo(() => {
1362
+ if (!editingTask) return null;
1363
+ return {
1364
+ ...editingTask,
1365
+ label: editingTask.name,
1366
+ projectId,
1367
+ projectName: project?.name ?? '',
1368
+ projectCode: project?.code,
1369
+ };
1370
+ }, [editingTask, projectId, project]);
1371
+
1323
1372
  const filteredTaskColumns: BoardColumns = useMemo(() => {
1324
1373
  const normalizedSearch = boardSearch.trim().toLocaleLowerCase();
1325
1374
  const filterTask = (task: BoardTask) => {
@@ -1373,8 +1422,8 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1373
1422
 
1374
1423
  const openCreateTaskForm = useCallback(
1375
1424
  (defaultStatus: BoardColumnId = 'todo') => {
1425
+ setCreateDefaultStatus(defaultStatus);
1376
1426
  setEditingTaskId(null);
1377
- setTaskFormData({ ...EMPTY_TASK_FORM, status: defaultStatus });
1378
1427
  setTaskFormOpen(true);
1379
1428
  },
1380
1429
  []
@@ -1382,73 +1431,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1382
1431
 
1383
1432
  const openEditTaskForm = useCallback((task: BoardTask) => {
1384
1433
  setEditingTaskId(task.id);
1385
- setTaskFormData({
1386
- name: task.name,
1387
- description: task.description ?? '',
1388
- priority: task.priority,
1389
- status: task.status,
1390
- assigneeCollaboratorId: task.assigneeCollaboratorId
1391
- ? String(task.assigneeCollaboratorId)
1392
- : 'none',
1393
- dueDate: normalizeDateInputValue(task.dueDate),
1394
- estimateHours:
1395
- task.estimateHours != null ? String(task.estimateHours) : '',
1396
- tags: task.tags ?? '',
1397
- });
1398
1434
  setSelectedTask(null);
1399
1435
  setTaskFormOpen(true);
1400
1436
  }, []);
1401
1437
 
1402
- const handleTaskFormSubmit = useCallback(async () => {
1403
- if (!taskFormData.name.trim()) return;
1404
- setTaskFormLoading(true);
1405
- try {
1406
- const payload: Record<string, unknown> = {
1407
- name: taskFormData.name.trim(),
1408
- description: taskFormData.description || null,
1409
- priority: taskFormData.priority,
1410
- status: taskFormData.status,
1411
- assigneeCollaboratorId:
1412
- taskFormData.assigneeCollaboratorId !== 'none'
1413
- ? Number(taskFormData.assigneeCollaboratorId)
1414
- : null,
1415
- dueDate: taskFormData.dueDate || null,
1416
- estimateHours: taskFormData.estimateHours
1417
- ? Number(taskFormData.estimateHours)
1418
- : null,
1419
- tags: taskFormData.tags || null,
1420
- };
1421
- if (editingTaskId) {
1422
- await mutateOperations(
1423
- request,
1424
- `/operations/tasks/${editingTaskId}`,
1425
- 'PATCH',
1426
- payload
1427
- );
1428
- } else {
1429
- await mutateOperations(request, '/operations/tasks', 'POST', {
1430
- projectId,
1431
- ...payload,
1432
- });
1433
- }
1434
- setBoardState(null);
1435
- await refetchTasks();
1436
- await refetchArchivedTasks();
1437
- setTaskFormOpen(false);
1438
- setEditingTaskId(null);
1439
- setTaskFormData(EMPTY_TASK_FORM);
1440
- } finally {
1441
- setTaskFormLoading(false);
1442
- }
1443
- }, [
1444
- taskFormData,
1445
- editingTaskId,
1446
- projectId,
1447
- request,
1448
- refetchTasks,
1449
- refetchArchivedTasks,
1450
- ]);
1451
-
1452
1438
  const handleArchiveTask = useCallback(
1453
1439
  async (taskId: number) => {
1454
1440
  setArchivingTaskId(taskId);
@@ -1548,6 +1534,286 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1548
1534
  [request, refetchTasks, refetchArchivedTasks]
1549
1535
  );
1550
1536
 
1537
+ const handleDeleteProject = useCallback(async () => {
1538
+ setDeletingProject(true);
1539
+ try {
1540
+ await mutateOperations(
1541
+ request,
1542
+ `/operations/projects/${projectId}`,
1543
+ 'DELETE'
1544
+ );
1545
+ router.push('/operations/projects');
1546
+ } catch {
1547
+ // ignore
1548
+ } finally {
1549
+ setDeletingProject(false);
1550
+ setDeleteProjectDialogOpen(false);
1551
+ setDeleteProjectConfirmText('');
1552
+ }
1553
+ }, [request, projectId, router]);
1554
+
1555
+ const openTimesheetEntrySheet = useCallback(
1556
+ (task: TaskDetailSheetData) => {
1557
+ if (!project) {
1558
+ return;
1559
+ }
1560
+
1561
+ const projectLabel = [project.name, project.code]
1562
+ .filter(Boolean)
1563
+ .join(' • ');
1564
+
1565
+ setTimesheetPrefill({
1566
+ projectId,
1567
+ projectAssignmentId: task.projectAssignmentId ?? null,
1568
+ projectLabel: projectLabel || commonT('labels.notAssigned'),
1569
+ taskId: task.id,
1570
+ taskLabel: task.name,
1571
+ });
1572
+ setSelectedTask(null);
1573
+ setIsTimesheetEntrySheetOpen(true);
1574
+ },
1575
+ [commonT, project, projectId]
1576
+ );
1577
+
1578
+ const loadCollaboratorOptions = useCallback(
1579
+ async ({
1580
+ page,
1581
+ pageSize,
1582
+ search,
1583
+ }: {
1584
+ page: number;
1585
+ pageSize: number;
1586
+ search: string;
1587
+ }) => {
1588
+ const params = new URLSearchParams({
1589
+ page: String(page),
1590
+ pageSize: String(pageSize),
1591
+ sortField: 'displayName',
1592
+ sortOrder: 'asc',
1593
+ });
1594
+ if (search.trim()) params.set('search', search.trim());
1595
+ const result = await fetchOperations<
1596
+ PaginatedResponse<OperationsCollaborator>
1597
+ >(request, `/operations/collaborators?${params.toString()}`);
1598
+ const items = result?.data ?? [];
1599
+ const total = result?.total ?? 0;
1600
+ return { items, hasMore: page * pageSize < total };
1601
+ },
1602
+ [request]
1603
+ );
1604
+
1605
+ const loadAssignmentCollaboratorById = useCallback(
1606
+ async (collaboratorId: number) => {
1607
+ const collaborator = await fetchOperations<OperationsCollaboratorDetails>(
1608
+ request,
1609
+ `/operations/collaborators/${collaboratorId}`
1610
+ );
1611
+ setSelectedAssignmentCollaborator(collaborator);
1612
+ return collaborator;
1613
+ },
1614
+ [request]
1615
+ );
1616
+
1617
+ const createAssignmentCollaborator = useCallback(
1618
+ async (values: Record<string, string>) => {
1619
+ const displayName = values.displayName?.trim() ?? '';
1620
+ const weeklyCapacityHours = parseNumberInput(
1621
+ values.weeklyCapacityHours ?? ''
1622
+ );
1623
+
1624
+ if (!displayName) {
1625
+ return null;
1626
+ }
1627
+
1628
+ return mutateOperations<OperationsCollaborator>(
1629
+ request,
1630
+ '/operations/collaborators',
1631
+ 'POST',
1632
+ {
1633
+ displayName,
1634
+ weeklyCapacityHours,
1635
+ status: 'active',
1636
+ autoGenerateContractDraft: false,
1637
+ }
1638
+ );
1639
+ },
1640
+ [request]
1641
+ );
1642
+
1643
+ const syncAssignmentFromWeeklyHours = useCallback(
1644
+ (
1645
+ weeklyHours: string,
1646
+ currentFormData: typeof assignmentFormData,
1647
+ collaborator?: OperationsCollaborator | null
1648
+ ) => {
1649
+ const capacity = collaborator?.weeklyCapacityHours;
1650
+ if (!capacity || capacity <= 0) {
1651
+ return {
1652
+ weeklyHours,
1653
+ allocationPercent: currentFormData.allocationPercent,
1654
+ };
1655
+ }
1656
+
1657
+ const parsedHours = parseNumberInput(weeklyHours);
1658
+ if (parsedHours == null) {
1659
+ return { weeklyHours, allocationPercent: '' };
1660
+ }
1661
+
1662
+ return {
1663
+ weeklyHours,
1664
+ allocationPercent: formatAssignmentNumericValue(
1665
+ (parsedHours / capacity) * 100
1666
+ ),
1667
+ };
1668
+ },
1669
+ []
1670
+ );
1671
+
1672
+ const syncAssignmentFromAllocation = useCallback(
1673
+ (
1674
+ allocationPercent: string,
1675
+ currentFormData: typeof assignmentFormData,
1676
+ collaborator?: OperationsCollaborator | null
1677
+ ) => {
1678
+ const capacity = collaborator?.weeklyCapacityHours;
1679
+ if (!capacity || capacity <= 0) {
1680
+ return {
1681
+ weeklyHours: currentFormData.weeklyHours,
1682
+ allocationPercent,
1683
+ };
1684
+ }
1685
+
1686
+ const parsedPercent = parseNumberInput(allocationPercent);
1687
+ if (parsedPercent == null) {
1688
+ return { weeklyHours: '', allocationPercent };
1689
+ }
1690
+
1691
+ return {
1692
+ weeklyHours: formatAssignmentNumericValue(
1693
+ (parsedPercent / 100) * capacity
1694
+ ),
1695
+ allocationPercent,
1696
+ };
1697
+ },
1698
+ []
1699
+ );
1700
+
1701
+ const openAddAssignment = useCallback(() => {
1702
+ setEditingAssignment(null);
1703
+ setSelectedAssignmentCollaborator(null);
1704
+ setAssignmentFormData({
1705
+ collaboratorId: '',
1706
+ weeklyHours: '',
1707
+ allocationPercent: '',
1708
+ status: 'active',
1709
+ startDate: '',
1710
+ endDate: '',
1711
+ });
1712
+ setAssignmentSheetOpen(true);
1713
+ }, []);
1714
+
1715
+ const openEditAssignment = useCallback(
1716
+ (assignment: OperationsProjectDetails['assignments'][0]) => {
1717
+ setEditingAssignment(assignment);
1718
+ setSelectedAssignmentCollaborator(null);
1719
+ setAssignmentFormData({
1720
+ collaboratorId: String(assignment.collaboratorId),
1721
+ weeklyHours:
1722
+ assignment.weeklyHours != null ? String(assignment.weeklyHours) : '',
1723
+ allocationPercent:
1724
+ assignment.allocationPercent != null
1725
+ ? String(assignment.allocationPercent)
1726
+ : '',
1727
+ status: assignment.status,
1728
+ startDate: assignment.startDate?.slice(0, 10) ?? '',
1729
+ endDate: assignment.endDate?.slice(0, 10) ?? '',
1730
+ });
1731
+ void loadAssignmentCollaboratorById(assignment.collaboratorId);
1732
+ setAssignmentSheetOpen(true);
1733
+ },
1734
+ [loadAssignmentCollaboratorById]
1735
+ );
1736
+
1737
+ const handleSaveAssignment = useCallback(async () => {
1738
+ if (!project) return;
1739
+ const collabId = Number(assignmentFormData.collaboratorId);
1740
+ if (!collabId) return;
1741
+ setSavingAssignment(true);
1742
+ const newEntry = {
1743
+ collaboratorId: collabId,
1744
+ weeklyHours: assignmentFormData.weeklyHours
1745
+ ? Number(assignmentFormData.weeklyHours)
1746
+ : null,
1747
+ allocationPercent: assignmentFormData.allocationPercent
1748
+ ? Number(assignmentFormData.allocationPercent)
1749
+ : null,
1750
+ status: assignmentFormData.status,
1751
+ startDate: assignmentFormData.startDate || null,
1752
+ endDate: assignmentFormData.endDate || null,
1753
+ };
1754
+ const existingMapped = project.assignments.map((a) => ({
1755
+ collaboratorId: a.collaboratorId,
1756
+ weeklyHours: a.weeklyHours ?? null,
1757
+ allocationPercent: a.allocationPercent ?? null,
1758
+ status: a.status,
1759
+ startDate: a.startDate ?? null,
1760
+ endDate: a.endDate ?? null,
1761
+ }));
1762
+ const updatedAssignments = editingAssignment
1763
+ ? existingMapped.map((a) =>
1764
+ a.collaboratorId === editingAssignment.collaboratorId ? newEntry : a
1765
+ )
1766
+ : [...existingMapped, newEntry];
1767
+ try {
1768
+ await mutateOperations(
1769
+ request,
1770
+ `/operations/projects/${projectId}`,
1771
+ 'PATCH',
1772
+ { teamAssignments: updatedAssignments }
1773
+ );
1774
+ await refetch();
1775
+ setAssignmentSheetOpen(false);
1776
+ } catch {
1777
+ // ignore
1778
+ } finally {
1779
+ setSavingAssignment(false);
1780
+ }
1781
+ }, [
1782
+ project,
1783
+ assignmentFormData,
1784
+ editingAssignment,
1785
+ request,
1786
+ projectId,
1787
+ refetch,
1788
+ ]);
1789
+
1790
+ const handleConfirmRemoveAssignment = useCallback(async () => {
1791
+ if (!project || removingAssignmentId === null) return;
1792
+ const updatedAssignments = project.assignments
1793
+ .filter((a) => a.collaboratorId !== removingAssignmentId)
1794
+ .map((a) => ({
1795
+ collaboratorId: a.collaboratorId,
1796
+ weeklyHours: a.weeklyHours ?? null,
1797
+ allocationPercent: a.allocationPercent ?? null,
1798
+ status: a.status,
1799
+ startDate: a.startDate ?? null,
1800
+ endDate: a.endDate ?? null,
1801
+ }));
1802
+ try {
1803
+ await mutateOperations(
1804
+ request,
1805
+ `/operations/projects/${projectId}`,
1806
+ 'PATCH',
1807
+ { teamAssignments: updatedAssignments }
1808
+ );
1809
+ await refetch();
1810
+ } catch {
1811
+ // ignore
1812
+ } finally {
1813
+ setRemovingAssignmentId(null);
1814
+ }
1815
+ }, [project, removingAssignmentId, request, projectId, refetch]);
1816
+
1551
1817
  const allocationChartData = useMemo(() => {
1552
1818
  if (projectStats?.allocationByCollaborator?.length) {
1553
1819
  return projectStats.allocationByCollaborator;
@@ -1621,14 +1887,33 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1621
1887
  },
1622
1888
  });
1623
1889
 
1624
- // Persist to API
1625
- mutateOperations(request, `/operations/tasks/${taskId}`, 'PATCH', {
1626
- status: targetColumn,
1627
- }).catch(() => {
1628
- // Rollback optimistic update on error
1629
- setBoardState(null);
1630
- void refetchTasks();
1631
- });
1890
+ mutateOperations<ApiBoardTask>(
1891
+ request,
1892
+ `/operations/tasks/${taskId}`,
1893
+ 'PATCH',
1894
+ {
1895
+ status: targetColumn,
1896
+ }
1897
+ )
1898
+ .then((updatedTask) => {
1899
+ if (!updatedTask) {
1900
+ return;
1901
+ }
1902
+
1903
+ const updatedBoardTask = apiTaskToBoardTask(updatedTask);
1904
+ setBoardState((current) => ({
1905
+ projectId: current?.projectId ?? project.id,
1906
+ columns: replaceTaskInColumns(
1907
+ current?.columns ?? taskColumns,
1908
+ updatedBoardTask
1909
+ ),
1910
+ }));
1911
+ })
1912
+ .catch(() => {
1913
+ // Rollback optimistic update on error
1914
+ setBoardState(null);
1915
+ void refetchTasks();
1916
+ });
1632
1917
  },
1633
1918
  [findColumnByTask, taskColumns, project, request, refetchTasks]
1634
1919
  );
@@ -2109,6 +2394,17 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2109
2394
  {commonT('actions.edit')}
2110
2395
  </Button>
2111
2396
  ) : null}
2397
+ {access.isDirector && project?.status === 'archived' ? (
2398
+ <Button
2399
+ size="sm"
2400
+ variant="destructive"
2401
+ className="cursor-pointer gap-1.5"
2402
+ onClick={() => setDeleteProjectDialogOpen(true)}
2403
+ >
2404
+ <Trash2 className="size-3.5" />
2405
+ {t('deleteProjectButton')}
2406
+ </Button>
2407
+ ) : null}
2112
2408
  </div>
2113
2409
  </div>
2114
2410
 
@@ -2118,10 +2414,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2118
2414
  <TooltipTrigger asChild>
2119
2415
  <div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
2120
2416
  <Avatar className="size-5 shrink-0 border bg-muted">
2121
- <AvatarImage
2122
- src={getPersonAvatarUrl(project.clientAvatarId)}
2123
- alt={project.clientName || ''}
2124
- />
2417
+ <AvatarImage
2418
+ src={
2419
+ getUserPhotoUrl(project.clientUserPhotoId) ||
2420
+ getPersonAvatarUrl(project.clientAvatarId)
2421
+ }
2422
+ alt={project.clientName || ''}
2423
+ />
2125
2424
  <AvatarFallback className="text-[9px]">
2126
2425
  {getInitials(project.clientName)}
2127
2426
  </AvatarFallback>
@@ -2406,6 +2705,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2406
2705
  <TabsTrigger value="team">{t('tabs.team')}</TabsTrigger>
2407
2706
  <TabsTrigger value="timeline">{t('tabs.timeline')}</TabsTrigger>
2408
2707
  <TabsTrigger value="archive">{t('tabs.archive')}</TabsTrigger>
2708
+ <TabsTrigger value="files">{t('tabs.files')}</TabsTrigger>
2409
2709
  <TabsTrigger value="costs">{t('tabs.costs')}</TabsTrigger>
2410
2710
  </TabsList>
2411
2711
 
@@ -2439,7 +2739,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2439
2739
  <div className="flex items-center gap-2">
2440
2740
  <Avatar className="h-8 w-8 border border-border/60 bg-muted">
2441
2741
  <AvatarImage
2442
- src={getPersonAvatarUrl(project.clientAvatarId)}
2742
+ src={
2743
+ getUserPhotoUrl(project.clientUserPhotoId) ||
2744
+ getPersonAvatarUrl(project.clientAvatarId)
2745
+ }
2443
2746
  alt={project.clientName || commonT('labels.client')}
2444
2747
  />
2445
2748
  <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
@@ -2581,13 +2884,32 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2581
2884
  <div className="font-medium">
2582
2885
  {project.relatedContract.name}
2583
2886
  </div>
2584
- <div className="text-sm text-muted-foreground">
2585
- {[
2586
- project.relatedContract.code,
2587
- project.relatedContract.clientName,
2588
- ]
2589
- .filter(Boolean)
2590
- .join(' • ') || commonT('labels.notAvailable')}
2887
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
2888
+ <span className="truncate">
2889
+ {project.relatedContract.code || '—'}
2890
+ </span>
2891
+ {project.relatedContract.clientName ? (
2892
+ <>
2893
+ <span>•</span>
2894
+ <Avatar className="h-4 w-4 shrink-0">
2895
+ <AvatarImage
2896
+ src={
2897
+ getUserPhotoUrl(project.clientUserPhotoId) ||
2898
+ getPersonAvatarUrl(project.clientAvatarId)
2899
+ }
2900
+ alt={project.relatedContract.clientName}
2901
+ />
2902
+ <AvatarFallback className="text-[8px] font-medium">
2903
+ {getInitials(
2904
+ project.relatedContract.clientName
2905
+ )}
2906
+ </AvatarFallback>
2907
+ </Avatar>
2908
+ <span className="truncate">
2909
+ {project.relatedContract.clientName}
2910
+ </span>
2911
+ </>
2912
+ ) : null}
2591
2913
  </div>
2592
2914
  </div>
2593
2915
  <div className="flex items-center gap-3">
@@ -3059,7 +3381,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
3059
3381
  {(isOver) => (
3060
3382
  <div
3061
3383
  className={[
3062
- 'flex min-h-128 flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
3384
+ 'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
3063
3385
  getColumnClassName(column.id),
3064
3386
  isOver
3065
3387
  ? 'border-primary shadow-lg ring-2 ring-primary/15'
@@ -3102,7 +3424,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
3102
3424
  </div>
3103
3425
  </div>
3104
3426
 
3105
- <div className="flex flex-1 flex-col gap-2">
3427
+ <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
3106
3428
  <AnimatePresence initial={false}>
3107
3429
  {filteredTaskColumns[column.id].map((task) => {
3108
3430
  const tags = getTaskTags(task);
@@ -3830,6 +4152,19 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
3830
4152
  'rounded-2xl border bg-card p-4 shadow-sm',
3831
4153
  isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
3832
4154
  ].join(' ')}
4155
+ actions={
4156
+ !isLimitedView ? (
4157
+ <Button
4158
+ size="sm"
4159
+ variant="outline"
4160
+ className="cursor-pointer"
4161
+ onClick={openAddAssignment}
4162
+ >
4163
+ <UserPlus className="mr-1.5 size-4" />
4164
+ {t('teamPanel.addCollaborator')}
4165
+ </Button>
4166
+ ) : undefined
4167
+ }
3833
4168
  >
3834
4169
  {project.assignments.length > 0 ? (
3835
4170
  <div className="space-y-4">
@@ -3937,6 +4272,34 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
3937
4272
  <ToneIcon className="size-3" />
3938
4273
  {t(`teamPanel.status.${tone.labelKey}`)}
3939
4274
  </span>
4275
+ {!isLimitedView ? (
4276
+ <div className="flex items-center gap-1">
4277
+ <Button
4278
+ type="button"
4279
+ variant="ghost"
4280
+ size="icon"
4281
+ className="size-7 cursor-pointer"
4282
+ onClick={() =>
4283
+ openEditAssignment(assignment)
4284
+ }
4285
+ >
4286
+ <Pencil className="size-3.5" />
4287
+ </Button>
4288
+ <Button
4289
+ type="button"
4290
+ variant="ghost"
4291
+ size="icon"
4292
+ className="size-7 cursor-pointer text-destructive hover:text-destructive"
4293
+ onClick={() =>
4294
+ setRemovingAssignmentId(
4295
+ assignment.collaboratorId
4296
+ )
4297
+ }
4298
+ >
4299
+ <Trash2 className="size-3.5" />
4300
+ </Button>
4301
+ </div>
4302
+ ) : null}
3940
4303
  </div>
3941
4304
  </div>
3942
4305
  </div>
@@ -4152,6 +4515,16 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4152
4515
  />
4153
4516
  </SectionCard>
4154
4517
  </TabsContent>
4518
+
4519
+ <TabsContent value="files">
4520
+ <SectionCard
4521
+ title={t('sections.files')}
4522
+ description={t('sections.filesDescription')}
4523
+ className="rounded-2xl border bg-card p-4 shadow-sm"
4524
+ >
4525
+ <ProjectFileAttachments projectId={projectId} />
4526
+ </SectionCard>
4527
+ </TabsContent>
4155
4528
  </Tabs>
4156
4529
 
4157
4530
  <TaskDetailSheet
@@ -4168,13 +4541,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4168
4541
  }
4169
4542
  footer={
4170
4543
  selectedTask && !isLimitedView ? (
4171
- <div className="grid grid-cols-2 gap-3">
4544
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
4172
4545
  {archivedTasks.some((task) => task.id === selectedTask.id) ? (
4173
4546
  <>
4174
4547
  <Button
4175
4548
  variant="outline"
4176
4549
  size="sm"
4177
- className="h-10 gap-2"
4550
+ className="h-10 gap-2 sm:col-span-2"
4178
4551
  disabled={restoringTaskId === selectedTask.id}
4179
4552
  onClick={() => void handleRestoreTask(selectedTask.id)}
4180
4553
  >
@@ -4196,26 +4569,408 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4196
4569
  </Button>
4197
4570
  </>
4198
4571
  ) : (
4199
- <Button
4200
- variant="outline"
4201
- size="sm"
4202
- className="col-span-2 h-10 gap-2"
4203
- disabled={archivingTaskId === selectedTask.id}
4204
- onClick={() => void handleArchiveTask(selectedTask.id)}
4205
- >
4206
- {archivingTaskId === selectedTask.id ? (
4207
- <Loader2 className="size-3.5 animate-spin" />
4208
- ) : (
4209
- <Archive className="size-3.5" />
4210
- )}
4211
- {commonT('actions.archive')}
4212
- </Button>
4572
+ <>
4573
+ <Button
4574
+ variant="outline"
4575
+ size="sm"
4576
+ className="h-10 gap-2"
4577
+ onClick={() => openTimesheetEntrySheet(selectedTask)}
4578
+ >
4579
+ <Send className="size-3.5" />
4580
+ {commonT('actions.logHours')}
4581
+ </Button>
4582
+ <Button
4583
+ variant="outline"
4584
+ size="sm"
4585
+ className="h-10 gap-2 sm:col-span-2"
4586
+ disabled={archivingTaskId === selectedTask.id}
4587
+ onClick={() => void handleArchiveTask(selectedTask.id)}
4588
+ >
4589
+ {archivingTaskId === selectedTask.id ? (
4590
+ <Loader2 className="size-3.5 animate-spin" />
4591
+ ) : (
4592
+ <Archive className="size-3.5" />
4593
+ )}
4594
+ {commonT('actions.archive')}
4595
+ </Button>
4596
+ </>
4213
4597
  )}
4214
4598
  </div>
4215
4599
  ) : null
4216
4600
  }
4217
4601
  />
4218
4602
 
4603
+ {/* Assignment Add/Edit Sheet */}
4604
+ <Sheet
4605
+ open={assignmentSheetOpen}
4606
+ onOpenChange={(open) => {
4607
+ if (!open) {
4608
+ setAssignmentSheetOpen(false);
4609
+ setSelectedAssignmentCollaborator(null);
4610
+ }
4611
+ }}
4612
+ >
4613
+ <SheetContent className="w-full overflow-y-auto sm:max-w-[min(92vw,28rem)]">
4614
+ <SheetHeader>
4615
+ <SheetTitle>
4616
+ {editingAssignment
4617
+ ? t('teamPanel.editTitle')
4618
+ : t('teamPanel.addTitle')}
4619
+ </SheetTitle>
4620
+ <SheetDescription>
4621
+ {t('teamPanel.formDescription')}
4622
+ </SheetDescription>
4623
+ </SheetHeader>
4624
+ <div className="mt-6 space-y-4 px-1 sm:px-4">
4625
+ {!editingAssignment ? (
4626
+ <div className="space-y-2">
4627
+ <Label className="text-sm font-medium">
4628
+ {commonT('labels.collaborator')}
4629
+ </Label>
4630
+ <EntityPicker<OperationsCollaborator>
4631
+ value={
4632
+ assignmentFormData.collaboratorId
4633
+ ? Number(assignmentFormData.collaboratorId)
4634
+ : null
4635
+ }
4636
+ onChange={(val, option) => {
4637
+ const collaborator = option ?? null;
4638
+ setSelectedAssignmentCollaborator(collaborator);
4639
+ setAssignmentFormData((prev) => {
4640
+ const collaboratorId = val ? String(val) : '';
4641
+ if (!collaboratorId) {
4642
+ return {
4643
+ ...prev,
4644
+ collaboratorId: '',
4645
+ weeklyHours: '',
4646
+ allocationPercent: '',
4647
+ };
4648
+ }
4649
+
4650
+ const capacity = collaborator?.weeklyCapacityHours;
4651
+ if (!capacity || capacity <= 0) {
4652
+ return {
4653
+ ...prev,
4654
+ collaboratorId,
4655
+ };
4656
+ }
4657
+
4658
+ const parsedHours = parseNumberInput(prev.weeklyHours);
4659
+ const parsedPercent = parseNumberInput(
4660
+ prev.allocationPercent
4661
+ );
4662
+
4663
+ if (parsedHours != null) {
4664
+ return {
4665
+ ...prev,
4666
+ collaboratorId,
4667
+ ...syncAssignmentFromWeeklyHours(
4668
+ prev.weeklyHours,
4669
+ prev,
4670
+ collaborator
4671
+ ),
4672
+ };
4673
+ }
4674
+
4675
+ if (parsedPercent != null) {
4676
+ return {
4677
+ ...prev,
4678
+ collaboratorId,
4679
+ ...syncAssignmentFromAllocation(
4680
+ prev.allocationPercent,
4681
+ prev,
4682
+ collaborator
4683
+ ),
4684
+ };
4685
+ }
4686
+
4687
+ return {
4688
+ ...prev,
4689
+ collaboratorId,
4690
+ weeklyHours: formatAssignmentNumericValue(capacity),
4691
+ allocationPercent: '100',
4692
+ };
4693
+ });
4694
+ }}
4695
+ placeholder={commonT('labels.collaborator')}
4696
+ searchPlaceholder={commonT('labels.collaborator')}
4697
+ loadOptions={loadCollaboratorOptions}
4698
+ getOptionValue={(opt) => opt.id}
4699
+ getOptionLabel={(opt) => opt.displayName}
4700
+ renderOption={({ option }) => (
4701
+ <div className="flex min-w-0 items-center gap-2.5">
4702
+ <Avatar className="size-6 shrink-0">
4703
+ <AvatarImage
4704
+ src={
4705
+ getUserPhotoUrl(option.userPhotoId) ||
4706
+ getPersonAvatarUrl(option.personAvatarId)
4707
+ }
4708
+ alt={option.displayName}
4709
+ />
4710
+ <AvatarFallback className="text-[9px]">
4711
+ {getInitials(option.displayName)}
4712
+ </AvatarFallback>
4713
+ </Avatar>
4714
+ <div className="min-w-0">
4715
+ <div className="truncate text-sm">
4716
+ {option.displayName}
4717
+ </div>
4718
+ {option.department ? (
4719
+ <div className="truncate text-xs text-muted-foreground">
4720
+ {option.department}
4721
+ </div>
4722
+ ) : null}
4723
+ </div>
4724
+ </div>
4725
+ )}
4726
+ valueType="number"
4727
+ clearable
4728
+ allowEmptySelection
4729
+ showCreateButton
4730
+ entityLabel={commonT('labels.collaborator').toLowerCase()}
4731
+ createActionLabel={`${commonT('actions.create')} ${commonT(
4732
+ 'labels.collaborator'
4733
+ ).toLowerCase()}`}
4734
+ createTitle={`${commonT('actions.create')} ${commonT(
4735
+ 'labels.collaborator'
4736
+ ).toLowerCase()}`}
4737
+ createDescription={t('teamPanel.formDescription')}
4738
+ createFields={[
4739
+ {
4740
+ name: 'displayName',
4741
+ label: collaboratorFormT('fields.displayName'),
4742
+ placeholder: collaboratorFormT('fields.displayName'),
4743
+ required: true,
4744
+ },
4745
+ {
4746
+ name: 'weeklyCapacityHours',
4747
+ label: collaboratorFormT('fields.weeklyCapacityHours'),
4748
+ placeholder: '40',
4749
+ type: 'number',
4750
+ },
4751
+ ]}
4752
+ mapSearchToCreateValues={(search) => ({
4753
+ displayName: search,
4754
+ weeklyCapacityHours: '40',
4755
+ })}
4756
+ onCreate={createAssignmentCollaborator}
4757
+ />
4758
+ </div>
4759
+ ) : (
4760
+ <div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-3">
4761
+ <Avatar className="size-10 border bg-muted">
4762
+ <AvatarImage
4763
+ src={
4764
+ getUserPhotoUrl(editingAssignment.userPhotoId) ||
4765
+ getPersonAvatarUrl(editingAssignment.personAvatarId)
4766
+ }
4767
+ alt={editingAssignment.collaboratorName}
4768
+ />
4769
+ <AvatarFallback className="text-xs">
4770
+ {getInitials(editingAssignment.collaboratorName)}
4771
+ </AvatarFallback>
4772
+ </Avatar>
4773
+ <div className="min-w-0">
4774
+ <div className="truncate text-sm font-semibold">
4775
+ {editingAssignment.collaboratorName}
4776
+ </div>
4777
+ {editingAssignment.roleLabel ? (
4778
+ <div className="truncate text-xs text-muted-foreground">
4779
+ {editingAssignment.roleLabel}
4780
+ </div>
4781
+ ) : null}
4782
+ </div>
4783
+ </div>
4784
+ )}
4785
+ <div className="space-y-2">
4786
+ <Label className="text-sm font-medium">
4787
+ {commonT('labels.allocationPercent')}
4788
+ </Label>
4789
+ <Input
4790
+ type="number"
4791
+ min="0"
4792
+ max="200"
4793
+ value={assignmentFormData.allocationPercent}
4794
+ onChange={(e) =>
4795
+ setAssignmentFormData((prev) => ({
4796
+ ...prev,
4797
+ ...syncAssignmentFromAllocation(
4798
+ e.target.value,
4799
+ prev,
4800
+ selectedAssignmentCollaborator
4801
+ ),
4802
+ }))
4803
+ }
4804
+ placeholder="100"
4805
+ />
4806
+ </div>
4807
+ <div className="space-y-2">
4808
+ <Label className="text-sm font-medium">
4809
+ {commonT('labels.weeklyCapacity')}
4810
+ </Label>
4811
+ <Input
4812
+ type="number"
4813
+ min="0"
4814
+ value={assignmentFormData.weeklyHours}
4815
+ onChange={(e) =>
4816
+ setAssignmentFormData((prev) => ({
4817
+ ...prev,
4818
+ ...syncAssignmentFromWeeklyHours(
4819
+ e.target.value,
4820
+ prev,
4821
+ selectedAssignmentCollaborator
4822
+ ),
4823
+ }))
4824
+ }
4825
+ placeholder="40"
4826
+ />
4827
+ </div>
4828
+ <div className="space-y-2">
4829
+ <Label className="text-sm font-medium">
4830
+ {commonT('labels.status')}
4831
+ </Label>
4832
+ <Select
4833
+ value={assignmentFormData.status}
4834
+ onValueChange={(val) =>
4835
+ setAssignmentFormData((prev) => ({ ...prev, status: val }))
4836
+ }
4837
+ >
4838
+ <SelectTrigger className="cursor-pointer">
4839
+ <SelectValue />
4840
+ </SelectTrigger>
4841
+ <SelectContent>
4842
+ <SelectItem value="active">
4843
+ {formatEnumLabel('active')}
4844
+ </SelectItem>
4845
+ <SelectItem value="inactive">
4846
+ {formatEnumLabel('inactive')}
4847
+ </SelectItem>
4848
+ <SelectItem value="completed">
4849
+ {formatEnumLabel('completed')}
4850
+ </SelectItem>
4851
+ </SelectContent>
4852
+ </Select>
4853
+ </div>
4854
+ <div className="grid grid-cols-2 gap-3">
4855
+ <div className="space-y-2">
4856
+ <Label className="text-sm font-medium">
4857
+ {commonT('labels.startDate')}
4858
+ </Label>
4859
+ <Input
4860
+ type="date"
4861
+ value={assignmentFormData.startDate}
4862
+ onChange={(e) =>
4863
+ setAssignmentFormData((prev) => ({
4864
+ ...prev,
4865
+ startDate: e.target.value,
4866
+ }))
4867
+ }
4868
+ />
4869
+ </div>
4870
+ <div className="space-y-2">
4871
+ <Label className="text-sm font-medium">
4872
+ {commonT('labels.endDate')}
4873
+ </Label>
4874
+ <Input
4875
+ type="date"
4876
+ value={assignmentFormData.endDate}
4877
+ onChange={(e) =>
4878
+ setAssignmentFormData((prev) => ({
4879
+ ...prev,
4880
+ endDate: e.target.value,
4881
+ }))
4882
+ }
4883
+ />
4884
+ </div>
4885
+ </div>
4886
+ <div className="flex gap-2 pt-2">
4887
+ <Button
4888
+ variant="outline"
4889
+ className="flex-1 cursor-pointer"
4890
+ onClick={() => setAssignmentSheetOpen(false)}
4891
+ disabled={savingAssignment}
4892
+ >
4893
+ {commonT('actions.cancel')}
4894
+ </Button>
4895
+ <Button
4896
+ className="flex-1 cursor-pointer"
4897
+ disabled={
4898
+ savingAssignment ||
4899
+ (!editingAssignment && !assignmentFormData.collaboratorId)
4900
+ }
4901
+ onClick={() => void handleSaveAssignment()}
4902
+ >
4903
+ {savingAssignment ? (
4904
+ <Loader2 className="mr-2 size-4 animate-spin" />
4905
+ ) : null}
4906
+ {commonT('actions.save')}
4907
+ </Button>
4908
+ </div>
4909
+ </div>
4910
+ </SheetContent>
4911
+ </Sheet>
4912
+
4913
+ {/* Remove Assignment Confirm Dialog */}
4914
+ <Dialog
4915
+ open={removingAssignmentId !== null}
4916
+ onOpenChange={(open) => {
4917
+ if (!open) setRemovingAssignmentId(null);
4918
+ }}
4919
+ >
4920
+ <DialogContent className="sm:max-w-sm">
4921
+ <DialogHeader>
4922
+ <DialogTitle>{t('teamPanel.removeTitle')}</DialogTitle>
4923
+ <DialogDescription>
4924
+ {t('teamPanel.removeDescription')}
4925
+ </DialogDescription>
4926
+ </DialogHeader>
4927
+ <DialogFooter className="mt-4">
4928
+ <Button
4929
+ variant="outline"
4930
+ onClick={() => setRemovingAssignmentId(null)}
4931
+ >
4932
+ {commonT('actions.cancel')}
4933
+ </Button>
4934
+ <Button
4935
+ variant="destructive"
4936
+ onClick={() => void handleConfirmRemoveAssignment()}
4937
+ >
4938
+ {commonT('actions.delete')}
4939
+ </Button>
4940
+ </DialogFooter>
4941
+ </DialogContent>
4942
+ </Dialog>
4943
+
4944
+ <TimesheetEntryCreateSheet
4945
+ open={isTimesheetEntrySheetOpen}
4946
+ onOpenChange={(open) => {
4947
+ setIsTimesheetEntrySheetOpen(open);
4948
+ if (!open) {
4949
+ setTimesheetPrefill(null);
4950
+ }
4951
+ }}
4952
+ project={
4953
+ timesheetPrefill
4954
+ ? {
4955
+ id: timesheetPrefill.projectId,
4956
+ label: timesheetPrefill.projectLabel,
4957
+ projectAssignmentId: timesheetPrefill.projectAssignmentId,
4958
+ }
4959
+ : null
4960
+ }
4961
+ task={
4962
+ timesheetPrefill
4963
+ ? {
4964
+ id: timesheetPrefill.taskId,
4965
+ label: timesheetPrefill.taskLabel,
4966
+ }
4967
+ : null
4968
+ }
4969
+ onCreated={() => {
4970
+ void refetchTasks();
4971
+ }}
4972
+ />
4973
+
4219
4974
  {!isLimitedView ? (
4220
4975
  <Sheet
4221
4976
  open={isEditSheetOpen}
@@ -4244,284 +4999,39 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4244
4999
  ) : null}
4245
5000
 
4246
5001
  {!isLimitedView ? (
4247
- <Sheet
5002
+ <TaskFormSheet
4248
5003
  open={taskFormOpen}
4249
5004
  onOpenChange={(open) => {
4250
5005
  if (!open) {
4251
5006
  setTaskFormOpen(false);
4252
5007
  setEditingTaskId(null);
4253
- setTaskFormData(EMPTY_TASK_FORM);
4254
5008
  }
4255
5009
  }}
4256
- >
4257
- <SheetContent className="flex w-full flex-col overflow-hidden sm:max-w-xl">
4258
- <SheetHeader className="shrink-0">
4259
- <SheetTitle>
4260
- {editingTaskId
4261
- ? t('taskForm.titleEdit')
4262
- : t('taskForm.titleNew')}
4263
- </SheetTitle>
4264
- </SheetHeader>
4265
-
4266
- <Tabs defaultValue="info" className="flex min-h-0 flex-1 flex-col">
4267
- <TabsList className="mx-4 grid w-[calc(100%-2rem)] shrink-0 grid-cols-2">
4268
- <TabsTrigger value="info">{t('taskForm.tabInfo')}</TabsTrigger>
4269
- {editingTaskId ? (
4270
- <TabsTrigger value="comments">
4271
- {t('taskForm.tabComments')}
4272
- </TabsTrigger>
4273
- ) : null}
4274
- </TabsList>
4275
- <TabsContent
4276
- value="info"
4277
- className="flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
4278
- >
4279
- <div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
4280
- <div className="space-y-1.5">
4281
- <Label htmlFor="task-name">
4282
- {t('taskForm.nameLabel')} *
4283
- </Label>
4284
- <Input
4285
- id="task-name"
4286
- placeholder={t('taskForm.namePlaceholder')}
4287
- value={taskFormData.name}
4288
- onChange={(e) =>
4289
- setTaskFormData((prev) => ({
4290
- ...prev,
4291
- name: e.target.value,
4292
- }))
4293
- }
4294
- />
4295
- </div>
4296
-
4297
- <div className="space-y-1.5">
4298
- <Label htmlFor="task-description">
4299
- {t('taskForm.descriptionLabel')}
4300
- </Label>
4301
- <RichTextEditor
4302
- value={taskFormData.description}
4303
- onChange={(val) =>
4304
- setTaskFormData((prev) => ({
4305
- ...prev,
4306
- description: val,
4307
- }))
4308
- }
4309
- mentions={mentionItems}
4310
- />
4311
- </div>
4312
-
4313
- <div className="grid grid-cols-2 gap-3">
4314
- <div className="space-y-1.5">
4315
- <Label>{t('taskForm.priorityLabel')}</Label>
4316
- <Select
4317
- value={taskFormData.priority}
4318
- onValueChange={(v) =>
4319
- setTaskFormData((prev) => ({
4320
- ...prev,
4321
- priority: v as TaskFormState['priority'],
4322
- }))
4323
- }
4324
- >
4325
- <SelectTrigger className="w-full">
4326
- <SelectValue />
4327
- </SelectTrigger>
4328
- <SelectContent>
4329
- <SelectItem value="low">
4330
- {getTaskPriorityLabel('low')}
4331
- </SelectItem>
4332
- <SelectItem value="medium">
4333
- {getTaskPriorityLabel('medium')}
4334
- </SelectItem>
4335
- <SelectItem value="high">
4336
- {getTaskPriorityLabel('high')}
4337
- </SelectItem>
4338
- </SelectContent>
4339
- </Select>
4340
- </div>
4341
-
4342
- <div className="space-y-1.5">
4343
- <Label>{t('taskForm.columnLabel')}</Label>
4344
- <Select
4345
- value={taskFormData.status}
4346
- onValueChange={(v) =>
4347
- setTaskFormData((prev) => ({
4348
- ...prev,
4349
- status: v as BoardColumnId,
4350
- }))
4351
- }
4352
- >
4353
- <SelectTrigger className="w-full">
4354
- <SelectValue />
4355
- </SelectTrigger>
4356
- <SelectContent>
4357
- {KANBAN_COLUMNS.map((col) => (
4358
- <SelectItem key={col.id} value={col.id}>
4359
- {col.label}
4360
- </SelectItem>
4361
- ))}
4362
- </SelectContent>
4363
- </Select>
4364
- </div>
4365
- </div>
4366
-
4367
- <div className="space-y-1.5">
4368
- <Label>Responsável</Label>
4369
- <Select
4370
- value={taskFormData.assigneeCollaboratorId}
4371
- onValueChange={(value) =>
4372
- setTaskFormData((prev) => ({
4373
- ...prev,
4374
- assigneeCollaboratorId: value,
4375
- }))
4376
- }
4377
- >
4378
- <SelectTrigger className="w-full">
4379
- <SelectValue
4380
- placeholder={commonT('labels.notAssigned')}
4381
- />
4382
- </SelectTrigger>
4383
- <SelectContent>
4384
- <SelectItem value="none">
4385
- {commonT('labels.notAssigned')}
4386
- </SelectItem>
4387
- {taskAssigneeOptions.map((option) => (
4388
- <SelectItem key={option.id} value={option.id}>
4389
- {option.label}
4390
- </SelectItem>
4391
- ))}
4392
- </SelectContent>
4393
- </Select>
4394
- </div>
4395
-
4396
- <div className="grid grid-cols-2 gap-3">
4397
- <div className="space-y-1.5">
4398
- <Label htmlFor="task-due-date">
4399
- {t('taskForm.deadlineLabel')}
4400
- </Label>
4401
- <Input
4402
- id="task-due-date"
4403
- type="date"
4404
- value={taskFormData.dueDate}
4405
- onChange={(e) =>
4406
- setTaskFormData((prev) => ({
4407
- ...prev,
4408
- dueDate: e.target.value,
4409
- }))
4410
- }
4411
- />
4412
- </div>
4413
-
4414
- <div className="space-y-1.5">
4415
- <Label htmlFor="task-estimate">
4416
- {t('taskForm.estimateLabel')}
4417
- </Label>
4418
- <Input
4419
- id="task-estimate"
4420
- type="number"
4421
- min="0"
4422
- step="0.5"
4423
- placeholder="0"
4424
- value={taskFormData.estimateHours}
4425
- onChange={(e) =>
4426
- setTaskFormData((prev) => ({
4427
- ...prev,
4428
- estimateHours: e.target.value,
4429
- }))
4430
- }
4431
- />
4432
- </div>
4433
- </div>
4434
-
4435
- <div className="space-y-1.5">
4436
- <Label htmlFor="task-tags">{t('taskForm.tagsLabel')}</Label>
4437
- <Input
4438
- id="task-tags"
4439
- placeholder={t('taskForm.tagsPlaceholder')}
4440
- value={taskFormData.tags}
4441
- onChange={(e) =>
4442
- setTaskFormData((prev) => ({
4443
- ...prev,
4444
- tags: e.target.value,
4445
- }))
4446
- }
4447
- />
4448
- </div>
4449
-
4450
- {editingTaskId ? (
4451
- <div className="space-y-1.5">
4452
- <Label className="flex items-center gap-1.5">
4453
- <Paperclip className="size-3.5" />
4454
- {t('taskForm.attachmentsLabel')}
4455
- </Label>
4456
- <TaskFileAttachments taskId={editingTaskId} />
4457
- </div>
4458
- ) : null}
4459
- </div>
4460
-
4461
- <div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
4462
- <div className="flex gap-2">
4463
- {editingTaskId ? (
4464
- <Button
4465
- type="button"
4466
- variant="outline"
4467
- disabled={
4468
- taskFormLoading || archivingTaskId === editingTaskId
4469
- }
4470
- onClick={() => {
4471
- if (!editingTaskId) return;
4472
- const id = editingTaskId;
4473
- setTaskFormOpen(false);
4474
- setEditingTaskId(null);
4475
- setTaskFormData(EMPTY_TASK_FORM);
4476
- void handleArchiveTask(id);
4477
- }}
4478
- >
4479
- {archivingTaskId === editingTaskId ? (
4480
- <Loader2 className="mr-2 size-4 animate-spin" />
4481
- ) : (
4482
- <Archive className="mr-2 size-4" />
4483
- )}
4484
- {commonT('actions.archive')}
4485
- </Button>
4486
- ) : null}
4487
- </div>
4488
- <div className="flex gap-2">
4489
- <Button
4490
- variant="outline"
4491
- onClick={() => {
4492
- setTaskFormOpen(false);
4493
- setEditingTaskId(null);
4494
- setTaskFormData(EMPTY_TASK_FORM);
4495
- }}
4496
- disabled={taskFormLoading}
4497
- >
4498
- {commonT('actions.cancel')}
4499
- </Button>
4500
- <Button
4501
- onClick={() => void handleTaskFormSubmit()}
4502
- disabled={taskFormLoading || !taskFormData.name.trim()}
4503
- >
4504
- {taskFormLoading
4505
- ? t('taskForm.saving')
4506
- : editingTaskId
4507
- ? commonT('actions.save')
4508
- : commonT('actions.create')}
4509
- </Button>
4510
- </div>
4511
- </div>
4512
- </TabsContent>
4513
-
4514
- {editingTaskId ? (
4515
- <TabsContent
4516
- value="comments"
4517
- className="min-h-0 flex-1 overflow-y-auto px-4 py-2 data-[state=inactive]:hidden"
4518
- >
4519
- <TaskCommentsSection taskId={editingTaskId} />
4520
- </TabsContent>
4521
- ) : null}
4522
- </Tabs>
4523
- </SheetContent>
4524
- </Sheet>
5010
+ request={request}
5011
+ editingTask={editingTaskAsOption}
5012
+ showProject={false}
5013
+ defaultProjectId={projectId}
5014
+ defaultStatus={createDefaultStatus}
5015
+ assigneeOptions={taskAssigneeOptions}
5016
+ onArchive={handleArchiveTask}
5017
+ onLogHours={
5018
+ editingTask
5019
+ ? () =>
5020
+ openTimesheetEntrySheet(
5021
+ editingTask as unknown as TaskDetailSheetData
5022
+ )
5023
+ : undefined
5024
+ }
5025
+ onCountChanged={() => {
5026
+ setBoardState(null);
5027
+ void refetchTasks();
5028
+ }}
5029
+ onSaved={() => {
5030
+ setBoardState(null);
5031
+ void refetchTasks();
5032
+ void refetchArchivedTasks();
5033
+ }}
5034
+ />
4525
5035
  ) : null}
4526
5036
 
4527
5037
  {!isLimitedView ? (
@@ -4535,10 +5045,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4535
5045
  <DialogContent className="sm:max-w-sm">
4536
5046
  <DialogHeader>
4537
5047
  <DialogTitle>{t('dialogs.deleteTitle')}</DialogTitle>
5048
+ <DialogDescription>
5049
+ {t('dialogs.deleteDescription')}
5050
+ </DialogDescription>
4538
5051
  </DialogHeader>
4539
- <p className="text-sm text-muted-foreground">
4540
- {t('dialogs.deleteDescription')}
4541
- </p>
4542
5052
  <DialogFooter className="mt-4">
4543
5053
  <Button
4544
5054
  variant="outline"
@@ -4561,6 +5071,57 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4561
5071
  </DialogFooter>
4562
5072
  </DialogContent>
4563
5073
  </Dialog>
5074
+
5075
+ <Dialog
5076
+ open={deleteProjectDialogOpen}
5077
+ onOpenChange={(open) => {
5078
+ if (!open) {
5079
+ setDeleteProjectDialogOpen(false);
5080
+ setDeleteProjectConfirmText('');
5081
+ }
5082
+ }}
5083
+ >
5084
+ <DialogContent className="sm:max-w-md">
5085
+ <DialogHeader>
5086
+ <DialogTitle>{t('deleteConfirmTitle')}</DialogTitle>
5087
+ </DialogHeader>
5088
+ <p className="text-sm text-muted-foreground">
5089
+ {t('deleteConfirmDescription')}
5090
+ </p>
5091
+ <div className="space-y-1.5">
5092
+ <Label className="text-xs">{t('deleteConfirmLabel')}</Label>
5093
+ <Input
5094
+ value={deleteProjectConfirmText}
5095
+ onChange={(e) => setDeleteProjectConfirmText(e.target.value)}
5096
+ placeholder={project?.name ?? ''}
5097
+ />
5098
+ </div>
5099
+ <DialogFooter className="mt-2">
5100
+ <Button
5101
+ variant="outline"
5102
+ onClick={() => {
5103
+ setDeleteProjectDialogOpen(false);
5104
+ setDeleteProjectConfirmText('');
5105
+ }}
5106
+ >
5107
+ {commonT('actions.cancel')}
5108
+ </Button>
5109
+ <Button
5110
+ variant="destructive"
5111
+ disabled={
5112
+ deletingProject ||
5113
+ deleteProjectConfirmText !== (project?.name ?? '')
5114
+ }
5115
+ onClick={() => void handleDeleteProject()}
5116
+ >
5117
+ {deletingProject ? (
5118
+ <Loader2 className="mr-2 size-3.5 animate-spin" />
5119
+ ) : null}
5120
+ {t('deleteConfirmButton')}
5121
+ </Button>
5122
+ </DialogFooter>
5123
+ </DialogContent>
5124
+ </Dialog>
4564
5125
  </>
4565
5126
  ) : null}
4566
5127
  </Page>