@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,784 +1,629 @@
1
- 'use client';
2
-
3
- import { RichTextEditor } from '@/components/rich-text-editor';
4
- import { Button } from '@/components/ui/button';
5
- import { Input } from '@/components/ui/input';
6
- import { Label } from '@/components/ui/label';
7
- import {
8
- Select,
9
- SelectContent,
10
- SelectItem,
11
- SelectTrigger,
12
- SelectValue,
13
- } from '@/components/ui/select';
14
- import {
15
- Sheet,
16
- SheetContent,
17
- SheetDescription,
18
- SheetHeader,
19
- SheetTitle,
20
- } from '@/components/ui/sheet';
21
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
22
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
23
- import { Archive, Loader2, Paperclip, Plus } from 'lucide-react';
24
- import { useTranslations } from 'next-intl';
25
- import { useEffect, useState } from 'react';
26
- import { fetchOperations, mutateOperations } from '../_lib/api';
27
- import { useMentionItems } from '../_lib/hooks/use-mention-items';
28
- import type {
29
- OperationsProjectDetails,
30
- OperationsProjectOption,
31
- OperationsTaskOption,
32
- PaginatedResponse,
33
- } from '../_lib/types';
34
- import { ProjectFormScreen } from './project-form-screen';
35
- import { TaskCommentsSection } from './task-detail-sheet';
36
- import { TaskFileAttachments } from './task-file-attachments';
37
-
38
- type TaskColumnId = 'todo' | 'doing' | 'review' | 'done';
39
-
40
- const KANBAN_COLUMNS: Array<{ id: TaskColumnId; label: string }> = [
41
- { id: 'todo', label: 'Backlog' },
42
- { id: 'doing', label: 'Em execução' },
43
- { id: 'review', label: 'Revisão' },
44
- { id: 'done', label: 'Concluído' },
45
- ];
46
-
47
- type TaskFormState = {
48
- projectId: string;
49
- name: string;
50
- description: string;
51
- priority: 'low' | 'medium' | 'high';
52
- status: TaskColumnId;
53
- dueDate: string;
54
- estimateHours: string;
55
- tags: string;
56
- };
57
-
58
- const EMPTY_TASK_FORM: TaskFormState = {
59
- projectId: '',
60
- name: '',
61
- description: '',
62
- priority: 'medium',
63
- status: 'todo',
64
- dueDate: '',
65
- estimateHours: '',
66
- tags: '',
67
- };
68
-
69
- function normalizeDateValue(value?: string | null) {
70
- if (!value) return '';
71
- const match = String(value)
72
- .trim()
73
- .match(/^\d{4}-\d{2}-\d{2}/);
74
- return match?.[0] ?? '';
75
- }
76
-
77
- function getTaskPriorityLabel(value?: string | null) {
78
- const labels: Record<string, string> = {
79
- low: 'Baixa',
80
- medium: 'Média',
81
- high: 'Alta',
82
- };
83
- return labels[value ?? ''] ?? String(value ?? '');
84
- }
85
-
86
- export type TaskFormSheetProps = {
87
- open: boolean;
88
- onOpenChange: (open: boolean) => void;
89
- request: Parameters<typeof mutateOperations>[0];
90
- /** When provided, the form enters edit mode. */
91
- editingTask?: OperationsTaskOption | null;
92
- /** Pre-assigns this collaborator on new task creation. */
93
- defaultAssigneeCollaboratorId?: number | null;
94
- /** When true the project field is required for creation. Default: false. */
95
- projectRequired?: boolean;
96
- /** When false the project selector is hidden (e.g. when the project is already known from context). Default: true. */
97
- showProject?: boolean;
98
- /** Default active tab when opening in edit mode. Default: 'info'. */
99
- defaultTab?: 'info' | 'comments';
100
- onSaved: () => void;
101
- };
102
-
103
- export function TaskFormSheet({
104
- open,
105
- onOpenChange,
106
- request,
107
- editingTask,
108
- defaultAssigneeCollaboratorId,
109
- projectRequired = false,
110
- showProject = true,
111
- defaultTab = 'info',
112
- onSaved,
113
- }: TaskFormSheetProps) {
114
- const { currentLocaleCode, request: appRequest } = useApp();
115
- const commonT = useTranslations('operations.Common');
116
- const taskT = useTranslations('operations.ProjectDetailsPage');
117
- const projectsT = useTranslations('operations.ProjectsPage');
118
-
119
- const mentionItems = useMentionItems(appRequest);
120
-
121
- const [formData, setFormData] = useState<TaskFormState>(EMPTY_TASK_FORM);
122
- const [loading, setLoading] = useState(false);
123
- const [archivingId, setArchivingId] = useState<number | null>(null);
124
- const [projectFormOpen, setProjectFormOpen] = useState(false);
125
- const [activeTab, setActiveTab] = useState<'info' | 'comments'>(defaultTab);
126
- const [localProjectOptions, setLocalProjectOptions] = useState<
127
- OperationsProjectOption[]
128
- >([]);
129
-
130
- const { data: projectOptionsResponse } = useQuery<
131
- PaginatedResponse<OperationsProjectOption>
132
- >({
133
- queryKey: ['task-form-project-options', currentLocaleCode],
134
- queryFn: () =>
135
- fetchOperations<PaginatedResponse<OperationsProjectOption>>(
136
- request,
137
- '/operations/projects/options?pageSize=200&sortField=name&sortOrder=asc'
138
- ),
139
- enabled: open,
140
- placeholderData: (previous) => previous,
141
- });
142
- const remoteProjectOptions = projectOptionsResponse?.data ?? [];
143
- const projectOptions = [
144
- ...localProjectOptions,
145
- ...remoteProjectOptions.filter(
146
- (r) => !localProjectOptions.some((l) => l.id === r.id)
147
- ),
148
- ];
149
-
150
- // Sync form state when editingTask or open changes.
151
- useEffect(() => {
152
- if (!open) return;
153
- setActiveTab(defaultTab);
154
- if (editingTask) {
155
- setFormData({
156
- projectId: String(editingTask.projectId ?? ''),
157
- name: editingTask.name,
158
- description: editingTask.description ?? '',
159
- priority:
160
- (editingTask.priority as TaskFormState['priority']) ?? 'medium',
161
- status: KANBAN_COLUMNS.some((c) => c.id === editingTask.status)
162
- ? (editingTask.status as TaskColumnId)
163
- : 'todo',
164
- dueDate: normalizeDateValue(editingTask.dueDate),
165
- estimateHours:
166
- editingTask.estimateHours != null
167
- ? String(editingTask.estimateHours)
168
- : '',
169
- tags: editingTask.tags ?? '',
170
- });
171
- } else {
172
- setFormData(EMPTY_TASK_FORM);
173
- }
174
- }, [open, editingTask]);
175
-
176
- const close = () => {
177
- onOpenChange(false);
178
- };
179
-
180
- const isSubmitDisabled = () => {
181
- if (!formData.name.trim()) return true;
182
- if (!editingTask && projectRequired && !formData.projectId) return true;
183
- return false;
184
- };
185
-
186
- const handleSubmit = async () => {
187
- if (isSubmitDisabled()) return;
188
- setLoading(true);
189
- try {
190
- const selectedProject = projectOptions.find(
191
- (p) => String(p.id) === formData.projectId
192
- );
193
-
194
- const payload: Record<string, unknown> = {
195
- name: formData.name.trim(),
196
- description: formData.description.trim() || null,
197
- priority: formData.priority,
198
- status: formData.status,
199
- dueDate: formData.dueDate || null,
200
- estimateHours: formData.estimateHours
201
- ? Number(formData.estimateHours)
202
- : null,
203
- tags: formData.tags.trim() || null,
204
- };
205
-
206
- if (editingTask) {
207
- await mutateOperations(
208
- request,
209
- `/operations/tasks/${editingTask.id}`,
210
- 'PATCH',
211
- payload
212
- );
213
- } else {
214
- if (selectedProject) {
215
- payload.projectId = selectedProject.id;
216
- if (selectedProject.projectAssignmentId != null) {
217
- payload.projectAssignmentId = selectedProject.projectAssignmentId;
218
- }
219
- } else if (formData.projectId) {
220
- payload.projectId = Number(formData.projectId);
221
- }
222
- if (defaultAssigneeCollaboratorId != null) {
223
- payload.assigneeCollaboratorId = defaultAssigneeCollaboratorId;
224
- }
225
- await mutateOperations(request, '/operations/tasks', 'POST', payload);
226
- }
227
-
228
- onSaved();
229
- close();
230
- } finally {
231
- setLoading(false);
232
- }
233
- };
234
-
235
- const handleArchive = async () => {
236
- if (!editingTask) return;
237
- setArchivingId(editingTask.id);
238
- try {
239
- await mutateOperations(
240
- request,
241
- `/operations/tasks/${editingTask.id}`,
242
- 'PATCH',
243
- { archived: true }
244
- );
245
- onSaved();
246
- close();
247
- } finally {
248
- setArchivingId(null);
249
- }
250
- };
251
-
252
- return (
253
- <>
254
- <Sheet
255
- open={open}
256
- onOpenChange={(value) => {
257
- if (!loading) onOpenChange(value);
258
- }}
259
- >
260
- <SheetContent className="flex w-full flex-col overflow-hidden sm:max-w-xl">
261
- <SheetHeader className="shrink-0">
262
- <SheetTitle>
263
- {editingTask
264
- ? taskT('taskForm.titleEdit')
265
- : taskT('taskForm.titleNew')}
266
- </SheetTitle>
267
- </SheetHeader>
268
-
269
- {editingTask ? (
270
- <Tabs
271
- value={activeTab}
272
- onValueChange={(v) => setActiveTab(v as 'info' | 'comments')}
273
- className="flex min-h-0 flex-1 flex-col"
274
- >
275
- <TabsList className="mx-4 shrink-0">
276
- <TabsTrigger value="info">Informações</TabsTrigger>
277
- <TabsTrigger value="comments">Comentários</TabsTrigger>
278
- </TabsList>
279
-
280
- <TabsContent
281
- value="info"
282
- className="flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
283
- >
284
- <div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
285
- {/* Project selector — hidden when showProject=false (project is known from context) */}
286
- {showProject ? (
287
- <div className="space-y-1.5">
288
- <Label htmlFor="task-form-project">
289
- {commonT('labels.project')}
290
- {!editingTask && projectRequired ? ' *' : ''}
291
- </Label>
292
- <div className="flex gap-2">
293
- <Select
294
- value={formData.projectId}
295
- onValueChange={(v) =>
296
- setFormData((prev) => ({ ...prev, projectId: v }))
297
- }
298
- >
299
- <SelectTrigger
300
- className="w-full"
301
- id="task-form-project"
302
- >
303
- <SelectValue
304
- placeholder={commonT('labels.project')}
305
- />
306
- </SelectTrigger>
307
- <SelectContent>
308
- {!projectRequired && !editingTask ? (
309
- <SelectItem value="none">
310
- {commonT('labels.notAssigned')}
311
- </SelectItem>
312
- ) : null}
313
- {editingTask?.projectId &&
314
- !projectOptions.some(
315
- (p) =>
316
- String(p.id) === String(editingTask.projectId)
317
- ) ? (
318
- <SelectItem
319
- key={`fallback-${editingTask.projectId}`}
320
- value={String(editingTask.projectId)}
321
- >
322
- {[
323
- editingTask.projectCode,
324
- editingTask.projectName,
325
- ]
326
- .filter(Boolean)
327
- .join(' • ') || String(editingTask.projectId)}
328
- </SelectItem>
329
- ) : null}
330
- {projectOptions.map((p) => (
331
- <SelectItem key={p.id} value={String(p.id)}>
332
- {p.label}
333
- </SelectItem>
334
- ))}
335
- </SelectContent>
336
- </Select>
337
- <Button
338
- type="button"
339
- variant="outline"
340
- size="icon"
341
- className="shrink-0"
342
- title={projectsT('sheet.createTitle')}
343
- onClick={() => setProjectFormOpen(true)}
344
- >
345
- <Plus className="size-4" />
346
- </Button>
347
- </div>
348
- </div>
349
- ) : null}
350
-
351
- <div className="space-y-1.5">
352
- <Label htmlFor="task-form-name">
353
- {taskT('taskForm.nameLabel')} *
354
- </Label>
355
- <Input
356
- id="task-form-name"
357
- placeholder={taskT('taskForm.namePlaceholder')}
358
- value={formData.name}
359
- onChange={(e) =>
360
- setFormData((prev) => ({
361
- ...prev,
362
- name: e.target.value,
363
- }))
364
- }
365
- />
366
- </div>
367
-
368
- <div className="space-y-1.5">
369
- <Label htmlFor="task-form-description">
370
- {taskT('taskForm.descriptionLabel')}
371
- </Label>
372
- <RichTextEditor
373
- value={formData.description}
374
- onChange={(val) =>
375
- setFormData((prev) => ({ ...prev, description: val }))
376
- }
377
- mentions={mentionItems}
378
- />
379
- </div>
380
-
381
- <div className="grid grid-cols-2 gap-3">
382
- <div className="space-y-1.5">
383
- <Label>{taskT('taskForm.priorityLabel')}</Label>
384
- <Select
385
- value={formData.priority}
386
- onValueChange={(v) =>
387
- setFormData((prev) => ({
388
- ...prev,
389
- priority: v as TaskFormState['priority'],
390
- }))
391
- }
392
- >
393
- <SelectTrigger className="w-full">
394
- <SelectValue />
395
- </SelectTrigger>
396
- <SelectContent>
397
- <SelectItem value="low">
398
- {getTaskPriorityLabel('low')}
399
- </SelectItem>
400
- <SelectItem value="medium">
401
- {getTaskPriorityLabel('medium')}
402
- </SelectItem>
403
- <SelectItem value="high">
404
- {getTaskPriorityLabel('high')}
405
- </SelectItem>
406
- </SelectContent>
407
- </Select>
408
- </div>
409
-
410
- <div className="space-y-1.5">
411
- <Label>{taskT('taskForm.columnLabel')}</Label>
412
- <Select
413
- value={formData.status}
414
- onValueChange={(v) =>
415
- setFormData((prev) => ({
416
- ...prev,
417
- status: v as TaskColumnId,
418
- }))
419
- }
420
- >
421
- <SelectTrigger className="w-full">
422
- <SelectValue />
423
- </SelectTrigger>
424
- <SelectContent>
425
- {KANBAN_COLUMNS.map((col) => (
426
- <SelectItem key={col.id} value={col.id}>
427
- {col.label}
428
- </SelectItem>
429
- ))}
430
- </SelectContent>
431
- </Select>
432
- </div>
433
- </div>
434
-
435
- <div className="grid grid-cols-2 gap-3">
436
- <div className="space-y-1.5">
437
- <Label htmlFor="task-form-due-date">
438
- {taskT('taskForm.deadlineLabel')}
439
- </Label>
440
- <Input
441
- id="task-form-due-date"
442
- type="date"
443
- value={formData.dueDate}
444
- onChange={(e) =>
445
- setFormData((prev) => ({
446
- ...prev,
447
- dueDate: e.target.value,
448
- }))
449
- }
450
- />
451
- </div>
452
-
453
- <div className="space-y-1.5">
454
- <Label htmlFor="task-form-estimate">
455
- {taskT('taskForm.estimateLabel')}
456
- </Label>
457
- <Input
458
- id="task-form-estimate"
459
- type="number"
460
- min="0"
461
- step="0.5"
462
- placeholder="0"
463
- value={formData.estimateHours}
464
- onChange={(e) =>
465
- setFormData((prev) => ({
466
- ...prev,
467
- estimateHours: e.target.value,
468
- }))
469
- }
470
- />
471
- </div>
472
- </div>
473
-
474
- <div className="space-y-1.5">
475
- <Label htmlFor="task-form-tags">
476
- {taskT('taskForm.tagsLabel')}
477
- </Label>
478
- <Input
479
- id="task-form-tags"
480
- placeholder={taskT('taskForm.tagsPlaceholder')}
481
- value={formData.tags}
482
- onChange={(e) =>
483
- setFormData((prev) => ({
484
- ...prev,
485
- tags: e.target.value,
486
- }))
487
- }
488
- />
489
- </div>
490
-
491
- <div className="space-y-1.5">
492
- <Label className="flex items-center gap-1.5">
493
- <Paperclip className="size-3.5" />
494
- {taskT('taskForm.attachmentsLabel')}
495
- </Label>
496
- <TaskFileAttachments taskId={editingTask.id} />
497
- </div>
498
- </div>
499
-
500
- <div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
501
- <div className="flex gap-2">
502
- <Button
503
- type="button"
504
- variant="outline"
505
- disabled={loading || archivingId === editingTask.id}
506
- onClick={() => void handleArchive()}
507
- >
508
- {archivingId === editingTask.id ? (
509
- <Loader2 className="mr-2 size-4 animate-spin" />
510
- ) : (
511
- <Archive className="mr-2 size-4" />
512
- )}
513
- {commonT('actions.archive')}
514
- </Button>
515
- </div>
516
- <div className="flex gap-2">
517
- <Button
518
- variant="outline"
519
- onClick={close}
520
- disabled={loading}
521
- >
522
- {commonT('actions.cancel')}
523
- </Button>
524
- <Button
525
- onClick={() => void handleSubmit()}
526
- disabled={loading || isSubmitDisabled()}
527
- >
528
- {loading ? (
529
- <Loader2 className="mr-2 size-4 animate-spin" />
530
- ) : null}
531
- {commonT('actions.save')}
532
- </Button>
533
- </div>
534
- </div>
535
- </TabsContent>
536
-
537
- <TabsContent
538
- value="comments"
539
- className="min-h-0 flex-1 overflow-y-auto px-4 py-2 data-[state=inactive]:hidden"
540
- >
541
- <TaskCommentsSection taskId={editingTask.id} />
542
- </TabsContent>
543
- </Tabs>
544
- ) : (
545
- <>
546
- <div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
547
- {/* Project selector — hidden when showProject=false (project is known from context) */}
548
- {showProject ? (
549
- <div className="space-y-1.5">
550
- <Label htmlFor="task-form-project">
551
- {commonT('labels.project')}
552
- {projectRequired ? ' *' : ''}
553
- </Label>
554
- <div className="flex gap-2">
555
- <Select
556
- value={formData.projectId}
557
- onValueChange={(v) =>
558
- setFormData((prev) => ({ ...prev, projectId: v }))
559
- }
560
- >
561
- <SelectTrigger
562
- className="w-full"
563
- id="task-form-project"
564
- >
565
- <SelectValue
566
- placeholder={commonT('labels.project')}
567
- />
568
- </SelectTrigger>
569
- <SelectContent>
570
- {!projectRequired ? (
571
- <SelectItem value="none">
572
- {commonT('labels.notAssigned')}
573
- </SelectItem>
574
- ) : null}
575
- {projectOptions.map((p) => (
576
- <SelectItem key={p.id} value={String(p.id)}>
577
- {p.label}
578
- </SelectItem>
579
- ))}
580
- </SelectContent>
581
- </Select>
582
- <Button
583
- type="button"
584
- variant="outline"
585
- size="icon"
586
- className="shrink-0"
587
- title={projectsT('sheet.createTitle')}
588
- onClick={() => setProjectFormOpen(true)}
589
- >
590
- <Plus className="size-4" />
591
- </Button>
592
- </div>
593
- </div>
594
- ) : null}
595
-
596
- <div className="space-y-1.5">
597
- <Label htmlFor="task-form-name">
598
- {taskT('taskForm.nameLabel')} *
599
- </Label>
600
- <Input
601
- id="task-form-name"
602
- placeholder={taskT('taskForm.namePlaceholder')}
603
- value={formData.name}
604
- onChange={(e) =>
605
- setFormData((prev) => ({ ...prev, name: e.target.value }))
606
- }
607
- />
608
- </div>
609
-
610
- <div className="space-y-1.5">
611
- <Label htmlFor="task-form-description">
612
- {taskT('taskForm.descriptionLabel')}
613
- </Label>
614
- <RichTextEditor
615
- value={formData.description}
616
- onChange={(val) =>
617
- setFormData((prev) => ({ ...prev, description: val }))
618
- }
619
- mentions={mentionItems}
620
- />
621
- </div>
622
-
623
- <div className="grid grid-cols-2 gap-3">
624
- <div className="space-y-1.5">
625
- <Label>{taskT('taskForm.priorityLabel')}</Label>
626
- <Select
627
- value={formData.priority}
628
- onValueChange={(v) =>
629
- setFormData((prev) => ({
630
- ...prev,
631
- priority: v as TaskFormState['priority'],
632
- }))
633
- }
634
- >
635
- <SelectTrigger className="w-full">
636
- <SelectValue />
637
- </SelectTrigger>
638
- <SelectContent>
639
- <SelectItem value="low">
640
- {getTaskPriorityLabel('low')}
641
- </SelectItem>
642
- <SelectItem value="medium">
643
- {getTaskPriorityLabel('medium')}
644
- </SelectItem>
645
- <SelectItem value="high">
646
- {getTaskPriorityLabel('high')}
647
- </SelectItem>
648
- </SelectContent>
649
- </Select>
650
- </div>
651
-
652
- <div className="space-y-1.5">
653
- <Label>{taskT('taskForm.columnLabel')}</Label>
654
- <Select
655
- value={formData.status}
656
- onValueChange={(v) =>
657
- setFormData((prev) => ({
658
- ...prev,
659
- status: v as TaskColumnId,
660
- }))
661
- }
662
- >
663
- <SelectTrigger className="w-full">
664
- <SelectValue />
665
- </SelectTrigger>
666
- <SelectContent>
667
- {KANBAN_COLUMNS.map((col) => (
668
- <SelectItem key={col.id} value={col.id}>
669
- {col.label}
670
- </SelectItem>
671
- ))}
672
- </SelectContent>
673
- </Select>
674
- </div>
675
- </div>
676
-
677
- <div className="grid grid-cols-2 gap-3">
678
- <div className="space-y-1.5">
679
- <Label htmlFor="task-form-due-date">
680
- {taskT('taskForm.deadlineLabel')}
681
- </Label>
682
- <Input
683
- id="task-form-due-date"
684
- type="date"
685
- value={formData.dueDate}
686
- onChange={(e) =>
687
- setFormData((prev) => ({
688
- ...prev,
689
- dueDate: e.target.value,
690
- }))
691
- }
692
- />
693
- </div>
694
-
695
- <div className="space-y-1.5">
696
- <Label htmlFor="task-form-estimate">
697
- {taskT('taskForm.estimateLabel')}
698
- </Label>
699
- <Input
700
- id="task-form-estimate"
701
- type="number"
702
- min="0"
703
- step="0.5"
704
- placeholder="0"
705
- value={formData.estimateHours}
706
- onChange={(e) =>
707
- setFormData((prev) => ({
708
- ...prev,
709
- estimateHours: e.target.value,
710
- }))
711
- }
712
- />
713
- </div>
714
- </div>
715
-
716
- <div className="space-y-1.5">
717
- <Label htmlFor="task-form-tags">
718
- {taskT('taskForm.tagsLabel')}
719
- </Label>
720
- <Input
721
- id="task-form-tags"
722
- placeholder={taskT('taskForm.tagsPlaceholder')}
723
- value={formData.tags}
724
- onChange={(e) =>
725
- setFormData((prev) => ({ ...prev, tags: e.target.value }))
726
- }
727
- />
728
- </div>
729
- </div>
730
-
731
- <div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
732
- <div className="flex gap-2" />
733
- <div className="flex gap-2">
734
- <Button variant="outline" onClick={close} disabled={loading}>
735
- {commonT('actions.cancel')}
736
- </Button>
737
- <Button
738
- onClick={() => void handleSubmit()}
739
- disabled={loading || isSubmitDisabled()}
740
- >
741
- {loading ? (
742
- <Loader2 className="mr-2 size-4 animate-spin" />
743
- ) : null}
744
- {commonT('actions.create')}
745
- </Button>
746
- </div>
747
- </div>
748
- </>
749
- )}
750
- </SheetContent>
751
- </Sheet>
752
-
753
- <Sheet open={projectFormOpen} onOpenChange={setProjectFormOpen}>
754
- <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
755
- <SheetHeader>
756
- <SheetTitle>{projectsT('sheet.createTitle')}</SheetTitle>
757
- <SheetDescription>
758
- {projectsT('sheet.description')}
759
- </SheetDescription>
760
- </SheetHeader>
761
- <ProjectFormScreen
762
- onCancel={() => setProjectFormOpen(false)}
763
- onSaved={(project: OperationsProjectDetails) => {
764
- const newOption: OperationsProjectOption = {
765
- id: project.id,
766
- label: project.name,
767
- name: project.name,
768
- code: project.code,
769
- clientName: project.clientName,
770
- status: project.status,
771
- };
772
- setLocalProjectOptions((prev) => [newOption, ...prev]);
773
- setFormData((prev) => ({
774
- ...prev,
775
- projectId: String(project.id),
776
- }));
777
- setProjectFormOpen(false);
778
- }}
779
- />
780
- </SheetContent>
781
- </Sheet>
782
- </>
783
- );
784
- }
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetDescription,
8
+ SheetHeader,
9
+ SheetTitle,
10
+ } from '@/components/ui/sheet';
11
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
12
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
13
+ import { Archive, History, Loader2, MessageSquare, Timer } from 'lucide-react';
14
+ import { useTranslations } from 'next-intl';
15
+ import { useCallback, useEffect, useState } from 'react';
16
+ import { fetchOperations, mutateOperations } from '../_lib/api';
17
+ import { useMentionItems } from '../_lib/hooks/use-mention-items';
18
+ import type {
19
+ OperationsProjectDetails,
20
+ OperationsProjectOption,
21
+ OperationsTaskOption,
22
+ PaginatedResponse,
23
+ } from '../_lib/types';
24
+ import { getElapsedDoingMinutes } from '../_lib/utils/task-ui';
25
+ import { ProjectFormScreen } from './project-form-screen';
26
+ import {
27
+ TaskActivitiesSection,
28
+ TaskCommentsSection,
29
+ } from './task-detail-sheet';
30
+ import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
31
+ import { TaskFormFields } from './task-form-fields';
32
+
33
+ const ASIDE_WIDTH_LS_KEY = 'task-form-sheet:aside-width';
34
+ const DEFAULT_ASIDE_WIDTH = 288;
35
+ const MIN_ASIDE_WIDTH = 180;
36
+ const MAX_ASIDE_WIDTH = 560;
37
+
38
+ type TaskColumnId = 'todo' | 'doing' | 'review' | 'done';
39
+
40
+ const KANBAN_COLUMNS: Array<{ id: TaskColumnId; label: string }> = [
41
+ { id: 'todo', label: 'Backlog' },
42
+ { id: 'doing', label: 'Em execução' },
43
+ { id: 'review', label: 'Revisão' },
44
+ { id: 'done', label: 'Concluído' },
45
+ ];
46
+
47
+ type TaskFormState = {
48
+ projectId: string;
49
+ name: string;
50
+ description: string;
51
+ priority: 'low' | 'medium' | 'high';
52
+ status: TaskColumnId;
53
+ dueDate: string;
54
+ estimateHours: string;
55
+ tags: string;
56
+ assigneeCollaboratorId: string;
57
+ };
58
+
59
+ const EMPTY_TASK_FORM: TaskFormState = {
60
+ projectId: '',
61
+ name: '',
62
+ description: '',
63
+ priority: 'medium',
64
+ status: 'todo',
65
+ dueDate: '',
66
+ estimateHours: '',
67
+ tags: '',
68
+ assigneeCollaboratorId: '',
69
+ };
70
+
71
+ function normalizeDateValue(value?: string | null) {
72
+ if (!value) return '';
73
+ const match = String(value)
74
+ .trim()
75
+ .match(/^\d{4}-\d{2}-\d{2}/);
76
+ return match?.[0] ?? '';
77
+ }
78
+
79
+ export type TaskFormSheetProps = {
80
+ open: boolean;
81
+ onOpenChange: (open: boolean) => void;
82
+ request: Parameters<typeof mutateOperations>[0];
83
+ /** When provided, the form enters edit mode. */
84
+ editingTask?: OperationsTaskOption | null;
85
+ /** Pre-assigns this collaborator on new task creation. */
86
+ defaultAssigneeCollaboratorId?: number | null;
87
+ /** When true the project field is required for creation. Default: false. */
88
+ projectRequired?: boolean;
89
+ /** When false the project selector is hidden (e.g. when the project is already known from context). Default: true. */
90
+ showProject?: boolean;
91
+ /** Default active tab when opening in edit mode. Default: 'comments'. */
92
+ defaultTab?: 'comments' | 'activities';
93
+ /** When false the assignee field is hidden entirely. Default: true. */
94
+ showAssignee?: boolean;
95
+ /** When provided, renders a static Select for assignee instead of the CollaboratorPicker API widget. */
96
+ assigneeOptions?: Array<{ id: string; label: string }>;
97
+ /** When provided, called instead of the internal PATCH archive. The sheet still closes and calls onSaved after. */
98
+ onArchive?: (taskId: number) => Promise<void> | void;
99
+ /** When provided, called on "Log hours" click instead of opening the internal timesheet sheet. */
100
+ onLogHours?: () => void;
101
+ /** Pre-populate projectId on create (useful when showProject=false but project is known from context). */
102
+ defaultProjectId?: number;
103
+ /** Pre-populate status on create. Default: 'todo'. */
104
+ defaultStatus?: string;
105
+ /** Called when a file or comment is added/removed, so parent can refresh card counts. */
106
+ onCountChanged?: () => void;
107
+ onSaved: () => void;
108
+ };
109
+
110
+ export function TaskFormSheet({
111
+ open,
112
+ onOpenChange,
113
+ request,
114
+ editingTask,
115
+ defaultAssigneeCollaboratorId,
116
+ projectRequired = false,
117
+ showProject = true,
118
+ defaultTab = 'comments',
119
+ showAssignee = true,
120
+ assigneeOptions,
121
+ onArchive,
122
+ onLogHours,
123
+ defaultProjectId,
124
+ defaultStatus,
125
+ onCountChanged,
126
+ onSaved,
127
+ }: TaskFormSheetProps) {
128
+ const { currentLocaleCode, request: appRequest } = useApp();
129
+ const commonT = useTranslations('operations.Common');
130
+ const taskT = useTranslations('operations.ProjectDetailsPage');
131
+ const projectsT = useTranslations('operations.ProjectsPage');
132
+
133
+ const mentionItems = useMentionItems(appRequest);
134
+
135
+ const [formData, setFormData] = useState<TaskFormState>(EMPTY_TASK_FORM);
136
+ const [loading, setLoading] = useState(false);
137
+ const [archivingId, setArchivingId] = useState<number | null>(null);
138
+ const [projectFormOpen, setProjectFormOpen] = useState(false);
139
+ const [activeTab, setActiveTab] = useState<'comments' | 'activities'>(
140
+ defaultTab
141
+ );
142
+ const [doingTick, setDoingTick] = useState(0);
143
+ const [isTimesheetOpen, setIsTimesheetOpen] = useState(false);
144
+ const [asideWidth, setAsideWidth] = useState(DEFAULT_ASIDE_WIDTH);
145
+
146
+ useEffect(() => {
147
+ const stored = localStorage.getItem(ASIDE_WIDTH_LS_KEY);
148
+ if (stored) {
149
+ const v = Number(stored);
150
+ if (!isNaN(v) && v >= MIN_ASIDE_WIDTH && v <= MAX_ASIDE_WIDTH)
151
+ setAsideWidth(v);
152
+ }
153
+ }, []);
154
+
155
+ const handleResizeMouseDown = useCallback(
156
+ (e: React.MouseEvent) => {
157
+ e.preventDefault();
158
+ const startX = e.clientX;
159
+ let width = asideWidth;
160
+
161
+ document.body.style.cursor = 'col-resize';
162
+ document.body.style.userSelect = 'none';
163
+
164
+ const onMouseMove = (ev: MouseEvent) => {
165
+ width = Math.min(
166
+ MAX_ASIDE_WIDTH,
167
+ Math.max(MIN_ASIDE_WIDTH, asideWidth + (startX - ev.clientX))
168
+ );
169
+ setAsideWidth(width);
170
+ };
171
+
172
+ const onMouseUp = () => {
173
+ document.body.style.cursor = '';
174
+ document.body.style.userSelect = '';
175
+ localStorage.setItem(ASIDE_WIDTH_LS_KEY, String(width));
176
+ document.removeEventListener('mousemove', onMouseMove);
177
+ document.removeEventListener('mouseup', onMouseUp);
178
+ };
179
+
180
+ document.addEventListener('mousemove', onMouseMove);
181
+ document.addEventListener('mouseup', onMouseUp);
182
+ },
183
+ [asideWidth]
184
+ );
185
+ const [localProjectOptions, setLocalProjectOptions] = useState<
186
+ OperationsProjectOption[]
187
+ >([]);
188
+
189
+ const { data: projectOptionsResponse } = useQuery<
190
+ PaginatedResponse<OperationsProjectOption>
191
+ >({
192
+ queryKey: ['task-form-project-options', currentLocaleCode],
193
+ queryFn: () =>
194
+ fetchOperations<PaginatedResponse<OperationsProjectOption>>(
195
+ request,
196
+ '/operations/projects/options?pageSize=200&sortField=name&sortOrder=asc'
197
+ ),
198
+ enabled: open,
199
+ placeholderData: (previous) => previous,
200
+ });
201
+
202
+ const remoteProjectOptions = projectOptionsResponse?.data ?? [];
203
+ const projectOptions = [
204
+ ...localProjectOptions,
205
+ ...remoteProjectOptions.filter(
206
+ (r) => !localProjectOptions.some((l) => l.id === r.id)
207
+ ),
208
+ ];
209
+ const projectOptionsWithFallback =
210
+ editingTask?.projectId &&
211
+ !projectOptions.some((p) => String(p.id) === String(editingTask.projectId))
212
+ ? [
213
+ {
214
+ id: editingTask.projectId,
215
+ label:
216
+ [editingTask.projectCode, editingTask.projectName]
217
+ .filter(Boolean)
218
+ .join(' • ') || String(editingTask.projectId),
219
+ name: editingTask.projectName ?? String(editingTask.projectId),
220
+ code: editingTask.projectCode,
221
+ status: editingTask.status,
222
+ },
223
+ ...projectOptions,
224
+ ]
225
+ : projectOptions;
226
+
227
+ // Sync form state when editingTask or open changes.
228
+ useEffect(() => {
229
+ if (!open) return;
230
+ setActiveTab(defaultTab);
231
+ if (editingTask) {
232
+ setFormData({
233
+ projectId: String(editingTask.projectId ?? ''),
234
+ name: editingTask.name,
235
+ description: editingTask.description ?? '',
236
+ priority:
237
+ (editingTask.priority as TaskFormState['priority']) ?? 'medium',
238
+ status: KANBAN_COLUMNS.some((c) => c.id === editingTask.status)
239
+ ? (editingTask.status as TaskColumnId)
240
+ : 'todo',
241
+ dueDate: normalizeDateValue(editingTask.dueDate),
242
+ estimateHours:
243
+ editingTask.estimateHours != null
244
+ ? String(editingTask.estimateHours)
245
+ : '',
246
+ tags: editingTask.tags ?? '',
247
+ assigneeCollaboratorId:
248
+ editingTask.assigneeCollaboratorId != null
249
+ ? String(editingTask.assigneeCollaboratorId)
250
+ : '',
251
+ });
252
+ } else {
253
+ setFormData({
254
+ ...EMPTY_TASK_FORM,
255
+ status: (defaultStatus as TaskFormState['status']) ?? 'todo',
256
+ projectId: defaultProjectId ? String(defaultProjectId) : '',
257
+ });
258
+ }
259
+ }, [open, editingTask, defaultTab, defaultStatus, defaultProjectId]);
260
+
261
+ useEffect(() => {
262
+ if (!open || editingTask?.status !== 'doing' || !editingTask.doingStartedAt)
263
+ return;
264
+ const timer = setInterval(() => setDoingTick((value) => value + 1), 30000);
265
+ return () => clearInterval(timer);
266
+ }, [editingTask?.doingStartedAt, editingTask?.status, open]);
267
+
268
+ const close = () => {
269
+ onOpenChange(false);
270
+ };
271
+
272
+ const isSubmitDisabled = () => {
273
+ if (!formData.name.trim()) return true;
274
+ if (!editingTask && projectRequired && !formData.projectId) return true;
275
+ return false;
276
+ };
277
+
278
+ const handleSubmit = async () => {
279
+ if (isSubmitDisabled()) return;
280
+ setLoading(true);
281
+ try {
282
+ const selectedProject = projectOptions.find(
283
+ (p) => String(p.id) === formData.projectId
284
+ );
285
+
286
+ const payload: Record<string, unknown> = {
287
+ name: formData.name.trim(),
288
+ description: formData.description.trim() || null,
289
+ priority: formData.priority,
290
+ status: formData.status,
291
+ dueDate: formData.dueDate || null,
292
+ estimateHours: formData.estimateHours
293
+ ? Number(formData.estimateHours)
294
+ : null,
295
+ tags: formData.tags.trim() || null,
296
+ assigneeCollaboratorId: formData.assigneeCollaboratorId
297
+ ? Number(formData.assigneeCollaboratorId)
298
+ : null,
299
+ };
300
+
301
+ if (editingTask) {
302
+ await mutateOperations(
303
+ request,
304
+ `/operations/tasks/${editingTask.id}`,
305
+ 'PATCH',
306
+ payload
307
+ );
308
+ } else {
309
+ if (selectedProject) {
310
+ payload.projectId = selectedProject.id;
311
+ if (selectedProject.projectAssignmentId != null) {
312
+ payload.projectAssignmentId = selectedProject.projectAssignmentId;
313
+ }
314
+ } else if (formData.projectId) {
315
+ payload.projectId = Number(formData.projectId);
316
+ }
317
+ // Form selection takes priority over the default prop
318
+ if (
319
+ !payload.assigneeCollaboratorId &&
320
+ defaultAssigneeCollaboratorId != null
321
+ ) {
322
+ payload.assigneeCollaboratorId = defaultAssigneeCollaboratorId;
323
+ }
324
+ await mutateOperations(request, '/operations/tasks', 'POST', payload);
325
+ }
326
+
327
+ onSaved();
328
+ close();
329
+ } finally {
330
+ setLoading(false);
331
+ }
332
+ };
333
+
334
+ const handleTagsChange = useCallback(
335
+ async (tags: string) => {
336
+ if (!editingTask) return;
337
+ try {
338
+ await mutateOperations(request, `/operations/tasks/${editingTask.id}`, 'PATCH', {
339
+ tags: tags || null,
340
+ });
341
+ } catch {
342
+ // auto-save failure is silent; the value stays in form state
343
+ }
344
+ },
345
+ [editingTask, request]
346
+ );
347
+
348
+ const handleArchive = async () => {
349
+ if (!editingTask) return;
350
+ setArchivingId(editingTask.id);
351
+ try {
352
+ if (onArchive) {
353
+ await onArchive(editingTask.id);
354
+ } else {
355
+ await mutateOperations(
356
+ request,
357
+ `/operations/tasks/${editingTask.id}`,
358
+ 'PATCH',
359
+ { archived: true }
360
+ );
361
+ }
362
+ onSaved();
363
+ close();
364
+ } finally {
365
+ setArchivingId(null);
366
+ }
367
+ };
368
+
369
+ const timesheetProject =
370
+ editingTask?.projectId
371
+ ? {
372
+ id: editingTask.projectId,
373
+ label:
374
+ [editingTask.projectName, editingTask.projectCode]
375
+ .filter(Boolean)
376
+ .join(' • ') || String(editingTask.projectId),
377
+ projectAssignmentId: editingTask.projectAssignmentId,
378
+ }
379
+ : null;
380
+
381
+ return (
382
+ <>
383
+ <Sheet
384
+ open={open}
385
+ onOpenChange={(value) => {
386
+ if (!loading) onOpenChange(value);
387
+ }}
388
+ >
389
+ <SheetContent className="flex w-full flex-col gap-0 overflow-hidden sm:max-w-5xl">
390
+ <SheetHeader className="shrink-0 border-b px-5 pb-3 pt-4">
391
+ <SheetTitle>
392
+ {editingTask
393
+ ? taskT('taskForm.titleEdit')
394
+ : taskT('taskForm.titleNew')}
395
+ </SheetTitle>
396
+ <SheetDescription className="sr-only">
397
+ {editingTask
398
+ ? taskT('taskForm.titleEdit')
399
+ : taskT('taskForm.titleNew')}
400
+ </SheetDescription>
401
+ </SheetHeader>
402
+
403
+ {editingTask ? (
404
+ <>
405
+ <div className="flex flex-1 min-h-0 overflow-hidden">
406
+ {/* ── Coluna esquerda ── */}
407
+ <div className="flex flex-1 flex-col min-h-0 overflow-hidden">
408
+ <div className="shrink-0 space-y-4 overflow-y-auto border-b px-5 py-4">
409
+ <TaskFormFields
410
+ formData={formData}
411
+ setFormData={setFormData}
412
+ statusOptions={KANBAN_COLUMNS}
413
+ mentionItems={mentionItems}
414
+ projectOptions={projectOptionsWithFallback}
415
+ showProject={showProject}
416
+ projectRequired={projectRequired}
417
+ onOpenCreateProject={() => setProjectFormOpen(true)}
418
+ section="main"
419
+ />
420
+ </div>
421
+
422
+ <Tabs
423
+ value={activeTab}
424
+ onValueChange={(v) =>
425
+ setActiveTab(v as 'comments' | 'activities')
426
+ }
427
+ className="flex flex-1 min-h-0 flex-col"
428
+ >
429
+ <div className="shrink-0 border-b px-5 py-2">
430
+ <TabsList className="grid grid-cols-2">
431
+ <TabsTrigger value="comments">
432
+ <MessageSquare className="mr-1.5 size-3.5" />
433
+ {taskT('taskForm.tabComments')}
434
+ </TabsTrigger>
435
+ <TabsTrigger value="activities">
436
+ <History className="mr-1.5 size-3.5" />
437
+ {taskT('taskForm.tabActivities')}
438
+ </TabsTrigger>
439
+ </TabsList>
440
+ </div>
441
+ <TabsContent
442
+ value="comments"
443
+ className="mt-0 flex-1 overflow-y-auto px-5 py-4 data-[state=inactive]:hidden"
444
+ >
445
+ <TaskCommentsSection taskId={editingTask.id} onChanged={onCountChanged} />
446
+ </TabsContent>
447
+ <TabsContent
448
+ value="activities"
449
+ className="mt-0 flex-1 overflow-y-auto px-5 py-4 data-[state=inactive]:hidden"
450
+ >
451
+ <TaskActivitiesSection
452
+ taskId={editingTask.id}
453
+ task={editingTask}
454
+ statusLabel={(status) =>
455
+ KANBAN_COLUMNS.find((column) => column.id === status)
456
+ ?.label ?? status
457
+ }
458
+ />
459
+ </TabsContent>
460
+ </Tabs>
461
+ </div>
462
+
463
+ {/* ── Resize handle ── */}
464
+ <div
465
+ className="group relative w-px shrink-0 cursor-col-resize bg-border transition-colors hover:bg-primary/40"
466
+ onMouseDown={handleResizeMouseDown}
467
+ >
468
+ <div className="absolute inset-y-0 -left-1.5 -right-1.5" />
469
+ <div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col gap-0.75 opacity-0 transition-opacity group-hover:opacity-100">
470
+ <div className="size-0.75 rounded-full bg-primary/60" />
471
+ <div className="size-0.75 rounded-full bg-primary/60" />
472
+ <div className="size-0.75 rounded-full bg-primary/60" />
473
+ <div className="size-0.75 rounded-full bg-primary/60" />
474
+ <div className="size-0.75 rounded-full bg-primary/60" />
475
+ </div>
476
+ </div>
477
+
478
+ {/* ── Aside direito ── */}
479
+ <aside
480
+ className="flex shrink-0 flex-col gap-4 overflow-y-auto px-4 py-4"
481
+ style={{ width: asideWidth }}
482
+ >
483
+ <TaskFormFields
484
+ formData={formData}
485
+ setFormData={setFormData}
486
+ statusOptions={KANBAN_COLUMNS}
487
+ mentionItems={mentionItems}
488
+ projectOptions={projectOptionsWithFallback}
489
+ showDoingTime
490
+ doingMinutes={getElapsedDoingMinutes(editingTask, doingTick)}
491
+ showAttachments
492
+ attachmentTaskId={editingTask.id}
493
+ section="aside"
494
+ showAssignee={showAssignee}
495
+ assigneeOptions={assigneeOptions}
496
+ onTagsChange={(tags) => void handleTagsChange(tags)}
497
+ onFileChanged={onCountChanged}
498
+ />
499
+ </aside>
500
+ </div>
501
+
502
+ <div className="flex flex-wrap items-center justify-between gap-2 border-t px-5 py-3">
503
+ <div className="flex gap-2">
504
+ <Button
505
+ type="button"
506
+ variant="outline"
507
+ disabled={loading || archivingId === editingTask.id}
508
+ onClick={() => void handleArchive()}
509
+ >
510
+ {archivingId === editingTask.id ? (
511
+ <Loader2 className="mr-2 size-4 animate-spin" />
512
+ ) : (
513
+ <Archive className="mr-2 size-4" />
514
+ )}
515
+ {commonT('actions.archive')}
516
+ </Button>
517
+ {timesheetProject ? (
518
+ <Button
519
+ type="button"
520
+ variant="outline"
521
+ disabled={loading}
522
+ onClick={() =>
523
+ onLogHours ? onLogHours() : setIsTimesheetOpen(true)
524
+ }
525
+ >
526
+ <Timer className="mr-2 size-4" />
527
+ {commonT('actions.logHours')}
528
+ </Button>
529
+ ) : null}
530
+ </div>
531
+ <div className="flex gap-2">
532
+ <Button variant="outline" onClick={close} disabled={loading}>
533
+ {commonT('actions.cancel')}
534
+ </Button>
535
+ <Button
536
+ onClick={() => void handleSubmit()}
537
+ disabled={loading || isSubmitDisabled()}
538
+ >
539
+ {loading ? (
540
+ <Loader2 className="mr-2 size-4 animate-spin" />
541
+ ) : null}
542
+ {commonT('actions.save')}
543
+ </Button>
544
+ </div>
545
+ </div>
546
+ </>
547
+ ) : (
548
+ <>
549
+ <div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
550
+ <TaskFormFields
551
+ formData={formData}
552
+ setFormData={setFormData}
553
+ statusOptions={KANBAN_COLUMNS}
554
+ mentionItems={mentionItems}
555
+ projectOptions={projectOptions}
556
+ showProject={showProject}
557
+ projectRequired={projectRequired}
558
+ allowUnassignedProject={!projectRequired}
559
+ onOpenCreateProject={() => setProjectFormOpen(true)}
560
+ showAssignee={showAssignee}
561
+ assigneeOptions={assigneeOptions}
562
+ />
563
+ </div>
564
+
565
+ <div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
566
+ <div className="flex gap-2" />
567
+ <div className="flex gap-2">
568
+ <Button variant="outline" onClick={close} disabled={loading}>
569
+ {commonT('actions.cancel')}
570
+ </Button>
571
+ <Button
572
+ onClick={() => void handleSubmit()}
573
+ disabled={loading || isSubmitDisabled()}
574
+ >
575
+ {loading ? (
576
+ <Loader2 className="mr-2 size-4 animate-spin" />
577
+ ) : null}
578
+ {commonT('actions.create')}
579
+ </Button>
580
+ </div>
581
+ </div>
582
+ </>
583
+ )}
584
+ </SheetContent>
585
+ </Sheet>
586
+
587
+ <Sheet open={projectFormOpen} onOpenChange={setProjectFormOpen}>
588
+ <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
589
+ <SheetHeader>
590
+ <SheetTitle>{projectsT('sheet.createTitle')}</SheetTitle>
591
+ <SheetDescription>
592
+ {projectsT('sheet.description')}
593
+ </SheetDescription>
594
+ </SheetHeader>
595
+ <ProjectFormScreen
596
+ onCancel={() => setProjectFormOpen(false)}
597
+ onSaved={(project: OperationsProjectDetails) => {
598
+ const newOption: OperationsProjectOption = {
599
+ id: project.id,
600
+ label: project.name,
601
+ name: project.name,
602
+ code: project.code,
603
+ clientName: project.clientName,
604
+ status: project.status,
605
+ };
606
+ setLocalProjectOptions((prev) => [newOption, ...prev]);
607
+ setFormData((prev) => ({
608
+ ...prev,
609
+ projectId: String(project.id),
610
+ }));
611
+ setProjectFormOpen(false);
612
+ }}
613
+ />
614
+ </SheetContent>
615
+ </Sheet>
616
+
617
+ {!onLogHours ? (
618
+ <TimesheetEntryCreateSheet
619
+ open={isTimesheetOpen}
620
+ onOpenChange={setIsTimesheetOpen}
621
+ project={timesheetProject}
622
+ task={
623
+ editingTask ? { id: editingTask.id, label: editingTask.name } : null
624
+ }
625
+ />
626
+ ) : null}
627
+ </>
628
+ );
629
+ }