@hed-hog/operations 0.0.330 → 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 (281) 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 +34 -9
  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 +182 -268
  146. package/dist/operations.service.d.ts.map +1 -1
  147. package/dist/operations.service.js +2147 -1337
  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_item.yaml +25 -25
  153. package/hedhog/data/route.yaml +61 -0
  154. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -99
  155. package/hedhog/frontend/app/_components/collaborator-picker.tsx.ejs +158 -0
  156. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +314 -116
  157. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +434 -449
  158. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +289 -412
  159. package/hedhog/frontend/app/_components/project-file-attachments.tsx.ejs +371 -0
  160. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +426 -374
  161. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +803 -581
  162. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +4 -1
  163. package/hedhog/frontend/app/_components/task-form-fields.tsx.ejs +406 -0
  164. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -784
  165. package/hedhog/frontend/app/_components/task-info-display.tsx.ejs +137 -0
  166. package/hedhog/frontend/app/_components/timesheet-entry-create-sheet.tsx.ejs +306 -0
  167. package/hedhog/frontend/app/_lib/api.ts.ejs +480 -476
  168. package/hedhog/frontend/app/_lib/types.ts.ejs +66 -5
  169. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +0 -2
  170. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +43 -0
  171. package/hedhog/frontend/app/approvals/page.tsx.ejs +6 -1
  172. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +6 -1
  173. package/hedhog/frontend/app/collaborators/page.tsx.ejs +59 -8
  174. package/hedhog/frontend/app/contracts/page.tsx.ejs +29 -8
  175. package/hedhog/frontend/app/dashboard/widgets/CapacityDistribution.tsx.ejs +84 -0
  176. package/hedhog/frontend/app/dashboard/widgets/EffortByProject.tsx.ejs +85 -0
  177. package/hedhog/frontend/app/dashboard/widgets/HeadcountByArea.tsx.ejs +101 -0
  178. package/hedhog/frontend/app/dashboard/widgets/ManagedProjectsStatus.tsx.ejs +113 -0
  179. package/hedhog/frontend/app/dashboard/widgets/MyHoursPeriodKpi.tsx.ejs +87 -0
  180. package/hedhog/frontend/app/dashboard/widgets/MyOpenRequestsKpi.tsx.ejs +97 -0
  181. package/hedhog/frontend/app/dashboard/widgets/MyPendingRequestsList.tsx.ejs +99 -0
  182. package/hedhog/frontend/app/dashboard/widgets/MyProjectAllocationsKpi.tsx.ejs +78 -0
  183. package/hedhog/frontend/app/dashboard/widgets/MyQuickActions.tsx.ejs +130 -0
  184. package/hedhog/frontend/app/dashboard/widgets/MyRelevantDeadlines.tsx.ejs +144 -0
  185. package/hedhog/frontend/app/dashboard/widgets/MyTimesheetStatusKpi.tsx.ejs +78 -0
  186. package/hedhog/frontend/app/dashboard/widgets/MyWeeklyJourney.tsx.ejs +99 -0
  187. package/hedhog/frontend/app/dashboard/widgets/PortfolioCostsKpi.tsx.ejs +112 -0
  188. package/hedhog/frontend/app/dashboard/widgets/PortfolioEffortKpi.tsx.ejs +93 -0
  189. package/hedhog/frontend/app/dashboard/widgets/PortfolioProjectsKpi.tsx.ejs +96 -0
  190. package/hedhog/frontend/app/dashboard/widgets/PortfolioRiskKpi.tsx.ejs +115 -0
  191. package/hedhog/frontend/app/dashboard/widgets/ProjectStatusOverview.tsx.ejs +120 -0
  192. package/hedhog/frontend/app/dashboard/widgets/StrategicDeadlines.tsx.ejs +146 -0
  193. package/hedhog/frontend/app/dashboard/widgets/TeamApprovalQueue.tsx.ejs +108 -0
  194. package/hedhog/frontend/app/dashboard/widgets/TeamCapacityKpi.tsx.ejs +97 -0
  195. package/hedhog/frontend/app/dashboard/widgets/TeamHeadcountKpi.tsx.ejs +100 -0
  196. package/hedhog/frontend/app/dashboard/widgets/TeamHoursKpi.tsx.ejs +104 -0
  197. package/hedhog/frontend/app/dashboard/widgets/TeamPendingApprovalsKpi.tsx.ejs +110 -0
  198. package/hedhog/frontend/app/dashboard/widgets/TeamUtilizationOverview.tsx.ejs +115 -0
  199. package/hedhog/frontend/app/dashboard/widgets/TeamWorkloadAlerts.tsx.ejs +117 -0
  200. package/hedhog/frontend/app/dashboard/widgets/index.ts.ejs +26 -0
  201. package/hedhog/frontend/app/departments/page.tsx.ejs +6 -1
  202. package/hedhog/frontend/app/my-projects/page.tsx.ejs +14 -10
  203. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +328 -105
  204. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +58 -52
  205. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +58 -51
  206. package/hedhog/frontend/app/projects/page.tsx.ejs +376 -30
  207. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +6 -1
  208. package/hedhog/frontend/app/time-off/page.tsx.ejs +6 -1
  209. package/hedhog/frontend/app/timesheets/page.tsx.ejs +10 -4
  210. package/hedhog/frontend/messages/en.json +238 -46
  211. package/hedhog/frontend/messages/operations/en.json +61 -52
  212. package/hedhog/frontend/messages/operations/pt.json +59 -43
  213. package/hedhog/frontend/messages/pt.json +238 -46
  214. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +17 -0
  215. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +17 -0
  216. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +17 -0
  217. package/hedhog/frontend/widgets/index.ts.ejs +25 -0
  218. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +17 -0
  219. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +17 -0
  220. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +17 -0
  221. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +17 -0
  222. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +17 -0
  223. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +17 -0
  224. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +17 -0
  225. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +17 -0
  226. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +17 -0
  227. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +17 -0
  228. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +17 -0
  229. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +17 -0
  230. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +17 -0
  231. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +17 -0
  232. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +170 -0
  233. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +17 -0
  234. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +17 -0
  235. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +17 -0
  236. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +17 -0
  237. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +17 -0
  238. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +17 -0
  239. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +17 -0
  240. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +17 -0
  241. package/hedhog/table/operations_collaborator.yaml +8 -13
  242. package/hedhog/table/operations_project.yaml +1 -1
  243. package/hedhog/table/operations_project_file.yaml +23 -0
  244. package/hedhog/table/operations_task.yaml +76 -69
  245. package/hedhog/table/operations_task_activity.yaml +51 -0
  246. package/package.json +6 -5
  247. package/src/controllers/operations-projects.controller.ts +41 -8
  248. package/src/controllers/operations-tasks.controller.ts +156 -166
  249. package/src/dashboard/README.md +214 -0
  250. package/src/dashboard/components/DashboardLayout.tsx +131 -0
  251. package/src/dashboard/components/widget-registry.ts +255 -0
  252. package/src/dashboard/hooks/useDashboardData.ts +29 -0
  253. package/src/dashboard/types/widgets.types.ts +237 -0
  254. package/src/dashboard/widgets/CapacityDistribution.tsx +56 -0
  255. package/src/dashboard/widgets/EffortByProject.tsx +51 -0
  256. package/src/dashboard/widgets/HeadcountByArea.tsx +57 -0
  257. package/src/dashboard/widgets/ManagedProjectsStatus.tsx +53 -0
  258. package/src/dashboard/widgets/MyHoursPeriodKpi.tsx +87 -0
  259. package/src/dashboard/widgets/MyOpenRequestsKpi.tsx +51 -0
  260. package/src/dashboard/widgets/MyPendingRequestsList.tsx +63 -0
  261. package/src/dashboard/widgets/MyProjectAllocationsKpi.tsx +57 -0
  262. package/src/dashboard/widgets/MyQuickActions.tsx +62 -0
  263. package/src/dashboard/widgets/MyRelevantDeadlines.tsx +84 -0
  264. package/src/dashboard/widgets/MyTimesheetStatusKpi.tsx +65 -0
  265. package/src/dashboard/widgets/MyWeeklyJourney.tsx +57 -0
  266. package/src/dashboard/widgets/PortfolioCostsKpi.tsx +48 -0
  267. package/src/dashboard/widgets/PortfolioEffortKpi.tsx +41 -0
  268. package/src/dashboard/widgets/PortfolioRiskKpi.tsx +50 -0
  269. package/src/dashboard/widgets/ProjectStatusOverview.tsx +52 -0
  270. package/src/dashboard/widgets/StrategicDeadlines.tsx +93 -0
  271. package/src/dashboard/widgets/TeamApprovalQueue.tsx +70 -0
  272. package/src/dashboard/widgets/TeamCapacityKpi.tsx +50 -0
  273. package/src/dashboard/widgets/TeamHoursKpi.tsx +51 -0
  274. package/src/dashboard/widgets/TeamPendingApprovalsKpi.tsx +53 -0
  275. package/src/dashboard/widgets/TeamUtilizationOverview.tsx +62 -0
  276. package/src/dashboard/widgets/TeamWorkloadAlerts.tsx +81 -0
  277. package/src/dashboard/widgets/index.ts +26 -0
  278. package/src/dto/create-collaborator.dto.ts +4 -11
  279. package/src/index.ts +3 -0
  280. package/src/operations.service.spec.ts +988 -764
  281. package/src/operations.service.ts +4277 -2535
@@ -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;
@@ -191,8 +201,6 @@ function buildEmptyForm(
191
201
  teamAssignments: collaborators.map((collaborator) => ({
192
202
  collaboratorId: collaborator.id,
193
203
  selected: false,
194
- projectRoleId: 'none',
195
- roleLabel: '',
196
204
  weeklyHours: '',
197
205
  allocationPercent: '',
198
206
  status: 'active',
@@ -265,10 +273,6 @@ function toFormState(
265
273
  return {
266
274
  collaboratorId: collaborator.id,
267
275
  selected: Boolean(assignment),
268
- projectRoleId: assignment?.projectRoleId
269
- ? String(assignment.projectRoleId)
270
- : 'none',
271
- roleLabel: assignment?.roleLabel ?? '',
272
276
  weeklyHours:
273
277
  assignment?.weeklyHours !== null &&
274
278
  assignment?.weeklyHours !== undefined
@@ -645,6 +649,9 @@ export function ProjectFormScreen({
645
649
  const access = useOperationsAccess();
646
650
  const router = useRouter();
647
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);
648
655
  const isSheetMode = Boolean(onCancel);
649
656
  const isCreateMode = !projectId;
650
657
  const [codeAutoMode, setCodeAutoMode] = useState(isCreateMode);
@@ -684,8 +691,6 @@ export function ProjectFormScreen({
684
691
  z.object({
685
692
  collaboratorId: z.number(),
686
693
  selected: z.boolean(),
687
- projectRoleId: z.string(),
688
- roleLabel: z.string(),
689
694
  weeklyHours: z.string(),
690
695
  allocationPercent: z.string(),
691
696
  status: z.string(),
@@ -694,14 +699,8 @@ export function ProjectFormScreen({
694
699
  })
695
700
  )
696
701
  .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
- }
702
+ assignments.forEach((_assignment, _index) => {
703
+ // role validation removed
705
704
  });
706
705
  }),
707
706
  }),
@@ -756,18 +755,32 @@ export function ProjectFormScreen({
756
755
  fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
757
756
  });
758
757
 
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
- ),
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 ?? [],
771
784
  });
772
785
 
773
786
  const { data: project, isLoading: isLoadingProject } =
@@ -875,39 +888,6 @@ export function ProjectFormScreen({
875
888
  [availableCollaborators]
876
889
  );
877
890
 
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
891
  const selectedAssignmentsCount = useMemo(
912
892
  () =>
913
893
  form.teamAssignments.filter((assignment) => assignment.selected).length,
@@ -993,11 +973,6 @@ export function ProjectFormScreen({
993
973
  .filter((assignment) => assignment.selected)
994
974
  .map((assignment) => ({
995
975
  collaboratorId: assignment.collaboratorId,
996
- projectRoleId:
997
- assignment.projectRoleId === 'none'
998
- ? null
999
- : parseNumberInput(assignment.projectRoleId),
1000
- roleLabel: trimToNull(assignment.roleLabel),
1001
976
  weeklyHours: parseNumberInput(assignment.weeklyHours),
1002
977
  allocationPercent: parseNumberInput(assignment.allocationPercent),
1003
978
  status: assignment.status,
@@ -1047,6 +1022,44 @@ export function ProjectFormScreen({
1047
1022
  showToastHandler?.('error', t('messages.requiredFields'));
1048
1023
  };
1049
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
+
1050
1063
  const noAccessState = (
1051
1064
  <EmptyState
1052
1065
  icon={<FolderKanban className="size-12" />}
@@ -1171,6 +1184,12 @@ export function ProjectFormScreen({
1171
1184
  personId ? String(personId) : ''
1172
1185
  );
1173
1186
  }}
1187
+ showEditButton
1188
+ editAriaLabel={commonT('actions.editPersonCrm')}
1189
+ onEditSelection={(personId) =>
1190
+ void handleOpenPersonEditSheet(personId)
1191
+ }
1192
+ onCreateNew={() => handleOpenPersonCreateSheet()}
1174
1193
  personTypeFilter="all"
1175
1194
  createType="company"
1176
1195
  />
@@ -1296,9 +1315,6 @@ export function ProjectFormScreen({
1296
1315
  <SelectItem value="active">
1297
1316
  {t('options.statuses.active')}
1298
1317
  </SelectItem>
1299
- <SelectItem value="at_risk">
1300
- {t('options.statuses.at_risk')}
1301
- </SelectItem>
1302
1318
  <SelectItem value="paused">
1303
1319
  {t('options.statuses.paused')}
1304
1320
  </SelectItem>
@@ -1355,321 +1371,316 @@ export function ProjectFormScreen({
1355
1371
 
1356
1372
  {!isCreateMode ? (
1357
1373
  <>
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">
1374
+ <Tabs defaultValue="financials" className="space-y-3">
1375
+ <TabsList className="grid w-full grid-cols-3">
1376
+ <TabsTrigger value="financials">
1361
1377
  {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
- />
1400
- </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>
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>
1427
1391
  </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
- />
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>
1474
1507
  </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
- />
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>
1500
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>
1501
1533
 
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
- }
1534
+ <div className="space-y-2">
1535
+ {filteredAssignments.map((assignment) => {
1536
+ const collaborator = availableCollaborators.find(
1537
+ (item) => item.id === assignment.collaboratorId
1538
+ );
1521
1539
 
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) =>
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) =>
1644
1553
  updateAssignment(assignment.collaboratorId, {
1645
- startDate: event.target.value,
1554
+ selected: checked === true,
1646
1555
  })
1647
1556
  }
1648
1557
  />
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
- })
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
+ );
1663
1595
  }
1664
- />
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>
1665
1662
  </div>
1666
1663
  </div>
1667
- </div>
1668
- );
1669
- })}
1664
+ );
1665
+ })}
1666
+ </div>
1670
1667
  </div>
1671
- </div>
1672
- </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>
1673
1684
  </>
1674
1685
  ) : (
1675
1686
  <section className="space-y-3 rounded-lg border border-dashed bg-muted/20 px-4 py-4">
@@ -1701,6 +1712,43 @@ export function ProjectFormScreen({
1701
1712
  </div>
1702
1713
  ) : null;
1703
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
+
1704
1752
  if (isSheetMode) {
1705
1753
  return (
1706
1754
  <div className="mt-6 space-y-4 pb-6">
@@ -1721,6 +1769,8 @@ export function ProjectFormScreen({
1721
1769
  submitLabel={commonT('actions.save')}
1722
1770
  submitSize="lg"
1723
1771
  />
1772
+
1773
+ {personSheet}
1724
1774
  </div>
1725
1775
  );
1726
1776
  }
@@ -1764,6 +1814,8 @@ export function ProjectFormScreen({
1764
1814
  {loadingState}
1765
1815
  {contractStatusState}
1766
1816
  </div>
1817
+
1818
+ {personSheet}
1767
1819
  </Page>
1768
1820
  );
1769
1821
  }