@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
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const common_1 = require("@nestjs/common");
5
5
  const operations_service_1 = require("./operations.service");
6
6
  const operations_access_service_1 = require("./services/shared/operations-access.service");
7
- describe('OperationsService proposal integration', () => {
7
+ describe("OperationsService proposal integration", () => {
8
8
  let service;
9
9
  let prisma;
10
10
  let integrationApi;
@@ -16,37 +16,43 @@ describe('OperationsService proposal integration', () => {
16
16
  publishEvent: jest.fn().mockResolvedValue(undefined),
17
17
  };
18
18
  service = new operations_service_1.OperationsService(prisma, {}, integrationApi, {}, {}, new operations_access_service_1.OperationsAccessService());
19
- jest.spyOn(service, 'generateContractCode').mockResolvedValue('CTR-001');
20
- jest.spyOn(service, 'replaceContractParties').mockResolvedValue(undefined);
21
- jest.spyOn(service, 'insertContractHistory').mockResolvedValue(undefined);
19
+ jest
20
+ .spyOn(service, "generateContractCode")
21
+ .mockResolvedValue("CTR-001");
22
+ jest
23
+ .spyOn(service, "replaceContractParties")
24
+ .mockResolvedValue(undefined);
25
+ jest
26
+ .spyOn(service, "insertContractHistory")
27
+ .mockResolvedValue(undefined);
22
28
  });
23
29
  afterEach(() => {
24
30
  jest.restoreAllMocks();
25
31
  });
26
- it('throws when proposalId is missing', async () => {
32
+ it("throws when proposalId is missing", async () => {
27
33
  await expect(service.createContractFromProposalIntegration({})).rejects.toThrow(common_1.BadRequestException);
28
34
  });
29
- it('returns the existing contract when the CRM proposal was already converted', async () => {
35
+ it("returns the existing contract when the CRM proposal was already converted", async () => {
30
36
  const tx = {
31
37
  $queryRawUnsafe: jest
32
38
  .fn()
33
39
  .mockResolvedValueOnce([])
34
- .mockResolvedValueOnce([{ id: 42, code: 'CTR-EXISTING' }]),
40
+ .mockResolvedValueOnce([{ id: 42, code: "CTR-EXISTING" }]),
35
41
  };
36
42
  prisma.$transaction.mockImplementation(async (callback) => callback(tx));
37
43
  const result = await service.createContractFromProposalIntegration({
38
44
  proposalId: 1001,
39
- title: 'Already converted proposal',
45
+ title: "Already converted proposal",
40
46
  });
41
47
  expect(result).toEqual({
42
48
  id: 42,
43
- code: 'CTR-EXISTING',
49
+ code: "CTR-EXISTING",
44
50
  });
45
51
  expect(service.generateContractCode).not.toHaveBeenCalled();
46
52
  expect(service.replaceContractParties).not.toHaveBeenCalled();
47
53
  expect(integrationApi.publishEvent).not.toHaveBeenCalled();
48
54
  });
49
- it('creates a draft contract with CRM origin metadata and emits an event', async () => {
55
+ it("creates a draft contract with CRM origin metadata and emits an event", async () => {
50
56
  const tx = {
51
57
  $queryRawUnsafe: jest
52
58
  .fn()
@@ -60,74 +66,74 @@ describe('OperationsService proposal integration', () => {
60
66
  proposalRevisionId: 21,
61
67
  personId: 5,
62
68
  approvedByUserId: 9,
63
- correlationId: 'proposal:1001',
64
- code: 'P-1001',
65
- title: 'Implementation Proposal',
69
+ correlationId: "proposal:1001",
70
+ code: "P-1001",
71
+ title: "Implementation Proposal",
66
72
  total: 1234,
67
- currency: 'USD',
68
- locale: 'en',
73
+ currency: "USD",
74
+ locale: "en",
69
75
  commercialTerms: {
70
- contractCategory: 'client',
71
- contractType: 'service_agreement',
72
- billingModel: 'monthly_retainer',
73
- validFrom: '2025-01-10',
74
- validUntil: '2025-12-31',
75
- notes: 'Annual support agreement',
76
+ contractCategory: "client",
77
+ contractType: "service_agreement",
78
+ billingModel: "monthly_retainer",
79
+ validFrom: "2025-01-10",
80
+ validUntil: "2025-12-31",
81
+ notes: "Annual support agreement",
76
82
  },
77
83
  person: {
78
84
  id: 5,
79
- name: 'Acme Contact',
80
- tradeName: 'Acme Ltd',
81
- email: 'ops@acme.test',
82
- phone: '555-0100',
83
- document: '12.345.678/0001-90',
85
+ name: "Acme Contact",
86
+ tradeName: "Acme Ltd",
87
+ email: "ops@acme.test",
88
+ phone: "555-0100",
89
+ document: "12.345.678/0001-90",
84
90
  },
85
91
  revision: {
86
92
  id: 21,
87
- title: 'Revision 1',
88
- summary: 'Approved commercial terms',
89
- contentHtml: '<p>draft</p>',
93
+ title: "Revision 1",
94
+ summary: "Approved commercial terms",
95
+ contentHtml: "<p>draft</p>",
90
96
  },
91
97
  items: [
92
98
  {
93
- name: 'Monthly retainer',
94
- description: 'Support hours',
99
+ name: "Monthly retainer",
100
+ description: "Support hours",
95
101
  amount: 1000,
96
- recurrence: 'monthly',
102
+ recurrence: "monthly",
97
103
  },
98
104
  {
99
- name: 'Setup fee',
105
+ name: "Setup fee",
100
106
  amount: 234,
101
- recurrence: 'one_time',
107
+ recurrence: "one_time",
102
108
  },
103
109
  ],
104
110
  };
105
111
  const result = await service.createContractFromProposalIntegration(payload);
106
112
  expect(result).toEqual({
107
113
  id: 77,
108
- code: 'CTR-001',
114
+ code: "CTR-001",
109
115
  });
110
116
  const insertCall = tx.$queryRawUnsafe.mock.calls[2];
111
- expect(insertCall[0]).toContain('INSERT INTO operations_contract');
112
- expect(insertCall).toContain('crm_proposal');
113
- expect(insertCall).toContain('1001');
117
+ expect(insertCall[0]).toContain("INSERT INTO operations_contract");
118
+ expect(insertCall).toContain("crm_proposal");
119
+ expect(insertCall).toContain("1001");
114
120
  expect(service.replaceContractParties).toHaveBeenCalledWith(tx, 77, [
115
121
  expect.objectContaining({
116
- partyRole: 'client',
117
- displayName: 'Acme Ltd',
122
+ partyRole: "client",
123
+ displayName: "Acme Ltd",
118
124
  isPrimary: true,
119
125
  }),
120
126
  ]);
121
- expect(service.insertContractHistory).toHaveBeenCalledWith(tx, 77, 9, 'created', 'Draft contract generated from CRM proposal P-1001.', expect.any(String));
127
+ expect(service.insertContractHistory).toHaveBeenCalledWith(tx, 77, 9, "created", "Draft contract generated from CRM proposal P-1001.", expect.any(String));
122
128
  expect(integrationApi.publishEvent).toHaveBeenCalledWith(expect.objectContaining({
123
- eventName: 'operations.contract.created',
129
+ eventName: "operations.contract.created",
124
130
  payload: expect.objectContaining({
125
131
  proposalId: 1001,
126
132
  contract: expect.objectContaining({
127
133
  id: 77,
128
- originType: 'crm_proposal',
129
- originId: '1001',
130
- contractType: 'service_agreement',
134
+ originType: "crm_proposal",
135
+ originId: "1001",
136
+ contractType: "service_agreement",
131
137
  }),
132
138
  }),
133
139
  }), expect.objectContaining({
@@ -135,7 +141,7 @@ describe('OperationsService proposal integration', () => {
135
141
  }));
136
142
  });
137
143
  });
138
- describe('OperationsService quick-entry timesheets', () => {
144
+ describe("OperationsService quick-entry timesheets", () => {
139
145
  let service;
140
146
  beforeEach(() => {
141
147
  service = new operations_service_1.OperationsService({
@@ -143,11 +149,11 @@ describe('OperationsService quick-entry timesheets', () => {
143
149
  }, {}, {
144
150
  publishEvent: jest.fn().mockResolvedValue(undefined),
145
151
  }, {}, {}, new operations_access_service_1.OperationsAccessService());
146
- jest.spyOn(service, 'getActorContext').mockResolvedValue({
152
+ jest.spyOn(service, "getActorContext").mockResolvedValue({
147
153
  userId: 15,
148
- roleSlugs: ['operations-collaborator'],
154
+ roleSlugs: ["operations-collaborator"],
149
155
  collaboratorId: 7,
150
- collaboratorName: 'Taylor Tester',
156
+ collaboratorName: "Taylor Tester",
151
157
  isDirector: false,
152
158
  isSupervisor: false,
153
159
  isCollaborator: true,
@@ -159,45 +165,55 @@ describe('OperationsService quick-entry timesheets', () => {
159
165
  afterEach(() => {
160
166
  jest.restoreAllMocks();
161
167
  });
162
- it('rejects quick entries with non-positive duration', async () => {
168
+ it("rejects quick entries with non-positive duration", async () => {
163
169
  await expect(service.createTimesheetEntry(15, {
164
170
  projectId: 11,
165
- workDate: '2026-04-09',
171
+ workDate: "2026-04-09",
166
172
  duration: 0,
167
- unit: 'minutes',
173
+ unit: "minutes",
168
174
  })).rejects.toThrow(common_1.BadRequestException);
169
175
  });
170
- it('rejects quick entries without a work date', async () => {
176
+ it("rejects quick entries without a work date", async () => {
171
177
  await expect(service.createTimesheetEntry(15, {
172
178
  projectId: 11,
173
179
  duration: 30,
174
- unit: 'minutes',
180
+ unit: "minutes",
175
181
  })).rejects.toThrow(common_1.BadRequestException);
176
182
  });
177
- it('stores quick-entry duration in minutes and keeps hours compatible', async () => {
183
+ it("stores quick-entry duration in minutes and keeps hours compatible", async () => {
178
184
  const tx = {
179
185
  $queryRawUnsafe: jest.fn().mockResolvedValue([{ id: 91 }]),
180
186
  };
181
187
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
182
- jest.spyOn(service, 'resolveOwnedProjectAssignment').mockResolvedValue({
188
+ jest
189
+ .spyOn(service, "resolveOwnedProjectAssignment")
190
+ .mockResolvedValue({
183
191
  id: 33,
184
192
  projectId: 11,
185
- projectName: 'Project Atlas',
186
- projectCode: 'OPS-11',
187
- roleLabel: 'Engineer',
193
+ projectName: "Project Atlas",
194
+ projectCode: "OPS-11",
195
+ roleLabel: "Engineer",
188
196
  });
189
- jest.spyOn(service, 'getOwnedTaskRecord').mockResolvedValue({
197
+ jest.spyOn(service, "getOwnedTaskRecord").mockResolvedValue({
190
198
  id: 44,
191
- name: 'Implement API',
199
+ name: "Implement API",
192
200
  projectAssignmentId: 33,
193
201
  projectId: 11,
194
- projectName: 'Project Atlas',
195
- projectCode: 'OPS-11',
202
+ projectName: "Project Atlas",
203
+ projectCode: "OPS-11",
196
204
  });
197
- jest.spyOn(service, 'getOrCreateTimesheetForWorkDate').mockResolvedValue(55);
198
- jest.spyOn(service, 'refreshTimesheetTotal').mockResolvedValue(undefined);
199
- jest.spyOn(service, 'submitTimesheetForApproval').mockResolvedValue(undefined);
200
- jest.spyOn(service, 'getTimesheetEntryByIdForActor').mockResolvedValue({
205
+ jest
206
+ .spyOn(service, "getOrCreateTimesheetForWorkDate")
207
+ .mockResolvedValue(55);
208
+ jest
209
+ .spyOn(service, "refreshTimesheetTotal")
210
+ .mockResolvedValue(undefined);
211
+ jest
212
+ .spyOn(service, "submitTimesheetForApproval")
213
+ .mockResolvedValue(undefined);
214
+ jest
215
+ .spyOn(service, "getTimesheetEntryByIdForActor")
216
+ .mockResolvedValue({
201
217
  id: 91,
202
218
  durationMinutes: 90,
203
219
  hours: 1.5,
@@ -205,58 +221,60 @@ describe('OperationsService quick-entry timesheets', () => {
205
221
  const result = await service.createTimesheetEntry(15, {
206
222
  projectId: 11,
207
223
  taskId: 44,
208
- workDate: '2026-04-09',
224
+ workDate: "2026-04-09",
209
225
  duration: 1.5,
210
- unit: 'hours',
211
- description: 'Worked on the API layer',
226
+ unit: "hours",
227
+ description: "Worked on the API layer",
212
228
  });
213
229
  expect(result).toEqual(expect.objectContaining({
214
230
  id: 91,
215
231
  durationMinutes: 90,
216
232
  hours: 1.5,
217
233
  }));
218
- expect(tx.$queryRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO operations_timesheet_entry'), 55, 33, 44, 'Implement API', '2026-04-09', 90, 1.5, 'Worked on the API layer');
234
+ expect(tx.$queryRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("INSERT INTO operations_timesheet_entry"), 55, 33, 44, "Implement API", "2026-04-09", 90, 1.5, "Worked on the API layer");
219
235
  expect(service.submitTimesheetForApproval).toHaveBeenCalledWith(tx, 55, 7);
220
236
  });
221
- it('uses the task assignment when full timesheet entries omit projectAssignmentId', async () => {
237
+ it("uses the task assignment when full timesheet entries omit projectAssignmentId", async () => {
222
238
  const tx = {
223
239
  $queryRawUnsafe: jest.fn().mockResolvedValue([{ id: 55 }]),
224
240
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
225
241
  };
226
242
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
227
- jest.spyOn(service, 'getCollaboratorById').mockResolvedValue({
243
+ jest.spyOn(service, "getCollaboratorById").mockResolvedValue({
228
244
  id: 7,
229
245
  supervisorId: 18,
230
246
  });
231
- jest.spyOn(service, 'getOwnedTaskRecord').mockResolvedValue({
247
+ jest.spyOn(service, "getOwnedTaskRecord").mockResolvedValue({
232
248
  id: 44,
233
- name: 'Implement API',
249
+ name: "Implement API",
234
250
  projectAssignmentId: 33,
235
251
  projectId: 11,
236
- projectName: 'Project Atlas',
237
- projectCode: 'OPS-11',
252
+ projectName: "Project Atlas",
253
+ projectCode: "OPS-11",
238
254
  });
239
- jest.spyOn(service, 'refreshTimesheetTotal').mockResolvedValue(undefined);
240
- jest.spyOn(service, 'listSingleTimesheet').mockResolvedValue({
255
+ jest
256
+ .spyOn(service, "refreshTimesheetTotal")
257
+ .mockResolvedValue(undefined);
258
+ jest.spyOn(service, "listSingleTimesheet").mockResolvedValue({
241
259
  id: 55,
242
- status: 'draft',
260
+ status: "draft",
243
261
  });
244
262
  await service.createTimesheet(15, {
245
- weekStartDate: '2026-04-06',
246
- weekEndDate: '2026-04-12',
263
+ weekStartDate: "2026-04-06",
264
+ weekEndDate: "2026-04-12",
247
265
  entries: [
248
266
  {
249
267
  taskId: 44,
250
- workDate: '2026-04-09',
268
+ workDate: "2026-04-09",
251
269
  duration: 2,
252
- unit: 'hours',
253
- description: 'Worked on the API layer',
270
+ unit: "hours",
271
+ description: "Worked on the API layer",
254
272
  },
255
273
  ],
256
274
  });
257
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO operations_timesheet_entry'), 55, 33, 44, 'Implement API', '2026-04-09', 120, 2, 'Worked on the API layer');
275
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("INSERT INTO operations_timesheet_entry"), 55, 33, 44, "Implement API", "2026-04-09", 120, 2, "Worked on the API layer");
258
276
  });
259
- it('rejects submitting a timesheet without a resolved approver', async () => {
277
+ it("rejects submitting a timesheet without a resolved approver", async () => {
260
278
  const tx = {
261
279
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
262
280
  $queryRawUnsafe: jest
@@ -265,24 +283,24 @@ describe('OperationsService quick-entry timesheets', () => {
265
283
  .mockResolvedValueOnce([{ exists: true }]),
266
284
  };
267
285
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
268
- jest.spyOn(service, 'getTimesheetById').mockResolvedValue({
286
+ jest.spyOn(service, "getTimesheetById").mockResolvedValue({
269
287
  id: 55,
270
288
  collaboratorId: 7,
271
289
  approverCollaboratorId: null,
272
- status: 'draft',
290
+ status: "draft",
273
291
  });
274
- jest.spyOn(service, 'getCollaboratorById').mockResolvedValue({
292
+ jest.spyOn(service, "getCollaboratorById").mockResolvedValue({
275
293
  id: 7,
276
294
  supervisorId: null,
277
295
  });
278
- jest.spyOn(service, 'listSingleTimesheet').mockResolvedValue({
296
+ jest.spyOn(service, "listSingleTimesheet").mockResolvedValue({
279
297
  id: 55,
280
- status: 'submitted',
298
+ status: "submitted",
281
299
  });
282
300
  await expect(service.submitTimesheet(15, 55)).rejects.toThrow(common_1.BadRequestException);
283
301
  expect(tx.$executeRawUnsafe).not.toHaveBeenCalled();
284
302
  });
285
- it('rejects submitting a timesheet without active entries', async () => {
303
+ it("rejects submitting a timesheet without active entries", async () => {
286
304
  const tx = {
287
305
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
288
306
  $queryRawUnsafe: jest
@@ -291,49 +309,54 @@ describe('OperationsService quick-entry timesheets', () => {
291
309
  .mockResolvedValueOnce([{ exists: false }]),
292
310
  };
293
311
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
294
- jest.spyOn(service, 'getTimesheetById').mockResolvedValue({
312
+ jest.spyOn(service, "getTimesheetById").mockResolvedValue({
295
313
  id: 56,
296
314
  collaboratorId: 7,
297
315
  approverCollaboratorId: 18,
298
- status: 'draft',
316
+ status: "draft",
299
317
  });
300
- jest.spyOn(service, 'getCollaboratorById').mockResolvedValue({
318
+ jest.spyOn(service, "getCollaboratorById").mockResolvedValue({
301
319
  id: 7,
302
320
  supervisorId: 18,
303
321
  });
304
- jest.spyOn(service, 'listSingleTimesheet').mockResolvedValue({
322
+ jest.spyOn(service, "listSingleTimesheet").mockResolvedValue({
305
323
  id: 56,
306
- status: 'submitted',
324
+ status: "submitted",
307
325
  });
308
326
  await expect(service.submitTimesheet(15, 56)).rejects.toThrow(common_1.BadRequestException);
309
327
  expect(tx.$executeRawUnsafe).not.toHaveBeenCalled();
310
328
  });
311
- it('deletes only draft quick entries and refreshes the weekly total', async () => {
329
+ it("deletes only draft quick entries and refreshes the weekly total", async () => {
312
330
  const tx = {
313
331
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
332
+ $queryRawUnsafe: jest.fn().mockResolvedValue([]),
314
333
  };
315
334
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
316
- jest.spyOn(service, 'getTimesheetEntryByIdForActor').mockResolvedValue({
335
+ jest
336
+ .spyOn(service, "getTimesheetEntryByIdForActor")
337
+ .mockResolvedValue({
317
338
  id: 91,
318
339
  timesheetId: 55,
319
340
  collaboratorId: 7,
320
- status: 'draft',
341
+ status: "draft",
321
342
  });
322
- jest.spyOn(service, 'refreshTimesheetTotal').mockResolvedValue(undefined);
343
+ jest
344
+ .spyOn(service, "refreshTimesheetTotal")
345
+ .mockResolvedValue(undefined);
323
346
  await expect(service.removeTimesheetEntry(15, 91)).resolves.toEqual({
324
347
  success: true,
325
348
  });
326
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('UPDATE operations_timesheet_entry'), 91);
349
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("UPDATE operations_timesheet_entry"), 91);
327
350
  expect(service.refreshTimesheetTotal).toHaveBeenCalledWith(tx, 55);
328
351
  });
329
- it('updates quick entries and moves them to the correct weekly timesheet', async () => {
352
+ it("updates quick entries and moves them to the correct weekly timesheet", async () => {
330
353
  const tx = {
331
354
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
332
355
  $queryRawUnsafe: jest.fn().mockResolvedValue([]),
333
356
  };
334
357
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
335
358
  jest
336
- .spyOn(service, 'getTimesheetEntryByIdForActor')
359
+ .spyOn(service, "getTimesheetEntryByIdForActor")
337
360
  .mockResolvedValueOnce({
338
361
  id: 93,
339
362
  timesheetId: 55,
@@ -341,10 +364,10 @@ describe('OperationsService quick-entry timesheets', () => {
341
364
  projectId: 11,
342
365
  projectAssignmentId: 33,
343
366
  taskId: 44,
344
- status: 'draft',
345
- weekStartDate: '2026-04-06',
346
- weekEndDate: '2026-04-12',
347
- workDate: '2026-04-09',
367
+ status: "draft",
368
+ weekStartDate: "2026-04-06",
369
+ weekEndDate: "2026-04-12",
370
+ workDate: "2026-04-09",
348
371
  hours: 1,
349
372
  durationMinutes: 60,
350
373
  })
@@ -355,80 +378,222 @@ describe('OperationsService quick-entry timesheets', () => {
355
378
  projectId: 12,
356
379
  projectAssignmentId: 34,
357
380
  taskId: 45,
358
- status: 'draft',
359
- weekStartDate: '2026-04-13',
360
- weekEndDate: '2026-04-19',
361
- workDate: '2026-04-14',
381
+ status: "draft",
382
+ weekStartDate: "2026-04-13",
383
+ weekEndDate: "2026-04-19",
384
+ workDate: "2026-04-14",
362
385
  hours: 2,
363
386
  durationMinutes: 120,
364
- taskName: 'Review backlog',
387
+ taskName: "Review backlog",
365
388
  });
366
- jest.spyOn(service, 'resolveOwnedProjectAssignment').mockResolvedValue({
389
+ jest
390
+ .spyOn(service, "resolveOwnedProjectAssignment")
391
+ .mockResolvedValue({
367
392
  id: 34,
368
393
  projectId: 12,
369
- projectName: 'Project Boreal',
370
- projectCode: 'OPS-12',
371
- roleLabel: 'Engineer',
394
+ projectName: "Project Boreal",
395
+ projectCode: "OPS-12",
396
+ roleLabel: "Engineer",
372
397
  });
373
- jest.spyOn(service, 'getOwnedTaskRecord').mockResolvedValue({
398
+ jest.spyOn(service, "getOwnedTaskRecord").mockResolvedValue({
374
399
  id: 45,
375
- name: 'Review backlog',
400
+ name: "Review backlog",
376
401
  projectAssignmentId: 34,
377
402
  projectId: 12,
378
- projectName: 'Project Boreal',
379
- projectCode: 'OPS-12',
403
+ projectName: "Project Boreal",
404
+ projectCode: "OPS-12",
380
405
  });
381
- jest.spyOn(service, 'getOrCreateTimesheetForWorkDate').mockResolvedValue(77);
382
- jest.spyOn(service, 'refreshTimesheetTotal').mockResolvedValue(undefined);
383
- jest.spyOn(service, 'cleanupEmptyEditableTimesheet').mockResolvedValue(undefined);
384
- jest.spyOn(service, 'submitTimesheetForApproval').mockResolvedValue(undefined);
406
+ jest
407
+ .spyOn(service, "getOrCreateTimesheetForWorkDate")
408
+ .mockResolvedValue(77);
409
+ jest
410
+ .spyOn(service, "refreshTimesheetTotal")
411
+ .mockResolvedValue(undefined);
412
+ jest
413
+ .spyOn(service, "cleanupEmptyEditableTimesheet")
414
+ .mockResolvedValue(undefined);
415
+ jest
416
+ .spyOn(service, "submitTimesheetForApproval")
417
+ .mockResolvedValue(undefined);
385
418
  const result = await service.updateTimesheetEntry(15, 93, {
386
419
  projectId: 12,
387
420
  projectAssignmentId: 34,
388
421
  taskId: 45,
389
- workDate: '2026-04-14',
422
+ workDate: "2026-04-14",
390
423
  duration: 2,
391
- unit: 'hours',
392
- description: 'Backlog refinement',
424
+ unit: "hours",
425
+ description: "Backlog refinement",
393
426
  });
394
427
  expect(result).toEqual(expect.objectContaining({
395
428
  id: 93,
396
429
  timesheetId: 77,
397
430
  }));
398
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('UPDATE operations_timesheet_entry'), 77, 34, 45, 'Review backlog', '2026-04-14', 120, 2, 'Backlog refinement', 93);
431
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("UPDATE operations_timesheet_entry"), 77, 34, 45, "Review backlog", "2026-04-14", 120, 2, "Backlog refinement", 93);
399
432
  expect(service.refreshTimesheetTotal).toHaveBeenCalledWith(tx, 55);
400
433
  expect(service.refreshTimesheetTotal).toHaveBeenCalledWith(tx, 77);
401
434
  expect(service.cleanupEmptyEditableTimesheet).toHaveBeenCalledWith(tx, 55);
402
435
  expect(service.submitTimesheetForApproval).toHaveBeenCalledWith(tx, 77, 7);
403
436
  });
404
- it('allows deleting entries from submitted timesheets until approval', async () => {
437
+ it("allows deleting entries from submitted timesheets until approval", async () => {
405
438
  const tx = {
406
439
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
407
440
  };
408
441
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
409
- jest.spyOn(service, 'getTimesheetEntryByIdForActor').mockResolvedValue({
442
+ jest
443
+ .spyOn(service, "getTimesheetEntryByIdForActor")
444
+ .mockResolvedValue({
410
445
  id: 92,
411
446
  timesheetId: 56,
412
447
  collaboratorId: 7,
413
- status: 'submitted',
448
+ status: "submitted",
414
449
  });
415
- jest.spyOn(service, 'refreshTimesheetTotal').mockResolvedValue(undefined);
416
- jest.spyOn(service, 'cleanupEmptyEditableTimesheet').mockResolvedValue(undefined);
450
+ jest
451
+ .spyOn(service, "refreshTimesheetTotal")
452
+ .mockResolvedValue(undefined);
453
+ jest
454
+ .spyOn(service, "cleanupEmptyEditableTimesheet")
455
+ .mockResolvedValue(undefined);
417
456
  await expect(service.removeTimesheetEntry(15, 92)).resolves.toEqual({
418
457
  success: true,
419
458
  });
420
459
  });
421
- it('rejects deleting entries from approved timesheets', async () => {
422
- jest.spyOn(service, 'getTimesheetEntryByIdForActor').mockResolvedValue({
460
+ it("rejects deleting entries from approved timesheets", async () => {
461
+ jest
462
+ .spyOn(service, "getTimesheetEntryByIdForActor")
463
+ .mockResolvedValue({
423
464
  id: 94,
424
465
  timesheetId: 57,
425
466
  collaboratorId: 7,
426
- status: 'approved',
467
+ status: "approved",
427
468
  });
428
469
  await expect(service.removeTimesheetEntry(15, 94)).rejects.toThrow(common_1.BadRequestException);
429
470
  });
430
471
  });
431
- describe('OperationsService approval side effects', () => {
472
+ describe("OperationsService task column activity", () => {
473
+ let service;
474
+ let prisma;
475
+ let integrationApi;
476
+ const baseTask = {
477
+ id: 44,
478
+ name: "Implement board",
479
+ description: null,
480
+ priority: "medium",
481
+ status: "todo",
482
+ dueDate: null,
483
+ estimateHours: null,
484
+ position: 0,
485
+ tags: null,
486
+ assigneeCollaboratorId: null,
487
+ projectAssignmentId: null,
488
+ projectId: 11,
489
+ projectName: "Project Atlas",
490
+ projectCode: "OPS-11",
491
+ doingStartedAt: null,
492
+ totalDoingMinutes: 0,
493
+ deletedAt: null,
494
+ };
495
+ beforeEach(() => {
496
+ prisma = {
497
+ $transaction: jest.fn(),
498
+ };
499
+ integrationApi = {
500
+ publishEvent: jest.fn().mockResolvedValue(undefined),
501
+ };
502
+ service = new operations_service_1.OperationsService(prisma, {}, integrationApi, {}, {}, new operations_access_service_1.OperationsAccessService());
503
+ jest.spyOn(service, "getActorContext").mockResolvedValue({
504
+ userId: 15,
505
+ roleSlugs: ["operations-collaborator"],
506
+ collaboratorId: 7,
507
+ collaboratorName: "Taylor Tester",
508
+ isDirector: false,
509
+ isSupervisor: false,
510
+ isCollaborator: true,
511
+ teamCollaboratorIds: [],
512
+ visibleCollaboratorIds: [7],
513
+ visibleProjectIds: [11],
514
+ });
515
+ jest
516
+ .spyOn(service, "assertProjectAccess")
517
+ .mockResolvedValue(undefined);
518
+ jest.spyOn(service, "getProjectBoardTask").mockResolvedValue(Object.assign(Object.assign({}, baseTask), { assigneeCollaboratorId: 7, status: "doing" }));
519
+ });
520
+ afterEach(() => {
521
+ jest.restoreAllMocks();
522
+ });
523
+ function mockTransaction() {
524
+ const tx = {
525
+ $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
526
+ };
527
+ prisma.$transaction.mockImplementation(async (callback) => callback(tx));
528
+ return tx;
529
+ }
530
+ it("records todo to doing and auto-assigns the actor", async () => {
531
+ const tx = mockTransaction();
532
+ jest
533
+ .spyOn(service, "getTaskRecordForActor")
534
+ .mockResolvedValue(baseTask);
535
+ await service.updateTask(15, 44, { status: "doing" });
536
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledTimes(2);
537
+ expect(tx.$executeRawUnsafe.mock.calls[0][0]).toContain("doing_started_at = CASE");
538
+ expect(tx.$executeRawUnsafe.mock.calls[0][3]).toBe(7);
539
+ expect(tx.$executeRawUnsafe.mock.calls[0][15]).toBe(true);
540
+ expect(tx.$executeRawUnsafe.mock.calls[0][16]).toBe(false);
541
+ expect(tx.$executeRawUnsafe.mock.calls[1][0]).toContain("INSERT INTO operations_task_activity");
542
+ expect(tx.$executeRawUnsafe.mock.calls[1].slice(1)).toEqual([
543
+ 44,
544
+ 7,
545
+ "status_changed",
546
+ "todo",
547
+ "doing",
548
+ ]);
549
+ });
550
+ it("records doing to review and closes accumulated doing minutes", async () => {
551
+ const tx = mockTransaction();
552
+ jest.spyOn(service, "getTaskRecordForActor").mockResolvedValue(Object.assign(Object.assign({}, baseTask), { status: "doing", assigneeCollaboratorId: 9, doingStartedAt: "2026-05-16T10:00:00.000Z", totalDoingMinutes: 15 }));
553
+ await service.updateTask(15, 44, { status: "review" });
554
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledTimes(2);
555
+ expect(tx.$executeRawUnsafe.mock.calls[0][3]).toBe(9);
556
+ expect(tx.$executeRawUnsafe.mock.calls[0][15]).toBe(false);
557
+ expect(tx.$executeRawUnsafe.mock.calls[0][16]).toBe(true);
558
+ expect(tx.$executeRawUnsafe.mock.calls[1].slice(1)).toEqual([
559
+ 44,
560
+ 7,
561
+ "status_changed",
562
+ "doing",
563
+ "review",
564
+ ]);
565
+ });
566
+ it("blocks moving advanced without a linked collaborator when no assignee exists", async () => {
567
+ prisma.$transaction.mockImplementation(async (callback) => callback({ $executeRawUnsafe: jest.fn() }));
568
+ jest.spyOn(service, "getActorContext").mockResolvedValue({
569
+ userId: 15,
570
+ roleSlugs: ["admin"],
571
+ collaboratorId: null,
572
+ collaboratorName: null,
573
+ isDirector: true,
574
+ isSupervisor: true,
575
+ isCollaborator: true,
576
+ teamCollaboratorIds: [],
577
+ visibleCollaboratorIds: [],
578
+ visibleProjectIds: [],
579
+ });
580
+ jest
581
+ .spyOn(service, "getTaskRecordForActor")
582
+ .mockResolvedValue(baseTask);
583
+ await expect(service.updateTask(15, 44, { status: "review" })).rejects.toThrow(common_1.BadRequestException);
584
+ });
585
+ it("does not create activity when the status does not change", async () => {
586
+ const tx = mockTransaction();
587
+ jest
588
+ .spyOn(service, "getTaskRecordForActor")
589
+ .mockResolvedValue(baseTask);
590
+ await service.updateTask(15, 44, { status: "todo" });
591
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledTimes(1);
592
+ expect(tx.$executeRawUnsafe.mock.calls[0][15]).toBe(false);
593
+ expect(tx.$executeRawUnsafe.mock.calls[0][16]).toBe(false);
594
+ });
595
+ });
596
+ describe("OperationsService approval side effects", () => {
432
597
  let service;
433
598
  beforeEach(() => {
434
599
  service = new operations_service_1.OperationsService({
@@ -436,11 +601,11 @@ describe('OperationsService approval side effects', () => {
436
601
  }, {}, {
437
602
  publishEvent: jest.fn().mockResolvedValue(undefined),
438
603
  }, {}, {}, new operations_access_service_1.OperationsAccessService());
439
- jest.spyOn(service, 'getActorContext').mockResolvedValue({
604
+ jest.spyOn(service, "getActorContext").mockResolvedValue({
440
605
  userId: 22,
441
- roleSlugs: ['admin-operations-supervisor'],
606
+ roleSlugs: ["admin-operations-supervisor"],
442
607
  collaboratorId: 18,
443
- collaboratorName: 'Sam Supervisor',
608
+ collaboratorName: "Sam Supervisor",
444
609
  isDirector: false,
445
610
  isSupervisor: true,
446
611
  isCollaborator: false,
@@ -452,7 +617,7 @@ describe('OperationsService approval side effects', () => {
452
617
  afterEach(() => {
453
618
  jest.restoreAllMocks();
454
619
  });
455
- it('applies approved permanent schedule adjustments to the active collaborator weekly schedule', async () => {
620
+ it("applies approved permanent schedule adjustments to the active collaborator weekly schedule", async () => {
456
621
  const tx = {
457
622
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
458
623
  $queryRawUnsafe: jest
@@ -460,19 +625,19 @@ describe('OperationsService approval side effects', () => {
460
625
  .mockResolvedValueOnce([
461
626
  {
462
627
  collaboratorId: 7,
463
- requestScope: 'permanent',
628
+ requestScope: "permanent",
464
629
  },
465
630
  ])
466
631
  .mockResolvedValueOnce([
467
632
  {
468
- weekday: 'monday',
633
+ weekday: "monday",
469
634
  isWorkingDay: true,
470
- startTime: '08:00',
471
- endTime: '17:00',
635
+ startTime: "08:00",
636
+ endTime: "17:00",
472
637
  breakMinutes: 45,
473
638
  },
474
639
  {
475
- weekday: 'friday',
640
+ weekday: "friday",
476
641
  isWorkingDay: false,
477
642
  startTime: null,
478
643
  endTime: null,
@@ -481,68 +646,74 @@ describe('OperationsService approval side effects', () => {
481
646
  ]),
482
647
  };
483
648
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
484
- jest.spyOn(service, 'querySingle')
649
+ jest
650
+ .spyOn(service, "querySingle")
485
651
  .mockResolvedValueOnce({
486
652
  id: 81,
487
- targetType: 'schedule_adjustment_request',
653
+ targetType: "schedule_adjustment_request",
488
654
  targetId: 400,
489
655
  requesterCollaboratorId: 7,
490
656
  approverCollaboratorId: 18,
491
- status: 'pending',
657
+ status: "pending",
492
658
  })
493
659
  .mockResolvedValueOnce({
494
660
  id: 81,
495
- targetType: 'schedule_adjustment_request',
661
+ targetType: "schedule_adjustment_request",
496
662
  targetId: 400,
497
- status: 'approved',
498
- decidedAt: '2026-04-11T12:00:00.000Z',
499
- decisionNote: 'Approved for the new weekly schedule.',
663
+ status: "approved",
664
+ decidedAt: "2026-04-11T12:00:00.000Z",
665
+ decisionNote: "Approved for the new weekly schedule.",
500
666
  });
501
- jest.spyOn(service, 'insertApprovalHistory').mockResolvedValue(undefined);
667
+ jest
668
+ .spyOn(service, "insertApprovalHistory")
669
+ .mockResolvedValue(undefined);
502
670
  await expect(service.approve(22, 81, {
503
- note: 'Approved for the new weekly schedule.',
671
+ note: "Approved for the new weekly schedule.",
504
672
  })).resolves.toEqual(expect.objectContaining({
505
673
  id: 81,
506
- status: 'approved',
674
+ status: "approved",
507
675
  }));
508
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('UPDATE operations_schedule_adjustment_request'), 'approved', 18, 400);
509
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('UPDATE operations_collaborator_schedule_day'), 7);
510
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO operations_collaborator_schedule_day'), 7, 'monday', true, '08:00', '17:00', 45);
511
- expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO operations_collaborator_schedule_day'), 7, 'friday', false, null, null, 0);
676
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("UPDATE operations_schedule_adjustment_request"), "approved", 18, 400);
677
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("UPDATE operations_collaborator_schedule_day"), 7);
678
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("INSERT INTO operations_collaborator_schedule_day"), 7, "monday", true, "08:00", "17:00", 45);
679
+ expect(tx.$executeRawUnsafe).toHaveBeenCalledWith(expect.stringContaining("INSERT INTO operations_collaborator_schedule_day"), 7, "friday", false, null, null, 0);
512
680
  });
513
- it('keeps the collaborator weekly schedule unchanged for approved temporary adjustments', async () => {
681
+ it("keeps the collaborator weekly schedule unchanged for approved temporary adjustments", async () => {
514
682
  const tx = {
515
683
  $executeRawUnsafe: jest.fn().mockResolvedValue(undefined),
516
684
  $queryRawUnsafe: jest.fn().mockResolvedValueOnce([
517
685
  {
518
686
  collaboratorId: 7,
519
- requestScope: 'temporary',
687
+ requestScope: "temporary",
520
688
  },
521
689
  ]),
522
690
  };
523
691
  service.prisma.$transaction.mockImplementation(async (callback) => callback(tx));
524
- jest.spyOn(service, 'querySingle')
692
+ jest
693
+ .spyOn(service, "querySingle")
525
694
  .mockResolvedValueOnce({
526
695
  id: 82,
527
- targetType: 'schedule_adjustment_request',
696
+ targetType: "schedule_adjustment_request",
528
697
  targetId: 401,
529
698
  requesterCollaboratorId: 7,
530
699
  approverCollaboratorId: 18,
531
- status: 'pending',
700
+ status: "pending",
532
701
  })
533
702
  .mockResolvedValueOnce({
534
703
  id: 82,
535
- targetType: 'schedule_adjustment_request',
704
+ targetType: "schedule_adjustment_request",
536
705
  targetId: 401,
537
- status: 'approved',
538
- decidedAt: '2026-04-11T12:05:00.000Z',
539
- decisionNote: 'Approved as a temporary exception.',
706
+ status: "approved",
707
+ decidedAt: "2026-04-11T12:05:00.000Z",
708
+ decisionNote: "Approved as a temporary exception.",
540
709
  });
541
- jest.spyOn(service, 'insertApprovalHistory').mockResolvedValue(undefined);
710
+ jest
711
+ .spyOn(service, "insertApprovalHistory")
712
+ .mockResolvedValue(undefined);
542
713
  await service.approve(22, 82, {
543
- note: 'Approved as a temporary exception.',
714
+ note: "Approved as a temporary exception.",
544
715
  });
545
- expect(tx.$executeRawUnsafe).not.toHaveBeenCalledWith(expect.stringContaining('UPDATE operations_collaborator_schedule_day'), 7);
716
+ expect(tx.$executeRawUnsafe).not.toHaveBeenCalledWith(expect.stringContaining("UPDATE operations_collaborator_schedule_day"), 7);
546
717
  });
547
718
  });
548
719
  //# sourceMappingURL=operations.service.spec.js.map