@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,5 +1,11 @@
1
1
  'use client';
2
2
 
3
+ import { PersonFormSheet } from '@/app/(app)/(libraries)/contact/person/_components/person-form-sheet';
4
+ import type {
5
+ ContactTypeOption,
6
+ DocumentTypeOption,
7
+ Person,
8
+ } from '@/app/(app)/(libraries)/contact/person/_components/person-types';
3
9
  import { EmptyState, Page } from '@/components/entity-list';
4
10
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
5
11
  import { Button } from '@/components/ui/button';
@@ -12,6 +18,7 @@ import {
12
18
  CommandItem,
13
19
  CommandList,
14
20
  } from '@/components/ui/command';
21
+ import { EntityPicker } from '@/components/ui/entity-picker';
15
22
  import {
16
23
  Form,
17
24
  FormControl,
@@ -43,6 +50,7 @@ import {
43
50
  SheetHeader,
44
51
  SheetTitle,
45
52
  } from '@/components/ui/sheet';
53
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
46
54
  import { Textarea } from '@/components/ui/textarea';
47
55
  import {
48
56
  Tooltip,
@@ -66,7 +74,7 @@ import {
66
74
  import { useTranslations } from 'next-intl';
67
75
  import Link from 'next/link';
68
76
  import { useRouter } from 'next/navigation';
69
- import { useEffect, useMemo, useState } from 'react';
77
+ import { useEffect, useMemo, useRef, useState } from 'react';
70
78
  import { useForm } from 'react-hook-form';
71
79
  import { z } from 'zod';
72
80
  import { PersonPicker } from '../../contact/_components/person-picker';
@@ -80,7 +88,6 @@ import type {
80
88
  OperationsCollaborator,
81
89
  OperationsContract,
82
90
  OperationsProjectDetails,
83
- OperationsProjectRole,
84
91
  } from '../_lib/types';
85
92
  import { formatEnumLabel } from '../_lib/utils/format';
86
93
  import {
@@ -89,16 +96,39 @@ import {
89
96
  trimToNull,
90
97
  } from '../_lib/utils/forms';
91
98
  import { ContractFormScreen } from './contract-form-screen';
92
- import { DepartmentPicker } from './department-picker';
93
99
  import { OperationsHeader } from './operations-header';
94
100
 
95
101
  const OPTION_PAGE_SIZE = 12;
96
102
 
103
+ function getPersonAvatarUrl(avatarId?: number | null) {
104
+ return typeof avatarId === 'number' && avatarId > 0
105
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
106
+ : undefined;
107
+ }
108
+
109
+ function getUserPhotoUrl(photoId?: number | null) {
110
+ return typeof photoId === 'number' && photoId > 0
111
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
112
+ : undefined;
113
+ }
114
+
115
+ function getInitials(value?: string | null) {
116
+ const parts = String(value ?? '')
117
+ .trim()
118
+ .split(/\s+/)
119
+ .filter(Boolean)
120
+ .slice(0, 2);
121
+
122
+ if (!parts.length) {
123
+ return '??';
124
+ }
125
+
126
+ return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
127
+ }
128
+
97
129
  type TeamAssignmentState = {
98
130
  collaboratorId: number;
99
131
  selected: boolean;
100
- projectRoleId: string;
101
- roleLabel: string;
102
132
  weeklyHours: string;
103
133
  allocationPercent: string;
104
134
  status: string;
@@ -191,8 +221,6 @@ function buildEmptyForm(
191
221
  teamAssignments: collaborators.map((collaborator) => ({
192
222
  collaboratorId: collaborator.id,
193
223
  selected: false,
194
- projectRoleId: 'none',
195
- roleLabel: '',
196
224
  weeklyHours: '',
197
225
  allocationPercent: '',
198
226
  status: 'active',
@@ -265,10 +293,6 @@ function toFormState(
265
293
  return {
266
294
  collaboratorId: collaborator.id,
267
295
  selected: Boolean(assignment),
268
- projectRoleId: assignment?.projectRoleId
269
- ? String(assignment.projectRoleId)
270
- : 'none',
271
- roleLabel: assignment?.roleLabel ?? '',
272
296
  weeklyHours:
273
297
  assignment?.weeklyHours !== null &&
274
298
  assignment?.weeklyHours !== undefined
@@ -641,10 +665,16 @@ export function ProjectFormScreen({
641
665
  const t = useTranslations('operations.ProjectFormPage');
642
666
  const commonT = useTranslations('operations.Common');
643
667
  const contractT = useTranslations('operations.ContractFormPage');
668
+ const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
644
669
  const { request, showToastHandler, currentLocaleCode } = useApp();
645
670
  const access = useOperationsAccess();
646
671
  const router = useRouter();
647
672
  const [assignmentSearch, setAssignmentSearch] = useState('');
673
+ const personSheetModeRef = useRef<'create' | 'edit'>('edit');
674
+ const [personSheetOpen, setPersonSheetOpen] = useState(false);
675
+ const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
676
+ const [createdManagerCollaborators, setCreatedManagerCollaborators] =
677
+ useState<OperationsCollaborator[]>([]);
648
678
  const isSheetMode = Boolean(onCancel);
649
679
  const isCreateMode = !projectId;
650
680
  const [codeAutoMode, setCodeAutoMode] = useState(isCreateMode);
@@ -684,8 +714,6 @@ export function ProjectFormScreen({
684
714
  z.object({
685
715
  collaboratorId: z.number(),
686
716
  selected: z.boolean(),
687
- projectRoleId: z.string(),
688
- roleLabel: z.string(),
689
717
  weeklyHours: z.string(),
690
718
  allocationPercent: z.string(),
691
719
  status: z.string(),
@@ -694,14 +722,8 @@ export function ProjectFormScreen({
694
722
  })
695
723
  )
696
724
  .superRefine((assignments, ctx) => {
697
- assignments.forEach((assignment, index) => {
698
- if (assignment.selected && !assignment.roleLabel.trim()) {
699
- ctx.addIssue({
700
- code: z.ZodIssueCode.custom,
701
- message: t('messages.roleRequired'),
702
- path: [index, 'roleLabel'],
703
- });
704
- }
725
+ assignments.forEach((_assignment, _index) => {
726
+ // role validation removed
705
727
  });
706
728
  }),
707
729
  }),
@@ -747,6 +769,37 @@ export function ProjectFormScreen({
747
769
  [rawCollaborators]
748
770
  );
749
771
 
772
+ const createManagerCollaborator = async (values: Record<string, string>) => {
773
+ const displayName = values.displayName?.trim() ?? '';
774
+ const weeklyCapacityHours = parseNumberInput(
775
+ values.weeklyCapacityHours ?? ''
776
+ );
777
+
778
+ if (!displayName) {
779
+ return null;
780
+ }
781
+
782
+ const created = await mutateOperations<OperationsCollaborator>(
783
+ request,
784
+ '/operations/collaborators',
785
+ 'POST',
786
+ {
787
+ displayName,
788
+ weeklyCapacityHours,
789
+ status: 'active',
790
+ autoGenerateContractDraft: false,
791
+ }
792
+ );
793
+
794
+ setCreatedManagerCollaborators((current) => {
795
+ const next = current.filter((item) => item.id !== created.id);
796
+ next.unshift(created);
797
+ return next;
798
+ });
799
+
800
+ return created;
801
+ };
802
+
750
803
  const { data: contracts = [], refetch: refetchContracts } = useQuery<
751
804
  OperationsContract[]
752
805
  >({
@@ -756,18 +809,32 @@ export function ProjectFormScreen({
756
809
  fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
757
810
  });
758
811
 
759
- const { data: projectRoles = [], refetch: refetchProjectRoles } = useQuery<
760
- OperationsProjectRole[]
761
- >({
762
- queryKey: ['operations-project-form-project-roles', currentLocaleCode],
763
- enabled: access.isDirector,
764
- staleTime: 0,
765
- refetchOnMount: 'always',
766
- queryFn: () =>
767
- fetchOperations<OperationsProjectRole[]>(
768
- request,
769
- '/operations/project-roles'
770
- ),
812
+ const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
813
+ queryKey: ['contact-person-contact-types', currentLocaleCode],
814
+ enabled: personSheetOpen,
815
+ queryFn: async () => {
816
+ const response = await request<{ data: ContactTypeOption[] }>({
817
+ url: '/person-contact-type?pageSize=100',
818
+ method: 'GET',
819
+ });
820
+
821
+ return response.data.data || [];
822
+ },
823
+ placeholderData: (previous) => previous ?? [],
824
+ });
825
+
826
+ const { data: documentTypes = [] } = useQuery<DocumentTypeOption[]>({
827
+ queryKey: ['contact-person-document-types', currentLocaleCode],
828
+ enabled: personSheetOpen,
829
+ queryFn: async () => {
830
+ const response = await request<{ data: DocumentTypeOption[] }>({
831
+ url: '/person-document-type?pageSize=100',
832
+ method: 'GET',
833
+ });
834
+
835
+ return response.data.data || [];
836
+ },
837
+ placeholderData: (previous) => previous ?? [],
771
838
  });
772
839
 
773
840
  const { data: project, isLoading: isLoadingProject } =
@@ -806,6 +873,10 @@ export function ProjectFormScreen({
806
873
  byId.set(collaborator.id, collaborator);
807
874
  }
808
875
 
876
+ for (const collaborator of createdManagerCollaborators) {
877
+ byId.set(collaborator.id, collaborator);
878
+ }
879
+
809
880
  if (
810
881
  project?.managerCollaboratorId &&
811
882
  !byId.has(project.managerCollaboratorId)
@@ -834,7 +905,7 @@ export function ProjectFormScreen({
834
905
  }
835
906
 
836
907
  return Array.from(byId.values());
837
- }, [collaborators, project]);
908
+ }, [collaborators, createdManagerCollaborators, project]);
838
909
 
839
910
  const availableContracts = useMemo(() => {
840
911
  if (!project?.relatedContract) {
@@ -867,47 +938,13 @@ export function ProjectFormScreen({
867
938
  .filter(Boolean)
868
939
  .join(' • '),
869
940
  avatarUrl:
870
- typeof collaborator.personAvatarId === 'number' &&
871
- collaborator.personAvatarId > 0
872
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${collaborator.personAvatarId}`
873
- : null,
941
+ getUserPhotoUrl(collaborator.userPhotoId) ??
942
+ getPersonAvatarUrl(collaborator.personAvatarId) ??
943
+ null,
874
944
  })),
875
945
  [availableCollaborators]
876
946
  );
877
947
 
878
- const projectRoleOptions = useMemo(
879
- () =>
880
- projectRoles.map((role) => ({
881
- id: role.id,
882
- name: role.name,
883
- code: role.code ?? null,
884
- description: role.description ?? null,
885
- })),
886
- [projectRoles]
887
- );
888
-
889
- const createProjectRole = async (projectRoleName: string) => {
890
- try {
891
- const createdRole = await mutateOperations<OperationsProjectRole>(
892
- request,
893
- '/operations/project-roles',
894
- 'POST',
895
- {
896
- name: projectRoleName,
897
- }
898
- );
899
-
900
- await refetchProjectRoles();
901
- return createdRole;
902
- } catch (error) {
903
- showToastHandler?.(
904
- 'error',
905
- getOperationsErrorMessage(error, t('messages.projectRoleSaveError'))
906
- );
907
- return null;
908
- }
909
- };
910
-
911
948
  const selectedAssignmentsCount = useMemo(
912
949
  () =>
913
950
  form.teamAssignments.filter((assignment) => assignment.selected).length,
@@ -993,11 +1030,6 @@ export function ProjectFormScreen({
993
1030
  .filter((assignment) => assignment.selected)
994
1031
  .map((assignment) => ({
995
1032
  collaboratorId: assignment.collaboratorId,
996
- projectRoleId:
997
- assignment.projectRoleId === 'none'
998
- ? null
999
- : parseNumberInput(assignment.projectRoleId),
1000
- roleLabel: trimToNull(assignment.roleLabel),
1001
1033
  weeklyHours: parseNumberInput(assignment.weeklyHours),
1002
1034
  allocationPercent: parseNumberInput(assignment.allocationPercent),
1003
1035
  status: assignment.status,
@@ -1047,6 +1079,44 @@ export function ProjectFormScreen({
1047
1079
  showToastHandler?.('error', t('messages.requiredFields'));
1048
1080
  };
1049
1081
 
1082
+ const resolvePersonDisplayName = (person?: Person | null) => {
1083
+ const tradeName = person?.trade_name?.trim();
1084
+ if (tradeName) {
1085
+ return tradeName;
1086
+ }
1087
+
1088
+ return person?.name?.trim() || '';
1089
+ };
1090
+
1091
+ const handleOpenPersonEditSheet = async (personId?: number | null) => {
1092
+ const targetPersonId =
1093
+ personId && personId > 0
1094
+ ? personId
1095
+ : parseNumberInput(formMethods.getValues('clientPersonId'));
1096
+
1097
+ if (!targetPersonId) {
1098
+ return;
1099
+ }
1100
+
1101
+ try {
1102
+ const response = await request<Person>({
1103
+ url: `/person/${targetPersonId}`,
1104
+ method: 'GET',
1105
+ });
1106
+ personSheetModeRef.current = 'edit';
1107
+ setPersonToEdit(response.data);
1108
+ setPersonSheetOpen(true);
1109
+ } catch {
1110
+ showToastHandler?.('error', t('messages.updateError'));
1111
+ }
1112
+ };
1113
+
1114
+ const handleOpenPersonCreateSheet = () => {
1115
+ personSheetModeRef.current = 'create';
1116
+ setPersonToEdit(null);
1117
+ setPersonSheetOpen(true);
1118
+ };
1119
+
1050
1120
  const noAccessState = (
1051
1121
  <EmptyState
1052
1122
  icon={<FolderKanban className="size-12" />}
@@ -1171,6 +1241,12 @@ export function ProjectFormScreen({
1171
1241
  personId ? String(personId) : ''
1172
1242
  );
1173
1243
  }}
1244
+ showEditButton
1245
+ editAriaLabel={commonT('actions.editPersonCrm')}
1246
+ onEditSelection={(personId) =>
1247
+ void handleOpenPersonEditSheet(personId)
1248
+ }
1249
+ onCreateNew={() => handleOpenPersonCreateSheet()}
1174
1250
  personTypeFilter="all"
1175
1251
  createType="company"
1176
1252
  />
@@ -1245,20 +1321,100 @@ export function ProjectFormScreen({
1245
1321
  )}
1246
1322
  </p>
1247
1323
  </div>
1248
- <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-5">
1324
+ <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-3">
1249
1325
  <div className="min-w-0 space-y-2">
1250
1326
  <FieldLabel label={commonT('labels.manager')} />
1251
- <SearchableSelect
1252
- label=""
1253
- value={form.managerCollaboratorId}
1327
+ <EntityPicker
1328
+ value={
1329
+ form.managerCollaboratorId === 'none'
1330
+ ? null
1331
+ : Number(form.managerCollaboratorId)
1332
+ }
1254
1333
  options={managerOptions}
1334
+ getOptionValue={(option) => option.id}
1335
+ getOptionLabel={(option) => option.title}
1336
+ getOptionDescription={(option) =>
1337
+ option.description || undefined
1338
+ }
1339
+ renderOption={({ option }) => (
1340
+ <div className="flex min-w-0 items-center gap-2.5">
1341
+ <Avatar className="size-6 shrink-0">
1342
+ <AvatarImage
1343
+ src={option.avatarUrl ?? undefined}
1344
+ alt={option.title}
1345
+ />
1346
+ <AvatarFallback className="text-[10px]">
1347
+ {getInitials(option.title)}
1348
+ </AvatarFallback>
1349
+ </Avatar>
1350
+ <div className="min-w-0">
1351
+ <div className="truncate text-sm">{option.title}</div>
1352
+ {option.description ? (
1353
+ <div className="truncate text-xs text-muted-foreground">
1354
+ {option.description}
1355
+ </div>
1356
+ ) : null}
1357
+ </div>
1358
+ </div>
1359
+ )}
1360
+ renderSelectedValue={({ option, label }) =>
1361
+ option ? (
1362
+ <div className="flex min-w-0 items-center gap-2">
1363
+ <Avatar className="size-5 shrink-0">
1364
+ <AvatarImage
1365
+ src={option.avatarUrl ?? undefined}
1366
+ alt={option.title}
1367
+ />
1368
+ <AvatarFallback className="text-[10px]">
1369
+ {getInitials(option.title)}
1370
+ </AvatarFallback>
1371
+ </Avatar>
1372
+ <span className="truncate">{option.title}</span>
1373
+ </div>
1374
+ ) : (
1375
+ <span className="text-muted-foreground">{label}</span>
1376
+ )
1377
+ }
1255
1378
  placeholder={commonT('labels.notAssigned')}
1256
1379
  searchPlaceholder={t('placeholders.managerSearch')}
1257
- emptyLabel={commonT('labels.notAssigned')}
1380
+ emptySelectionLabel={commonT('labels.notAssigned')}
1381
+ valueType="number"
1382
+ clearable
1383
+ allowEmptySelection
1384
+ showCreateButton
1385
+ entityLabel={commonT('labels.manager').toLowerCase()}
1386
+ createActionLabel={`${commonT('actions.create')} ${commonT(
1387
+ 'labels.manager'
1388
+ ).toLowerCase()}`}
1389
+ createTitle={`${commonT('actions.create')} ${commonT(
1390
+ 'labels.manager'
1391
+ ).toLowerCase()}`}
1392
+ createDescription={collaboratorFormT(
1393
+ 'sections.employmentInfoCreateDescription'
1394
+ )}
1395
+ createFields={[
1396
+ {
1397
+ name: 'displayName',
1398
+ label: collaboratorFormT('fields.displayName'),
1399
+ placeholder: collaboratorFormT('fields.displayName'),
1400
+ required: true,
1401
+ },
1402
+ {
1403
+ name: 'weeklyCapacityHours',
1404
+ label: collaboratorFormT('fields.weeklyCapacityHours'),
1405
+ placeholder: '40',
1406
+ type: 'number',
1407
+ },
1408
+ ]}
1409
+ mapSearchToCreateValues={(search) => ({
1410
+ displayName: search,
1411
+ weeklyCapacityHours: '40',
1412
+ })}
1413
+ onCreate={createManagerCollaborator}
1258
1414
  onChange={(value) =>
1259
1415
  setForm((current) => ({
1260
1416
  ...current,
1261
- managerCollaboratorId: value,
1417
+ managerCollaboratorId: value ? String(value) : 'none',
1262
1418
  }))
1263
1419
  }
1264
1420
  />
@@ -1296,9 +1452,6 @@ export function ProjectFormScreen({
1296
1452
  <SelectItem value="active">
1297
1453
  {t('options.statuses.active')}
1298
1454
  </SelectItem>
1299
- <SelectItem value="at_risk">
1300
- {t('options.statuses.at_risk')}
1301
- </SelectItem>
1302
1455
  <SelectItem value="paused">
1303
1456
  {t('options.statuses.paused')}
1304
1457
  </SelectItem>
@@ -1355,321 +1508,299 @@ export function ProjectFormScreen({
1355
1508
 
1356
1509
  {!isCreateMode ? (
1357
1510
  <>
1358
- <section className="space-y-3">
1359
- <div className="space-y-0.5">
1360
- <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1511
+ <Tabs defaultValue="financials" className="space-y-3">
1512
+ <TabsList className="grid w-full grid-cols-2">
1513
+ <TabsTrigger value="financials">
1361
1514
  {t('sections.financials')}
1362
- </h3>
1363
- <p className="text-[11px] text-muted-foreground/80">
1364
- {t('sections.financialsDescription')}
1365
- </p>
1366
- </div>
1367
- <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-4">
1368
- <div className="min-w-0 space-y-2">
1369
- <FieldLabel label={commonT('labels.budget')} />
1370
- <InputMoney
1371
- value={
1372
- form.budgetAmount === '' ? '' : Number(form.budgetAmount)
1373
- }
1374
- onValueChange={(value) =>
1375
- setForm((current) => ({
1376
- ...current,
1377
- budgetAmount: value !== null ? String(value) : '',
1378
- }))
1379
- }
1380
- />
1381
- </div>
1382
- <div className="min-w-0 space-y-2">
1383
- <FieldLabel
1384
- label={commonT('labels.monthlyHourCap')}
1385
- hint={t('hints.monthlyHourCap')}
1386
- />
1387
- <Input
1388
- type="text"
1389
- inputMode="numeric"
1390
- placeholder={t('placeholders.monthlyHourCap')}
1391
- value={form.monthlyHourCap}
1392
- onChange={(event) => {
1393
- const raw = event.target.value.replace(/[^0-9]/g, '');
1394
- setForm((current) => ({
1395
- ...current,
1396
- monthlyHourCap: raw,
1397
- }));
1398
- }}
1399
- />
1515
+ </TabsTrigger>
1516
+ <TabsTrigger value="team">{t('sections.team')}</TabsTrigger>
1517
+ </TabsList>
1518
+
1519
+ <TabsContent value="financials" className="space-y-3">
1520
+ <div className="space-y-0.5">
1521
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1522
+ {t('sections.financials')}
1523
+ </h3>
1524
+ <p className="text-[11px] text-muted-foreground/80">
1525
+ {t('sections.financialsDescription')}
1526
+ </p>
1400
1527
  </div>
1401
- <div className="min-w-0 space-y-2">
1402
- <FieldLabel label={commonT('labels.billingModel')} />
1403
- <Select
1404
- value={form.billingModel}
1405
- onValueChange={(value) =>
1406
- setForm((current) => ({
1407
- ...current,
1408
- billingModel: value,
1409
- }))
1410
- }
1411
- >
1412
- <SelectTrigger className="w-full">
1413
- <SelectValue />
1414
- </SelectTrigger>
1415
- <SelectContent>
1416
- <SelectItem value="time_and_material">
1417
- {t('options.billingModels.time_and_material')}
1418
- </SelectItem>
1419
- <SelectItem value="monthly_retainer">
1420
- {t('options.billingModels.monthly_retainer')}
1421
- </SelectItem>
1422
- <SelectItem value="fixed_price">
1423
- {t('options.billingModels.fixed_price')}
1424
- </SelectItem>
1425
- </SelectContent>
1426
- </Select>
1528
+ <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-4">
1529
+ <div className="min-w-0 space-y-2">
1530
+ <FieldLabel label={commonT('labels.budget')} />
1531
+ <InputMoney
1532
+ value={
1533
+ form.budgetAmount === ''
1534
+ ? ''
1535
+ : Number(form.budgetAmount)
1536
+ }
1537
+ onValueChange={(value) =>
1538
+ setForm((current) => ({
1539
+ ...current,
1540
+ budgetAmount: value !== null ? String(value) : '',
1541
+ }))
1542
+ }
1543
+ />
1544
+ </div>
1545
+ <div className="min-w-0 space-y-2">
1546
+ <FieldLabel
1547
+ label={commonT('labels.monthlyHourCap')}
1548
+ hint={t('hints.monthlyHourCap')}
1549
+ />
1550
+ <Input
1551
+ type="text"
1552
+ inputMode="numeric"
1553
+ placeholder={t('placeholders.monthlyHourCap')}
1554
+ value={form.monthlyHourCap}
1555
+ onChange={(event) => {
1556
+ const raw = event.target.value.replace(/[^0-9]/g, '');
1557
+ setForm((current) => ({
1558
+ ...current,
1559
+ monthlyHourCap: raw,
1560
+ }));
1561
+ }}
1562
+ />
1563
+ </div>
1564
+ <div className="min-w-0 space-y-2">
1565
+ <FieldLabel label={commonT('labels.billingModel')} />
1566
+ <Select
1567
+ value={form.billingModel}
1568
+ onValueChange={(value) =>
1569
+ setForm((current) => ({
1570
+ ...current,
1571
+ billingModel: value,
1572
+ }))
1573
+ }
1574
+ >
1575
+ <SelectTrigger className="w-full">
1576
+ <SelectValue />
1577
+ </SelectTrigger>
1578
+ <SelectContent>
1579
+ <SelectItem value="time_and_material">
1580
+ {t('options.billingModels.time_and_material')}
1581
+ </SelectItem>
1582
+ <SelectItem value="monthly_retainer">
1583
+ {t('options.billingModels.monthly_retainer')}
1584
+ </SelectItem>
1585
+ <SelectItem value="fixed_price">
1586
+ {t('options.billingModels.fixed_price')}
1587
+ </SelectItem>
1588
+ </SelectContent>
1589
+ </Select>
1590
+ </div>
1591
+ <div className="min-w-0 space-y-2">
1592
+ <FieldLabel
1593
+ label={commonT('labels.contract')}
1594
+ hint={t('hints.contract')}
1595
+ />
1596
+ <ContractSelectWithCreate
1597
+ label=""
1598
+ value={form.contractId}
1599
+ contracts={availableContracts}
1600
+ selectPlaceholder={commonT('labels.notAssigned')}
1601
+ searchPlaceholder={t('placeholders.contractSearch')}
1602
+ onChange={(value) =>
1603
+ setForm((current) => ({
1604
+ ...current,
1605
+ contractId: value,
1606
+ }))
1607
+ }
1608
+ onCreated={async (contract) => {
1609
+ await refetchContracts();
1610
+ setForm((current) => ({
1611
+ ...current,
1612
+ contractId: contract?.id
1613
+ ? String(contract.id)
1614
+ : current.contractId,
1615
+ billingModel:
1616
+ contract?.billingModel ?? current.billingModel,
1617
+ monthlyHourCap:
1618
+ contract?.monthlyHourCap !== null &&
1619
+ contract?.monthlyHourCap !== undefined
1620
+ ? String(contract.monthlyHourCap)
1621
+ : current.monthlyHourCap,
1622
+ }));
1623
+ }}
1624
+ initialValues={{
1625
+ code: form.code ? `PRJ-${form.code}` : '',
1626
+ name: form.name ? `${form.name} Service Agreement` : '',
1627
+ clientName: form.clientName,
1628
+ contractCategory: 'client',
1629
+ contractType: 'service_agreement',
1630
+ signatureStatus: 'not_started',
1631
+ billingModel: form.billingModel,
1632
+ budgetAmount: form.budgetAmount,
1633
+ monthlyHourCap: form.monthlyHourCap,
1634
+ startDate: form.startDate,
1635
+ endDate: form.endDate,
1636
+ description: form.summary,
1637
+ contentHtml: '',
1638
+ }}
1639
+ />
1640
+ </div>
1427
1641
  </div>
1428
- <div className="min-w-0 space-y-2">
1429
- <FieldLabel
1430
- label={commonT('labels.contract')}
1431
- hint={t('hints.contract')}
1432
- />
1433
- <ContractSelectWithCreate
1434
- label=""
1435
- value={form.contractId}
1436
- contracts={availableContracts}
1437
- selectPlaceholder={commonT('labels.notAssigned')}
1438
- searchPlaceholder={t('placeholders.contractSearch')}
1439
- onChange={(value) =>
1440
- setForm((current) => ({ ...current, contractId: value }))
1441
- }
1442
- onCreated={async (contract) => {
1443
- await refetchContracts();
1444
- setForm((current) => ({
1445
- ...current,
1446
- contractId: contract?.id
1447
- ? String(contract.id)
1448
- : current.contractId,
1449
- billingModel:
1450
- contract?.billingModel ?? current.billingModel,
1451
- monthlyHourCap:
1452
- contract?.monthlyHourCap !== null &&
1453
- contract?.monthlyHourCap !== undefined
1454
- ? String(contract.monthlyHourCap)
1455
- : current.monthlyHourCap,
1456
- }));
1457
- }}
1458
- initialValues={{
1459
- code: form.code ? `PRJ-${form.code}` : '',
1460
- name: form.name ? `${form.name} Service Agreement` : '',
1461
- clientName: form.clientName,
1462
- contractCategory: 'client',
1463
- contractType: 'service_agreement',
1464
- signatureStatus: 'not_started',
1465
- billingModel: form.billingModel,
1466
- budgetAmount: form.budgetAmount,
1467
- monthlyHourCap: form.monthlyHourCap,
1468
- startDate: form.startDate,
1469
- endDate: form.endDate,
1470
- description: form.summary,
1471
- contentHtml: '',
1472
- }}
1473
- />
1474
- </div>
1475
- </div>
1476
- </section>
1477
-
1478
- <section className="space-y-3">
1479
- <div className="space-y-0.5">
1480
- <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1481
- {t('sections.team')}
1482
- </h3>
1483
- <p className="text-[11px] text-muted-foreground/80">
1484
- {t('sections.teamDescription', {
1485
- count: selectedAssignmentsCount,
1486
- })}
1487
- </p>
1488
- </div>
1489
- <div className="space-y-3">
1490
- <div className="relative">
1491
- <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
1492
- <Input
1493
- className="pl-9"
1494
- value={assignmentSearch}
1495
- placeholder={t('placeholders.assignmentSearch')}
1496
- onChange={(event) =>
1497
- setAssignmentSearch(event.target.value)
1498
- }
1499
- />
1642
+ </TabsContent>
1643
+
1644
+ <TabsContent value="team" className="space-y-3">
1645
+ <div className="space-y-0.5">
1646
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1647
+ {t('sections.team')}
1648
+ </h3>
1649
+ <p className="text-[11px] text-muted-foreground/80">
1650
+ {t('sections.teamDescription', {
1651
+ count: selectedAssignmentsCount,
1652
+ })}
1653
+ </p>
1500
1654
  </div>
1655
+ <div className="space-y-3">
1656
+ <div className="relative">
1657
+ <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
1658
+ <Input
1659
+ className="pl-9"
1660
+ value={assignmentSearch}
1661
+ placeholder={t('placeholders.assignmentSearch')}
1662
+ onChange={(event) =>
1663
+ setAssignmentSearch(event.target.value)
1664
+ }
1665
+ />
1666
+ </div>
1501
1667
 
1502
- <div className="space-y-2">
1503
- {filteredAssignments.map((assignment) => {
1504
- const collaborator = availableCollaborators.find(
1505
- (item) => item.id === assignment.collaboratorId
1506
- );
1507
- const assignmentIndex = form.teamAssignments.findIndex(
1508
- (item) =>
1509
- item.collaboratorId === assignment.collaboratorId
1510
- );
1511
- const roleError =
1512
- assignmentIndex >= 0
1513
- ? formMethods.formState.errors.teamAssignments?.[
1514
- assignmentIndex
1515
- ]?.roleLabel
1516
- : undefined;
1517
-
1518
- if (!collaborator) {
1519
- return null;
1520
- }
1668
+ <div className="space-y-2">
1669
+ {filteredAssignments.map((assignment) => {
1670
+ const collaborator = availableCollaborators.find(
1671
+ (item) => item.id === assignment.collaboratorId
1672
+ );
1521
1673
 
1522
- return (
1523
- <div
1524
- key={assignment.collaboratorId}
1525
- className="grid min-w-0 gap-2 rounded-lg border px-3 py-2 xl:grid-cols-[minmax(0,1.25fr)_minmax(0,1.2fr)_110px_110px]"
1526
- >
1527
- <label className="flex cursor-pointer items-start gap-3 py-1">
1528
- <Checkbox
1529
- checked={assignment.selected}
1530
- onCheckedChange={(checked) =>
1531
- updateAssignment(assignment.collaboratorId, {
1532
- selected: checked === true,
1533
- })
1534
- }
1535
- />
1536
- <div className="min-w-0">
1537
- <div className="truncate font-medium">
1538
- {collaborator.displayName}
1539
- </div>
1540
- <div className="truncate text-xs text-muted-foreground">
1541
- {[
1542
- collaborator.department,
1543
- collaborator.title,
1544
- collaborator.code,
1545
- ]
1546
- .filter(Boolean)
1547
- .join(' • ') || commonT('labels.notAvailable')}
1548
- </div>
1549
- </div>
1550
- </label>
1551
- <div className="space-y-1">
1552
- <DepartmentPicker
1553
- label=""
1554
- value={assignment.roleLabel}
1555
- options={projectRoleOptions}
1556
- disabled={!assignment.selected}
1557
- selectPlaceholder={t('placeholders.roleLabel')}
1558
- createDescription={t('fields.roleLabel')}
1559
- createPlaceholder={t(
1560
- 'placeholders.roleLabelCreate'
1561
- )}
1562
- onChange={(role) =>
1563
- updateAssignment(assignment.collaboratorId, {
1564
- projectRoleId: role.id
1565
- ? String(role.id)
1566
- : 'none',
1567
- roleLabel: role.name,
1568
- })
1569
- }
1570
- onCreate={createProjectRole}
1571
- />
1572
- {roleError?.message ? (
1573
- <p className="text-sm text-destructive">
1574
- {String(roleError.message)}
1575
- </p>
1576
- ) : null}
1577
- </div>
1578
- <Input
1579
- className="h-9 mt-2 self-start"
1580
- type="number"
1581
- min="0"
1582
- step="0.5"
1583
- placeholder={t('fields.weeklyHours')}
1584
- value={assignment.weeklyHours}
1585
- disabled={!assignment.selected}
1586
- onChange={(event) => {
1587
- const hours = event.target.value;
1588
- const updates: Partial<TeamAssignmentState> = {
1589
- weeklyHours: hours,
1590
- };
1591
- const cap = collaborator.weeklyCapacityHours;
1592
- if (cap && hours && Number(hours) > 0) {
1593
- const pct = Math.round(
1594
- (Number(hours) / cap) * 100
1595
- );
1596
- updates.allocationPercent = String(
1597
- Math.min(pct, 100)
1598
- );
1599
- }
1600
- updateAssignment(
1601
- assignment.collaboratorId,
1602
- updates
1603
- );
1604
- }}
1605
- />
1606
- <Input
1607
- className="h-9 mt-2 self-start"
1608
- type="text"
1609
- inputMode="decimal"
1610
- placeholder={t('fields.allocationPercent')}
1611
- value={assignment.allocationPercent}
1612
- disabled={!assignment.selected}
1613
- onChange={(event) => {
1614
- const pct = normalizePercentInput(
1615
- event.target.value
1616
- );
1617
- const updates: Partial<TeamAssignmentState> = {
1618
- allocationPercent: pct,
1619
- };
1620
- const cap = collaborator.weeklyCapacityHours;
1621
- if (cap && pct && Number(pct) > 0) {
1622
- const hours = Math.round(
1623
- (Number(pct) / 100) * cap
1624
- );
1625
- updates.weeklyHours = String(hours);
1626
- }
1627
- updateAssignment(
1628
- assignment.collaboratorId,
1629
- updates
1630
- );
1631
- }}
1632
- />
1633
- <div className="flex items-center gap-2 xl:col-span-4">
1634
- <div className="flex-1">
1635
- <label className="text-xs text-muted-foreground">
1636
- {t('fields.assignmentStartDate')}
1637
- </label>
1638
- <Input
1639
- className="h-9"
1640
- type="date"
1641
- value={assignment.startDate}
1642
- disabled={!assignment.selected}
1643
- onChange={(event) =>
1674
+ if (!collaborator) {
1675
+ return null;
1676
+ }
1677
+
1678
+ return (
1679
+ <div
1680
+ key={assignment.collaboratorId}
1681
+ className="grid min-w-0 gap-2 rounded-lg border px-3 py-2 xl:grid-cols-[minmax(0,2fr)_110px_110px]"
1682
+ >
1683
+ <label className="flex cursor-pointer items-start gap-3 py-1">
1684
+ <Checkbox
1685
+ checked={assignment.selected}
1686
+ onCheckedChange={(checked) =>
1644
1687
  updateAssignment(assignment.collaboratorId, {
1645
- startDate: event.target.value,
1688
+ selected: checked === true,
1646
1689
  })
1647
1690
  }
1648
1691
  />
1649
- </div>
1650
- <div className="flex-1">
1651
- <label className="text-xs text-muted-foreground">
1652
- {t('fields.assignmentEndDate')}
1653
- </label>
1654
- <Input
1655
- className="h-9"
1656
- type="date"
1657
- value={assignment.endDate}
1658
- disabled={!assignment.selected}
1659
- onChange={(event) =>
1660
- updateAssignment(assignment.collaboratorId, {
1661
- endDate: event.target.value,
1662
- })
1692
+ <div className="min-w-0">
1693
+ <div className="truncate font-medium">
1694
+ {collaborator.displayName}
1695
+ </div>
1696
+ <div className="truncate text-xs text-muted-foreground">
1697
+ {[
1698
+ collaborator.department,
1699
+ collaborator.title,
1700
+ collaborator.code,
1701
+ ]
1702
+ .filter(Boolean)
1703
+ .join(' • ') ||
1704
+ commonT('labels.notAvailable')}
1705
+ </div>
1706
+ </div>
1707
+ </label>
1708
+ <Input
1709
+ className="h-9 mt-2 self-start"
1710
+ type="number"
1711
+ min="0"
1712
+ step="0.5"
1713
+ placeholder={t('fields.weeklyHours')}
1714
+ value={assignment.weeklyHours}
1715
+ disabled={!assignment.selected}
1716
+ onChange={(event) => {
1717
+ const hours = event.target.value;
1718
+ const updates: Partial<TeamAssignmentState> = {
1719
+ weeklyHours: hours,
1720
+ };
1721
+ const cap = collaborator.weeklyCapacityHours;
1722
+ if (cap && hours && Number(hours) > 0) {
1723
+ const pct = Math.round(
1724
+ (Number(hours) / cap) * 100
1725
+ );
1726
+ updates.allocationPercent = String(
1727
+ Math.min(pct, 100)
1728
+ );
1663
1729
  }
1664
- />
1730
+ updateAssignment(
1731
+ assignment.collaboratorId,
1732
+ updates
1733
+ );
1734
+ }}
1735
+ />
1736
+ <Input
1737
+ className="h-9 mt-2 self-start"
1738
+ type="text"
1739
+ inputMode="decimal"
1740
+ placeholder={t('fields.allocationPercent')}
1741
+ value={assignment.allocationPercent}
1742
+ disabled={!assignment.selected}
1743
+ onChange={(event) => {
1744
+ const pct = normalizePercentInput(
1745
+ event.target.value
1746
+ );
1747
+ const updates: Partial<TeamAssignmentState> = {
1748
+ allocationPercent: pct,
1749
+ };
1750
+ const cap = collaborator.weeklyCapacityHours;
1751
+ if (cap && pct && Number(pct) > 0) {
1752
+ const hours = Math.round(
1753
+ (Number(pct) / 100) * cap
1754
+ );
1755
+ updates.weeklyHours = String(hours);
1756
+ }
1757
+ updateAssignment(
1758
+ assignment.collaboratorId,
1759
+ updates
1760
+ );
1761
+ }}
1762
+ />
1763
+ <div className="flex items-center gap-2 xl:col-span-4">
1764
+ <div className="flex-1">
1765
+ <label className="text-xs text-muted-foreground">
1766
+ {t('fields.assignmentStartDate')}
1767
+ </label>
1768
+ <Input
1769
+ className="h-9"
1770
+ type="date"
1771
+ value={assignment.startDate}
1772
+ disabled={!assignment.selected}
1773
+ onChange={(event) =>
1774
+ updateAssignment(assignment.collaboratorId, {
1775
+ startDate: event.target.value,
1776
+ })
1777
+ }
1778
+ />
1779
+ </div>
1780
+ <div className="flex-1">
1781
+ <label className="text-xs text-muted-foreground">
1782
+ {t('fields.assignmentEndDate')}
1783
+ </label>
1784
+ <Input
1785
+ className="h-9"
1786
+ type="date"
1787
+ value={assignment.endDate}
1788
+ disabled={!assignment.selected}
1789
+ onChange={(event) =>
1790
+ updateAssignment(assignment.collaboratorId, {
1791
+ endDate: event.target.value,
1792
+ })
1793
+ }
1794
+ />
1795
+ </div>
1665
1796
  </div>
1666
1797
  </div>
1667
- </div>
1668
- );
1669
- })}
1798
+ );
1799
+ })}
1800
+ </div>
1670
1801
  </div>
1671
- </div>
1672
- </section>
1802
+ </TabsContent>
1803
+ </Tabs>
1673
1804
  </>
1674
1805
  ) : (
1675
1806
  <section className="space-y-3 rounded-lg border border-dashed bg-muted/20 px-4 py-4">
@@ -1701,6 +1832,43 @@ export function ProjectFormScreen({
1701
1832
  </div>
1702
1833
  ) : null;
1703
1834
 
1835
+ const personSheet = (
1836
+ <PersonFormSheet
1837
+ open={personSheetOpen}
1838
+ person={personToEdit}
1839
+ contactTypes={contactTypes}
1840
+ documentTypes={documentTypes}
1841
+ onOpenChange={(nextOpen) => {
1842
+ setPersonSheetOpen(nextOpen);
1843
+ if (!nextOpen) {
1844
+ setPersonToEdit(null);
1845
+ }
1846
+ }}
1847
+ onSuccess={(person) => {
1848
+ const personLabel = resolvePersonDisplayName(person);
1849
+
1850
+ if (personSheetModeRef.current === 'create' && person?.id) {
1851
+ formMethods.setValue('clientPersonId', String(person.id), {
1852
+ shouldDirty: true,
1853
+ shouldTouch: true,
1854
+ shouldValidate: true,
1855
+ });
1856
+ }
1857
+
1858
+ if (personLabel) {
1859
+ formMethods.setValue('clientName', personLabel, {
1860
+ shouldDirty: true,
1861
+ shouldTouch: true,
1862
+ shouldValidate: true,
1863
+ });
1864
+ }
1865
+
1866
+ personSheetModeRef.current = 'edit';
1867
+ }}
1868
+ allowedTypes={['company']}
1869
+ />
1870
+ );
1871
+
1704
1872
  if (isSheetMode) {
1705
1873
  return (
1706
1874
  <div className="mt-6 space-y-4 pb-6">
@@ -1721,6 +1889,8 @@ export function ProjectFormScreen({
1721
1889
  submitLabel={commonT('actions.save')}
1722
1890
  submitSize="lg"
1723
1891
  />
1892
+
1893
+ {personSheet}
1724
1894
  </div>
1725
1895
  );
1726
1896
  }
@@ -1764,6 +1934,8 @@ export function ProjectFormScreen({
1764
1934
  {loadingState}
1765
1935
  {contractStatusState}
1766
1936
  </div>
1937
+
1938
+ {personSheet}
1767
1939
  </Page>
1768
1940
  );
1769
1941
  }