@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,450 +1,643 @@
1
- 'use client';
2
-
3
- import { Button } from '@/components/ui/button';
4
- import { EntityPicker } from '@/components/ui/entity-picker';
5
- import { Input } from '@/components/ui/input';
6
- import {
7
- Sheet,
8
- SheetContent,
9
- SheetDescription,
10
- SheetHeader,
11
- SheetTitle,
12
- } from '@/components/ui/sheet';
13
- import { Check, Loader2, Pencil, Plus, X } from 'lucide-react';
14
- import { useTranslations } from 'next-intl';
15
- import { useState } from 'react';
16
- import { fetchOperations, mutateOperations } from '../_lib/api';
17
- import type {
18
- OperationsCollaboratorDetails,
19
- OperationsProject,
20
- PaginatedResponse,
21
- } from '../_lib/types';
22
- import {
23
- formatDateRange,
24
- formatEnumLabel,
25
- getStatusBadgeClass,
26
- } from '../_lib/utils/format';
27
- import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
28
- import { ProjectFormScreen } from './project-form-screen';
29
- import { StatusBadge } from './status-badge';
30
-
31
- const PAGE_SIZE = 10;
32
-
33
- type AssignmentEditState = {
34
- roleLabel: string;
35
- weeklyHours: string;
36
- allocationPercent: string;
37
- startDate: string;
38
- endDate: string;
39
- };
40
-
41
- type ProjectAssignmentsTabProps = {
42
- collaborator: OperationsCollaboratorDetails | null;
43
- request: Parameters<typeof mutateOperations>[0];
44
- onUpdated: () => void;
45
- disabled?: boolean;
46
- };
47
-
48
- export function ProjectAssignmentsTab({
49
- collaborator,
50
- request,
51
- onUpdated,
52
- disabled,
53
- }: ProjectAssignmentsTabProps) {
54
- const commonT = useTranslations('operations.Common');
55
- const detailsT = useTranslations('operations.CollaboratorDetailsPage');
56
-
57
- const [page, setPage] = useState(0);
58
- const [editingId, setEditingId] = useState<number | null>(null);
59
- const [editState, setEditState] = useState<AssignmentEditState | null>(null);
60
- const [saving, setSaving] = useState(false);
61
- const [adding, setAdding] = useState(false);
62
- const [createSheetOpen, setCreateSheetOpen] = useState(false);
63
- const [pickerKey, setPickerKey] = useState(0);
64
-
65
- const projects = collaborator?.assignedProjects ?? [];
66
- const assignedIds = new Set(projects.map((p) => p.id));
67
- const totalPages = Math.ceil(projects.length / PAGE_SIZE);
68
- const visibleProjects = projects.slice(
69
- page * PAGE_SIZE,
70
- (page + 1) * PAGE_SIZE
71
- );
72
-
73
- const startEditing = (
74
- project: OperationsCollaboratorDetails['assignedProjects'][number]
75
- ) => {
76
- setEditingId(project.id);
77
- setEditState({
78
- roleLabel: project.roleLabel ?? '',
79
- weeklyHours:
80
- project.weeklyHours != null ? String(project.weeklyHours) : '',
81
- allocationPercent:
82
- project.allocationPercent != null
83
- ? String(project.allocationPercent)
84
- : '',
85
- startDate: project.startDate
86
- ? String(project.startDate).slice(0, 10)
87
- : '',
88
- endDate: project.endDate ? String(project.endDate).slice(0, 10) : '',
89
- });
90
- };
91
-
92
- const cancelEditing = () => {
93
- setEditingId(null);
94
- setEditState(null);
95
- };
96
-
97
- const saveEditing = async (projectId: number) => {
98
- if (!collaborator || !editState) return;
99
- setSaving(true);
100
- try {
101
- await mutateOperations(
102
- request,
103
- `/operations/collaborators/${collaborator.id}/projects/${projectId}`,
104
- 'PATCH',
105
- {
106
- roleLabel: trimToNull(editState.roleLabel),
107
- weeklyHours: parseNumberInput(editState.weeklyHours),
108
- allocationPercent: parseNumberInput(editState.allocationPercent),
109
- startDate: trimToNull(editState.startDate),
110
- endDate: trimToNull(editState.endDate),
111
- }
112
- );
113
- setEditingId(null);
114
- setEditState(null);
115
- onUpdated();
116
- } catch {
117
- /* errors surfaced by onUpdated not triggering */
118
- } finally {
119
- setSaving(false);
120
- }
121
- };
122
-
123
- const assignProject = async (projectId: number) => {
124
- if (!collaborator) return;
125
- setAdding(true);
126
- try {
127
- await mutateOperations(
128
- request,
129
- `/operations/collaborators/${collaborator.id}/projects`,
130
- 'POST',
131
- { projectId }
132
- );
133
- setPickerKey((k) => k + 1);
134
- onUpdated();
135
- } finally {
136
- setAdding(false);
137
- }
138
- };
139
-
140
- return (
141
- <div className="space-y-3">
142
- {!disabled ? (
143
- <div className="flex items-center gap-2">
144
- <div className="min-w-0 flex-1">
145
- <EntityPicker<OperationsProject>
146
- key={pickerKey}
147
- placeholder={detailsT('addProject')}
148
- searchPlaceholder={detailsT('searchProject')}
149
- showCreateButton={false}
150
- clearable={false}
151
- disabled={!collaborator || adding}
152
- loadOptions={async ({ page: p, pageSize: ps, search }) => {
153
- const params = new URLSearchParams({
154
- page: String(p),
155
- pageSize: String(ps),
156
- });
157
- if (search.trim()) params.set('search', search.trim());
158
- const res = await fetchOperations<
159
- PaginatedResponse<OperationsProject>
160
- >(request, `/operations/projects?${params.toString()}`);
161
- return {
162
- items: res.data.filter((proj) => !assignedIds.has(proj.id)),
163
- hasMore:
164
- (res.page ?? p) * (res.pageSize ?? ps) <
165
- (res.total ?? 0),
166
- };
167
- }}
168
- getOptionValue={(opt) => opt.id}
169
- getOptionLabel={(opt) => opt.name}
170
- getOptionDescription={(opt) =>
171
- [opt.code, opt.clientName].filter(Boolean).join(' • ') ||
172
- undefined
173
- }
174
- onChange={(value) => {
175
- if (value != null) void assignProject(Number(value));
176
- }}
177
- />
178
- </div>
179
- <Button
180
- type="button"
181
- variant="outline"
182
- size="icon"
183
- className="shrink-0"
184
- title={detailsT('createProject')}
185
- onClick={() => setCreateSheetOpen(true)}
186
- >
187
- {adding ? (
188
- <Loader2 className="size-4 animate-spin" />
189
- ) : (
190
- <Plus className="size-4" />
191
- )}
192
- </Button>
193
- </div>
194
- ) : null}
195
-
196
- {projects.length === 0 ? (
197
- <p className="text-sm text-muted-foreground">{detailsT('noProjects')}</p>
198
- ) : (
199
- <>
200
- <div className="space-y-2">
201
- {visibleProjects.map((project) => {
202
- const isEditing = editingId === project.id;
203
-
204
- if (isEditing && editState) {
205
- return (
206
- <div
207
- key={project.id}
208
- className="space-y-2 rounded-lg border px-3 py-2.5"
209
- >
210
- <div className="flex items-center justify-between gap-2">
211
- <div className="min-w-0">
212
- <div className="truncate text-sm font-medium">
213
- {project.name}
214
- </div>
215
- {project.code ? (
216
- <div className="text-xs text-muted-foreground">
217
- {project.code}
218
- </div>
219
- ) : null}
220
- </div>
221
- <StatusBadge
222
- label={formatEnumLabel(project.status)}
223
- className={getStatusBadgeClass(project.status)}
224
- />
225
- </div>
226
-
227
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
228
- <div className="col-span-2 space-y-1 sm:col-span-1">
229
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
230
- {commonT('labels.role')}
231
- </div>
232
- <Input
233
- className="h-7 text-xs"
234
- value={editState.roleLabel}
235
- onChange={(e) =>
236
- setEditState(
237
- (s) => s && { ...s, roleLabel: e.target.value }
238
- )
239
- }
240
- />
241
- </div>
242
- <div className="space-y-1">
243
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
244
- {commonT('labels.weeklyCapacity')}
245
- </div>
246
- <Input
247
- className="h-7 text-xs"
248
- type="number"
249
- min="0"
250
- step="0.5"
251
- value={editState.weeklyHours}
252
- onChange={(e) =>
253
- setEditState(
254
- (s) => s && { ...s, weeklyHours: e.target.value }
255
- )
256
- }
257
- />
258
- </div>
259
- <div className="space-y-1">
260
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
261
- {commonT('labels.allocationPercent')}
262
- </div>
263
- <div className="relative">
264
- <Input
265
- className="h-7 pr-5 text-xs"
266
- type="number"
267
- min="0"
268
- max="100"
269
- step="1"
270
- value={editState.allocationPercent}
271
- onChange={(e) =>
272
- setEditState(
273
- (s) =>
274
- s && {
275
- ...s,
276
- allocationPercent: e.target.value,
277
- }
278
- )
279
- }
280
- />
281
- <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground">
282
- %
283
- </span>
284
- </div>
285
- </div>
286
- <div className="space-y-1">
287
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
288
- {commonT('labels.startDate')}
289
- </div>
290
- <Input
291
- className="h-7 text-xs"
292
- type="date"
293
- value={editState.startDate}
294
- onChange={(e) =>
295
- setEditState(
296
- (s) => s && { ...s, startDate: e.target.value }
297
- )
298
- }
299
- />
300
- </div>
301
- <div className="space-y-1">
302
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
303
- {commonT('labels.endDate')}
304
- </div>
305
- <Input
306
- className="h-7 text-xs"
307
- type="date"
308
- value={editState.endDate}
309
- onChange={(e) =>
310
- setEditState(
311
- (s) => s && { ...s, endDate: e.target.value }
312
- )
313
- }
314
- />
315
- </div>
316
- </div>
317
-
318
- <div className="flex justify-end gap-1.5 pt-1">
319
- <Button
320
- type="button"
321
- size="sm"
322
- variant="ghost"
323
- className="h-7 px-2 text-xs"
324
- onClick={cancelEditing}
325
- disabled={saving}
326
- >
327
- <X className="mr-1 size-3" />
328
- {commonT('actions.cancel')}
329
- </Button>
330
- <Button
331
- type="button"
332
- size="sm"
333
- className="h-7 px-2 text-xs"
334
- onClick={() => void saveEditing(project.id)}
335
- disabled={saving}
336
- >
337
- {saving ? (
338
- <Loader2 className="mr-1 size-3 animate-spin" />
339
- ) : (
340
- <Check className="mr-1 size-3" />
341
- )}
342
- {commonT('actions.save')}
343
- </Button>
344
- </div>
345
- </div>
346
- );
347
- }
348
-
349
- return (
350
- <div
351
- key={project.id}
352
- className="flex items-center gap-3 rounded-lg border px-3 py-2.5"
353
- >
354
- <div className="min-w-0 flex-1">
355
- <div className="truncate text-sm font-medium">
356
- {project.name}
357
- </div>
358
- <div className="truncate text-xs text-muted-foreground">
359
- {[
360
- project.code,
361
- project.roleLabel || null,
362
- project.weeklyHours != null
363
- ? `${project.weeklyHours}h/sem`
364
- : null,
365
- project.allocationPercent != null
366
- ? `${project.allocationPercent}%`
367
- : null,
368
- formatDateRange(project.startDate, project.endDate),
369
- ]
370
- .filter(Boolean)
371
- .join(' ')}
372
- </div>
373
- </div>
374
- <div className="flex shrink-0 items-center gap-2">
375
- <StatusBadge
376
- label={formatEnumLabel(project.status)}
377
- className={getStatusBadgeClass(project.status)}
378
- />
379
- {!disabled ? (
380
- <Button
381
- type="button"
382
- variant="ghost"
383
- size="icon"
384
- className="h-6 w-6"
385
- onClick={() => startEditing(project)}
386
- >
387
- <Pencil className="size-3" />
388
- <span className="sr-only">{commonT('actions.edit')}</span>
389
- </Button>
390
- ) : null}
391
- </div>
392
- </div>
393
- );
394
- })}
395
- </div>
396
-
397
- {totalPages > 1 ? (
398
- <div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
399
- <span>
400
- {page * PAGE_SIZE + 1}–
401
- {Math.min((page + 1) * PAGE_SIZE, projects.length)} /{' '}
402
- {projects.length}
403
- </span>
404
- <div className="flex gap-1">
405
- <Button
406
- type="button"
407
- variant="outline"
408
- size="sm"
409
- className="h-6 px-2 text-xs"
410
- disabled={page === 0}
411
- onClick={() => setPage((p) => p - 1)}
412
- >
413
-
414
- </Button>
415
- <Button
416
- type="button"
417
- variant="outline"
418
- size="sm"
419
- className="h-6 px-2 text-xs"
420
- disabled={page >= totalPages - 1}
421
- onClick={() => setPage((p) => p + 1)}
422
- >
423
-
424
- </Button>
425
- </div>
426
- </div>
427
- ) : null}
428
- </>
429
- )}
430
-
431
- <Sheet open={createSheetOpen} onOpenChange={setCreateSheetOpen}>
432
- <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
433
- <SheetHeader>
434
- <SheetTitle>{detailsT('createProject')}</SheetTitle>
435
- <SheetDescription>{detailsT('createProjectDescription')}</SheetDescription>
436
- </SheetHeader>
437
- <ProjectFormScreen
438
- onCancel={() => setCreateSheetOpen(false)}
439
- onSaved={async (project) => {
440
- setCreateSheetOpen(false);
441
- if (collaborator) {
442
- await assignProject(project.id);
443
- }
444
- }}
445
- />
446
- </SheetContent>
447
- </Sheet>
448
- </div>
449
- );
450
- }
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { EntityPicker } from '@/components/ui/entity-picker';
5
+ import { Input } from '@/components/ui/input';
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetDescription,
10
+ SheetHeader,
11
+ SheetTitle,
12
+ } from '@/components/ui/sheet';
13
+ import { Switch } from '@/components/ui/switch';
14
+ import {
15
+ Check,
16
+ Loader2,
17
+ Pencil,
18
+ Plus,
19
+ SlidersHorizontal,
20
+ X,
21
+ } from 'lucide-react';
22
+ import { useTranslations } from 'next-intl';
23
+ import { useEffect, useState } from 'react';
24
+ import { fetchOperations, mutateOperations } from '../_lib/api';
25
+ import type {
26
+ OperationsCollaboratorDetails,
27
+ OperationsProject,
28
+ PaginatedResponse,
29
+ } from '../_lib/types';
30
+ import {
31
+ formatDateRange,
32
+ formatEnumLabel,
33
+ getStatusBadgeClass,
34
+ } from '../_lib/utils/format';
35
+ import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
36
+ import { ProjectFormScreen } from './project-form-screen';
37
+ import { StatusBadge } from './status-badge';
38
+
39
+ const PAGE_SIZE = 10;
40
+ const LS_AUTO_REBALANCE_KEY = 'operations.projectAllocation.autoRebalance';
41
+
42
+ type AssignmentEditState = {
43
+ roleLabel: string;
44
+ weeklyHours: string;
45
+ allocationPercent: string;
46
+ startDate: string;
47
+ endDate: string;
48
+ };
49
+
50
+ type ProjectAssignmentsTabProps = {
51
+ collaborator: OperationsCollaboratorDetails | null;
52
+ request: Parameters<typeof mutateOperations>[0];
53
+ onUpdated: () => void;
54
+ disabled?: boolean;
55
+ };
56
+
57
+ export function ProjectAssignmentsTab({
58
+ collaborator,
59
+ request,
60
+ onUpdated,
61
+ disabled,
62
+ }: ProjectAssignmentsTabProps) {
63
+ const commonT = useTranslations('operations.Common');
64
+ const detailsT = useTranslations('operations.CollaboratorDetailsPage');
65
+ const allocT = useTranslations(
66
+ 'operations.CollaboratorFormPage.projectAllocation'
67
+ );
68
+
69
+ const [page, setPage] = useState(0);
70
+ const [editingId, setEditingId] = useState<number | null>(null);
71
+ const [editState, setEditState] = useState<AssignmentEditState | null>(null);
72
+ const [saving, setSaving] = useState(false);
73
+ const [distributing, setDistributing] = useState(false);
74
+ const [adding, setAdding] = useState(false);
75
+ const [createSheetOpen, setCreateSheetOpen] = useState(false);
76
+ const [pickerKey, setPickerKey] = useState(0);
77
+ const [autoRebalance, setAutoRebalance] = useState(() => {
78
+ if (typeof window === 'undefined') return false;
79
+ return localStorage.getItem(LS_AUTO_REBALANCE_KEY) === 'true';
80
+ });
81
+
82
+ useEffect(() => {
83
+ localStorage.setItem(LS_AUTO_REBALANCE_KEY, String(autoRebalance));
84
+ }, [autoRebalance]);
85
+
86
+ const projects = collaborator?.assignedProjects ?? [];
87
+ const assignedIds = new Set(projects.map((p) => p.id));
88
+ const totalPages = Math.ceil(projects.length / PAGE_SIZE);
89
+ const visibleProjects = projects.slice(
90
+ page * PAGE_SIZE,
91
+ (page + 1) * PAGE_SIZE
92
+ );
93
+
94
+ const savedTotal = projects.reduce(
95
+ (sum, p) => sum + (p.allocationPercent ?? 0),
96
+ 0
97
+ );
98
+
99
+ // When editing, show projected total
100
+ const editingProject =
101
+ editingId != null ? projects.find((p) => p.id === editingId) : null;
102
+ const projectedTotal =
103
+ editState && editingProject
104
+ ? projects.reduce((sum, p) => {
105
+ if (p.id === editingId) {
106
+ return sum + (parseNumberInput(editState.allocationPercent) ?? 0);
107
+ }
108
+ return sum + (p.allocationPercent ?? 0);
109
+ }, 0)
110
+ : savedTotal;
111
+
112
+ const displayTotal = editState ? projectedTotal : savedTotal;
113
+
114
+ const allocationWarning =
115
+ displayTotal < 100 ? 'idle' : displayTotal > 100 ? 'overloaded' : null;
116
+
117
+ const startEditing = (
118
+ project: OperationsCollaboratorDetails['assignedProjects'][number]
119
+ ) => {
120
+ setEditingId(project.id);
121
+ setEditState({
122
+ roleLabel: project.roleLabel ?? '',
123
+ weeklyHours:
124
+ project.weeklyHours != null ? String(project.weeklyHours) : '',
125
+ allocationPercent:
126
+ project.allocationPercent != null
127
+ ? String(project.allocationPercent)
128
+ : '',
129
+ startDate: project.startDate
130
+ ? String(project.startDate).slice(0, 10)
131
+ : '',
132
+ endDate: project.endDate ? String(project.endDate).slice(0, 10) : '',
133
+ });
134
+ };
135
+
136
+ const cancelEditing = () => {
137
+ setEditingId(null);
138
+ setEditState(null);
139
+ };
140
+
141
+ const saveEditing = async (projectId: number) => {
142
+ if (!collaborator || !editState) return;
143
+ setSaving(true);
144
+ try {
145
+ const newPercent = parseNumberInput(editState.allocationPercent);
146
+
147
+ await mutateOperations(
148
+ request,
149
+ `/operations/collaborators/${collaborator.id}/projects/${projectId}`,
150
+ 'PATCH',
151
+ {
152
+ roleLabel: trimToNull(editState.roleLabel),
153
+ weeklyHours: parseNumberInput(editState.weeklyHours),
154
+ allocationPercent: newPercent,
155
+ startDate: trimToNull(editState.startDate),
156
+ endDate: trimToNull(editState.endDate),
157
+ }
158
+ );
159
+
160
+ // Auto-rebalance: distribute remaining % among other projects
161
+ if (autoRebalance && newPercent != null) {
162
+ const others = projects.filter((p) => p.id !== projectId);
163
+ if (others.length > 0) {
164
+ const remaining = 100 - newPercent;
165
+ const othersTotal = others.reduce(
166
+ (sum, p) => sum + (p.allocationPercent ?? 0),
167
+ 0
168
+ );
169
+ await Promise.all(
170
+ others.map((p, idx) => {
171
+ let newValue: number;
172
+ if (othersTotal > 0) {
173
+ newValue =
174
+ Math.round(
175
+ ((p.allocationPercent ?? 0) / othersTotal) * remaining * 100
176
+ ) / 100;
177
+ } else {
178
+ newValue = Math.round((remaining / others.length) * 100) / 100;
179
+ }
180
+ // Ensure the last item corrects rounding drift
181
+ if (idx === others.length - 1) {
182
+ const alreadyAssigned = others
183
+ .slice(0, idx)
184
+ .reduce((sum, op) => {
185
+ if (othersTotal > 0) {
186
+ return (
187
+ sum +
188
+ Math.round(
189
+ ((op.allocationPercent ?? 0) / othersTotal) *
190
+ remaining *
191
+ 100
192
+ ) /
193
+ 100
194
+ );
195
+ }
196
+ return (
197
+ sum + Math.round((remaining / others.length) * 100) / 100
198
+ );
199
+ }, 0);
200
+ newValue =
201
+ Math.round((remaining - alreadyAssigned) * 100) / 100;
202
+ }
203
+ return mutateOperations(
204
+ request,
205
+ `/operations/collaborators/${collaborator.id}/projects/${p.id}`,
206
+ 'PATCH',
207
+ { allocationPercent: newValue }
208
+ );
209
+ })
210
+ );
211
+ }
212
+ }
213
+
214
+ setEditingId(null);
215
+ setEditState(null);
216
+ onUpdated();
217
+ } catch {
218
+ /* errors surfaced by onUpdated not triggering */
219
+ } finally {
220
+ setSaving(false);
221
+ }
222
+ };
223
+
224
+ const distributeEqually = async () => {
225
+ if (!collaborator) return;
226
+ const activeProjects = projects.filter((p) => p.status === 'active');
227
+ const targetProjects =
228
+ activeProjects.length > 0 ? activeProjects : projects;
229
+ if (targetProjects.length === 0) return;
230
+ setDistributing(true);
231
+ try {
232
+ const equalShare = Math.floor((100 / targetProjects.length) * 100) / 100;
233
+ const remainder =
234
+ Math.round((100 - equalShare * (targetProjects.length - 1)) * 100) /
235
+ 100;
236
+ await Promise.all(
237
+ targetProjects.map((p, idx) =>
238
+ mutateOperations(
239
+ request,
240
+ `/operations/collaborators/${collaborator.id}/projects/${p.id}`,
241
+ 'PATCH',
242
+ {
243
+ allocationPercent:
244
+ idx === targetProjects.length - 1 ? remainder : equalShare,
245
+ }
246
+ )
247
+ )
248
+ );
249
+ onUpdated();
250
+ } finally {
251
+ setDistributing(false);
252
+ }
253
+ };
254
+
255
+ const assignProject = async (projectId: number) => {
256
+ if (!collaborator) return;
257
+ setAdding(true);
258
+ try {
259
+ await mutateOperations(
260
+ request,
261
+ `/operations/collaborators/${collaborator.id}/projects`,
262
+ 'POST',
263
+ { projectId }
264
+ );
265
+ setPickerKey((k) => k + 1);
266
+ onUpdated();
267
+ } finally {
268
+ setAdding(false);
269
+ }
270
+ };
271
+
272
+ return (
273
+ <div className="space-y-3">
274
+ {/* Allocation summary bar */}
275
+ {projects.length > 0 && (
276
+ <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border px-3 py-2">
277
+ <div className="flex items-center gap-3">
278
+ <span className="text-xs text-muted-foreground">
279
+ {allocT('total')}:
280
+ </span>
281
+ <span
282
+ className={`text-sm font-semibold ${
283
+ allocationWarning === 'overloaded'
284
+ ? 'text-destructive'
285
+ : allocationWarning === 'idle'
286
+ ? 'text-amber-600 dark:text-amber-400'
287
+ : 'text-green-600 dark:text-green-400'
288
+ }`}
289
+ >
290
+ {displayTotal}%
291
+ </span>
292
+ {allocationWarning === 'overloaded' && (
293
+ <span className="rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">
294
+ {allocT('overloaded')} +
295
+ {Math.round((displayTotal - 100) * 100) / 100}%
296
+ </span>
297
+ )}
298
+ {allocationWarning === 'idle' && (
299
+ <span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">
300
+ {allocT('idle')} -{Math.round((100 - displayTotal) * 100) / 100}
301
+ %
302
+ </span>
303
+ )}
304
+ </div>
305
+ {!disabled && (
306
+ <div className="flex items-center gap-3">
307
+ {/* Auto-rebalance toggle */}
308
+ <div className="flex items-center gap-1.5">
309
+ <SlidersHorizontal className="size-3 text-muted-foreground" />
310
+ <span className="text-[11px] text-muted-foreground">
311
+ {allocT('autoRebalance')}
312
+ </span>
313
+ <Switch
314
+ checked={autoRebalance}
315
+ onCheckedChange={setAutoRebalance}
316
+ className="scale-75"
317
+ />
318
+ </div>
319
+ {/* Distribute equally */}
320
+ <Button
321
+ type="button"
322
+ variant="outline"
323
+ size="sm"
324
+ className="h-6 px-2 text-[11px]"
325
+ disabled={distributing || projects.length === 0}
326
+ onClick={() => void distributeEqually()}
327
+ >
328
+ {distributing ? (
329
+ <Loader2 className="mr-1 size-3 animate-spin" />
330
+ ) : null}
331
+ {distributing
332
+ ? allocT('distributing')
333
+ : allocT('distributeEqually')}
334
+ </Button>
335
+ </div>
336
+ )}
337
+ </div>
338
+ )}
339
+
340
+ {!disabled ? (
341
+ <div className="flex items-center gap-2">
342
+ <div className="min-w-0 flex-1">
343
+ <EntityPicker<OperationsProject>
344
+ key={pickerKey}
345
+ placeholder={detailsT('addProject')}
346
+ searchPlaceholder={detailsT('searchProject')}
347
+ showCreateButton={false}
348
+ clearable={false}
349
+ disabled={!collaborator || adding}
350
+ loadOptions={async ({ page: p, pageSize: ps, search }) => {
351
+ const params = new URLSearchParams({
352
+ page: String(p),
353
+ pageSize: String(ps),
354
+ });
355
+ if (search.trim()) params.set('search', search.trim());
356
+ const res = await fetchOperations<
357
+ PaginatedResponse<OperationsProject>
358
+ >(request, `/operations/projects?${params.toString()}`);
359
+ return {
360
+ items: res.data.filter((proj) => !assignedIds.has(proj.id)),
361
+ hasMore:
362
+ (res.page ?? p) * (res.pageSize ?? ps) < (res.total ?? 0),
363
+ };
364
+ }}
365
+ getOptionValue={(opt) => opt.id}
366
+ getOptionLabel={(opt) => opt.name}
367
+ getOptionDescription={(opt) =>
368
+ [opt.code, opt.clientName].filter(Boolean).join(' • ') ||
369
+ undefined
370
+ }
371
+ onChange={(value) => {
372
+ if (value != null) void assignProject(Number(value));
373
+ }}
374
+ />
375
+ </div>
376
+ <Button
377
+ type="button"
378
+ variant="outline"
379
+ size="icon"
380
+ className="shrink-0"
381
+ title={detailsT('createProject')}
382
+ onClick={() => setCreateSheetOpen(true)}
383
+ >
384
+ {adding ? (
385
+ <Loader2 className="size-4 animate-spin" />
386
+ ) : (
387
+ <Plus className="size-4" />
388
+ )}
389
+ </Button>
390
+ </div>
391
+ ) : null}
392
+
393
+ {projects.length === 0 ? (
394
+ <p className="text-sm text-muted-foreground">
395
+ {detailsT('noProjects')}
396
+ </p>
397
+ ) : (
398
+ <>
399
+ <div className="space-y-2">
400
+ {visibleProjects.map((project) => {
401
+ const isEditing = editingId === project.id;
402
+
403
+ if (isEditing && editState) {
404
+ return (
405
+ <div
406
+ key={project.id}
407
+ className="space-y-2 rounded-lg border px-3 py-2.5"
408
+ >
409
+ <div className="flex items-center justify-between gap-2">
410
+ <div className="min-w-0">
411
+ <div className="truncate text-sm font-medium">
412
+ {project.name}
413
+ </div>
414
+ {project.code ? (
415
+ <div className="text-xs text-muted-foreground">
416
+ {project.code}
417
+ </div>
418
+ ) : null}
419
+ </div>
420
+ <StatusBadge
421
+ label={formatEnumLabel(project.status)}
422
+ className={getStatusBadgeClass(project.status)}
423
+ />
424
+ </div>
425
+
426
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
427
+ <div className="space-y-1">
428
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
429
+ {commonT('labels.weeklyCapacity')}
430
+ </div>
431
+ <Input
432
+ className="h-7 text-xs"
433
+ type="number"
434
+ min="0"
435
+ step="0.5"
436
+ value={editState.weeklyHours}
437
+ onChange={(e) =>
438
+ setEditState(
439
+ (s) => s && { ...s, weeklyHours: e.target.value }
440
+ )
441
+ }
442
+ />
443
+ </div>
444
+ <div className="space-y-1">
445
+ <div className="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground">
446
+ {commonT('labels.allocationPercent')}
447
+ {autoRebalance && (
448
+ <span className="rounded bg-primary/10 px-1 text-[9px] text-primary">
449
+ auto
450
+ </span>
451
+ )}
452
+ </div>
453
+ <div className="relative">
454
+ <Input
455
+ className="h-7 pr-5 text-xs"
456
+ type="number"
457
+ min="0"
458
+ max="100"
459
+ step="1"
460
+ value={editState.allocationPercent}
461
+ onChange={(e) =>
462
+ setEditState(
463
+ (s) =>
464
+ s && {
465
+ ...s,
466
+ allocationPercent: e.target.value,
467
+ }
468
+ )
469
+ }
470
+ />
471
+ <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground">
472
+ %
473
+ </span>
474
+ </div>
475
+ </div>
476
+ <div className="space-y-1">
477
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
478
+ {commonT('labels.startDate')}
479
+ </div>
480
+ <Input
481
+ className="h-7 text-xs"
482
+ type="date"
483
+ value={editState.startDate}
484
+ onChange={(e) =>
485
+ setEditState(
486
+ (s) => s && { ...s, startDate: e.target.value }
487
+ )
488
+ }
489
+ />
490
+ </div>
491
+ <div className="space-y-1">
492
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
493
+ {commonT('labels.endDate')}
494
+ </div>
495
+ <Input
496
+ className="h-7 text-xs"
497
+ type="date"
498
+ value={editState.endDate}
499
+ onChange={(e) =>
500
+ setEditState(
501
+ (s) => s && { ...s, endDate: e.target.value }
502
+ )
503
+ }
504
+ />
505
+ </div>
506
+ </div>
507
+
508
+ <div className="flex justify-end gap-1.5 pt-1">
509
+ <Button
510
+ type="button"
511
+ size="sm"
512
+ variant="ghost"
513
+ className="h-7 px-2 text-xs"
514
+ onClick={cancelEditing}
515
+ disabled={saving}
516
+ >
517
+ <X className="mr-1 size-3" />
518
+ {commonT('actions.cancel')}
519
+ </Button>
520
+ <Button
521
+ type="button"
522
+ size="sm"
523
+ className="h-7 px-2 text-xs"
524
+ onClick={() => void saveEditing(project.id)}
525
+ disabled={saving}
526
+ >
527
+ {saving ? (
528
+ <Loader2 className="mr-1 size-3 animate-spin" />
529
+ ) : (
530
+ <Check className="mr-1 size-3" />
531
+ )}
532
+ {commonT('actions.save')}
533
+ </Button>
534
+ </div>
535
+ </div>
536
+ );
537
+ }
538
+
539
+ return (
540
+ <div
541
+ key={project.id}
542
+ className="flex items-center gap-3 rounded-lg border px-3 py-2.5"
543
+ >
544
+ <div className="min-w-0 flex-1">
545
+ <div className="truncate text-sm font-medium">
546
+ {project.name}
547
+ </div>
548
+ <div className="truncate text-xs text-muted-foreground">
549
+ {[
550
+ project.code,
551
+ project.weeklyHours != null
552
+ ? `${project.weeklyHours}h/sem`
553
+ : null,
554
+ project.allocationPercent != null
555
+ ? `${project.allocationPercent}%`
556
+ : null,
557
+ formatDateRange(project.startDate, project.endDate),
558
+ ]
559
+ .filter(Boolean)
560
+ .join(' • ')}
561
+ </div>
562
+ </div>
563
+ <div className="flex shrink-0 items-center gap-2">
564
+ <StatusBadge
565
+ label={formatEnumLabel(project.status)}
566
+ className={getStatusBadgeClass(project.status)}
567
+ />
568
+ {!disabled ? (
569
+ <Button
570
+ type="button"
571
+ variant="ghost"
572
+ size="icon"
573
+ className="h-6 w-6"
574
+ onClick={() => startEditing(project)}
575
+ >
576
+ <Pencil className="size-3" />
577
+ <span className="sr-only">
578
+ {commonT('actions.edit')}
579
+ </span>
580
+ </Button>
581
+ ) : null}
582
+ </div>
583
+ </div>
584
+ );
585
+ })}
586
+ </div>
587
+
588
+ {totalPages > 1 ? (
589
+ <div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
590
+ <span>
591
+ {page * PAGE_SIZE + 1}–
592
+ {Math.min((page + 1) * PAGE_SIZE, projects.length)} /{' '}
593
+ {projects.length}
594
+ </span>
595
+ <div className="flex gap-1">
596
+ <Button
597
+ type="button"
598
+ variant="outline"
599
+ size="sm"
600
+ className="h-6 px-2 text-xs"
601
+ disabled={page === 0}
602
+ onClick={() => setPage((p) => p - 1)}
603
+ >
604
+
605
+ </Button>
606
+ <Button
607
+ type="button"
608
+ variant="outline"
609
+ size="sm"
610
+ className="h-6 px-2 text-xs"
611
+ disabled={page >= totalPages - 1}
612
+ onClick={() => setPage((p) => p + 1)}
613
+ >
614
+
615
+ </Button>
616
+ </div>
617
+ </div>
618
+ ) : null}
619
+ </>
620
+ )}
621
+
622
+ <Sheet open={createSheetOpen} onOpenChange={setCreateSheetOpen}>
623
+ <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
624
+ <SheetHeader>
625
+ <SheetTitle>{detailsT('createProject')}</SheetTitle>
626
+ <SheetDescription>
627
+ {detailsT('createProjectDescription')}
628
+ </SheetDescription>
629
+ </SheetHeader>
630
+ <ProjectFormScreen
631
+ onCancel={() => setCreateSheetOpen(false)}
632
+ onSaved={async (project) => {
633
+ setCreateSheetOpen(false);
634
+ if (collaborator) {
635
+ await assignProject(project.id);
636
+ }
637
+ }}
638
+ />
639
+ </SheetContent>
640
+ </Sheet>
641
+ </div>
642
+ );
643
+ }