@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
@@ -26,25 +26,39 @@ import {
26
26
  TableRow,
27
27
  } from '@/components/ui/table';
28
28
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
29
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
30
+ import {
31
+ closestCenter,
32
+ DndContext,
33
+ PointerSensor,
34
+ useDraggable,
35
+ useDroppable,
36
+ useSensor,
37
+ useSensors,
38
+ type DragEndEvent,
39
+ type UniqueIdentifier,
40
+ } from '@dnd-kit/core';
41
+ import { CSS } from '@dnd-kit/utilities';
29
42
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
30
43
  import {
31
44
  Archive,
32
45
  ArchiveRestore,
33
46
  CalendarDays,
47
+ CheckCircle2,
34
48
  Eye,
35
49
  EyeOff,
36
50
  FileText,
37
51
  FolderKanban,
52
+ KanbanSquare,
38
53
  LayoutGrid,
39
54
  List,
40
55
  Pencil,
41
56
  PlayCircle,
42
- ShieldAlert,
43
57
  } from 'lucide-react';
44
58
  import { useTranslations } from 'next-intl';
45
59
  import Link from 'next/link';
46
60
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
47
- import { useMemo, useState } from 'react';
61
+ import { useMemo, useState, type ReactNode } from 'react';
48
62
  import { OperationsHeader } from '../_components/operations-header';
49
63
  import { ProjectFormScreen } from '../_components/project-form-screen';
50
64
  import { StatusBadge } from '../_components/status-badge';
@@ -63,11 +77,132 @@ import {
63
77
 
64
78
  const PROJECT_VIEW_STORAGE_KEY = 'operations-projects-view-mode';
65
79
 
66
- type ProjectViewMode = 'table' | 'cards';
80
+ type ProjectViewMode = 'table' | 'cards' | 'board';
81
+
82
+ type ProjectBoardStatus = 'planning' | 'active' | 'paused' | 'completed';
83
+
84
+ type ProjectBoardColumns = Record<ProjectBoardStatus, OperationsProject[]>;
85
+
86
+ const PROJECT_BOARD_COLUMNS: Array<{ id: ProjectBoardStatus; color: string }> =
87
+ [
88
+ { id: 'planning', color: 'bg-amber-500' },
89
+ { id: 'active', color: 'bg-emerald-500' },
90
+ { id: 'paused', color: 'bg-slate-500' },
91
+ { id: 'completed', color: 'bg-sky-500' },
92
+ ];
93
+
94
+ function projectDragId(projectId: number) {
95
+ return `project-${projectId}`;
96
+ }
97
+
98
+ function columnDropId(columnId: ProjectBoardStatus) {
99
+ return `project-col-${columnId}`;
100
+ }
101
+
102
+ function parseProjectId(value: UniqueIdentifier | null | undefined) {
103
+ if (!value) return null;
104
+ const match = String(value).match(/^project-(\d+)$/);
105
+ return match ? Number(match[1]) : null;
106
+ }
107
+
108
+ function parseBoardColumnId(
109
+ value: UniqueIdentifier | null | undefined
110
+ ): ProjectBoardStatus | null {
111
+ if (!value) return null;
112
+ const match = String(value).match(/^project-col-(.+)$/);
113
+ const id = match?.[1];
114
+ return PROJECT_BOARD_COLUMNS.some((column) => column.id === id)
115
+ ? (id as ProjectBoardStatus)
116
+ : null;
117
+ }
118
+
119
+ function splitProjectsByStatus(
120
+ projects: OperationsProject[]
121
+ ): ProjectBoardColumns {
122
+ return {
123
+ planning: projects.filter((project) => project.status === 'planning'),
124
+ active: projects.filter((project) => project.status === 'active'),
125
+ paused: projects.filter((project) => project.status === 'paused'),
126
+ completed: projects.filter((project) => project.status === 'completed'),
127
+ };
128
+ }
129
+
130
+ function moveProjectToColumn(
131
+ columns: ProjectBoardColumns,
132
+ projectId: number,
133
+ target: ProjectBoardStatus
134
+ ): ProjectBoardColumns {
135
+ const sourceProject =
136
+ columns.planning.find((project) => project.id === projectId) ??
137
+ columns.active.find((project) => project.id === projectId) ??
138
+ columns.paused.find((project) => project.id === projectId) ??
139
+ columns.completed.find((project) => project.id === projectId);
140
+
141
+ if (!sourceProject) {
142
+ return columns;
143
+ }
144
+
145
+ const nextColumns: ProjectBoardColumns = {
146
+ planning: columns.planning.filter((project) => project.id !== projectId),
147
+ active: columns.active.filter((project) => project.id !== projectId),
148
+ paused: columns.paused.filter((project) => project.id !== projectId),
149
+ completed: columns.completed.filter((project) => project.id !== projectId),
150
+ };
151
+
152
+ return {
153
+ ...nextColumns,
154
+ [target]: [{ ...sourceProject, status: target }, ...nextColumns[target]],
155
+ };
156
+ }
157
+
158
+ function DraggableProjectCard({
159
+ project,
160
+ disabled,
161
+ children,
162
+ }: {
163
+ project: OperationsProject;
164
+ disabled?: boolean;
165
+ children: (isDragging: boolean) => ReactNode;
166
+ }) {
167
+ const { attributes, listeners, setNodeRef, transform, isDragging } =
168
+ useDraggable({
169
+ id: projectDragId(project.id),
170
+ disabled,
171
+ });
172
+
173
+ return (
174
+ <div
175
+ ref={setNodeRef}
176
+ style={{ transform: CSS.Translate.toString(transform) }}
177
+ {...listeners}
178
+ {...attributes}
179
+ className={isDragging ? 'z-20 touch-none' : 'touch-none'}
180
+ >
181
+ {children(isDragging)}
182
+ </div>
183
+ );
184
+ }
185
+
186
+ function DroppableProjectColumn({
187
+ columnId,
188
+ children,
189
+ }: {
190
+ columnId: ProjectBoardStatus;
191
+ children: (isOver: boolean) => ReactNode;
192
+ }) {
193
+ const { setNodeRef, isOver } = useDroppable({ id: columnDropId(columnId) });
194
+ return <div ref={setNodeRef}>{children(isOver)}</div>;
195
+ }
67
196
 
68
- function getPersonAvatarUrl(avatarId?: number | null): string {
197
+ function getPersonAvatarUrl(avatarId?: number | null) {
69
198
  return typeof avatarId === 'number' && avatarId > 0
70
199
  ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
200
+ : undefined;
201
+ }
202
+
203
+ function getUserPhotoUrl(photoId?: number | null) {
204
+ return typeof photoId === 'number' && photoId > 0
205
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
71
206
  : '/placeholder.png';
72
207
  }
73
208
 
@@ -113,7 +248,11 @@ export default function OperationsProjectsPage() {
113
248
  const [search, setSearch] = useState('');
114
249
  const [statusFilter, setStatusFilter] = useState('all');
115
250
  const [page, setPage] = useState(1);
116
- const [pageSize, setPageSize] = useState(12);
251
+ const [pageSize, setPageSize] = usePersistedPageSize({
252
+ storageKey: 'pagination:operations-projects:pageSize',
253
+ defaultValue: 12,
254
+ allowedValues: [12, 24, 48],
255
+ });
117
256
  const [viewMode, setViewMode] = useState<ProjectViewMode>(() => {
118
257
  if (typeof window === 'undefined') {
119
258
  return 'table';
@@ -121,8 +260,20 @@ export default function OperationsProjectsPage() {
121
260
 
122
261
  const savedViewMode = window.localStorage.getItem(PROJECT_VIEW_STORAGE_KEY);
123
262
 
124
- return savedViewMode === 'cards' ? 'cards' : 'table';
263
+ if (
264
+ savedViewMode === 'table' ||
265
+ savedViewMode === 'cards' ||
266
+ savedViewMode === 'board'
267
+ ) {
268
+ return savedViewMode;
269
+ }
270
+
271
+ return 'table';
125
272
  });
273
+ const [boardOverride, setBoardOverride] =
274
+ useState<ProjectBoardColumns | null>(null);
275
+ const activeViewMode: ProjectViewMode =
276
+ statusFilter === 'archived' && viewMode === 'board' ? 'table' : viewMode;
126
277
 
127
278
  const createParam = searchParams.get('create');
128
279
  const editProjectId = parseEditProjectId(searchParams.get('edit'));
@@ -197,8 +348,57 @@ export default function OperationsProjectsPage() {
197
348
  },
198
349
  placeholderData: (previous) => previous,
199
350
  });
200
- const projects = projectsResponse?.data ?? [];
351
+ const { data: boardProjectsResponse, refetch: refetchBoard } = useQuery<
352
+ PaginatedResponse<OperationsProject>
353
+ >({
354
+ queryKey: [
355
+ 'operations-projects-board',
356
+ currentLocaleCode,
357
+ search,
358
+ statusFilter,
359
+ ],
360
+ queryFn: () => {
361
+ const params = new URLSearchParams({
362
+ page: '1',
363
+ pageSize: '1000',
364
+ });
365
+
366
+ if (search.trim()) {
367
+ params.set('search', search.trim());
368
+ }
369
+
370
+ if (statusFilter !== 'all') {
371
+ params.set('status', statusFilter);
372
+ }
373
+
374
+ return fetchOperations<PaginatedResponse<OperationsProject>>(
375
+ request,
376
+ `/operations/projects?${params.toString()}`
377
+ );
378
+ },
379
+ enabled: activeViewMode === 'board',
380
+ placeholderData: (previous) => previous,
381
+ });
382
+ const projects = useMemo(
383
+ () => projectsResponse?.data ?? [],
384
+ [projectsResponse?.data]
385
+ );
386
+ const boardProjects = useMemo(
387
+ () => boardProjectsResponse?.data ?? [],
388
+ [boardProjectsResponse?.data]
389
+ );
390
+ const boardColumns = useMemo(() => {
391
+ if (boardOverride) {
392
+ return boardOverride;
393
+ }
394
+
395
+ return splitProjectsByStatus(boardProjects);
396
+ }, [boardOverride, boardProjects]);
201
397
  const filteredRows = projects;
398
+ const hasRowsForCurrentView =
399
+ activeViewMode === 'board'
400
+ ? boardProjects.length > 0
401
+ : filteredRows.length > 0;
202
402
 
203
403
  const statsCards = useMemo(
204
404
  () => [
@@ -221,13 +421,13 @@ export default function OperationsProjectsPage() {
221
421
  iconContainerClassName: 'bg-green-50 text-green-600',
222
422
  },
223
423
  {
224
- key: 'atRisk',
225
- title: t('cards.atRisk'),
226
- description: t('cards.atRiskDescription'),
227
- value: projects.filter((item) => item.status === 'at_risk').length,
228
- icon: ShieldAlert,
229
- accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
230
- iconContainerClassName: 'bg-amber-50 text-amber-600',
424
+ key: 'completed',
425
+ title: t('cards.completed'),
426
+ description: t('cards.completedDescription'),
427
+ value: projects.filter((item) => item.status === 'completed').length,
428
+ icon: CheckCircle2,
429
+ accentClassName: 'from-sky-500/20 via-cyan-500/10 to-transparent',
430
+ iconContainerClassName: 'bg-sky-50 text-sky-600',
231
431
  },
232
432
  {
233
433
  key: 'upcomingDeliveries',
@@ -243,7 +443,7 @@ export default function OperationsProjectsPage() {
243
443
  );
244
444
 
245
445
  const handleViewModeChange = (value: string) => {
246
- if (value !== 'table' && value !== 'cards') {
446
+ if (value !== 'table' && value !== 'cards' && value !== 'board') {
247
447
  return;
248
448
  }
249
449
 
@@ -254,6 +454,105 @@ export default function OperationsProjectsPage() {
254
454
  }
255
455
  };
256
456
 
457
+ const sensors = useSensors(
458
+ useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
459
+ );
460
+
461
+ const handleBoardDragEnd = async (event: DragEndEvent) => {
462
+ if (!access.isDirector) {
463
+ return;
464
+ }
465
+
466
+ const projectId = parseProjectId(event.active.id);
467
+ const targetColumn = parseBoardColumnId(event.over?.id);
468
+
469
+ if (!projectId || !targetColumn) {
470
+ return;
471
+ }
472
+
473
+ const sourceColumn = PROJECT_BOARD_COLUMNS.find((column) =>
474
+ boardColumns[column.id].some((project) => project.id === projectId)
475
+ )?.id;
476
+
477
+ if (!sourceColumn || sourceColumn === targetColumn) {
478
+ return;
479
+ }
480
+
481
+ setBoardOverride(
482
+ moveProjectToColumn(boardColumns, projectId, targetColumn)
483
+ );
484
+
485
+ try {
486
+ await mutateOperations(
487
+ request,
488
+ `/operations/projects/${projectId}`,
489
+ 'PATCH',
490
+ {
491
+ status: targetColumn,
492
+ }
493
+ );
494
+ showToastHandler?.('success', t('messages.statusSuccess'));
495
+ await Promise.all([refetch(), refetchBoard()]);
496
+ setBoardOverride(null);
497
+ } catch {
498
+ setBoardOverride(null);
499
+ showToastHandler?.('error', t('messages.statusError'));
500
+ await Promise.all([refetch(), refetchBoard()]);
501
+ }
502
+ };
503
+
504
+ const renderBoardCard = (project: OperationsProject, isDragging = false) => (
505
+ <Card
506
+ className={`cursor-pointer border-border/60 py-0 shadow-sm transition ${
507
+ isDragging
508
+ ? 'opacity-70 shadow-lg'
509
+ : 'hover:-translate-y-0.5 hover:shadow-md'
510
+ }`}
511
+ onDoubleClick={() => router.push(`/operations/projects/${project.id}`)}
512
+ >
513
+ <CardContent className="space-y-2 p-3">
514
+ <div className="truncate text-sm font-semibold">{project.name}</div>
515
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
516
+ {project.clientName ? (
517
+ <>
518
+ <span className="truncate">{project.code || '—'}</span>
519
+ <span>•</span>
520
+ <Avatar className="h-4 w-4 shrink-0">
521
+ <AvatarImage
522
+ src={
523
+ project.clientUserPhotoId
524
+ ? getUserPhotoUrl(project.clientUserPhotoId)
525
+ : getPersonAvatarUrl(project.clientAvatarId)
526
+ }
527
+ alt={project.clientName}
528
+ />
529
+ <AvatarFallback className="text-[8px] font-medium">
530
+ {getInitials(project.clientName)}
531
+ </AvatarFallback>
532
+ </Avatar>
533
+ <span className="truncate">{project.clientName}</span>
534
+ </>
535
+ ) : (
536
+ <span className="truncate">
537
+ {project.code || commonT('labels.notAvailable')}
538
+ </span>
539
+ )}
540
+ </div>
541
+ <div className="flex items-center justify-between gap-2 pt-1">
542
+ <StatusBadge
543
+ label={tProjectStatus(project.status)}
544
+ className={getStatusBadgeClass(project.status)}
545
+ />
546
+ <Button variant="outline" size="icon" asChild>
547
+ <Link href={`/operations/projects/${project.id}`}>
548
+ <Eye className="size-4" />
549
+ </Link>
550
+ </Button>
551
+ </div>
552
+ </CardContent>
553
+ </Card>
554
+ );
555
+
257
556
  const toggleArchived = async (project: OperationsProject) => {
258
557
  const nextStatus = project.status === 'archived' ? 'active' : 'archived';
259
558
 
@@ -267,7 +566,7 @@ export default function OperationsProjectsPage() {
267
566
  }
268
567
  );
269
568
  showToastHandler?.('success', t('messages.statusSuccess'));
270
- await refetch();
569
+ await Promise.all([refetch(), refetchBoard()]);
271
570
  } catch {
272
571
  showToastHandler?.('error', t('messages.statusError'));
273
572
  }
@@ -332,7 +631,6 @@ export default function OperationsProjectsPage() {
332
631
  { value: 'all', label: commonT('filters.allStatuses') },
333
632
  { value: 'planning', label: tProjectStatus('planning') },
334
633
  { value: 'active', label: tProjectStatus('active') },
335
- { value: 'at_risk', label: tProjectStatus('at_risk') },
336
634
  { value: 'paused', label: tProjectStatus('paused') },
337
635
  { value: 'completed', label: tProjectStatus('completed') },
338
636
  { value: 'archived', label: tProjectStatus('archived') },
@@ -348,7 +646,7 @@ export default function OperationsProjectsPage() {
348
646
  </span>
349
647
  <ToggleGroup
350
648
  type="single"
351
- value={viewMode}
649
+ value={activeViewMode}
352
650
  onValueChange={handleViewModeChange}
353
651
  variant="outline"
354
652
  size="sm"
@@ -370,12 +668,86 @@ export default function OperationsProjectsPage() {
370
668
  <LayoutGrid className="h-4 w-4" />
371
669
  <span className="hidden sm:inline">{t('viewModeCards')}</span>
372
670
  </ToggleGroupItem>
671
+ <ToggleGroupItem
672
+ value="board"
673
+ className="gap-1.5 px-2.5"
674
+ aria-label={t('viewModeBoard')}
675
+ >
676
+ <KanbanSquare className="h-4 w-4" />
677
+ <span className="hidden sm:inline">{t('viewModeBoard')}</span>
678
+ </ToggleGroupItem>
373
679
  </ToggleGroup>
374
680
  </div>
375
681
  </div>
376
682
 
377
- {filteredRows.length > 0 ? (
378
- viewMode === 'cards' ? (
683
+ {hasRowsForCurrentView ? (
684
+ activeViewMode === 'board' ? (
685
+ <>
686
+ {!access.isDirector ? (
687
+ <p className="text-sm text-muted-foreground">
688
+ {t('board.directorOnly')}
689
+ </p>
690
+ ) : null}
691
+ <DndContext
692
+ sensors={access.isDirector ? sensors : undefined}
693
+ collisionDetection={closestCenter}
694
+ onDragEnd={(event) => void handleBoardDragEnd(event)}
695
+ >
696
+ <div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-4">
697
+ {PROJECT_BOARD_COLUMNS.map((column) => (
698
+ <DroppableProjectColumn key={column.id} columnId={column.id}>
699
+ {(isOver) => (
700
+ <div
701
+ className={`flex min-h-72 flex-col gap-3 rounded-md border bg-muted/20 p-3 ${
702
+ isOver && access.isDirector
703
+ ? 'border-primary/60 bg-primary/5'
704
+ : 'border-border/60'
705
+ }`}
706
+ >
707
+ <div className="flex items-center gap-2">
708
+ <span
709
+ className={`size-2.5 rounded-full ${column.color}`}
710
+ />
711
+ <span className="text-sm font-semibold">
712
+ {t(`board.columns.${column.id}`)}
713
+ </span>
714
+ <span className="text-xs text-muted-foreground">
715
+ ({boardColumns[column.id].length})
716
+ </span>
717
+ </div>
718
+
719
+ <div className="space-y-2">
720
+ {boardColumns[column.id].length ? (
721
+ boardColumns[column.id].map((project) =>
722
+ access.isDirector ? (
723
+ <DraggableProjectCard
724
+ key={project.id}
725
+ project={project}
726
+ >
727
+ {(isDragging) =>
728
+ renderBoardCard(project, isDragging)
729
+ }
730
+ </DraggableProjectCard>
731
+ ) : (
732
+ <div key={project.id}>
733
+ {renderBoardCard(project)}
734
+ </div>
735
+ )
736
+ )
737
+ ) : (
738
+ <div className="rounded-md border border-dashed border-border/70 px-3 py-5 text-center text-xs text-muted-foreground">
739
+ {t('board.emptyColumn')}
740
+ </div>
741
+ )}
742
+ </div>
743
+ </div>
744
+ )}
745
+ </DroppableProjectColumn>
746
+ ))}
747
+ </div>
748
+ </DndContext>
749
+ </>
750
+ ) : activeViewMode === 'cards' ? (
379
751
  <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
380
752
  {filteredRows.map((project) => (
381
753
  <Card
@@ -414,7 +786,11 @@ export default function OperationsProjectsPage() {
414
786
  </span>
415
787
  <Avatar className="h-5 w-5 shrink-0">
416
788
  <AvatarImage
417
- src={getPersonAvatarUrl(project.clientAvatarId)}
789
+ src={
790
+ project.clientUserPhotoId
791
+ ? getUserPhotoUrl(project.clientUserPhotoId)
792
+ : getPersonAvatarUrl(project.clientAvatarId)
793
+ }
418
794
  alt={project.clientName ?? ''}
419
795
  />
420
796
  <AvatarFallback className="text-[9px] font-medium">
@@ -619,7 +995,11 @@ export default function OperationsProjectsPage() {
619
995
  <div className="flex min-w-0 items-center gap-1.5">
620
996
  <Avatar className="h-6 w-6 shrink-0">
621
997
  <AvatarImage
622
- src={getPersonAvatarUrl(project.clientAvatarId)}
998
+ src={
999
+ project.clientUserPhotoId
1000
+ ? getUserPhotoUrl(project.clientUserPhotoId)
1001
+ : getPersonAvatarUrl(project.clientAvatarId)
1002
+ }
623
1003
  alt={project.clientName ?? ''}
624
1004
  />
625
1005
  <AvatarFallback className="text-[9px] font-medium">
@@ -769,17 +1149,19 @@ export default function OperationsProjectsPage() {
769
1149
  />
770
1150
  )}
771
1151
 
772
- <PaginationFooter
773
- currentPage={projectsResponse?.page ?? page}
774
- pageSize={projectsResponse?.pageSize ?? pageSize}
775
- totalItems={projectsResponse?.total ?? 0}
776
- onPageChange={setPage}
777
- onPageSizeChange={(value) => {
778
- setPageSize(value);
779
- setPage(1);
780
- }}
781
- pageSizeOptions={[12, 24, 48]}
782
- />
1152
+ {activeViewMode !== 'board' ? (
1153
+ <PaginationFooter
1154
+ currentPage={projectsResponse?.page ?? page}
1155
+ pageSize={projectsResponse?.pageSize ?? pageSize}
1156
+ totalItems={projectsResponse?.total ?? 0}
1157
+ onPageChange={setPage}
1158
+ onPageSizeChange={(value) => {
1159
+ setPageSize(value);
1160
+ setPage(1);
1161
+ }}
1162
+ pageSizeOptions={[12, 24, 48]}
1163
+ />
1164
+ ) : null}
783
1165
 
784
1166
  <Sheet
785
1167
  open={isSheetOpen}
@@ -52,6 +52,7 @@ import { OperationsHeader } from '../_components/operations-header';
52
52
  import { StatusBadge } from '../_components/status-badge';
53
53
  import { fetchOperations, mutateOperations } from '../_lib/api';
54
54
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
55
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
55
56
  import type {
56
57
  OperationsScheduleAdjustmentDay,
57
58
  OperationsScheduleAdjustmentRequest,
@@ -237,7 +238,11 @@ export default function OperationsScheduleAdjustmentsPage() {
237
238
  const [search, setSearch] = useState('');
238
239
  const [statusFilter, setStatusFilter] = useState('all');
239
240
  const [page, setPage] = useState(1);
240
- const [pageSize, setPageSize] = useState(12);
241
+ const [pageSize, setPageSize] = usePersistedPageSize({
242
+ storageKey: 'pagination:operations-schedule-adjustments:pageSize',
243
+ defaultValue: 12,
244
+ allowedValues: [12, 24, 48],
245
+ });
241
246
  const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
242
247
  if (typeof window === 'undefined') return 'cards';
243
248
  const saved = window.localStorage.getItem(