@hed-hog/operations 0.0.330 → 0.0.332

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +5 -5
  2. package/dist/controllers/operations-collaborators.controller.d.ts +58 -213
  3. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  4. package/dist/controllers/operations-collaborators.controller.js +100 -0
  5. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  6. package/dist/controllers/operations-contracts.controller.d.ts +6 -6
  7. package/dist/controllers/operations-projects.controller.d.ts +25 -0
  8. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  9. package/dist/controllers/operations-projects.controller.js +48 -0
  10. package/dist/controllers/operations-projects.controller.js.map +1 -1
  11. package/dist/controllers/operations-reports.controller.d.ts +1 -1
  12. package/dist/controllers/operations-tasks.controller.d.ts +34 -9
  13. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  14. package/dist/controllers/operations-tasks.controller.js +43 -32
  15. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  16. package/dist/controllers/operations-timesheets.controller.d.ts +9 -9
  17. package/dist/dashboard/components/DashboardLayout.d.ts +30 -0
  18. package/dist/dashboard/components/DashboardLayout.d.ts.map +1 -0
  19. package/dist/dashboard/components/DashboardLayout.js +87 -0
  20. package/dist/dashboard/components/DashboardLayout.js.map +1 -0
  21. package/dist/dashboard/components/widget-registry.d.ts +23 -0
  22. package/dist/dashboard/components/widget-registry.d.ts.map +1 -0
  23. package/dist/dashboard/components/widget-registry.js +245 -0
  24. package/dist/dashboard/components/widget-registry.js.map +1 -0
  25. package/dist/dashboard/hooks/useDashboardData.d.ts +20 -0
  26. package/dist/dashboard/hooks/useDashboardData.d.ts.map +1 -0
  27. package/dist/dashboard/hooks/useDashboardData.js +24 -0
  28. package/dist/dashboard/hooks/useDashboardData.js.map +1 -0
  29. package/dist/dashboard/types/widgets.types.d.ts +233 -0
  30. package/dist/dashboard/types/widgets.types.d.ts.map +1 -0
  31. package/dist/dashboard/types/widgets.types.js +6 -0
  32. package/dist/dashboard/types/widgets.types.js.map +1 -0
  33. package/dist/dashboard/widgets/CapacityDistribution.d.ts +23 -0
  34. package/dist/dashboard/widgets/CapacityDistribution.d.ts.map +1 -0
  35. package/dist/dashboard/widgets/CapacityDistribution.js +11 -0
  36. package/dist/dashboard/widgets/CapacityDistribution.js.map +1 -0
  37. package/dist/dashboard/widgets/EffortByProject.d.ts +22 -0
  38. package/dist/dashboard/widgets/EffortByProject.d.ts.map +1 -0
  39. package/dist/dashboard/widgets/EffortByProject.js +11 -0
  40. package/dist/dashboard/widgets/EffortByProject.js.map +1 -0
  41. package/dist/dashboard/widgets/HeadcountByArea.d.ts +24 -0
  42. package/dist/dashboard/widgets/HeadcountByArea.d.ts.map +1 -0
  43. package/dist/dashboard/widgets/HeadcountByArea.js +11 -0
  44. package/dist/dashboard/widgets/HeadcountByArea.js.map +1 -0
  45. package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts +18 -0
  46. package/dist/dashboard/widgets/ManagedProjectsStatus.d.ts.map +1 -0
  47. package/dist/dashboard/widgets/ManagedProjectsStatus.js +12 -0
  48. package/dist/dashboard/widgets/ManagedProjectsStatus.js.map +1 -0
  49. package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts +22 -0
  50. package/dist/dashboard/widgets/MyHoursPeriodKpi.d.ts.map +1 -0
  51. package/dist/dashboard/widgets/MyHoursPeriodKpi.js +12 -0
  52. package/dist/dashboard/widgets/MyHoursPeriodKpi.js.map +1 -0
  53. package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts +19 -0
  54. package/dist/dashboard/widgets/MyOpenRequestsKpi.d.ts.map +1 -0
  55. package/dist/dashboard/widgets/MyOpenRequestsKpi.js +17 -0
  56. package/dist/dashboard/widgets/MyOpenRequestsKpi.js.map +1 -0
  57. package/dist/dashboard/widgets/MyPendingRequestsList.d.ts +23 -0
  58. package/dist/dashboard/widgets/MyPendingRequestsList.d.ts.map +1 -0
  59. package/dist/dashboard/widgets/MyPendingRequestsList.js +14 -0
  60. package/dist/dashboard/widgets/MyPendingRequestsList.js.map +1 -0
  61. package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts +22 -0
  62. package/dist/dashboard/widgets/MyProjectAllocationsKpi.d.ts.map +1 -0
  63. package/dist/dashboard/widgets/MyProjectAllocationsKpi.js +11 -0
  64. package/dist/dashboard/widgets/MyProjectAllocationsKpi.js.map +1 -0
  65. package/dist/dashboard/widgets/MyQuickActions.d.ts +23 -0
  66. package/dist/dashboard/widgets/MyQuickActions.d.ts.map +1 -0
  67. package/dist/dashboard/widgets/MyQuickActions.js +18 -0
  68. package/dist/dashboard/widgets/MyQuickActions.js.map +1 -0
  69. package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts +23 -0
  70. package/dist/dashboard/widgets/MyRelevantDeadlines.d.ts.map +1 -0
  71. package/dist/dashboard/widgets/MyRelevantDeadlines.js +22 -0
  72. package/dist/dashboard/widgets/MyRelevantDeadlines.js.map +1 -0
  73. package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts +17 -0
  74. package/dist/dashboard/widgets/MyTimesheetStatusKpi.d.ts.map +1 -0
  75. package/dist/dashboard/widgets/MyTimesheetStatusKpi.js +11 -0
  76. package/dist/dashboard/widgets/MyTimesheetStatusKpi.js.map +1 -0
  77. package/dist/dashboard/widgets/MyWeeklyJourney.d.ts +21 -0
  78. package/dist/dashboard/widgets/MyWeeklyJourney.d.ts.map +1 -0
  79. package/dist/dashboard/widgets/MyWeeklyJourney.js +19 -0
  80. package/dist/dashboard/widgets/MyWeeklyJourney.js.map +1 -0
  81. package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts +19 -0
  82. package/dist/dashboard/widgets/PortfolioCostsKpi.d.ts.map +1 -0
  83. package/dist/dashboard/widgets/PortfolioCostsKpi.js +12 -0
  84. package/dist/dashboard/widgets/PortfolioCostsKpi.js.map +1 -0
  85. package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts +18 -0
  86. package/dist/dashboard/widgets/PortfolioEffortKpi.d.ts.map +1 -0
  87. package/dist/dashboard/widgets/PortfolioEffortKpi.js +8 -0
  88. package/dist/dashboard/widgets/PortfolioEffortKpi.js.map +1 -0
  89. package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts +22 -0
  90. package/dist/dashboard/widgets/PortfolioProjectsKpi.d.ts.map +1 -0
  91. package/dist/dashboard/widgets/PortfolioProjectsKpi.js +56 -0
  92. package/dist/dashboard/widgets/PortfolioProjectsKpi.js.map +1 -0
  93. package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts +19 -0
  94. package/dist/dashboard/widgets/PortfolioRiskKpi.d.ts.map +1 -0
  95. package/dist/dashboard/widgets/PortfolioRiskKpi.js +11 -0
  96. package/dist/dashboard/widgets/PortfolioRiskKpi.js.map +1 -0
  97. package/dist/dashboard/widgets/ProjectStatusOverview.d.ts +19 -0
  98. package/dist/dashboard/widgets/ProjectStatusOverview.d.ts.map +1 -0
  99. package/dist/dashboard/widgets/ProjectStatusOverview.js +18 -0
  100. package/dist/dashboard/widgets/ProjectStatusOverview.js.map +1 -0
  101. package/dist/dashboard/widgets/StrategicDeadlines.d.ts +24 -0
  102. package/dist/dashboard/widgets/StrategicDeadlines.d.ts.map +1 -0
  103. package/dist/dashboard/widgets/StrategicDeadlines.js +22 -0
  104. package/dist/dashboard/widgets/StrategicDeadlines.js.map +1 -0
  105. package/dist/dashboard/widgets/TeamApprovalQueue.d.ts +24 -0
  106. package/dist/dashboard/widgets/TeamApprovalQueue.d.ts.map +1 -0
  107. package/dist/dashboard/widgets/TeamApprovalQueue.js +12 -0
  108. package/dist/dashboard/widgets/TeamApprovalQueue.js.map +1 -0
  109. package/dist/dashboard/widgets/TeamCapacityKpi.d.ts +18 -0
  110. package/dist/dashboard/widgets/TeamCapacityKpi.d.ts.map +1 -0
  111. package/dist/dashboard/widgets/TeamCapacityKpi.js +19 -0
  112. package/dist/dashboard/widgets/TeamCapacityKpi.js.map +1 -0
  113. package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts +22 -0
  114. package/dist/dashboard/widgets/TeamHeadcountKpi.d.ts.map +1 -0
  115. package/dist/dashboard/widgets/TeamHeadcountKpi.js +56 -0
  116. package/dist/dashboard/widgets/TeamHeadcountKpi.js.map +1 -0
  117. package/dist/dashboard/widgets/TeamHoursKpi.d.ts +19 -0
  118. package/dist/dashboard/widgets/TeamHoursKpi.d.ts.map +1 -0
  119. package/dist/dashboard/widgets/TeamHoursKpi.js +13 -0
  120. package/dist/dashboard/widgets/TeamHoursKpi.js.map +1 -0
  121. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts +20 -0
  122. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.d.ts.map +1 -0
  123. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js +11 -0
  124. package/dist/dashboard/widgets/TeamPendingApprovalsKpi.js.map +1 -0
  125. package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts +18 -0
  126. package/dist/dashboard/widgets/TeamUtilizationOverview.d.ts.map +1 -0
  127. package/dist/dashboard/widgets/TeamUtilizationOverview.js +17 -0
  128. package/dist/dashboard/widgets/TeamUtilizationOverview.js.map +1 -0
  129. package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts +24 -0
  130. package/dist/dashboard/widgets/TeamWorkloadAlerts.d.ts.map +1 -0
  131. package/dist/dashboard/widgets/TeamWorkloadAlerts.js +19 -0
  132. package/dist/dashboard/widgets/TeamWorkloadAlerts.js.map +1 -0
  133. package/dist/dashboard/widgets/index.d.ts +24 -0
  134. package/dist/dashboard/widgets/index.d.ts.map +1 -0
  135. package/dist/dashboard/widgets/index.js +54 -0
  136. package/dist/dashboard/widgets/index.js.map +1 -0
  137. package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
  138. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
  139. package/dist/dto/create-collaborator-invoice.dto.js +55 -0
  140. package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
  141. package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
  142. package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
  143. package/dist/dto/create-collaborator-payment.dto.js +50 -0
  144. package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
  145. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  146. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  147. package/dist/dto/create-collaborator.dto.js +0 -6
  148. package/dist/dto/create-collaborator.dto.js.map +1 -1
  149. package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
  150. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
  151. package/dist/dto/list-collaborator-invoice.dto.js +8 -0
  152. package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
  153. package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
  154. package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
  155. package/dist/dto/list-collaborator-payment.dto.js +8 -0
  156. package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
  157. package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
  158. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
  159. package/dist/dto/update-collaborator-invoice.dto.js +9 -0
  160. package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
  161. package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
  162. package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
  163. package/dist/dto/update-collaborator-payment.dto.js +9 -0
  164. package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
  165. package/dist/index.d.ts +2 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +2 -0
  168. package/dist/index.js.map +1 -1
  169. package/dist/operations.controller.d.ts +42 -0
  170. package/dist/operations.controller.d.ts.map +1 -1
  171. package/dist/operations.service.d.ts +258 -268
  172. package/dist/operations.service.d.ts.map +1 -1
  173. package/dist/operations.service.js +2381 -1341
  174. package/dist/operations.service.js.map +1 -1
  175. package/dist/operations.service.spec.js +345 -174
  176. package/dist/operations.service.spec.js.map +1 -1
  177. package/hedhog/data/dashboard_component.yaml +66 -0
  178. package/hedhog/data/dashboard_item.yaml +25 -25
  179. package/hedhog/data/menu.yaml +27 -8
  180. package/hedhog/data/route.yaml +133 -0
  181. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +78 -102
  182. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
  183. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
  184. package/hedhog/frontend/app/_components/collaborator-picker.tsx.ejs +158 -0
  185. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +247 -50
  186. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +643 -450
  187. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +992 -431
  188. package/hedhog/frontend/app/_components/project-file-attachments.tsx.ejs +371 -0
  189. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +558 -386
  190. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +383 -157
  191. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +4 -1
  192. package/hedhog/frontend/app/_components/task-form-fields.tsx.ejs +406 -0
  193. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -784
  194. package/hedhog/frontend/app/_components/task-info-display.tsx.ejs +137 -0
  195. package/hedhog/frontend/app/_components/timesheet-entry-create-sheet.tsx.ejs +306 -0
  196. package/hedhog/frontend/app/_lib/api.ts.ejs +155 -0
  197. package/hedhog/frontend/app/_lib/types.ts.ejs +62 -0
  198. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +0 -2
  199. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +61 -0
  200. package/hedhog/frontend/app/approvals/page.tsx.ejs +6 -1
  201. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +6 -1
  202. package/hedhog/frontend/app/collaborators/page.tsx.ejs +59 -8
  203. package/hedhog/frontend/app/contracts/page.tsx.ejs +29 -8
  204. package/hedhog/frontend/app/dashboard/widgets/CapacityDistribution.tsx.ejs +84 -0
  205. package/hedhog/frontend/app/dashboard/widgets/EffortByProject.tsx.ejs +85 -0
  206. package/hedhog/frontend/app/dashboard/widgets/HeadcountByArea.tsx.ejs +101 -0
  207. package/hedhog/frontend/app/dashboard/widgets/ManagedProjectsStatus.tsx.ejs +113 -0
  208. package/hedhog/frontend/app/dashboard/widgets/MyHoursPeriodKpi.tsx.ejs +87 -0
  209. package/hedhog/frontend/app/dashboard/widgets/MyOpenRequestsKpi.tsx.ejs +97 -0
  210. package/hedhog/frontend/app/dashboard/widgets/MyPendingRequestsList.tsx.ejs +99 -0
  211. package/hedhog/frontend/app/dashboard/widgets/MyProjectAllocationsKpi.tsx.ejs +78 -0
  212. package/hedhog/frontend/app/dashboard/widgets/MyQuickActions.tsx.ejs +130 -0
  213. package/hedhog/frontend/app/dashboard/widgets/MyRelevantDeadlines.tsx.ejs +144 -0
  214. package/hedhog/frontend/app/dashboard/widgets/MyTimesheetStatusKpi.tsx.ejs +78 -0
  215. package/hedhog/frontend/app/dashboard/widgets/MyWeeklyJourney.tsx.ejs +99 -0
  216. package/hedhog/frontend/app/dashboard/widgets/PortfolioCostsKpi.tsx.ejs +112 -0
  217. package/hedhog/frontend/app/dashboard/widgets/PortfolioEffortKpi.tsx.ejs +93 -0
  218. package/hedhog/frontend/app/dashboard/widgets/PortfolioProjectsKpi.tsx.ejs +96 -0
  219. package/hedhog/frontend/app/dashboard/widgets/PortfolioRiskKpi.tsx.ejs +115 -0
  220. package/hedhog/frontend/app/dashboard/widgets/ProjectStatusOverview.tsx.ejs +120 -0
  221. package/hedhog/frontend/app/dashboard/widgets/StrategicDeadlines.tsx.ejs +146 -0
  222. package/hedhog/frontend/app/dashboard/widgets/TeamApprovalQueue.tsx.ejs +108 -0
  223. package/hedhog/frontend/app/dashboard/widgets/TeamCapacityKpi.tsx.ejs +97 -0
  224. package/hedhog/frontend/app/dashboard/widgets/TeamHeadcountKpi.tsx.ejs +100 -0
  225. package/hedhog/frontend/app/dashboard/widgets/TeamHoursKpi.tsx.ejs +104 -0
  226. package/hedhog/frontend/app/dashboard/widgets/TeamPendingApprovalsKpi.tsx.ejs +110 -0
  227. package/hedhog/frontend/app/dashboard/widgets/TeamUtilizationOverview.tsx.ejs +115 -0
  228. package/hedhog/frontend/app/dashboard/widgets/TeamWorkloadAlerts.tsx.ejs +117 -0
  229. package/hedhog/frontend/app/dashboard/widgets/index.ts.ejs +26 -0
  230. package/hedhog/frontend/app/departments/page.tsx.ejs +6 -1
  231. package/hedhog/frontend/app/my-projects/page.tsx.ejs +30 -12
  232. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +286 -125
  233. package/hedhog/frontend/app/project-cost-categories/page.tsx.ejs +58 -52
  234. package/hedhog/frontend/app/project-cost-types/page.tsx.ejs +58 -51
  235. package/hedhog/frontend/app/projects/page.tsx.ejs +415 -33
  236. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +6 -1
  237. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
  238. package/hedhog/frontend/app/time-off/page.tsx.ejs +6 -1
  239. package/hedhog/frontend/app/timesheets/page.tsx.ejs +10 -4
  240. package/hedhog/frontend/messages/en.json +332 -46
  241. package/hedhog/frontend/messages/operations/en.json +61 -52
  242. package/hedhog/frontend/messages/operations/pt.json +59 -43
  243. package/hedhog/frontend/messages/pt.json +332 -46
  244. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +17 -0
  245. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +17 -0
  246. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +17 -0
  247. package/hedhog/frontend/widgets/index.ts.ejs +25 -0
  248. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +17 -0
  249. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +17 -0
  250. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +17 -0
  251. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +17 -0
  252. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +17 -0
  253. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +17 -0
  254. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +17 -0
  255. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +17 -0
  256. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +17 -0
  257. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +17 -0
  258. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +17 -0
  259. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +17 -0
  260. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +17 -0
  261. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +17 -0
  262. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +170 -0
  263. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +17 -0
  264. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +17 -0
  265. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +17 -0
  266. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +17 -0
  267. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +17 -0
  268. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +17 -0
  269. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +17 -0
  270. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +17 -0
  271. package/hedhog/table/operations_collaborator.yaml +8 -13
  272. package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
  273. package/hedhog/table/operations_collaborator_payment.yaml +32 -0
  274. package/hedhog/table/operations_project.yaml +1 -1
  275. package/hedhog/table/operations_project_file.yaml +23 -0
  276. package/hedhog/table/operations_task.yaml +76 -69
  277. package/hedhog/table/operations_task_activity.yaml +51 -0
  278. package/package.json +6 -5
  279. package/src/controllers/operations-collaborators.controller.ts +117 -8
  280. package/src/controllers/operations-projects.controller.ts +41 -8
  281. package/src/controllers/operations-tasks.controller.ts +156 -166
  282. package/src/dashboard/README.md +214 -0
  283. package/src/dashboard/components/DashboardLayout.tsx +131 -0
  284. package/src/dashboard/components/widget-registry.ts +255 -0
  285. package/src/dashboard/hooks/useDashboardData.ts +29 -0
  286. package/src/dashboard/types/widgets.types.ts +237 -0
  287. package/src/dashboard/widgets/CapacityDistribution.tsx +56 -0
  288. package/src/dashboard/widgets/EffortByProject.tsx +51 -0
  289. package/src/dashboard/widgets/HeadcountByArea.tsx +57 -0
  290. package/src/dashboard/widgets/ManagedProjectsStatus.tsx +53 -0
  291. package/src/dashboard/widgets/MyHoursPeriodKpi.tsx +87 -0
  292. package/src/dashboard/widgets/MyOpenRequestsKpi.tsx +51 -0
  293. package/src/dashboard/widgets/MyPendingRequestsList.tsx +63 -0
  294. package/src/dashboard/widgets/MyProjectAllocationsKpi.tsx +57 -0
  295. package/src/dashboard/widgets/MyQuickActions.tsx +62 -0
  296. package/src/dashboard/widgets/MyRelevantDeadlines.tsx +84 -0
  297. package/src/dashboard/widgets/MyTimesheetStatusKpi.tsx +65 -0
  298. package/src/dashboard/widgets/MyWeeklyJourney.tsx +57 -0
  299. package/src/dashboard/widgets/PortfolioCostsKpi.tsx +48 -0
  300. package/src/dashboard/widgets/PortfolioEffortKpi.tsx +41 -0
  301. package/src/dashboard/widgets/PortfolioRiskKpi.tsx +50 -0
  302. package/src/dashboard/widgets/ProjectStatusOverview.tsx +52 -0
  303. package/src/dashboard/widgets/StrategicDeadlines.tsx +93 -0
  304. package/src/dashboard/widgets/TeamApprovalQueue.tsx +70 -0
  305. package/src/dashboard/widgets/TeamCapacityKpi.tsx +50 -0
  306. package/src/dashboard/widgets/TeamHoursKpi.tsx +51 -0
  307. package/src/dashboard/widgets/TeamPendingApprovalsKpi.tsx +53 -0
  308. package/src/dashboard/widgets/TeamUtilizationOverview.tsx +62 -0
  309. package/src/dashboard/widgets/TeamWorkloadAlerts.tsx +81 -0
  310. package/src/dashboard/widgets/index.ts +26 -0
  311. package/src/dto/create-collaborator-invoice.dto.ts +39 -0
  312. package/src/dto/create-collaborator-payment.dto.ts +35 -0
  313. package/src/dto/create-collaborator.dto.ts +4 -11
  314. package/src/dto/list-collaborator-invoice.dto.ts +3 -0
  315. package/src/dto/list-collaborator-payment.dto.ts +3 -0
  316. package/src/dto/update-collaborator-invoice.dto.ts +6 -0
  317. package/src/dto/update-collaborator-payment.dto.ts +6 -0
  318. package/src/index.ts +3 -0
  319. package/src/operations.service.spec.ts +988 -764
  320. package/src/operations.service.ts +4689 -2624
@@ -0,0 +1,953 @@
1
+ 'use client';
2
+
3
+ import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Calendar } from '@/components/ui/calendar';
7
+ import { Card, CardContent } from '@/components/ui/card';
8
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
9
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from '@/components/ui/popover';
15
+ import { cn } from '@/lib/utils';
16
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
17
+ import {
18
+ AlertCircle,
19
+ CalendarRange,
20
+ Clock3,
21
+ FolderKanban,
22
+ Loader2,
23
+ PlayCircle,
24
+ X,
25
+ } from 'lucide-react';
26
+ import { useTranslations } from 'next-intl';
27
+ import Link from 'next/link';
28
+ import { useMemo, useState } from 'react';
29
+ import { OperationsHeader } from '../_components/operations-header';
30
+ import { StatusBadge } from '../_components/status-badge';
31
+ import {
32
+ TaskDetailSheet,
33
+ type TaskDetailSheetData,
34
+ } from '../_components/task-detail-sheet';
35
+ import { fetchOperations } from '../_lib/api';
36
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
37
+ import type {
38
+ OperationsProject,
39
+ OperationsTaskOption,
40
+ PaginatedResponse,
41
+ } from '../_lib/types';
42
+ import { formatDate, getStatusBadgeClass } from '../_lib/utils/format';
43
+ import { getTaskDescriptionPreview } from '../_lib/utils/task-ui';
44
+
45
+ type RequestFn = (input: {
46
+ url: string;
47
+ method: string;
48
+ data?: unknown;
49
+ }) => Promise<{ data: unknown }>;
50
+
51
+ type TimelineTask = {
52
+ task: OperationsTaskOption;
53
+ project: OperationsProject;
54
+ start: Date;
55
+ end: Date;
56
+ left: number;
57
+ width: number;
58
+ overdue: boolean;
59
+ };
60
+
61
+ const PAGE_SIZE = 200;
62
+ const DAY_WIDTH = 44;
63
+ const LABEL_WIDTH = 320;
64
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
65
+ const TODAY = (() => {
66
+ const d = new Date();
67
+ d.setHours(0, 0, 0, 0);
68
+ return d;
69
+ })();
70
+
71
+ function getPersonAvatarUrl(avatarId?: number | null): string {
72
+ return typeof avatarId === 'number' && avatarId > 0
73
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
74
+ : '/placeholder.png';
75
+ }
76
+
77
+ function getUserPhotoUrl(photoId?: number | null): string {
78
+ return typeof photoId === 'number' && photoId > 0
79
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
80
+ : '/placeholder.png';
81
+ }
82
+
83
+ function getInitials(value?: string | null): string {
84
+ if (!value) return '?';
85
+ return value
86
+ .split(' ')
87
+ .filter(Boolean)
88
+ .slice(0, 2)
89
+ .map((w) => w[0]!.toUpperCase())
90
+ .join('');
91
+ }
92
+
93
+ function parseDate(value?: string | null) {
94
+ if (!value) return null;
95
+ // Date-only strings (YYYY-MM-DD) must be parsed as local time to avoid timezone shift
96
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
97
+ if (m) {
98
+ const date = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
99
+ if (Number.isNaN(date.getTime())) return null;
100
+ return date;
101
+ }
102
+ const date = new Date(value);
103
+ if (Number.isNaN(date.getTime())) return null;
104
+ date.setHours(0, 0, 0, 0);
105
+ return date;
106
+ }
107
+
108
+ function addDays(base: Date, amount: number) {
109
+ const date = new Date(base);
110
+ date.setDate(date.getDate() + amount);
111
+ return date;
112
+ }
113
+
114
+ function diffDays(start: Date, end: Date) {
115
+ return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY);
116
+ }
117
+
118
+ function toDateKey(date: Date) {
119
+ const year = date.getFullYear();
120
+ const month = String(date.getMonth() + 1).padStart(2, '0');
121
+ const day = String(date.getDate()).padStart(2, '0');
122
+ return `${year}-${month}-${day}`;
123
+ }
124
+
125
+ function getEstimatedSpanDays(task: OperationsTaskOption) {
126
+ const estimateHours = Number(task.estimateHours ?? 0);
127
+ if (estimateHours > 0) {
128
+ return Math.max(1, Math.ceil(estimateHours / 8));
129
+ }
130
+
131
+ if (task.status === 'done') return 1;
132
+ if (task.status === 'review') return 2;
133
+ return 3;
134
+ }
135
+
136
+ function buildTaskWindow(
137
+ task: OperationsTaskOption,
138
+ project: OperationsProject
139
+ ) {
140
+ const start =
141
+ parseDate(task.doingStartedAt) ??
142
+ parseDate(task.createdAt) ??
143
+ parseDate(project.startDate) ??
144
+ parseDate(task.dueDate) ??
145
+ new Date();
146
+
147
+ const fallbackEnd = addDays(start, getEstimatedSpanDays(task) - 1);
148
+ const end =
149
+ parseDate(task.dueDate) ?? parseDate(project.endDate) ?? fallbackEnd;
150
+
151
+ if (end < start) {
152
+ return {
153
+ start,
154
+ end: addDays(start, Math.max(1, getEstimatedSpanDays(task)) - 1),
155
+ };
156
+ }
157
+
158
+ return { start, end };
159
+ }
160
+
161
+ function getBarClassName(status?: string | null, overdue?: boolean) {
162
+ if (overdue && status !== 'done') {
163
+ return 'border-rose-300 bg-rose-500/90 text-white shadow-rose-500/20';
164
+ }
165
+
166
+ const classes: Record<string, string> = {
167
+ todo: 'border-slate-300 bg-slate-700 text-white shadow-slate-900/20',
168
+ doing: 'border-sky-300 bg-sky-500 text-white shadow-sky-500/20',
169
+ review: 'border-amber-300 bg-amber-500 text-slate-950 shadow-amber-500/20',
170
+ done: 'border-emerald-300 bg-emerald-500 text-white shadow-emerald-500/20',
171
+ };
172
+
173
+ return classes[status ?? ''] ?? classes.todo;
174
+ }
175
+
176
+ async function fetchAllPages<T>(
177
+ request: RequestFn,
178
+ baseUrl: string,
179
+ pageSize = PAGE_SIZE
180
+ ) {
181
+ const items: T[] = [];
182
+ let page = 1;
183
+ let lastPage = 1;
184
+
185
+ do {
186
+ const separator = baseUrl.includes('?') ? '&' : '?';
187
+ const response = await fetchOperations<PaginatedResponse<T>>(
188
+ request,
189
+ `${baseUrl}${separator}page=${page}&pageSize=${pageSize}`
190
+ );
191
+ items.push(...(response.data ?? []));
192
+ lastPage = Math.max(response.lastPage ?? 1, 1);
193
+ page += 1;
194
+ } while (page <= lastPage);
195
+
196
+ return items;
197
+ }
198
+
199
+ export default function OperationsTasksGanttPage() {
200
+ const t = useTranslations('operations.TasksGanttPage');
201
+ const commonT = useTranslations('operations.Common');
202
+ const { request, currentLocaleCode, getSettingValue } = useApp();
203
+ const access = useOperationsAccess();
204
+
205
+ const [search, setSearch] = useState('');
206
+ const [statusFilter, setStatusFilter] = useState('all');
207
+ const [projectFilter, setProjectFilter] = useState('all');
208
+ const [selectedTask, setSelectedTask] = useState<TaskDetailSheetData | null>(
209
+ null
210
+ );
211
+ const [dateRange, setDateRange] = useState<{ from: Date; to: Date } | null>(
212
+ null
213
+ );
214
+ const [datePickerOpen, setDatePickerOpen] = useState(false);
215
+
216
+ const {
217
+ data: activeProjects = [],
218
+ isLoading: isProjectsLoading,
219
+ error: projectsError,
220
+ refetch: refetchProjects,
221
+ } = useQuery<OperationsProject[]>({
222
+ queryKey: ['operations-tasks-gantt-projects', currentLocaleCode],
223
+ enabled: access.isCollaborator,
224
+ queryFn: () =>
225
+ fetchAllPages<OperationsProject>(
226
+ request as RequestFn,
227
+ '/operations/projects?status=active&sortField=startDate&sortOrder=asc'
228
+ ),
229
+ });
230
+
231
+ const {
232
+ data: allTasks = [],
233
+ isLoading: isTasksLoading,
234
+ error: tasksError,
235
+ refetch: refetchTasks,
236
+ } = useQuery<OperationsTaskOption[]>({
237
+ queryKey: ['operations-tasks-gantt-my-tasks', currentLocaleCode],
238
+ enabled: access.isCollaborator,
239
+ queryFn: () =>
240
+ fetchAllPages<OperationsTaskOption>(
241
+ request as RequestFn,
242
+ '/operations/my-tasks?sortField=createdAt&sortOrder=desc'
243
+ ),
244
+ });
245
+
246
+ const activeProjectMap = useMemo(
247
+ () => new Map(activeProjects.map((project) => [project.id, project])),
248
+ [activeProjects]
249
+ );
250
+
251
+ const projectOptions = useMemo(() => {
252
+ const seen = new Map<string, string>();
253
+ for (const task of allTasks) {
254
+ const key = String(task.projectId);
255
+ if (!seen.has(key)) {
256
+ seen.set(
257
+ key,
258
+ [task.projectName, task.projectCode].filter(Boolean).join(' • ')
259
+ );
260
+ }
261
+ }
262
+ return Array.from(seen.entries())
263
+ .map(([value, label]) => ({ value, label }))
264
+ .sort((a, b) => a.label.localeCompare(b.label, currentLocaleCode));
265
+ }, [allTasks, currentLocaleCode]);
266
+
267
+ const filteredTasks = useMemo(() => {
268
+ const normalizedSearch = search.trim().toLowerCase();
269
+
270
+ return allTasks
271
+ .filter((task) => statusFilter === 'all' || task.status === statusFilter)
272
+ .filter(
273
+ (task) =>
274
+ projectFilter === 'all' || String(task.projectId) === projectFilter
275
+ )
276
+ .filter((task) => {
277
+ if (!normalizedSearch) return true;
278
+
279
+ const haystack = [
280
+ task.name,
281
+ task.projectName,
282
+ task.projectCode,
283
+ task.assigneeName,
284
+ task.description ? getTaskDescriptionPreview(task.description) : '',
285
+ ]
286
+ .filter(Boolean)
287
+ .join(' ')
288
+ .toLowerCase();
289
+
290
+ return haystack.includes(normalizedSearch);
291
+ });
292
+ }, [allTasks, projectFilter, search, statusFilter]);
293
+
294
+ const timelineBase = useMemo(() => {
295
+ return filteredTasks
296
+ .map((task) => {
297
+ const project =
298
+ activeProjectMap.get(task.projectId) ??
299
+ ({
300
+ id: task.projectId,
301
+ code: task.projectCode ?? '',
302
+ name: task.projectName,
303
+ status: 'active',
304
+ startDate: null,
305
+ endDate: null,
306
+ } as OperationsProject);
307
+
308
+ const { start, end } = buildTaskWindow(task, project);
309
+ const dueDate = parseDate(task.dueDate);
310
+
311
+ return {
312
+ task,
313
+ project,
314
+ start,
315
+ end,
316
+ overdue:
317
+ task.status !== 'done' &&
318
+ dueDate !== null &&
319
+ dueDate.getTime() < TODAY.getTime(),
320
+ };
321
+ })
322
+ .filter((item): item is Omit<TimelineTask, 'left' | 'width'> =>
323
+ Boolean(item)
324
+ )
325
+ .filter((item) => {
326
+ if (!dateRange) return true;
327
+ return item.start <= dateRange.to && item.end >= dateRange.from;
328
+ })
329
+ .sort((a, b) => {
330
+ const byProject = a.project.name.localeCompare(
331
+ b.project.name,
332
+ currentLocaleCode
333
+ );
334
+ if (byProject !== 0) return byProject;
335
+ const byStart = a.start.getTime() - b.start.getTime();
336
+ if (byStart !== 0) return byStart;
337
+ return a.task.name.localeCompare(b.task.name, currentLocaleCode);
338
+ });
339
+ }, [activeProjectMap, currentLocaleCode, dateRange, filteredTasks]);
340
+
341
+ const timelineBounds = useMemo(() => {
342
+ let start: Date;
343
+ let end: Date;
344
+
345
+ if (dateRange) {
346
+ start = addDays(dateRange.from, -1);
347
+ end = addDays(dateRange.to, 1);
348
+ } else {
349
+ if (!timelineBase.length) return null;
350
+ const [firstItem] = timelineBase;
351
+ if (!firstItem) return null;
352
+
353
+ const minStart = timelineBase.reduce(
354
+ (earliest, item) => (item.start < earliest ? item.start : earliest),
355
+ firstItem.start
356
+ );
357
+ const maxEnd = timelineBase.reduce(
358
+ (latest, item) => (item.end > latest ? item.end : latest),
359
+ firstItem.end
360
+ );
361
+
362
+ start = addDays(minStart, -1);
363
+ end = addDays(maxEnd, 1);
364
+ }
365
+
366
+ const totalDays = diffDays(start, end) + 1;
367
+ const todayOffset =
368
+ TODAY >= start && TODAY <= end ? diffDays(start, TODAY) : null;
369
+
370
+ const days = Array.from({ length: totalDays }, (_, index) =>
371
+ addDays(start, index)
372
+ );
373
+
374
+ return {
375
+ start,
376
+ end,
377
+ totalDays,
378
+ width: totalDays * DAY_WIDTH,
379
+ days,
380
+ todayOffset,
381
+ };
382
+ }, [dateRange, timelineBase]);
383
+
384
+ const timelineTasks = useMemo(() => {
385
+ if (!timelineBounds) return [];
386
+
387
+ return timelineBase.map((item) => {
388
+ const offsetDays = diffDays(timelineBounds.start, item.start);
389
+ const spanDays = Math.max(1, diffDays(item.start, item.end) + 1);
390
+
391
+ return {
392
+ ...item,
393
+ left: offsetDays * DAY_WIDTH + 4,
394
+ width: Math.max(DAY_WIDTH - 8, spanDays * DAY_WIDTH - 8),
395
+ };
396
+ });
397
+ }, [timelineBase, timelineBounds]);
398
+
399
+ const groupedProjects = useMemo(() => {
400
+ const groups = new Map<
401
+ number,
402
+ { project: OperationsProject; tasks: TimelineTask[] }
403
+ >();
404
+
405
+ for (const item of timelineTasks) {
406
+ const current = groups.get(item.project.id);
407
+ if (current) {
408
+ current.tasks.push(item);
409
+ } else {
410
+ groups.set(item.project.id, {
411
+ project: item.project,
412
+ tasks: [item],
413
+ });
414
+ }
415
+ }
416
+
417
+ return Array.from(groups.values()).sort((a, b) => {
418
+ const aStart = parseDate(a.project.startDate);
419
+ const bStart = parseDate(b.project.startDate);
420
+ if (aStart && bStart && aStart.getTime() !== bStart.getTime()) {
421
+ return aStart.getTime() - bStart.getTime();
422
+ }
423
+ return a.project.name.localeCompare(b.project.name, currentLocaleCode);
424
+ });
425
+ }, [currentLocaleCode, timelineTasks]);
426
+
427
+ const statsCards = useMemo(
428
+ () => [
429
+ {
430
+ key: 'active-projects',
431
+ title: t('cards.activeProjects'),
432
+ description: t('cards.activeProjectsDescription'),
433
+ value: groupedProjects.length,
434
+ icon: FolderKanban,
435
+ },
436
+ {
437
+ key: 'tasks',
438
+ title: t('cards.tasks'),
439
+ description: t('cards.tasksDescription'),
440
+ value: timelineTasks.length,
441
+ icon: CalendarRange,
442
+ },
443
+ {
444
+ key: 'doing',
445
+ title: t('cards.doing'),
446
+ description: t('cards.doingDescription'),
447
+ value: timelineTasks.filter((item) => item.task.status === 'doing')
448
+ .length,
449
+ icon: PlayCircle,
450
+ },
451
+ {
452
+ key: 'overdue',
453
+ title: t('cards.overdue'),
454
+ description: t('cards.overdueDescription'),
455
+ value: timelineTasks.filter((item) => item.overdue).length,
456
+ icon: AlertCircle,
457
+ },
458
+ ],
459
+ [groupedProjects.length, t, timelineTasks]
460
+ );
461
+
462
+ const isLoading = access.isLoading || isProjectsLoading || isTasksLoading;
463
+ const hasError = Boolean(projectsError || tasksError);
464
+
465
+ const timelineRowBackground = timelineBounds
466
+ ? {
467
+ backgroundImage: `repeating-linear-gradient(to right, transparent, transparent ${
468
+ DAY_WIDTH - 1
469
+ }px, hsl(var(--border)) ${DAY_WIDTH - 1}px, hsl(var(--border)) ${DAY_WIDTH}px)`,
470
+ }
471
+ : undefined;
472
+
473
+ const monthFormatter = useMemo(
474
+ () =>
475
+ new Intl.DateTimeFormat(currentLocaleCode, {
476
+ month: 'short',
477
+ }),
478
+ [currentLocaleCode]
479
+ );
480
+
481
+ if (!access.isLoading && !access.isCollaborator) {
482
+ return (
483
+ <Page>
484
+ <OperationsHeader
485
+ title={t('title')}
486
+ description={t('description')}
487
+ current={t('breadcrumb')}
488
+ />
489
+
490
+ <div className="pt-2">
491
+ <EmptyState
492
+ icon={<AlertCircle className="size-12" />}
493
+ title={commonT('states.noAccessTitle')}
494
+ description={t('noAccessDescription')}
495
+ />
496
+ </div>
497
+ </Page>
498
+ );
499
+ }
500
+
501
+ return (
502
+ <Page>
503
+ <OperationsHeader
504
+ title={t('title')}
505
+ description={t('description')}
506
+ current={t('breadcrumb')}
507
+ />
508
+
509
+ <KpiCardsGrid items={statsCards} columns={4} />
510
+
511
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
512
+ <SearchBar
513
+ className="w-auto"
514
+ searchQuery={search}
515
+ onSearchChange={setSearch}
516
+ showSearchButton={false}
517
+ debounceMs={400}
518
+ placeholder={t('searchPlaceholder')}
519
+ controls={[
520
+ {
521
+ id: 'status',
522
+ type: 'select',
523
+ value: statusFilter,
524
+ onChange: setStatusFilter,
525
+ placeholder: commonT('labels.status'),
526
+ options: [
527
+ { value: 'all', label: t('filters.allStatuses') },
528
+ { value: 'todo', label: t('legend.todo') },
529
+ { value: 'doing', label: t('legend.doing') },
530
+ { value: 'review', label: t('legend.review') },
531
+ { value: 'done', label: t('legend.done') },
532
+ ],
533
+ },
534
+ {
535
+ id: 'project',
536
+ type: 'select',
537
+ value: projectFilter,
538
+ onChange: setProjectFilter,
539
+ placeholder: commonT('labels.project'),
540
+ options: [
541
+ { value: 'all', label: t('filters.allProjects') },
542
+ ...projectOptions,
543
+ ],
544
+ },
545
+ ]}
546
+ />
547
+ </div>
548
+
549
+ {isLoading ? (
550
+ <Card className="border-dashed">
551
+ <CardContent className="flex min-h-80 items-center justify-center gap-3 text-muted-foreground">
552
+ <Loader2 className="size-5 animate-spin" />
553
+ <span>{t('loading')}</span>
554
+ </CardContent>
555
+ </Card>
556
+ ) : hasError ? (
557
+ <EmptyState
558
+ icon={<AlertCircle className="size-12" />}
559
+ title={commonT('states.emptyTitle')}
560
+ description={t('loadErrorDescription')}
561
+ actionLabel={commonT('actions.refresh')}
562
+ onAction={() => {
563
+ void refetchProjects();
564
+ void refetchTasks();
565
+ }}
566
+ />
567
+ ) : !timelineBounds || groupedProjects.length === 0 ? (
568
+ <EmptyState
569
+ icon={<CalendarRange className="size-12" />}
570
+ title={commonT('states.emptyTitle')}
571
+ description={t('emptyDescription')}
572
+ actionLabel={commonT('actions.refresh')}
573
+ onAction={() => {
574
+ void refetchProjects();
575
+ void refetchTasks();
576
+ }}
577
+ />
578
+ ) : (
579
+ <div className="overflow-hidden rounded-3xl border bg-card shadow-sm">
580
+ <div className="flex flex-wrap items-center gap-2 border-b bg-muted/20 px-4 py-3">
581
+ <Popover open={datePickerOpen} onOpenChange={setDatePickerOpen}>
582
+ <PopoverTrigger asChild>
583
+ <Badge
584
+ variant="secondary"
585
+ className="cursor-pointer gap-1.5 hover:bg-secondary/80"
586
+ >
587
+ <Clock3 className="size-3.5" />
588
+ {t('labels.range', {
589
+ from: formatDate(
590
+ toDateKey(dateRange?.from ?? timelineBounds.start),
591
+ getSettingValue,
592
+ currentLocaleCode
593
+ ),
594
+ to: formatDate(
595
+ toDateKey(dateRange?.to ?? timelineBounds.end),
596
+ getSettingValue,
597
+ currentLocaleCode
598
+ ),
599
+ })}
600
+ {dateRange ? <X className="size-3 opacity-70" /> : null}
601
+ </Badge>
602
+ </PopoverTrigger>
603
+ <PopoverContent className="w-auto p-0" align="start">
604
+ <Calendar
605
+ mode="range"
606
+ selected={
607
+ dateRange
608
+ ? { from: dateRange.from, to: dateRange.to }
609
+ : undefined
610
+ }
611
+ onSelect={(range) => {
612
+ if (range?.from && range?.to) {
613
+ setDateRange({ from: range.from, to: range.to });
614
+ setDatePickerOpen(false);
615
+ } else if (range?.from && !range?.to) {
616
+ setDateRange(null);
617
+ }
618
+ }}
619
+ numberOfMonths={2}
620
+ />
621
+ {dateRange ? (
622
+ <div className="border-t p-2">
623
+ <Button
624
+ variant="ghost"
625
+ size="sm"
626
+ className="w-full"
627
+ onClick={() => {
628
+ setDateRange(null);
629
+ setDatePickerOpen(false);
630
+ }}
631
+ >
632
+ <X className="mr-2 size-3" />
633
+ {t('filters.clearDateRange')}
634
+ </Button>
635
+ </div>
636
+ ) : null}
637
+ </PopoverContent>
638
+ </Popover>
639
+
640
+ {(
641
+ [
642
+ {
643
+ status: 'todo',
644
+ className:
645
+ 'border-slate-400 bg-slate-700 text-white hover:bg-slate-600',
646
+ },
647
+ {
648
+ status: 'doing',
649
+ className:
650
+ 'border-sky-300 bg-sky-500 text-white hover:bg-sky-400',
651
+ },
652
+ {
653
+ status: 'review',
654
+ className:
655
+ 'border-amber-300 bg-amber-500 text-slate-950 hover:bg-amber-400',
656
+ },
657
+ {
658
+ status: 'done',
659
+ className:
660
+ 'border-emerald-300 bg-emerald-500 text-white hover:bg-emerald-400',
661
+ },
662
+ ] as const
663
+ ).map(({ status, className }) => (
664
+ <Badge
665
+ key={status}
666
+ className={cn(
667
+ 'cursor-pointer transition',
668
+ className,
669
+ statusFilter === status
670
+ ? 'ring-2 ring-white/60 ring-offset-1'
671
+ : 'opacity-60 hover:opacity-100'
672
+ )}
673
+ onClick={() =>
674
+ setStatusFilter(statusFilter === status ? 'all' : status)
675
+ }
676
+ >
677
+ {t(`legend.${status}`)}
678
+ </Badge>
679
+ ))}
680
+
681
+ <Badge className="border-rose-300 bg-rose-500/10 text-rose-700 hover:bg-rose-500/10">
682
+ {t('legend.overdue')}
683
+ </Badge>
684
+ </div>
685
+
686
+ <div className="overflow-auto">
687
+ <div
688
+ style={{
689
+ minWidth: LABEL_WIDTH + timelineBounds.width,
690
+ }}
691
+ >
692
+ <div
693
+ className="sticky top-0 z-30 grid border-b bg-background/95 backdrop-blur"
694
+ style={{
695
+ gridTemplateColumns: `${LABEL_WIDTH}px ${timelineBounds.width}px`,
696
+ }}
697
+ >
698
+ <div className="sticky left-0 z-30 border-r bg-background px-4 py-3">
699
+ <p className="text-sm font-semibold">
700
+ {t('labels.projectTask')}
701
+ </p>
702
+ <p className="text-xs text-muted-foreground">
703
+ {t('viewDescription')}
704
+ </p>
705
+ </div>
706
+
707
+ <div className="relative">
708
+ <div
709
+ className="grid"
710
+ style={{
711
+ gridTemplateColumns: `repeat(${timelineBounds.totalDays}, ${DAY_WIDTH}px)`,
712
+ }}
713
+ >
714
+ {timelineBounds.days.map((day, index) => (
715
+ <div
716
+ key={toDateKey(day)}
717
+ className="border-r px-1 py-2 text-center"
718
+ title={formatDate(
719
+ toDateKey(day),
720
+ getSettingValue,
721
+ currentLocaleCode
722
+ )}
723
+ >
724
+ <div className="text-xs font-semibold">
725
+ {day.getDate()}
726
+ </div>
727
+ <div className="text-[10px] uppercase text-muted-foreground">
728
+ {index === 0 || day.getDate() === 1
729
+ ? monthFormatter.format(day)
730
+ : ' '}
731
+ </div>
732
+ </div>
733
+ ))}
734
+ </div>
735
+
736
+ {timelineBounds.todayOffset !== null ? (
737
+ <div
738
+ className="pointer-events-none absolute inset-y-0 w-0.5 bg-primary/80"
739
+ style={{
740
+ left:
741
+ timelineBounds.todayOffset * DAY_WIDTH +
742
+ DAY_WIDTH / 2,
743
+ }}
744
+ />
745
+ ) : null}
746
+ </div>
747
+ </div>
748
+
749
+ {groupedProjects.map((group) => (
750
+ <div
751
+ key={group.project.id}
752
+ className="border-b last:border-b-0"
753
+ >
754
+ <div
755
+ className="grid border-b bg-muted/10"
756
+ style={{
757
+ gridTemplateColumns: `${LABEL_WIDTH}px ${timelineBounds.width}px`,
758
+ }}
759
+ >
760
+ <div className="sticky left-0 z-20 border-r bg-card px-4 py-3">
761
+ <div className="flex items-start justify-between gap-3">
762
+ <div className="min-w-0">
763
+ <p className="truncate text-sm font-semibold">
764
+ {group.project.name}
765
+ </p>
766
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
767
+ {group.project.clientName ? (
768
+ <>
769
+ <span className="truncate">
770
+ {group.project.code || '—'}
771
+ </span>
772
+ <span>•</span>
773
+ <Avatar className="h-4 w-4 shrink-0">
774
+ <AvatarImage
775
+ src={
776
+ group.project.clientUserPhotoId
777
+ ? getUserPhotoUrl(
778
+ group.project.clientUserPhotoId
779
+ )
780
+ : getPersonAvatarUrl(
781
+ group.project.clientAvatarId
782
+ )
783
+ }
784
+ alt={group.project.clientName}
785
+ />
786
+ <AvatarFallback className="text-[8px] font-medium">
787
+ {getInitials(group.project.clientName)}
788
+ </AvatarFallback>
789
+ </Avatar>
790
+ <span className="truncate">
791
+ {group.project.clientName}
792
+ </span>
793
+ </>
794
+ ) : (
795
+ <span className="truncate">
796
+ {group.project.code ||
797
+ commonT('labels.notAvailable')}
798
+ </span>
799
+ )}
800
+ </div>
801
+ </div>
802
+ <Badge variant="secondary">
803
+ {t('labels.tasksCount', {
804
+ count: group.tasks.length,
805
+ })}
806
+ </Badge>
807
+ </div>
808
+ </div>
809
+
810
+ <div
811
+ className="relative h-12 border-l-0"
812
+ style={timelineRowBackground}
813
+ >
814
+ {timelineBounds.todayOffset !== null ? (
815
+ <div
816
+ className="pointer-events-none absolute inset-y-0 w-0.5 bg-primary/80"
817
+ style={{
818
+ left:
819
+ timelineBounds.todayOffset * DAY_WIDTH +
820
+ DAY_WIDTH / 2,
821
+ }}
822
+ />
823
+ ) : null}
824
+ </div>
825
+ </div>
826
+
827
+ {group.tasks.map((item) => (
828
+ <div
829
+ key={item.task.id}
830
+ className="grid border-b last:border-b-0"
831
+ style={{
832
+ gridTemplateColumns: `${LABEL_WIDTH}px ${timelineBounds.width}px`,
833
+ }}
834
+ >
835
+ <button
836
+ type="button"
837
+ className="sticky left-0 z-10 flex cursor-pointer flex-col gap-2 border-r bg-background px-4 py-3 text-left transition hover:bg-muted/40"
838
+ onClick={() => setSelectedTask(item.task)}
839
+ >
840
+ <div className="flex items-start justify-between gap-2">
841
+ <div className="min-w-0">
842
+ <p className="truncate text-sm font-medium">
843
+ {item.task.name}
844
+ </p>
845
+ <p className="truncate text-xs text-muted-foreground">
846
+ {item.task.assigneeName ??
847
+ commonT('labels.notAvailable')}
848
+ </p>
849
+ </div>
850
+ <StatusBadge
851
+ label={t(`legend.${item.task.status}`)}
852
+ className={getStatusBadgeClass(item.task.status)}
853
+ />
854
+ </div>
855
+
856
+ {item.task.description ? (
857
+ <p className="line-clamp-2 text-xs text-muted-foreground">
858
+ {getTaskDescriptionPreview(item.task.description)}
859
+ </p>
860
+ ) : null}
861
+ </button>
862
+
863
+ <div
864
+ className="relative h-18"
865
+ style={timelineRowBackground}
866
+ >
867
+ {timelineBounds.todayOffset !== null ? (
868
+ <div
869
+ className="pointer-events-none absolute inset-y-0 w-0.5 bg-primary/80"
870
+ style={{
871
+ left:
872
+ timelineBounds.todayOffset * DAY_WIDTH +
873
+ DAY_WIDTH / 2,
874
+ }}
875
+ />
876
+ ) : null}
877
+
878
+ <button
879
+ type="button"
880
+ className={[
881
+ 'absolute top-1/2 flex h-10 -translate-y-1/2 cursor-pointer items-center gap-2 overflow-hidden rounded-2xl border px-3 text-left shadow-lg transition hover:brightness-95',
882
+ getBarClassName(item.task.status, item.overdue),
883
+ ].join(' ')}
884
+ style={{
885
+ left: item.left,
886
+ width: item.width,
887
+ }}
888
+ onClick={() => setSelectedTask(item.task)}
889
+ title={`${item.task.name} • ${formatDate(
890
+ toDateKey(item.start),
891
+ getSettingValue,
892
+ currentLocaleCode
893
+ )} - ${formatDate(
894
+ toDateKey(item.end),
895
+ getSettingValue,
896
+ currentLocaleCode
897
+ )}`}
898
+ >
899
+ <span className="truncate text-xs font-semibold">
900
+ {item.task.name}
901
+ </span>
902
+ <span className="hidden truncate text-[11px] opacity-90 md:inline">
903
+ {formatDate(
904
+ toDateKey(item.start),
905
+ getSettingValue,
906
+ currentLocaleCode
907
+ )}{' '}
908
+ -{' '}
909
+ {formatDate(
910
+ toDateKey(item.end),
911
+ getSettingValue,
912
+ currentLocaleCode
913
+ )}
914
+ </span>
915
+ </button>
916
+ </div>
917
+ </div>
918
+ ))}
919
+ </div>
920
+ ))}
921
+ </div>
922
+ </div>
923
+ </div>
924
+ )}
925
+
926
+ <TaskDetailSheet
927
+ task={selectedTask}
928
+ open={selectedTask !== null}
929
+ defaultTab="comments"
930
+ onOpenChange={(open) => {
931
+ if (!open) setSelectedTask(null);
932
+ }}
933
+ statusLabel={(status) => {
934
+ try {
935
+ return t(`legend.${status}`);
936
+ } catch {
937
+ return status;
938
+ }
939
+ }}
940
+ footer={
941
+ selectedTask?.projectId ? (
942
+ <Button asChild variant="outline" className="gap-2">
943
+ <Link href={`/operations/my-projects/${selectedTask.projectId}`}>
944
+ <FolderKanban className="size-4" />
945
+ {t('actions.openProject')}
946
+ </Link>
947
+ </Button>
948
+ ) : null
949
+ }
950
+ />
951
+ </Page>
952
+ );
953
+ }