@hed-hog/operations 0.0.329 → 0.0.331

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 (290) hide show
  1. package/README.md +5 -5
  2. package/dist/controllers/operations-collaborators.controller.d.ts +7 -216
  3. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  4. package/dist/controllers/operations-contracts.controller.d.ts +6 -6
  5. package/dist/controllers/operations-projects.controller.d.ts +25 -0
  6. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-projects.controller.js +48 -0
  8. package/dist/controllers/operations-projects.controller.js.map +1 -1
  9. package/dist/controllers/operations-reports.controller.d.ts +1 -1
  10. package/dist/controllers/operations-tasks.controller.d.ts +30 -5
  11. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  12. package/dist/controllers/operations-tasks.controller.js +43 -32
  13. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  14. package/dist/controllers/operations-timesheets.controller.d.ts +9 -9
  15. package/dist/dashboard/components/DashboardLayout.d.ts +30 -0
  16. package/dist/dashboard/components/DashboardLayout.d.ts.map +1 -0
  17. package/dist/dashboard/components/DashboardLayout.js +87 -0
  18. package/dist/dashboard/components/DashboardLayout.js.map +1 -0
  19. package/dist/dashboard/components/widget-registry.d.ts +23 -0
  20. package/dist/dashboard/components/widget-registry.d.ts.map +1 -0
  21. package/dist/dashboard/components/widget-registry.js +245 -0
  22. package/dist/dashboard/components/widget-registry.js.map +1 -0
  23. package/dist/dashboard/hooks/useDashboardData.d.ts +20 -0
  24. package/dist/dashboard/hooks/useDashboardData.d.ts.map +1 -0
  25. package/dist/dashboard/hooks/useDashboardData.js +24 -0
  26. package/dist/dashboard/hooks/useDashboardData.js.map +1 -0
  27. package/dist/dashboard/types/widgets.types.d.ts +233 -0
  28. package/dist/dashboard/types/widgets.types.d.ts.map +1 -0
  29. package/dist/dashboard/types/widgets.types.js +6 -0
  30. package/dist/dashboard/types/widgets.types.js.map +1 -0
  31. package/dist/dashboard/widgets/CapacityDistribution.d.ts +23 -0
  32. package/dist/dashboard/widgets/CapacityDistribution.d.ts.map +1 -0
  33. package/dist/dashboard/widgets/CapacityDistribution.js +11 -0
  34. package/dist/dashboard/widgets/CapacityDistribution.js.map +1 -0
  35. package/dist/dashboard/widgets/EffortByProject.d.ts +22 -0
  36. package/dist/dashboard/widgets/EffortByProject.d.ts.map +1 -0
  37. package/dist/dashboard/widgets/EffortByProject.js +11 -0
  38. package/dist/dashboard/widgets/EffortByProject.js.map +1 -0
  39. package/dist/dashboard/widgets/HeadcountByArea.d.ts +24 -0
  40. package/dist/dashboard/widgets/HeadcountByArea.d.ts.map +1 -0
  41. package/dist/dashboard/widgets/HeadcountByArea.js +11 -0
  42. package/dist/dashboard/widgets/HeadcountByArea.js.map +1 -0
  43. package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts +18 -0
  44. package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts.map +1 -0
  45. package/dist/dashboard/widgets/ManagedProjectsStatus.js +12 -0
  46. package/dist/dashboard/widgets/ManagedProjectsStatus.js.map +1 -0
  47. package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts +22 -0
  48. package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts.map +1 -0
  49. package/dist/dashboard/widgets/MyHoursPeriodKpi.js +12 -0
  50. package/dist/dashboard/widgets/MyHoursPeriodKpi.js.map +1 -0
  51. package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts +19 -0
  52. package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts.map +1 -0
  53. package/dist/dashboard/widgets/MyOpenRequestsKpi.js +17 -0
  54. package/dist/dashboard/widgets/MyOpenRequestsKpi.js.map +1 -0
  55. package/dist/dashboard/widgets/MyPendingRequestsList.d.ts +23 -0
  56. package/dist/dashboard/widgets/MyPendingRequestsList.d.ts.map +1 -0
  57. package/dist/dashboard/widgets/MyPendingRequestsList.js +14 -0
  58. package/dist/dashboard/widgets/MyPendingRequestsList.js.map +1 -0
  59. package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts +22 -0
  60. package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts.map +1 -0
  61. package/dist/dashboard/widgets/MyProjectAllocationsKpi.js +11 -0
  62. package/dist/dashboard/widgets/MyProjectAllocationsKpi.js.map +1 -0
  63. package/dist/dashboard/widgets/MyQuickActions.d.ts +23 -0
  64. package/dist/dashboard/widgets/MyQuickActions.d.ts.map +1 -0
  65. package/dist/dashboard/widgets/MyQuickActions.js +18 -0
  66. package/dist/dashboard/widgets/MyQuickActions.js.map +1 -0
  67. package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts +23 -0
  68. package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts.map +1 -0
  69. package/dist/dashboard/widgets/MyRelevantDeadlines.js +22 -0
  70. package/dist/dashboard/widgets/MyRelevantDeadlines.js.map +1 -0
  71. package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts +17 -0
  72. package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts.map +1 -0
  73. package/dist/dashboard/widgets/MyTimesheetStatusKpi.js +11 -0
  74. package/dist/dashboard/widgets/MyTimesheetStatusKpi.js.map +1 -0
  75. package/dist/dashboard/widgets/MyWeeklyJourney.d.ts +21 -0
  76. package/dist/dashboard/widgets/MyWeeklyJourney.d.ts.map +1 -0
  77. package/dist/dashboard/widgets/MyWeeklyJourney.js +19 -0
  78. package/dist/dashboard/widgets/MyWeeklyJourney.js.map +1 -0
  79. package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts +19 -0
  80. package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts.map +1 -0
  81. package/dist/dashboard/widgets/PortfolioCostsKpi.js +12 -0
  82. package/dist/dashboard/widgets/PortfolioCostsKpi.js.map +1 -0
  83. package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts +18 -0
  84. package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts.map +1 -0
  85. package/dist/dashboard/widgets/PortfolioEffortKpi.js +8 -0
  86. package/dist/dashboard/widgets/PortfolioEffortKpi.js.map +1 -0
  87. package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts +22 -0
  88. package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts.map +1 -0
  89. package/dist/dashboard/widgets/PortfolioProjectsKpi.js +56 -0
  90. package/dist/dashboard/widgets/PortfolioProjectsKpi.js.map +1 -0
  91. package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts +19 -0
  92. package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts.map +1 -0
  93. package/dist/dashboard/widgets/PortfolioRiskKpi.js +11 -0
  94. package/dist/dashboard/widgets/PortfolioRiskKpi.js.map +1 -0
  95. package/dist/dashboard/widgets/ProjectStatusOverview.d.ts +19 -0
  96. package/dist/dashboard/widgets/ProjectStatusOverview.d.ts.map +1 -0
  97. package/dist/dashboard/widgets/ProjectStatusOverview.js +18 -0
  98. package/dist/dashboard/widgets/ProjectStatusOverview.js.map +1 -0
  99. package/dist/dashboard/widgets/StrategicDeadlines.d.ts +24 -0
  100. package/dist/dashboard/widgets/StrategicDeadlines.d.ts.map +1 -0
  101. package/dist/dashboard/widgets/StrategicDeadlines.js +22 -0
  102. package/dist/dashboard/widgets/StrategicDeadlines.js.map +1 -0
  103. package/dist/dashboard/widgets/TeamApprovalQueue.d.ts +24 -0
  104. package/dist/dashboard/widgets/TeamApprovalQueue.d.ts.map +1 -0
  105. package/dist/dashboard/widgets/TeamApprovalQueue.js +12 -0
  106. package/dist/dashboard/widgets/TeamApprovalQueue.js.map +1 -0
  107. package/dist/dashboard/widgets/TeamCapacityKpi.d.ts +18 -0
  108. package/dist/dashboard/widgets/TeamCapacityKpi.d.ts.map +1 -0
  109. package/dist/dashboard/widgets/TeamCapacityKpi.js +19 -0
  110. package/dist/dashboard/widgets/TeamCapacityKpi.js.map +1 -0
  111. package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts +22 -0
  112. package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts.map +1 -0
  113. package/dist/dashboard/widgets/TeamHeadcountKpi.js +56 -0
  114. package/dist/dashboard/widgets/TeamHeadcountKpi.js.map +1 -0
  115. package/dist/dashboard/widgets/TeamHoursKpi.d.ts +19 -0
  116. package/dist/dashboard/widgets/TeamHoursKpi.d.ts.map +1 -0
  117. package/dist/dashboard/widgets/TeamHoursKpi.js +13 -0
  118. package/dist/dashboard/widgets/TeamHoursKpi.js.map +1 -0
  119. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts +20 -0
  120. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts.map +1 -0
  121. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js +11 -0
  122. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js.map +1 -0
  123. package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts +18 -0
  124. package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts.map +1 -0
  125. package/dist/dashboard/widgets/TeamUtilizationOverview.js +17 -0
  126. package/dist/dashboard/widgets/TeamUtilizationOverview.js.map +1 -0
  127. package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts +24 -0
  128. package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts.map +1 -0
  129. package/dist/dashboard/widgets/TeamWorkloadAlerts.js +19 -0
  130. package/dist/dashboard/widgets/TeamWorkloadAlerts.js.map +1 -0
  131. package/dist/dashboard/widgets/index.d.ts +24 -0
  132. package/dist/dashboard/widgets/index.d.ts.map +1 -0
  133. package/dist/dashboard/widgets/index.js +54 -0
  134. package/dist/dashboard/widgets/index.js.map +1 -0
  135. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  136. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  137. package/dist/dto/create-collaborator.dto.js +0 -6
  138. package/dist/dto/create-collaborator.dto.js.map +1 -1
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +2 -0
  142. package/dist/index.js.map +1 -1
  143. package/dist/operations.controller.d.ts +42 -0
  144. package/dist/operations.controller.d.ts.map +1 -1
  145. package/dist/operations.service.d.ts +178 -264
  146. package/dist/operations.service.d.ts.map +1 -1
  147. package/dist/operations.service.js +2170 -1340
  148. package/dist/operations.service.js.map +1 -1
  149. package/dist/operations.service.spec.js +345 -174
  150. package/dist/operations.service.spec.js.map +1 -1
  151. package/hedhog/data/dashboard_component.yaml +66 -0
  152. package/hedhog/data/dashboard_component_role.yaml +8 -8
  153. package/hedhog/data/dashboard_item.yaml +25 -25
  154. package/hedhog/data/dashboard_role.yaml +1 -1
  155. package/hedhog/data/menu.yaml +6 -16
  156. package/hedhog/data/role.yaml +1 -1
  157. package/hedhog/data/route.yaml +116 -55
  158. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +15 -9
  159. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -99
  160. package/hedhog/frontend/app/_components/collaborator-picker.tsx.ejs +158 -0
  161. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +314 -116
  162. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +434 -449
  163. package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +51 -81
  164. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +328 -423
  165. package/hedhog/frontend/app/_components/project-file-attachments.tsx.ejs +371 -0
  166. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +446 -377
  167. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +803 -581
  168. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +14 -9
  169. package/hedhog/frontend/app/_components/task-form-fields.tsx.ejs +406 -0
  170. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -784
  171. package/hedhog/frontend/app/_components/task-info-display.tsx.ejs +137 -0
  172. package/hedhog/frontend/app/_components/timesheet-entry-create-sheet.tsx.ejs +306 -0
  173. package/hedhog/frontend/app/_lib/api.ts.ejs +480 -476
  174. package/hedhog/frontend/app/_lib/hooks/use-values-visibility.ts.ejs +61 -0
  175. package/hedhog/frontend/app/_lib/types.ts.ejs +66 -5
  176. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +0 -2
  177. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +43 -0
  178. package/hedhog/frontend/app/approvals/page.tsx.ejs +11 -2
  179. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +6 -1
  180. package/hedhog/frontend/app/collaborators/page.tsx.ejs +127 -42
  181. package/hedhog/frontend/app/contracts/page.tsx.ejs +29 -8
  182. package/hedhog/frontend/app/dashboard/widgets/CapacityDistribution.tsx.ejs +84 -0
  183. package/hedhog/frontend/app/dashboard/widgets/EffortByProject.tsx.ejs +85 -0
  184. package/hedhog/frontend/app/dashboard/widgets/HeadcountByArea.tsx.ejs +101 -0
  185. package/hedhog/frontend/app/dashboard/widgets/ManagedProjectsStatus.tsx.ejs +113 -0
  186. package/hedhog/frontend/app/dashboard/widgets/MyHoursPeriodKpi.tsx.ejs +87 -0
  187. package/hedhog/frontend/app/dashboard/widgets/MyOpenRequestsKpi.tsx.ejs +97 -0
  188. package/hedhog/frontend/app/dashboard/widgets/MyPendingRequestsList.tsx.ejs +99 -0
  189. package/hedhog/frontend/app/dashboard/widgets/MyProjectAllocationsKpi.tsx.ejs +78 -0
  190. package/hedhog/frontend/app/dashboard/widgets/MyQuickActions.tsx.ejs +130 -0
  191. package/hedhog/frontend/app/dashboard/widgets/MyRelevantDeadlines.tsx.ejs +144 -0
  192. package/hedhog/frontend/app/dashboard/widgets/MyTimesheetStatusKpi.tsx.ejs +78 -0
  193. package/hedhog/frontend/app/dashboard/widgets/MyWeeklyJourney.tsx.ejs +99 -0
  194. package/hedhog/frontend/app/dashboard/widgets/PortfolioCostsKpi.tsx.ejs +112 -0
  195. package/hedhog/frontend/app/dashboard/widgets/PortfolioEffortKpi.tsx.ejs +93 -0
  196. package/hedhog/frontend/app/dashboard/widgets/PortfolioProjectsKpi.tsx.ejs +96 -0
  197. package/hedhog/frontend/app/dashboard/widgets/PortfolioRiskKpi.tsx.ejs +115 -0
  198. package/hedhog/frontend/app/dashboard/widgets/ProjectStatusOverview.tsx.ejs +120 -0
  199. package/hedhog/frontend/app/dashboard/widgets/StrategicDeadlines.tsx.ejs +146 -0
  200. package/hedhog/frontend/app/dashboard/widgets/TeamApprovalQueue.tsx.ejs +108 -0
  201. package/hedhog/frontend/app/dashboard/widgets/TeamCapacityKpi.tsx.ejs +97 -0
  202. package/hedhog/frontend/app/dashboard/widgets/TeamHeadcountKpi.tsx.ejs +100 -0
  203. package/hedhog/frontend/app/dashboard/widgets/TeamHoursKpi.tsx.ejs +104 -0
  204. package/hedhog/frontend/app/dashboard/widgets/TeamPendingApprovalsKpi.tsx.ejs +110 -0
  205. package/hedhog/frontend/app/dashboard/widgets/TeamUtilizationOverview.tsx.ejs +115 -0
  206. package/hedhog/frontend/app/dashboard/widgets/TeamWorkloadAlerts.tsx.ejs +117 -0
  207. package/hedhog/frontend/app/dashboard/widgets/index.ts.ejs +26 -0
  208. package/hedhog/frontend/app/departments/page.tsx.ejs +6 -1
  209. package/hedhog/frontend/app/my-projects/page.tsx.ejs +59 -16
  210. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +329 -106
  211. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +58 -52
  212. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +58 -51
  213. package/hedhog/frontend/app/projects/page.tsx.ejs +436 -35
  214. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +65 -52
  215. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +80 -82
  216. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +13 -2
  217. package/hedhog/frontend/app/time-off/page.tsx.ejs +6 -1
  218. package/hedhog/frontend/app/timesheets/page.tsx.ejs +10 -4
  219. package/hedhog/frontend/messages/en.json +460 -61
  220. package/hedhog/frontend/messages/operations/en.json +61 -52
  221. package/hedhog/frontend/messages/operations/pt.json +59 -43
  222. package/hedhog/frontend/messages/pt.json +460 -61
  223. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +17 -0
  224. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +17 -0
  225. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +17 -0
  226. package/hedhog/frontend/widgets/index.ts.ejs +25 -0
  227. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +17 -0
  228. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +17 -0
  229. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +17 -0
  230. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +17 -0
  231. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +17 -0
  232. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +17 -0
  233. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +17 -0
  234. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +17 -0
  235. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +17 -0
  236. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +17 -0
  237. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +17 -0
  238. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +17 -0
  239. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +17 -0
  240. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +17 -0
  241. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +170 -0
  242. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +17 -0
  243. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +17 -0
  244. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +17 -0
  245. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +17 -0
  246. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +17 -0
  247. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +17 -0
  248. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +17 -0
  249. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +17 -0
  250. package/hedhog/table/operations_collaborator.yaml +8 -13
  251. package/hedhog/table/operations_project.yaml +1 -1
  252. package/hedhog/table/operations_project_file.yaml +23 -0
  253. package/hedhog/table/operations_task.yaml +76 -69
  254. package/hedhog/table/operations_task_activity.yaml +51 -0
  255. package/package.json +7 -6
  256. package/src/controllers/operations-projects.controller.ts +41 -8
  257. package/src/controllers/operations-tasks.controller.ts +156 -166
  258. package/src/dashboard/README.md +214 -0
  259. package/src/dashboard/components/DashboardLayout.tsx +131 -0
  260. package/src/dashboard/components/widget-registry.ts +255 -0
  261. package/src/dashboard/hooks/useDashboardData.ts +29 -0
  262. package/src/dashboard/types/widgets.types.ts +237 -0
  263. package/src/dashboard/widgets/CapacityDistribution.tsx +56 -0
  264. package/src/dashboard/widgets/EffortByProject.tsx +51 -0
  265. package/src/dashboard/widgets/HeadcountByArea.tsx +57 -0
  266. package/src/dashboard/widgets/ManagedProjectsStatus.tsx +53 -0
  267. package/src/dashboard/widgets/MyHoursPeriodKpi.tsx +87 -0
  268. package/src/dashboard/widgets/MyOpenRequestsKpi.tsx +51 -0
  269. package/src/dashboard/widgets/MyPendingRequestsList.tsx +63 -0
  270. package/src/dashboard/widgets/MyProjectAllocationsKpi.tsx +57 -0
  271. package/src/dashboard/widgets/MyQuickActions.tsx +62 -0
  272. package/src/dashboard/widgets/MyRelevantDeadlines.tsx +84 -0
  273. package/src/dashboard/widgets/MyTimesheetStatusKpi.tsx +65 -0
  274. package/src/dashboard/widgets/MyWeeklyJourney.tsx +57 -0
  275. package/src/dashboard/widgets/PortfolioCostsKpi.tsx +48 -0
  276. package/src/dashboard/widgets/PortfolioEffortKpi.tsx +41 -0
  277. package/src/dashboard/widgets/PortfolioRiskKpi.tsx +50 -0
  278. package/src/dashboard/widgets/ProjectStatusOverview.tsx +52 -0
  279. package/src/dashboard/widgets/StrategicDeadlines.tsx +93 -0
  280. package/src/dashboard/widgets/TeamApprovalQueue.tsx +70 -0
  281. package/src/dashboard/widgets/TeamCapacityKpi.tsx +50 -0
  282. package/src/dashboard/widgets/TeamHoursKpi.tsx +51 -0
  283. package/src/dashboard/widgets/TeamPendingApprovalsKpi.tsx +53 -0
  284. package/src/dashboard/widgets/TeamUtilizationOverview.tsx +62 -0
  285. package/src/dashboard/widgets/TeamWorkloadAlerts.tsx +81 -0
  286. package/src/dashboard/widgets/index.ts +26 -0
  287. package/src/dto/create-collaborator.dto.ts +4 -11
  288. package/src/index.ts +3 -0
  289. package/src/operations.service.spec.ts +988 -764
  290. package/src/operations.service.ts +4300 -2538
@@ -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';
@@ -43,6 +49,12 @@ import {
43
49
  SheetHeader,
44
50
  SheetTitle,
45
51
  } from '@/components/ui/sheet';
52
+ import {
53
+ Tabs,
54
+ TabsContent,
55
+ TabsList,
56
+ TabsTrigger,
57
+ } from '@/components/ui/tabs';
46
58
  import { Textarea } from '@/components/ui/textarea';
47
59
  import {
48
60
  Tooltip,
@@ -66,7 +78,7 @@ import {
66
78
  import { useTranslations } from 'next-intl';
67
79
  import Link from 'next/link';
68
80
  import { useRouter } from 'next/navigation';
69
- import { useEffect, useMemo, useState } from 'react';
81
+ import { useEffect, useMemo, useRef, useState } from 'react';
70
82
  import { useForm } from 'react-hook-form';
71
83
  import { z } from 'zod';
72
84
  import { PersonPicker } from '../../contact/_components/person-picker';
@@ -80,7 +92,6 @@ import type {
80
92
  OperationsCollaborator,
81
93
  OperationsContract,
82
94
  OperationsProjectDetails,
83
- OperationsProjectRole,
84
95
  } from '../_lib/types';
85
96
  import { formatEnumLabel } from '../_lib/utils/format';
86
97
  import {
@@ -91,14 +102,13 @@ import {
91
102
  import { ContractFormScreen } from './contract-form-screen';
92
103
  import { DepartmentPicker } from './department-picker';
93
104
  import { OperationsHeader } from './operations-header';
105
+ import { ProjectFileAttachments } from './project-file-attachments';
94
106
 
95
107
  const OPTION_PAGE_SIZE = 12;
96
108
 
97
109
  type TeamAssignmentState = {
98
110
  collaboratorId: number;
99
111
  selected: boolean;
100
- projectRoleId: string;
101
- roleLabel: string;
102
112
  weeklyHours: string;
103
113
  allocationPercent: string;
104
114
  status: string;
@@ -111,6 +121,7 @@ type ProjectFormState = {
111
121
  managerCollaboratorId: string;
112
122
  code: string;
113
123
  name: string;
124
+ clientPersonId: string;
114
125
  clientName: string;
115
126
  summary: string;
116
127
  status: string;
@@ -173,6 +184,7 @@ function buildEmptyForm(
173
184
  managerCollaboratorId: 'none',
174
185
  code: '',
175
186
  name: '',
187
+ clientPersonId: '',
176
188
  clientName: '',
177
189
  summary: '',
178
190
  status: 'planning',
@@ -189,8 +201,6 @@ function buildEmptyForm(
189
201
  teamAssignments: collaborators.map((collaborator) => ({
190
202
  collaboratorId: collaborator.id,
191
203
  selected: false,
192
- projectRoleId: 'none',
193
- roleLabel: '',
194
204
  weeklyHours: '',
195
205
  allocationPercent: '',
196
206
  status: 'active',
@@ -232,6 +242,9 @@ function toFormState(
232
242
  : 'none',
233
243
  code: project.code ?? '',
234
244
  name: project.name ?? '',
245
+ clientPersonId: project.clientPersonId
246
+ ? String(project.clientPersonId)
247
+ : '',
235
248
  clientName: project.clientName ?? '',
236
249
  summary: project.summary ?? '',
237
250
  status: project.status ?? 'planning',
@@ -260,10 +273,6 @@ function toFormState(
260
273
  return {
261
274
  collaboratorId: collaborator.id,
262
275
  selected: Boolean(assignment),
263
- projectRoleId: assignment?.projectRoleId
264
- ? String(assignment.projectRoleId)
265
- : 'none',
266
- roleLabel: assignment?.roleLabel ?? '',
267
276
  weeklyHours:
268
277
  assignment?.weeklyHours !== null &&
269
278
  assignment?.weeklyHours !== undefined
@@ -640,6 +649,9 @@ export function ProjectFormScreen({
640
649
  const access = useOperationsAccess();
641
650
  const router = useRouter();
642
651
  const [assignmentSearch, setAssignmentSearch] = useState('');
652
+ const personSheetModeRef = useRef<'create' | 'edit'>('edit');
653
+ const [personSheetOpen, setPersonSheetOpen] = useState(false);
654
+ const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
643
655
  const isSheetMode = Boolean(onCancel);
644
656
  const isCreateMode = !projectId;
645
657
  const [codeAutoMode, setCodeAutoMode] = useState(isCreateMode);
@@ -651,6 +663,7 @@ export function ProjectFormScreen({
651
663
  managerCollaboratorId: z.string(),
652
664
  code: z.string().trim().min(1, t('messages.requiredFields')),
653
665
  name: z.string().trim().min(1, t('messages.requiredFields')),
666
+ clientPersonId: z.string(),
654
667
  clientName: z.string().trim().min(1, t('messages.requiredFields')),
655
668
  summary: z.string(),
656
669
  status: z.string(),
@@ -678,8 +691,6 @@ export function ProjectFormScreen({
678
691
  z.object({
679
692
  collaboratorId: z.number(),
680
693
  selected: z.boolean(),
681
- projectRoleId: z.string(),
682
- roleLabel: z.string(),
683
694
  weeklyHours: z.string(),
684
695
  allocationPercent: z.string(),
685
696
  status: z.string(),
@@ -688,14 +699,8 @@ export function ProjectFormScreen({
688
699
  })
689
700
  )
690
701
  .superRefine((assignments, ctx) => {
691
- assignments.forEach((assignment, index) => {
692
- if (assignment.selected && !assignment.roleLabel.trim()) {
693
- ctx.addIssue({
694
- code: z.ZodIssueCode.custom,
695
- message: t('messages.roleRequired'),
696
- path: [index, 'roleLabel'],
697
- });
698
- }
702
+ assignments.forEach((_assignment, _index) => {
703
+ // role validation removed
699
704
  });
700
705
  }),
701
706
  }),
@@ -750,18 +755,32 @@ export function ProjectFormScreen({
750
755
  fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
751
756
  });
752
757
 
753
- const { data: projectRoles = [], refetch: refetchProjectRoles } = useQuery<
754
- OperationsProjectRole[]
755
- >({
756
- queryKey: ['operations-project-form-project-roles', currentLocaleCode],
757
- enabled: access.isDirector,
758
- staleTime: 0,
759
- refetchOnMount: 'always',
760
- queryFn: () =>
761
- fetchOperations<OperationsProjectRole[]>(
762
- request,
763
- '/operations/project-roles'
764
- ),
758
+ const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
759
+ queryKey: ['contact-person-contact-types', currentLocaleCode],
760
+ enabled: personSheetOpen,
761
+ queryFn: async () => {
762
+ const response = await request<{ data: ContactTypeOption[] }>({
763
+ url: '/person-contact-type?pageSize=100',
764
+ method: 'GET',
765
+ });
766
+
767
+ return response.data.data || [];
768
+ },
769
+ placeholderData: (previous) => previous ?? [],
770
+ });
771
+
772
+ const { data: documentTypes = [] } = useQuery<DocumentTypeOption[]>({
773
+ queryKey: ['contact-person-document-types', currentLocaleCode],
774
+ enabled: personSheetOpen,
775
+ queryFn: async () => {
776
+ const response = await request<{ data: DocumentTypeOption[] }>({
777
+ url: '/person-document-type?pageSize=100',
778
+ method: 'GET',
779
+ });
780
+
781
+ return response.data.data || [];
782
+ },
783
+ placeholderData: (previous) => previous ?? [],
765
784
  });
766
785
 
767
786
  const { data: project, isLoading: isLoadingProject } =
@@ -869,39 +888,6 @@ export function ProjectFormScreen({
869
888
  [availableCollaborators]
870
889
  );
871
890
 
872
- const projectRoleOptions = useMemo(
873
- () =>
874
- projectRoles.map((role) => ({
875
- id: role.id,
876
- name: role.name,
877
- code: role.code ?? null,
878
- description: role.description ?? null,
879
- })),
880
- [projectRoles]
881
- );
882
-
883
- const createProjectRole = async (projectRoleName: string) => {
884
- try {
885
- const createdRole = await mutateOperations<OperationsProjectRole>(
886
- request,
887
- '/operations/project-roles',
888
- 'POST',
889
- {
890
- name: projectRoleName,
891
- }
892
- );
893
-
894
- await refetchProjectRoles();
895
- return createdRole;
896
- } catch (error) {
897
- showToastHandler?.(
898
- 'error',
899
- getOperationsErrorMessage(error, t('messages.projectRoleSaveError'))
900
- );
901
- return null;
902
- }
903
- };
904
-
905
891
  const selectedAssignmentsCount = useMemo(
906
892
  () =>
907
893
  form.teamAssignments.filter((assignment) => assignment.selected).length,
@@ -965,6 +951,9 @@ export function ProjectFormScreen({
965
951
  values.managerCollaboratorId === 'none'
966
952
  ? null
967
953
  : parseNumberInput(values.managerCollaboratorId),
954
+ clientPersonId: values.clientPersonId
955
+ ? parseNumberInput(values.clientPersonId)
956
+ : null,
968
957
  code: values.code.trim(),
969
958
  name: values.name.trim(),
970
959
  clientName: trimToNull(values.clientName),
@@ -984,11 +973,6 @@ export function ProjectFormScreen({
984
973
  .filter((assignment) => assignment.selected)
985
974
  .map((assignment) => ({
986
975
  collaboratorId: assignment.collaboratorId,
987
- projectRoleId:
988
- assignment.projectRoleId === 'none'
989
- ? null
990
- : parseNumberInput(assignment.projectRoleId),
991
- roleLabel: trimToNull(assignment.roleLabel),
992
976
  weeklyHours: parseNumberInput(assignment.weeklyHours),
993
977
  allocationPercent: parseNumberInput(assignment.allocationPercent),
994
978
  status: assignment.status,
@@ -1038,6 +1022,44 @@ export function ProjectFormScreen({
1038
1022
  showToastHandler?.('error', t('messages.requiredFields'));
1039
1023
  };
1040
1024
 
1025
+ const resolvePersonDisplayName = (person?: Person | null) => {
1026
+ const tradeName = person?.trade_name?.trim();
1027
+ if (tradeName) {
1028
+ return tradeName;
1029
+ }
1030
+
1031
+ return person?.name?.trim() || '';
1032
+ };
1033
+
1034
+ const handleOpenPersonEditSheet = async (personId?: number | null) => {
1035
+ const targetPersonId =
1036
+ personId && personId > 0
1037
+ ? personId
1038
+ : parseNumberInput(formMethods.getValues('clientPersonId'));
1039
+
1040
+ if (!targetPersonId) {
1041
+ return;
1042
+ }
1043
+
1044
+ try {
1045
+ const response = await request<Person>({
1046
+ url: `/person/${targetPersonId}`,
1047
+ method: 'GET',
1048
+ });
1049
+ personSheetModeRef.current = 'edit';
1050
+ setPersonToEdit(response.data);
1051
+ setPersonSheetOpen(true);
1052
+ } catch {
1053
+ showToastHandler?.('error', t('messages.updateError'));
1054
+ }
1055
+ };
1056
+
1057
+ const handleOpenPersonCreateSheet = () => {
1058
+ personSheetModeRef.current = 'create';
1059
+ setPersonToEdit(null);
1060
+ setPersonSheetOpen(true);
1061
+ };
1062
+
1041
1063
  const noAccessState = (
1042
1064
  <EmptyState
1043
1065
  icon={<FolderKanban className="size-12" />}
@@ -1148,12 +1170,26 @@ export function ProjectFormScreen({
1148
1170
  <PersonPicker
1149
1171
  label=""
1150
1172
  entityLabel={t('fields.clientName')}
1151
- value={null}
1173
+ value={
1174
+ formMethods.watch('clientPersonId')
1175
+ ? Number(formMethods.watch('clientPersonId'))
1176
+ : null
1177
+ }
1152
1178
  initialSelectedLabel={field.value}
1153
1179
  selectPlaceholder={t('placeholders.clientName')}
1154
- onChange={(_, personName) =>
1155
- field.onChange(personName ?? '')
1180
+ onChange={(personId, personName) => {
1181
+ field.onChange(personName ?? '');
1182
+ formMethods.setValue(
1183
+ 'clientPersonId',
1184
+ personId ? String(personId) : ''
1185
+ );
1186
+ }}
1187
+ showEditButton
1188
+ editAriaLabel={commonT('actions.editPersonCrm')}
1189
+ onEditSelection={(personId) =>
1190
+ void handleOpenPersonEditSheet(personId)
1156
1191
  }
1192
+ onCreateNew={() => handleOpenPersonCreateSheet()}
1157
1193
  personTypeFilter="all"
1158
1194
  createType="company"
1159
1195
  />
@@ -1279,9 +1315,6 @@ export function ProjectFormScreen({
1279
1315
  <SelectItem value="active">
1280
1316
  {t('options.statuses.active')}
1281
1317
  </SelectItem>
1282
- <SelectItem value="at_risk">
1283
- {t('options.statuses.at_risk')}
1284
- </SelectItem>
1285
1318
  <SelectItem value="paused">
1286
1319
  {t('options.statuses.paused')}
1287
1320
  </SelectItem>
@@ -1338,321 +1371,316 @@ export function ProjectFormScreen({
1338
1371
 
1339
1372
  {!isCreateMode ? (
1340
1373
  <>
1341
- <section className="space-y-3">
1342
- <div className="space-y-0.5">
1343
- <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1374
+ <Tabs defaultValue="financials" className="space-y-3">
1375
+ <TabsList className="grid w-full grid-cols-3">
1376
+ <TabsTrigger value="financials">
1344
1377
  {t('sections.financials')}
1345
- </h3>
1346
- <p className="text-[11px] text-muted-foreground/80">
1347
- {t('sections.financialsDescription')}
1348
- </p>
1349
- </div>
1350
- <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-4">
1351
- <div className="min-w-0 space-y-2">
1352
- <FieldLabel label={commonT('labels.budget')} />
1353
- <InputMoney
1354
- value={
1355
- form.budgetAmount === '' ? '' : Number(form.budgetAmount)
1356
- }
1357
- onValueChange={(value) =>
1358
- setForm((current) => ({
1359
- ...current,
1360
- budgetAmount: value !== null ? String(value) : '',
1361
- }))
1362
- }
1363
- />
1364
- </div>
1365
- <div className="min-w-0 space-y-2">
1366
- <FieldLabel
1367
- label={commonT('labels.monthlyHourCap')}
1368
- hint={t('hints.monthlyHourCap')}
1369
- />
1370
- <Input
1371
- type="text"
1372
- inputMode="numeric"
1373
- placeholder={t('placeholders.monthlyHourCap')}
1374
- value={form.monthlyHourCap}
1375
- onChange={(event) => {
1376
- const raw = event.target.value.replace(/[^0-9]/g, '');
1377
- setForm((current) => ({
1378
- ...current,
1379
- monthlyHourCap: raw,
1380
- }));
1381
- }}
1382
- />
1383
- </div>
1384
- <div className="min-w-0 space-y-2">
1385
- <FieldLabel label={commonT('labels.billingModel')} />
1386
- <Select
1387
- value={form.billingModel}
1388
- onValueChange={(value) =>
1389
- setForm((current) => ({
1390
- ...current,
1391
- billingModel: value,
1392
- }))
1393
- }
1394
- >
1395
- <SelectTrigger className="w-full">
1396
- <SelectValue />
1397
- </SelectTrigger>
1398
- <SelectContent>
1399
- <SelectItem value="time_and_material">
1400
- {t('options.billingModels.time_and_material')}
1401
- </SelectItem>
1402
- <SelectItem value="monthly_retainer">
1403
- {t('options.billingModels.monthly_retainer')}
1404
- </SelectItem>
1405
- <SelectItem value="fixed_price">
1406
- {t('options.billingModels.fixed_price')}
1407
- </SelectItem>
1408
- </SelectContent>
1409
- </Select>
1378
+ </TabsTrigger>
1379
+ <TabsTrigger value="team">{t('sections.team')}</TabsTrigger>
1380
+ <TabsTrigger value="files">{t('sections.files')}</TabsTrigger>
1381
+ </TabsList>
1382
+
1383
+ <TabsContent value="financials" className="space-y-3">
1384
+ <div className="space-y-0.5">
1385
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1386
+ {t('sections.financials')}
1387
+ </h3>
1388
+ <p className="text-[11px] text-muted-foreground/80">
1389
+ {t('sections.financialsDescription')}
1390
+ </p>
1410
1391
  </div>
1411
- <div className="min-w-0 space-y-2">
1412
- <FieldLabel
1413
- label={commonT('labels.contract')}
1414
- hint={t('hints.contract')}
1415
- />
1416
- <ContractSelectWithCreate
1417
- label=""
1418
- value={form.contractId}
1419
- contracts={availableContracts}
1420
- selectPlaceholder={commonT('labels.notAssigned')}
1421
- searchPlaceholder={t('placeholders.contractSearch')}
1422
- onChange={(value) =>
1423
- setForm((current) => ({ ...current, contractId: value }))
1424
- }
1425
- onCreated={async (contract) => {
1426
- await refetchContracts();
1427
- setForm((current) => ({
1428
- ...current,
1429
- contractId: contract?.id
1430
- ? String(contract.id)
1431
- : current.contractId,
1432
- billingModel:
1433
- contract?.billingModel ?? current.billingModel,
1434
- monthlyHourCap:
1435
- contract?.monthlyHourCap !== null &&
1436
- contract?.monthlyHourCap !== undefined
1437
- ? String(contract.monthlyHourCap)
1438
- : current.monthlyHourCap,
1439
- }));
1440
- }}
1441
- initialValues={{
1442
- code: form.code ? `PRJ-${form.code}` : '',
1443
- name: form.name ? `${form.name} Service Agreement` : '',
1444
- clientName: form.clientName,
1445
- contractCategory: 'client',
1446
- contractType: 'service_agreement',
1447
- signatureStatus: 'not_started',
1448
- billingModel: form.billingModel,
1449
- budgetAmount: form.budgetAmount,
1450
- monthlyHourCap: form.monthlyHourCap,
1451
- startDate: form.startDate,
1452
- endDate: form.endDate,
1453
- description: form.summary,
1454
- contentHtml: '',
1455
- }}
1456
- />
1392
+ <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-4">
1393
+ <div className="min-w-0 space-y-2">
1394
+ <FieldLabel label={commonT('labels.budget')} />
1395
+ <InputMoney
1396
+ value={
1397
+ form.budgetAmount === ''
1398
+ ? ''
1399
+ : Number(form.budgetAmount)
1400
+ }
1401
+ onValueChange={(value) =>
1402
+ setForm((current) => ({
1403
+ ...current,
1404
+ budgetAmount: value !== null ? String(value) : '',
1405
+ }))
1406
+ }
1407
+ />
1408
+ </div>
1409
+ <div className="min-w-0 space-y-2">
1410
+ <FieldLabel
1411
+ label={commonT('labels.monthlyHourCap')}
1412
+ hint={t('hints.monthlyHourCap')}
1413
+ />
1414
+ <Input
1415
+ type="text"
1416
+ inputMode="numeric"
1417
+ placeholder={t('placeholders.monthlyHourCap')}
1418
+ value={form.monthlyHourCap}
1419
+ onChange={(event) => {
1420
+ const raw = event.target.value.replace(/[^0-9]/g, '');
1421
+ setForm((current) => ({
1422
+ ...current,
1423
+ monthlyHourCap: raw,
1424
+ }));
1425
+ }}
1426
+ />
1427
+ </div>
1428
+ <div className="min-w-0 space-y-2">
1429
+ <FieldLabel label={commonT('labels.billingModel')} />
1430
+ <Select
1431
+ value={form.billingModel}
1432
+ onValueChange={(value) =>
1433
+ setForm((current) => ({
1434
+ ...current,
1435
+ billingModel: value,
1436
+ }))
1437
+ }
1438
+ >
1439
+ <SelectTrigger className="w-full">
1440
+ <SelectValue />
1441
+ </SelectTrigger>
1442
+ <SelectContent>
1443
+ <SelectItem value="time_and_material">
1444
+ {t('options.billingModels.time_and_material')}
1445
+ </SelectItem>
1446
+ <SelectItem value="monthly_retainer">
1447
+ {t('options.billingModels.monthly_retainer')}
1448
+ </SelectItem>
1449
+ <SelectItem value="fixed_price">
1450
+ {t('options.billingModels.fixed_price')}
1451
+ </SelectItem>
1452
+ </SelectContent>
1453
+ </Select>
1454
+ </div>
1455
+ <div className="min-w-0 space-y-2">
1456
+ <FieldLabel
1457
+ label={commonT('labels.contract')}
1458
+ hint={t('hints.contract')}
1459
+ />
1460
+ <ContractSelectWithCreate
1461
+ label=""
1462
+ value={form.contractId}
1463
+ contracts={availableContracts}
1464
+ selectPlaceholder={commonT('labels.notAssigned')}
1465
+ searchPlaceholder={t('placeholders.contractSearch')}
1466
+ onChange={(value) =>
1467
+ setForm((current) => ({
1468
+ ...current,
1469
+ contractId: value,
1470
+ }))
1471
+ }
1472
+ onCreated={async (contract) => {
1473
+ await refetchContracts();
1474
+ setForm((current) => ({
1475
+ ...current,
1476
+ contractId: contract?.id
1477
+ ? String(contract.id)
1478
+ : current.contractId,
1479
+ billingModel:
1480
+ contract?.billingModel ?? current.billingModel,
1481
+ monthlyHourCap:
1482
+ contract?.monthlyHourCap !== null &&
1483
+ contract?.monthlyHourCap !== undefined
1484
+ ? String(contract.monthlyHourCap)
1485
+ : current.monthlyHourCap,
1486
+ }));
1487
+ }}
1488
+ initialValues={{
1489
+ code: form.code ? `PRJ-${form.code}` : '',
1490
+ name: form.name
1491
+ ? `${form.name} Service Agreement`
1492
+ : '',
1493
+ clientName: form.clientName,
1494
+ contractCategory: 'client',
1495
+ contractType: 'service_agreement',
1496
+ signatureStatus: 'not_started',
1497
+ billingModel: form.billingModel,
1498
+ budgetAmount: form.budgetAmount,
1499
+ monthlyHourCap: form.monthlyHourCap,
1500
+ startDate: form.startDate,
1501
+ endDate: form.endDate,
1502
+ description: form.summary,
1503
+ contentHtml: '',
1504
+ }}
1505
+ />
1506
+ </div>
1457
1507
  </div>
1458
- </div>
1459
- </section>
1460
-
1461
- <section className="space-y-3">
1462
- <div className="space-y-0.5">
1463
- <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1464
- {t('sections.team')}
1465
- </h3>
1466
- <p className="text-[11px] text-muted-foreground/80">
1467
- {t('sections.teamDescription', {
1468
- count: selectedAssignmentsCount,
1469
- })}
1470
- </p>
1471
- </div>
1472
- <div className="space-y-3">
1473
- <div className="relative">
1474
- <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
1475
- <Input
1476
- className="pl-9"
1477
- value={assignmentSearch}
1478
- placeholder={t('placeholders.assignmentSearch')}
1479
- onChange={(event) =>
1480
- setAssignmentSearch(event.target.value)
1481
- }
1482
- />
1508
+ </TabsContent>
1509
+
1510
+ <TabsContent value="team" className="space-y-3">
1511
+ <div className="space-y-0.5">
1512
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1513
+ {t('sections.team')}
1514
+ </h3>
1515
+ <p className="text-[11px] text-muted-foreground/80">
1516
+ {t('sections.teamDescription', {
1517
+ count: selectedAssignmentsCount,
1518
+ })}
1519
+ </p>
1483
1520
  </div>
1521
+ <div className="space-y-3">
1522
+ <div className="relative">
1523
+ <Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
1524
+ <Input
1525
+ className="pl-9"
1526
+ value={assignmentSearch}
1527
+ placeholder={t('placeholders.assignmentSearch')}
1528
+ onChange={(event) =>
1529
+ setAssignmentSearch(event.target.value)
1530
+ }
1531
+ />
1532
+ </div>
1484
1533
 
1485
- <div className="space-y-2">
1486
- {filteredAssignments.map((assignment) => {
1487
- const collaborator = availableCollaborators.find(
1488
- (item) => item.id === assignment.collaboratorId
1489
- );
1490
- const assignmentIndex = form.teamAssignments.findIndex(
1491
- (item) =>
1492
- item.collaboratorId === assignment.collaboratorId
1493
- );
1494
- const roleError =
1495
- assignmentIndex >= 0
1496
- ? formMethods.formState.errors.teamAssignments?.[
1497
- assignmentIndex
1498
- ]?.roleLabel
1499
- : undefined;
1500
-
1501
- if (!collaborator) {
1502
- return null;
1503
- }
1534
+ <div className="space-y-2">
1535
+ {filteredAssignments.map((assignment) => {
1536
+ const collaborator = availableCollaborators.find(
1537
+ (item) => item.id === assignment.collaboratorId
1538
+ );
1504
1539
 
1505
- return (
1506
- <div
1507
- key={assignment.collaboratorId}
1508
- 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]"
1509
- >
1510
- <label className="flex cursor-pointer items-start gap-3 py-1">
1511
- <Checkbox
1512
- checked={assignment.selected}
1513
- onCheckedChange={(checked) =>
1514
- updateAssignment(assignment.collaboratorId, {
1515
- selected: checked === true,
1516
- })
1517
- }
1518
- />
1519
- <div className="min-w-0">
1520
- <div className="truncate font-medium">
1521
- {collaborator.displayName}
1522
- </div>
1523
- <div className="truncate text-xs text-muted-foreground">
1524
- {[
1525
- collaborator.department,
1526
- collaborator.title,
1527
- collaborator.code,
1528
- ]
1529
- .filter(Boolean)
1530
- .join(' • ') || commonT('labels.notAvailable')}
1531
- </div>
1532
- </div>
1533
- </label>
1534
- <div className="space-y-1">
1535
- <DepartmentPicker
1536
- label=""
1537
- value={assignment.roleLabel}
1538
- options={projectRoleOptions}
1539
- disabled={!assignment.selected}
1540
- selectPlaceholder={t('placeholders.roleLabel')}
1541
- createDescription={t('fields.roleLabel')}
1542
- createPlaceholder={t(
1543
- 'placeholders.roleLabelCreate'
1544
- )}
1545
- onChange={(role) =>
1546
- updateAssignment(assignment.collaboratorId, {
1547
- projectRoleId: role.id
1548
- ? String(role.id)
1549
- : 'none',
1550
- roleLabel: role.name,
1551
- })
1552
- }
1553
- onCreate={createProjectRole}
1554
- />
1555
- {roleError?.message ? (
1556
- <p className="text-sm text-destructive">
1557
- {String(roleError.message)}
1558
- </p>
1559
- ) : null}
1560
- </div>
1561
- <Input
1562
- className="h-9 mt-2 self-start"
1563
- type="number"
1564
- min="0"
1565
- step="0.5"
1566
- placeholder={t('fields.weeklyHours')}
1567
- value={assignment.weeklyHours}
1568
- disabled={!assignment.selected}
1569
- onChange={(event) => {
1570
- const hours = event.target.value;
1571
- const updates: Partial<TeamAssignmentState> = {
1572
- weeklyHours: hours,
1573
- };
1574
- const cap = collaborator.weeklyCapacityHours;
1575
- if (cap && hours && Number(hours) > 0) {
1576
- const pct = Math.round(
1577
- (Number(hours) / cap) * 100
1578
- );
1579
- updates.allocationPercent = String(
1580
- Math.min(pct, 100)
1581
- );
1582
- }
1583
- updateAssignment(
1584
- assignment.collaboratorId,
1585
- updates
1586
- );
1587
- }}
1588
- />
1589
- <Input
1590
- className="h-9 mt-2 self-start"
1591
- type="text"
1592
- inputMode="decimal"
1593
- placeholder={t('fields.allocationPercent')}
1594
- value={assignment.allocationPercent}
1595
- disabled={!assignment.selected}
1596
- onChange={(event) => {
1597
- const pct = normalizePercentInput(
1598
- event.target.value
1599
- );
1600
- const updates: Partial<TeamAssignmentState> = {
1601
- allocationPercent: pct,
1602
- };
1603
- const cap = collaborator.weeklyCapacityHours;
1604
- if (cap && pct && Number(pct) > 0) {
1605
- const hours = Math.round(
1606
- (Number(pct) / 100) * cap
1607
- );
1608
- updates.weeklyHours = String(hours);
1609
- }
1610
- updateAssignment(
1611
- assignment.collaboratorId,
1612
- updates
1613
- );
1614
- }}
1615
- />
1616
- <div className="flex items-center gap-2 xl:col-span-4">
1617
- <div className="flex-1">
1618
- <label className="text-xs text-muted-foreground">
1619
- {t('fields.assignmentStartDate')}
1620
- </label>
1621
- <Input
1622
- className="h-9"
1623
- type="date"
1624
- value={assignment.startDate}
1625
- disabled={!assignment.selected}
1626
- onChange={(event) =>
1540
+ if (!collaborator) {
1541
+ return null;
1542
+ }
1543
+
1544
+ return (
1545
+ <div
1546
+ key={assignment.collaboratorId}
1547
+ className="grid min-w-0 gap-2 rounded-lg border px-3 py-2 xl:grid-cols-[minmax(0,2fr)_110px_110px]"
1548
+ >
1549
+ <label className="flex cursor-pointer items-start gap-3 py-1">
1550
+ <Checkbox
1551
+ checked={assignment.selected}
1552
+ onCheckedChange={(checked) =>
1627
1553
  updateAssignment(assignment.collaboratorId, {
1628
- startDate: event.target.value,
1554
+ selected: checked === true,
1629
1555
  })
1630
1556
  }
1631
1557
  />
1632
- </div>
1633
- <div className="flex-1">
1634
- <label className="text-xs text-muted-foreground">
1635
- {t('fields.assignmentEndDate')}
1636
- </label>
1637
- <Input
1638
- className="h-9"
1639
- type="date"
1640
- value={assignment.endDate}
1641
- disabled={!assignment.selected}
1642
- onChange={(event) =>
1643
- updateAssignment(assignment.collaboratorId, {
1644
- endDate: event.target.value,
1645
- })
1558
+ <div className="min-w-0">
1559
+ <div className="truncate font-medium">
1560
+ {collaborator.displayName}
1561
+ </div>
1562
+ <div className="truncate text-xs text-muted-foreground">
1563
+ {[
1564
+ collaborator.department,
1565
+ collaborator.title,
1566
+ collaborator.code,
1567
+ ]
1568
+ .filter(Boolean)
1569
+ .join(' • ') ||
1570
+ commonT('labels.notAvailable')}
1571
+ </div>
1572
+ </div>
1573
+ </label>
1574
+ <Input
1575
+ className="h-9 mt-2 self-start"
1576
+ type="number"
1577
+ min="0"
1578
+ step="0.5"
1579
+ placeholder={t('fields.weeklyHours')}
1580
+ value={assignment.weeklyHours}
1581
+ disabled={!assignment.selected}
1582
+ onChange={(event) => {
1583
+ const hours = event.target.value;
1584
+ const updates: Partial<TeamAssignmentState> = {
1585
+ weeklyHours: hours,
1586
+ };
1587
+ const cap = collaborator.weeklyCapacityHours;
1588
+ if (cap && hours && Number(hours) > 0) {
1589
+ const pct = Math.round(
1590
+ (Number(hours) / cap) * 100
1591
+ );
1592
+ updates.allocationPercent = String(
1593
+ Math.min(pct, 100)
1594
+ );
1646
1595
  }
1647
- />
1596
+ updateAssignment(
1597
+ assignment.collaboratorId,
1598
+ updates
1599
+ );
1600
+ }}
1601
+ />
1602
+ <Input
1603
+ className="h-9 mt-2 self-start"
1604
+ type="text"
1605
+ inputMode="decimal"
1606
+ placeholder={t('fields.allocationPercent')}
1607
+ value={assignment.allocationPercent}
1608
+ disabled={!assignment.selected}
1609
+ onChange={(event) => {
1610
+ const pct = normalizePercentInput(
1611
+ event.target.value
1612
+ );
1613
+ const updates: Partial<TeamAssignmentState> = {
1614
+ allocationPercent: pct,
1615
+ };
1616
+ const cap = collaborator.weeklyCapacityHours;
1617
+ if (cap && pct && Number(pct) > 0) {
1618
+ const hours = Math.round(
1619
+ (Number(pct) / 100) * cap
1620
+ );
1621
+ updates.weeklyHours = String(hours);
1622
+ }
1623
+ updateAssignment(
1624
+ assignment.collaboratorId,
1625
+ updates
1626
+ );
1627
+ }}
1628
+ />
1629
+ <div className="flex items-center gap-2 xl:col-span-4">
1630
+ <div className="flex-1">
1631
+ <label className="text-xs text-muted-foreground">
1632
+ {t('fields.assignmentStartDate')}
1633
+ </label>
1634
+ <Input
1635
+ className="h-9"
1636
+ type="date"
1637
+ value={assignment.startDate}
1638
+ disabled={!assignment.selected}
1639
+ onChange={(event) =>
1640
+ updateAssignment(assignment.collaboratorId, {
1641
+ startDate: event.target.value,
1642
+ })
1643
+ }
1644
+ />
1645
+ </div>
1646
+ <div className="flex-1">
1647
+ <label className="text-xs text-muted-foreground">
1648
+ {t('fields.assignmentEndDate')}
1649
+ </label>
1650
+ <Input
1651
+ className="h-9"
1652
+ type="date"
1653
+ value={assignment.endDate}
1654
+ disabled={!assignment.selected}
1655
+ onChange={(event) =>
1656
+ updateAssignment(assignment.collaboratorId, {
1657
+ endDate: event.target.value,
1658
+ })
1659
+ }
1660
+ />
1661
+ </div>
1648
1662
  </div>
1649
1663
  </div>
1650
- </div>
1651
- );
1652
- })}
1664
+ );
1665
+ })}
1666
+ </div>
1653
1667
  </div>
1654
- </div>
1655
- </section>
1668
+ </TabsContent>
1669
+
1670
+ <TabsContent value="files" className="space-y-3">
1671
+ <div className="space-y-0.5">
1672
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1673
+ {t('sections.files')}
1674
+ </h3>
1675
+ <p className="text-[11px] text-muted-foreground/80">
1676
+ {t('sections.filesDescription')}
1677
+ </p>
1678
+ </div>
1679
+ {projectId ? (
1680
+ <ProjectFileAttachments projectId={projectId} />
1681
+ ) : null}
1682
+ </TabsContent>
1683
+ </Tabs>
1656
1684
  </>
1657
1685
  ) : (
1658
1686
  <section className="space-y-3 rounded-lg border border-dashed bg-muted/20 px-4 py-4">
@@ -1684,6 +1712,43 @@ export function ProjectFormScreen({
1684
1712
  </div>
1685
1713
  ) : null;
1686
1714
 
1715
+ const personSheet = (
1716
+ <PersonFormSheet
1717
+ open={personSheetOpen}
1718
+ person={personToEdit}
1719
+ contactTypes={contactTypes}
1720
+ documentTypes={documentTypes}
1721
+ onOpenChange={(nextOpen) => {
1722
+ setPersonSheetOpen(nextOpen);
1723
+ if (!nextOpen) {
1724
+ setPersonToEdit(null);
1725
+ }
1726
+ }}
1727
+ onSuccess={(person) => {
1728
+ const personLabel = resolvePersonDisplayName(person);
1729
+
1730
+ if (personSheetModeRef.current === 'create' && person?.id) {
1731
+ formMethods.setValue('clientPersonId', String(person.id), {
1732
+ shouldDirty: true,
1733
+ shouldTouch: true,
1734
+ shouldValidate: true,
1735
+ });
1736
+ }
1737
+
1738
+ if (personLabel) {
1739
+ formMethods.setValue('clientName', personLabel, {
1740
+ shouldDirty: true,
1741
+ shouldTouch: true,
1742
+ shouldValidate: true,
1743
+ });
1744
+ }
1745
+
1746
+ personSheetModeRef.current = 'edit';
1747
+ }}
1748
+ allowedTypes={['company']}
1749
+ />
1750
+ );
1751
+
1687
1752
  if (isSheetMode) {
1688
1753
  return (
1689
1754
  <div className="mt-6 space-y-4 pb-6">
@@ -1704,6 +1769,8 @@ export function ProjectFormScreen({
1704
1769
  submitLabel={commonT('actions.save')}
1705
1770
  submitSize="lg"
1706
1771
  />
1772
+
1773
+ {personSheet}
1707
1774
  </div>
1708
1775
  );
1709
1776
  }
@@ -1747,6 +1814,8 @@ export function ProjectFormScreen({
1747
1814
  {loadingState}
1748
1815
  {contractStatusState}
1749
1816
  </div>
1817
+
1818
+ {personSheet}
1750
1819
  </Page>
1751
1820
  );
1752
1821
  }