@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,450 +1,435 @@
1
- 'use client';
2
-
3
- import { Button } from '@/components/ui/button';
4
- import { EntityPicker } from '@/components/ui/entity-picker';
5
- import { Input } from '@/components/ui/input';
6
- import {
7
- Sheet,
8
- SheetContent,
9
- SheetDescription,
10
- SheetHeader,
11
- SheetTitle,
12
- } from '@/components/ui/sheet';
13
- import { Check, Loader2, Pencil, Plus, X } from 'lucide-react';
14
- import { useTranslations } from 'next-intl';
15
- import { useState } from 'react';
16
- import { fetchOperations, mutateOperations } from '../_lib/api';
17
- import type {
18
- OperationsCollaboratorDetails,
19
- OperationsProject,
20
- PaginatedResponse,
21
- } from '../_lib/types';
22
- import {
23
- formatDateRange,
24
- formatEnumLabel,
25
- getStatusBadgeClass,
26
- } from '../_lib/utils/format';
27
- import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
28
- import { ProjectFormScreen } from './project-form-screen';
29
- import { StatusBadge } from './status-badge';
30
-
31
- const PAGE_SIZE = 10;
32
-
33
- type AssignmentEditState = {
34
- roleLabel: string;
35
- weeklyHours: string;
36
- allocationPercent: string;
37
- startDate: string;
38
- endDate: string;
39
- };
40
-
41
- type ProjectAssignmentsTabProps = {
42
- collaborator: OperationsCollaboratorDetails | null;
43
- request: Parameters<typeof mutateOperations>[0];
44
- onUpdated: () => void;
45
- disabled?: boolean;
46
- };
47
-
48
- export function ProjectAssignmentsTab({
49
- collaborator,
50
- request,
51
- onUpdated,
52
- disabled,
53
- }: ProjectAssignmentsTabProps) {
54
- const commonT = useTranslations('operations.Common');
55
- const detailsT = useTranslations('operations.CollaboratorDetailsPage');
56
-
57
- const [page, setPage] = useState(0);
58
- const [editingId, setEditingId] = useState<number | null>(null);
59
- const [editState, setEditState] = useState<AssignmentEditState | null>(null);
60
- const [saving, setSaving] = useState(false);
61
- const [adding, setAdding] = useState(false);
62
- const [createSheetOpen, setCreateSheetOpen] = useState(false);
63
- const [pickerKey, setPickerKey] = useState(0);
64
-
65
- const projects = collaborator?.assignedProjects ?? [];
66
- const assignedIds = new Set(projects.map((p) => p.id));
67
- const totalPages = Math.ceil(projects.length / PAGE_SIZE);
68
- const visibleProjects = projects.slice(
69
- page * PAGE_SIZE,
70
- (page + 1) * PAGE_SIZE
71
- );
72
-
73
- const startEditing = (
74
- project: OperationsCollaboratorDetails['assignedProjects'][number]
75
- ) => {
76
- setEditingId(project.id);
77
- setEditState({
78
- roleLabel: project.roleLabel ?? '',
79
- weeklyHours:
80
- project.weeklyHours != null ? String(project.weeklyHours) : '',
81
- allocationPercent:
82
- project.allocationPercent != null
83
- ? String(project.allocationPercent)
84
- : '',
85
- startDate: project.startDate
86
- ? String(project.startDate).slice(0, 10)
87
- : '',
88
- endDate: project.endDate ? String(project.endDate).slice(0, 10) : '',
89
- });
90
- };
91
-
92
- const cancelEditing = () => {
93
- setEditingId(null);
94
- setEditState(null);
95
- };
96
-
97
- const saveEditing = async (projectId: number) => {
98
- if (!collaborator || !editState) return;
99
- setSaving(true);
100
- try {
101
- await mutateOperations(
102
- request,
103
- `/operations/collaborators/${collaborator.id}/projects/${projectId}`,
104
- 'PATCH',
105
- {
106
- roleLabel: trimToNull(editState.roleLabel),
107
- weeklyHours: parseNumberInput(editState.weeklyHours),
108
- allocationPercent: parseNumberInput(editState.allocationPercent),
109
- startDate: trimToNull(editState.startDate),
110
- endDate: trimToNull(editState.endDate),
111
- }
112
- );
113
- setEditingId(null);
114
- setEditState(null);
115
- onUpdated();
116
- } catch {
117
- /* errors surfaced by onUpdated not triggering */
118
- } finally {
119
- setSaving(false);
120
- }
121
- };
122
-
123
- const assignProject = async (projectId: number) => {
124
- if (!collaborator) return;
125
- setAdding(true);
126
- try {
127
- await mutateOperations(
128
- request,
129
- `/operations/collaborators/${collaborator.id}/projects`,
130
- 'POST',
131
- { projectId }
132
- );
133
- setPickerKey((k) => k + 1);
134
- onUpdated();
135
- } finally {
136
- setAdding(false);
137
- }
138
- };
139
-
140
- return (
141
- <div className="space-y-3">
142
- {!disabled ? (
143
- <div className="flex items-center gap-2">
144
- <div className="min-w-0 flex-1">
145
- <EntityPicker<OperationsProject>
146
- key={pickerKey}
147
- placeholder={detailsT('addProject')}
148
- searchPlaceholder={detailsT('searchProject')}
149
- showCreateButton={false}
150
- clearable={false}
151
- disabled={!collaborator || adding}
152
- loadOptions={async ({ page: p, pageSize: ps, search }) => {
153
- const params = new URLSearchParams({
154
- page: String(p),
155
- pageSize: String(ps),
156
- });
157
- if (search.trim()) params.set('search', search.trim());
158
- const res = await fetchOperations<
159
- PaginatedResponse<OperationsProject>
160
- >(request, `/operations/projects?${params.toString()}`);
161
- return {
162
- items: res.data.filter((proj) => !assignedIds.has(proj.id)),
163
- hasMore:
164
- (res.page ?? p) * (res.pageSize ?? ps) <
165
- (res.total ?? 0),
166
- };
167
- }}
168
- getOptionValue={(opt) => opt.id}
169
- getOptionLabel={(opt) => opt.name}
170
- getOptionDescription={(opt) =>
171
- [opt.code, opt.clientName].filter(Boolean).join(' • ') ||
172
- undefined
173
- }
174
- onChange={(value) => {
175
- if (value != null) void assignProject(Number(value));
176
- }}
177
- />
178
- </div>
179
- <Button
180
- type="button"
181
- variant="outline"
182
- size="icon"
183
- className="shrink-0"
184
- title={detailsT('createProject')}
185
- onClick={() => setCreateSheetOpen(true)}
186
- >
187
- {adding ? (
188
- <Loader2 className="size-4 animate-spin" />
189
- ) : (
190
- <Plus className="size-4" />
191
- )}
192
- </Button>
193
- </div>
194
- ) : null}
195
-
196
- {projects.length === 0 ? (
197
- <p className="text-sm text-muted-foreground">{detailsT('noProjects')}</p>
198
- ) : (
199
- <>
200
- <div className="space-y-2">
201
- {visibleProjects.map((project) => {
202
- const isEditing = editingId === project.id;
203
-
204
- if (isEditing && editState) {
205
- return (
206
- <div
207
- key={project.id}
208
- className="space-y-2 rounded-lg border px-3 py-2.5"
209
- >
210
- <div className="flex items-center justify-between gap-2">
211
- <div className="min-w-0">
212
- <div className="truncate text-sm font-medium">
213
- {project.name}
214
- </div>
215
- {project.code ? (
216
- <div className="text-xs text-muted-foreground">
217
- {project.code}
218
- </div>
219
- ) : null}
220
- </div>
221
- <StatusBadge
222
- label={formatEnumLabel(project.status)}
223
- className={getStatusBadgeClass(project.status)}
224
- />
225
- </div>
226
-
227
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
228
- <div className="col-span-2 space-y-1 sm:col-span-1">
229
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
230
- {commonT('labels.role')}
231
- </div>
232
- <Input
233
- className="h-7 text-xs"
234
- value={editState.roleLabel}
235
- onChange={(e) =>
236
- setEditState(
237
- (s) => s && { ...s, roleLabel: e.target.value }
238
- )
239
- }
240
- />
241
- </div>
242
- <div className="space-y-1">
243
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
244
- {commonT('labels.weeklyCapacity')}
245
- </div>
246
- <Input
247
- className="h-7 text-xs"
248
- type="number"
249
- min="0"
250
- step="0.5"
251
- value={editState.weeklyHours}
252
- onChange={(e) =>
253
- setEditState(
254
- (s) => s && { ...s, weeklyHours: e.target.value }
255
- )
256
- }
257
- />
258
- </div>
259
- <div className="space-y-1">
260
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
261
- {commonT('labels.allocationPercent')}
262
- </div>
263
- <div className="relative">
264
- <Input
265
- className="h-7 pr-5 text-xs"
266
- type="number"
267
- min="0"
268
- max="100"
269
- step="1"
270
- value={editState.allocationPercent}
271
- onChange={(e) =>
272
- setEditState(
273
- (s) =>
274
- s && {
275
- ...s,
276
- allocationPercent: e.target.value,
277
- }
278
- )
279
- }
280
- />
281
- <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground">
282
- %
283
- </span>
284
- </div>
285
- </div>
286
- <div className="space-y-1">
287
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
288
- {commonT('labels.startDate')}
289
- </div>
290
- <Input
291
- className="h-7 text-xs"
292
- type="date"
293
- value={editState.startDate}
294
- onChange={(e) =>
295
- setEditState(
296
- (s) => s && { ...s, startDate: e.target.value }
297
- )
298
- }
299
- />
300
- </div>
301
- <div className="space-y-1">
302
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
303
- {commonT('labels.endDate')}
304
- </div>
305
- <Input
306
- className="h-7 text-xs"
307
- type="date"
308
- value={editState.endDate}
309
- onChange={(e) =>
310
- setEditState(
311
- (s) => s && { ...s, endDate: e.target.value }
312
- )
313
- }
314
- />
315
- </div>
316
- </div>
317
-
318
- <div className="flex justify-end gap-1.5 pt-1">
319
- <Button
320
- type="button"
321
- size="sm"
322
- variant="ghost"
323
- className="h-7 px-2 text-xs"
324
- onClick={cancelEditing}
325
- disabled={saving}
326
- >
327
- <X className="mr-1 size-3" />
328
- {commonT('actions.cancel')}
329
- </Button>
330
- <Button
331
- type="button"
332
- size="sm"
333
- className="h-7 px-2 text-xs"
334
- onClick={() => void saveEditing(project.id)}
335
- disabled={saving}
336
- >
337
- {saving ? (
338
- <Loader2 className="mr-1 size-3 animate-spin" />
339
- ) : (
340
- <Check className="mr-1 size-3" />
341
- )}
342
- {commonT('actions.save')}
343
- </Button>
344
- </div>
345
- </div>
346
- );
347
- }
348
-
349
- return (
350
- <div
351
- key={project.id}
352
- className="flex items-center gap-3 rounded-lg border px-3 py-2.5"
353
- >
354
- <div className="min-w-0 flex-1">
355
- <div className="truncate text-sm font-medium">
356
- {project.name}
357
- </div>
358
- <div className="truncate text-xs text-muted-foreground">
359
- {[
360
- project.code,
361
- project.roleLabel || null,
362
- project.weeklyHours != null
363
- ? `${project.weeklyHours}h/sem`
364
- : null,
365
- project.allocationPercent != null
366
- ? `${project.allocationPercent}%`
367
- : null,
368
- formatDateRange(project.startDate, project.endDate),
369
- ]
370
- .filter(Boolean)
371
- .join(' • ')}
372
- </div>
373
- </div>
374
- <div className="flex shrink-0 items-center gap-2">
375
- <StatusBadge
376
- label={formatEnumLabel(project.status)}
377
- className={getStatusBadgeClass(project.status)}
378
- />
379
- {!disabled ? (
380
- <Button
381
- type="button"
382
- variant="ghost"
383
- size="icon"
384
- className="h-6 w-6"
385
- onClick={() => startEditing(project)}
386
- >
387
- <Pencil className="size-3" />
388
- <span className="sr-only">{commonT('actions.edit')}</span>
389
- </Button>
390
- ) : null}
391
- </div>
392
- </div>
393
- );
394
- })}
395
- </div>
396
-
397
- {totalPages > 1 ? (
398
- <div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
399
- <span>
400
- {page * PAGE_SIZE + 1}–
401
- {Math.min((page + 1) * PAGE_SIZE, projects.length)} /{' '}
402
- {projects.length}
403
- </span>
404
- <div className="flex gap-1">
405
- <Button
406
- type="button"
407
- variant="outline"
408
- size="sm"
409
- className="h-6 px-2 text-xs"
410
- disabled={page === 0}
411
- onClick={() => setPage((p) => p - 1)}
412
- >
413
-
414
- </Button>
415
- <Button
416
- type="button"
417
- variant="outline"
418
- size="sm"
419
- className="h-6 px-2 text-xs"
420
- disabled={page >= totalPages - 1}
421
- onClick={() => setPage((p) => p + 1)}
422
- >
423
-
424
- </Button>
425
- </div>
426
- </div>
427
- ) : null}
428
- </>
429
- )}
430
-
431
- <Sheet open={createSheetOpen} onOpenChange={setCreateSheetOpen}>
432
- <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
433
- <SheetHeader>
434
- <SheetTitle>{detailsT('createProject')}</SheetTitle>
435
- <SheetDescription>{detailsT('createProjectDescription')}</SheetDescription>
436
- </SheetHeader>
437
- <ProjectFormScreen
438
- onCancel={() => setCreateSheetOpen(false)}
439
- onSaved={async (project) => {
440
- setCreateSheetOpen(false);
441
- if (collaborator) {
442
- await assignProject(project.id);
443
- }
444
- }}
445
- />
446
- </SheetContent>
447
- </Sheet>
448
- </div>
449
- );
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { EntityPicker } from '@/components/ui/entity-picker';
5
+ import { Input } from '@/components/ui/input';
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetDescription,
10
+ SheetHeader,
11
+ SheetTitle,
12
+ } from '@/components/ui/sheet';
13
+ import { Check, Loader2, Pencil, Plus, X } from 'lucide-react';
14
+ import { useTranslations } from 'next-intl';
15
+ import { useState } from 'react';
16
+ import { fetchOperations, mutateOperations } from '../_lib/api';
17
+ import type {
18
+ OperationsCollaboratorDetails,
19
+ OperationsProject,
20
+ PaginatedResponse,
21
+ } from '../_lib/types';
22
+ import {
23
+ formatDateRange,
24
+ formatEnumLabel,
25
+ getStatusBadgeClass,
26
+ } from '../_lib/utils/format';
27
+ import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
28
+ import { ProjectFormScreen } from './project-form-screen';
29
+ import { StatusBadge } from './status-badge';
30
+
31
+ const PAGE_SIZE = 10;
32
+
33
+ type AssignmentEditState = {
34
+ roleLabel: string;
35
+ weeklyHours: string;
36
+ allocationPercent: string;
37
+ startDate: string;
38
+ endDate: string;
39
+ };
40
+
41
+ type ProjectAssignmentsTabProps = {
42
+ collaborator: OperationsCollaboratorDetails | null;
43
+ request: Parameters<typeof mutateOperations>[0];
44
+ onUpdated: () => void;
45
+ disabled?: boolean;
46
+ };
47
+
48
+ export function ProjectAssignmentsTab({
49
+ collaborator,
50
+ request,
51
+ onUpdated,
52
+ disabled,
53
+ }: ProjectAssignmentsTabProps) {
54
+ const commonT = useTranslations('operations.Common');
55
+ const detailsT = useTranslations('operations.CollaboratorDetailsPage');
56
+
57
+ const [page, setPage] = useState(0);
58
+ const [editingId, setEditingId] = useState<number | null>(null);
59
+ const [editState, setEditState] = useState<AssignmentEditState | null>(null);
60
+ const [saving, setSaving] = useState(false);
61
+ const [adding, setAdding] = useState(false);
62
+ const [createSheetOpen, setCreateSheetOpen] = useState(false);
63
+ const [pickerKey, setPickerKey] = useState(0);
64
+
65
+ const projects = collaborator?.assignedProjects ?? [];
66
+ const assignedIds = new Set(projects.map((p) => p.id));
67
+ const totalPages = Math.ceil(projects.length / PAGE_SIZE);
68
+ const visibleProjects = projects.slice(
69
+ page * PAGE_SIZE,
70
+ (page + 1) * PAGE_SIZE
71
+ );
72
+
73
+ const startEditing = (
74
+ project: OperationsCollaboratorDetails['assignedProjects'][number]
75
+ ) => {
76
+ setEditingId(project.id);
77
+ setEditState({
78
+ roleLabel: project.roleLabel ?? '',
79
+ weeklyHours:
80
+ project.weeklyHours != null ? String(project.weeklyHours) : '',
81
+ allocationPercent:
82
+ project.allocationPercent != null
83
+ ? String(project.allocationPercent)
84
+ : '',
85
+ startDate: project.startDate
86
+ ? String(project.startDate).slice(0, 10)
87
+ : '',
88
+ endDate: project.endDate ? String(project.endDate).slice(0, 10) : '',
89
+ });
90
+ };
91
+
92
+ const cancelEditing = () => {
93
+ setEditingId(null);
94
+ setEditState(null);
95
+ };
96
+
97
+ const saveEditing = async (projectId: number) => {
98
+ if (!collaborator || !editState) return;
99
+ setSaving(true);
100
+ try {
101
+ await mutateOperations(
102
+ request,
103
+ `/operations/collaborators/${collaborator.id}/projects/${projectId}`,
104
+ 'PATCH',
105
+ {
106
+ roleLabel: trimToNull(editState.roleLabel),
107
+ weeklyHours: parseNumberInput(editState.weeklyHours),
108
+ allocationPercent: parseNumberInput(editState.allocationPercent),
109
+ startDate: trimToNull(editState.startDate),
110
+ endDate: trimToNull(editState.endDate),
111
+ }
112
+ );
113
+ setEditingId(null);
114
+ setEditState(null);
115
+ onUpdated();
116
+ } catch {
117
+ /* errors surfaced by onUpdated not triggering */
118
+ } finally {
119
+ setSaving(false);
120
+ }
121
+ };
122
+
123
+ const assignProject = async (projectId: number) => {
124
+ if (!collaborator) return;
125
+ setAdding(true);
126
+ try {
127
+ await mutateOperations(
128
+ request,
129
+ `/operations/collaborators/${collaborator.id}/projects`,
130
+ 'POST',
131
+ { projectId }
132
+ );
133
+ setPickerKey((k) => k + 1);
134
+ onUpdated();
135
+ } finally {
136
+ setAdding(false);
137
+ }
138
+ };
139
+
140
+ return (
141
+ <div className="space-y-3">
142
+ {!disabled ? (
143
+ <div className="flex items-center gap-2">
144
+ <div className="min-w-0 flex-1">
145
+ <EntityPicker<OperationsProject>
146
+ key={pickerKey}
147
+ placeholder={detailsT('addProject')}
148
+ searchPlaceholder={detailsT('searchProject')}
149
+ showCreateButton={false}
150
+ clearable={false}
151
+ disabled={!collaborator || adding}
152
+ loadOptions={async ({ page: p, pageSize: ps, search }) => {
153
+ const params = new URLSearchParams({
154
+ page: String(p),
155
+ pageSize: String(ps),
156
+ });
157
+ if (search.trim()) params.set('search', search.trim());
158
+ const res = await fetchOperations<
159
+ PaginatedResponse<OperationsProject>
160
+ >(request, `/operations/projects?${params.toString()}`);
161
+ return {
162
+ items: res.data.filter((proj) => !assignedIds.has(proj.id)),
163
+ hasMore:
164
+ (res.page ?? p) * (res.pageSize ?? ps) <
165
+ (res.total ?? 0),
166
+ };
167
+ }}
168
+ getOptionValue={(opt) => opt.id}
169
+ getOptionLabel={(opt) => opt.name}
170
+ getOptionDescription={(opt) =>
171
+ [opt.code, opt.clientName].filter(Boolean).join(' • ') ||
172
+ undefined
173
+ }
174
+ onChange={(value) => {
175
+ if (value != null) void assignProject(Number(value));
176
+ }}
177
+ />
178
+ </div>
179
+ <Button
180
+ type="button"
181
+ variant="outline"
182
+ size="icon"
183
+ className="shrink-0"
184
+ title={detailsT('createProject')}
185
+ onClick={() => setCreateSheetOpen(true)}
186
+ >
187
+ {adding ? (
188
+ <Loader2 className="size-4 animate-spin" />
189
+ ) : (
190
+ <Plus className="size-4" />
191
+ )}
192
+ </Button>
193
+ </div>
194
+ ) : null}
195
+
196
+ {projects.length === 0 ? (
197
+ <p className="text-sm text-muted-foreground">{detailsT('noProjects')}</p>
198
+ ) : (
199
+ <>
200
+ <div className="space-y-2">
201
+ {visibleProjects.map((project) => {
202
+ const isEditing = editingId === project.id;
203
+
204
+ if (isEditing && editState) {
205
+ return (
206
+ <div
207
+ key={project.id}
208
+ className="space-y-2 rounded-lg border px-3 py-2.5"
209
+ >
210
+ <div className="flex items-center justify-between gap-2">
211
+ <div className="min-w-0">
212
+ <div className="truncate text-sm font-medium">
213
+ {project.name}
214
+ </div>
215
+ {project.code ? (
216
+ <div className="text-xs text-muted-foreground">
217
+ {project.code}
218
+ </div>
219
+ ) : null}
220
+ </div>
221
+ <StatusBadge
222
+ label={formatEnumLabel(project.status)}
223
+ className={getStatusBadgeClass(project.status)}
224
+ />
225
+ </div>
226
+
227
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
228
+ <div className="space-y-1">
229
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
230
+ {commonT('labels.weeklyCapacity')}
231
+ </div>
232
+ <Input
233
+ className="h-7 text-xs"
234
+ type="number"
235
+ min="0"
236
+ step="0.5"
237
+ value={editState.weeklyHours}
238
+ onChange={(e) =>
239
+ setEditState(
240
+ (s) => s && { ...s, weeklyHours: e.target.value }
241
+ )
242
+ }
243
+ />
244
+ </div>
245
+ <div className="space-y-1">
246
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
247
+ {commonT('labels.allocationPercent')}
248
+ </div>
249
+ <div className="relative">
250
+ <Input
251
+ className="h-7 pr-5 text-xs"
252
+ type="number"
253
+ min="0"
254
+ max="100"
255
+ step="1"
256
+ value={editState.allocationPercent}
257
+ onChange={(e) =>
258
+ setEditState(
259
+ (s) =>
260
+ s && {
261
+ ...s,
262
+ allocationPercent: e.target.value,
263
+ }
264
+ )
265
+ }
266
+ />
267
+ <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground">
268
+ %
269
+ </span>
270
+ </div>
271
+ </div>
272
+ <div className="space-y-1">
273
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
274
+ {commonT('labels.startDate')}
275
+ </div>
276
+ <Input
277
+ className="h-7 text-xs"
278
+ type="date"
279
+ value={editState.startDate}
280
+ onChange={(e) =>
281
+ setEditState(
282
+ (s) => s && { ...s, startDate: e.target.value }
283
+ )
284
+ }
285
+ />
286
+ </div>
287
+ <div className="space-y-1">
288
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
289
+ {commonT('labels.endDate')}
290
+ </div>
291
+ <Input
292
+ className="h-7 text-xs"
293
+ type="date"
294
+ value={editState.endDate}
295
+ onChange={(e) =>
296
+ setEditState(
297
+ (s) => s && { ...s, endDate: e.target.value }
298
+ )
299
+ }
300
+ />
301
+ </div>
302
+ </div>
303
+
304
+ <div className="flex justify-end gap-1.5 pt-1">
305
+ <Button
306
+ type="button"
307
+ size="sm"
308
+ variant="ghost"
309
+ className="h-7 px-2 text-xs"
310
+ onClick={cancelEditing}
311
+ disabled={saving}
312
+ >
313
+ <X className="mr-1 size-3" />
314
+ {commonT('actions.cancel')}
315
+ </Button>
316
+ <Button
317
+ type="button"
318
+ size="sm"
319
+ className="h-7 px-2 text-xs"
320
+ onClick={() => void saveEditing(project.id)}
321
+ disabled={saving}
322
+ >
323
+ {saving ? (
324
+ <Loader2 className="mr-1 size-3 animate-spin" />
325
+ ) : (
326
+ <Check className="mr-1 size-3" />
327
+ )}
328
+ {commonT('actions.save')}
329
+ </Button>
330
+ </div>
331
+ </div>
332
+ );
333
+ }
334
+
335
+ return (
336
+ <div
337
+ key={project.id}
338
+ className="flex items-center gap-3 rounded-lg border px-3 py-2.5"
339
+ >
340
+ <div className="min-w-0 flex-1">
341
+ <div className="truncate text-sm font-medium">
342
+ {project.name}
343
+ </div>
344
+ <div className="truncate text-xs text-muted-foreground">
345
+ {[
346
+ project.code,
347
+ project.weeklyHours != null
348
+ ? `${project.weeklyHours}h/sem`
349
+ : null,
350
+ project.allocationPercent != null
351
+ ? `${project.allocationPercent}%`
352
+ : null,
353
+ formatDateRange(project.startDate, project.endDate),
354
+ ]
355
+ .filter(Boolean)
356
+ .join(' • ')}
357
+ </div>
358
+ </div>
359
+ <div className="flex shrink-0 items-center gap-2">
360
+ <StatusBadge
361
+ label={formatEnumLabel(project.status)}
362
+ className={getStatusBadgeClass(project.status)}
363
+ />
364
+ {!disabled ? (
365
+ <Button
366
+ type="button"
367
+ variant="ghost"
368
+ size="icon"
369
+ className="h-6 w-6"
370
+ onClick={() => startEditing(project)}
371
+ >
372
+ <Pencil className="size-3" />
373
+ <span className="sr-only">{commonT('actions.edit')}</span>
374
+ </Button>
375
+ ) : null}
376
+ </div>
377
+ </div>
378
+ );
379
+ })}
380
+ </div>
381
+
382
+ {totalPages > 1 ? (
383
+ <div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
384
+ <span>
385
+ {page * PAGE_SIZE + 1}
386
+ {Math.min((page + 1) * PAGE_SIZE, projects.length)} /{' '}
387
+ {projects.length}
388
+ </span>
389
+ <div className="flex gap-1">
390
+ <Button
391
+ type="button"
392
+ variant="outline"
393
+ size="sm"
394
+ className="h-6 px-2 text-xs"
395
+ disabled={page === 0}
396
+ onClick={() => setPage((p) => p - 1)}
397
+ >
398
+
399
+ </Button>
400
+ <Button
401
+ type="button"
402
+ variant="outline"
403
+ size="sm"
404
+ className="h-6 px-2 text-xs"
405
+ disabled={page >= totalPages - 1}
406
+ onClick={() => setPage((p) => p + 1)}
407
+ >
408
+
409
+ </Button>
410
+ </div>
411
+ </div>
412
+ ) : null}
413
+ </>
414
+ )}
415
+
416
+ <Sheet open={createSheetOpen} onOpenChange={setCreateSheetOpen}>
417
+ <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
418
+ <SheetHeader>
419
+ <SheetTitle>{detailsT('createProject')}</SheetTitle>
420
+ <SheetDescription>{detailsT('createProjectDescription')}</SheetDescription>
421
+ </SheetHeader>
422
+ <ProjectFormScreen
423
+ onCancel={() => setCreateSheetOpen(false)}
424
+ onSaved={async (project) => {
425
+ setCreateSheetOpen(false);
426
+ if (collaborator) {
427
+ await assignProject(project.id);
428
+ }
429
+ }}
430
+ />
431
+ </SheetContent>
432
+ </Sheet>
433
+ </div>
434
+ );
450
435
  }